PDA

View Full Version : TabPanel with plugins to rename tab inline, add a new tab using plus button in tab list and confirm close



anup
Jun 30, 2011, 5:00 PM
Posting this as a reference for myself mostly, but a while back we needed to provide a tab-based UI where users could

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.

Daniil
Jul 11, 2011, 12:37 PM
Hi @anup,

Thanks for the sharing such excellent example!

At the quick look I have the single note only.

I think it would be better to localize editors in a TabPanel instance. I mean the following code:

window[this.editor].currentTab = tab;

To don't "clog" a window instance.

anup
Jul 15, 2011, 12:06 AM
Thanks for kind words and well spotted Daniil.

I am not sure why I put the editor into the window like that... I'll have to go back to the original threads where we may have discussed some of this to see if there are any hints there, otherwise I agree that ideally it could just be localized against the tab panel instance.

Will look into that and update the example accordingly.

anup
Jul 15, 2011, 3:35 PM
Finally got back to looking at this (was away from my desk when I replied earlier).

The reason window[this.editor] is used is not to cache the editor instance, but because that is how ASP.NET stores its controls (including Ext.Net ones)

this.editor is a string, so I use the window as an associative array to get the instance.

In ExtJs fashion however, I should have just used Ext.getCmp like this:



tabEl.on('dblclick', function(e, t) {
var originalTarget = Ext.get(t),
textTarget = originalTarget.parent().child(".x-tab-strip-text") || t,
editorCmp = Ext.getCmp(this.editor);

editorCmp.currentTab = tab;
editorCmp.un('beforeComplete', this.editorBeforeComplete);
editorCmp.on('beforeComplete', this.editorBeforeComplete);
editorCmp.field.setWidth(textTarget.getWidth());
editorCmp.startEdit(textTarget);
}, this);


The reason that this.editor is a string inside the plugin I think is because of this line:



<ext:GenericPlugin Id="EditableTabName" runat="server" InstanceName="Ext.ux.EditableTabName">
<CustomConfig>
<ext:ConfigItem Name="editor" Value="#{Editor1}" Mode="Value" />
</CustomConfig>
</ext:GenericPlugin>


So, I could change Mode="Value" to Mode="Raw" in which case I think the JavaScript generated by Ext.Net is as expected: an object.

However that then causes another problem: by the time the config item runs (on the client side) the Editor1 editor has not actually been created yet, thus causing an error (and nothing renders).

I could define the Editor earlier but I don't want to force that, so if a string is passed in, then I need to find the component. Ext.getCmp is probably more readable than using window[this.editor]

anup
Jul 15, 2011, 3:40 PM
I posted too soon! I looked at my actual production code and saw that I had already defined editor like 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,
editor = this.editor instanceof Ext.Editor ? this.editor : window[this.editor];

if (editor.currentTab && editor.currentTab.id != tab.id) {
editor.completeEdit();
}

editor.currentTab = tab;
editor.un('beforeComplete', this.editorBeforeComplete);
editor.on('beforeComplete', this.editorBeforeComplete);
editor.field.setWidth(textTarget.getWidth());
editor.startEdit(textTarget);
}, this);
},


This is because in my production code, I dynamically generate all these objects and I think for me the Editor is already available elsewhere and I can pass it in raw sometimes (hence an object).

That being said, I can still fallback to Ext.getCmp instead of window[this.editor]

(P.S. The extra if statement there I think was to handle some scenario that we eventually came across that if you double clicked on one tab to edit its name and then double clicked the next tab to rename that, the editor would update the second tab, or something like that.)

Daniil
Jul 15, 2011, 4:33 PM
Thanks for the such detailed info!

I agree that Ext.getCmp() would look better.

jchau
Nov 07, 2011, 9:47 PM
I was struggling for hours trying to get the AddTabButton plugin to work. Thanks so much for this! For Ext.NET, is there any reason why the plugin as posted on Sencha will not work with Ext.NET without all these additional chagnes?

Daniil
Nov 08, 2011, 6:54 AM
Hi @jchau,

Well, there are several aspects:

1. A plugin should support the same ExtJS version which Ext.Net uses.

2. I believe you know that we have extended/overrode many ExtJS classes. In general, it can break an ExtJS plugin.

Please provide a downloading link of the plugin you mentioned, I will check.

anup
Nov 15, 2011, 3:11 PM
Hi,

Sorry for my delayed reply (been away for a while).

Yeah, I believe the Ext.Net tab overrides and extends the Ext Js tab, so the plugin as provided on the sencha forums did not work as is without some minor tweaking.

(Daniil: the link to the original is in the code sample I provided - search for the comment: "courtesy")

Off the top of my head, I think methods like findTarget were subtly different. Or something like that...

I also slightly extended it to have some configuration options about how many maximum tabs you can add (and to support disable/enabling accordingly) plus a few other such things.

Hope that answers?

Anup

Daniil
Nov 15, 2011, 3:37 PM
(Daniil: the link to the original is in the code sample I provided - search for the comment: "courtesy")


Thanks, @anup!