Upgrade to ExtJS 3.2.1 - Released 04/27/2010
[extjs.git] / examples / ux / Reorderer.js
1 /*!
2  * Ext JS Library 3.2.1
3  * Copyright(c) 2006-2010 Ext JS, Inc.
4  * licensing@extjs.com
5  * http://www.extjs.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         for (var i=0; i < items.length; i++) {
71             this.createIfReorderable(items[i]);
72         }
73     },
74     
75     /**
76      * Reorders the items in the target component according to the given mapping object. Example:
77      * this.reorder({
78      *     1: 5,
79      *     3: 2
80      * });
81      * Would move the item at index 1 to index 5, and the item at index 3 to index 2
82      * @param {Object} mappings Object containing current item index as key and new index as property
83      */
84     reorder: function(mappings) {
85         var target = this.target;
86         
87         if (target.fireEvent('before-reorder', mappings, target, this) !== false) {
88             this.doReorder(mappings);
89             
90             target.fireEvent('reorder', mappings, target, this);
91         }
92     },
93     
94     /**
95      * Abstract function to perform the actual reordering. This MUST be overridden in a subclass
96      * @param {Object} mappings Mappings of the old item indexes to new item indexes
97      */
98     doReorder: function(paramName) {
99         throw new Error("doReorder must be implemented in the Ext.ux.Reorderer subclass");
100     },
101     
102     /**
103      * Should create and return an Ext.dd.DD for the given item. This MUST be overridden in a subclass
104      * @param {Mixed} item The item to create a DD for. This could be a TabPanel tab, a Toolbar button, etc
105      * @return {Ext.dd.DD} The DD for the given item
106      */
107     createItemDD: function(item) {
108         throw new Error("createItemDD must be implemented in the Ext.ux.Reorderer subclass");
109     },
110     
111     /**
112      * Sets up the given Toolbar item as a draggable
113      * @param {Mixed} button The item to make draggable (usually an Ext.Button instance)
114      */
115     createItemDD: function(button) {
116         var el   = button.getEl(),
117             id   = el.id,
118             tbar = this.target,
119             me   = this;
120         
121         button.dd = new Ext.dd.DD(el, undefined, {
122             isTarget: false
123         });
124         
125         button.dd.constrainTo(tbar.getEl());
126         button.dd.setYConstraint(0, 0, 0);
127         
128         Ext.apply(button.dd, {
129             b4StartDrag: function() {       
130                 this.startPosition = el.getXY();
131                 
132                 //bump up the z index of the button being dragged but keep a reference to the original
133                 this.startZIndex = el.getStyle('zIndex');
134                 el.setStyle('zIndex', 10000);
135                 
136                 button.suspendEvents();
137             },
138             
139             onDrag: function(e) {
140                 //calculate the button's index within the toolbar and its current midpoint
141                 var buttonX  = el.getXY()[0],
142                     deltaX   = buttonX - this.startPosition[0],
143                     items    = tbar.items.items,
144                     oldIndex = items.indexOf(button),
145                     newIndex;
146                 
147                 //find which item in the toolbar the midpoint is currently over
148                 for (var index = 0; index < items.length; index++) {
149                     var item = items[index];
150                     
151                     if (item.reorderable && item.id != button.id) {
152                         //find the midpoint of the button
153                         var box        = item.getEl().getBox(),
154                             midpoint   = (me.buttonXCache[item.id] || box.x) + (box.width / 2),
155                             movedLeft  = oldIndex > index && deltaX < 0 && buttonX < midpoint,
156                             movedRight = oldIndex < index && deltaX > 0 && (buttonX + el.getWidth()) > midpoint;
157                         
158                         if (movedLeft || movedRight) {
159                             me[movedLeft ? 'onMovedLeft' : 'onMovedRight'](button, index, oldIndex);
160                             break;
161                         }                        
162                     }
163                 }
164             },
165             
166             /**
167              * After the drag has been completed, make sure the button being dragged makes it back to
168              * the correct location and resets its z index
169              */
170             endDrag: function() {
171                 //we need to update the cache here for cases where the button was dragged but its
172                 //position in the toolbar did not change
173                 me.updateButtonXCache();
174                 
175                 el.moveTo(me.buttonXCache[button.id], undefined, {
176                     duration: me.animationDuration,
177                     scope   : this,
178                     callback: function() {
179                         button.resumeEvents();
180                         
181                         tbar.fireEvent('reordered', button, tbar);
182                     }
183                 });
184                 
185                 el.setStyle('zIndex', this.startZIndex);
186             }
187         });
188     },
189     
190     /**
191      * @private
192      * Creates a DD instance for a given item if it is reorderable
193      * @param {Mixed} item The item
194      */
195     createIfReorderable: function(item) {
196         if (this.defaultReorderable && item.reorderable == undefined) item.reorderable = true;
197         
198         if (item.reorderable) {
199             if (item.rendered) {
200                 this.createItemDD(item);                
201             } else {
202                 item.on('render', this.createItemDD.createDelegate(this, [item]), this, {single: true});
203             }
204         }
205     },
206     
207     /**
208      * Returns an array of items which will be made draggable. This defaults to the contents of this.target.items,
209      * but can be overridden - e.g. for TabPanels
210      * @return {Array} The array of items which will be made draggable
211      */
212     getItems: function() {
213         return this.target.items.items;
214     },
215     
216     /**
217      * Adds before-reorder and reorder events to the target component
218      */
219     initEvents: function() {
220         this.target.addEvents(
221           /**
222            * @event before-reorder
223            * Fires before a reorder occurs. Return false to cancel
224            * @param {Object} mappings Mappings of the old item indexes to new item indexes
225            * @param {Mixed} component The target component
226            * @param {Ext.ux.TabReorderer} this The plugin instance
227            */
228           'before-reorder',
229           
230           /**
231            * @event reorder
232            * Fires after a reorder has occured.
233            * @param {Object} mappings Mappings of the old item indexes to the new item indexes
234            * @param {Mixed} component The target component
235            * @param {Ext.ux.TabReorderer} this The plugin instance
236            */
237           'reorder'
238         );
239     }
240 });