1 Ext.define('Ext.ux.BoxReorderer', {
3 observable: 'Ext.util.Observable'
7 * @cfg {String} itemSelector
8 * <p>Optional. Defaults to <code>'.x-box-item'</code>
9 * <p>A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child Components which participate in reordering.</p>
11 itemSelector: '.x-box-item',
14 * @cfg {Mixed} animate
15 * <p>Defaults to 300.</p>
16 * <p>If truthy, child reordering is animated so that moved boxes slide smoothly into position.
17 * If this option is numeric, it is used as the animation duration <b>in milliseconds</b>.</p>
21 constructor: function() {
25 * Fires when dragging of a child Component begins.
26 * @param {BoxReorder} this
27 * @param {Container} container The owning Container
28 * @param {Component} dragCmp The Component being dragged
29 * @param {Number} idx The start index of the Component being dragged.
34 * Fires during dragging of a child Component.
35 * @param {BoxReorder} this
36 * @param {Container} container The owning Container
37 * @param {Component} dragCmp The Component being dragged
38 * @param {Number} startIdx The index position from which the Component was initially dragged.
39 * @param {Number} idx The current closest index to which the Component would drop.
44 * Fires when dragging of a child Component causes its drop index to change.
45 * @param {BoxReorder} this
46 * @param {Container} container The owning Container
47 * @param {Component} dragCmp The Component being dragged
48 * @param {Number} startIdx The index position from which the Component was initially dragged.
49 * @param {Number} idx The current closest index to which the Component would drop.
54 * Fires when a child Component is dropped at a new index position.
55 * @param {BoxReorder} this
56 * @param {Container} container The owning Container
57 * @param {Component} dragCmp The Component being dropped
58 * @param {Number} startIdx The index position from which the Component was initially dragged.
59 * @param {Number} idx The index at which the Component is being dropped.
63 this.mixins.observable.constructor.apply(this, arguments);
66 init: function(container) {
67 this.container = container;
69 // Initialize the DD on first layout, when the innerCt has been created.
70 this.container.afterLayout = Ext.Function.createSequence(this.container.afterLayout, this.afterFirstLayout, this);
72 container.destroy = Ext.Function.createSequence(container.destroy, this.onContainerDestroy, this);
76 * @private Clear up on Container destroy
78 onContainerDestroy: function() {
84 afterFirstLayout: function() {
86 l = me.container.getLayout();
88 // delete the sequence
89 delete me.container.afterLayout;
91 // Create a DD instance. Poke the handlers in.
92 // TODO: Ext5's DD classes should apply config to themselves.
93 // TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin
94 // TODO: Ext5's DD classes should be Observable.
95 // TODO: When all the above are trus, this plugin should extend the DD class.
96 me.dd = Ext.create('Ext.dd.DD', l.innerCt, me.container.id + '-reorderer');
100 container: me.container,
101 getDragCmp: this.getDragCmp,
102 clickValidator: Ext.Function.createInterceptor(me.dd.clickValidator, me.clickValidator, me, false),
103 onMouseDown: me.onMouseDown,
104 startDrag: me.startDrag,
107 getNewIndex: me.getNewIndex,
109 findReorderable: me.findReorderable
112 // Decide which dimension we are measuring, and which measurement metric defines
113 // the *start* of the box depending upon orientation.
114 me.dd.dim = l.parallelPrefix;
115 me.dd.startAttr = l.parallelBefore;
116 me.dd.endAttr = l.parallelAfter;
119 getDragCmp: function(e) {
120 return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
123 // check if the clicked component is reorderable
124 clickValidator: function(e) {
125 var cmp = this.getDragCmp(e);
127 // If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false
128 return !!(cmp && cmp.reorderable !== false);
131 onMouseDown: function(e) {
133 container = me.container,
138 // Ascertain which child Component is being mousedowned
139 me.dragCmp = me.getDragCmp(e);
141 cmpEl = me.dragCmp.getEl();
142 me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);
144 // Start position of dragged Component
145 cmpBox = cmpEl.getPageBox();
147 // Last tracked start position
148 me.lastPos = cmpBox[this.startAttr];
150 // Calculate constraints depending upon orientation
151 // Calculate offset from mouse to dragEl position
152 containerBox = container.el.getPageBox();
153 if (me.dim === 'width') {
154 me.minX = containerBox.left;
155 me.maxX = containerBox.right - cmpBox.width;
156 me.minY = me.maxY = cmpBox.top;
157 me.deltaX = e.getPageX() - cmpBox.left;
159 me.minY = containerBox.top;
160 me.maxY = containerBox.bottom - cmpBox.height;
161 me.minX = me.maxX = cmpBox.left;
162 me.deltaY = e.getPageY() - cmpBox.top;
164 me.constrainY = me.constrainX = true;
168 startDrag: function() {
171 // For the entire duration of dragging the *Element*, defeat any positioning of the dragged *Component*
172 me.dragCmp.setPosition = Ext.emptyFn;
174 // If the BoxLayout is not animated, animate it just for the duration of the drag operation.
175 if (!me.container.layout.animate && me.animate) {
176 me.container.layout.animate = me.animate;
177 me.removeAnimate = true;
179 // We drag the Component element
180 me.dragElId = me.dragCmp.getEl().id;
181 me.reorderer.fireEvent('StartDrag', me, me.container, me.dragCmp, me.curIndex);
182 // Suspend events, and set the disabled flag so that the mousedown and mouseup events
183 // that are going to take place do not cause any other UI interaction.
184 me.dragCmp.suspendEvents();
185 me.dragCmp.disabled = true;
186 me.dragCmp.el.setStyle('zIndex', 100);
196 * Find next or previous reorderable component index.
197 * @param {Number} newIndex The initial drop index.
198 * @return {Number} The index of the reorderable component.
200 findReorderable: function(newIndex) {
202 items = me.container.items,
205 if (items.getAt(newIndex).reorderable === false) {
206 newItem = items.getAt(newIndex);
207 if (newIndex > me.startIndex) {
208 while(newItem && newItem.reorderable === false) {
210 newItem = items.getAt(newIndex);
213 while(newItem && newItem.reorderable === false) {
215 newItem = items.getAt(newIndex);
220 newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
222 if (items.getAt(newIndex).reorderable === false) {
231 * @param {Number} newIndex The initial drop index.
233 doSwap: function(newIndex) {
235 items = me.container.items,
236 orig, dest, tmpIndex;
238 newIndex = me.findReorderable(newIndex);
240 if (newIndex === -1) {
244 me.reorderer.fireEvent('ChangeIndex', me, me.container, me.dragCmp, me.startIndex, newIndex);
245 orig = items.getAt(me.curIndex);
246 dest = items.getAt(newIndex);
248 tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
249 items.insert(tmpIndex, orig);
251 items.insert(me.curIndex, dest);
253 me.container.layout.layout();
254 me.curIndex = newIndex;
257 onDrag: function(e) {
261 newIndex = me.getNewIndex(e.getPoint());
262 if ((newIndex !== undefined)) {
263 me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
269 endDrag: function(e) {
275 me.container.layout.animate = {
276 // Call afterBoxReflow after the animation finishes.
277 callback: Ext.Function.bind(me.reorderer.afterBoxReflow, me)
281 // Reinstate the Component's positioning method after mouseup.
282 // Call the layout directly: Bypass the layoutBusy barrier
283 delete me.dragCmp.setPosition;
284 me.container.layout.layout();
286 if (me.removeAnimate) {
287 delete me.removeAnimate;
288 delete me.container.layout.animate;
290 me.reorderer.afterBoxReflow.call(me);
292 me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
298 * Called after the boxes have been reflowed after the drop.
300 afterBoxReflow: function() {
302 me.dragCmp.el.setStyle('zIndex', '');
303 me.dragCmp.disabled = false;
304 me.dragCmp.resumeEvents();
309 * Calculate drop index based upon the dragEl's position.
311 getNewIndex: function(pointerPos) {
313 dragEl = me.getDragEl(),
314 dragBox = Ext.fly(dragEl).getPageBox(),
319 it = me.container.items.items,
321 lastPos = me.lastPos;
323 me.lastPos = dragBox[me.startAttr];
325 for (; i < ln; i++) {
326 targetEl = it[i].getEl();
328 // Only look for a drop point if this found item is an item according to our selector
329 if (targetEl.is(me.reorderer.itemSelector)) {
330 targetBox = targetEl.getPageBox();
331 targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1);
332 if (i < me.curIndex) {
333 if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) {
336 } else if (i > me.curIndex) {
337 if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {