PDA

View Full Version : Draggable Tabs on TabPanel (Ext.NET 1.x only)



anup
Jan 13, 2013, 1:43 PM
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):

5419


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.c onstructor.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 (http://www.onenaught.com/posts/502/ext-js-tabpanel-plugin-for-draggable-tabs) which includes a live demo, screenshot and further info.

Hope that is useful!

Vladimir
Jan 13, 2013, 3:40 PM
Please note that in v2 there is TabReorderer plugin already

anup
Jan 14, 2013, 9:15 AM
Many thanks for that! I didn't know about it.

Not tried it but from the docs it seems like just what would be needed!

http://docs.sencha.com/ext-js/4-1/#!/api/Ext.ux.TabReorderer

If I'd known about it earlier, would have tried to find some space to put it in the book :)

Btw, might be useful to put it in the Examples Explorer?

Vladimir
Jan 14, 2013, 9:26 AM
Example is added already, just it is available in SVN only
I forgot to mention that in Ext.net it is called as BoxReorderer. It can be used in container with HBox/VBox layout and TabPanel

Online samples with BoxReorderer
http://examples2.ext.net/#/Layout/HBoxLayout/Reordering/
http://examples2.ext.net/#/Layout/VBoxLayout/Reordering/
http://examples2.ext.net/#/Toolbar/Plugins/ToolbarDroppable/
http://examples2.ext.net/#/Toolbar/Plugins/ToolbarReorderable/
http://examples2.ext.net/#/GridPanel/Paging_and_Sorting/Multiple_Sorting_Local/

anup
Jan 14, 2013, 9:26 AM
Not tried it...

Just realized it should be super quick to try, so tried with this:


<ext:TabPanel runat="server" Height="150" Width="300">
<Plugins>
<ext:GenericPlugin runat="server" InstanceName="Ext.ux.TabReorderer" />
</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>


I used GenericPlugin this way because I could not see a TabReorderer plugin.

But even then it looks like Ext.ux.TabReorderer is not defined (not included in Ext.NET)??

Should I raise a separate forum post/request about this?

anup
Jan 14, 2013, 9:28 AM
Looks like you and I replied at same time -- ignore my question then :)

Just for my reference, here is a working example:


<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>