/** * TabBar is used internally by a {@link Ext.tab.Panel TabPanel} and typically should not * need to be created manually. */Ext.define('Ext.tab.Bar', { extend: 'Ext.panel.Bar', xtype: 'tabbar', requires: [ 'Ext.tab.Tab', 'Ext.util.Point', 'Ext.layout.component.Body' ], baseCls: Ext.baseCSSPrefix + 'tab-bar', componentLayout: 'body', /** * @property {Boolean} isTabBar * `true` in this class to identify an object as an instantiated Tab Bar, or subclass thereof. */ isTabBar: true, config: { /** * @cfg {'default'/0/1/2} tabRotation * The rotation of the tabs. Can be one of the following values: * * - `default` - use the default rotation, depending on the dock position (see below) * - `0` - no rotation * - `1` - rotate 90deg clockwise * - `2` - rotate 90deg counter-clockwise * * The default behavior of this config depends on the dock position: * * - `'top'` or `'bottom'` - `0` * - `'right'` - `1` * - `'left'` - `2` */ tabRotation: 'default', /** * @cfg {Boolean} tabStretchMax * `true` to stretch all tabs to the height of the tallest tab when the tabBar * is docked horizontally, or the width of the widest tab when the tabBar is * docked vertically. */ tabStretchMax: true, // NB: This option is named this way for the intent, but in fact activation // happens in arrow key handler, not in focus handler. In IE focus events are // asynchronous, so activation happens before the tab's focus handler is fired. /** * @cfg {Boolean} [activateOnFocus=true] * `true` to follow WAI-ARIA requirement and activate tab when it is navigated to * with arrow keys, or `false` to disable that behavior. When activation on focus * is disabled, users will have to use arrow keys to focus a tab, and then press * Space key to activate it. */ activateOnFocus: true }, /** * @private */ defaultType: 'tab', /** * @cfg {Boolean} plain * True to not show the full background on the tabbar */ plain: false, /** * @cfg {Boolean} ensureActiveVisibleOnChange * `true` to ensure the active tab is scrolled into view when the tab changes, the text, the * icon or the glyph. This is only applicable if using an overflow scroller. * * @since 5.1.1 */ ensureActiveVisibleOnChange: true, ariaRole: 'tablist', focusableContainer: true, childEls: [ 'body', 'strip' ], _stripCls: Ext.baseCSSPrefix + 'tab-bar-strip', _baseBodyCls: Ext.baseCSSPrefix + 'tab-bar-body', /* eslint-disable max-len */ renderTpl: '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'before\'); %}</tpl>' + '<div id="{id}-body" data-ref="body" role="presentation" class="{baseBodyCls} {baseBodyCls}-{ui} ' + '{bodyCls} {bodyTargetCls}{childElCls}"<tpl if="bodyStyle"> style="{bodyStyle}"</tpl>>' + '{%this.renderContainer(out,values)%}' + '</div>' + '<tpl if="hasTabGuard">{% this.renderTabGuard(out, values, \'after\'); %}</tpl>' + '<div id="{id}-strip" data-ref="strip" role="presentation" class="{stripCls} {stripCls}-{ui}{childElCls}"></div>', /* eslint-enable max-len */ /** * @cfg {Number} minTabWidth * The minimum width for a tab in this tab Bar. Defaults to the tab Panel's * {@link Ext.tab.Panel#minTabWidth minTabWidth} value. * @deprecated 6.5.0 This config is deprecated. Please use the * {@link Ext.tab.Panel#minTabWidth minTabWidth} config on the TabPanel. */ /** * @cfg {Number} maxTabWidth * The maximum width for a tab in this tab Bar. Defaults to the tab Panel's * {@link Ext.tab.Panel#maxTabWidth maxTabWidth} value. * @deprecated 6.5.0 This config is deprecated. Please use the * {@link Ext.tab.Panel#maxTabWidth maxTabWidth} config on the TabPanel. */ _reverseDockNames: { left: 'right', right: 'left' }, _layoutAlign: { top: 'end', right: 'begin', bottom: 'begin', left: 'end' }, /** * @event change * Fired when the currently-active tab has changed * @param {Ext.tab.Bar} tabBar The TabBar * @param {Ext.tab.Tab} tab The new Tab * @param {Ext.Component} card The card that was just shown in the TabPanel */ /** * @private */ initComponent: function() { var me = this, initialLayout = me.initialConfig.layout, initialAlign = initialLayout && initialLayout.align, initialOverflowHandler = initialLayout && initialLayout.overflowHandler; if (me.plain) { me.addCls(me.baseCls + '-plain'); } // Will be applied to Ext.panel.Bar's layout config in parent initComponent me.layout = Ext.apply({ align: initialAlign || (me.getTabStretchMax() ? 'stretchmax' : me._layoutAlign[me.dock]), overflowHandler: initialOverflowHandler || 'scroller' }, me.layout); me.callParent(); me.on({ click: me.onClick, element: 'el', scope: me }); }, /** * Ensure the passed tab is visible if using overflow scrolling * @param {Ext.tab.Tab/Ext.Component/Number} [tab] The tab, item in the owning * {@link Ext.tab.Panel} or the index of the item to scroll to. Defaults to the active tab. */ ensureTabVisible: function(tab) { var me = this, tabPanel = me.tabPanel, overflowHandler = me.layout.overflowHandler; if (me.rendered && overflowHandler && me.tooNarrow && overflowHandler.scrollToItem) { if (tab || tab === 0) { if (!tab.isTab) { if (Ext.isNumber(tab)) { tab = this.items.getAt(tab); } else if (tab.isComponent && tabPanel && tabPanel.items.contains(tab)) { tab = tab.tab; } } } if (!tab) { tab = me.activeTab; } if (tab) { overflowHandler.scrollToItem(tab); } } }, initRenderData: function() { var me = this; return Ext.apply(me.callParent(), { bodyCls: me.bodyCls, baseBodyCls: me._baseBodyCls, bodyTargetCls: me.bodyTargetCls, stripCls: me._stripCls, dock: me.dock }); }, setDock: function(dock) { var me = this, items = me.items, ownerCt = me.ownerCt, item, i, ln; items = items && items.items; if (items) { for (i = 0, ln = items.length; i < ln; i++) { item = items[i]; if (item.isTab) { item.setTabPosition(dock); } } } if (me.rendered) { // TODO: remove resetItemMargins once EXTJS-13359 is fixed me.resetItemMargins(); if (ownerCt && ownerCt.isHeader) { ownerCt.resetItemMargins(); } me.needsScroll = true; } me.callParent([dock]); }, updateTabRotation: function(tabRotation) { var me = this, items = me.items, i, ln, item; items = items && items.items; if (items) { for (i = 0, ln = items.length; i < ln; i++) { item = items[i]; if (item.isTab) { item.setRotation(tabRotation); } } } if (me.rendered) { // TODO: remove resetItemMargins once EXTJS-13359 is fixed me.resetItemMargins(); me.needsScroll = true; me.updateLayout(); } }, onRender: function() { var me = this, overflowHandler = this.layout.overflowHandler; me.callParent(); if (Ext.isIE8 && me.vertical) { me.el.on({ mousemove: me.onMouseMove, scope: me }); } if (overflowHandler && overflowHandler.type === 'menu') { overflowHandler.menu.on('click', 'onOverflowMenuItemClick', me); } }, afterLayout: function() { this.adjustTabPositions(); this.callParent(arguments); }, onAdd: function(tab, pos) { var fn = this.onTabContentChange; if (this.ensureActiveVisibleOnChange) { tab.barListeners = tab.on({ scope: this, destroyable: true, glyphchange: fn, iconchange: fn, textchange: fn }); } this.callParent([tab, pos]); }, onAdded: function(container, pos, instanced) { if (container.isHeader) { this.addCls(container.baseCls + '-' + container.ui + '-tab-bar'); } this.callParent([container, pos, instanced]); }, onRemove: function(tab, destroying) { var me = this; // If we're not destroying, no need to do this here since they will // be cleaned up if (me.ensureActiveVisibleOnChange) { if (!destroying) { tab.barListeners.destroy(); } tab.barListeners = null; } if (tab === me.previousTab) { me.previousTab = null; } me.callParent([tab, destroying]); }, onRemoved: function(destroying) { var ownerCt = this.ownerCt; if (ownerCt.isHeader) { this.removeCls(ownerCt.baseCls + '-' + ownerCt.ui + '-tab-bar'); } this.callParent([destroying]); }, onTabContentChange: function(tab) { if (tab === this.activeTab) { this.ensureTabVisible(tab); } }, afterComponentLayout: function(width) { var me = this, needsScroll = me.needsScroll, overflowHandler = me.layout.overflowHandler; me.callParent(arguments); if (overflowHandler && needsScroll && me.tooNarrow && overflowHandler.scrollToItem) { overflowHandler.scrollToItem(me.activeTab); } delete me.needsScroll; }, /** * @private */ onMouseMove: function(e) { var me = this, overTab = me._overTab, tabInfo, tab; if (e.getTarget('.' + Ext.baseCSSPrefix + 'box-scroller')) { return; } tabInfo = me.getTabInfoFromPoint(e.getXY()); tab = tabInfo.tab; if (tab !== overTab) { if (overTab && overTab.rendered) { overTab.onMouseLeave(e); me._overTab = null; } if (tab) { tab.onMouseEnter(e); me._overTab = tab; if (!tab.disabled) { me.el.setStyle('cursor', 'pointer'); } } else { me.el.setStyle('cursor', 'default'); } } }, onMouseLeave: function(e) { var overTab = this._overTab; if (overTab && overTab.rendered) { overTab.onMouseLeave(e); } }, /** * @private * in IE8 and IE9 the clickable region of a rotated element is not its new rotated * position, but it's original unrotated position. The result is that rotated tabs do * not capture click and mousenter/mosueleave events correctly. This method accepts * an xy position and calculates if the coordinates are within a tab and if they * are within the tab's close icon (if any) */ getTabInfoFromPoint: function(xy) { var me = this, tabs = me.items.items, length = tabs.length, innerCt = me.layout.innerCt, innerCtXY = innerCt.getXY(), point = new Ext.util.Point(xy[0], xy[1]), i = 0, lastBox, tabRegion, closeEl, close, closeXY, closeX, closeY, closeWidth, closeHeight, tabX, tabY, tabWidth, tabHeight, closeRegion, isTabReversed, direction, tab; for (; i < length; i++) { tab = tabs[i]; lastBox = tab.lastBox; if (!lastBox || !tab.isTab) { // avoid looping hidden or not laid out, or if the item // is not a tab continue; } tabX = innerCtXY[0] + lastBox.x; tabY = innerCtXY[1] - innerCt.dom.scrollTop + lastBox.y; tabWidth = lastBox.width; tabHeight = lastBox.height; tabRegion = new Ext.util.Region( tabY, tabX + tabWidth, tabY + tabHeight, tabX ); if (tabRegion.contains(point)) { closeEl = tab.closeEl; if (closeEl) { // Read the dom to determine if the contents of the tab are reversed // (rotated 180 degrees). If so, we can cache the result becuase // it's safe to assume all tabs in the tabbar will be the same if (me._isTabReversed === undefined) { me._isTabReversed = isTabReversed = // use currentStyle because getComputedStyle won't get the // filter property in IE9 (tab.btnWrap.dom.currentStyle.filter.indexOf('rotation=2') !== -1); } direction = isTabReversed ? this._reverseDockNames[me.dock] : me.dock; closeWidth = closeEl.getWidth(); closeHeight = closeEl.getHeight(); closeXY = me.getCloseXY( closeEl, tabX, tabY, tabWidth, tabHeight, closeWidth, closeHeight, direction ); closeX = closeXY[0]; closeY = closeXY[1]; closeRegion = new Ext.util.Region( closeY, closeX + closeWidth, closeY + closeHeight, closeX ); close = closeRegion.contains(point); } break; } } return { tab: tab, close: close }; }, /** * @private */ getCloseXY: function(closeEl, tabX, tabY, tabWidth, tabHeight, closeWidth, closeHeight, direction) { // eslint-disable-line max-len var closeXY = closeEl.getXY(), closeX, closeY; if (direction === 'right') { closeX = tabX + tabWidth - ((closeXY[1] - tabY) + closeHeight); closeY = tabY + (closeXY[0] - tabX); } else { closeX = tabX + (closeXY[1] - tabY); closeY = tabY + tabX + tabHeight - closeXY[0] - closeWidth; } return [closeX, closeY]; }, /** * @private * Closes the given tab by removing it from the TabBar and removing the corresponding card * from the TabPanel * @param {Ext.tab.Tab} toClose The tab to close */ closeTab: function(toClose) { var me = this, card = toClose.card, tabPanel = me.tabPanel, toActivate; if (card && card.fireEvent('beforeclose', card) === false) { return false; } // If we are closing the active tab, revert to the previously active tab (or the previous // or next enabled sibling if there *is* no previously active tab, or the previously // active tab is the one that's being closed or the previously active tab has since // been disabled) toActivate = me.findNextActivatable(toClose); // We are going to remove the associated card, and then, if that was sucessful, // remove the Tab, and then potentially activate another Tab. // We should not layout for each of these operations. Ext.suspendLayouts(); // If we are closing the active tab, revert to the previously active tab // (or the previous sibling or the next sibling) if (toActivate) { // Our owning TabPanel calls our setActiveTab method, so only call that // if this Bar is being used // in some other context (unlikely) if (tabPanel) { tabPanel.setActiveTab(toActivate.card); } else { me.setActiveTab(toActivate); } toActivate.focus(); } if (tabPanel && card) { // Remove the ownerCt so the tab doesn't get destroyed if the remove is successful // We need this so we can have the tab fire it's own close event. delete toClose.ownerCt; // we must fire 'close' before removing the card from panel, otherwise // the event will no loger have any listener card.fireEvent('close', card); tabPanel.remove(card); // Remove succeeded if (card.ownerCt !== tabPanel) { /* * Force the close event to fire. By the time this function returns, * the tab is already destroyed and all listeners have been purged * so the tab can't fire itself. */ toClose.fireClose(); me.remove(toClose); } else { // Restore the ownerCt from above toClose.ownerCt = me; Ext.resumeLayouts(true); return false; } } Ext.resumeLayouts(true); }, /** * @private * Determines the next tab to activate when one tab is closed. */ findNextActivatable: function(toClose) { var me = this, previousTab = me.previousTab, nextTab; if (toClose.active && me.items.getCount() > 1) { if (previousTab && previousTab !== toClose && !previousTab.disabled) { nextTab = previousTab; } else { nextTab = toClose.next('tab[disabled=false]') || toClose.prev('tab[disabled=false]'); } } // If we couldn't find the next tab to activate, fall back // to the currently active one. We need to have a focused tab // at all times. return nextTab || me.activeTab; }, /** * @private * Marks the given tab as active * @param {Ext.tab.Tab} tab The tab to mark active * @param {Boolean} initial True if we're setting the tab during setup */ setActiveTab: function(tab, initial) { var me = this; if (!tab.disabled && tab !== me.activeTab) { // Deactivate the previous tab, and ensure this FocusableContainer knows about it if (me.activeTab) { if (me.activeTab.destroyed) { me.previousTab = null; } else { me.previousTab = me.activeTab; me.activeTab.deactivate(); me.deactivateFocusable(me.activeTab); } } // Activate the new tab, and ensure this FocusableContainer knows about it tab.activate(); me.activateFocusable(tab); me.activeTab = tab; me.needsScroll = true; // We don't fire the change event when setting the first tab. // Also no need to run a layout if (!initial) { me.fireEvent('change', me, tab, tab.card); // Ensure that after the currently in progress layout, // the active tab is scrolled into view me.updateLayout(); } } }, privates: { adjustTabPositions: function() { var me = this, items = me.items.items, i = items.length, tab, lastBox, el, rotation, prop; // When tabs are rotated vertically we don't have a reliable way to position // them using CSS in modern browsers. This is because of the way transform-orign // works - it requires the width to be known, and the width is not known in css. // Consequently we have to make an adjustment to the tab's position in these browsers. // This is similar to what we do in Ext.panel.Header#adjustTitlePosition if (!Ext.isIE8) { // 'left' in normal mode, 'right' in rtl prop = me._getTabAdjustProp(); while (i--) { tab = items[i]; el = tab.el; lastBox = tab.lastBox; rotation = tab.isTab ? tab.getActualRotation() : 0; if (rotation === 1 && tab.isVisible()) { // rotated 90 degrees using the top left corner as the axis. // tabs need to be shifted to the right by their width el.setStyle(prop, (lastBox.x + lastBox.width) + 'px'); } else if (rotation === 2 && tab.isVisible()) { // rotated 270 degrees using the bottom right corner as the axis. // tabs need to be shifted to the left by their height el.setStyle(prop, (lastBox.x - lastBox.height) + 'px'); } } } }, applyTargetCls: function(targetCls) { this.bodyTargetCls = targetCls; }, // rtl hook _getTabAdjustProp: function() { return 'left'; }, getTargetEl: function() { return this.body || this.frameBody || this.el; }, onClick: function(e, target) { var me = this, tabEl, tab, isCloseClick, tabInfo; if (e.getTarget('.' + Ext.baseCSSPrefix + 'box-scroller')) { return; } if (Ext.isIE8 && me.vertical) { tabInfo = me.getTabInfoFromPoint(e.getXY()); tab = tabInfo.tab; isCloseClick = tabInfo.close; } else { // The target might not be a valid tab el. tabEl = e.getTarget('.' + Ext.tab.Tab.prototype.baseCls); tab = tabEl && Ext.getCmp(tabEl.id); isCloseClick = tab && tab.closeEl && (target === tab.closeEl.dom); } if (isCloseClick) { e.preventDefault(); } if (tab && tab.isDisabled && !tab.isDisabled()) { // This will focus the tab; we do it before activating the card // because the card may attempt to focus itself or a child item. // We need to focus the tab explicitly because click target is // the Bar, not the Tab. tab.beforeClick(isCloseClick); if (tab.closable && isCloseClick) { tab.onCloseClick(); } else { me.doActivateTab(tab); } } }, onOverflowMenuItemClick: function(menu, item, e, eOpts) { var tab = item && item.masterComponent, overflowHandler = this.layout.overflowHandler; if (tab && !tab.isDisabled()) { this.doActivateTab(tab); // set focus to menuTrigger so that it doesn't revert to previous activeTab if (overflowHandler.menuTrigger) { overflowHandler.menuTrigger.focus(); } } }, doActivateTab: function(tab) { var tabPanel = this.tabPanel; if (tabPanel) { // TabPanel will call setActiveTab of the TabBar if (!tab.disabled) { tabPanel.setActiveTab(tab.card); } } else { this.setActiveTab(tab); } }, onFocusableContainerFocus: function(e) { var me = this, mixin = me.mixins.focusablecontainer, child; child = mixin.onFocusableContainerFocus.call(me, e); if (child && child.isTab) { me.doActivateTab(child); } }, onFocusableContainerFocusEnter: function(e) { var me = this, mixin = me.mixins.focusablecontainer, child; child = mixin.onFocusableContainerFocusEnter.call(me, e); if (child && child.isTab) { me.doActivateTab(child); } }, focusChild: function(child, forward) { var me = this, mixin = me.mixins.focusablecontainer, nextChild; nextChild = mixin.focusChild.call(me, child, forward); if (me.activateOnFocus && nextChild && nextChild.isTab) { me.doActivateTab(nextChild); } } }});