X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/ee06f37b0f6f6d94cd05a6ffae556660f7c4a2bc..c930e9176a5a85509c5b0230e2bff5c22a591432:/src/widgets/TabPanel.js diff --git a/src/widgets/TabPanel.js b/src/widgets/TabPanel.js new file mode 100644 index 00000000..ccd3fdcb --- /dev/null +++ b/src/widgets/TabPanel.js @@ -0,0 +1,1100 @@ +/*! + * Ext JS Library 3.0.0 + * Copyright(c) 2006-2009 Ext JS, LLC + * licensing@extjs.com + * http://www.extjs.com/license + */ +/** + * @class Ext.TabPanel + *

A basic tab container. TabPanels can be used exactly like a standard {@link Ext.Panel} + * for layout purposes, but also have special support for containing child Components + * ({@link Ext.Container#items items}) that are managed using a + * {@link Ext.layout.CardLayout CardLayout layout manager}, and displayed as separate tabs.

+ * + * Note: By default, a tab's close tool destroys the child tab Component + * and all its descendants. This makes the child tab Component, and all its descendants unusable. To enable + * re-use of a tab, configure the TabPanel with {@link #autoDestroy autoDestroy: false}. + * + *

TabPanel header/footer elements

+ *

TabPanels use their {@link Ext.Panel#header header} or {@link Ext.Panel#footer footer} element + * (depending on the {@link #tabPosition} configuration) to accommodate the tab selector buttons. + * This means that a TabPanel will not display any configured title, and will not display any + * configured header {@link Ext.Panel#tools tools}.

+ *

To display a header, embed the TabPanel in a {@link Ext.Panel Panel} which uses + * {@link Ext.Container#layout layout:'fit'}.

+ * + *

Tab Events

+ *

There is no actual tab class — each tab is simply a {@link Ext.BoxComponent Component} + * such as a {@link Ext.Panel Panel}. However, when rendered in a TabPanel, each child Component + * can fire additional events that only exist for tabs and are not available from other Components. + * These events are:

+ *
+ *

Creating TabPanels from Code

+ *

TabPanels can be created and rendered completely in code, as in this example:

+ *

+var tabs = new Ext.TabPanel({
+    renderTo: Ext.getBody(),
+    activeTab: 0,
+    items: [{
+        title: 'Tab 1',
+        html: 'A simple tab'
+    },{
+        title: 'Tab 2',
+        html: 'Another one'
+    }]
+});
+
+ *

Creating TabPanels from Existing Markup

+ *

TabPanels can also be rendered from pre-existing markup in a couple of ways.

+ *
+ * + * @extends Ext.Panel + * @constructor + * @param {Object} config The configuration options + * @xtype tabpanel + */ +Ext.TabPanel = Ext.extend(Ext.Panel, { + /** + * @cfg {Boolean} layoutOnTabChange + * Set to true to force a layout of the active tab when the tab is changed. Defaults to false. + * See {@link Ext.layout.CardLayout}.{@link Ext.layout.CardLayout#layoutOnCardChange layoutOnCardChange}. + */ + /** + * @cfg {String} tabCls This config option is used on child Components of ths TabPanel. A CSS + * class name applied to the tab strip item representing the child Component, allowing special + * styling to be applied. + */ + /** + * @cfg {Boolean} monitorResize True to automatically monitor window resize events and rerender the layout on + * browser resize (defaults to true). + */ + monitorResize : true, + /** + * @cfg {Boolean} deferredRender + *

true by default to defer the rendering of child {@link Ext.Container#items items} + * to the browsers DOM until a tab is activated. false will render all contained + * {@link Ext.Container#items items} as soon as the {@link Ext.layout.CardLayout layout} + * is rendered. 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.

+ *

The deferredRender property is internally passed to the layout manager for + * TabPanels ({@link Ext.layout.CardLayout}) as its {@link Ext.layout.CardLayout#deferredRender} + * configuration value.

+ *

Note: leaving deferredRender as true means that the content + * within an unactivated tab will not be available. For example, this means that if the TabPanel + * is within a {@link Ext.form.FormPanel form}, then until a tab is activated, any Fields within + * unactivated tabs will not be rendered, and will therefore not be submitted and will not be + * available to either {@link Ext.form.BasicForm#getValues getValues} or + * {@link Ext.form.BasicForm#setValues setValues}.

+ */ + deferredRender : true, + /** + * @cfg {Number} tabWidth The initial width in pixels of each new tab (defaults to 120). + */ + tabWidth : 120, + /** + * @cfg {Number} minTabWidth The minimum width in pixels for each tab when {@link #resizeTabs} = true (defaults to 30). + */ + minTabWidth : 30, + /** + * @cfg {Boolean} resizeTabs True to automatically resize each tab so that the tabs will completely fill the + * tab strip (defaults to false). Setting this to true may cause specific widths that might be set per tab to + * be overridden in order to fit them all into view (although {@link #minTabWidth} will always be honored). + */ + resizeTabs : false, + /** + * @cfg {Boolean} enableTabScroll True to enable scrolling to tabs that may be invisible due to overflowing the + * overall TabPanel width. Only available with tabPosition:'top' (defaults to false). + */ + enableTabScroll : false, + /** + * @cfg {Number} scrollIncrement The number of pixels to scroll each time a tab scroll button is pressed + * (defaults to 100, or if {@link #resizeTabs} = true, the calculated tab width). Only + * applies when {@link #enableTabScroll} = true. + */ + scrollIncrement : 0, + /** + * @cfg {Number} scrollRepeatInterval Number of milliseconds between each scroll while a tab scroll button is + * continuously pressed (defaults to 400). + */ + scrollRepeatInterval : 400, + /** + * @cfg {Float} scrollDuration The number of milliseconds that each scroll animation should last (defaults + * to .35). Only applies when {@link #animScroll} = true. + */ + scrollDuration : 0.35, + /** + * @cfg {Boolean} animScroll True to animate tab scrolling so that hidden tabs slide smoothly into view (defaults + * to true). Only applies when {@link #enableTabScroll} = true. + */ + animScroll : true, + /** + * @cfg {String} tabPosition The position where the tab strip should be rendered (defaults to 'top'). + * The only other supported value is 'bottom'. Note: tab scrolling is only supported for + * tabPosition: 'top'. + */ + tabPosition : 'top', + /** + * @cfg {String} baseCls The base CSS class applied to the panel (defaults to 'x-tab-panel'). + */ + baseCls : 'x-tab-panel', + /** + * @cfg {Boolean} autoTabs + *

true to query the DOM for any divs with a class of 'x-tab' to be automatically converted + * to tabs and added to this panel (defaults to false). Note that the query will be executed within + * the scope of the container element only (so that multiple tab panels from markup can be supported via this + * method).

+ *

This method is only possible when the markup is structured correctly as a container with nested divs + * containing the class 'x-tab'. To create TabPanels without these limitations, or to pull tab content + * from other elements on the page, see the example at the top of the class for generating tabs from markup.

+ *

There are a couple of things to note when using this method:

Example usage:

+ *

+var tabs = new Ext.TabPanel({
+    applyTo: 'my-tabs',
+    activeTab: 0,
+    deferredRender: false,
+    autoTabs: true
+});
+
+// This markup will be converted to a TabPanel from the code above
+<div id="my-tabs">
+    <div class="x-tab" title="Tab 1">A simple tab</div>
+    <div class="x-tab" title="Tab 2">Another one</div>
+</div>
+
+ */ + autoTabs : false, + /** + * @cfg {String} autoTabSelector The CSS selector used to search for tabs in existing markup when + * {@link #autoTabs} = true (defaults to 'div.x-tab'). This can be any valid selector + * supported by {@link Ext.DomQuery#select}. Note that the query will be executed within the scope of this + * tab panel only (so that multiple tab panels from markup can be supported on a page). + */ + autoTabSelector : 'div.x-tab', + /** + * @cfg {String/Number} activeTab A string id or the numeric index of the tab that should be initially + * activated on render (defaults to none). + */ + activeTab : null, + /** + * @cfg {Number} tabMargin The number of pixels of space to calculate into the sizing and scrolling of + * tabs. If you change the margin in CSS, you will need to update this value so calculations are correct + * with either {@link #resizeTabs} or scrolling tabs. (defaults to 2) + */ + tabMargin : 2, + /** + * @cfg {Boolean} plain true to render the tab strip without a background container image + * (defaults to false). + */ + plain : false, + /** + * @cfg {Number} wheelIncrement For scrolling tabs, the number of pixels to increment on mouse wheel + * scrolling (defaults to 20). + */ + wheelIncrement : 20, + + /* + * This is a protected property used when concatenating tab ids to the TabPanel id for internal uniqueness. + * It does not generally need to be changed, but can be if external code also uses an id scheme that can + * potentially clash with this one. + */ + idDelimiter : '__', + + // private + itemCls : 'x-tab-item', + + // private config overrides + elements : 'body', + headerAsText : false, + frame : false, + hideBorders :true, + + // private + initComponent : function(){ + this.frame = false; + Ext.TabPanel.superclass.initComponent.call(this); + this.addEvents( + /** + * @event beforetabchange + * Fires before the active tab changes. Handlers can return false to cancel the tab change. + * @param {TabPanel} this + * @param {Panel} newTab The tab being activated + * @param {Panel} currentTab The current active tab + */ + 'beforetabchange', + /** + * @event tabchange + * Fires after the active tab has changed. + * @param {TabPanel} this + * @param {Panel} tab The new active tab + */ + 'tabchange', + /** + * @event contextmenu + * Relays the contextmenu event from a tab selector element in the tab strip. + * @param {TabPanel} this + * @param {Panel} tab The target tab + * @param {EventObject} e + */ + 'contextmenu' + ); + /** + * @cfg {Object} layoutConfig + * TabPanel implicitly uses {@link Ext.layout.CardLayout} as its layout manager. + * layoutConfig may be used to configure this layout manager. + * {@link #deferredRender} and {@link #layoutOnTabChange} + * configured on the TabPanel will be applied as configs to the layout manager. + */ + this.setLayout(new Ext.layout.CardLayout(Ext.apply({ + layoutOnCardChange: this.layoutOnTabChange, + deferredRender: this.deferredRender + }, this.layoutConfig))); + + if(this.tabPosition == 'top'){ + this.elements += ',header'; + this.stripTarget = 'header'; + }else { + this.elements += ',footer'; + this.stripTarget = 'footer'; + } + if(!this.stack){ + this.stack = Ext.TabPanel.AccessStack(); + } + this.initItems(); + }, + + // private + onRender : function(ct, position){ + Ext.TabPanel.superclass.onRender.call(this, ct, position); + + if(this.plain){ + var pos = this.tabPosition == 'top' ? 'header' : 'footer'; + this[pos].addClass('x-tab-panel-'+pos+'-plain'); + } + + var st = this[this.stripTarget]; + + this.stripWrap = st.createChild({cls:'x-tab-strip-wrap', cn:{ + tag:'ul', cls:'x-tab-strip x-tab-strip-'+this.tabPosition}}); + + var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null); + this.stripSpacer = st.createChild({cls:'x-tab-strip-spacer'}, beforeEl); + this.strip = new Ext.Element(this.stripWrap.dom.firstChild); + + this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge'}); + this.strip.createChild({cls:'x-clear'}); + + this.body.addClass('x-tab-panel-body-'+this.tabPosition); + + /** + * @cfg {Template/XTemplate} itemTpl

(Optional) A {@link Ext.Template Template} or + * {@link Ext.XTemplate XTemplate} which may be provided to process the data object returned from + * {@link #getTemplateArgs} to produce a clickable selector element in the tab strip.

+ *

The main element created should be a <li> element. In order for a click event on + * a selector element to be connected to its item, it must take its id from the TabPanel's + * native {@link #getTemplateArgs}.

+ *

The child element which contains the title text must be marked by the CSS class + * x-tab-strip-inner.

+ *

To enable closability, the created element should contain an element marked by the CSS class + * x-tab-strip-close.

+ *

If a custom itemTpl is supplied, it is the developer's responsibility to create CSS + * style rules to create the desired appearance.

+ * Below is an example of how to create customized tab selector items:

+new Ext.TabPanel({
+    renderTo: document.body,
+    minTabWidth: 115,
+    tabWidth: 135,
+    enableTabScroll: true,
+    width: 600,
+    height: 250,
+    defaults: {autoScroll:true},
+    itemTpl: new Ext.XTemplate(
+    '<li class="{cls}" id="{id}" style="overflow:hidden">',
+         '<tpl if="closable">',
+            '<a class="x-tab-strip-close" onclick="return false;"></a>',
+         '</tpl>',
+         '<a class="x-tab-right" href="#" onclick="return false;" style="padding-left:6px">',
+            '<em class="x-tab-left">',
+                '<span class="x-tab-strip-inner">',
+                    '<img src="{src}" style="float:left;margin:3px 3px 0 0">',
+                    '<span style="margin-left:20px" class="x-tab-strip-text {iconCls}">{text} {extra}</span>',
+                '</span>',
+            '</em>',
+        '</a>',
+    '</li>'
+    ),
+    getTemplateArgs: function(item) {
+//      Call the native method to collect the base data. Like the ID!
+        var result = Ext.TabPanel.prototype.getTemplateArgs.call(this, item);
+
+//      Add stuff used in our template
+        return Ext.apply(result, {
+            closable: item.closable,
+            src: item.iconSrc,
+            extra: item.extraText || ''
+        });
+    },
+    items: [{
+        title: 'New Tab 1',
+        iconSrc: '../shared/icons/fam/grid.png',
+        html: 'Tab Body 1',
+        closable: true
+    }, {
+        title: 'New Tab 2',
+        iconSrc: '../shared/icons/fam/grid.png',
+        html: 'Tab Body 2',
+        extraText: 'Extra stuff in the tab button'
+    }]
+});
+
+ */ + if(!this.itemTpl){ + var tt = new Ext.Template( + '
  • ', + '', + '{text}', + '
  • ' + ); + tt.disableFormats = true; + tt.compile(); + Ext.TabPanel.prototype.itemTpl = tt; + } + + this.items.each(this.initTab, this); + }, + + // private + afterRender : function(){ + Ext.TabPanel.superclass.afterRender.call(this); + if(this.autoTabs){ + this.readTabs(false); + } + if(this.activeTab !== undefined){ + var item = Ext.isObject(this.activeTab) ? this.activeTab : this.items.get(this.activeTab); + delete this.activeTab; + this.setActiveTab(item); + } + }, + + // private + initEvents : function(){ + Ext.TabPanel.superclass.initEvents.call(this); + this.on('add', this.onAdd, this, {target: this}); + this.on('remove', this.onRemove, this, {target: this}); + + this.mon(this.strip, 'mousedown', this.onStripMouseDown, this); + this.mon(this.strip, 'contextmenu', this.onStripContextMenu, this); + if(this.enableTabScroll){ + this.mon(this.strip, 'mousewheel', this.onWheel, this); + } + }, + + // private + findTargets : function(e){ + var item = null; + var itemEl = e.getTarget('li', this.strip); + if(itemEl){ + item = this.getComponent(itemEl.id.split(this.idDelimiter)[1]); + if(item.disabled){ + return { + close : null, + item : null, + el : null + }; + } + } + return { + close : e.getTarget('.x-tab-strip-close', this.strip), + item : item, + el : itemEl + }; + }, + + // private + onStripMouseDown : function(e){ + if(e.button !== 0){ + return; + } + e.preventDefault(); + var t = this.findTargets(e); + if(t.close){ + if (t.item.fireEvent('beforeclose', t.item) !== false) { + t.item.fireEvent('close', t.item); + this.remove(t.item); + } + return; + } + if(t.item && t.item != this.activeTab){ + this.setActiveTab(t.item); + } + }, + + // private + onStripContextMenu : function(e){ + e.preventDefault(); + var t = this.findTargets(e); + if(t.item){ + this.fireEvent('contextmenu', this, t.item, e); + } + }, + + /** + * True to scan the markup in this tab panel for {@link #autoTabs} using the + * {@link #autoTabSelector} + * @param {Boolean} removeExisting True to remove existing tabs + */ + readTabs : function(removeExisting){ + if(removeExisting === true){ + this.items.each(function(item){ + this.remove(item); + }, this); + } + var tabs = this.el.query(this.autoTabSelector); + for(var i = 0, len = tabs.length; i < len; i++){ + var tab = tabs[i]; + var title = tab.getAttribute('title'); + tab.removeAttribute('title'); + this.add({ + title: title, + contentEl: tab + }); + } + }, + + // private + initTab : function(item, index){ + var before = this.strip.dom.childNodes[index]; + var p = this.getTemplateArgs(item); + var el = before ? + this.itemTpl.insertBefore(before, p) : + this.itemTpl.append(this.strip, p); + + Ext.fly(el).addClassOnOver('x-tab-strip-over'); + + if(item.tabTip){ + Ext.fly(el).child('span.x-tab-strip-text', true).qtip = item.tabTip; + } + item.tabEl = el; + + item.on('disable', this.onItemDisabled, this); + item.on('enable', this.onItemEnabled, this); + item.on('titlechange', this.onItemTitleChanged, this); + item.on('iconchange', this.onItemIconChanged, this); + item.on('beforeshow', this.onBeforeShowItem, this); + }, + + /** + *

    Provides template arguments for rendering a tab selector item in the tab strip.

    + *

    This method returns an object hash containing properties used by the TabPanel's {@link #itemTpl} + * to create a formatted, clickable tab selector element. The properties which must be returned + * are:

    + * @param {BoxComponent} item The {@link Ext.BoxComponent BoxComponent} for which to create a selector element in the tab strip. + * @return {Object} An object hash containing the properties required to render the selector element. + */ + getTemplateArgs : function(item) { + var cls = item.closable ? 'x-tab-strip-closable' : ''; + if(item.disabled){ + cls += ' x-item-disabled'; + } + if(item.iconCls){ + cls += ' x-tab-with-icon'; + } + if(item.tabCls){ + cls += ' ' + item.tabCls; + } + + return { + id: this.id + this.idDelimiter + item.getItemId(), + text: item.title, + cls: cls, + iconCls: item.iconCls || '' + }; + }, + + // private + onAdd : function(tp, item, index){ + this.initTab(item, index); + if(this.items.getCount() == 1){ + this.syncSize(); + } + this.delegateUpdates(); + }, + + // private + onBeforeAdd : function(item){ + var existing = item.events ? (this.items.containsKey(item.getItemId()) ? item : null) : this.items.get(item); + if(existing){ + this.setActiveTab(item); + return false; + } + Ext.TabPanel.superclass.onBeforeAdd.apply(this, arguments); + var es = item.elements; + item.elements = es ? es.replace(',header', '') : es; + item.border = (item.border === true); + }, + + // private + onRemove : function(tp, item){ + Ext.destroy(Ext.get(this.getTabEl(item))); + this.stack.remove(item); + item.un('disable', this.onItemDisabled, this); + item.un('enable', this.onItemEnabled, this); + item.un('titlechange', this.onItemTitleChanged, this); + item.un('iconchange', this.onItemIconChanged, this); + item.un('beforeshow', this.onBeforeShowItem, this); + if(item == this.activeTab){ + var next = this.stack.next(); + if(next){ + this.setActiveTab(next); + }else if(this.items.getCount() > 0){ + this.setActiveTab(0); + }else{ + this.activeTab = null; + } + } + this.delegateUpdates(); + }, + + // private + onBeforeShowItem : function(item){ + if(item != this.activeTab){ + this.setActiveTab(item); + return false; + } + }, + + // private + onItemDisabled : function(item){ + var el = this.getTabEl(item); + if(el){ + Ext.fly(el).addClass('x-item-disabled'); + } + this.stack.remove(item); + }, + + // private + onItemEnabled : function(item){ + var el = this.getTabEl(item); + if(el){ + Ext.fly(el).removeClass('x-item-disabled'); + } + }, + + // private + onItemTitleChanged : function(item){ + var el = this.getTabEl(item); + if(el){ + Ext.fly(el).child('span.x-tab-strip-text', true).innerHTML = item.title; + } + }, + + //private + onItemIconChanged : function(item, iconCls, oldCls){ + var el = this.getTabEl(item); + if(el){ + Ext.fly(el).child('span.x-tab-strip-text').replaceClass(oldCls, iconCls); + } + }, + + /** + * Gets the DOM element for the tab strip item which activates the child panel with the specified + * ID. Access this to change the visual treatment of the item, for example by changing the CSS class name. + * @param {Panel/Number/String} tab The tab component, or the tab's index, or the tabs id or itemId. + * @return {HTMLElement} The DOM node + */ + getTabEl : function(item){ + return document.getElementById(this.id + this.idDelimiter + this.getComponent(item).getItemId()); + }, + + // private + onResize : function(){ + Ext.TabPanel.superclass.onResize.apply(this, arguments); + this.delegateUpdates(); + }, + + /** + * Suspends any internal calculations or scrolling while doing a bulk operation. See {@link #endUpdate} + */ + beginUpdate : function(){ + this.suspendUpdates = true; + }, + + /** + * Resumes calculations and scrolling at the end of a bulk operation. See {@link #beginUpdate} + */ + endUpdate : function(){ + this.suspendUpdates = false; + this.delegateUpdates(); + }, + + /** + * Hides the tab strip item for the passed tab + * @param {Number/String/Panel} item The tab index, id or item + */ + hideTabStripItem : function(item){ + item = this.getComponent(item); + var el = this.getTabEl(item); + if(el){ + el.style.display = 'none'; + this.delegateUpdates(); + } + this.stack.remove(item); + }, + + /** + * Unhides the tab strip item for the passed tab + * @param {Number/String/Panel} item The tab index, id or item + */ + unhideTabStripItem : function(item){ + item = this.getComponent(item); + var el = this.getTabEl(item); + if(el){ + el.style.display = ''; + this.delegateUpdates(); + } + }, + + // private + delegateUpdates : function(){ + if(this.suspendUpdates){ + return; + } + if(this.resizeTabs && this.rendered){ + this.autoSizeTabs(); + } + if(this.enableTabScroll && this.rendered){ + this.autoScrollTabs(); + } + }, + + // private + autoSizeTabs : function(){ + var count = this.items.length; + var ce = this.tabPosition != 'bottom' ? 'header' : 'footer'; + var ow = this[ce].dom.offsetWidth; + var aw = this[ce].dom.clientWidth; + + if(!this.resizeTabs || count < 1 || !aw){ // !aw for display:none + return; + } + + var each = Math.max(Math.min(Math.floor((aw-4) / count) - this.tabMargin, this.tabWidth), this.minTabWidth); // -4 for float errors in IE + this.lastTabWidth = each; + var lis = this.strip.query("li:not([className^=x-tab-edge])"); + for(var i = 0, len = lis.length; i < len; i++) { + var li = lis[i]; + var inner = Ext.fly(li).child('.x-tab-strip-inner', true); + var tw = li.offsetWidth; + var iw = inner.offsetWidth; + inner.style.width = (each - (tw-iw)) + 'px'; + } + }, + + // private + adjustBodyWidth : function(w){ + if(this.header){ + this.header.setWidth(w); + } + if(this.footer){ + this.footer.setWidth(w); + } + return w; + }, + + /** + * Sets the specified tab as the active tab. This method fires the {@link #beforetabchange} event which + * can return false to cancel the tab change. + * @param {String/Number} item + * The id or tab Panel to activate. This parameter may be any of the following: + *
    + *

    For additional information see {@link Ext.util.MixedCollection#get}. + */ + setActiveTab : function(item){ + item = this.getComponent(item); + if(!item || this.fireEvent('beforetabchange', this, item, this.activeTab) === false){ + return; + } + if(!this.rendered){ + this.activeTab = item; + return; + } + if(this.activeTab != item){ + if(this.activeTab){ + var oldEl = this.getTabEl(this.activeTab); + if(oldEl){ + Ext.fly(oldEl).removeClass('x-tab-strip-active'); + } + this.activeTab.fireEvent('deactivate', this.activeTab); + } + var el = this.getTabEl(item); + Ext.fly(el).addClass('x-tab-strip-active'); + this.activeTab = item; + this.stack.add(item); + + this.layout.setActiveItem(item); + if(this.scrolling){ + this.scrollToTab(item, this.animScroll); + } + + item.fireEvent('activate', item); + this.fireEvent('tabchange', this, item); + } + }, + + /** + * Gets the currently active tab. + * @return {Panel} The active tab + */ + getActiveTab : function(){ + return this.activeTab || null; + }, + + /** + * Gets the specified tab by id. + * @param {String} id The tab id + * @return {Panel} The tab + */ + getItem : function(item){ + return this.getComponent(item); + }, + + // private + autoScrollTabs : function(){ + this.pos = this.tabPosition=='bottom' ? this.footer : this.header; + var count = this.items.length; + var ow = this.pos.dom.offsetWidth; + var tw = this.pos.dom.clientWidth; + + var wrap = this.stripWrap; + var wd = wrap.dom; + var cw = wd.offsetWidth; + var pos = this.getScrollPos(); + var l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos; + + if(!this.enableTabScroll || count < 1 || cw < 20){ // 20 to prevent display:none issues + return; + } + if(l <= tw){ + wd.scrollLeft = 0; + wrap.setWidth(tw); + if(this.scrolling){ + this.scrolling = false; + this.pos.removeClass('x-tab-scrolling'); + this.scrollLeft.hide(); + this.scrollRight.hide(); + // See here: http://extjs.com/forum/showthread.php?t=49308&highlight=isSafari + if(Ext.isAir || Ext.isWebKit){ + wd.style.marginLeft = ''; + wd.style.marginRight = ''; + } + } + }else{ + if(!this.scrolling){ + this.pos.addClass('x-tab-scrolling'); + // See here: http://extjs.com/forum/showthread.php?t=49308&highlight=isSafari + if(Ext.isAir || Ext.isWebKit){ + wd.style.marginLeft = '18px'; + wd.style.marginRight = '18px'; + } + } + tw -= wrap.getMargins('lr'); + wrap.setWidth(tw > 20 ? tw : 20); + if(!this.scrolling){ + if(!this.scrollLeft){ + this.createScrollers(); + }else{ + this.scrollLeft.show(); + this.scrollRight.show(); + } + } + this.scrolling = true; + if(pos > (l-tw)){ // ensure it stays within bounds + wd.scrollLeft = l-tw; + }else{ // otherwise, make sure the active tab is still visible + this.scrollToTab(this.activeTab, false); + } + this.updateScrollButtons(); + } + }, + + // private + createScrollers : function(){ + this.pos.addClass('x-tab-scrolling-' + this.tabPosition); + var h = this.stripWrap.dom.offsetHeight; + + // left + var sl = this.pos.insertFirst({ + cls:'x-tab-scroller-left' + }); + sl.setHeight(h); + sl.addClassOnOver('x-tab-scroller-left-over'); + this.leftRepeater = new Ext.util.ClickRepeater(sl, { + interval : this.scrollRepeatInterval, + handler: this.onScrollLeft, + scope: this + }); + this.scrollLeft = sl; + + // right + var sr = this.pos.insertFirst({ + cls:'x-tab-scroller-right' + }); + sr.setHeight(h); + sr.addClassOnOver('x-tab-scroller-right-over'); + this.rightRepeater = new Ext.util.ClickRepeater(sr, { + interval : this.scrollRepeatInterval, + handler: this.onScrollRight, + scope: this + }); + this.scrollRight = sr; + }, + + // private + getScrollWidth : function(){ + return this.edge.getOffsetsTo(this.stripWrap)[0] + this.getScrollPos(); + }, + + // private + getScrollPos : function(){ + return parseInt(this.stripWrap.dom.scrollLeft, 10) || 0; + }, + + // private + getScrollArea : function(){ + return parseInt(this.stripWrap.dom.clientWidth, 10) || 0; + }, + + // private + getScrollAnim : function(){ + return {duration:this.scrollDuration, callback: this.updateScrollButtons, scope: this}; + }, + + // private + getScrollIncrement : function(){ + return this.scrollIncrement || (this.resizeTabs ? this.lastTabWidth+2 : 100); + }, + + /** + * Scrolls to a particular tab if tab scrolling is enabled + * @param {Panel} item The item to scroll to + * @param {Boolean} animate True to enable animations + */ + + scrollToTab : function(item, animate){ + if(!item){ return; } + var el = this.getTabEl(item); + var pos = this.getScrollPos(), area = this.getScrollArea(); + var left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos; + var right = left + el.offsetWidth; + if(left < pos){ + this.scrollTo(left, animate); + }else if(right > (pos + area)){ + this.scrollTo(right - area, animate); + } + }, + + // private + scrollTo : function(pos, animate){ + this.stripWrap.scrollTo('left', pos, animate ? this.getScrollAnim() : false); + if(!animate){ + this.updateScrollButtons(); + } + }, + + onWheel : function(e){ + var d = e.getWheelDelta()*this.wheelIncrement*-1; + e.stopEvent(); + + var pos = this.getScrollPos(); + var newpos = pos + d; + var sw = this.getScrollWidth()-this.getScrollArea(); + + var s = Math.max(0, Math.min(sw, newpos)); + if(s != pos){ + this.scrollTo(s, false); + } + }, + + // private + onScrollRight : function(){ + var sw = this.getScrollWidth()-this.getScrollArea(); + var pos = this.getScrollPos(); + var s = Math.min(sw, pos + this.getScrollIncrement()); + if(s != pos){ + this.scrollTo(s, this.animScroll); + } + }, + + // private + onScrollLeft : function(){ + var pos = this.getScrollPos(); + var s = Math.max(0, pos - this.getScrollIncrement()); + if(s != pos){ + this.scrollTo(s, this.animScroll); + } + }, + + // private + updateScrollButtons : function(){ + var pos = this.getScrollPos(); + this.scrollLeft[pos === 0 ? 'addClass' : 'removeClass']('x-tab-scroller-left-disabled'); + this.scrollRight[pos >= (this.getScrollWidth()-this.getScrollArea()) ? 'addClass' : 'removeClass']('x-tab-scroller-right-disabled'); + }, + + // private + beforeDestroy : function() { + if(this.items){ + this.items.each(function(item){ + if(item && item.tabEl){ + Ext.get(item.tabEl).removeAllListeners(); + item.tabEl = null; + } + }, this); + } + if(this.strip){ + this.strip.removeAllListeners(); + } + Ext.TabPanel.superclass.beforeDestroy.apply(this); + } + + /** + * @cfg {Boolean} collapsible + * @hide + */ + /** + * @cfg {String} header + * @hide + */ + /** + * @cfg {Boolean} headerAsText + * @hide + */ + /** + * @property header + * @hide + */ + /** + * @property title + * @hide + */ + /** + * @cfg {Array} tools + * @hide + */ + /** + * @cfg {Array} toolTemplate + * @hide + */ + /** + * @cfg {Boolean} hideCollapseTool + * @hide + */ + /** + * @cfg {Boolean} titleCollapse + * @hide + */ + /** + * @cfg {Boolean} collapsed + * @hide + */ + /** + * @cfg {String} layout + * @hide + */ + /** + * @cfg {Boolean} preventBodyReset + * @hide + */ +}); +Ext.reg('tabpanel', Ext.TabPanel); + +/** + * See {@link #setActiveTab}. Sets the specified tab as the active tab. This method fires + * the {@link #beforetabchange} event which can return false to cancel the tab change. + * @param {String/Panel} tab The id or tab Panel to activate + * @method activate + */ +Ext.TabPanel.prototype.activate = Ext.TabPanel.prototype.setActiveTab; + +// private utility class used by TabPanel +Ext.TabPanel.AccessStack = function(){ + var items = []; + return { + add : function(item){ + items.push(item); + if(items.length > 10){ + items.shift(); + } + }, + + remove : function(item){ + var s = []; + for(var i = 0, len = items.length; i < len; i++) { + if(items[i] != item){ + s.push(items[i]); + } + } + items = s; + }, + + next : function(){ + return items.pop(); + } + }; +}; \ No newline at end of file