Description
在 ExtJS 5.1 中使用 tabpanel,要求每个 tab 可以拖动,顺序可以改变。tabpanel 默认是不支持的,不过可以加上一个 plugin - Ext.ux.TabReorderer。在 ExtJS官网(http://docs.sencha.com/extjs/5.1/5.1.0-apidocs/)搜索 TabReorderer 可以查看其源码,发现其继承自 Ext.ux.BoxReorderer, 把两个源码 js 都放到项目中去。见附录。
Solution
先把把'Ext.ux.TabReorderer' require 进来,在创建 tabpanel 的时候 加上 plugins: Ext.create('Ext.ux.TabReorderer'),
1 Ext.define('MyApp.view.CenterTabs', { 2 extend: 'Ext.tab.Panel', 3 plugins: Ext.create('Ext.ux.TabReorderer'), 4 items: [{ 5 xtype: 'panel', 6 title: 'Tab 1', 7 html : 'Test 1', 8 closable: true 9 }, {10 xtype: 'panel',11 title: 'Tab 2',12 html : 'Test 2',13 closable: true14 },{15 xtype: 'panel',16 title: 'Tab 3',17 html : 'Test 3',18 closable: true19 },{20 xtype: 'panel',21 title: 'Tab 4',22 html : 'Test 4',23 closable: true24 }]25 });
Reference
- https://www.sencha.com/forum/showthread.php?211867-Using-TabReorderer-as-a-plugin
- http://docs.sencha.com/extjs/5.1/5.1.0-apidocs/source/TabReorderer.html#Ext-ux-TabReorderer
- http://docs.sencha.com/extjs/5.1/5.1.0-apidocs/source/BoxReorderer.html#Ext-ux-BoxReorderer
Appendix
/** * Base class from Ext.ux.TabReorderer. */Ext.define('Ext.ux.BoxReorderer', { requires: [ 'Ext.dd.DD' ], mixins: { observable: 'Ext.util.Observable' }, /** * @cfg {String} itemSelector * A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child * Components which participate in reordering. */ itemSelector: '.x-box-item', /** * @cfg {Mixed} animate * If truthy, child reordering is animated so that moved boxes slide smoothly into position. * If this option is numeric, it is used as the animation duration in milliseconds. */ animate: 100, /** * @event StartDrag * Fires when dragging of a child Component begins. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dragged * @param {Number} idx The start index of the Component being dragged. */ /** * @event Drag * Fires during dragging of a child Component. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dragged * @param {Number} startIdx The index position from which the Component was initially dragged. * @param {Number} idx The current closest index to which the Component would drop. */ /** * @event ChangeIndex * Fires when dragging of a child Component causes its drop index to change. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dragged * @param {Number} startIdx The index position from which the Component was initially dragged. * @param {Number} idx The current closest index to which the Component would drop. */ /** * @event Drop * Fires when a child Component is dropped at a new index position. * @param {Ext.ux.BoxReorderer} this * @param {Ext.container.Container} container The owning Container * @param {Ext.Component} dragCmp The Component being dropped * @param {Number} startIdx The index position from which the Component was initially dragged. * @param {Number} idx The index at which the Component is being dropped. */ constructor: function() { this.mixins.observable.constructor.apply(this, arguments); }, init: function(container) { var me = this; me.container = container; // Set our animatePolicy to animate the start position (ie x for HBox, y for VBox) me.animatePolicy = {}; me.animatePolicy[container.getLayout().names.x] = true; // Initialize the DD on first layout, when the innerCt has been created. me.container.on({ scope: me, boxready: me.onBoxReady, beforedestroy: me.onContainerDestroy }); }, /** * @private Clear up on Container destroy */ onContainerDestroy: function() { var dd = this.dd; if (dd) { dd.unreg(); this.dd = null; } }, onBoxReady: function() { var me = this, layout = me.container.getLayout(), names = layout.names, dd; // Create a DD instance. Poke the handlers in. // TODO: Ext5's DD classes should apply config to themselves. // TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin // TODO: Ext5's DD classes should be Observable. // TODO: When all the above are trus, this plugin should extend the DD class. dd = me.dd = new Ext.dd.DD(layout.innerCt, me.container.id + '-reorderer'); Ext.apply(dd, { animate: me.animate, reorderer: me, container: me.container, getDragCmp: me.getDragCmp, clickValidator: Ext.Function.createInterceptor(dd.clickValidator, me.clickValidator, me, false), onMouseDown: me.onMouseDown, startDrag: me.startDrag, onDrag: me.onDrag, endDrag: me.endDrag, getNewIndex: me.getNewIndex, doSwap: me.doSwap, findReorderable: me.findReorderable }); // Decide which dimension we are measuring, and which measurement metric defines // the *start* of the box depending upon orientation. dd.dim = names.width; dd.startAttr = names.beforeX; dd.endAttr = names.afterX; }, getDragCmp: function(e) { return this.container.getChildByElement(e.getTarget(this.itemSelector, 10)); }, // check if the clicked component is reorderable clickValidator: function(e) { var cmp = this.getDragCmp(e); // If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false return !!(cmp && cmp.reorderable !== false); }, onMouseDown: function(e) { var me = this, container = me.container, containerBox, cmpEl, cmpBox; // Ascertain which child Component is being mousedowned me.dragCmp = me.getDragCmp(e); if (me.dragCmp) { cmpEl = me.dragCmp.getEl(); me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp); // Start position of dragged Component cmpBox = cmpEl.getBox(); // Last tracked start position me.lastPos = cmpBox[me.startAttr]; // Calculate constraints depending upon orientation // Calculate offset from mouse to dragEl position containerBox = container.el.getBox(); if (me.dim === 'width') { me.minX = containerBox.left; me.maxX = containerBox.right - cmpBox.width; me.minY = me.maxY = cmpBox.top; me.deltaX = e.getX() - cmpBox.left; } else { me.minY = containerBox.top; me.maxY = containerBox.bottom - cmpBox.height; me.minX = me.maxX = cmpBox.left; me.deltaY = e.getY() - cmpBox.top; } me.constrainY = me.constrainX = true; } }, startDrag: function() { var me = this, dragCmp = me.dragCmp; if (dragCmp) { // For the entire duration of dragging the *Element*, defeat any positioning and animation of the dragged *Component* dragCmp.setPosition = Ext.emptyFn; dragCmp.animate = false; // Animate the BoxLayout just for the duration of the drag operation. if (me.animate) { me.container.getLayout().animatePolicy = me.reorderer.animatePolicy; } // We drag the Component element me.dragElId = dragCmp.getEl().id; me.reorderer.fireEvent('StartDrag', me, me.container, dragCmp, me.curIndex); // Suspend events, and set the disabled flag so that the mousedown and mouseup events // that are going to take place do not cause any other UI interaction. dragCmp.suspendEvents(); dragCmp.disabled = true; dragCmp.el.setStyle('zIndex', 100); } else { me.dragElId = null; } }, /** * @private * Find next or previous reorderable component index. * @param {Number} newIndex The initial drop index. * @return {Number} The index of the reorderable component. */ findReorderable: function(newIndex) { var me = this, items = me.container.items, newItem; if (items.getAt(newIndex).reorderable === false) { newItem = items.getAt(newIndex); if (newIndex > me.startIndex) { while(newItem && newItem.reorderable === false) { newIndex++; newItem = items.getAt(newIndex); } } else { while(newItem && newItem.reorderable === false) { newIndex--; newItem = items.getAt(newIndex); } } } newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1); if (items.getAt(newIndex).reorderable === false) { return -1; } return newIndex; }, /** * @private * Swap 2 components. * @param {Number} newIndex The initial drop index. */ doSwap: function(newIndex) { var me = this, items = me.container.items, container = me.container, wasRoot = me.container._isLayoutRoot, orig, dest, tmpIndex; newIndex = me.findReorderable(newIndex); if (newIndex === -1) { return; } me.reorderer.fireEvent('ChangeIndex', me, container, me.dragCmp, me.startIndex, newIndex); orig = items.getAt(me.curIndex); dest = items.getAt(newIndex); items.remove(orig); tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1); items.insert(tmpIndex, orig); items.remove(dest); items.insert(me.curIndex, dest); // Make the Box Container the topmost layout participant during the layout. container._isLayoutRoot = true; container.updateLayout(); container._isLayoutRoot = wasRoot; me.curIndex = newIndex; }, onDrag: function(e) { var me = this, newIndex; newIndex = me.getNewIndex(e.getPoint()); if ((newIndex !== undefined)) { me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex); me.doSwap(newIndex); } }, endDrag: function(e) { if (e) { e.stopEvent(); } var me = this, layout = me.container.getLayout(), temp; if (me.dragCmp) { delete me.dragElId; // Reinstate the Component's positioning method after mouseup, and allow the layout system to animate it. delete me.dragCmp.setPosition; me.dragCmp.animate = true; // Ensure the lastBox is correct for the animation system to restore to when it creates the "from" animation frame me.dragCmp.lastBox[layout.names.x] = me.dragCmp.getPosition(true)[layout.names.widthIndex]; // Make the Box Container the topmost layout participant during the layout. me.container._isLayoutRoot = true; me.container.updateLayout(); me.container._isLayoutRoot = undefined; // Attempt to hook into the afteranimate event of the drag Component to call the cleanup temp = Ext.fx.Manager.getFxQueue(me.dragCmp.el.id)[0]; if (temp) { temp.on({ afteranimate: me.reorderer.afterBoxReflow, scope: me }); } // If not animated, clean up after the mouseup has happened so that we don't click the thing being dragged else { Ext.Function.defer(me.reorderer.afterBoxReflow, 1, me); } if (me.animate) { delete layout.animatePolicy; } me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex); } }, /** * @private * Called after the boxes have been reflowed after the drop. * Re-enabled the dragged Component. */ afterBoxReflow: function() { var me = this; me.dragCmp.el.setStyle('zIndex', ''); me.dragCmp.disabled = false; me.dragCmp.resumeEvents(); }, /** * @private * Calculate drop index based upon the dragEl's position. */ getNewIndex: function(pointerPos) { var me = this, dragEl = me.getDragEl(), dragBox = Ext.fly(dragEl).getBox(), targetEl, targetBox, targetMidpoint, i = 0, it = me.container.items.items, ln = it.length, lastPos = me.lastPos; me.lastPos = dragBox[me.startAttr]; for (; i < ln; i++) { targetEl = it[i].getEl(); // Only look for a drop point if this found item is an item according to our selector if (targetEl.is(me.reorderer.itemSelector)) { targetBox = targetEl.getBox(); targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1); if (i < me.curIndex) { if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) { return i; } } else if (i > me.curIndex) { if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) { return i; } } } } }});
/** * This plugin allow you to reorder tabs of a TabPanel. */Ext.define('Ext.ux.TabReorderer', { extend: 'Ext.ux.BoxReorderer', alias: 'plugin.tabreorderer', itemSelector: '.' + Ext.baseCSSPrefix + 'tab', init: function(tabPanel) { var me = this; me.callParent([tabPanel.getTabBar()]); // Ensure reorderable property is copied into dynamically added tabs tabPanel.onAdd = Ext.Function.createSequence(tabPanel.onAdd, me.onAdd); }, onBoxReady: function() { var tabs, len, i = 0, tab; this.callParent(arguments); // Copy reorderable property from card into tab for (tabs = this.container.items.items, len = tabs.length; i < len; i++) { tab = tabs[i]; if (tab.card) { tab.reorderable = tab.card.reorderable; } } }, onAdd: function(card, index) { card.tab.reorderable = card.reorderable; }, afterBoxReflow: function() { var me = this; // Cannot use callParent, this is not called in the scope of this plugin, but that of its Ext.dd.DD object Ext.ux.BoxReorderer.prototype.afterBoxReflow.apply(me, arguments); // Move the associated card to match the tab order if (me.dragCmp) { me.container.tabPanel.setActiveTab(me.dragCmp.card); me.container.tabPanel.move(me.startIndex, me.curIndex); } }});