Jun 30, 2011, 4:00 PM
TabPanel with plugins to rename tab inline, add a new tab using plus button in tab list and confirm close
Posting this as a reference for myself mostly, but a while back we needed to provide a tab-based UI where users could
With some help from Daniil and Vladimir in earlier forum posts (links within the code below) here's a contrived example close to what I needed. The main thing for reference are the 3 plugins. I tried to write it so that the 3 requirements above were workable on their own (sometimes we allow renaming but not adding more tabs, or don't allow closing, etc, so you can mix and match as needed).
More recently Daniil also gave me a good tip to implement destroy for custom plugins which I've tried to incorporate in the plugins below where needed.
Here's the code (important limitations listed after)
(I'm also assuming a gray theme)
Obviously in real code, the custom css and the plugins would be in an external minified/combined files, etc etc.
Any improvements, corrections, suggestions etc welcome.
- Dynamically add more tabs (with optional limits as to how many you can add)
- Rename the tabs (a bit like Excel whereby they are renamed inline by double clicking the tab name), and
- Prompt users for confirmation of closing those tabs.
With some help from Daniil and Vladimir in earlier forum posts (links within the code below) here's a contrived example close to what I needed. The main thing for reference are the 3 plugins. I tried to write it so that the 3 requirements above were workable on their own (sometimes we allow renaming but not adding more tabs, or don't allow closing, etc, so you can mix and match as needed).
More recently Daniil also gave me a good tip to implement destroy for custom plugins which I've tried to incorporate in the plugins below where needed.
Here's the code (important limitations listed after)
(I'm also assuming a gray theme)
<%@ Page Language="C#" %>
<%@ Register Assembly="Ext.Net" Namespace="Ext.Net" TagPrefix="ext" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected override void OnLoad(EventArgs e)
{
ScriptManager1.RegisterIcon(Icon.PageWhiteStar);
base.OnLoad(e);
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
<title>TabPanel plugins combined</title>
<ext:ResourcePlaceHolder ID="StyleContainer1" Mode="Style" runat="server" />
<style type="text/css">
.x-tab-strip-wrap .add-tab a .x-tab-strip-inner {
background:url(/icons/add-png/ext.axd) no-repeat center center;
width:16px;
height:21px;
}
.new-tab { /* background-image:url(/icons/page_white_star-png/ext.axd); */ }
.add-tab.disabled .x-tab-strip-inner { opacity: 0.5; }
.tab-rename-field { border-color:#aaa; }
.tab-bottom-insert-before { background-image:url(icons/tab-bottom-add-after.png); }
.tab-bottom-insert-after { background-image:url(icons/tab-bottom-add-before.png); }
.tab-bottom-remove { background-image:url(icons/tab-bottom-remove.png); }
</style>
<ext:ResourcePlaceHolder ID="ResourcePlaceHolder1" runat="server" Mode="ScriptFiles" />
<script type="text/javascript">
Ext.ux.ConfirmTabClose = Ext.extend(Object, {
constructor : function (config) {
config = config || {};
config.confirmRemoveTitle = config.confirmRemoveTitle || 'Are you sure you?';
config.confirmRemoveMessage = config.confirmRemoveMessage || 'Are you sure you want to remove {0}?';
Ext.apply(this, config);
},
init : function (tp) {
if (tp instanceof Ext.TabPanel) {
tp.confirmRemoveTitle = this.confirmRemoveTitle;
tp.confirmRemoveMessage = this.confirmRemoveMessage;
tp.on('beforeremove', this.onBeforeRemove);
}
},
onBeforeRemove : function (tabPanel, tab) {
if (!!tabPanel.locked) {
return true;
}
tabPanel.setActiveTab(tab);
Ext.Msg.show({
title : tabPanel.confirmRemoveTitle,
msg : String.format(tabPanel.confirmRemoveMessage, tab.title),
buttons : Ext.Msg.YESNO,
icon : Ext.Msg.QUESTION,
animEl : tab.tabEl,
fn : function (btn) {
if (btn == 'yes') {
tabPanel.locked = true;
tabPanel.remove(tab);
tabPanel.locked = false;
}
}
});
return false;
}
});
Ext.reg('Ext.ux.ConfirmTabClose', Ext.ux.ConfirmTabClose);
</script>
<script type="text/javascript">
Ext.ux.EditableTabName = Ext.extend(Object, {
constructor: function (config) {
config = config || {};
Ext.apply(this, config);
},
init: function(tp) {
if (tp instanceof Ext.TabPanel) {
this.tp = tp;
tp.onRender = tp.onRender.createSequence(this.bodyInit, this);
tp.on('add', this.onNewTabAdded, this);
tp.on('destroy', this.destroy, this);
}
},
destroy: function() {
delete this.tp;
},
bodyInit: function() {
this.tp.items.each(function(item, index) {
this.setTabEditable(this.tp, index);
},
this);
},
setTabEditable: function(tabPanel, index) {
var tabEl = Ext.get(tabPanel.getTabEl(index)),
tab = tabPanel.getItem(index);
if (!tabEl)
return;
tabEl.on('dblclick', function(e, t) {
var originalTarget = Ext.get(t),
textTarget = originalTarget.parent().child(".x-tab-strip-text") || t;
window[this.editor].currentTab = tab;
window[this.editor].un('beforeComplete', this.editorBeforeComplete);
window[this.editor].on('beforeComplete', this.editorBeforeComplete);
window[this.editor].field.setWidth(textTarget.getWidth());
window[this.editor].startEdit(textTarget);
}, this);
},
onNewTabAdded: function(container, newTab, index) {
var tabPanel = container.isXType('tabpanel') ? container : container.findParentByType('tabpanel');
this.setTabEditable(tabPanel, index);
},
editorBeforeComplete: function(editor, value, startValue) {
editor.currentTab.setTitle(value);
editor.setValue(value);
delete editor.currentTab;
return true;
}
});
Ext.reg('Ext.ux.EditableTabName', Ext.ux.EditableTabName);
</script>
<script type="text/javascript">
// Courtesy: http://www.sencha.com/forum/showthread.php?83213-Add-new-tab-button-in-tab-strip&p=513914#post513914
Ext.ux.AddTabButton = Ext.extend(Object, {
newTabHandler: Ext.emptyFn,
constructor: function (config) {
config = config || {};
config.maxNumberOfTabs = config.maxNumberOfTabs || 5; // a reasonable upper limit?
config.newTabTip = config.newTabTip || 'New tab';
config.disabledTabTip = config.disabledTabTip || String.format('Cannot add more than {0} tabs', config.maxNumberOfTabs);
Ext.apply(this, config);
},
init: function(tp) {
if (tp instanceof Ext.TabPanel) {
this.tp = tp;
tp.onRender = tp.onRender.createSequence(this.onTabPanelRender, this);
tp.findTargets = this.findTargets;
tp.on('remove', this.setEnableState, this);
tp.on('destroy', this.destroy, this);
}
},
destroy: function() {
delete this.tp;
delete this.addTab;
if (this.tooltip) {
Ext.destroy(this.tooltip)
delete this.tooltip;
}
},
findTargets : function(e) {
var item = null,
itemEl = e.getTarget('li:not(.x-tab-edge)', this.strip);
if (itemEl) {
item = this.getComponent(itemEl.id.split(this.idDelimiter)[1]);
if (item && item.disabled) {
return {
close : null,
item : null,
el : null
};
}
}
return {
close : e.getTarget('.x-tab-strip-close', this.strip),
item : item,
el : itemEl
};
},
enable: function() {
this.addTab.removeClass('disabled');
this.addTab.un('click', Ext.emptyFn, this);
this.addTab.on('click', this.onAddTabClick.createDelegate(this, [this.tp]), this, { stopEvent: true });
this.setTooltip(this.newTabTip);
this.enabled = true;
},
disable: function() {
this.addTab.addClass('disabled');
this.addTab.purgeAllListeners();
this.addTab.on('click', Ext.emptyFn, this, { stopEvent: true }); // prevent # in url
this.setTooltip(this.disabledTabTip);
this.enabled = false;
},
//private
setTooltip: function(msg) {
if (this.tooltip)
this.tooltip.destroy();
this.tooltip = new Ext.ToolTip({
target: this.addTab,
bodyCfg: { html: msg }
});
},
//private
onAddTabClick: function(tabPanel) {
var newTab = this.newTabHandler(tabPanel);
if (!newTab)
return;
tabPanel.add(newTab);
tabPanel.setActiveTab(tabPanel.items.items.length - 1);
this.setEnableState();
},
//private, ideally
onTabPanelRender: function() {
this.addTab = this.tp.itemTpl.insertBefore(this.tp.edge, {
id: this.tp.id + 'addTabButton',
cls: 'add-tab',
text: this.addTabText || ' ',
iconCls: ''
}, true);
this.addTab.child('em.x-tab-left').setStyle('padding-right', '6px');
this.addTab.child('a.x-tab-right').setStyle('padding-left', this.tp.tabPosition == 'top' ? '6px' : '0px');
this.setEnableState();
},
//private
setEnableState: function() {
var enabled = this.enabled,
exceededMaxAllowed = this.tp.items.length >= this.maxNumberOfTabs;
if (exceededMaxAllowed) {
this.disable();
}
else if (!enabled && !exceededMaxAllowed) {
this.enable();
}
}
});
Ext.reg('Ext.ux.AddTabButton', Ext.ux.AddTabButton);
</script>
<script type="text/javascript">
var tabEditor = {
createNewTab: function(tabPanel) {
return {
title : '(untitled)',
iconCls : 'new-tab icon-pagewhitestar',
layout : 'fit',
id : Ext.id(),
height : '100%',
closable : true,
items: {
layout: "ux.row",
border: false,
layoutConfig: { split: true, background: true },
items: [{
height : 70,
boxMinHeight : 70,
autoScroll : true,
border : false,
bodyStyle : 'border-bottom:1px solid #ccc',
title : 'Initial Height = 70px'
},
{
rowHeight : 1,
autoScroll : true,
border : false,
style : 'border-top:1px solid #ccc',
title : 'Initial Height = 100%'
}]
}
};
},
complete: function(editor, value, startValue) {
// Notify user the Editor value has changed.
Ext.Msg.notify("Editor Changed",
String.format("<b>{0}</b><br />changed to<br /><b>{1}</b>", startValue, value));
}
};
// potential script to move tabs around:
//var old0 = TabPanel2.items.items[0];
//TabPanel2.closeTab(old0, "hide");
//TabPanel2.addTab(old0, 1);
//old0.doLayout();
// something in here causes IE6 to hang when simply clicking the little tab menu icon...
var beforeMenu = function (pnl, tab, menu) {
menu.items.get(0).component.setText(tab.title);
var doLayout = false;
// while (menu.items.getCount() > 8) {
// var item = menu.getComponent(8);
// item.destroy();
// menu.remove(item);
// }
if (tab.id == "customMenuTab") {
menu.addSeparator();
menu.addMenuItem({
text : "Show Menu for last Tab",
handler : function () {
pnl.items.get(pnl.items.getCount() - 1).showTabMenu();
}
});
menu.addMenuItem({
text : "Hide Menu for last Tab",
handler : function () {
pnl.items.get(pnl.items.getCount() - 1).hideTabMenu();
}
});
}
};
</script>
</head>
<body>
<ext:ResourceManager ID="ScriptManager1" runat="server" />
<ext:ViewPort ID="Layout1" runat="server" Layout="Absolute">
<Items>
<ext:TabPanel ID="TabPanel1" runat="server" Title="TabPanel1 title" Width="600" Height="400" X="10" Y="10">
<Items>
<ext:Panel ID="Panel5" runat="server" Title="General" Padding="3">
<Content>
<p>Tab 1 contents</p>
</Content>
</ext:Panel>
<ext:Panel ID="Panel6" runat="server" Title="Products" Layout="fit" Border="false" PaddingSummary="0 0 3px 0">
<Items>
<ext:TabPanel ID="TabPanel2" Plain="true" TabPosition="Bottom" runat="server" Border="false">
<DefaultTabMenu>
<ext:Menu ID="Menu4" runat="server" EnableScrolling="false">
<Items>
<ext:ComponentMenuItem ID="ComponentMenuItem1" runat="server">
<Component>
<ext:Label ID="Label1" runat="server" StyleSpec="font-weight:bold;" />
</Component>
</ext:ComponentMenuItem>
<ext:MenuSeparator ID="MenuSeparator1" runat="server" />
<ext:MenuItem ID="MenuItem5" runat="server" IconCls="tab-bottom-insert-before" Text="Insert new item before" />
<ext:MenuItem ID="MenuItem6" runat="server" IconCls="tab-bottom-insert-after" Text="Insert new item after" />
<ext:MenuSeparator ID="MenuSeparator3" runat="server" />
<ext:MenuItem ID="MenuItem7" runat="server" IconCls="tab-bottom-remove" Text="Remove this item">
<Listeners>
<Click Handler="var tab = this.parentMenu.tab; tab.ownerCt.closeTab(tab);"/>
</Listeners>
</ext:MenuItem>
<ext:MenuSeparator ID="MenuSeparator2" runat="server" />
<ext:ComponentMenuItem ID="ComponentMenuItem2" runat="server">
<Component>
<ext:Label ID="Label2" runat="server" Text="Rename Tab:" />
</Component>
</ext:ComponentMenuItem>
<ext:ComponentMenuItem ID="ComponentMenuItem3" runat="server" ComponentElement="Wrap">
<Component>
<ext:TriggerField ID="RenameField" runat="server" Text="New title">
<Triggers>
<ext:FieldTrigger Icon="Empty" Qtip="Click to rename" />
</Triggers>
<Listeners>
<TriggerClick Handler="this.parentMenu.tab.setTitle(this.getValue());this.parentMenu.hide();" />
</Listeners>
</ext:TriggerField>
</Component>
</ext:ComponentMenuItem>
</Items>
<Listeners>
<Show Handler="#{RenameField}.setValue(this.tab.title);" />
</Listeners>
</ext:Menu>
</DefaultTabMenu>
<Listeners>
<BeforeTabMenuShow Fn="beforeMenu" />
</Listeners>
<Plugins>
<ext:GenericPlugin Id="AddTabButton" runat="server" InstanceName="Ext.ux.AddTabButton">
<CustomConfig>
<ext:ConfigItem Name="newTabHandler" Value="tabEditor.createNewTab" />
</CustomConfig>
</ext:GenericPlugin>
<ext:GenericPlugin Id="EditableTabName" runat="server" InstanceName="Ext.ux.EditableTabName">
<CustomConfig>
<ext:ConfigItem Name="editor" Value="#{Editor1}" Mode="Value" />
</CustomConfig>
</ext:GenericPlugin>
<ext:GenericPlugin Id="ConfirmTabClose" runat="server" InstanceName="Ext.ux.ConfirmTabClose" />
</Plugins>
<Items>
<ext:Panel ID="RowLayout1" Title="Tab 1" Layout="ux.row" runat="server" Border="false" Closable="true">
<Items>
<ext:Panel ID="Panel2" runat="server" Height="70px" AutoScroll="true" border="false" BodyStyle="border-bottom:1px solid #ccc;">
<Content>
<p>Top part for Tab 1</p>
</Content>
</ext:Panel>
<ext:Panel ID="Panel3" runat="server" Title="Details" RowHeight="1" AutoScroll="true" Icon="Table" border="false" StyleSpec="border-top:1px solid #ccc;">
<BottomBar>
<ext:Toolbar ID="Toolbar1" runat="server">
<Items>
<ext:Button ID="Button3" Icon="ArrowLeft" runat="server" />
<ext:Button ID="Button4" Icon="ArrowRight" runat="server" />
</Items>
</ext:Toolbar>
</BottomBar>
</ext:Panel>
</Items>
<LayoutConfig>
<ext:RowLayoutConfig Split="true" Background="true" />
</LayoutConfig>
</ext:Panel>
<ext:Panel ID="Panel9" runat="server" Title="Tab 2" Layout="border" Closable="true">
<Items>
<ext:Panel
ID="Panel17" runat="server" Region="Center" Height="70" AutoScroll="true" MinHeight="70" Border="false" BodyStyle="border-bottom:1px solid #ccc;">
<Content>
<p>Top part for Tab 2</p>
</Content>
</ext:Panel>
<ext:Panel ID="Panel18" runat="server" Region="South" Split="true" AutoScroll="true" CollapseFirst="false" Collapsible="true" Height="313" Icon="Table" Title="Contents" TitleCollapse="true" Floatable="false" Border="false" />
</Items>
</ext:Panel>
</Items>
</ext:TabPanel>
<ext:Editor ID="Editor1" runat="server" Shadow="None" IgnoreNoChange="true">
<Alignment ElementAnchor="Left" TargetAnchor="Left" />
<Field>
<ext:TextField ID="TextField1" runat="server" Cls="tab-rename-field" AllowBlank="false" Width="60" SelectOnFocus="true" />
</Field>
<Listeners>
<Complete Fn="tabEditor.complete" />
</Listeners>
</ext:Editor>
</Items>
</ext:Panel>
<ext:Panel ID="Panel10" runat="server" Title="Blah blah" Layout="border">
<Items>
<ext:Panel
ID="Panel11" runat="server" Region="Center" Height="70" AutoScroll="true" Border="false" StyleSpec="border-bottom:1px solid #d0d0d0;" MinHeight="70">
<Content>
<p>Content for center panel</p>
</Content>
</ext:Panel>
<ext:Panel ID="Panel12" runat="server" Region="South" Border="false" StyleSpec="border-top:1px solid #d0d0d0;" Split="true" AutoScroll="true" CollapseFirst="false" Collapsible="true" Height="313" Icon="Table" Title="Contents" TitleCollapse="true" Floatable="false" />
</Items>
</ext:Panel>
</Items>
</ext:TabPanel>
</Items>
</ext:ViewPort>
</body>
</html>
Limitations:- The tab menu hangs when clicked on IE6 (in our real code I opted to not go for it, anyway, so didn't look into it further)
- The tab menu has additional options (insert before, after etc) that are not implemented
- For my needs the tab strip is positioned at the bottom, though it *should* work when positioned at the top
- ExtJs has a tab scroll feature which is great. But, it is not supported when tabs are positioned at the bottom which is a great shame, as the UI for the above example looks better when tabs are positioned at the bottom.
Obviously in real code, the custom css and the plugins would be in an external minified/combined files, etc etc.
Any improvements, corrections, suggestions etc welcome.