Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / examples / ux / BoxReorderer.js
1 Ext.define('Ext.ux.BoxReorderer', {
2     mixins: {
3         observable: 'Ext.util.Observable'
4     },
5
6     /**
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>
10      */
11     itemSelector: '.x-box-item',
12
13     /**
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>
18      */
19     animate: 100,
20
21     constructor: function() {
22         this.addEvents(
23             /**
24              * @event StartDrag
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.
30              */
31              'StartDrag',
32             /**
33              * @event Drag
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.
40              */
41              'Drag',
42             /**
43              * @event ChangeIndex
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.
50              */
51              'ChangeIndex',
52             /**
53              * @event 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.
60              */
61              'Drop'
62         );
63         this.mixins.observable.constructor.apply(this, arguments);
64     },
65
66     init: function(container) {
67         this.container = container;
68
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);
71
72         container.destroy = Ext.Function.createSequence(container.destroy, this.onContainerDestroy, this);
73     },
74
75     /**
76      * @private Clear up on Container destroy
77      */
78     onContainerDestroy: function() {
79         if (this.dd) {
80             this.dd.unreg();
81         }
82     },
83
84     afterFirstLayout: function() {
85         var me = this,
86             l = me.container.getLayout();
87
88         // delete the sequence
89         delete me.container.afterLayout;
90
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');
97         Ext.apply(me.dd, {
98             animate: me.animate,
99             reorderer: me,
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,
105             onDrag: me.onDrag,
106             endDrag: me.endDrag,
107             getNewIndex: me.getNewIndex,
108             doSwap: me.doSwap,
109             findReorderable: me.findReorderable
110         });
111
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;
117     },
118
119     getDragCmp: function(e) {
120         return this.container.getChildByElement(e.getTarget(this.itemSelector, 10));
121     },
122
123     // check if the clicked component is reorderable
124     clickValidator: function(e) {
125         var cmp = this.getDragCmp(e);
126
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);
129     },
130
131     onMouseDown: function(e) {
132         var me = this,
133             container = me.container,
134             containerBox,
135             cmpEl,
136             cmpBox;
137
138         // Ascertain which child Component is being mousedowned
139         me.dragCmp = me.getDragCmp(e);
140         if (me.dragCmp) {
141             cmpEl = me.dragCmp.getEl();
142             me.startIndex = me.curIndex = container.items.indexOf(me.dragCmp);
143
144             // Start position of dragged Component
145             cmpBox = cmpEl.getPageBox();
146
147             // Last tracked start position
148             me.lastPos = cmpBox[this.startAttr];
149
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;
158             } else {
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;
163             }
164             me.constrainY = me.constrainX = true;
165         }
166     },
167
168     startDrag: function() {
169         var me = this;
170         if (me.dragCmp) {
171             // For the entire duration of dragging the *Element*, defeat any positioning of the dragged *Component*
172             me.dragCmp.setPosition = Ext.emptyFn;
173
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;
178             }
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);
187
188
189         } else {
190             me.dragElId = null;
191         }
192     },
193
194     /**
195      * @private
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.
199      */
200     findReorderable: function(newIndex) {
201         var me = this,
202             items = me.container.items,
203             newItem;
204
205         if (items.getAt(newIndex).reorderable === false) {
206             newItem = items.getAt(newIndex);
207             if (newIndex > me.startIndex) {
208                  while(newItem && newItem.reorderable === false) {
209                     newIndex++;
210                     newItem = items.getAt(newIndex);
211                 }
212             } else {
213                 while(newItem && newItem.reorderable === false) {
214                     newIndex--;
215                     newItem = items.getAt(newIndex);
216                 }
217             }
218         }
219
220         newIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
221
222         if (items.getAt(newIndex).reorderable === false) {
223             return -1;
224         }
225         return newIndex;
226     },
227
228     /**
229      * @private
230      * Swap 2 components.
231      * @param {Number} newIndex The initial drop index.
232      */
233     doSwap: function(newIndex) {
234         var me = this,
235             items = me.container.items,
236             orig, dest, tmpIndex;
237
238         newIndex = me.findReorderable(newIndex);
239
240         if (newIndex === -1) {
241             return;
242         }
243
244         me.reorderer.fireEvent('ChangeIndex', me, me.container, me.dragCmp, me.startIndex, newIndex);
245         orig = items.getAt(me.curIndex);
246         dest = items.getAt(newIndex);
247         items.remove(orig);
248         tmpIndex = Math.min(Math.max(newIndex, 0), items.getCount() - 1);
249         items.insert(tmpIndex, orig);
250         items.remove(dest);
251         items.insert(me.curIndex, dest);
252
253         me.container.layout.layout();
254         me.curIndex = newIndex;
255     },
256
257     onDrag: function(e) {
258         var me = this,
259             newIndex;
260
261         newIndex = me.getNewIndex(e.getPoint());
262         if ((newIndex !== undefined)) {
263             me.reorderer.fireEvent('Drag', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
264             me.doSwap(newIndex);
265         }
266
267     },
268
269     endDrag: function(e) {
270         e.stopEvent();
271         var me = this;
272         if (me.dragCmp) {
273             delete me.dragElId;
274             if (me.animate) {
275                 me.container.layout.animate = {
276                     // Call afterBoxReflow after the animation finishes.
277                     callback: Ext.Function.bind(me.reorderer.afterBoxReflow, me)
278                 };
279             }
280
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();
285
286             if (me.removeAnimate) {
287                 delete me.removeAnimate;
288                 delete me.container.layout.animate;
289             } else {
290                 me.reorderer.afterBoxReflow.call(me);
291             }
292             me.reorderer.fireEvent('drop', me, me.container, me.dragCmp, me.startIndex, me.curIndex);
293         }
294     },
295
296     /**
297      * @private
298      * Called after the boxes have been reflowed after the drop.
299      */
300     afterBoxReflow: function() {
301         var me = this;
302         me.dragCmp.el.setStyle('zIndex', '');
303         me.dragCmp.disabled = false;
304         me.dragCmp.resumeEvents();
305     },
306
307     /**
308      * @private
309      * Calculate drop index based upon the dragEl's position.
310      */
311     getNewIndex: function(pointerPos) {
312         var me = this,
313             dragEl = me.getDragEl(),
314             dragBox = Ext.fly(dragEl).getPageBox(),
315             targetEl,
316             targetBox,
317             targetMidpoint,
318             i = 0,
319             it = me.container.items.items,
320             ln = it.length,
321             lastPos = me.lastPos;
322
323         me.lastPos = dragBox[me.startAttr];
324
325         for (; i < ln; i++) {
326             targetEl = it[i].getEl();
327
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))) {
334                         return i;
335                     }
336                 } else if (i > me.curIndex) {
337                     if ((dragBox[me.startAttr] > lastPos) && (dragBox[me.endAttr] > (targetMidpoint + 5))) {
338                         return i;
339                     }
340                 }
341             }
342         }
343     }
344 });