Draggable Tabs on TabPanel (Ext.NET 1.x only)

  1. #1

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

    Click image for larger version. 

Name:	ext-draggable-tabs.png 
Views:	284 
Size:	8.9 KB 
ID:	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.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!
  2. #2
    Please note that in v2 there is TabReorderer plugin already
  3. #3
    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/#!...x.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?
  4. #4
    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
    https://examples2.ext.net/#/Layout/H...ut/Reordering/
    https://examples2.ext.net/#/Layout/V...ut/Reordering/
    https://examples2.ext.net/#/Toolbar/...lbarDroppable/
    https://examples2.ext.net/#/Toolbar/...arReorderable/
    https://examples2.ext.net/#/GridPane...Sorting_Local/
  5. #5
    Quote Originally Posted by anup View Post
    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?
  6. #6
    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>
    Last edited by anup; Jan 14, 2013 at 9:31 AM.

Similar Threads

  1. Add tabs to the tabpanel
    By Vaishali in forum 1.x Help
    Replies: 1
    Last Post: Oct 04, 2012, 11:23 AM
  2. Tabs are not added to tabPanel
    By Vaishali in forum 1.x Help
    Replies: 26
    Last Post: Feb 21, 2012, 8:13 AM
  3. icon on tabs in tabpanel
    By silverstarsky in forum 1.x Help
    Replies: 2
    Last Post: Nov 20, 2009, 5:11 AM
  4. Hi, Why the TabPanel cannot update 2 tabs?
    By bruce in forum 1.x Help
    Replies: 2
    Last Post: Apr 20, 2009, 10:25 PM

Posting Permissions