Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / src / widgets / Slider.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 Ext.ns('Ext.slider');
8
9 /**
10  * @class Ext.slider.Thumb
11  * @extends Object
12  * Represents a single thumb element on a Slider. This would not usually be created manually and would instead
13  * be created internally by an {@link Ext.slider.MultiSlider Ext.Slider}.
14  */
15 Ext.slider.Thumb = Ext.extend(Object, {
16     
17     /**
18      * True while the thumb is in a drag operation
19      * @type Boolean
20      */
21     dragging: false,
22
23     /**
24      * @constructor
25      * @cfg {Ext.slider.MultiSlider} slider The Slider to render to (required)
26      */
27     constructor: function(config) {
28         /**
29          * @property slider
30          * @type Ext.slider.MultiSlider
31          * The slider this thumb is contained within
32          */
33         Ext.apply(this, config || {}, {
34             cls: 'x-slider-thumb',
35
36             /**
37              * @cfg {Boolean} constrain True to constrain the thumb so that it cannot overlap its siblings
38              */
39             constrain: false
40         });
41
42         Ext.slider.Thumb.superclass.constructor.call(this, config);
43
44         if (this.slider.vertical) {
45             Ext.apply(this, Ext.slider.Thumb.Vertical);
46         }
47     },
48
49     /**
50      * Renders the thumb into a slider
51      */
52     render: function() {
53         this.el = this.slider.innerEl.insertFirst({cls: this.cls});
54
55         this.initEvents();
56     },
57
58     /**
59      * Enables the thumb if it is currently disabled
60      */
61     enable: function() {
62         this.disabled = false;
63         this.el.removeClass(this.slider.disabledClass);
64     },
65
66     /**
67      * Disables the thumb if it is currently enabled
68      */
69     disable: function() {
70         this.disabled = true;
71         this.el.addClass(this.slider.disabledClass);
72     },
73
74     /**
75      * Sets up an Ext.dd.DragTracker for this thumb
76      */
77     initEvents: function() {
78         var el = this.el;
79
80         el.addClassOnOver('x-slider-thumb-over');
81
82         this.tracker = new Ext.dd.DragTracker({
83             onBeforeStart: this.onBeforeDragStart.createDelegate(this),
84             onStart      : this.onDragStart.createDelegate(this),
85             onDrag       : this.onDrag.createDelegate(this),
86             onEnd        : this.onDragEnd.createDelegate(this),
87             tolerance    : 3,
88             autoStart    : 300
89         });
90
91         this.tracker.initEl(el);
92     },
93
94     /**
95      * @private
96      * This is tied into the internal Ext.dd.DragTracker. If the slider is currently disabled,
97      * this returns false to disable the DragTracker too.
98      * @return {Boolean} False if the slider is currently disabled
99      */
100     onBeforeDragStart : function(e) {
101         if (this.disabled) {
102             return false;
103         } else {
104             this.slider.promoteThumb(this);
105             return true;
106         }
107     },
108
109     /**
110      * @private
111      * This is tied into the internal Ext.dd.DragTracker's onStart template method. Adds the drag CSS class
112      * to the thumb and fires the 'dragstart' event
113      */
114     onDragStart: function(e){
115         this.el.addClass('x-slider-thumb-drag');
116         this.dragging = true;
117         this.dragStartValue = this.value;
118
119         this.slider.fireEvent('dragstart', this.slider, e, this);
120     },
121
122     /**
123      * @private
124      * This is tied into the internal Ext.dd.DragTracker's onDrag template method. This is called every time
125      * the DragTracker detects a drag movement. It updates the Slider's value using the position of the drag
126      */
127     onDrag: function(e) {
128         var slider   = this.slider,
129             index    = this.index,
130             newValue = this.getNewValue();
131
132         if (this.constrain) {
133             var above = slider.thumbs[index + 1],
134                 below = slider.thumbs[index - 1];
135
136             if (below != undefined && newValue <= below.value) newValue = below.value;
137             if (above != undefined && newValue >= above.value) newValue = above.value;
138         }
139
140         slider.setValue(index, newValue, false);
141         slider.fireEvent('drag', slider, e, this);
142     },
143
144     getNewValue: function() {
145         var slider   = this.slider,
146             pos      = slider.innerEl.translatePoints(this.tracker.getXY());
147
148         return Ext.util.Format.round(slider.reverseValue(pos.left), slider.decimalPrecision);
149     },
150
151     /**
152      * @private
153      * This is tied to the internal Ext.dd.DragTracker's onEnd template method. Removes the drag CSS class and
154      * fires the 'changecomplete' event with the new value
155      */
156     onDragEnd: function(e) {
157         var slider = this.slider,
158             value  = this.value;
159
160         this.el.removeClass('x-slider-thumb-drag');
161
162         this.dragging = false;
163         slider.fireEvent('dragend', slider, e);
164
165         if (this.dragStartValue != value) {
166             slider.fireEvent('changecomplete', slider, value, this);
167         }
168     },
169     
170     /**
171      * @private
172      * Destroys the thumb
173      */
174     destroy: function(){
175         Ext.destroyMembers(this, 'tracker', 'el');
176     }
177 });
178
179 /**
180  * @class Ext.slider.MultiSlider
181  * @extends Ext.BoxComponent
182  * Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis clicking and animation. Can be added as an item to any container. Example usage:
183 <pre>
184 new Ext.Slider({
185     renderTo: Ext.getBody(),
186     width: 200,
187     value: 50,
188     increment: 10,
189     minValue: 0,
190     maxValue: 100
191 });
192 </pre>
193  * Sliders can be created with more than one thumb handle by passing an array of values instead of a single one:
194 <pre>
195 new Ext.Slider({
196     renderTo: Ext.getBody(),
197     width: 200,
198     values: [25, 50, 75],
199     minValue: 0,
200     maxValue: 100,
201
202     //this defaults to true, setting to false allows the thumbs to pass each other
203     {@link #constrainThumbs}: false
204 });
205 </pre>
206  */
207 Ext.slider.MultiSlider = Ext.extend(Ext.BoxComponent, {
208     /**
209      * @cfg {Number} value The value to initialize the slider with. Defaults to minValue.
210      */
211     /**
212      * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false.
213      */
214     vertical: false,
215     /**
216      * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0.
217      */
218     minValue: 0,
219     /**
220      * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100.
221      */
222     maxValue: 100,
223     /**
224      * @cfg {Number/Boolean} decimalPrecision.
225      * <p>The number of decimal places to which to round the Slider's value. Defaults to 0.</p>
226      * <p>To disable rounding, configure as <tt><b>false</b></tt>.</p>
227      */
228     decimalPrecision: 0,
229     /**
230      * @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.
231      */
232     keyIncrement: 1,
233     /**
234      * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.
235      */
236     increment: 0,
237
238     /**
239      * @private
240      * @property clickRange
241      * @type Array
242      * Determines whether or not a click to the slider component is considered to be a user request to change the value. Specified as an array of [top, bottom],
243      * the click event's 'top' property is compared to these numbers and the click only considered a change request if it falls within them. e.g. if the 'top'
244      * value of the click event is 4 or 16, the click is not considered a change request as it falls outside of the [5, 15] range
245      */
246     clickRange: [5,15],
247
248     /**
249      * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true
250      */
251     clickToChange : true,
252     /**
253      * @cfg {Boolean} animate Turn on or off animation. Defaults to true
254      */
255     animate: true,
256     /**
257      * @cfg {Boolean} constrainThumbs True to disallow thumbs from overlapping one another. Defaults to true
258      */
259     constrainThumbs: true,
260
261     /**
262      * @private
263      * @property topThumbZIndex
264      * @type Number
265      * The number used internally to set the z index of the top thumb (see promoteThumb for details)
266      */
267     topThumbZIndex: 10000,
268
269     // private override
270     initComponent : function(){
271         if(!Ext.isDefined(this.value)){
272             this.value = this.minValue;
273         }
274
275         /**
276          * @property thumbs
277          * @type Array
278          * Array containing references to each thumb
279          */
280         this.thumbs = [];
281
282         Ext.slider.MultiSlider.superclass.initComponent.call(this);
283
284         this.keyIncrement = Math.max(this.increment, this.keyIncrement);
285         this.addEvents(
286             /**
287              * @event beforechange
288              * Fires before the slider value is changed. By returning false from an event handler,
289              * you can cancel the event and prevent the slider from changing.
290              * @param {Ext.slider.MultiSlider} slider The slider
291              * @param {Number} newValue The new value which the slider is being changed to.
292              * @param {Number} oldValue The old value which the slider was previously.
293              */
294             'beforechange',
295
296             /**
297              * @event change
298              * Fires when the slider value is changed.
299              * @param {Ext.slider.MultiSlider} slider The slider
300              * @param {Number} newValue The new value which the slider has been changed to.
301              * @param {Ext.slider.Thumb} thumb The thumb that was changed
302              */
303             'change',
304
305             /**
306              * @event changecomplete
307              * Fires when the slider value is changed by the user and any drag operations have completed.
308              * @param {Ext.slider.MultiSlider} slider The slider
309              * @param {Number} newValue The new value which the slider has been changed to.
310              * @param {Ext.slider.Thumb} thumb The thumb that was changed
311              */
312             'changecomplete',
313
314             /**
315              * @event dragstart
316              * Fires after a drag operation has started.
317              * @param {Ext.slider.MultiSlider} slider The slider
318              * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
319              */
320             'dragstart',
321
322             /**
323              * @event drag
324              * Fires continuously during the drag operation while the mouse is moving.
325              * @param {Ext.slider.MultiSlider} slider The slider
326              * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
327              */
328             'drag',
329
330             /**
331              * @event dragend
332              * Fires after the drag operation has completed.
333              * @param {Ext.slider.MultiSlider} slider The slider
334              * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
335              */
336             'dragend'
337         );
338
339         /**
340          * @property values
341          * @type Array
342          * Array of values to initalize the thumbs with
343          */
344         if (this.values == undefined || Ext.isEmpty(this.values)) this.values = [0];
345
346         var values = this.values;
347
348         for (var i=0; i < values.length; i++) {
349             this.addThumb(values[i]);
350         }
351
352         if(this.vertical){
353             Ext.apply(this, Ext.slider.Vertical);
354         }
355     },
356
357     /**
358      * Creates a new thumb and adds it to the slider
359      * @param {Number} value The initial value to set on the thumb. Defaults to 0
360      */
361     addThumb: function(value) {
362         var thumb = new Ext.slider.Thumb({
363             value    : value,
364             slider   : this,
365             index    : this.thumbs.length,
366             constrain: this.constrainThumbs
367         });
368         this.thumbs.push(thumb);
369
370         //render the thumb now if needed
371         if (this.rendered) thumb.render();
372     },
373
374     /**
375      * @private
376      * Moves the given thumb above all other by increasing its z-index. This is called when as drag
377      * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
378      * required when the thumbs are stacked on top of each other at one of the ends of the slider's
379      * range, which can result in the user not being able to move any of them.
380      * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
381      */
382     promoteThumb: function(topThumb) {
383         var thumbs = this.thumbs,
384             zIndex, thumb;
385
386         for (var i = 0, j = thumbs.length; i < j; i++) {
387             thumb = thumbs[i];
388
389             if (thumb == topThumb) {
390                 zIndex = this.topThumbZIndex;
391             } else {
392                 zIndex = '';
393             }
394
395             thumb.el.setStyle('zIndex', zIndex);
396         }
397     },
398
399     // private override
400     onRender : function() {
401         this.autoEl = {
402             cls: 'x-slider ' + (this.vertical ? 'x-slider-vert' : 'x-slider-horz'),
403             cn : {
404                 cls: 'x-slider-end',
405                 cn : {
406                     cls:'x-slider-inner',
407                     cn : [{tag:'a', cls:'x-slider-focus', href:"#", tabIndex: '-1', hidefocus:'on'}]
408                 }
409             }
410         };
411
412         Ext.slider.MultiSlider.superclass.onRender.apply(this, arguments);
413
414         this.endEl   = this.el.first();
415         this.innerEl = this.endEl.first();
416         this.focusEl = this.innerEl.child('.x-slider-focus');
417
418         //render each thumb
419         for (var i=0; i < this.thumbs.length; i++) {
420             this.thumbs[i].render();
421         }
422
423         //calculate the size of half a thumb
424         var thumb      = this.innerEl.child('.x-slider-thumb');
425         this.halfThumb = (this.vertical ? thumb.getHeight() : thumb.getWidth()) / 2;
426
427         this.initEvents();
428     },
429
430     /**
431      * @private
432      * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element.
433      * Creates a new DragTracker which is used to control what happens when the user drags the thumb around.
434      */
435     initEvents : function(){
436         this.mon(this.el, {
437             scope    : this,
438             mousedown: this.onMouseDown,
439             keydown  : this.onKeyDown
440         });
441
442         this.focusEl.swallowEvent("click", true);
443     },
444
445     /**
446      * @private
447      * Mousedown handler for the slider. If the clickToChange is enabled and the click was not on the draggable 'thumb',
448      * this calculates the new value of the slider and tells the implementation (Horizontal or Vertical) to move the thumb
449      * @param {Ext.EventObject} e The click event
450      */
451     onMouseDown : function(e){
452         if(this.disabled){
453             return;
454         }
455
456         //see if the click was on any of the thumbs
457         var thumbClicked = false;
458         for (var i=0; i < this.thumbs.length; i++) {
459             thumbClicked = thumbClicked || e.target == this.thumbs[i].el.dom;
460         }
461
462         if (this.clickToChange && !thumbClicked) {
463             var local = this.innerEl.translatePoints(e.getXY());
464             this.onClickChange(local);
465         }
466         this.focus();
467     },
468
469     /**
470      * @private
471      * Moves the thumb to the indicated position. Note that a Vertical implementation is provided in Ext.slider.Vertical.
472      * Only changes the value if the click was within this.clickRange.
473      * @param {Object} local Object containing top and left values for the click event.
474      */
475     onClickChange : function(local) {
476         if (local.top > this.clickRange[0] && local.top < this.clickRange[1]) {
477             //find the nearest thumb to the click event
478             var thumb = this.getNearest(local, 'left'),
479                 index = thumb.index;
480
481             this.setValue(index, Ext.util.Format.round(this.reverseValue(local.left), this.decimalPrecision), undefined, true);
482         }
483     },
484
485     /**
486      * @private
487      * Returns the nearest thumb to a click event, along with its distance
488      * @param {Object} local Object containing top and left values from a click event
489      * @param {String} prop The property of local to compare on. Use 'left' for horizontal sliders, 'top' for vertical ones
490      * @return {Object} The closest thumb object and its distance from the click event
491      */
492     getNearest: function(local, prop) {
493         var localValue = prop == 'top' ? this.innerEl.getHeight() - local[prop] : local[prop],
494             clickValue = this.reverseValue(localValue),
495             nearestDistance = (this.maxValue - this.minValue) + 5, //add a small fudge for the end of the slider 
496             index = 0,
497             nearest = null;
498
499         for (var i=0; i < this.thumbs.length; i++) {
500             var thumb = this.thumbs[i],
501                 value = thumb.value,
502                 dist  = Math.abs(value - clickValue);
503
504             if (Math.abs(dist <= nearestDistance)) {
505                 nearest = thumb;
506                 index = i;
507                 nearestDistance = dist;
508             }
509         }
510         return nearest;
511     },
512
513     /**
514      * @private
515      * Handler for any keypresses captured by the slider. If the key is UP or RIGHT, the thumb is moved along to the right
516      * by this.keyIncrement. If DOWN or LEFT it is moved left. Pressing CTRL moves the slider to the end in either direction
517      * @param {Ext.EventObject} e The Event object
518      */
519     onKeyDown : function(e){
520         /*
521          * The behaviour for keyboard handling with multiple thumbs is currently undefined.
522          * There's no real sane default for it, so leave it like this until we come up
523          * with a better way of doing it.
524          */
525         if(this.disabled || this.thumbs.length !== 1){
526             e.preventDefault();
527             return;
528         }
529         var k = e.getKey(),
530             val;
531         switch(k){
532             case e.UP:
533             case e.RIGHT:
534                 e.stopEvent();
535                 val = e.ctrlKey ? this.maxValue : this.getValue(0) + this.keyIncrement;
536                 this.setValue(0, val, undefined, true);
537             break;
538             case e.DOWN:
539             case e.LEFT:
540                 e.stopEvent();
541                 val = e.ctrlKey ? this.minValue : this.getValue(0) - this.keyIncrement;
542                 this.setValue(0, val, undefined, true);
543             break;
544             default:
545                 e.preventDefault();
546         }
547     },
548
549     /**
550      * @private
551      * If using snapping, this takes a desired new value and returns the closest snapped
552      * value to it
553      * @param {Number} value The unsnapped value
554      * @return {Number} The value of the nearest snap target
555      */
556     doSnap : function(value){
557         if (!(this.increment && value)) {
558             return value;
559         }
560         var newValue = value,
561             inc = this.increment,
562             m = value % inc;
563         if (m != 0) {
564             newValue -= m;
565             if (m * 2 >= inc) {
566                 newValue += inc;
567             } else if (m * 2 < -inc) {
568                 newValue -= inc;
569             }
570         }
571         return newValue.constrain(this.minValue,  this.maxValue);
572     },
573
574     // private
575     afterRender : function(){
576         Ext.slider.MultiSlider.superclass.afterRender.apply(this, arguments);
577
578         for (var i=0; i < this.thumbs.length; i++) {
579             var thumb = this.thumbs[i];
580
581             if (thumb.value !== undefined) {
582                 var v = this.normalizeValue(thumb.value);
583
584                 if (v !== thumb.value) {
585                     // delete this.value;
586                     this.setValue(i, v, false);
587                 } else {
588                     this.moveThumb(i, this.translateValue(v), false);
589                 }
590             }
591         };
592     },
593
594     /**
595      * @private
596      * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100,
597      * the ratio is 2
598      * @return {Number} The ratio of pixels to mapped values
599      */
600     getRatio : function(){
601         var w = this.innerEl.getWidth(),
602             v = this.maxValue - this.minValue;
603         return v == 0 ? w : (w/v);
604     },
605
606     /**
607      * @private
608      * Returns a snapped, constrained value when given a desired value
609      * @param {Number} value Raw number value
610      * @return {Number} The raw value rounded to the correct d.p. and constrained within the set max and min values
611      */
612     normalizeValue : function(v){
613         v = this.doSnap(v);
614         v = Ext.util.Format.round(v, this.decimalPrecision);
615         v = v.constrain(this.minValue, this.maxValue);
616         return v;
617     },
618
619     /**
620      * Sets the minimum value for the slider instance. If the current value is less than the
621      * minimum value, the current value will be changed.
622      * @param {Number} val The new minimum value
623      */
624     setMinValue : function(val){
625         this.minValue = val;
626         var i = 0,
627             thumbs = this.thumbs,
628             len = thumbs.length,
629             t;
630             
631         for(; i < len; ++i){
632             t = thumbs[i];
633             t.value = t.value < val ? val : t.value;
634         }
635         this.syncThumb();
636     },
637
638     /**
639      * Sets the maximum value for the slider instance. If the current value is more than the
640      * maximum value, the current value will be changed.
641      * @param {Number} val The new maximum value
642      */
643     setMaxValue : function(val){
644         this.maxValue = val;
645         var i = 0,
646             thumbs = this.thumbs,
647             len = thumbs.length,
648             t;
649             
650         for(; i < len; ++i){
651             t = thumbs[i];
652             t.value = t.value > val ? val : t.value;
653         }
654         this.syncThumb();
655     },
656
657     /**
658      * Programmatically sets the value of the Slider. Ensures that the value is constrained within
659      * the minValue and maxValue.
660      * @param {Number} index Index of the thumb to move
661      * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
662      * @param {Boolean} animate Turn on or off animation, defaults to true
663      */
664     setValue : function(index, v, animate, changeComplete) {
665         var thumb = this.thumbs[index],
666             el    = thumb.el;
667
668         v = this.normalizeValue(v);
669
670         if (v !== thumb.value && this.fireEvent('beforechange', this, v, thumb.value, thumb) !== false) {
671             thumb.value = v;
672             if(this.rendered){
673                 this.moveThumb(index, this.translateValue(v), animate !== false);
674                 this.fireEvent('change', this, v, thumb);
675                 if(changeComplete){
676                     this.fireEvent('changecomplete', this, v, thumb);
677                 }
678             }
679         }
680     },
681
682     /**
683      * @private
684      */
685     translateValue : function(v) {
686         var ratio = this.getRatio();
687         return (v * ratio) - (this.minValue * ratio) - this.halfThumb;
688     },
689
690     /**
691      * @private
692      * Given a pixel location along the slider, returns the mapped slider value for that pixel.
693      * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500, reverseValue(50)
694      * returns 200
695      * @param {Number} pos The position along the slider to return a mapped value for
696      * @return {Number} The mapped value for the given position
697      */
698     reverseValue : function(pos){
699         var ratio = this.getRatio();
700         return (pos + (this.minValue * ratio)) / ratio;
701     },
702
703     /**
704      * @private
705      * @param {Number} index Index of the thumb to move
706      */
707     moveThumb: function(index, v, animate){
708         var thumb = this.thumbs[index].el;
709
710         if(!animate || this.animate === false){
711             thumb.setLeft(v);
712         }else{
713             thumb.shift({left: v, stopFx: true, duration:.35});
714         }
715     },
716
717     // private
718     focus : function(){
719         this.focusEl.focus(10);
720     },
721
722     // private
723     onResize : function(w, h){
724         var thumbs = this.thumbs,
725             len = thumbs.length,
726             i = 0;
727             
728         /*
729          * If we happen to be animating during a resize, the position of the thumb will likely be off
730          * when the animation stops. As such, just stop any animations before syncing the thumbs.
731          */
732         for(; i < len; ++i){
733             thumbs[i].el.stopFx();    
734         }
735         // check to see if we're using an auto width
736         if(Ext.isNumber(w)){
737             this.innerEl.setWidth(w - (this.el.getPadding('l') + this.endEl.getPadding('r')));
738         }
739         this.syncThumb();
740         Ext.slider.MultiSlider.superclass.onResize.apply(this, arguments);
741     },
742
743     //private
744     onDisable: function(){
745         Ext.slider.MultiSlider.superclass.onDisable.call(this);
746
747         for (var i=0; i < this.thumbs.length; i++) {
748             var thumb = this.thumbs[i],
749                 el    = thumb.el;
750
751             thumb.disable();
752
753             if(Ext.isIE){
754                 //IE breaks when using overflow visible and opacity other than 1.
755                 //Create a place holder for the thumb and display it.
756                 var xy = el.getXY();
757                 el.hide();
758
759                 this.innerEl.addClass(this.disabledClass).dom.disabled = true;
760
761                 if (!this.thumbHolder) {
762                     this.thumbHolder = this.endEl.createChild({cls: 'x-slider-thumb ' + this.disabledClass});
763                 }
764
765                 this.thumbHolder.show().setXY(xy);
766             }
767         }
768     },
769
770     //private
771     onEnable: function(){
772         Ext.slider.MultiSlider.superclass.onEnable.call(this);
773
774         for (var i=0; i < this.thumbs.length; i++) {
775             var thumb = this.thumbs[i],
776                 el    = thumb.el;
777
778             thumb.enable();
779
780             if (Ext.isIE) {
781                 this.innerEl.removeClass(this.disabledClass).dom.disabled = false;
782
783                 if (this.thumbHolder) this.thumbHolder.hide();
784
785                 el.show();
786                 this.syncThumb();
787             }
788         }
789     },
790
791     /**
792      * Synchronizes the thumb position to the proper proportion of the total component width based
793      * on the current slider {@link #value}.  This will be called automatically when the Slider
794      * is resized by a layout, but if it is rendered auto width, this method can be called from
795      * another resize handler to sync the Slider if necessary.
796      */
797     syncThumb : function() {
798         if (this.rendered) {
799             for (var i=0; i < this.thumbs.length; i++) {
800                 this.moveThumb(i, this.translateValue(this.thumbs[i].value));
801             }
802         }
803     },
804
805     /**
806      * Returns the current value of the slider
807      * @param {Number} index The index of the thumb to return a value for
808      * @return {Number} The current value of the slider
809      */
810     getValue : function(index) {
811         return this.thumbs[index].value;
812     },
813
814     /**
815      * Returns an array of values - one for the location of each thumb
816      * @return {Array} The set of thumb values
817      */
818     getValues: function() {
819         var values = [];
820
821         for (var i=0; i < this.thumbs.length; i++) {
822             values.push(this.thumbs[i].value);
823         }
824
825         return values;
826     },
827
828     // private
829     beforeDestroy : function(){
830         var thumbs = this.thumbs;
831         for(var i = 0, len = thumbs.length; i < len; ++i){
832             thumbs[i].destroy();
833             thumbs[i] = null;
834         }
835         Ext.destroyMembers(this, 'endEl', 'innerEl', 'focusEl', 'thumbHolder');
836         Ext.slider.MultiSlider.superclass.beforeDestroy.call(this);
837     }
838 });
839
840 Ext.reg('multislider', Ext.slider.MultiSlider);
841
842 /**
843  * @class Ext.slider.SingleSlider
844  * @extends Ext.slider.MultiSlider
845  * Slider which supports vertical or horizontal orientation, keyboard adjustments,
846  * configurable snapping, axis clicking and animation. Can be added as an item to
847  * any container. Example usage:
848 <pre><code>
849 new Ext.slider.SingleSlider({
850     renderTo: Ext.getBody(),
851     width: 200,
852     value: 50,
853     increment: 10,
854     minValue: 0,
855     maxValue: 100
856 });
857 </code></pre>
858  * The class Ext.slider.SingleSlider is aliased to Ext.Slider for backwards compatibility.
859  */
860 Ext.slider.SingleSlider = Ext.extend(Ext.slider.MultiSlider, {
861     constructor: function(config) {
862       config = config || {};
863
864       Ext.applyIf(config, {
865           values: [config.value || 0]
866       });
867
868       Ext.slider.SingleSlider.superclass.constructor.call(this, config);
869     },
870
871     /**
872      * Returns the current value of the slider
873      * @return {Number} The current value of the slider
874      */
875     getValue: function() {
876         //just returns the value of the first thumb, which should be the only one in a single slider
877         return Ext.slider.SingleSlider.superclass.getValue.call(this, 0);
878     },
879
880     /**
881      * Programmatically sets the value of the Slider. Ensures that the value is constrained within
882      * the minValue and maxValue.
883      * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
884      * @param {Boolean} animate Turn on or off animation, defaults to true
885      */
886     setValue: function(value, animate) {
887         var args = Ext.toArray(arguments),
888             len  = args.length;
889
890         //this is to maintain backwards compatiblity for sliders with only one thunb. Usually you must pass the thumb
891         //index to setValue, but if we only have one thumb we inject the index here first if given the multi-slider
892         //signature without the required index. The index will always be 0 for a single slider
893         if (len == 1 || (len <= 3 && typeof arguments[1] != 'number')) {
894             args.unshift(0);
895         }
896
897         return Ext.slider.SingleSlider.superclass.setValue.apply(this, args);
898     },
899
900     /**
901      * Synchronizes the thumb position to the proper proportion of the total component width based
902      * on the current slider {@link #value}.  This will be called automatically when the Slider
903      * is resized by a layout, but if it is rendered auto width, this method can be called from
904      * another resize handler to sync the Slider if necessary.
905      */
906     syncThumb : function() {
907         return Ext.slider.SingleSlider.superclass.syncThumb.apply(this, [0].concat(arguments));
908     },
909     
910     // private
911     getNearest : function(){
912         // Since there's only 1 thumb, it's always the nearest
913         return this.thumbs[0];    
914     }
915 });
916
917 //backwards compatibility
918 Ext.Slider = Ext.slider.SingleSlider;
919
920 Ext.reg('slider', Ext.slider.SingleSlider);
921
922 // private class to support vertical sliders
923 Ext.slider.Vertical = {
924     onResize : function(w, h){
925         this.innerEl.setHeight(h - (this.el.getPadding('t') + this.endEl.getPadding('b')));
926         this.syncThumb();
927     },
928
929     getRatio : function(){
930         var h = this.innerEl.getHeight(),
931             v = this.maxValue - this.minValue;
932         return h/v;
933     },
934
935     moveThumb: function(index, v, animate) {
936         var thumb = this.thumbs[index],
937             el    = thumb.el;
938
939         if (!animate || this.animate === false) {
940             el.setBottom(v);
941         } else {
942             el.shift({bottom: v, stopFx: true, duration:.35});
943         }
944     },
945
946     onClickChange : function(local) {
947         if (local.left > this.clickRange[0] && local.left < this.clickRange[1]) {
948             var thumb = this.getNearest(local, 'top'),
949                 index = thumb.index,
950                 value = this.minValue + this.reverseValue(this.innerEl.getHeight() - local.top);
951
952             this.setValue(index, Ext.util.Format.round(value, this.decimalPrecision), undefined, true);
953         }
954     }
955 };
956
957 //private class to support vertical dragging of thumbs within a slider
958 Ext.slider.Thumb.Vertical = {
959     getNewValue: function() {
960         var slider   = this.slider,
961             innerEl  = slider.innerEl,
962             pos      = innerEl.translatePoints(this.tracker.getXY()),
963             bottom   = innerEl.getHeight() - pos.top;
964
965         return slider.minValue + Ext.util.Format.round(bottom / slider.getRatio(), slider.decimalPrecision);
966     }
967 };