PDA

View Full Version : Tab Panel scroller menu



jay@moduscreate.com
16 Jan 2009, 6:40 AM
This is from my blog (http://tdg-i.com/59/how-to-add-a-tab-scroller-menu) post:



I was writing about the limitations Tab Display Interface (TabPanel) and I wondered how we could make it better. It dawned on me that we should be able to easily add a menu to the right of the scroll-right buttons. So, I launched photoshop, and modified the existing scroll-right menu sprite (enlarged):
http://tdg-i.com/js/examples/ext/tdgiux/tabScrollerMenu/tabmenu.gif
Next, I needed to come up with a solution on how to inject that sprite into the tab panels’ stab strip. I started by creating a bunch of CSS overrides and Ext.TabPanel overrides. This clearly was not the best solution, as I thought it would be best to give developers the choice on whether they want to use it or not. I then transformed things into an extension. That didn’t work well because the CSS overrides were still in place. So, I decided to transform it again into a plugin, and this is where it really shines!


http://tdg-i.com/img/screencasts/2009-01-16_0931.png



Download: zip (http://tdg-i.com/js/examples/ext/tdgiux/tabScrollerMenu/tabScrollerMenu.zip) (contains related CSS and images);

Example page: http://tdg-i.com/js/examples/ext/tdgiux/tabScrollerMenu/

******* update 16:03 1/16 *****
http://tdg-i.com/img/screencasts/2009-01-16_1559.png
see: http://extjs.com/forum/showthread.php?p=274776#post274776

Condor
16 Jan 2009, 7:03 AM
Too bad the company firewall doesn't allow access to tdg-i.com, so I'll have to wait to check this out at home...

Just a note:
Ext 3.0 also contains menu support for toolbars with too many buttons (maybe you can match the style).

jay@moduscreate.com
16 Jan 2009, 7:09 AM
Really? :( that bites.

i'll look closer at the ext 3 toolbar menu. I've attached the plugin for your convenience.

Condor
16 Jan 2009, 7:31 AM
Nice!

One problem though:
I would have expected to use this feature in combination with enableTabScroll:false and resizeTabs:false (= the way Eclipse shows too many tabs), but that doesn't work. Maybe you should replace createScrollers instead of creating a sequence and only optionally render the left and right scroller buttons.

Also, some small requests:
1. Could you also use the iconCls of the tabs in the menuitems?
2. Could you make the submenu size (currently 10) configurable?

jay@moduscreate.com
16 Jan 2009, 7:37 AM
Those are great suggestions. I'll integrate them :)

Thanks for the valuable feedback, as always, Condor! :D

edspencer
16 Jan 2009, 9:21 AM
This looks great. It's always interesting to see other people's coding style too.

I second Condor's suggestion about page size. Would be awesome to have a search filter too ;)

Animal
16 Jan 2009, 9:57 AM
Very nice. I think the API docs need this. What's the licence on it? Could it be added to Ext?

mystix
16 Jan 2009, 10:55 AM
Neat as usual. :)

Might I suggest adding ellipses to the truncated tab titles?

jay@moduscreate.com
16 Jan 2009, 12:53 PM
Updated with the following items:
Tab scroller menu now accepts a config parameter:

+ maxText // elipsis, thanks mystix
+ pageSize // custom page size , thanks condor

New features/bug fixes:
+ iconCls now transferred from tab to menu
+ strip-wrap CSS override nolonger required
+ menu automatically hides and shows with the scroll left button hide call.
+ getter and setter methods for maxText and pageSize
Example usage:
[img]


var scrollerMenu = new Ext.plugins.TDGi.tabScrollerMenu({
maxText : 15,
pageSize : 5
});

new Ext.Window({
height : 400,
width : 400,
layout : 'fit',
title : 'Exercising scrollable tabs with a tabscroller menu',
items : {
xtype : 'tabpanel',
activeTab : 0,
id : 'myTPanel',
enableTabScroll : true,
resizeTabs : true,
minTabWidth : 75,
plugins : [ scrollerMenu ],
items : [
{
title : 'our first tab'
}
]
}
}).show();

// Add a bunch of tabs dynamically
var tabLimit = 20;
(function (num) {
for (var i = 1; i <= tabLimit; i++) {
var title = 'Long Title Tab # ' + i;
Ext.getCmp('myTPanel').add({
title : title,
html : 'Hi, i am tab ' + i,
tabTip : title,
closable : true
});
}
}).defer(1000);

jay@moduscreate.com
16 Jan 2009, 12:54 PM
Very nice. I think the API docs need this. What's the licence on it? Could it be added to Ext?

Nige, it's free. It would be cool to have this added to the docs. It would be cool also if i was credited.

do i need to sign release paperwork for this though?

edspencer
16 Jan 2009, 3:56 PM
This is such a great idea I had to give it a go myself - here's my own humble effort: http://extjs.edspencer.net/tabscrollermenu/.

I've done exactly the same thing, except I've subclassed Ext.menu.Menu directly instead of creating a menu from inside the plugin. I've also added a little search filter so you can filter through the tabs if you like. Based on the pageSize it'll decide whether to place the items in the top menu level or paginate them with submenus (filtering knows how to deal with either possibility).

This was done under the influence of a considerable amount of alcohol so please excuse me if the code (http://extjs.edspencer.net/tabscrollermenu/javascripts/TabScrollerMenu.js) is not up to scratch! As usual, all of the code is up on Github. (http://github.com/edspencer/extjs-tabscrollermenu)

Like I said before it's really good to see how other people approach problems, so hopefully my version will be of some limited use in that area. I agree that this kind of thing would be great in the API docs, though I've got no idea about the paperwork you'd need for that Jay (if any). It would also be great to have the API app preserve your tab state - if I had a dollar for every time I refreshed the API docs by mistake... well, I'd be quite rich :)

edspencer
17 Jan 2009, 3:17 AM
Ok search filtering makes a lot more sense as a top level menu item - check it out (http://extjs.edspencer.net/tabscrollermenu/) (you may need to force refresh).

Cool

Animal
18 Jan 2009, 1:57 AM
I agree on making the API docs preserve tab state. I often refresh the page.

Also, integrating Ext history support would be good. Back and forward would switch tabs.

The Ext API docs should be a kind of "reference implementation" and exhibit all the best usage practices of Ext.

edspencer
18 Jan 2009, 8:25 AM
** UPDATE ** I moved this into its own thread (http://extjs.com/forum/showthread.php?t=57616).


I agree on making the API docs preserve tab state. I often refresh the page.

Also, integrating Ext history support would be good. Back and forward would switch tabs.

We were saying the same thing in the office a few days ago... good thing I was bored this afternoon: http://extjs.edspencer.net/extjs/docs.

With a few updates it's now saving tab state (open a few tabs, hit refresh, smile :) ), and uses History as well. When you refresh it'll add your tabs back in using autoLoad so that it doesn't hit the server for each tab every time you refresh.

I've also set up a custom build of ExtJS, which I think is about as slim as it can be for this application. It's coming off the Cachefly CDN now. Finally, I've tidied index.html a little and made it validate.

Oh yeah, I also made it backwards-compatible with the current direct links, so http://extjs.edspencer.net/extjs/docs/?class=Ext.Element is automatically parsed and redirected to http://extjs.edspencer.net/extjs/docs/#class=Ext.Element.


The Ext API docs should be a kind of "reference implementation" and exhibit all the best usage practices of Ext.

That would be great. I think it's currently in need of a little TLC to get it to that stage... I've chopped up the previously monolithic application file into one file per class, and tidied up some of the indentation/added a few comments. Definitely needs a bit more work though. Those files are concatenated and minified into 1 file in production, which comes to 13kb.

You can see a more development-friendly version at http://extjs.edspencer.net/extjs/docs/indexdev.html, which is using ext-all-debug and loads each class file individually, without minification.

Full source available at http://github.com/edspencer/extjs-docs. That's the full build of ExtJS, minus the examples and other unneeded dirs.

rdougan
18 Jan 2009, 10:12 AM
Great work edspencer- very useful.

tobiu
24 Apr 2009, 8:53 AM
hi jay and edspencer,

today i found some time to play around with this ux.
i really like it!

while testing, i noticed that nobody has thought, that tabPanels with tabPosition:'bottom' do exist.
so i fixed this.

i'm not sure, which version to use, so i took the one of j included in the ext3.0-rc examples.
the changes are marked red (feel free to copy them).



/*
* Ext JS Library 3.0 RC1
* Copyright(c) 2006-2009, Ext JS, LLC.
* licensing@extjs.com
*
* http://extjs.com/license
*/



Ext.ux.TabScrollerMenu = Ext.extend(Object, {
pageSize : 10,
maxText : 15,
menuPrefixText : 'Items',
constructor : function(config) {
config = config || {};
Ext.apply(this, config);
},
init : function(tabPanel) {
Ext.apply(tabPanel, this.tabPanelMethods);

tabPanel.tabScrollerMenu = this;
var thisRef = this;

tabPanel.on({
render : {
scope : tabPanel,
single : true,
fn : function() {
var newFn = tabPanel.createScrollers.createSequence(thisRef.createPanelsMenu, this);
tabPanel.createScrollers = newFn;
}
}
});
},
// private && sequeneced
createPanelsMenu : function() {
var h = this.stripWrap.dom.offsetHeight;

//move the right menu item to the left 18px

if(this.tabPosition == 'bottom'){
var topBottom = this.footer;
} else {
var topBottom = this.header;
}
var rtScrBtn = topBottom.dom.firstChild;
Ext.fly(rtScrBtn).applyStyles({
right : '18px'
});

var stripWrap = Ext.get(this.strip.dom.parentNode);
stripWrap.applyStyles({
'margin-right' : '36px'
});

// Add the new righthand menu
var scrollMenu = topBottom.insertFirst({
cls:'x-tab-tabmenu-right'
});
scrollMenu.setHeight(h);
scrollMenu.addClassOnOver('x-tab-tabmenu-over');
scrollMenu.on('click', this.showTabsMenu, this);

this.scrollLeft.show = this.scrollLeft.show.createSequence(function() {
scrollMenu.show();
});

this.scrollLeft.hide = this.scrollLeft.hide.createSequence(function() {
scrollMenu.hide();
});

},
// public
getPageSize : function() {
return this.pageSize;
},
// public
setPageSize : function(pageSize) {
this.pageSize = pageSize;
},
// public
getMaxText : function() {
return this.maxText;
},
// public
setMaxText : function(t) {
this.maxText = t;
},
getMenuPrefixText : function() {
return this.menuPrefixText;
},
setMenuPrefixText : function(t) {
this.menuPrefixText = t;
},
// private && applied to the tab panel itself.
tabPanelMethods : {
// all execute within the scope of the tab panel
// private
showTabsMenu : function(e) {
if (! this.tabsMenu) {
this.tabsMenu = new Ext.menu.Menu();
this.on('beforedestroy', this.tabsMenu.destroy, this.tabsMenu);
}

this.tabsMenu.removeAll();

this.generateTabMenuItems();

var target = Ext.get(e.getTarget());
var xy = target.getXY();

//Y param + 24 pixels
xy[1] += 24;

this.tabsMenu.showAt(xy);
},
// private
generateTabMenuItems : function() {
var curActive = this.getActiveTab();
var totalItems = this.items.getCount();
var pageSize = this.tabScrollerMenu.getPageSize();


if (totalItems > pageSize) {
var numSubMenus = Math.floor(totalItems / pageSize);
var remainder = totalItems % pageSize;

// Loop through all of the items and create submenus in chunks of 10
for (var i = 0 ; i < numSubMenus; i++) {
var curPage = (i + 1) * pageSize;
var menuItems = [];


for (var x = 0; x < pageSize; x++) {
index = x + curPage - pageSize;
var item = this.items.get(index);
menuItems.push(this.autoGenMenuItem(item));
}

this.tabsMenu.add({
text : this.tabScrollerMenu.getMenuPrefixText() + ' ' + (curPage - pageSize + 1) + ' - ' + curPage,
menu : menuItems
});

}
// remaining items
if (remainder > 0) {
var start = numSubMenus * pageSize;
menuItems = [];
for (var i = start ; i < totalItems; i ++ ) {
var item = this.items.get(i);
menuItems.push(this.autoGenMenuItem(item));
}


this.tabsMenu.add({
text : this.tabScrollerMenu.menuPrefixText + ' ' + (start + 1) + ' - ' + (start + menuItems.length),
menu : menuItems
});


}
}
else {
this.items.each(function(item) {
if (item.id != curActive.id && ! item.hidden) {
menuItems.push(this.autoGenMenuItem(item));
}
}, this);
}
},
// private
autoGenMenuItem : function(item) {
var maxText = this.tabScrollerMenu.getMaxText();
var text = Ext.util.Format.stripTags(item.title);
text = Ext.util.Format.ellipsis(text, maxText);

return {
text : text,
handler : this.showTabFromMenu,
scope : this,
disabled : item.disabled,
tabToShow : item,
iconCls : item.iconCls
}

},
// private
showTabFromMenu : function(menuItem) {
this.setActiveTab(menuItem.tabToShow);
}
}
});



there is another thing i noticed, but can not exactly tell you when / why it happens.
sometimes, when you scroll to the right end of tabs, the last tab is not display for the full tab-width, exactly 18px to small. i post a screen in my next answer.

kind regards, tobiu

tobiu
24 Apr 2009, 8:58 AM
so, here is the screen.
i played around with resizing the browser-window and adding tabs / scrolling.

jay@moduscreate.com
24 Apr 2009, 8:58 AM
Thanks. i never use bottom positioned tabs ;)

jay@moduscreate.com
24 Apr 2009, 8:58 AM
so, here is the screen.
i played around with resizing the browser-window and adding tabs / scrolling.

where?

tobiu
24 Apr 2009, 9:01 AM
hi j,

that was fast ;)
the screen is an attached thumbnail under my last answer.

jay@moduscreate.com
24 Apr 2009, 9:05 AM
Strange.

go to:http://tdg-i.com/js/examples/ext/tdgiux/tabScrollerMenu/

and paste the following in your firebug console. does it happen there?



var scrollerMenu = new Ext.plugins.TDGi.tabScrollerMenu({
maxText : 15,
pageSize : 5
});
new Ext.Window({
height : 400,
width : 400,
layout : 'fit',
title : 'Exercising scrollable tabs with a tabscroller menu',
items : {
xtype : 'tabpanel',
activeTab : 0,
id : 'myTPanel',
enableTabScroll : true,
//resizeTabs : true,
minTabWidth : 75,
plugins : [ scrollerMenu ],
items : [
{
title : 'our first tab'
}
]
}
}).show();

// Add a bunch of tabs dynamically
var tabLimit = 20;
(function (num) {
for (var i = 1; i <= tabLimit; i++) {
var title = 'Long Title Tab # ' + i;
Ext.getCmp('myTPanel').add({
title : title,
html : 'Hi, i am tab ' + i,
tabTip : title,
closable : true
});
}
}).defer(1000);

tobiu
24 Apr 2009, 9:15 AM
i will test a bit more,
but it will need time.

i found a case, where it always happens with my app:
create a tabpanel, where the tabs do fit (no left-right arrows are displayed).
then resize the browser window, so that the scrolling arrows and the menu icon get displayed. then scroll to the right and you should be able to see it.

this only happens the first time(!) you resize your window.
from the second time on, scolling to the right end works as intended.

kind regards, tobiu

tobiu
24 Apr 2009, 9:36 AM
hi jay,

i am able to reproduce it in your case,
if we modify it a bit:



Ext.onReady(function(){
new Ext.Window({
height : 400,
width : 400,
layout : 'fit',
title : 'Exercising scrollable tabs with a tabscroller menu',
items : {
xtype : 'tabpanel',
activeTab : 0,
id : 'myTPanel',
enableTabScroll : true,
//resizeTabs : true,
minTabWidth : 75,
plugins : [new Ext.plugins.TDGi.tabScrollerMenu()],
items : [{
title : 'our first tab'
}]
}
}).show();

// Add a bunch of tabs dynamically
var tabLimit = 3;
(function (num) {
for (var i = 1; i <= tabLimit; i++) {
var title = 'Long Title Tab # ' + i;
Ext.getCmp('myTPanel').add({
title : title,
html : 'Hi, i am tab ' + i,
tabTip : title,
closable : true
});
}
}).defer(1000);
});


so, scrolling to the right will give us the attached screenshot.

there is another thing: clicking on the menu throws a firebug-error:

menuItems is undefined
menuItems.push(this.autoGenMenuItem(item));


kind regards, tobiu

jay@moduscreate.com
24 Apr 2009, 9:38 AM
Awesome find. i'll have to do some investigation.

Thanks :)

tobiu
24 Apr 2009, 9:43 AM
funny,

in this case it is basically the same.
if you resize the ext-window 1+ times, after that it works nice.

kind regards, tobiu

tobiu
24 Apr 2009, 10:12 AM
hi jay,

i can explain the firebug-error a bit more detailed:

we have 4 menu-items.
pageSize is set to 10.

so, on construction we have an if-clause:



if (totalItems > pageSize) { //no
var numSubMenus = Math.floor(totalItems / pageSize);
var remainder = totalItems % pageSize;

// Loop through all of the items and create submenus in chunks of 10
for (var i = 0 ; i < numSubMenus; i++) {
var curPage = (i + 1) * pageSize;
var menuItems = [];


for (var x = 0; x < pageSize; x++) {
index = x + curPage - pageSize;
var item = this.items.get(index);
console.log(this.items.get(index));
menuItems.push(this.autoGenMenuItem(item));
}

this.tabsMenu.add({
text : 'items ' + (curPage - pageSize + 1) + ' - ' + curPage,
menu : menuItems
});

}
// remaining items
if (remainder > 0) {
var start = numSubMenus * pageSize;
menuItems = [];
for (var i = start ; i < totalItems; i ++ ) {
var item = this.items.get(i);
console.log(i);
menuItems.push(this.autoGenMenuItem(item));

}
this.tabsMenu.add({
text : 'items ' + start + ' - rest',
menu : menuItems
});
}
}
else {
this.items.each(function(item) {
if (item.id != curActive.id && ! item.hidden) {
console.log("here comes the error");
menuItems.push(this.autoGenMenuItem(item));
}
}, this);
}


the else-part does not create the array.

tobiu
24 Apr 2009, 10:44 AM
here is my next modification for the "else" part mentioned in my last posting:



else {
this.items.each(function(item) {
if (item.id != curActive.id /*&& ! item.hidden*/) {
this.tabsMenu.add(this.autoGenMenuItem(item));
}
}, this);
}


the "hidden" attribute is quite confusing.
if we create the demo-case and show the menu (with && ! item.hidden ) not commented out, the the menu shows 3 items: all except the active tab. tabs that have not gotten activated DO have the value hidden = false. after activating the next tab, the menu has only 2 items, after clicking the 3rd tab the menu has 1 item.

so, i think we need an attribute for each tab, that shows us, if the tab strip is display or not. like



stripVisible : [boolean]


kind regards, tobiu

tobiu
24 Apr 2009, 11:56 AM
hmm,

we have 2 options:

1) combine the versions of j and egbert.
2) make 2 different topics.

if not, we will run into chaos =)

@egbert:



init: function(tabPanel) {
this.tabPanel = tabPanel;
this.createMenuItems();

var menuRef = this;

//creates the menu trigger element in the TabPanel header
this.tabPanel.createScrollers = this.tabPanel.createScrollers.createSequence(function() {
//adjust containing elements to make room for new menu trigger
Ext.fly(this.header.child('.x-tab-scroller-right')).applyStyles({right: '18px'});
Ext.get(this.strip.dom.parentNode).applyStyles({'margin-right': '36px'});

//create the menu trigger
this.tabMenuTrigger = this.header.insertFirst({cls: "x-tab-panel-menu"});
this.tabMenuTrigger.setHeight(this.stripWrap.dom.offsetHeight);
this.tabMenuTrigger.on('click', function() { menuRef.show(this.tabMenuTrigger); }, this);
this.tabMenuTrigger.addClassOnOver('x-tab-panel-menu-over');

//update the menu when TabPanel items change
this.tabPanel.on('add', menuRef.createMenuItems, menuRef);
this.tabPanel.on('remove', menuRef.createMenuItems, menuRef);
});
},


i changed it to:



init: function(tabPanel) {
this.tabPanel = tabPanel;
this.createMenuItems();

var menuRef = this;

//creates the menu trigger element in the TabPanel header
this.tabPanel.createScrollers = this.tabPanel.createScrollers.createSequence(function() {
//adjust containing elements to make room for new menu trigger
Ext.fly(this.header.child('.x-tab-scroller-right')).applyStyles({right: '18px'});
Ext.get(this.strip.dom.parentNode).applyStyles({'margin-right': '36px'});

//create the menu trigger
this.tabMenuTrigger = this.header.insertFirst({cls: "x-tab-panel-menu"});
this.tabMenuTrigger.setHeight(this.stripWrap.dom.offsetHeight);
this.tabMenuTrigger.on('click', function() { menuRef.show(this.tabMenuTrigger); }, this);
this.tabMenuTrigger.addClassOnOver('x-tab-panel-menu-over');
});

//update the menu when TabPanel items change
this.tabPanel.on('add', menuRef.createMenuItems, menuRef);
this.tabPanel.on('remove', menuRef.createMenuItems, menuRef);
},


;)

kind regards, tobiu

tobiu
24 Apr 2009, 12:21 PM
hmm,

i think i have my bug =)

at least i know why it is here.

screen1 is before window resizing,
screen2 after.

the yellow blocks are the margins, displayed via firebug

kind regards, tobiu

tobiu
24 Apr 2009, 12:36 PM
ok, for both of your extensions.

@egbert:



init: function(tabPanel) {
this.tabPanel = tabPanel;
this.createMenuItems();

var menuRef = this;

//creates the menu trigger element in the TabPanel header / footer
this.tabPanel.createScrollers = this.tabPanel.createScrollers.createSequence(function() {

var pos = this.tabPosition == 'bottom' ? 'footer' : 'header';

//adjust containing elements to make room for new menu trigger
Ext.fly(this[pos].child('.x-tab-scroller-right')).applyStyles({right: '18px'});
var stripWidth = parseInt(this.strip.dom.parentNode.style.width) - 18;
Ext.get(this.strip.dom.parentNode).applyStyles({'margin-right': '36px', 'width': stripWidth + 'px'});

//create the menu trigger
this.tabMenuTrigger = this[pos].insertFirst({cls: "x-tab-panel-menu"});
this.tabMenuTrigger.setHeight(this.stripWrap.dom.offsetHeight);
this.tabMenuTrigger.on('click', function() { menuRef.show(this.tabMenuTrigger); }, this);
this.tabMenuTrigger.addClassOnOver('x-tab-panel-menu-over');
});

//update the menu when TabPanel items change
this.tabPanel.on('add', menuRef.createMenuItems, menuRef);
this.tabPanel.on('remove', menuRef.createMenuItems, menuRef);
}


@jay:



createPanelsMenu : function() {
var h = this.stripWrap.dom.offsetHeight;
var pos = this.tabPosition == 'bottom' ? 'footer' : 'header';

//move the right menu item to the left 18px
var rtScrBtn = this[pos].dom.firstChild;
Ext.fly(rtScrBtn).applyStyles({
right : '18px'
});

var stripWidth = parseInt(this.strip.dom.parentNode.style.width) - 18;
var stripWrap = Ext.get(this.strip.dom.parentNode);
stripWrap.applyStyles({
'margin-right' : '36px',
'width' : stripWidth + 'px'
});

// Add the new righthand menu
var scrollMenu = this[pos].insertFirst({
cls:'x-tab-tabmenu-right'
});
scrollMenu.setHeight(h);
scrollMenu.addClassOnOver('x-tab-tabmenu-over');
scrollMenu.on('click', this.showTabsMenu, this);

this.scrollLeft.show = this.scrollLeft.show.createSequence(function() {
scrollMenu.show();
});

this.scrollLeft.hide = this.scrollLeft.hide.createSequence(function() {
scrollMenu.hide();
});
}


kind regards, tobiu

tobiu
24 Apr 2009, 1:51 PM
[edit]: 27.4.09: new version

so, here is my "final" version,

a combination of edspencer, jay garcia, parts of the ext3.0rc1 and some own parts.

@ext-team: in case, the others authors are on the same opinion, feel free to use and / or modify this code, if you like it.

kind regards, tobiu



Ext.ns('Ext.ux');

/**
* @class Ext.ux.TabScrollerMenu
* @extends Ext.menu.Menu
* @author
* Jay Garcia (http://tdg-i.com/59/how-to-add-a-tab-scroller-menu),
* Ed Spencer (http://edspencer.net),
* Tobias Uhlig (info@internetsachen.com)
* ExtJS - Team (who did the modified version of 3.0rc1?)
*/
Ext.ux.TabScrollerMenu = Ext.extend(Ext.menu.Menu, {

/**
* @property pageSize
* @type int
* The number of tab links to show per submenu
*/
pageSize : 10,

/**
* @property maxTextLength
* @type int
* The maximum length of submenu text to display before truncation
*/
maxTextLength : 30,
menuPrefixText : 'Tabs',

/**
* @property hasFilter
* @type Boolean
* True to include an optional filter textbox which removes any non-matching menu items
* Needs Ext.ux.menu.TextFilterItem
*/
hasFilter : false,
filterEmptyText : "filter tabs...",

/**
* Sets up plugin, creates a clickable element to trigger this menu to be displayed
* @param {Ext.TabPanel} tabPanel The TabPanel to attach this plugin to
*/
init: function(tabPanel) {
this.tabPanel = tabPanel;
this.createMenuItems();

var menuRef = this;

//creates the menu trigger element in the TabPanel header / footer
this.tabPanel.createScrollers = this.tabPanel.createScrollers.createSequence(function() {

var pos = this.tabPosition == 'bottom' ? 'footer' : 'header';

//adjust containing elements to make room for new menu trigger
Ext.fly(this[pos].child('.x-tab-scroller-right')).applyStyles({right: '18px'});

var stripWidth = parseInt(this.strip.dom.parentNode.style.width) - 18;
Ext.get(this.strip.dom.parentNode).applyStyles({
'margin-right': '36px',
'width' : stripWidth + 'px'
});

//create the menu trigger
var tabMenuTrigger = this[pos].insertFirst({cls: "x-tab-panel-menu"});
if(pos == 'footer')tabMenuTrigger.applyStyles({'margin-top': '1px'});
tabMenuTrigger.setHeight(this.stripWrap.dom.offsetHeight);
tabMenuTrigger.on('click', function() { menuRef.show(tabMenuTrigger); }, this);
tabMenuTrigger.addClassOnOver('x-tab-panel-menu-over');

this.scrollLeft.show = this.scrollLeft.show.createSequence(function() {
tabMenuTrigger.show();
});

this.scrollLeft.hide = this.scrollLeft.hide.createSequence(function() {
tabMenuTrigger.hide();
});
});

this.tabPanel.on('add', menuRef.createMenuItems, menuRef);
this.tabPanel.on('remove', menuRef.createMenuItems, menuRef);
},

/**
* Creates a menu item for each tab in the TabPanel (paginated if there are more than
* the requested page size). Optionally removes current menu items first
* @param {Boolean} clearExisting True to remove all current menu items first (defaults to true)
*/
createMenuItems: function(clearExisting) {
var clearExisting = clearExisting ? clearExisting : true;
if (clearExisting) { this.removeAll(); }
this.createFilterMenu();

var numberOfItems = this.tabPanel.items.length;

if (numberOfItems > this.pageSize) {
var numberOfPages = Math.ceil(numberOfItems / this.pageSize);

//create each submenu
for (var i=0; i < numberOfPages; i++) {
var subMenuItems = [];

//create each submenu item
for (var j = 0; j <= this.pageSize - 1; j++){
var currentItem = this.tabPanel.items.items[j + (i * this.pageSize)];
if (currentItem) {
subMenuItems.push(this.createSubMenuItem(currentItem));
};
};

//calculate text label for this submenu
var lowerNumber = 1 + (this.pageSize * i);
var higherNumber = Math.min(((i + 1) * this.pageSize), numberOfItems);
var subMenuText = String.format(this.menuPrefixText + ' {0} - {1}', lowerNumber, higherNumber);

this.addMenuItem({text: subMenuText, menu: subMenuItems, iconCls: 'many'});
}
} else {
//can put all items in the same 'page'
this.tabPanel.items.each(function(item) {
this.addMenuItem(this.createSubMenuItem(item));
}, this);
}
},

/**
* Returns a config object for a tab, suitable for placement inside a submenu
* @param {Object} panel The panel instance to create this submenu item from
* @param function handler The handler of the top-Level menu item
* @return {Object} An object suitable for addition to a submenu
*/
createSubMenuItem: function(panel, handler) {
var text = Ext.util.Format.stripTags(panel.title);
var menuHandler = handler ? handler : this.tabPanel.setActiveTab.createDelegate(this.tabPanel, [panel]);

var menuItem = {
disabled : panel.disabled
,iconCls : panel.iconCls
,handler : menuHandler
,rawText : panel.title
,scope : this
,text : Ext.util.Format.ellipsis(text, this.maxTextLength)
};

if(panel.tabPosition){
var parentMenu = this;

if(!panel.parentHandler){
panel.on({
add : function(){parentMenu.createMenuItems();}
,remove : function(){parentMenu.createMenuItems();}
});
}
panel.parentHandler = true;

var subMenuItems = [];
panel.items.each(function(item) {
var subHandler = menuHandler.createSequence(item.ownerCt.setActiveTab.createDelegate(item.ownerCt, [item]));
subMenuItems.push(this.createSubMenuItem(item, subHandler));
}, this);
menuItem.menu = subMenuItems;
}
return menuItem;
},

// public
getPageSize : function() {
return this.pageSize;
},
// public
setPageSize : function(pageSize) {
this.pageSize = pageSize;
},
// public
getMaxTextLength : function() {
return this.maxTextLength
},
// public
setMaxTextLength : function(maxText) {
this.maxText = maxTextLength;
},
// public
getMenuPrefixText : function() {
return this.menuPrefixText;
},
// public
setMenuPrefixText : function(t) {
this.menuPrefixText = t;
},

/**
* Adds a filter menu item with a TextField if this.hasFilter is true
* @return {Ext.menu.MenuItem/Null} The filter menu item
*/
createFilterMenu: function() {
if (this.hasFilter) {
this.filterMenu = this.addItem(
new Ext.ux.menu.TextFilterItem({
name : 'filter',
emptyText : this.filterEmptyText,
listeners: {
keyup: {
scope: this,
fn: function(e, input) {
if(e.getKey() == 40){ // down
var m = this;
if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){
m.tryActivate(0, 1);
}
} else {
this.filterItems(input.value);
}
}
}
}
})
);
return this.filterMenu;
}
},

/**
* Iterates over each submenu item, hiding it if it does not match the filter text
* Also hides the menu item itself if all submenu items are hidden
*/
filterItems: function(filterText) {
var filterRegex = new RegExp(filterText, ['i']);

this.items.each(function(topMenu) {
//don't filter on the filterMenu itself...
if (topMenu != this.filterMenu) {
if (topMenu.menu) {
var hideMenu = true;

//if we have submenu items, iterate over each and hide if necessary
topMenu.menu.items.each(function(subMenuItem) {
if (filterRegex.test(subMenuItem.rawText)) {
subMenuItem.show();
hideMenu = false;
} else
subMenuItem.hide();
});

//if we've hidden everything, hide the whole menu
hideMenu ? topMenu.hide() : topMenu.show();
} else {
//if we don't have submenu items, iterate over top level menus and hide if necessary
filterRegex.test(topMenu.rawText) ? topMenu.show() : topMenu.hide();
}
}
}, this);
this.getEl().sync();
}
});


css:



.x-tab-panel-menu {
background : transparent url(../ext-2.2.1/ux/tabScrollerMenu/tab-scroller-menu.gif) no-repeat 0 0;
border-bottom : 1px solid #8db2e3;
cursor : pointer;
float : right;
position : absolute;
right : 0;
top : 0;
width : 18px;
z-index : 10;
}

.x-tab-panel-menu-over {
background-position : -18px 0;
}

.x-tab-filter {
padding : 1px;
}

.x-tab-filter-empty {
color : #aaa;
font-style : italic;
}

tobiu
24 Apr 2009, 2:34 PM
@edspencer:

i added



this.getEl().sync();


to the filterItems-method. this makes it possible to use menues with shadows.

kind regards, tobiu

tobiu
24 Apr 2009, 2:49 PM
added a config:


filterEmptyText


to specify the filter-text directly.

tobiu
24 Apr 2009, 3:17 PM
2 more changes:

moved


var pos = this.tabPosition == 'bottom' ? 'footer' : 'header';


into the sequence.

added the disbabled-config from tabs to the menuItems like in the ext3.0-rc1 version.

kind regards, tobiu

tobiu
24 Apr 2009, 3:30 PM
added


if(pos == 'footer')tabMenuTrigger.applyStyles({'margin-top': '1px'});


because the image with tabPosition:'bottom' was not at the right position.

kind regards, tobiu

tobiu
25 Apr 2009, 11:00 AM
i changed the keyup-listener of the filterMenu a bit:




fn: function(e, input) {
if(e.getKey() == 40){ // down
var m = this;
if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){
m.tryActivate(0, 1);
}
} else {
this.filterItems(input.value);
}
}


is there a way, to let the key-event bubble to the parent-menu?
then i would not need to copy the keydown-event to select the next menuItem...

kind regards, tobiu

tobiu
26 Apr 2009, 2:43 PM
@jay and all the others interested in this:

i have another goodie for you =)

imagine the following case: you have a nested layout with a tabpanel containing other tabpanels. wouldn't it be nice, to show the child tabs as sumenu-items in the tab menu?

i build a recursive structure, chaining for tabpanels of any depth.
the handlers are modified to a sequence, activating the top-tab and then the tab of the next level until the target tab is reached.

here is the code:



/**
* Returns a config object for a tab, suitable for placement inside a submenu
* @param {Object} panel The panel instance to create this submenu item from
* @param function handler The handler of the top-Level menu item
* @return {Object} An object suitable for addition to a submenu
*/
createSubMenuItem: function(panel, handler) {
var text = Ext.util.Format.stripTags(panel.title);
var menuHandler = handler ? handler : this.tabPanel.setActiveTab.createDelegate(this.tabPanel, [panel]);

var menuItem = {
disabled : panel.disabled
,iconCls : panel.iconCls
,handler : menuHandler
,rawText : panel.title
,scope : this
,text : Ext.util.Format.ellipsis(text, this.maxTextLength)
};

if(panel.tabPosition){
var subMenuItems = [];
panel.items.each(function(item) {
var subHandler = menuHandler.createSequence(item.ownerCt.setActiveTab.createDelegate(item.ownerCt, [item]));
subMenuItems.push(this.createSubMenuItem(item, subHandler));
}, this);
menuItem.menu = subMenuItems;
}
return menuItem;
}


sreen attached (don't wory, only demo-customer-data)

kind regards, tobiu

TODO: child-panels will be displayed as one menu, not regarding how many items they have.
-> even if more than pageSize. we could modify the code to add submenues for child-tabs as well...
hmm, i dont need it right now, since i don't have that many sub-tabs ;)

edit: i updated this to my full code some postings above.

jay@moduscreate.com
26 Apr 2009, 3:08 PM
holy crap dude you are going to town on this dude. sorry i have not been online much, weekends are mostly family time ;)

tobiu
26 Apr 2009, 3:20 PM
i enjoyed having weekend too :P

tobiu
27 Apr 2009, 1:58 AM
i forgot about renewing the (topLevel) menu,
when adding or removig in child-panels.



/**
* Returns a config object for a tab, suitable for placement inside a submenu
* @param {Object} panel The panel instance to create this submenu item from
* @param function handler The handler of the top-Level menu item
* @return {Object} An object suitable for addition to a submenu
*/
createSubMenuItem: function(panel, handler) {
var text = Ext.util.Format.stripTags(panel.title);
var menuHandler = handler ? handler : this.tabPanel.setActiveTab.createDelegate(this.tabPanel, [panel]);

var menuItem = {
disabled : panel.disabled
,iconCls : panel.iconCls
,handler : menuHandler
,rawText : panel.title
,scope : this
,text : Ext.util.Format.ellipsis(text, this.maxTextLength)
};

if(panel.tabPosition){
var parentMenu = this;

if(!panel.parentHandler){
panel.on({
add : function(){parentMenu.createMenuItems();}
,remove : function(){parentMenu.createMenuItems();}
});
}
panel.parentHandler = true;

var subMenuItems = [];

panel.items.each(function(item) {
var subHandler = menuHandler.createSequence(item.ownerCt.setActiveTab.createDelegate(item.ownerCt, [item]));
subMenuItems.push(this.createSubMenuItem(item, subHandler));
}, this);
menuItem.menu = subMenuItems;
}
return menuItem;
}


i will insert it in my code-posting above.

kind regards, tobiu

tobiu
27 Apr 2009, 4:25 AM
since we have filtering of tabs and a way to display subTabs,
the triggerMenu is no longer only a scrollerMenu,
but can be used to faster navigate to subTabs in general.

so, it should be possible to show the menu always, even if the scrollers are not displayed.
i started with adding a config value


alwaysShowTrigger : true


and changed the code quite a bit again.
in the init-function, i extracted the creation of the trigger to the method createMenuTrigger(), since the trigger has to be created in 2 ways:

1) in this class (independend from the scrollerMenu)
2) as a sequence on scrollerCreation in the tabPanel-scope.

in general, it is already working, but i found some margin-problems again.
the problem is, that tabPanels do not have a method for removeScrollers.
so, when scrollers get hidden, the margin-right of the tabStrip wont fit and the menu might be over the right edge of the last tab.

the code also should be beautified a bit more.
i'm sorry, but i dont have more time for this right now.
if anyone would like to always show the menu, feel free to improve the code here.

so, this is a NOT STABLE version!!
the stable-one is some postings above this.

kind regards, tobiu



Ext.ns('Ext.ux');

/**
* @class Ext.ux.TabScrollerMenu
* @extends Ext.menu.Menu
* @author
* Jay Garcia (http://tdg-i.com/59/how-to-add-a-tab-scroller-menu),
* Ed Spencer (http://edspencer.net),
* Tobias Uhlig (info@internetsachen.com)
* ExtJS - Team (who did the modified version of 3.0rc1?)
*/
Ext.ux.TabScrollerMenu = Ext.extend(Ext.menu.Menu, {

/**
* @property alwaysShowTrigger
* @type boolean
* Show the trigger even if all tabs are displayed
*/
alwaysShowTrigger : true,

/**
* @property pageSize
* @type int
* The number of tab links to show per submenu
*/
pageSize : 10,

/**
* @property maxTextLength
* @type int
* The maximum length of submenu text to display before truncation
*/
maxTextLength : 30,
menuPrefixText : 'Tabs',

/**
* @property hasFilter
* @type Boolean
* True to include an optional filter textbox which removes any non-matching menu items
* Needs Ext.ux.menu.TextFilterItem
*/
hasFilter : true,
filterEmptyText : "Tabs filtern...",

/**
* Sets up plugin, creates a clickable element to trigger this menu to be displayed
* @param {Ext.TabPanel} tabPanel The TabPanel to attach this plugin to
*/
init: function(tabPanel) {
this.tabPanel = tabPanel;
this.createMenuItems();

var menuRef = this;

if(this.alwaysShowTrigger === true){
this.tabPanel.on('render', function() {
var tabMenuTrigger = menuRef.createMenuTrigger();
tabMenuTrigger.show();
});

} else {
//creates the menu trigger element in the TabPanel header / footer
this.tabPanel.createScrollers = this.tabPanel.createScrollers.createSequence(function() {

var tabMenuTrigger = menuRef.createMenuTrigger();

this.scrollLeft.show = this.scrollLeft.show.createSequence(function() {
tabMenuTrigger.show();
});

this.scrollLeft.hide = this.scrollLeft.hide.createSequence(function() {
tabMenuTrigger.hide();
});
});
}

this.tabPanel.createScrollers = this.tabPanel.createScrollers.createSequence(function() {
var pos = this.tabPosition == 'bottom' ? 'footer' : 'header';
Ext.fly(this[pos].child('.x-tab-scroller-right')).applyStyles({right: '18px'});

Ext.get(this.strip.dom.parentNode).applyStyles({
'margin-right': '36px'
});
});

this.tabPanel.on('add', menuRef.createMenuItems, menuRef);
this.tabPanel.on('remove', menuRef.createMenuItems, menuRef);
},

createMenuTrigger: function() {
var pos = this.tabPosition == 'bottom' ? 'footer' : 'header';

//create the menu trigger
var tabMenuTrigger = this.tabPanel[pos].insertFirst({cls: "x-tab-panel-menu"});
if(pos == 'footer')tabMenuTrigger.applyStyles({'margin-top': '1px'});
tabMenuTrigger.setHeight(this.tabPanel.stripWrap.dom.offsetHeight);
tabMenuTrigger.on('click', function() { this.show(tabMenuTrigger); }, this);
tabMenuTrigger.addClassOnOver('x-tab-panel-menu-over');

var stripWidth = parseInt(this.tabPanel.strip.dom.parentNode.style.width) - 36;
Ext.get(this.tabPanel.strip.dom.parentNode).applyStyles({
'margin-right': '18px',
'width' : stripWidth + 'px'
});

return tabMenuTrigger;
},

/**
* Creates a menu item for each tab in the TabPanel (paginated if there are more than
* the requested page size). Optionally removes current menu items first
* @param {Boolean} clearExisting True to remove all current menu items first (defaults to true)
*/
createMenuItems: function(clearExisting) {
var clearExisting = clearExisting ? clearExisting : true;
if (clearExisting) { this.removeAll(); }
this.createFilterMenu();

var numberOfItems = this.tabPanel.items.length;

if (numberOfItems > this.pageSize) {
var numberOfPages = Math.ceil(numberOfItems / this.pageSize);

//create each submenu
for (var i=0; i < numberOfPages; i++) {
var subMenuItems = [];

//create each submenu item
for (var j = 0; j <= this.pageSize - 1; j++){
var currentItem = this.tabPanel.items.items[j + (i * this.pageSize)];
if (currentItem) {
subMenuItems.push(this.createSubMenuItem(currentItem));
};
};

//calculate text label for this submenu
var lowerNumber = 1 + (this.pageSize * i);
var higherNumber = Math.min(((i + 1) * this.pageSize), numberOfItems);
var subMenuText = String.format(this.menuPrefixText + ' {0} - {1}', lowerNumber, higherNumber);

this.addMenuItem({text: subMenuText, menu: subMenuItems, iconCls: 'page_copy'});
}
} else {
//can put all items in the same 'page'
this.tabPanel.items.each(function(item) {
this.addMenuItem(this.createSubMenuItem(item));
}, this);
}
},

/**
* Returns a config object for a tab, suitable for placement inside a submenu
* @param {Object} panel The panel instance to create this submenu item from
* @param function handler The handler of the top-Level menu item
* @return {Object} An object suitable for addition to a submenu
*/
createSubMenuItem: function(panel, handler) {
var text = Ext.util.Format.stripTags(panel.title);
var menuHandler = handler ? handler : this.tabPanel.setActiveTab.createDelegate(this.tabPanel, [panel]);

var menuItem = {
disabled : panel.disabled
,iconCls : panel.iconCls
,handler : menuHandler
,rawText : panel.title
,scope : this
,text : Ext.util.Format.ellipsis(text, this.maxTextLength)
};

if(panel.tabPosition){
var parentMenu = this;

if(!panel.parentHandler){
panel.on({
add : function(){parentMenu.createMenuItems();}
,remove : function(){parentMenu.createMenuItems();}
});
}
panel.parentHandler = true;

var subMenuItems = [];

panel.items.each(function(item) {
var subHandler = menuHandler.createSequence(item.ownerCt.setActiveTab.createDelegate(item.ownerCt, [item]));
subMenuItems.push(this.createSubMenuItem(item, subHandler));
}, this);
menuItem.menu = subMenuItems;
}
return menuItem;
},

// public
getPageSize : function() {
return this.pageSize;
},
// public
setPageSize : function(pageSize) {
this.pageSize = pageSize;
},
// public
getMaxTextLength : function() {
return this.maxTextLength
},
// public
setMaxTextLength : function(maxText) {
this.maxText = maxTextLength;
},
// public
getMenuPrefixText : function() {
return this.menuPrefixText;
},
// public
setMenuPrefixText : function(t) {
this.menuPrefixText = t;
},

/**
* Adds a filter menu item with a TextField if this.hasFilter is true
* @return {Ext.menu.MenuItem/Null} The filter menu item
*/
createFilterMenu: function() {
if (this.hasFilter) {
this.filterMenu = this.addItem(
new Ext.ux.menu.TextFilterItem({
name : 'filter',
emptyText : this.filterEmptyText,
listeners: {
keyup: {
scope: this,
fn: function(e, input) {
if(e.getKey() == 40){ // down
var m = this;
if(!m.tryActivate(m.items.indexOf(m.activeItem)+1, 1)){
m.tryActivate(0, 1);
}
} else {
this.filterItems(input.value);
}
}
}
}
})
);
return this.filterMenu;
}
},

/**
* Iterates over each submenu item, hiding it if it does not match the filter text
* Also hides the menu item itself if all submenu items are hidden
*/
filterItems: function(filterText) {
var filterRegex = new RegExp(filterText, ['i']);

this.items.each(function(topMenu) {
//don't filter on the filterMenu itself...
if (topMenu != this.filterMenu) {
if (topMenu.menu) {
var hideMenu = true;

//if we have submenu items, iterate over each and hide if necessary
topMenu.menu.items.each(function(subMenuItem) {
if (filterRegex.test(subMenuItem.rawText)) {
subMenuItem.show();
hideMenu = false;
} else
subMenuItem.hide();
});

//if we've hidden everything, hide the whole menu
hideMenu ? topMenu.hide() : topMenu.show();
} else {
//if we don't have submenu items, iterate over top level menus and hide if necessary
filterRegex.test(topMenu.rawText) ? topMenu.show() : topMenu.hide();
}
}
}, this);
this.getEl().sync();
}
});

coriolis
23 Mar 2010, 7:11 PM
For some reason when EXT loads in the browser it chokes and shows nothing.

If I convert plugins: [ new Ext.ux.TabScrollerMenu() ], to plugins: [ ], it works fine.

What am I doing wrong?

Thanks
C


Here are my includes
<link rel="stylesheet" type="text/css" href="ext-3.1.1/resources/css/ext-all.css" />
<link rel="stylesheet" type="text/css" href="ext-3.1.1/examples/ux/css/ux-all-debug.css" />
<link rel="stylesheet" type="text/css" href="ext-3.1.1/examples/ux/css/RowEditor.css" />
<link rel="stylesheet" type="text/css" href="ext-3.1.1/examples/tabs/tab-scroller-menu.css" />
<link rel="stylesheet" type="text/css" href="ext-3.1.1/examples/samples.css" />

<script type="text/javascript" src="ext-3.1.1/examples/ux/TabScrollerMenu.js"></script>
<script type="text/javascript" src="ext-3.1.1/adapter/ext/ext-base-debug.js"></script>
<script type="text/javascript" src="ext-3.1.1/ext-all-debug.js"></script>
<script type="text/javascript" src="ext-3.1.1/examples/ux/RowEditor.js"></script>



Here is a snippit of my usage

var viewport = new Ext.Viewport({
layout:'border',
items:
[
{
region:'north',
layout:'fit',
header: false,
collapsible: false,
collapseMode: 'mini',
height: 30
}, {
region:'south',
split:true,
header: false,
height: 300,
minSize: 0,
collapsible: true,
collapsed: true,
collapseMode: 'mini',
title:'South',
margins:'0 0 0 0',
items: new Ext.TabPanel({
region:'south',
deferredRender:false,
enableTabScroll:true,
resizeTabs:true,
minTabWidth: 75,
activeTab:0,
plugins: [ new Ext.ux.TabScrollerMenu() ],
items:
[
{
title: 'Console',
closable:true,
autoScroll:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Errors',
autoScroll:true,
closable:true
}
]
})
}, {

jay@moduscreate.com
24 Mar 2010, 3:45 AM
ok? open up firebug, what exceptions do you see?

jay@moduscreate.com
24 Mar 2010, 4:18 AM
btw, please post code within code tags!

coriolis
24 Mar 2010, 4:33 AM
Hi,

I changed the <link> and <script> tags and now it just shows the regular tab panel but with no TabScrollerMenu effects.

Firebug shows no issues



<link rel="stylesheet" type="text/css" href="ext-3.1.1/examples/ux/css/ux-all-debug.css" />
<link rel="stylesheet" type="text/css" href="ext-3.1.1/resources/css/ext-all.css" />
<link rel="stylesheet" type="text/css" href="ext-3.1.1/examples/ux/css/us-all.css" />

<script type="text/javascript" src="ext-3.1.1/adapter/ext/ext-base-debug.js"></script>
<script type="text/javascript" src="ext-3.1.1/ext-all-debug.js"></script>
<script type="text/javascript" src="ext-3.1.1/examples/ux/ux-all.js"></script>
<script type="text/javascript" src="ext-3.1.1/examples/ux/ux-all-debug.js"></script>




xtype : 'tabpanel',
activeTab : 0,
id : 'myTPanel',
enableTabScroll : true,
resizeTabs : true,
minTabWidth : 75,
border : false,
plugins : [ new Ext.ux.TabScrollerMenu({maxText : 15, pageSize : 5 }) ],
items : [
{
title: 'Console',
closable:true,
autoScroll:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Search',
autoScroll:true,
closable:true
},{
title: 'Errors',
autoScroll:true,
closable:true
}
]
}