/** * Base class from Ext.ux.TabReorderer. */ Ext.define('Ext.ux.BoxReorderer', { 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, constructor: function() { this.addEvents( /** * @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. */ 'StartDrag', /** * @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. */ 'Drag', /** * @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. */ 'ChangeIndex', /** * @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. */ 'Drop' ); 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.afterFirstLayout, destroy: me.onContainerDestroy }); }, /** * @private Clear up on Container destroy */ onContainerDestroy: function() { if (this.dd) { this.dd.unreg(); } }, afterFirstLayout: 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 = Ext.create('Ext.dd.DD', layout.innerCt, me.container.id + '-reorderer'); Ext.apply(dd, { animate: me.animate, reorderer: me, container: me.container, getDragCmp: this.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.left; dd.endAttr = names.right; }, 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.getPageBox(); // Last tracked start position me.lastPos = cmpBox[this.startAttr]; // Calculate constraints depending upon orientation // Calculate offset from mouse to dragEl position containerBox = container.el.getPageBox(); if (me.dim === 'width') { me.minX = containerBox.left; me.maxX = containerBox.right - cmpBox.width; me.minY = me.maxY = cmpBox.top; me.deltaX = e.getPageX() - cmpBox.left; } else { me.minY = containerBox.top; me.maxY = containerBox.bottom - cmpBox.height; me.minX = me.maxX = cmpBox.left; me.deltaY = e.getPageY() - 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, temp; 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).getPageBox(), 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.getPageBox(); 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; } } } } } });