Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / examples / ux / BoxReorderer.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 Ext.define('Ext.ux.BoxReorderer', {
16     mixins: {
17         observable: 'Ext.util.Observable'
18     },
19
20     /**
21      * @cfg {String} itemSelector
22      * <p>Optional. Defaults to <code>'.x-box-item'</code>
23      * <p>A {@link Ext.DomQuery DomQuery} selector which identifies the encapsulating elements of child Components which participate in reordering.</p>
24      */
25     itemSelector: '.x-box-item',
26
27     /**
28      * @cfg {Mixed} animate
29      * <p>Defaults to 300.</p>
30      * <p>If truthy, child reordering is animated so that moved boxes slide smoothly into position.
31      * If this option is numeric, it is used as the animation duration <b>in milliseconds</b>.</p>
32      */
33     animate: 100,
34
35     constructor: function() {
36         this.addEvents(
37             /**
38              * @event StartDrag
39              * Fires when dragging of a child Component begins.
40              * @param {BoxReorder} this
41              * @param {Container} container The owning Container
42              * @param {Component} dragCmp The Component being dragged
43              * @param {Number} idx The start index of the Component being dragged.
44              */
45              'StartDrag',
46             /**
47              * @event Drag
48              * Fires during dragging of a child Component.
49              * @param {BoxReorder} this
50              * @param {Container} container The owning Container
51              * @param {Component} dragCmp The Component being dragged
52              * @param {Number} startIdx The index position from which the Component was initially dragged.
53              * @param {Number} idx The current closest index to which the Component would drop.
54              */
55              'Drag',
56             /**
57              * @event ChangeIndex
58              * Fires when dragging of a child Component causes its drop index to change.
59              * @param {BoxReorder} this
60              * @param {Container} container The owning Container
61              * @param {Component} dragCmp The Component being dragged
62              * @param {Number} startIdx The index position from which the Component was initially dragged.
63              * @param {Number} idx The current closest index to which the Component would drop.
64              */
65              'ChangeIndex',
66             /**
67              * @event Drop
68              * Fires when a child Component is dropped at a new index position.
69              * @param {BoxReorder} this
70              * @param {Container} container The owning Container
71              * @param {Component} dragCmp The Component being dropped
72              * @param {Number} startIdx The index position from which the Component was initially dragged.
73              * @param {Number} idx The index at which the Component is being dropped.
74              */
75              'Drop'
76         );
77         this.mixins.observable.constructor.apply(this, arguments);
78     },
79
80     init: function(container) {
81         this.container = container;
82
83         // Initialize the DD on first layout, when the innerCt has been created.
84         this.container.afterLayout = Ext.Function.createSequence(this.container.afterLayout, this.afterFirstLayout, this);
85
86         container.destroy = Ext.Function.createSequence(container.destroy, this.onContainerDestroy, this);
87     },
88
89     /**
90      * @private Clear up on Container destroy
91      */
92     onContainerDestroy: function() {
93         if (this.dd) {
94             this.dd.unreg();
95         }
96     },
97
98     afterFirstLayout: function() {
99         var me = this,
100             l = me.container.getLayout();
101
102         // delete the sequence
103         delete me.container.afterLayout;
104
105         // Create a DD instance. Poke the handlers in.
106         // TODO: Ext5's DD classes should apply config to themselves.
107         // TODO: Ext5's DD classes should not use init internally because it collides with use as a plugin
108         // TODO: Ext5's DD classes should be Observable.
109         // TODO: When all the above are trus, this plugin should extend the DD class.
110         me.dd = Ext.create('Ext.dd.DD', l.innerCt, me.container.id + '-reorderer');
111         Ext.apply(me.dd, {
112             animate: me.animate,
113             reorderer: me,
114             container: me.container,
115             getDragCmp: this.getDragCmp,
116             clickValidator: Ext.Function.createInterceptor(me.dd.clickValidator, me.clickValidator, me, false),
117             onMouseDown: me.onMouseDown,
118             startDrag: me.startDrag,
119             onDrag: me.onDrag,
120             endDrag: me.endDrag,
121             getNewIndex: me.getNewIndex,
122             doSwap: me.doSwap,
123             findReorderable: me.findReorderable
124         });
125
126         // Decide which dimension we are measuring, and which measurement metric defines
127         // the *start* of the box depending upon orientation.
128         me.dd.dim = l.parallelPrefix;
129         me.dd.startAttr = l.parallelBefore;
130         me.dd.endAttr = l.parallelAfter;
131     },
132
133     getDragCmp: function(e) {
134         return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
135     },
136
137     // check if the clicked component is reorderable
138     clickValidator: function(e) {
139         var cmp = this.getDragCmp(e);
140
141         // If cmp is null, this expression MUST be coerced to boolean so that createInterceptor is able to test it against false
142         return !!(cmp && cmp.reorderable !== false);
143     },
144
145     onMouseDown: function(e) {
146         var me = this,
147             container = me.container,
148             containerBox,
149             cmpEl,
150             cmpBox;
151
152         // Ascertain which child Component is being mousedowned
153         me.dragCmp = me.getDragCmp(e);
154         if (me.dragCmp) {
155             cmpEl = me.dragCmp.getEl();
156             me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);
157
158             // Start position of dragged Component
159             cmpBox = cmpEl.getPageBox();
160
161             // Last tracked start position
162             me.lastPos = cmpBox[this.startAttr];
163
164             // Calculate constraints depending upon orientation
165             // Calculate offset from mouse to dragEl position
166             containerBox = container.el.getPageBox();
167             if (me.dim === 'width') {
168                 me.minX = containerBox.left;
169                 me.maxX = containerBox.right - cmpBox.width;
170                 me.minY = me.maxY = cmpBox.top;
171                 me.deltaX = e.getPageX() - cmpBox.left;
172             } else {
173                 me.minY = containerBox.top;
174                 me.maxY = containerBox.bottom - cmpBox.height;
175                 me.minX = me.maxX = cmpBox.left;
176                 me.deltaY = e.getPageY() - cmpBox.top;
177             }
178             me.constrainY = me.constrainX = true;
179         }
180     },
181
182     startDrag: function() {
183         var me = this;
184         if (me.dragCmp) {
185             // For the entire duration of dragging the *Element*, defeat any positioning of the dragged *Component*
186             me.dragCmp.setPosition = Ext.emptyFn;
187
188             // If the BoxLayout is not animated, animate it just for the duration of the drag operation.
189             if (!me.container.layout.animate && me.animate) {
190                 me.container.layout.animate = me.animate;
191                 me.removeAnimate = true;
192             }
193             // We drag the Component element
194             me.dragElId = me.dragCmp.getEl().id;
195             me.reorderer.fireEvent('StartDrag', me, me.container, me.dragCmp, me.curIndex);
196             // Suspend events, and set the disabled flag so that the mousedown and mouseup events
197             // that are going to take place do not cause any other UI interaction.
198             me.dragCmp.suspendEvents();
199             me.dragCmp.disabled = true;
200             me.dragCmp.el.setStyle('zIndex', 100);
201
202
203         } else {
204             me.dragElId = null;
205         }
206     },
207
208     /**
209      * @private
210      * Find next or previous reorderable component index.
211      * @param {Number} newIndex The initial drop index.
212      * @return {Number} The index of the reorderable component.
213      */
214     findReorderable: function(newIndex) {
215         var me = this,
216             items = me.container.items,
217             newItem;
218
219         if (items.getAt(newIndex).reorderable === false) {
220             newItem = items.getAt(newIndex);
221             if (newIndex > me.startIndex) {
222                  while(newItem && newItem.reorderable === false) {
223                     newIndex++;
224                     newItem = items.getAt(newIndex);
225                 }
226             } else {
227                 while(newItem && newItem.reorderable === false) {
228                     newIndex--;
229                     newItem = items.getAt(newIndex);
230                 }
231             }
232         }
233
234         newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
235
236         if (items.getAt(newIndex).reorderable === false) {
237             return -1;
238         }
239         return newIndex;
240     },
241
242     /**
243      * @private
244      * Swap 2 components.
245      * @param {Number} newIndex The initial drop index.
246      */
247     doSwap: function(newIndex) {
248         var me = this,
249             items = me.container.items,
250             orig, dest, tmpIndex;
251
252         newIndex = me.findReorderable(newIndex);
253
254         if (newIndex === -1) {
255             return;
256         }
257
258         me.reorderer.fireEvent('ChangeIndex', me, me.container, me.dragCmp, me.startIndex, newIndex);
259         orig = items.getAt(me.curIndex);
260         dest = items.getAt(newIndex);
261         items.remove(orig);
262         tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
263         items.insert(tmpIndex, orig);
264         items.remove(dest);
265         items.insert(me.curIndex, dest);
266
267         me.container.layout.layout();
268         me.curIndex = newIndex;
269     },
270
271     onDrag: function(e) {
272         var me = this,
273             newIndex;
274
275         newIndex = me.getNewIndex(e.getPoint());
276         if ((newIndex !== undefined)) {
277             me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
278             me.doSwap(newIndex);
279         }
280
281     },
282
283     endDrag: function(e) {
284         e.stopEvent();
285         var me = this;
286         if (me.dragCmp) {
287             delete me.dragElId;
288             if (me.animate) {
289                 me.container.layout.animate = {
290                     // Call afterBoxReflow after the animation finishes.
291                     callback: Ext.Function.bind(me.reorderer.afterBoxReflow, me)
292                 };
293             }
294
295             // Reinstate the Component's positioning method after mouseup.
296             // Call the layout directly: Bypass the layoutBusy barrier
297             delete me.dragCmp.setPosition;
298             me.container.layout.layout();
299
300             if (me.removeAnimate) {
301                 delete me.removeAnimate;
302                 delete me.container.layout.animate;
303             } else {
304                 me.reorderer.afterBoxReflow.call(me);
305             }
306             me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
307         }
308     },
309
310     /**
311      * @private
312      * Called after the boxes have been reflowed after the drop.
313      */
314     afterBoxReflow: function() {
315         var me = this;
316         me.dragCmp.el.setStyle('zIndex', '');
317         me.dragCmp.disabled = false;
318         me.dragCmp.resumeEvents();
319     },
320
321     /**
322      * @private
323      * Calculate drop index based upon the dragEl's position.
324      */
325     getNewIndex: function(pointerPos) {
326         var me = this,
327             dragEl = me.getDragEl(),
328             dragBox = Ext.fly(dragEl).getPageBox(),
329             targetEl,
330             targetBox,
331             targetMidpoint,
332             i = 0,
333             it = me.container.items.items,
334             ln = it.length,
335             lastPos = me.lastPos;
336
337         me.lastPos = dragBox[me.startAttr];
338
339         for (; i < ln; i++) {
340             targetEl = it[i].getEl();
341
342             // Only look for a drop point if this found item is an item according to our selector
343             if (targetEl.is(me.reorderer.itemSelector)) {
344                 targetBox = targetEl.getPageBox();
345                 targetMidpoint = targetBox[me.startAttr] + (targetBox[me.dim] >> 1);
346                 if (i < me.curIndex) {
347                     if ((dragBox[me.startAttr] < lastPos) && (dragBox[me.startAttr] < (targetMidpoint - 5))) {
348                         return i;
349                     }
350                 } else if (i > me.curIndex) {
351                     if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {
352                         return i;
353                     }
354                 }
355             }
356         }
357     }
358 });
359