Jan 13, 2013, 1:43 PM
Draggable Tabs on TabPanel (Ext.NET 1.x only)
Hi,
I came across a useful draggable tabs implementation but it was a subclass of Ext.TabPanel. I think it is better as a plugin so you are not forced down a particular inheritance chain as I see the draggable tabs capability of a TabPanel as a feature rather than as a subclass (composition vs inheritance I guess?).
Here's a screenshot of what it is about (click it to see full size):
Here's how it might be used:
Main limitation: it is Ext JS 3.x and Ext.NET 1.x only at the moment. I didn't write the original code, and have not had the time to look into what is needed to convert it into Ext JS 4.x so it would work with Ext.NET 2. If someone does have the time to do so, please do let me know.
Further info: I created a blog post which includes a live demo, screenshot and further info.
Hope that is useful!
I came across a useful draggable tabs implementation but it was a subclass of Ext.TabPanel. I think it is better as a plugin so you are not forced down a particular inheritance chain as I see the draggable tabs capability of a TabPanel as a feature rather than as a subclass (composition vs inheritance I guess?).
Here's a screenshot of what it is about (click it to see full size):
Here's how it might be used:
<ext:TabPanel runat="server" Height="150" Width="300" TabPosition="Bottom" >
<Plugins>
<ext:GenericPlugin runat="server" InstanceName="Ext.ux.panel.DraggableTabs" />
</Plugins>
<Items>
<ext:Panel runat="server" Title="Tab1" Border="false" Closable="true" Html="Tab 1 contents" />
<ext:Panel runat="server" Title="Tab2" Border="false" Closable="true" Html="Tab 2 contents" />
<ext:Panel runat="server" Title="Tab3" Border="false" Closable="true" Html="Tab 3 contents" />
</Items>
</ext:TabPanel>
Here's the plugin code that the above is referring to:Ext.namespace('Ext.ux.panel');
/**
* @class Ext.ux.panel.DDTabPanel
* @extends Ext.TabPanel
* @author
* Original by
* <a href="http://extjs.com/forum/member.php?u=22731">thommy</a> and
* <a href="http://extjs.com/forum/member.php?u=37284">rizjoj</a><br />
* Published and polished by: Mattias Buelens (<a href="http://extjs.com/forum/member.php?u=41421">Matti</a>)<br />
* With help from: <a href="http://extjs.com/forum/member.php?u=1459">mystix</a>
* Polished and debugged by: Tobias Uhlig (info@internetsachen.com) 04-25-2009
* Ported to Ext-3.1.1 by: Tobias Uhlig (info@internetsachen.com) 02-14-2010
* Updated by <a href="http://www.sencha.com/forum/member.php?56442-brombs">brombs</a>
* to include reorder event
* Modified by <a href="http://www.onenaught.com">Anup Shah</a> to work as a plugin
* instead of subclass of TabPanel
* @license Licensed under the terms of the Open Source <a href="http://www.gnu.org/licenses/lgpl.html">LGPL 3.0 license</a>.
* Commercial use is permitted to the extent that the code/component(s) do NOT
* become part of another Open Source or Commercially licensed development library
* or toolkit without explicit permission.
* @version 2.0.1 (Jan 11, 2013)
*/
Ext.ux.panel.DraggableTabs = Ext.extend(Object, {
constructor: function (config) {
if (config) {
Ext.apply(this, config);
}
},
init: function(tp) {
if ((tp instanceof Ext.TabPanel) === false)
return;
// make these available onto the TabPanel as per original plugin, where used externally
tp.arrowOffsetX = this.arrowOffsetX;
tp.arrowOffsetY = this.arrowOffsetY;
tp.addEvents('reorder');
// TODO: check if ddGroupId can be left as a property of this plugin rather than on the TabPanel
if (!tp.ddGroupId) {
tp.ddGroupId = 'dd-tabpanel-group-' + tp.getId();
}
// New Event fired after drop tab. Is there a cleaner way to do this?
tp.reorder = this.reorder;
tp.oldinitTab = tp.initTab;
tp.initTab = this.initTab;
tp.onRemove = this.onRemove;
tp.on('afterrender', this.afterRender, this);
this.tabPanel = tp;
},
destroy: function () {
tp.un('afterrender', this.afterRender, this);
delete this.tabPanel;
Ext.destroy(this.dd, this.arrow);
},
/**
* @cfg {Number} arrowOffsetX The horizontal offset for the drop arrow indicator, in pixels (defaults to -9).
*/
arrowOffsetX: -9,
/**
* @cfg {Number} arrowOffsetY The vertical offset for the drop arrow indicator, in pixels (defaults to -8).
*/
arrowOffsetY: -8,
reorder: function(tab) {
this.fireEvent('reorder', this, tab);
},
// Declare the tab panel as a drop target
/** @private */
afterRender: function () {
// Create a drop arrow indicator
this.tabPanel.arrow = Ext.DomHelper.append(
Ext.getBody(),
'<div class="dd-arrow-down"></div>',
true
);
this.tabPanel.arrow.hide();
// Create a drop target for this tab panel
var tabsDDGroup = this.tabPanel.ddGroupId;
this.dd = new Ext.ux.panel.DraggableTabs.DropTarget(this, {
ddGroup: tabsDDGroup
});
// needed for the onRemove-Listener
this.move = false;
},
// Init the drag source after (!) rendering the tab
/** @private */
initTab: function (tab, index) {
this.oldinitTab(tab, index);
var id = this.id + '__' + tab.id;
// Hotfix 3.2.0
Ext.fly(id).on('click', function () { tab.ownerCt.setActiveTab(tab.id); });
// Enable dragging on all tabs by default
Ext.applyIf(tab, { allowDrag: true });
// Extend the tab
Ext.apply(tab, {
// Make this tab a drag source
ds: new Ext.dd.DragSource(id, {
ddGroup: this.ddGroupId
, dropEl: tab
, dropElHeader: Ext.get(id, true)
, scroll: false
// Update the drag proxy ghost element
, onStartDrag: function () {
if (this.dropEl.iconCls) {
var el = this.getProxy().getGhost().select(".x-tab-strip-text");
el.addClass('x-panel-inline-icon');
var proxyText = el.elements[0].innerHTML;
proxyText = Ext.util.Format.stripTags(proxyText);
el.elements[0].innerHTML = proxyText;
el.applyStyles({
paddingLeft: "20px"
});
}
}
// Activate this tab on mouse up
// (Fixes bug which prevents a tab from being activated by clicking it)
, onMouseUp: function (event) {
if (this.dropEl.ownerCt.move) {
if (!this.dropEl.disabled && this.dropEl.ownerCt.activeTab == null) {
this.dropEl.ownerCt.setActiveTab(this.dropEl);
}
this.dropEl.ownerCt.move = false;
return;
}
if (!this.dropEl.isVisible() && !this.dropEl.disabled) {
this.dropEl.show();
}
}
})
// Method to enable dragging
, enableTabDrag: function () {
this.allowDrag = true;
return this.ds.unlock();
}
// Method to disable dragging
, disableTabDrag: function () {
this.allowDrag = false;
return this.ds.lock();
}
});
// Initial dragging state
if (tab.allowDrag) {
tab.enableTabDrag();
} else {
tab.disableTabDrag();
}
}
/** @private */
, onRemove: function (c) {
var te = Ext.get(c.tabEl);
// check if the tabEl exists, it won't if the tab isn't rendered
if (te) {
// DragSource cleanup on removed tabs
//Ext.destroy(c.ds.proxy, c.ds);
te.select('a').removeAllListeners();
Ext.destroy(te);
}
// ignore the remove-function of the TabPanel
Ext.TabPanel.superclass.onRemove.call(this, c);
this.stack.remove(c);
delete c.tabEl;
c.un('disable', this.onItemDisabled, this);
c.un('enable', this.onItemEnabled, this);
c.un('titlechange', this.onItemTitleChanged, this);
c.un('iconchange', this.onItemIconChanged, this);
c.un('beforeshow', this.onBeforeShowItem, this);
// if this.move, the active tab stays the active one
if (c == this.activeTab) {
if (!this.move) {
var next = this.stack.next();
if (next) {
this.setActiveTab(next);
} else if (this.items.getCount() > 0) {
this.setActiveTab(0);
} else {
this.activeTab = null;
}
}
else {
this.activeTab = null;
}
}
if (!this.destroying) {
this.delegateUpdates();
}
}
});
Ext.preg('draggabletabs', Ext.ux.panel.DraggableTabs);
// Ext.ux.panel.DraggableTabs.DropTarget
// Implements the drop behavior of the tab panel
/** @private */
Ext.ux.panel.DraggableTabs.DropTarget = Ext.extend(Ext.dd.DropTarget, {
constructor: function (dd, config) {
this.tabpanel = dd.tabPanel;
// The drop target is the tab strip wrap
Ext.ux.panel.DraggableTabs.DropTarget.superclass.constructor.call(this, this.tabpanel.stripWrap, config);
}
, notifyOver: function (dd, e, data) {
var tabs = this.tabpanel.items;
var last = tabs.length;
if (!e.within(this.getEl()) || dd.dropEl == this.tabpanel) {
return 'x-dd-drop-nodrop';
}
var larrow = this.tabpanel.arrow;
// Getting the absolute Y coordinate of the tabpanel
var tabPanelTop = this.el.getY();
var left, prevTab, tab;
var eventPosX = e.getPageX();
for (var i = 0; i < last; i++) {
prevTab = tab;
tab = tabs.itemAt(i);
// Is this tab target of the drop operation?
var tabEl = tab.ds.dropElHeader;
// Getting the absolute X coordinate of the tab
var tabLeft = tabEl.getX();
// Get the middle of the tab
var tabMiddle = tabLeft + tabEl.dom.clientWidth / 2;
if (eventPosX <= tabMiddle) {
left = tabLeft;
break;
}
}
if (typeof left == 'undefined') {
var lastTab = tabs.itemAt(last - 1);
if (lastTab == dd.dropEl) return 'x-dd-drop-nodrop';
var dom = lastTab.ds.dropElHeader.dom;
left = (new Ext.Element(dom).getX() + dom.clientWidth) + 3;
}
else if (tab == dd.dropEl || prevTab == dd.dropEl) {
this.tabpanel.arrow.hide();
return 'x-dd-drop-nodrop';
}
larrow.setTop(tabPanelTop + this.tabpanel.arrowOffsetY).setLeft(left + this.tabpanel.arrowOffsetX).show();
return 'x-dd-drop-ok';
}
, notifyDrop: function (dd, e, data) {
this.tabpanel.arrow.hide();
// no parent into child
if (dd.dropEl == this.tabpanel) {
return false;
}
var tabs = this.tabpanel.items;
var eventPosX = e.getPageX();
for (var i = 0; i < tabs.length; i++) {
var tab = tabs.itemAt(i);
// Is this tab target of the drop operation?
var tabEl = tab.ds.dropElHeader;
// Getting the absolute X coordinate of the tab
var tabLeft = tabEl.getX();
// Get the middle of the tab
var tabMiddle = tabLeft + tabEl.dom.clientWidth / 2;
if (eventPosX <= tabMiddle) break;
}
// do not insert at the same location
if (tab == dd.dropEl || tabs.itemAt(i - 1) == dd.dropEl) {
return false;
}
dd.proxy.hide();
// if tab stays in the same tabPanel
if (dd.dropEl.ownerCt == this.tabpanel) {
if (i > tabs.indexOf(dd.dropEl)) i--;
}
this.tabpanel.move = true;
var dropEl = dd.dropEl.ownerCt.remove(dd.dropEl, false);
this.tabpanel.insert(i, dropEl);
// Event drop
this.tabpanel.fireEvent('drop', this.tabpanel);
// Fire event reorder
this.tabpanel.reorder(tabs.itemAt(i));
return true;
}
, notifyOut: function (dd, e, data) {
this.tabpanel.arrow.hide();
}
});
Example CSS for the arrow button that appears as tabs are ready to be dropped:.dd-arrow-down.dd-arrow-down-invisible {
display: none;
visibility: hidden;
}
.dd-arrow-down {
background-image: url(icons/tab-drop-arrow-down.png);
display: block;
visibility: visible;
z-index: 20000;
position: absolute;
width: 16px;
height: 16px;
top: 0;
left: 0;
}
Main limitation: it is Ext JS 3.x and Ext.NET 1.x only at the moment. I didn't write the original code, and have not had the time to look into what is needed to convert it into Ext JS 4.x so it would work with Ext.NET 2. If someone does have the time to do so, please do let me know.
Further info: I created a blog post which includes a live demo, screenshot and further info.
Hope that is useful!