commit extjs-2.2.1
[extjs.git] / source / widgets / TabPanel.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /**\r
10  * @class Ext.TabPanel\r
11  * <p>A basic tab container. TabPanels can be used exactly like a standard {@link Ext.Panel} for layout\r
12  * purposes, but also have special support for containing child Components that are managed using a CardLayout\r
13  * layout manager, and displayed as seperate tabs.</p>\r
14  * <p>There is no actual tab class &mdash; each tab is simply an {@link Ext.BoxComponent Component} such\r
15  * as a {@link Ext.Panel Panel}. However, when rendered in a TabPanel, each child Component can fire\r
16  * additional events that only exist for tabs and are not available from other Component. These are:</p>\r
17  * <ul>\r
18  * <li><b>activate</b>: Fires when this Component becomes the active tab.\r
19  * <div class="mdetail-params">\r
20  *      <strong style="font-weight: normal;">Listeners will be called with the following arguments:</strong>\r
21  *      <ul><li><code>tab</code> : Panel<div class="sub-desc">The tab that was activated</div></li></ul>\r
22  *  </div></li>\r
23  * <li><b>deactivate</b>: Fires when the Component that was the active tab becomes deactivated.\r
24  * <div class="mdetail-params">\r
25  *      <strong style="font-weight: normal;">Listeners will be called with the following arguments:</strong>\r
26  *      <ul><li><code>tab</code> : Panel<div class="sub-desc">The tab that was deactivated</div></li></ul>\r
27  *  </div></li>\r
28  * </ul>\r
29  * <p>To add Components to a TabPanel which are generated dynamically on the server, it is necessary to\r
30  * create a server script to generate the Javascript to create the Component required.</p>\r
31  * For example, to add a GridPanel to a TabPanel where the GridPanel is generated by the server\r
32  * based on certain parameters, you would need to execute an Ajax request to invoke your the script,\r
33  * and process the response object to add it to the TabPanel:</p><pre><code>\r
34 Ext.Ajax.request({\r
35     url: 'gen-invoice-grid.php',\r
36     params: {\r
37         startDate = Ext.getCmp('start-date').getValue(),\r
38         endDate = Ext.getCmp('end-date').getValue()\r
39     },\r
40     success: function(xhr) {\r
41         var newComponent = eval(xhr.responseText);\r
42         myTabPanel.add(newComponent);\r
43         myTabPanel.setActiveTab(newComponent);\r
44     },\r
45     failure: function() {\r
46         Ext.Msg.alert("Grid create failed", "Server communication failure");\r
47     }\r
48 });\r
49 </code></pre>\r
50  * <p>The server script would need to return an executable Javascript statement which, when processed\r
51  * using <tt>eval()</tt> will return either a config object with an {@link Ext.Component#xtype xtype},\r
52  * or an instantiated Component. For example:</p><pre><code>\r
53 (function() {\r
54     function formatDate(value){\r
55         return value ? value.dateFormat('M d, Y') : '';\r
56     };\r
57 \r
58     var store = new Ext.data.Store({\r
59         url: 'get-invoice-data.php',\r
60         baseParams: {\r
61             startDate: '01/01/2008',\r
62             endDate: '01/31/2008'\r
63         },\r
64         reader: new Ext.data.JsonReader({\r
65             record: 'transaction',\r
66             id: 'id',\r
67             totalRecords: 'total'\r
68         }, [\r
69            'customer',\r
70            'invNo',\r
71            {name: 'date', type: 'date', dateFormat: 'm/d/Y'},\r
72            {name: 'value', type: 'float'}\r
73         ])\r
74     });\r
75 \r
76     var grid = new Ext.grid.GridPanel({\r
77         title: 'Invoice Report',\r
78         bbar: new Ext.PagingToolbar(store),\r
79         store: store,\r
80         columns: [\r
81             {header: "Customer", width: 250, dataIndex: 'customer', sortable: true},\r
82             {header: "Invoice Number", width: 120, dataIndex: 'invNo', sortable: true},\r
83             {header: "Invoice Date", width: 100, dataIndex: 'date', renderer: formatDate, sortable: true},\r
84             {header: "Value", width: 120, dataIndex: 'value', renderer: 'usMoney', sortable: true}\r
85         ],\r
86     });\r
87     store.load();\r
88     return grid;\r
89 })();\r
90 </code></pre>\r
91  * <p>Since that code is <i>generated</i> by a server script, the <tt>baseParams</tt> for the Store\r
92  * can be configured into the Store. The metadata to allow generation of the Record layout, and the\r
93  * ColumnModel is also known on the server, so these can be generated into the code.</p>\r
94  * <p>When that code fragment is passed through the <tt>eval</tt> function in the success handler\r
95  * of the Ajax request, the code is executed by the Javascript processor, and the anonymous function\r
96  * runs, and returns the grid.</p>\r
97  * <p>There are several other methods available for creating TabPanels. The output of the following\r
98  * examples should produce exactly the same appearance. The tabs can be created and rendered completely\r
99  * in code, as in this example:</p>\r
100  * <pre><code>\r
101 var tabs = new Ext.TabPanel({\r
102     renderTo: Ext.getBody(),\r
103     activeTab: 0,\r
104     items: [{\r
105         title: 'Tab 1',\r
106         html: 'A simple tab'\r
107     },{\r
108         title: 'Tab 2',\r
109         html: 'Another one'\r
110     }]\r
111 });\r
112 </code></pre>\r
113   * <p>TabPanels can also be rendered from pre-existing markup in a couple of ways.  See the {@link #autoTabs} example for\r
114   * rendering entirely from markup that is already structured correctly as a TabPanel (a container div with\r
115   * one or more nested tab divs with class 'x-tab'). You can also render from markup that is not strictly\r
116   * structured by simply specifying by id which elements should be the container and the tabs. Using this method,\r
117   * tab content can be pulled from different elements within the page by id regardless of page structure.  Note\r
118   * that the tab divs in this example contain the class 'x-hide-display' so that they can be rendered deferred\r
119   * without displaying outside the tabs. You could alternately set {@link #deferredRender} to false to render all\r
120   * content tabs on page load. For example:\r
121   * <pre><code>\r
122 var tabs = new Ext.TabPanel({\r
123     renderTo: 'my-tabs',\r
124     activeTab: 0,\r
125     items:[\r
126         {contentEl:'tab1', title:'Tab 1'},\r
127         {contentEl:'tab2', title:'Tab 2'}\r
128     ]\r
129 });\r
130 \r
131 // Note that the tabs do not have to be nested within the container (although they can be)\r
132 &lt;div id="my-tabs">&lt;/div>\r
133 &lt;div id="tab1" class="x-hide-display">A simple tab&lt;/div>\r
134 &lt;div id="tab2" class="x-hide-display">Another one&lt;/div>\r
135 </code></pre>\r
136  * @extends Ext.Panel\r
137  * @constructor\r
138  * @param {Object} config The configuration options\r
139  */\r
140 Ext.TabPanel = Ext.extend(Ext.Panel,  {\r
141     /**\r
142      * @cfg {Boolean} layoutOnTabChange Set to true to do a layout of tab items as tabs are changed.\r
143      */\r
144     /**\r
145      * @cfg {String} tabCls <b>This config option is used on <u>child Components</u> of ths TabPanel.</b> A CSS\r
146      * class name applied to the tab strip item representing the child Component, allowing special\r
147      * styling to be applied.\r
148      */\r
149     /**\r
150      * @cfg {Boolean} monitorResize True to automatically monitor window resize events and rerender the layout on\r
151      * browser resize (defaults to true).\r
152      */\r
153     monitorResize : true,\r
154     /**\r
155      * @cfg {Boolean} deferredRender Internally, the TabPanel uses a {@link Ext.layout.CardLayout} to manage its tabs.\r
156      * This property will be passed on to the layout as its {@link Ext.layout.CardLayout#deferredRender} config value,\r
157      * determining whether or not each tab is rendered only when first accessed (defaults to true).\r
158      * <p>Be aware that leaving deferredRender as <b><tt>true</tt></b> means that, if the TabPanel is within\r
159      * a {@link Ext.form.FormPanel form}, then until a tab is activated, any Fields within that tab will not\r
160      * be rendered, and will therefore not be submitted and will not be available to either\r
161      * {@link Ext.form.BasicForm#getValues getValues} or {@link Ext.form.BasicForm#setValues setValues}.</p>\r
162      */\r
163     deferredRender : true,\r
164     /**\r
165      * @cfg {Number} tabWidth The initial width in pixels of each new tab (defaults to 120).\r
166      */\r
167     tabWidth: 120,\r
168     /**\r
169      * @cfg {Number} minTabWidth The minimum width in pixels for each tab when {@link #resizeTabs} = true (defaults to 30).\r
170      */\r
171     minTabWidth: 30,\r
172     /**\r
173      * @cfg {Boolean} resizeTabs True to automatically resize each tab so that the tabs will completely fill the\r
174      * tab strip (defaults to false).  Setting this to true may cause specific widths that might be set per tab to\r
175      * be overridden in order to fit them all into view (although {@link #minTabWidth} will always be honored).\r
176      */\r
177     resizeTabs:false,\r
178     /**\r
179      * @cfg {Boolean} enableTabScroll True to enable scrolling to tabs that may be invisible due to overflowing the\r
180      * overall TabPanel width. Only available with tabPosition:'top' (defaults to false).\r
181      */\r
182     enableTabScroll: false,\r
183     /**\r
184      * @cfg {Number} scrollIncrement The number of pixels to scroll each time a tab scroll button is pressed (defaults\r
185      * to 100, or if {@link #resizeTabs} = true, the calculated tab width).  Only applies when {@link #enableTabScroll} = true.\r
186      */\r
187     scrollIncrement : 0,\r
188     /**\r
189      * @cfg {Number} scrollRepeatInterval Number of milliseconds between each scroll while a tab scroll button is\r
190      * continuously pressed (defaults to 400).\r
191      */\r
192     scrollRepeatInterval : 400,\r
193     /**\r
194      * @cfg {Float} scrollDuration The number of milliseconds that each scroll animation should last (defaults to .35).\r
195      * Only applies when {@link #animScroll} = true.\r
196      */\r
197     scrollDuration : .35,\r
198     /**\r
199      * @cfg {Boolean} animScroll True to animate tab scrolling so that hidden tabs slide smoothly into view (defaults\r
200      * to true).  Only applies when {@link #enableTabScroll} = true.\r
201      */\r
202     animScroll : true,\r
203     /**\r
204      * @cfg {String} tabPosition The position where the tab strip should be rendered (defaults to 'top').  The only\r
205      * other supported value is 'bottom'.  Note that tab scrolling is only supported for position 'top'.\r
206      */\r
207     tabPosition: 'top',\r
208     /**\r
209      * @cfg {String} baseCls The base CSS class applied to the panel (defaults to 'x-tab-panel').\r
210      */\r
211     baseCls: 'x-tab-panel',\r
212     /**\r
213      * @cfg {Boolean} autoTabs\r
214      * <p>True to query the DOM for any divs with a class of 'x-tab' to be automatically converted\r
215      * to tabs and added to this panel (defaults to false).  Note that the query will be executed within the scope of\r
216      * the container element only (so that multiple tab panels from markup can be supported via this method).</p>\r
217      * <p>This method is only possible when the markup is structured correctly as a container with nested\r
218      * divs containing the class 'x-tab'. To create TabPanels without these limitations, or to pull tab content from\r
219      * other elements on the page, see the example at the top of the class for generating tabs from markup.</p>\r
220      * <p>There are a couple of things to note when using this method:<ul>\r
221      * <li>When using the autoTabs config (as opposed to passing individual tab configs in the TabPanel's\r
222      * {@link #items} collection), you must use {@link #applyTo} to correctly use the specified id as the tab container.\r
223      * The autoTabs method <em>replaces</em> existing content with the TabPanel components.</li>\r
224      * <li>Make sure that you set {@link #deferredRender} to false so that the content elements for each tab will be\r
225      * rendered into the TabPanel immediately upon page load, otherwise they will not be transformed until each tab\r
226      * is activated and will be visible outside the TabPanel.</li>\r
227      * </ul>Example usage:</p>\r
228      * <pre><code>\r
229 var tabs = new Ext.TabPanel({\r
230     applyTo: 'my-tabs',\r
231     activeTab: 0,\r
232     deferredRender: false,\r
233     autoTabs: true\r
234 });\r
235 \r
236 // This markup will be converted to a TabPanel from the code above\r
237 &lt;div id="my-tabs">\r
238     &lt;div class="x-tab" title="Tab 1">A simple tab&lt;/div>\r
239     &lt;div class="x-tab" title="Tab 2">Another one&lt;/div>\r
240 &lt;/div>\r
241 </code></pre>\r
242      */\r
243     autoTabs : false,\r
244     /**\r
245      * @cfg {String} autoTabSelector The CSS selector used to search for tabs in existing markup when {@link #autoTabs}\r
246      * = true (defaults to 'div.x-tab').  This can be any valid selector supported by {@link Ext.DomQuery#select}.\r
247      * Note that the query will be executed within the scope of this tab panel only (so that multiple tab panels from\r
248      * markup can be supported on a page).\r
249      */\r
250     autoTabSelector:'div.x-tab',\r
251     /**\r
252      * @cfg {String/Number} activeTab A string id or the numeric index of the tab that should be initially\r
253      * activated on render (defaults to none).\r
254      */\r
255     activeTab : null,\r
256     /**\r
257      * @cfg {Number} tabMargin The number of pixels of space to calculate into the sizing and scrolling of tabs. If you\r
258      * change the margin in CSS, you will need to update this value so calculations are correct with either resizeTabs\r
259      * or scrolling tabs. (defaults to 2)\r
260      */\r
261     tabMargin : 2,\r
262     /**\r
263      * @cfg {Boolean} plain True to render the tab strip without a background container image (defaults to false).\r
264      */\r
265     plain: false,\r
266     /**\r
267      * @cfg {Number} wheelIncrement For scrolling tabs, the number of pixels to increment on mouse wheel scrolling (defaults to 20).\r
268      */\r
269     wheelIncrement : 20,\r
270 \r
271     /*\r
272      * This is a protected property used when concatenating tab ids to the TabPanel id for internal uniqueness.\r
273      * It does not generally need to be changed, but can be if external code also uses an id scheme that can\r
274      * potentially clash with this one.\r
275      */\r
276     idDelimiter : '__',\r
277 \r
278     // private\r
279     itemCls : 'x-tab-item',\r
280 \r
281     // private config overrides\r
282     elements: 'body',\r
283     headerAsText: false,\r
284     frame: false,\r
285     hideBorders:true,\r
286 \r
287     // private\r
288     initComponent : function(){\r
289         this.frame = false;\r
290         Ext.TabPanel.superclass.initComponent.call(this);\r
291         this.addEvents(\r
292             /**\r
293              * @event beforetabchange\r
294              * Fires before the active tab changes. Handlers can return false to cancel the tab change.\r
295              * @param {TabPanel} this\r
296              * @param {Panel} newTab The tab being activated\r
297              * @param {Panel} currentTab The current active tab\r
298              */\r
299             'beforetabchange',\r
300             /**\r
301              * @event tabchange\r
302              * Fires after the active tab has changed.\r
303              * @param {TabPanel} this\r
304              * @param {Panel} tab The new active tab\r
305              */\r
306             'tabchange',\r
307             /**\r
308              * @event contextmenu\r
309              * Relays the contextmenu event from a tab selector element in the tab strip.\r
310              * @param {TabPanel} this\r
311              * @param {Panel} tab The target tab\r
312              * @param {EventObject} e\r
313              */\r
314             'contextmenu'\r
315         );\r
316         this.setLayout(new Ext.layout.CardLayout({\r
317             deferredRender: this.deferredRender\r
318         }));\r
319         if(this.tabPosition == 'top'){\r
320             this.elements += ',header';\r
321             this.stripTarget = 'header';\r
322         }else {\r
323             this.elements += ',footer';\r
324             this.stripTarget = 'footer';\r
325         }\r
326         if(!this.stack){\r
327             this.stack = Ext.TabPanel.AccessStack();\r
328         }\r
329         this.initItems();\r
330     },\r
331 \r
332     // private\r
333     render : function(){\r
334         Ext.TabPanel.superclass.render.apply(this, arguments);\r
335         if(this.activeTab !== undefined){\r
336             var item = this.activeTab;\r
337             delete this.activeTab;\r
338             this.setActiveTab(item);\r
339         }\r
340     },\r
341 \r
342     // private\r
343     onRender : function(ct, position){\r
344         Ext.TabPanel.superclass.onRender.call(this, ct, position);\r
345 \r
346         if(this.plain){\r
347             var pos = this.tabPosition == 'top' ? 'header' : 'footer';\r
348             this[pos].addClass('x-tab-panel-'+pos+'-plain');\r
349         }\r
350 \r
351         var st = this[this.stripTarget];\r
352 \r
353         this.stripWrap = st.createChild({cls:'x-tab-strip-wrap', cn:{\r
354             tag:'ul', cls:'x-tab-strip x-tab-strip-'+this.tabPosition}});\r
355 \r
356         var beforeEl = (this.tabPosition=='bottom' ? this.stripWrap : null);\r
357         this.stripSpacer = st.createChild({cls:'x-tab-strip-spacer'}, beforeEl);\r
358         this.strip = new Ext.Element(this.stripWrap.dom.firstChild);\r
359 \r
360         this.edge = this.strip.createChild({tag:'li', cls:'x-tab-edge'});\r
361         this.strip.createChild({cls:'x-clear'});\r
362 \r
363         this.body.addClass('x-tab-panel-body-'+this.tabPosition);\r
364 \r
365         if(!this.itemTpl){\r
366             var tt = new Ext.Template(\r
367                  '<li class="{cls}" id="{id}"><a class="x-tab-strip-close" onclick="return false;"></a>',\r
368                  '<a class="x-tab-right" href="#" onclick="return false;"><em class="x-tab-left">',\r
369                  '<span class="x-tab-strip-inner"><span class="x-tab-strip-text {iconCls}">{text}</span></span>',\r
370                  '</em></a></li>'\r
371             );\r
372             tt.disableFormats = true;\r
373             tt.compile();\r
374             Ext.TabPanel.prototype.itemTpl = tt;\r
375         }\r
376 \r
377         this.items.each(this.initTab, this);\r
378     },\r
379 \r
380     // private\r
381     afterRender : function(){\r
382         Ext.TabPanel.superclass.afterRender.call(this);\r
383         if(this.autoTabs){\r
384             this.readTabs(false);\r
385         }\r
386     },\r
387 \r
388     // private\r
389     initEvents : function(){\r
390         Ext.TabPanel.superclass.initEvents.call(this);\r
391         this.on('add', this.onAdd, this);\r
392         this.on('remove', this.onRemove, this);\r
393 \r
394         this.strip.on('mousedown', this.onStripMouseDown, this);\r
395         this.strip.on('contextmenu', this.onStripContextMenu, this);\r
396         if(this.enableTabScroll){\r
397             this.strip.on('mousewheel', this.onWheel, this);\r
398         }\r
399     },\r
400 \r
401     // private\r
402     findTargets : function(e){\r
403         var item = null;\r
404         var itemEl = e.getTarget('li', this.strip);\r
405         if(itemEl){\r
406             item = this.getComponent(itemEl.id.split(this.idDelimiter)[1]);\r
407             if(item.disabled){\r
408                 return {\r
409                     close : null,\r
410                     item : null,\r
411                     el : null\r
412                 };\r
413             }\r
414         }\r
415         return {\r
416             close : e.getTarget('.x-tab-strip-close', this.strip),\r
417             item : item,\r
418             el : itemEl\r
419         };\r
420     },\r
421 \r
422     // private\r
423     onStripMouseDown : function(e){\r
424         if(e.button != 0){\r
425             return;\r
426         }\r
427         e.preventDefault();\r
428         var t = this.findTargets(e);\r
429         if(t.close){\r
430             this.remove(t.item);\r
431             return;\r
432         }\r
433         if(t.item && t.item != this.activeTab){\r
434             this.setActiveTab(t.item);\r
435         }\r
436     },\r
437 \r
438     // private\r
439     onStripContextMenu : function(e){\r
440         e.preventDefault();\r
441         var t = this.findTargets(e);\r
442         if(t.item){\r
443             this.fireEvent('contextmenu', this, t.item, e);\r
444         }\r
445     },\r
446 \r
447     /**\r
448      * True to scan the markup in this tab panel for autoTabs using the autoTabSelector\r
449      * @param {Boolean} removeExisting True to remove existing tabs\r
450      */\r
451     readTabs : function(removeExisting){\r
452         if(removeExisting === true){\r
453             this.items.each(function(item){\r
454                 this.remove(item);\r
455             }, this);\r
456         }\r
457         var tabs = this.el.query(this.autoTabSelector);\r
458         for(var i = 0, len = tabs.length; i < len; i++){\r
459             var tab = tabs[i];\r
460             var title = tab.getAttribute('title');\r
461             tab.removeAttribute('title');\r
462             this.add({\r
463                 title: title,\r
464                 el: tab\r
465             });\r
466         }\r
467     },\r
468 \r
469     // private\r
470     initTab : function(item, index){\r
471         var before = this.strip.dom.childNodes[index];\r
472         var cls = item.closable ? 'x-tab-strip-closable' : '';\r
473         if(item.disabled){\r
474             cls += ' x-item-disabled';\r
475         }\r
476         if(item.iconCls){\r
477             cls += ' x-tab-with-icon';\r
478         }\r
479         if(item.tabCls){\r
480             cls += ' ' + item.tabCls;\r
481         }\r
482 \r
483         var p = {\r
484             id: this.id + this.idDelimiter + item.getItemId(),\r
485             text: item.title,\r
486             cls: cls,\r
487             iconCls: item.iconCls || ''\r
488         };\r
489         var el = before ?\r
490                  this.itemTpl.insertBefore(before, p) :\r
491                  this.itemTpl.append(this.strip, p);\r
492 \r
493         Ext.fly(el).addClassOnOver('x-tab-strip-over');\r
494 \r
495         if(item.tabTip){\r
496             Ext.fly(el).child('span.x-tab-strip-text', true).qtip = item.tabTip;\r
497         }\r
498         item.tabEl = el;\r
499 \r
500         item.on('disable', this.onItemDisabled, this);\r
501         item.on('enable', this.onItemEnabled, this);\r
502         item.on('titlechange', this.onItemTitleChanged, this);\r
503         item.on('iconchange', this.onItemIconChanged, this);\r
504         item.on('beforeshow', this.onBeforeShowItem, this);\r
505     },\r
506 \r
507     // private\r
508     onAdd : function(tp, item, index){\r
509         this.initTab(item, index);\r
510         if(this.items.getCount() == 1){\r
511             this.syncSize();\r
512         }\r
513         this.delegateUpdates();\r
514     },\r
515 \r
516     // private\r
517     onBeforeAdd : function(item){\r
518         var existing = item.events ? (this.items.containsKey(item.getItemId()) ? item : null) : this.items.get(item);\r
519         if(existing){\r
520             this.setActiveTab(item);\r
521             return false;\r
522         }\r
523         Ext.TabPanel.superclass.onBeforeAdd.apply(this, arguments);\r
524         var es = item.elements;\r
525         item.elements = es ? es.replace(',header', '') : es;\r
526         item.border = (item.border === true);\r
527     },\r
528 \r
529     // private\r
530     onRemove : function(tp, item){\r
531         Ext.destroy(Ext.get(this.getTabEl(item)));\r
532         this.stack.remove(item);\r
533         item.un('disable', this.onItemDisabled, this);\r
534         item.un('enable', this.onItemEnabled, this);\r
535         item.un('titlechange', this.onItemTitleChanged, this);\r
536         item.un('iconchange', this.onItemIconChanged, this);\r
537         item.un('beforeshow', this.onBeforeShowItem, this);\r
538         if(item == this.activeTab){\r
539             var next = this.stack.next();\r
540             if(next){\r
541                 this.setActiveTab(next);\r
542             }else if(this.items.getCount() > 0){\r
543                 this.setActiveTab(0);\r
544             }else{\r
545                 this.activeTab = null;\r
546             }\r
547         }\r
548         this.delegateUpdates();\r
549     },\r
550 \r
551     // private\r
552     onBeforeShowItem : function(item){\r
553         if(item != this.activeTab){\r
554             this.setActiveTab(item);\r
555             return false;\r
556         }\r
557     },\r
558 \r
559     // private\r
560     onItemDisabled : function(item){\r
561         var el = this.getTabEl(item);\r
562         if(el){\r
563             Ext.fly(el).addClass('x-item-disabled');\r
564         }\r
565         this.stack.remove(item);\r
566     },\r
567 \r
568     // private\r
569     onItemEnabled : function(item){\r
570         var el = this.getTabEl(item);\r
571         if(el){\r
572             Ext.fly(el).removeClass('x-item-disabled');\r
573         }\r
574     },\r
575 \r
576     // private\r
577     onItemTitleChanged : function(item){\r
578         var el = this.getTabEl(item);\r
579         if(el){\r
580             Ext.fly(el).child('span.x-tab-strip-text', true).innerHTML = item.title;\r
581         }\r
582     },\r
583     \r
584     //private\r
585     onItemIconChanged: function(item, iconCls, oldCls){\r
586         var el = this.getTabEl(item);\r
587         if(el){\r
588             Ext.fly(el).child('span.x-tab-strip-text').replaceClass(oldCls, iconCls);\r
589         }\r
590     },\r
591 \r
592     /**\r
593      * Gets the DOM element for tab strip item which activates the\r
594      * child panel with the specified ID. Access this to change the visual treatment of the\r
595      * item, for example by changing the CSS class name.\r
596      * @param {Panel/Number} tab The tab component, or the tab's index\r
597      * @return {HTMLElement} The DOM node\r
598      */\r
599     getTabEl : function(item){\r
600         var itemId = (typeof item === 'number')?this.items.items[item].getItemId() : item.getItemId();\r
601         return document.getElementById(this.id+this.idDelimiter+itemId);\r
602     },\r
603 \r
604     // private\r
605     onResize : function(){\r
606         Ext.TabPanel.superclass.onResize.apply(this, arguments);\r
607         this.delegateUpdates();\r
608     },\r
609 \r
610     /**\r
611      * Suspends any internal calculations or scrolling while doing a bulk operation. See {@link #endUpdate}\r
612      */\r
613     beginUpdate : function(){\r
614         this.suspendUpdates = true;\r
615     },\r
616 \r
617     /**\r
618      * Resumes calculations and scrolling at the end of a bulk operation. See {@link #beginUpdate}\r
619      */\r
620     endUpdate : function(){\r
621         this.suspendUpdates = false;\r
622         this.delegateUpdates();\r
623     },\r
624 \r
625     /**\r
626      * Hides the tab strip item for the passed tab\r
627      * @param {Number/String/Panel} item The tab index, id or item\r
628      */\r
629     hideTabStripItem : function(item){\r
630         item = this.getComponent(item);\r
631         var el = this.getTabEl(item);\r
632         if(el){\r
633             el.style.display = 'none';\r
634             this.delegateUpdates();\r
635         }\r
636         this.stack.remove(item);\r
637     },\r
638 \r
639     /**\r
640      * Unhides the tab strip item for the passed tab\r
641      * @param {Number/String/Panel} item The tab index, id or item\r
642      */\r
643     unhideTabStripItem : function(item){\r
644         item = this.getComponent(item);\r
645         var el = this.getTabEl(item);\r
646         if(el){\r
647             el.style.display = '';\r
648             this.delegateUpdates();\r
649         }\r
650     },\r
651 \r
652     // private\r
653     delegateUpdates : function(){\r
654         if(this.suspendUpdates){\r
655             return;\r
656         }\r
657         if(this.resizeTabs && this.rendered){\r
658             this.autoSizeTabs();\r
659         }\r
660         if(this.enableTabScroll && this.rendered){\r
661             this.autoScrollTabs();\r
662         }\r
663     },\r
664 \r
665     // private\r
666     autoSizeTabs : function(){\r
667         var count = this.items.length;\r
668         var ce = this.tabPosition != 'bottom' ? 'header' : 'footer';\r
669         var ow = this[ce].dom.offsetWidth;\r
670         var aw = this[ce].dom.clientWidth;\r
671 \r
672         if(!this.resizeTabs || count < 1 || !aw){ // !aw for display:none\r
673             return;\r
674         }\r
675 \r
676         var each = Math.max(Math.min(Math.floor((aw-4) / count) - this.tabMargin, this.tabWidth), this.minTabWidth); // -4 for float errors in IE\r
677         this.lastTabWidth = each;\r
678         var lis = this.stripWrap.dom.getElementsByTagName('li');\r
679         for(var i = 0, len = lis.length-1; i < len; i++) { // -1 for the "edge" li\r
680             var li = lis[i];\r
681             var inner = li.childNodes[1].firstChild.firstChild;\r
682             var tw = li.offsetWidth;\r
683             var iw = inner.offsetWidth;\r
684             inner.style.width = (each - (tw-iw)) + 'px';\r
685         }\r
686     },\r
687 \r
688     // private\r
689     adjustBodyWidth : function(w){\r
690         if(this.header){\r
691             this.header.setWidth(w);\r
692         }\r
693         if(this.footer){\r
694             this.footer.setWidth(w);\r
695         }\r
696         return w;\r
697     },\r
698 \r
699     /**\r
700      * Sets the specified tab as the active tab. This method fires the {@link #beforetabchange} event which\r
701      * can return false to cancel the tab change.\r
702      * @param {String/Panel} tab The id or tab Panel to activate\r
703      */\r
704     setActiveTab : function(item){\r
705         item = this.getComponent(item);\r
706         if(!item || this.fireEvent('beforetabchange', this, item, this.activeTab) === false){\r
707             return;\r
708         }\r
709         if(!this.rendered){\r
710             this.activeTab = item;\r
711             return;\r
712         }\r
713         if(this.activeTab != item){\r
714             if(this.activeTab){\r
715                 var oldEl = this.getTabEl(this.activeTab);\r
716                 if(oldEl){\r
717                     Ext.fly(oldEl).removeClass('x-tab-strip-active');\r
718                 }\r
719                 this.activeTab.fireEvent('deactivate', this.activeTab);\r
720             }\r
721             var el = this.getTabEl(item);\r
722             Ext.fly(el).addClass('x-tab-strip-active');\r
723             this.activeTab = item;\r
724             this.stack.add(item);\r
725 \r
726             this.layout.setActiveItem(item);\r
727             if(this.layoutOnTabChange && item.doLayout){\r
728                 item.doLayout();\r
729             }\r
730             if(this.scrolling){\r
731                 this.scrollToTab(item, this.animScroll);\r
732             }\r
733 \r
734             item.fireEvent('activate', item);\r
735             this.fireEvent('tabchange', this, item);\r
736         }\r
737     },\r
738 \r
739     /**\r
740      * Gets the currently active tab.\r
741      * @return {Panel} The active tab\r
742      */\r
743     getActiveTab : function(){\r
744         return this.activeTab || null;\r
745     },\r
746 \r
747     /**\r
748      * Gets the specified tab by id.\r
749      * @param {String} id The tab id\r
750      * @return {Panel} The tab\r
751      */\r
752     getItem : function(item){\r
753         return this.getComponent(item);\r
754     },\r
755 \r
756     // private\r
757     autoScrollTabs : function(){\r
758         this.pos = this.tabPosition=='bottom' ? this.footer : this.header;\r
759         var count = this.items.length;\r
760         var ow = this.pos.dom.offsetWidth;\r
761         var tw = this.pos.dom.clientWidth;\r
762 \r
763         var wrap = this.stripWrap;\r
764         var wd = wrap.dom;\r
765         var cw = wd.offsetWidth;\r
766         var pos = this.getScrollPos();\r
767         var l = this.edge.getOffsetsTo(this.stripWrap)[0] + pos;\r
768 \r
769         if(!this.enableTabScroll || count < 1 || cw < 20){ // 20 to prevent display:none issues\r
770             return;\r
771         }\r
772         if(l <= tw){\r
773             wd.scrollLeft = 0;\r
774             wrap.setWidth(tw);\r
775             if(this.scrolling){\r
776                 this.scrolling = false;\r
777                 this.pos.removeClass('x-tab-scrolling');\r
778                 this.scrollLeft.hide();\r
779                 this.scrollRight.hide();\r
780                 if(Ext.isAir || Ext.isSafari){\r
781                     wd.style.marginLeft = '';\r
782                     wd.style.marginRight = '';\r
783                 }\r
784             }\r
785         }else{\r
786             if(!this.scrolling){\r
787                 this.pos.addClass('x-tab-scrolling');\r
788                 if(Ext.isAir || Ext.isSafari){\r
789                     wd.style.marginLeft = '18px';\r
790                     wd.style.marginRight = '18px';\r
791                 }\r
792             }\r
793             tw -= wrap.getMargins('lr');\r
794             wrap.setWidth(tw > 20 ? tw : 20);\r
795             if(!this.scrolling){\r
796                 if(!this.scrollLeft){\r
797                     this.createScrollers();\r
798                 }else{\r
799                     this.scrollLeft.show();\r
800                     this.scrollRight.show();\r
801                 }\r
802             }\r
803             this.scrolling = true;\r
804             if(pos > (l-tw)){ // ensure it stays within bounds\r
805                 wd.scrollLeft = l-tw;\r
806             }else{ // otherwise, make sure the active tab is still visible\r
807                 this.scrollToTab(this.activeTab, false);\r
808             }\r
809             this.updateScrollButtons();\r
810         }\r
811     },\r
812 \r
813     // private\r
814     createScrollers : function(){\r
815         this.pos.addClass('x-tab-scrolling-' + this.tabPosition);\r
816         var h = this.stripWrap.dom.offsetHeight;\r
817 \r
818         // left\r
819         var sl = this.pos.insertFirst({\r
820             cls:'x-tab-scroller-left'\r
821         });\r
822         sl.setHeight(h);\r
823         sl.addClassOnOver('x-tab-scroller-left-over');\r
824         this.leftRepeater = new Ext.util.ClickRepeater(sl, {\r
825             interval : this.scrollRepeatInterval,\r
826             handler: this.onScrollLeft,\r
827             scope: this\r
828         });\r
829         this.scrollLeft = sl;\r
830 \r
831         // right\r
832         var sr = this.pos.insertFirst({\r
833             cls:'x-tab-scroller-right'\r
834         });\r
835         sr.setHeight(h);\r
836         sr.addClassOnOver('x-tab-scroller-right-over');\r
837         this.rightRepeater = new Ext.util.ClickRepeater(sr, {\r
838             interval : this.scrollRepeatInterval,\r
839             handler: this.onScrollRight,\r
840             scope: this\r
841         });\r
842         this.scrollRight = sr;\r
843     },\r
844 \r
845     // private\r
846     getScrollWidth : function(){\r
847         return this.edge.getOffsetsTo(this.stripWrap)[0] + this.getScrollPos();\r
848     },\r
849 \r
850     // private\r
851     getScrollPos : function(){\r
852         return parseInt(this.stripWrap.dom.scrollLeft, 10) || 0;\r
853     },\r
854 \r
855     // private\r
856     getScrollArea : function(){\r
857         return parseInt(this.stripWrap.dom.clientWidth, 10) || 0;\r
858     },\r
859 \r
860     // private\r
861     getScrollAnim : function(){\r
862         return {duration:this.scrollDuration, callback: this.updateScrollButtons, scope: this};\r
863     },\r
864 \r
865     // private\r
866     getScrollIncrement : function(){\r
867         return this.scrollIncrement || (this.resizeTabs ? this.lastTabWidth+2 : 100);\r
868     },\r
869 \r
870     /**\r
871      * Scrolls to a particular tab if tab scrolling is enabled\r
872      * @param {Panel} item The item to scroll to\r
873      * @param {Boolean} animate True to enable animations\r
874      */\r
875 \r
876     scrollToTab : function(item, animate){\r
877         if(!item){ return; }\r
878         var el = this.getTabEl(item);\r
879         var pos = this.getScrollPos(), area = this.getScrollArea();\r
880         var left = Ext.fly(el).getOffsetsTo(this.stripWrap)[0] + pos;\r
881         var right = left + el.offsetWidth;\r
882         if(left < pos){\r
883             this.scrollTo(left, animate);\r
884         }else if(right > (pos + area)){\r
885             this.scrollTo(right - area, animate);\r
886         }\r
887     },\r
888 \r
889     // private\r
890     scrollTo : function(pos, animate){\r
891         this.stripWrap.scrollTo('left', pos, animate ? this.getScrollAnim() : false);\r
892         if(!animate){\r
893             this.updateScrollButtons();\r
894         }\r
895     },\r
896 \r
897     onWheel : function(e){\r
898         var d = e.getWheelDelta()*this.wheelIncrement*-1;\r
899         e.stopEvent();\r
900 \r
901         var pos = this.getScrollPos();\r
902         var newpos = pos + d;\r
903         var sw = this.getScrollWidth()-this.getScrollArea();\r
904 \r
905         var s = Math.max(0, Math.min(sw, newpos));\r
906         if(s != pos){\r
907             this.scrollTo(s, false);\r
908         }\r
909     },\r
910 \r
911     // private\r
912     onScrollRight : function(){\r
913         var sw = this.getScrollWidth()-this.getScrollArea();\r
914         var pos = this.getScrollPos();\r
915         var s = Math.min(sw, pos + this.getScrollIncrement());\r
916         if(s != pos){\r
917             this.scrollTo(s, this.animScroll);\r
918         }\r
919     },\r
920 \r
921     // private\r
922     onScrollLeft : function(){\r
923         var pos = this.getScrollPos();\r
924         var s = Math.max(0, pos - this.getScrollIncrement());\r
925         if(s != pos){\r
926             this.scrollTo(s, this.animScroll);\r
927         }\r
928     },\r
929 \r
930     // private\r
931     updateScrollButtons : function(){\r
932         var pos = this.getScrollPos();\r
933         this.scrollLeft[pos == 0 ? 'addClass' : 'removeClass']('x-tab-scroller-left-disabled');\r
934         this.scrollRight[pos >= (this.getScrollWidth()-this.getScrollArea()) ? 'addClass' : 'removeClass']('x-tab-scroller-right-disabled');\r
935     },\r
936 \r
937     // private\r
938     beforeDestroy : function() {\r
939         if(this.items){\r
940             this.items.each(function(item){\r
941                 if(item && item.tabEl){\r
942                     Ext.get(item.tabEl).removeAllListeners();\r
943                     item.tabEl = null;\r
944                 }\r
945             }, this);\r
946         }\r
947         if(this.strip){\r
948             this.strip.removeAllListeners();\r
949         }\r
950         Ext.TabPanel.superclass.beforeDestroy.apply(this);\r
951     }\r
952 \r
953     /**\r
954      * @cfg {Boolean} collapsible\r
955      * @hide\r
956      */\r
957     /**\r
958      * @cfg {String} header\r
959      * @hide\r
960      */\r
961     /**\r
962      * @cfg {Boolean} headerAsText\r
963      * @hide\r
964      */\r
965     /**\r
966      * @property header\r
967      * @hide\r
968      */\r
969     /**\r
970      * @property title\r
971      * @hide\r
972      */\r
973     /**\r
974      * @cfg {Array} tools\r
975      * @hide\r
976      */\r
977     /**\r
978      * @cfg {Boolean} hideCollapseTool\r
979      * @hide\r
980      */\r
981     /**\r
982      * @cfg {Boolean} titleCollapse\r
983      * @hide\r
984      */\r
985     /**\r
986      * @cfg {Boolean} collapsed\r
987      * @hide\r
988      */\r
989     /**\r
990      * @cfg {String} layout\r
991      * @hide\r
992      */\r
993     /**\r
994      * @cfg {Object} layoutConfig\r
995      * @hide\r
996      */\r
997 \r
998 });\r
999 Ext.reg('tabpanel', Ext.TabPanel);\r
1000 \r
1001 /**\r
1002  * Sets the specified tab as the active tab. This method fires the {@link #beforetabchange} event which\r
1003  * can return false to cancel the tab change.\r
1004  * @param {String/Panel} tab The id or tab Panel to activate\r
1005  * @method activate\r
1006  */\r
1007 Ext.TabPanel.prototype.activate = Ext.TabPanel.prototype.setActiveTab;\r
1008 \r
1009 // private utility class used by TabPanel\r
1010 Ext.TabPanel.AccessStack = function(){\r
1011     var items = [];\r
1012     return {\r
1013         add : function(item){\r
1014             items.push(item);\r
1015             if(items.length > 10){\r
1016                 items.shift();\r
1017             }\r
1018         },\r
1019 \r
1020         remove : function(item){\r
1021             var s = [];\r
1022             for(var i = 0, len = items.length; i < len; i++) {\r
1023                 if(items[i] != item){\r
1024                     s.push(items[i]);\r
1025                 }\r
1026             }\r
1027             items = s;\r
1028         },\r
1029 \r
1030         next : function(){\r
1031             return items.pop();\r
1032         }\r
1033     };\r
1034 };\r
1035 \r