The source code

    /*!
     * Ext JS Library 3.3.1
     * Copyright(c) 2006-2010 Sencha Inc.
     * licensing@sencha.com
     * http://www.sencha.com/license
     */
    /**
     * @class Ext.ux.Reorderer
     * @extends Object
     * Generic base class for handling reordering of items. This base class must be extended to provide the
     * actual reordering functionality - the base class just sets up events and abstract logic functions.
     * It will fire events and set defaults, deferring the actual reordering to a doReorder implementation.
     * See Ext.ux.TabReorderer for an example.
     */
    Ext.ux.Reorderer = Ext.extend(Object, {
        /**
         * @property defaults
         * @type Object
         * Object containing default values for plugin configuration details. These can be overridden when
         * constructing the plugin
         */
        defaults: {
            /**
             * @cfg animate
             * @type Boolean
             * If set to true, the rearranging of the toolbar items is animated
             */
            animate: true,

            /**
             * @cfg animationDuration
             * @type Number
             * The duration of the animation used to move other toolbar items out of the way
             */
            animationDuration: 0.2,

            /**
             * @cfg defaultReorderable
             * @type Boolean
             * True to make every toolbar draggable unless reorderable is specifically set to false.
             * This defaults to false
             */
            defaultReorderable: false
        },

        /**
         * Creates the plugin instance, applies defaults
         * @constructor
         * @param {Object} config Optional config object
         */
        constructor: function(config) {
            Ext.apply(this, config || {}, this.defaults);
        },

        /**
         * Initializes the plugin, stores a reference to the target
         * @param {Mixed} target The target component which contains the reorderable items
         */
        init: function(target) {
            /**
             * @property target
             * @type Ext.Component
             * Reference to the target component which contains the reorderable items
             */
            this.target = target;

            this.initEvents();

            var items  = this.getItems(),
                length = items.length,
                i;

            for (i = 0; i < length; i++) {
                this.createIfReorderable(items[i]);
            }
        },

        /**
         * Reorders the items in the target component according to the given mapping object. Example:
         * this.reorder({
         *     1: 5,
         *     3: 2
         * });
         * Would move the item at index 1 to index 5, and the item at index 3 to index 2
         * @param {Object} mappings Object containing current item index as key and new index as property
         */
        reorder: function(mappings) {
            var target = this.target;

            if (target.fireEvent('before-reorder', mappings, target, this) !== false) {
                this.doReorder(mappings);

                target.fireEvent('reorder', mappings, target, this);
            }
        },

        /**
         * Abstract function to perform the actual reordering. This MUST be overridden in a subclass
         * @param {Object} mappings Mappings of the old item indexes to new item indexes
         */
        doReorder: function(paramName) {
            throw new Error("doReorder must be implemented in the Ext.ux.Reorderer subclass");
        },

        /**
         * Should create and return an Ext.dd.DD for the given item. This MUST be overridden in a subclass
         * @param {Mixed} item The item to create a DD for. This could be a TabPanel tab, a Toolbar button, etc
         * @return {Ext.dd.DD} The DD for the given item
         */
        createItemDD: function(item) {
            throw new Error("createItemDD must be implemented in the Ext.ux.Reorderer subclass");
        },

        /**
         * Sets up the given Toolbar item as a draggable
         * @param {Mixed} button The item to make draggable (usually an Ext.Button instance)
         */
        createItemDD: function(button) {
            var el   = button.getEl(),
                id   = el.id,
                tbar = this.target,
                me   = this;

            button.dd = new Ext.dd.DD(el, undefined, {
                isTarget: false
            });

            button.dd.constrainTo(tbar.getEl());
            button.dd.setYConstraint(0, 0, 0);

            Ext.apply(button.dd, {
                b4StartDrag: function() {
                    this.startPosition = el.getXY();

                    //bump up the z index of the button being dragged but keep a reference to the original
                    this.startZIndex = el.getStyle('zIndex');
                    el.setStyle('zIndex', 10000);

                    button.suspendEvents();
                },

                onDrag: function(e) {
                    //calculate the button's index within the toolbar and its current midpoint
                    var buttonX  = el.getXY()[0],
                        deltaX   = buttonX - this.startPosition[0],
                        items    = tbar.items.items,
                        oldIndex = items.indexOf(button),
                        newIndex;

                    //find which item in the toolbar the midpoint is currently over
                    for (var index = 0; index < items.length; index++) {
                        var item = items[index];

                        if (item.reorderable && item.id != button.id) {
                            //find the midpoint of the button
                            var box        = item.getEl().getBox(),
                                midpoint   = (me.buttonXCache[item.id] || box.x) + (box.width / 2),
                                movedLeft  = oldIndex > index && deltaX < 0 && buttonX < midpoint,
                                movedRight = oldIndex < index && deltaX > 0 && (buttonX + el.getWidth()) > midpoint;

                            if (movedLeft || movedRight) {
                                me[movedLeft ? 'onMovedLeft' : 'onMovedRight'](button, index, oldIndex);
                                break;
                            }
                        }
                    }
                },

                /**
                 * After the drag has been completed, make sure the button being dragged makes it back to
                 * the correct location and resets its z index
                 */
                endDrag: function() {
                    //we need to update the cache here for cases where the button was dragged but its
                    //position in the toolbar did not change
                    me.updateButtonXCache();

                    el.moveTo(me.buttonXCache[button.id], undefined, {
                        duration: me.animationDuration,
                        scope   : this,
                        callback: function() {
                            button.resumeEvents();

                            tbar.fireEvent('reordered', button, tbar);
                        }
                    });

                    el.setStyle('zIndex', this.startZIndex);
                }
            });
        },

        /**
         * @private
         * Creates a DD instance for a given item if it is reorderable
         * @param {Mixed} item The item
         */
        createIfReorderable: function(item) {
            if (this.defaultReorderable && item.reorderable == undefined) {
                item.reorderable = true;
            }

            if (item.reorderable && !item.dd) {
                if (item.rendered) {
                    this.createItemDD(item);
                } else {
                    item.on('render', this.createItemDD.createDelegate(this, [item]), this, {single: true});
                }
            }
        },

        /**
         * Returns an array of items which will be made draggable. This defaults to the contents of this.target.items,
         * but can be overridden - e.g. for TabPanels
         * @return {Array} The array of items which will be made draggable
         */
        getItems: function() {
            return this.target.items.items;
        },

        /**
         * Adds before-reorder and reorder events to the target component
         */
        initEvents: function() {
            this.target.addEvents(
              /**
               * @event before-reorder
               * Fires before a reorder occurs. Return false to cancel
               * @param {Object} mappings Mappings of the old item indexes to new item indexes
               * @param {Mixed} component The target component
               * @param {Ext.ux.TabReorderer} this The plugin instance
               */
              'before-reorder',

              /**
               * @event reorder
               * Fires after a reorder has occured.
               * @param {Object} mappings Mappings of the old item indexes to the new item indexes
               * @param {Mixed} component The target component
               * @param {Ext.ux.TabReorderer} this The plugin instance
               */
              'reorder'
            );
        }
    });

    /**
     * @class Ext.ux.HBoxReorderer
     * @extends Ext.ux.Reorderer
     * Description
     */
    Ext.ux.HBoxReorderer = Ext.extend(Ext.ux.Reorderer, {
        /**
         * Initializes the plugin, decorates the container with additional functionality
         */
        init: function(container) {
            /**
             * This is used to store the correct x value of each button in the array. We need to use this
             * instead of the button's reported x co-ordinate because the buttons are animated when they move -
             * if another onDrag is fired while the button is still moving, the comparison x value will be incorrect
             */
            this.buttonXCache = {};

            container.on({
                scope: this,
                add  : function(container, item) {
                    this.createIfReorderable(item);
                }
            });

            //super sets a reference to the toolbar in this.target
            Ext.ux.HBoxReorderer.superclass.init.apply(this, arguments);
        },

        /**
         * Sets up the given Toolbar item as a draggable
         * @param {Mixed} button The item to make draggable (usually an Ext.Button instance)
         */
        createItemDD: function(button) {
            if (button.dd != undefined) {
                return;
            }

            var el   = button.getEl(),
                id   = el.id,
                me   = this,
                tbar = me.target;

            button.dd = new Ext.dd.DD(el, undefined, {
                isTarget: false
            });

            el.applyStyles({
                position: 'absolute'
            });

            //if a button has a menu, it is disabled while dragging with this function
            var menuDisabler = function() {
                return false;
            };

            Ext.apply(button.dd, {
                b4StartDrag: function() {
                    this.startPosition = el.getXY();

                    //bump up the z index of the button being dragged but keep a reference to the original
                    this.startZIndex = el.getStyle('zIndex');
                    el.setStyle('zIndex', 10000);

                    button.suspendEvents();
                    if (button.menu) {
                        button.menu.on('beforeshow', menuDisabler, me);
                    }
                },

                startDrag: function() {
                    this.constrainTo(tbar.getEl());
                    this.setYConstraint(0, 0, 0);
                },

                onDrag: function(e) {
                    //calculate the button's index within the toolbar and its current midpoint
                    var buttonX  = el.getXY()[0],
                        deltaX   = buttonX - this.startPosition[0],
                        items    = tbar.items.items,
                        length   = items.length,
                        oldIndex = items.indexOf(button),
                        newIndex, index, item;

                    //find which item in the toolbar the midpoint is currently over
                    for (index = 0; index < length; index++) {
                        item = items[index];

                        if (item.reorderable && item.id != button.id) {
                            //find the midpoint of the button
                            var box        = item.getEl().getBox(),
                                midpoint   = (me.buttonXCache[item.id] || box.x) + (box.width / 2),
                                movedLeft  = oldIndex > index && deltaX < 0 && buttonX < midpoint,
                                movedRight = oldIndex < index && deltaX > 0 && (buttonX + el.getWidth()) > midpoint;

                            if (movedLeft || movedRight) {
                                me[movedLeft ? 'onMovedLeft' : 'onMovedRight'](button, index, oldIndex);
                                break;
                            }
                        }
                    }
                },

                /**
                 * After the drag has been completed, make sure the button being dragged makes it back to
                 * the correct location and resets its z index
                 */
                endDrag: function() {
                    //we need to update the cache here for cases where the button was dragged but its
                    //position in the toolbar did not change
                    me.updateButtonXCache();

                    el.moveTo(me.buttonXCache[button.id], el.getY(), {
                        duration: me.animationDuration,
                        scope   : this,
                        callback: function() {
                            button.resumeEvents();
                            if (button.menu) {
                                button.menu.un('beforeshow', menuDisabler, me);
                            }

                            tbar.fireEvent('reordered', button, tbar);
                        }
                    });

                    el.setStyle('zIndex', this.startZIndex);
                }
            });
        },

        onMovedLeft: function(item, newIndex, oldIndex) {
            var tbar   = this.target,
                items  = tbar.items.items,
                length = items.length,
                index;

            if (newIndex != undefined && newIndex != oldIndex) {
                //move the button currently under drag to its new location
                tbar.remove(item, false);
                tbar.insert(newIndex, item);

                //set the correct x location of each item in the toolbar
                this.updateButtonXCache();
                for (index = 0; index < length; index++) {
                    var obj  = items[index],
                        newX = this.buttonXCache[obj.id];

                    if (item == obj) {
                        item.dd.startPosition[0] = newX;
                    } else {
                        var el = obj.getEl();

                        el.moveTo(newX, el.getY(), {
                            duration: this.animationDuration
                        });
                    }
                }
            }
        },

        onMovedRight: function(item, newIndex, oldIndex) {
            this.onMovedLeft.apply(this, arguments);
        },

        /**
         * @private
         * Updates the internal cache of button X locations.
         */
        updateButtonXCache: function() {
            var tbar   = this.target,
                items  = tbar.items,
                totalX = tbar.getEl().getBox(true).x;

            items.each(function(item) {
                this.buttonXCache[item.id] = totalX;

                totalX += item.getEl().getWidth();
            }, this);
        }
    });