Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / examples / ux / Reorderer.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.com/license
6  */
7 /**
8  * @class Ext.ux.Reorderer
9  * @extends Object
10  * Generic base class for handling reordering of items. This base class must be extended to provide the
11  * actual reordering functionality - the base class just sets up events and abstract logic functions.
12  * It will fire events and set defaults, deferring the actual reordering to a doReorder implementation.
13  * See Ext.ux.TabReorderer for an example.
14  */
15 Ext.ux.Reorderer = Ext.extend(Object, {
16     /**
17      * @property defaults
18      * @type Object
19      * Object containing default values for plugin configuration details. These can be overridden when
20      * constructing the plugin
21      */
22     defaults: {
23         /**
24          * @cfg animate
25          * @type Boolean
26          * If set to true, the rearranging of the toolbar items is animated
27          */
28         animate: true,
29         
30         /**
31          * @cfg animationDuration
32          * @type Number
33          * The duration of the animation used to move other toolbar items out of the way
34          */
35         animationDuration: 0.2,
36         
37         /**
38          * @cfg defaultReorderable
39          * @type Boolean
40          * True to make every toolbar draggable unless reorderable is specifically set to false.
41          * This defaults to false
42          */
43         defaultReorderable: false
44     },
45     
46     /**
47      * Creates the plugin instance, applies defaults
48      * @constructor
49      * @param {Object} config Optional config object
50      */
51     constructor: function(config) {
52         Ext.apply(this, config || {}, this.defaults);
53     },
54     
55     /**
56      * Initializes the plugin, stores a reference to the target 
57      * @param {Mixed} target The target component which contains the reorderable items
58      */
59     init: function(target) {
60         /**
61          * @property target
62          * @type Ext.Component
63          * Reference to the target component which contains the reorderable items
64          */
65         this.target = target;
66         
67         this.initEvents();
68         
69         var items  = this.getItems(),
70             length = items.length,
71             i;
72         
73         for (i = 0; i < length; i++) {
74             this.createIfReorderable(items[i]);
75         }
76     },
77     
78     /**
79      * Reorders the items in the target component according to the given mapping object. Example:
80      * this.reorder({
81      *     1: 5,
82      *     3: 2
83      * });
84      * Would move the item at index 1 to index 5, and the item at index 3 to index 2
85      * @param {Object} mappings Object containing current item index as key and new index as property
86      */
87     reorder: function(mappings) {
88         var target = this.target;
89         
90         if (target.fireEvent('before-reorder', mappings, target, this) !== false) {
91             this.doReorder(mappings);
92             
93             target.fireEvent('reorder', mappings, target, this);
94         }
95     },
96     
97     /**
98      * Abstract function to perform the actual reordering. This MUST be overridden in a subclass
99      * @param {Object} mappings Mappings of the old item indexes to new item indexes
100      */
101     doReorder: function(paramName) {
102         throw new Error("doReorder must be implemented in the Ext.ux.Reorderer subclass");
103     },
104     
105     /**
106      * Should create and return an Ext.dd.DD for the given item. This MUST be overridden in a subclass
107      * @param {Mixed} item The item to create a DD for. This could be a TabPanel tab, a Toolbar button, etc
108      * @return {Ext.dd.DD} The DD for the given item
109      */
110     createItemDD: function(item) {
111         throw new Error("createItemDD must be implemented in the Ext.ux.Reorderer subclass");
112     },
113     
114     /**
115      * Sets up the given Toolbar item as a draggable
116      * @param {Mixed} button The item to make draggable (usually an Ext.Button instance)
117      */
118     createItemDD: function(button) {
119         var el   = button.getEl(),
120             id   = el.id,
121             tbar = this.target,
122             me   = this;
123         
124         button.dd = new Ext.dd.DD(el, undefined, {
125             isTarget: false
126         });
127         
128         button.dd.constrainTo(tbar.getEl());
129         button.dd.setYConstraint(0, 0, 0);
130         
131         Ext.apply(button.dd, {
132             b4StartDrag: function() {       
133                 this.startPosition = el.getXY();
134                 
135                 //bump up the z index of the button being dragged but keep a reference to the original
136                 this.startZIndex = el.getStyle('zIndex');
137                 el.setStyle('zIndex', 10000);
138                 
139                 button.suspendEvents();
140             },
141             
142             onDrag: function(e) {
143                 //calculate the button's index within the toolbar and its current midpoint
144                 var buttonX  = el.getXY()[0],
145                     deltaX   = buttonX - this.startPosition[0],
146                     items    = tbar.items.items,
147                     oldIndex = items.indexOf(button),
148                     newIndex;
149                 
150                 //find which item in the toolbar the midpoint is currently over
151                 for (var index = 0; index < items.length; index++) {
152                     var item = items[index];
153                     
154                     if (item.reorderable && item.id != button.id) {
155                         //find the midpoint of the button
156                         var box        = item.getEl().getBox(),
157                             midpoint   = (me.buttonXCache[item.id] || box.x) + (box.width / 2),
158                             movedLeft  = oldIndex > index && deltaX < 0 && buttonX < midpoint,
159                             movedRight = oldIndex < index && deltaX > 0 && (buttonX + el.getWidth()) > midpoint;
160                         
161                         if (movedLeft || movedRight) {
162                             me[movedLeft ? 'onMovedLeft' : 'onMovedRight'](button, index, oldIndex);
163                             break;
164                         }                        
165                     }
166                 }
167             },
168             
169             /**
170              * After the drag has been completed, make sure the button being dragged makes it back to
171              * the correct location and resets its z index
172              */
173             endDrag: function() {
174                 //we need to update the cache here for cases where the button was dragged but its
175                 //position in the toolbar did not change
176                 me.updateButtonXCache();
177                 
178                 el.moveTo(me.buttonXCache[button.id], undefined, {
179                     duration: me.animationDuration,
180                     scope   : this,
181                     callback: function() {
182                         button.resumeEvents();
183                         
184                         tbar.fireEvent('reordered', button, tbar);
185                     }
186                 });
187                 
188                 el.setStyle('zIndex', this.startZIndex);
189             }
190         });
191     },
192     
193     /**
194      * @private
195      * Creates a DD instance for a given item if it is reorderable
196      * @param {Mixed} item The item
197      */
198     createIfReorderable: function(item) {
199         if (this.defaultReorderable && item.reorderable == undefined) {
200             item.reorderable = true;
201         }
202         
203         if (item.reorderable && !item.dd) {
204             if (item.rendered) {
205                 this.createItemDD(item);                
206             } else {
207                 item.on('render', this.createItemDD.createDelegate(this, [item]), this, {single: true});
208             }
209         }
210     },
211     
212     /**
213      * Returns an array of items which will be made draggable. This defaults to the contents of this.target.items,
214      * but can be overridden - e.g. for TabPanels
215      * @return {Array} The array of items which will be made draggable
216      */
217     getItems: function() {
218         return this.target.items.items;
219     },
220     
221     /**
222      * Adds before-reorder and reorder events to the target component
223      */
224     initEvents: function() {
225         this.target.addEvents(
226           /**
227            * @event before-reorder
228            * Fires before a reorder occurs. Return false to cancel
229            * @param {Object} mappings Mappings of the old item indexes to new item indexes
230            * @param {Mixed} component The target component
231            * @param {Ext.ux.TabReorderer} this The plugin instance
232            */
233           'before-reorder',
234           
235           /**
236            * @event reorder
237            * Fires after a reorder has occured.
238            * @param {Object} mappings Mappings of the old item indexes to the new item indexes
239            * @param {Mixed} component The target component
240            * @param {Ext.ux.TabReorderer} this The plugin instance
241            */
242           'reorder'
243         );
244     }
245 });
246
247 /**
248  * @class Ext.ux.HBoxReorderer
249  * @extends Ext.ux.Reorderer
250  * Description
251  */
252 Ext.ux.HBoxReorderer = Ext.extend(Ext.ux.Reorderer, {
253     /**
254      * Initializes the plugin, decorates the container with additional functionality
255      */
256     init: function(container) {
257         /**
258          * This is used to store the correct x value of each button in the array. We need to use this
259          * instead of the button's reported x co-ordinate because the buttons are animated when they move -
260          * if another onDrag is fired while the button is still moving, the comparison x value will be incorrect
261          */
262         this.buttonXCache = {};
263         
264         container.on({
265             scope: this,
266             add  : function(container, item) {
267                 this.createIfReorderable(item);
268             }
269         });
270         
271         //super sets a reference to the toolbar in this.target
272         Ext.ux.HBoxReorderer.superclass.init.apply(this, arguments);
273     },
274     
275     /**
276      * Sets up the given Toolbar item as a draggable
277      * @param {Mixed} button The item to make draggable (usually an Ext.Button instance)
278      */
279     createItemDD: function(button) {
280         if (button.dd != undefined) {
281             return;
282         }
283         
284         var el   = button.getEl(),
285             id   = el.id,
286             me   = this,
287             tbar = me.target;
288         
289         button.dd = new Ext.dd.DD(el, undefined, {
290             isTarget: false
291         });
292         
293         el.applyStyles({
294             position: 'absolute'
295         });
296         
297         //if a button has a menu, it is disabled while dragging with this function
298         var menuDisabler = function() {
299             return false;
300         };
301         
302         Ext.apply(button.dd, {
303             b4StartDrag: function() {       
304                 this.startPosition = el.getXY();
305                 
306                 //bump up the z index of the button being dragged but keep a reference to the original
307                 this.startZIndex = el.getStyle('zIndex');
308                 el.setStyle('zIndex', 10000);
309                 
310                 button.suspendEvents();
311                 if (button.menu) {
312                     button.menu.on('beforeshow', menuDisabler, me);
313                 }
314             },
315             
316             startDrag: function() {
317                 this.constrainTo(tbar.getEl());
318                 this.setYConstraint(0, 0, 0);
319             },
320             
321             onDrag: function(e) {
322                 //calculate the button's index within the toolbar and its current midpoint
323                 var buttonX  = el.getXY()[0],
324                     deltaX   = buttonX - this.startPosition[0],
325                     items    = tbar.items.items,
326                     length   = items.length,
327                     oldIndex = items.indexOf(button),
328                     newIndex, index, item;
329                 
330                 //find which item in the toolbar the midpoint is currently over
331                 for (index = 0; index < length; index++) {
332                     item = items[index];
333                     
334                     if (item.reorderable && item.id != button.id) {
335                         //find the midpoint of the button
336                         var box        = item.getEl().getBox(),
337                             midpoint   = (me.buttonXCache[item.id] || box.x) + (box.width / 2),
338                             movedLeft  = oldIndex > index && deltaX < 0 && buttonX < midpoint,
339                             movedRight = oldIndex < index && deltaX > 0 && (buttonX + el.getWidth()) > midpoint;
340                         
341                         if (movedLeft || movedRight) {
342                             me[movedLeft ? 'onMovedLeft' : 'onMovedRight'](button, index, oldIndex);
343                             break;
344                         }                        
345                     }
346                 }
347             },
348             
349             /**
350              * After the drag has been completed, make sure the button being dragged makes it back to
351              * the correct location and resets its z index
352              */
353             endDrag: function() {
354                 //we need to update the cache here for cases where the button was dragged but its
355                 //position in the toolbar did not change
356                 me.updateButtonXCache();
357                 
358                 el.moveTo(me.buttonXCache[button.id], el.getY(), {
359                     duration: me.animationDuration,
360                     scope   : this,
361                     callback: function() {
362                         button.resumeEvents();
363                         if (button.menu) {
364                             button.menu.un('beforeshow', menuDisabler, me);
365                         }
366                         
367                         tbar.fireEvent('reordered', button, tbar);
368                     }
369                 });
370                 
371                 el.setStyle('zIndex', this.startZIndex);
372             }
373         });
374     },
375     
376     onMovedLeft: function(item, newIndex, oldIndex) {
377         var tbar   = this.target,
378             items  = tbar.items.items,
379             length = items.length,
380             index;
381         
382         if (newIndex != undefined && newIndex != oldIndex) {
383             //move the button currently under drag to its new location
384             tbar.remove(item, false);
385             tbar.insert(newIndex, item);
386             
387             //set the correct x location of each item in the toolbar
388             this.updateButtonXCache();
389             for (index = 0; index < length; index++) {
390                 var obj  = items[index],
391                     newX = this.buttonXCache[obj.id];
392                 
393                 if (item == obj) {
394                     item.dd.startPosition[0] = newX;
395                 } else {
396                     var el = obj.getEl();
397                     
398                     el.moveTo(newX, el.getY(), {
399                         duration: this.animationDuration
400                     });
401                 }
402             }
403         }
404     },
405     
406     onMovedRight: function(item, newIndex, oldIndex) {
407         this.onMovedLeft.apply(this, arguments);
408     },
409     
410     /**
411      * @private
412      * Updates the internal cache of button X locations. 
413      */
414     updateButtonXCache: function() {
415         var tbar   = this.target,
416             items  = tbar.items,
417             totalX = tbar.getEl().getBox(true).x;
418             
419         items.each(function(item) {
420             this.buttonXCache[item.id] = totalX;
421
422             totalX += item.getEl().getWidth();
423         }, this);
424     }
425 });