/**
* Represents a single Tab in a {@link Ext.tab.Panel TabPanel}. A Tab is simply a slightly
* customized {@link Ext.button.Button Button}, styled to look like a tab. Tabs are optionally
* closable, and can also be disabled. 99% of the time you will not need to create Tabs manually
* as the framework does so automatically when you use a {@link Ext.tab.Panel TabPanel}
*/
Ext.define('Ext.tab.Tab', {
extend: 'Ext.button.Button',
alias: 'widget.tab',
/**
* @property {Boolean} isTab
* `true` in this class to identify an object as an instantiated Tab, or subclass thereof.
*/
isTab: true,
baseCls: Ext.baseCSSPrefix + 'tab',
closeElOverCls: Ext.baseCSSPrefix + 'tab-close-btn-over',
closeElPressedCls: Ext.baseCSSPrefix + 'tab-close-btn-pressed',
config: {
/**
* @cfg {'default'/0/1/2} rotation
* The rotation of the tab. Can be one of the following values:
*
* - `null` - use the default rotation, depending on the dock position of the tabbar
* - `0` - no rotation
* - `1` - rotate 90deg clockwise
* - `2` - rotate 90deg counter-clockwise
*
* The default behavior of this config depends on the dock position of the tabbar:
*
* - `'top'` or `'bottom'` - `0`
* - `'right'` - `1`
* - `'left'` - `2`
*/
rotation: 'default',
/**
* @cfg {'top'/'right'/'bottom'/'left'} tabPosition
* The tab's position. Users should not typically need to set this, as it is
* configured automatically by the tab bar
*/
tabPosition: 'top'
},
/**
* @cfg {Boolean} closable
* True to make the Tab start closable (the close icon will be visible).
*/
closable: true,
// The wording is chosen to be less confusing to blind users.
/**
* @cfg {String} [closeText="removable"]
* The accessible text label for the close button link to be announced by screen readers
* when the tab is focused. This text does not appear visually and is only used when
* {@link #cfg-closable} is `true`.
* @locale
*/
closeText: 'removable',
/**
* @property {Boolean} active
* Indicates that this tab is currently active. This is NOT a public configuration.
* @readonly
*/
active: false,
/**
* @property {Boolean} closable
* True if the tab is currently closable
*/
childEls: [
'closeEl'
],
scale: false,
/**
* @event activate
* Fired when the tab is activated.
* @param {Ext.tab.Tab} this
*/
/**
* @event deactivate
* Fired when the tab is deactivated.
* @param {Ext.tab.Tab} this
*/
/**
* @event beforeclose
* Fires if the user clicks on the Tab's close button, but before the {@link #close}
* event is fired. Return false from any listener to stop the close event being fired
* @param {Ext.tab.Tab} tab The Tab object
*/
/**
* @event close
* Fires to indicate that the tab is to be closed, usually because the user has clicked
* the close button.
* @param {Ext.tab.Tab} tab The Tab object
*/
ariaRole: 'tab',
tabIndex: -1,
keyMap: {
scope: 'this',
DELETE: 'onDeleteKey'
},
_btnWrapCls: Ext.baseCSSPrefix + 'tab-wrap',
_btnCls: Ext.baseCSSPrefix + 'tab-button',
_baseIconCls: Ext.baseCSSPrefix + 'tab-icon-el',
_glyphCls: Ext.baseCSSPrefix + 'tab-glyph',
_innerCls: Ext.baseCSSPrefix + 'tab-inner',
_textCls: Ext.baseCSSPrefix + 'tab-text',
_noTextCls: Ext.baseCSSPrefix + 'tab-no-text',
_hasIconCls: Ext.baseCSSPrefix + 'tab-icon',
_activeCls: Ext.baseCSSPrefix + 'tab-active',
_closableCls: Ext.baseCSSPrefix + 'tab-closable',
overCls: Ext.baseCSSPrefix + 'tab-over',
_pressedCls: Ext.baseCSSPrefix + 'tab-pressed',
_disabledCls: Ext.baseCSSPrefix + 'tab-disabled',
_rotateClasses: {
1: Ext.baseCSSPrefix + 'tab-rotate-right',
2: Ext.baseCSSPrefix + 'tab-rotate-left'
},
// a mapping of the "ui" positions. When "rotation" is anything other than 0, a ui
// position other than the docked side must be used.
_positions: {
top: {
'default': 'top',
0: 'top',
1: 'left',
2: 'right'
},
right: {
'default': 'top',
0: 'right',
1: 'top',
2: 'bottom'
},
bottom: {
'default': 'bottom',
0: 'bottom',
1: 'right',
2: 'left'
},
left: {
'default': 'top',
0: 'left',
1: 'bottom',
2: 'top'
}
},
_defaultRotations: {
top: 0,
right: 1,
bottom: 0,
left: 2
},
initComponent: function() {
var me = this;
// Although WAI-ARIA spec has a provision for deleting tab panels,
// according to accessibility experts at University of Washington
// closable tab panels can be very confusing to vision impaired users.
// On top of that there are some technical issues with screen readers
// not recognizing the changed number of open tabs, so it is better
// to avoid closable tabs in accessible applications.
//<debug>
if (me.closable) {
Ext.ariaWarn(
me,
"Closable tabs can be confusing to users relying on Assistive Technologies " +
"such as Screen Readers, and are not recommended in accessible applications. " +
"Please consider setting " + me.title + " tab (" + me.id + ") to closable: false."
);
}
//</debug>
if (me.card) {
me.setCard(me.card);
}
me.callParent(arguments);
},
getActualRotation: function() {
var rotation = this.getRotation();
return (rotation !== 'default') ? rotation : this._defaultRotations[this.getTabPosition()];
},
updateRotation: function() {
this.syncRotationAndPosition();
},
updateTabPosition: function() {
this.syncRotationAndPosition();
},
syncRotationAndPosition: function() {
var me = this,
rotateClasses = me._rotateClasses,
position = me.getTabPosition(),
rotation = me.getActualRotation(),
oldRotateCls = me._rotateCls,
rotateCls = me._rotateCls = rotateClasses[rotation],
oldPositionCls = me._positionCls,
positionCls = me._positionCls = me._positions[position][rotation];
if (oldRotateCls !== rotateCls) {
if (oldRotateCls) {
me.removeCls(oldRotateCls);
}
if (rotateCls) {
me.addCls(rotateCls);
}
}
if (oldPositionCls !== positionCls) {
if (oldPositionCls) {
me.removeClsWithUI(oldPositionCls);
}
if (positionCls) {
me.addClsWithUI(positionCls);
}
if (me.rendered) {
me.updateFrame();
}
}
if (me.rendered) {
me.setElOrientation();
}
},
onAdded: function(container, pos, instanced) {
this.callParent([container, pos, instanced]);
this.syncRotationAndPosition();
},
getTemplateArgs: function() {
var me = this,
result = me.callParent();
result.closable = me.closable;
result.closeText = me.closeText;
return result;
},
beforeRender: function() {
var me = this,
tabBar = me.up('tabbar'),
tabPanel = me.up('tabpanel');
me.callParent();
me.ariaRenderAttributes = me.ariaRenderAttributes || {};
if (me.active) {
me.ariaRenderAttributes['aria-selected'] = true;
me.addCls(me._activeCls);
}
else {
me.ariaRenderAttributes['aria-selected'] = false;
}
me.syncClosableCls();
// Propagate minTabWidth and maxTabWidth settings from the owning TabBar then TabPanel
if (!me.minWidth) {
me.minWidth = (tabBar) ? tabBar.minTabWidth : me.minWidth;
if (!me.minWidth && tabPanel) {
me.minWidth = tabPanel.minTabWidth;
}
if (me.minWidth && me.iconCls) {
me.minWidth += 25;
}
}
if (!me.maxWidth) {
me.maxWidth = (tabBar) ? tabBar.maxTabWidth : me.maxWidth;
if (!me.maxWidth && tabPanel) {
me.maxWidth = tabPanel.maxTabWidth;
}
}
},
onRender: function() {
var me = this;
me.setElOrientation();
me.callParent(arguments);
if (me.closable) {
me.closeEl.addClsOnOver(me.closeElOverCls);
me.closeEl.addClsOnClick(me.closeElPressedCls);
}
},
setElOrientation: function() {
var me = this,
rotation = me.getActualRotation(),
el = me.el;
if (rotation) {
el.setVertical(rotation === 1 ? 90 : 270);
}
else {
el.setHorizontal();
}
},
enable: function(silent) {
var me = this;
me.callParent(arguments);
me.removeCls(me._disabledCls);
return me;
},
disable: function(silent) {
var me = this;
me.callParent(arguments);
me.addCls(me._disabledCls);
return me;
},
/**
* Sets the tab as either closable or not.
* @param {Boolean} closable Pass false to make the tab not closable. Otherwise the tab
* will be made closable (eg a close button will appear on the tab)
*/
setClosable: function(closable) {
var me = this;
// Closable must be true if no args
closable = (!arguments.length || !!closable);
if (me.closable !== closable) {
me.closable = closable;
// set property on the user-facing item ('card'):
if (me.card) {
me.card.closable = closable;
}
me.syncClosableCls();
if (me.rendered) {
me.syncClosableElements();
// Tab will change width to accommodate close icon
me.updateLayout();
}
}
},
/**
* This method ensures that the closeBtn element exists or not based on 'closable'.
* @private
*/
syncClosableElements: function() {
var me = this,
closeEl = me.closeEl;
if (me.closable) {
if (!closeEl) {
closeEl = me.closeEl = me.btnWrap.insertSibling({
tag: 'span',
id: me.id + '-closeEl',
cls: me.baseCls + '-close-btn',
html: me.closeText
}, 'after');
}
closeEl.addClsOnOver(me.closeElOverCls);
closeEl.addClsOnClick(me.closeElPressedCls);
}
else if (closeEl) {
closeEl.destroy();
delete me.closeEl;
}
},
/**
* This method ensures that the closable cls are added or removed based on 'closable'.
* @private
*/
syncClosableCls: function() {
var me = this,
closableCls = me._closableCls;
if (me.closable) {
me.addCls(closableCls);
}
else {
me.removeCls(closableCls);
}
},
/**
* Sets this tab's attached card. Usually this is handled automatically by the
* {@link Ext.tab.Panel} that this Tab belongs to and would not need to be done by the developer
* @param {Ext.Component} card The card to set
*/
setCard: function(card) {
var me = this;
me.card = card;
if (card.iconAlign) {
me.setIconAlign(card.iconAlign);
}
if (card.textAlign) {
me.setTextAlign(card.textAlign);
}
me.setText(me.title || card.title);
me.setIconCls(me.iconCls || card.iconCls);
me.setIcon(me.icon || card.icon);
me.setGlyph(me.glyph || card.glyph);
},
/**
* @private
* Listener attached to click events on the Tab's close button
*/
onCloseClick: function() {
var me = this;
if (me.fireEvent('beforeclose', me) !== false) {
if (me.tabBar) {
if (me.tabBar.closeTab(me) === false) {
// beforeclose on the panel vetoed the event, stop here
return;
}
}
else {
// if there's no tabbar, fire the close event
me.fireClose();
}
}
},
/**
* Fires the close event on the tab.
* @private
*/
fireClose: function() {
this.fireEvent('close', this);
},
/**
* @private
*/
onEnterKey: function(e) {
var me = this;
if (me.tabBar) {
me.tabBar.onClick(e, me.el);
e.stopEvent();
return false;
}
},
/**
* @private
*/
onDeleteKey: function(e) {
if (this.closable) {
this.onCloseClick();
e.stopEvent();
return false;
}
},
/**
* @private
*/
beforeClick: function(isCloseClick) {
if (!isCloseClick) {
this.focus();
}
},
/**
* @private
*/
activate: function(supressEvent) {
var me = this,
card = me.card,
ariaDom = me.ariaEl.dom;
me.active = true;
me.addCls(me._activeCls);
if (ariaDom) {
ariaDom.setAttribute('aria-selected', true);
}
else {
me.ariaRenderAttributes = me.ariaRenderAttributes || {};
me.ariaRenderAttributes['aria-selected'] = true;
}
if (card) {
if (card.ariaEl.dom) {
card.ariaEl.dom.setAttribute('aria-expanded', true);
}
else {
card.ariaRenderAttributes = card.ariaRenderAttributes || {};
card.ariaRenderAttributes['aria-expanded'] = true;
}
}
if (supressEvent !== true) {
me.fireEvent('activate', me);
}
},
/**
* @private
*/
deactivate: function(supressEvent) {
var me = this,
card = me.card,
ariaDom = me.ariaEl.dom;
me.active = false;
me.removeCls(me._activeCls);
if (ariaDom) {
ariaDom.setAttribute('aria-selected', false);
}
else {
me.ariaRenderAttributes = me.ariaRenderAttributes || {};
me.ariaRenderAttributes['aria-selected'] = false;
}
if (card) {
if (card.ariaEl.dom) {
card.ariaEl.dom.setAttribute('aria-expanded', false);
}
else {
card.ariaRenderAttributes = card.ariaRenderAttributes || {};
card.ariaRenderAttributes['aria-expanded'] = false;
}
}
if (supressEvent !== true) {
me.fireEvent('deactivate', me);
}
},
privates: {
getFramingInfoCls: function() {
return this.baseCls + '-' + this.ui + '-' + this._positionCls;
},
wrapPrimaryEl: function(dom) {
// Tabs don't need the hacks in Ext.dom.ButtonElement
Ext.Button.superclass.wrapPrimaryEl.call(this, dom);
}
}
});