On this page:
Ext JS 4 comes with a plugin for TabPanels to allow reordering tabs using drag and drop. There isn’t an equivalent for Ext JS 3, however.
I came across a useful extension of the Ext JS 3.x TabPanel to re-order tabs via drag and drop. It is implemented as a subclass of Ext.TabPanel, as Ext.ux.panel.DDTabPanel and a version of it includes a useful reorder event that is fired once a tab is dragged to a new position in the TabPanel.
I refactored it from a subclass of Ext.TabPanel to a plugin so the original functionality is unchanged.
Demo
Here’s a simple screenshot of it in action
Or try out the demo. (View the demo source code to grab the code, or continue reading.)
Subclass or Plugin?
The original is a subclass of Ext.TabPanel. So to use for any of your existing subclasses of TabPanels, you have to change them to inherit this one.
However, I see this more as a feature of an Ext.TabPanel so it might be better provided as a plugin, rather than a subclass. Being a plugin means it could be reused in combination with various other plugins and potentially any manner of TabPanel subclasses.
For example, there may be other features you add to the TabPanel (via plugins) such as being able to rename a tab by double-clicking it (like Excel), or inserting an add tab button as the last tab, etc. These could all be plugins so you can choose which one(s) you want for your needs. But if they were all subclasses, trying to use a particular combination would mean you would need a particular subclass to represent the permutations. If you wanted another combination, you’d need another subclass, and so on. As a plugin, you can just pick and choose which other plugins you want as well.
So I attempted to refactor it as a plugin (without changing functionality).
The Code
The code involves 3 parts:
- The original subclass refactored into a plugin
- The accompanying DropTarget (only the constructor was changed)
- Example CSS to show the drop arrow (the blue one in the screenshot)
Original subclass refactored into a plugin
This is where the bulk of the changes are, but the functionality remains as is.
Ext.namespace('Ext.ux.panel'); /** * @class Ext.ux.panel.DDTabPanel * @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 ([email protected]) 04-25-2009 * Ported to Ext-3.1.1 by: Tobias Uhlig ([email protected]) 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);
The accompanying DropTarget to implement the drop behaviour
The only change from the original is the constructor:
// 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(); } });
CSS for the drop arrow
This of course can be customized. For example, you may prefer data URLs to inline the image (as the image is likely to be very small), but this illustrates what you’d need:
.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; }
Example usage
As a plugin, you simply add it to the plugins
configuration when constructing an instance of an Ext.TabPanel (or a subclass, potentially):
plugins: new Ext.ux.panel.DraggableTabs(),
Here’s an example of how you might use it (including the use of the new reorder
event):
var tabPanel = new Ext.TabPanel({ plugins: new Ext.ux.panel.DraggableTabs(), renderTo: document.body, activeTab: 0, width:300, height:150, items:[{ title: 'Tab 1', html: "Tab 1 contents" },{ title: 'Tab 2', html: "Tab 2 contents" },{ title: 'Tab 3', html: "Tab 3 contents" }], listeners: { reorder: { fn: function (tabPanel, movedTab) { // do something here } } } });
The above example is simplistic. Drag and drop will work on disabled tabs, tabs that are dynamically added (the demo has an example of this), etc.
Limitations
Any limitations are listed in the original form post (I’ve not changed the functionality – at least not knowingly!)
The main limitation here is that the original class and this refactored plugin is for Ext JS 3.x, not the latest Ext JS 4.x
If anyone’s had the time to create one for Ext JS 4.x please do let me know! Update: Turns out that for Ext JS 4, there is built in plugin. More further below.
What about Ext.NET 1.x?
This plugin can be used with Ext.NET 1.x. (For Ext.NET 2, which is based on Ext JS 4.*, see below). Assuming the JavaScript has been referenced, here is an example of how it can 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>
What about Ext JS 4 and Ext.NET 2?
Turns out Ext JS 4 includes Ext.ux.TabReorderer which is also a plugin.
For Ext.NET 2, it is part of the BoxReorderer plugin so you only need this:
<ext:TabPanel runat="server" Height="150" Width="300"> <Plugins> <ext:BoxReorderer /> </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>
Credits
All credits go to the original authors for the useful functionality.
Help
Let me know if you spot any problems or better practices in the conversion to a plugin. If you have improvements to the actual functionality, please post it to the original thread on the Sencha forums.
Very useful and detailed post as usual. Thank you for sharing!
I want to run same into Ext.js 6.5.1, but it’s not allowing me to do addEvents and my initTab is no calling.
I want to run this on ExtJs 6.5.1, is this possible?
Hi Jenish. Did you try the
Ext.ux.TabReorderer
plugin mentioned above for Ext JS 4 (sorry, this is an old post!)?While that above link is broken (which I will fix shortly) a quick google (I haven’t use Ext JS or Ext.NET in last couple of years, sadly) shows it still exists in the latest Ext JS: https://docs.sencha.com/extjs/6.5.1/classic/Ext.ux.TabReorderer.html
Hope that helps!
I want to use it in version 2.2.1,is it possible?
How to use this function in version 2.2.1
Hi, angel. Do you mean in 2.2.1 of Ext.NET – if so, does the last section of the post help?
hi!
I didn’t solve my problem. I want to do ext JS TabPanel plugin for draggable tabs, which can be upgraded. I don’t know how to write this function? Thanks!
It doesn’t solve my problem. It’s not ext.net. I want to do ext JS TabPanel plugin for draggable tabs, which can be upgraded. I don’t know how to write this function?
Does this plug-in you write not support ext-all.js?
Hi, I am not sure I follow.
Your earlier question was “How to use this function in version 2.2.1” – so I asked if you meant Ext.NET 2.2.1 but an earlier reply you said “It’s not ext.net. I want to do ext JS TabPanel plugin for draggable tabs, which can be upgraded.” so I am not clear now if you are referring to an even older version of Ext JS 2 or still Ext.NET 2.2…?
Unfortunately I have not worked on Ext.NET for about 4 years, so I am a bit rusty but per above, the JavaScript plugin for tab reordering, `Ext.ux.TabReorderer` is pure Ext JS 4. You would use it like any other plugin in Ext JS.
If you are referring to a much older Ext JS 2 then I have no ideal unfortunately…