commit extjs-2.2.1
[extjs.git] / source / widgets / Slider.js
1 /*\r
2  * Ext JS Library 2.2.1\r
3  * Copyright(c) 2006-2009, Ext JS, LLC.\r
4  * licensing@extjs.com\r
5  * \r
6  * http://extjs.com/license\r
7  */\r
8 \r
9 /**\r
10  * @class Ext.Slider\r
11  * @extends Ext.BoxComponent\r
12  * Slider which supports vertical or horizontal orientation, keyboard adjustments, \r
13  * configurable snapping, axis clicking and animation. Can be added as an item to\r
14  * any container. Example usage:\r
15 <pre><code>\r
16 new Ext.Slider({\r
17     renderTo: Ext.getBody(),\r
18     width: 200,\r
19     value: 50,\r
20     increment: 10,\r
21     minValue: 0,\r
22     maxValue: 100\r
23 });\r
24 </code></pre>\r
25  */\r
26 Ext.Slider = Ext.extend(Ext.BoxComponent, {\r
27         /**\r
28          * @cfg {Number} value The value to initialize the slider with. Defaults to minValue.\r
29          */\r
30         /**\r
31          * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false.\r
32          */\r
33     vertical: false,\r
34         /**\r
35          * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0.\r
36          */\r
37     minValue: 0,\r
38         /**\r
39          * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100.\r
40          */     \r
41     maxValue: 100,\r
42         /**\r
43          * @cfg {Number} keyIncrement How many units to change the Slider when adjusting with keyboard navigation. Defaults to 1. If the increment config is larger, it will be used instead.\r
44          */\r
45     keyIncrement: 1,\r
46         /**\r
47          * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.\r
48          */\r
49     increment: 0,\r
50         // private\r
51     clickRange: [5,15],\r
52         /**\r
53          * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true\r
54          */\r
55     clickToChange : true,\r
56         /**\r
57          * @cfg {Boolean} animate Turn on or off animation. Defaults to true\r
58          */\r
59     animate: true,\r
60 \r
61     /**\r
62      * True while the thumb is in a drag operation\r
63      * @type boolean\r
64      */\r
65     dragging: false,\r
66 \r
67     // private override\r
68     initComponent : function(){\r
69         if(this.value === undefined){\r
70             this.value = this.minValue;\r
71         }\r
72         Ext.Slider.superclass.initComponent.call(this);\r
73         this.keyIncrement = Math.max(this.increment, this.keyIncrement); \r
74         this.addEvents(\r
75             /**\r
76              * @event beforechange\r
77              * Fires before the slider value is changed. By returning false from an event handler, \r
78              * you can cancel the event and prevent the slider from changing.\r
79                          * @param {Ext.Slider} slider The slider\r
80                          * @param {Number} newValue The new value which the slider is being changed to.\r
81                          * @param {Number} oldValue The old value which the slider was previously.\r
82              */         \r
83                         'beforechange', \r
84                         /**\r
85                          * @event change\r
86                          * Fires when the slider value is changed.\r
87                          * @param {Ext.Slider} slider The slider\r
88                          * @param {Number} newValue The new value which the slider has been changed to.\r
89                          */\r
90                         'change',\r
91                         /**\r
92                          * @event changecomplete\r
93                          * Fires when the slider value is changed by the user and any drag operations have completed.\r
94                          * @param {Ext.Slider} slider The slider\r
95                          * @param {Number} newValue The new value which the slider has been changed to.\r
96                          */\r
97                         'changecomplete',\r
98                         /**\r
99                          * @event dragstart\r
100              * Fires after a drag operation has started.\r
101                          * @param {Ext.Slider} slider The slider\r
102                          * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker\r
103                          */\r
104                         'dragstart', \r
105                         /**\r
106                          * @event drag\r
107              * Fires continuously during the drag operation while the mouse is moving.\r
108                          * @param {Ext.Slider} slider The slider\r
109                          * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker\r
110                          */\r
111                         'drag', \r
112                         /**\r
113                          * @event dragend\r
114              * Fires after the drag operation has completed.\r
115                          * @param {Ext.Slider} slider The slider\r
116                          * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker\r
117                          */\r
118                         'dragend'\r
119                 );\r
120 \r
121         if(this.vertical){\r
122             Ext.apply(this, Ext.Slider.Vertical);\r
123         }\r
124     },\r
125 \r
126         // private override\r
127     onRender : function(){\r
128         this.autoEl = {\r
129             cls: 'x-slider ' + (this.vertical ? 'x-slider-vert' : 'x-slider-horz'),\r
130             cn:{cls:'x-slider-end',cn:{cls:'x-slider-inner',cn:[{cls:'x-slider-thumb'},{tag:'a', cls:'x-slider-focus', href:"#", tabIndex: '-1', hidefocus:'on'}]}}\r
131         };\r
132         Ext.Slider.superclass.onRender.apply(this, arguments);\r
133         this.endEl = this.el.first();\r
134         this.innerEl = this.endEl.first();\r
135         this.thumb = this.innerEl.first();\r
136         this.halfThumb = (this.vertical ? this.thumb.getHeight() : this.thumb.getWidth())/2;\r
137         this.focusEl = this.thumb.next();\r
138         this.initEvents();\r
139     },\r
140 \r
141         // private override\r
142     initEvents : function(){\r
143         this.thumb.addClassOnOver('x-slider-thumb-over');\r
144         this.mon(this.el, 'mousedown', this.onMouseDown, this);\r
145         this.mon(this.el, 'keydown', this.onKeyDown, this);\r
146 \r
147         this.focusEl.swallowEvent("click", true);\r
148 \r
149         this.tracker = new Ext.dd.DragTracker({\r
150             onBeforeStart: this.onBeforeDragStart.createDelegate(this),\r
151             onStart: this.onDragStart.createDelegate(this),\r
152             onDrag: this.onDrag.createDelegate(this),\r
153             onEnd: this.onDragEnd.createDelegate(this),\r
154             tolerance: 3,\r
155             autoStart: 300\r
156         });\r
157         this.tracker.initEl(this.thumb);\r
158         this.on('beforedestroy', this.tracker.destroy, this.tracker);\r
159     },\r
160 \r
161         // private override\r
162     onMouseDown : function(e){\r
163         if(this.disabled) {return;}\r
164         if(this.clickToChange && e.target != this.thumb.dom){\r
165             var local = this.innerEl.translatePoints(e.getXY());\r
166             this.onClickChange(local);\r
167         }\r
168         this.focus();\r
169     },\r
170 \r
171         // private\r
172     onClickChange : function(local){\r
173         if(local.top > this.clickRange[0] && local.top < this.clickRange[1]){\r
174             this.setValue(Math.round(this.reverseValue(local.left)), undefined, true);\r
175         }\r
176     },\r
177         \r
178         // private\r
179     onKeyDown : function(e){\r
180         if(this.disabled){e.preventDefault();return;}\r
181         var k = e.getKey();\r
182         switch(k){\r
183             case e.UP:\r
184             case e.RIGHT:\r
185                 e.stopEvent();\r
186                 if(e.ctrlKey){\r
187                     this.setValue(this.maxValue, undefined, true);\r
188                 }else{\r
189                     this.setValue(this.value+this.keyIncrement, undefined, true);\r
190                 }\r
191             break;\r
192             case e.DOWN:\r
193             case e.LEFT:\r
194                 e.stopEvent();\r
195                 if(e.ctrlKey){\r
196                     this.setValue(this.minValue, undefined, true);\r
197                 }else{\r
198                     this.setValue(this.value-this.keyIncrement, undefined, true);\r
199                 }\r
200             break;\r
201             default:\r
202                 e.preventDefault();\r
203         }\r
204     },\r
205         \r
206         // private\r
207     doSnap : function(value){\r
208         if(!this.increment || this.increment == 1 || !value) {\r
209             return value;\r
210         }\r
211         var newValue = value, inc = this.increment;\r
212         var m = value % inc;\r
213         if(m > 0){\r
214             if(m > (inc/2)){\r
215                 newValue = value + (inc-m);\r
216             }else{\r
217                 newValue = value - m;\r
218             }\r
219         }\r
220         return newValue.constrain(this.minValue,  this.maxValue);\r
221     },\r
222         \r
223         // private\r
224     afterRender : function(){\r
225         Ext.Slider.superclass.afterRender.apply(this, arguments);\r
226         if(this.value !== undefined){\r
227             var v = this.normalizeValue(this.value);\r
228             if(v !== this.value){\r
229                 delete this.value;\r
230                 this.setValue(v, false);\r
231             }else{\r
232                 this.moveThumb(this.translateValue(v), false);\r
233             }\r
234         }\r
235     },\r
236 \r
237         // private\r
238     getRatio : function(){\r
239         var w = this.innerEl.getWidth();\r
240         var v = this.maxValue - this.minValue;\r
241         return v == 0 ? w : (w/v);\r
242     },\r
243 \r
244         // private\r
245     normalizeValue : function(v){\r
246        if(typeof v != 'number'){\r
247             v = parseInt(v);\r
248         }\r
249         v = Math.round(v);\r
250         v = this.doSnap(v);\r
251         v = v.constrain(this.minValue, this.maxValue);\r
252         return v;\r
253     },\r
254 \r
255         /**\r
256          * Programmatically sets the value of the Slider. Ensures that the value is constrained within\r
257          * the minValue and maxValue.\r
258          * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)\r
259          * @param {Boolean} animate Turn on or off animation, defaults to true\r
260          */\r
261     setValue : function(v, animate, changeComplete){\r
262         v = this.normalizeValue(v);\r
263         if(v !== this.value && this.fireEvent('beforechange', this, v, this.value) !== false){\r
264             this.value = v;\r
265             this.moveThumb(this.translateValue(v), animate !== false);\r
266             this.fireEvent('change', this, v);\r
267             if(changeComplete){\r
268                 this.fireEvent('changecomplete', this, v);\r
269             }\r
270         }\r
271     },\r
272 \r
273         // private\r
274     translateValue : function(v){\r
275         var ratio = this.getRatio();\r
276         return (v * ratio)-(this.minValue * ratio)-this.halfThumb;\r
277     },\r
278 \r
279         reverseValue : function(pos){\r
280         var ratio = this.getRatio();\r
281         return (pos+this.halfThumb+(this.minValue * ratio))/ratio;\r
282     },\r
283 \r
284         // private\r
285     moveThumb: function(v, animate){\r
286         if(!animate || this.animate === false){\r
287             this.thumb.setLeft(v);\r
288         }else{\r
289             this.thumb.shift({left: v, stopFx: true, duration:.35});\r
290         }\r
291     },\r
292 \r
293         // private\r
294     focus : function(){\r
295         this.focusEl.focus(10);\r
296     },\r
297 \r
298         // private\r
299     onBeforeDragStart : function(e){\r
300         return !this.disabled;\r
301     },\r
302 \r
303         // private\r
304     onDragStart: function(e){\r
305         this.thumb.addClass('x-slider-thumb-drag');\r
306         this.dragging = true;\r
307         this.dragStartValue = this.value;\r
308         this.fireEvent('dragstart', this, e);\r
309     },\r
310 \r
311         // private\r
312     onDrag: function(e){\r
313         var pos = this.innerEl.translatePoints(this.tracker.getXY());\r
314         this.setValue(Math.round(this.reverseValue(pos.left)), false);\r
315         this.fireEvent('drag', this, e);\r
316     },\r
317         \r
318         // private\r
319     onDragEnd: function(e){\r
320         this.thumb.removeClass('x-slider-thumb-drag');\r
321         this.dragging = false;\r
322         this.fireEvent('dragend', this, e);\r
323         if(this.dragStartValue != this.value){\r
324             this.fireEvent('changecomplete', this, this.value);\r
325         }\r
326     },\r
327     \r
328     //private\r
329     onDisable: function(){\r
330         Ext.Slider.superclass.onDisable.call(this);\r
331         this.thumb.addClass(this.disabledClass);\r
332         if(Ext.isIE){\r
333             //IE breaks when using overflow visible and opacity other than 1.\r
334             //Create a place holder for the thumb and display it.\r
335             var xy = this.thumb.getXY();\r
336             this.thumb.hide();\r
337             this.innerEl.addClass(this.disabledClass).dom.disabled = true;\r
338             if (!this.thumbHolder){\r
339                 this.thumbHolder = this.endEl.createChild({cls: 'x-slider-thumb ' + this.disabledClass});    \r
340             }\r
341             this.thumbHolder.show().setXY(xy);\r
342         }\r
343     },\r
344     \r
345     //private\r
346     onEnable: function(){\r
347         Ext.Slider.superclass.onEnable.call(this);\r
348         this.thumb.removeClass(this.disabledClass);\r
349         if(Ext.isIE){\r
350             this.innerEl.removeClass(this.disabledClass).dom.disabled = false;\r
351             if (this.thumbHolder){\r
352                 this.thumbHolder.hide();\r
353             }\r
354             this.thumb.show();\r
355             this.syncThumb();\r
356         }\r
357     },\r
358 \r
359     // private\r
360     onResize : function(w, h){\r
361         this.innerEl.setWidth(w - (this.el.getPadding('l') + this.endEl.getPadding('r')));\r
362         this.syncThumb();\r
363     },\r
364     \r
365     /**\r
366      * Synchronizes the thumb position to the proper proportion of the total component width based\r
367      * on the current slider {@link #value}.  This will be called automatically when the Slider\r
368      * is resized by a layout, but if it is rendered auto width, this method can be called from\r
369      * another resize handler to sync the Slider if necessary.\r
370      */\r
371     syncThumb : function(){\r
372         if(this.rendered){\r
373             this.moveThumb(this.translateValue(this.value));\r
374         }\r
375     },\r
376         \r
377         /**\r
378          * Returns the current value of the slider\r
379          * @return {Number} The current value of the slider\r
380          */\r
381     getValue : function(){\r
382         return this.value;\r
383     }\r
384 });\r
385 Ext.reg('slider', Ext.Slider);\r
386 \r
387 // private class to support vertical sliders\r
388 Ext.Slider.Vertical = {\r
389     onResize : function(w, h){\r
390         this.innerEl.setHeight(h - (this.el.getPadding('t') + this.endEl.getPadding('b')));\r
391         this.syncThumb();\r
392     },\r
393 \r
394     getRatio : function(){\r
395         var h = this.innerEl.getHeight();\r
396         var v = this.maxValue - this.minValue;\r
397         return h/v;\r
398     },\r
399 \r
400     moveThumb: function(v, animate){\r
401         if(!animate || this.animate === false){\r
402             this.thumb.setBottom(v);\r
403         }else{\r
404             this.thumb.shift({bottom: v, stopFx: true, duration:.35});\r
405         }\r
406     },\r
407 \r
408     onDrag: function(e){\r
409         var pos = this.innerEl.translatePoints(this.tracker.getXY());\r
410         var bottom = this.innerEl.getHeight()-pos.top;\r
411         this.setValue(this.minValue + Math.round(bottom/this.getRatio()), false);\r
412         this.fireEvent('drag', this, e);\r
413     },\r
414 \r
415     onClickChange : function(local){\r
416         if(local.left > this.clickRange[0] && local.left < this.clickRange[1]){\r
417             var bottom = this.innerEl.getHeight()-local.top;\r
418             this.setValue(this.minValue + Math.round(bottom/this.getRatio()), undefined, true);\r
419         }\r
420     }\r
421 };