Upgrade to ExtJS 4.0.1 - Released 05/18/2011
[extjs.git] / src / slider / Multi.js
1 /**
2  * @class Ext.slider.Multi
3  * @extends Ext.form.field.Base
4  * <p>Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis
5  * clicking and animation. Can be added as an item to any container. In addition,  
6  * {@img Ext.slider.Multi/Ext.slider.Multi.png Ext.slider.Multi component}
7  * <p>Example usage:</p>
8  * Sliders can be created with more than one thumb handle by passing an array of values instead of a single one:
9 <pre>
10     Ext.create('Ext.slider.Multi', {
11         width: 200,
12         values: [25, 50, 75],
13         increment: 5,
14         minValue: 0,
15         maxValue: 100,
16
17         //this defaults to true, setting to false allows the thumbs to pass each other
18         {@link #constrainThumbs}: false,
19         renderTo: Ext.getBody()
20     });  
21 </pre>
22  * @xtype multislider
23  */
24 Ext.define('Ext.slider.Multi', {
25     extend: 'Ext.form.field.Base',
26     alias: 'widget.multislider',
27     alternateClassName: 'Ext.slider.MultiSlider',
28
29     requires: [
30         'Ext.slider.Thumb',
31         'Ext.slider.Tip',
32         'Ext.Number',
33         'Ext.util.Format',
34         'Ext.Template',
35         'Ext.layout.component.field.Slider'
36     ],
37
38     fieldSubTpl: [
39         '<div class="' + Ext.baseCSSPrefix + 'slider {fieldCls} {vertical}" aria-valuemin="{minValue}" aria-valuemax="{maxValue}" aria-valuenow="{value}" aria-valuetext="{value}">',
40             '<div class="' + Ext.baseCSSPrefix + 'slider-end" role="presentation">',
41                 '<div class="' + Ext.baseCSSPrefix + 'slider-inner" role="presentation">',
42                     '<a class="' + Ext.baseCSSPrefix + 'slider-focus" href="#" tabIndex="-1" hidefocus="on" role="presentation"></a>',
43                 '</div>',
44             '</div>',
45         '</div>',
46         {
47             disableFormats: true,
48             compiled: true
49         }
50     ],
51
52     /**
53      * @cfg {Number} value
54      * A value with which to initialize the slider. Defaults to minValue. Setting this will only
55      * result in the creation of a single slider thumb; if you want multiple thumbs then use the
56      * {@link #values} config instead.
57      */
58
59     /**
60      * @cfg {Array} values
61      * Array of Number values with which to initalize the slider. A separate slider thumb will be created for
62      * each value in this array. This will take precedence over the single {@link #value} config.
63      */
64
65     /**
66      * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false.
67      */
68     vertical: false,
69     /**
70      * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0.
71      */
72     minValue: 0,
73     /**
74      * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100.
75      */
76     maxValue: 100,
77     /**
78      * @cfg {Number/Boolean} decimalPrecision.
79      * <p>The number of decimal places to which to round the Slider's value. Defaults to 0.</p>
80      * <p>To disable rounding, configure as <tt><b>false</b></tt>.</p>
81      */
82     decimalPrecision: 0,
83     /**
84      * @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.
85      */
86     keyIncrement: 1,
87     /**
88      * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.
89      */
90     increment: 0,
91
92     /**
93      * @private
94      * @property clickRange
95      * @type Array
96      * 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],
97      * 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'
98      * 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
99      */
100     clickRange: [5,15],
101
102     /**
103      * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true
104      */
105     clickToChange : true,
106     /**
107      * @cfg {Boolean} animate Turn on or off animation. Defaults to true
108      */
109     animate: true,
110
111     /**
112      * True while the thumb is in a drag operation
113      * @type Boolean
114      */
115     dragging: false,
116
117     /**
118      * @cfg {Boolean} constrainThumbs True to disallow thumbs from overlapping one another. Defaults to true
119      */
120     constrainThumbs: true,
121
122     componentLayout: 'sliderfield',
123
124     /**
125      * @cfg {Boolean} useTips
126      * True to use an Ext.slider.Tip to display tips for the value. Defaults to <tt>true</tt>.
127      */
128     useTips : true,
129
130     /**
131      * @cfg {Function} tipText
132      * A function used to display custom text for the slider tip. Defaults to <tt>null</tt>, which will
133      * use the default on the plugin.
134      */
135     tipText : null,
136
137     ariaRole: 'slider',
138
139     // private override
140     initValue: function() {
141         var me = this,
142             extValue = Ext.value,
143             // Fallback for initial values: values config -> value config -> minValue config -> 0
144             values = extValue(me.values, [extValue(me.value, extValue(me.minValue, 0))]),
145             i = 0,
146             len = values.length;
147
148         // Store for use in dirty check
149         me.originalValue = values;
150
151         // Add a thumb for each value
152         for (; i < len; i++) {
153             me.addThumb(values[i]);
154         }
155     },
156
157     // private override
158     initComponent : function() {
159         var me = this,
160             tipPlug,
161             hasTip;
162         
163         /**
164          * @property thumbs
165          * @type Array
166          * Array containing references to each thumb
167          */
168         me.thumbs = [];
169
170         me.keyIncrement = Math.max(me.increment, me.keyIncrement);
171
172         me.addEvents(
173             /**
174              * @event beforechange
175              * Fires before the slider value is changed. By returning false from an event handler,
176              * you can cancel the event and prevent the slider from changing.
177              * @param {Ext.slider.Multi} slider The slider
178              * @param {Number} newValue The new value which the slider is being changed to.
179              * @param {Number} oldValue The old value which the slider was previously.
180              */
181             'beforechange',
182
183             /**
184              * @event change
185              * Fires when the slider value is changed.
186              * @param {Ext.slider.Multi} slider The slider
187              * @param {Number} newValue The new value which the slider has been changed to.
188              * @param {Ext.slider.Thumb} thumb The thumb that was changed
189              */
190             'change',
191
192             /**
193              * @event changecomplete
194              * Fires when the slider value is changed by the user and any drag operations have completed.
195              * @param {Ext.slider.Multi} slider The slider
196              * @param {Number} newValue The new value which the slider has been changed to.
197              * @param {Ext.slider.Thumb} thumb The thumb that was changed
198              */
199             'changecomplete',
200
201             /**
202              * @event dragstart
203              * Fires after a drag operation has started.
204              * @param {Ext.slider.Multi} slider The slider
205              * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
206              */
207             'dragstart',
208
209             /**
210              * @event drag
211              * Fires continuously during the drag operation while the mouse is moving.
212              * @param {Ext.slider.Multi} slider The slider
213              * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
214              */
215             'drag',
216
217             /**
218              * @event dragend
219              * Fires after the drag operation has completed.
220              * @param {Ext.slider.Multi} slider The slider
221              * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
222              */
223             'dragend'
224         );
225
226         if (me.vertical) {
227             Ext.apply(me, Ext.slider.Multi.Vertical);
228         }
229
230         me.callParent();
231
232         // only can use it if it exists.
233         if (me.useTips) {
234             tipPlug = me.tipText ? {getText: me.tipText} : {};
235             me.plugins = me.plugins || [];
236             Ext.each(me.plugins, function(plug){
237                 if (plug.isSliderTip) {
238                     hasTip = true;
239                     return false;
240                 }
241             });
242             if (!hasTip) {
243                 me.plugins.push(Ext.create('Ext.slider.Tip', tipPlug));
244             }
245         }
246     },
247
248     /**
249      * Creates a new thumb and adds it to the slider
250      * @param {Number} value The initial value to set on the thumb. Defaults to 0
251      * @return {Ext.slider.Thumb} The thumb
252      */
253     addThumb: function(value) {
254         var me = this,
255             thumb = Ext.create('Ext.slider.Thumb', {
256             value    : value,
257             slider   : me,
258             index    : me.thumbs.length,
259             constrain: me.constrainThumbs
260         });
261         me.thumbs.push(thumb);
262
263         //render the thumb now if needed
264         if (me.rendered) {
265             thumb.render();
266         }
267
268         return thumb;
269     },
270
271     /**
272      * @private
273      * Moves the given thumb above all other by increasing its z-index. This is called when as drag
274      * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
275      * required when the thumbs are stacked on top of each other at one of the ends of the slider's
276      * range, which can result in the user not being able to move any of them.
277      * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
278      */
279     promoteThumb: function(topThumb) {
280         var thumbs = this.thumbs,
281             ln = thumbs.length,
282             zIndex, thumb, i;
283             
284         for (i = 0; i < ln; i++) {
285             thumb = thumbs[i];
286
287             if (thumb == topThumb) {
288                 thumb.bringToFront();
289             } else {
290                 thumb.sendToBack();
291             }
292         }
293     },
294
295     // private override
296     onRender : function() {
297         var me = this,
298             i = 0,
299             thumbs = me.thumbs,
300             len = thumbs.length,
301             thumb;
302
303         Ext.applyIf(me.subTplData, {
304             vertical: me.vertical ? Ext.baseCSSPrefix + 'slider-vert' : Ext.baseCSSPrefix + 'slider-horz',
305             minValue: me.minValue,
306             maxValue: me.maxValue,
307             value: me.value
308         });
309
310         Ext.applyIf(me.renderSelectors, {
311             endEl: '.' + Ext.baseCSSPrefix + 'slider-end',
312             innerEl: '.' + Ext.baseCSSPrefix + 'slider-inner',
313             focusEl: '.' + Ext.baseCSSPrefix + 'slider-focus'
314         });
315
316         me.callParent(arguments);
317
318         //render each thumb
319         for (; i < len; i++) {
320             thumbs[i].render();
321         }
322
323         //calculate the size of half a thumb
324         thumb = me.innerEl.down('.' + Ext.baseCSSPrefix + 'slider-thumb');
325         me.halfThumb = (me.vertical ? thumb.getHeight() : thumb.getWidth()) / 2;
326
327     },
328
329     /**
330      * Utility method to set the value of the field when the slider changes.
331      * @param {Object} slider The slider object.
332      * @param {Object} v The new value.
333      * @private
334      */
335     onChange : function(slider, v) {
336         this.setValue(v, undefined, true);
337     },
338
339     /**
340      * @private
341      * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element.
342      */
343     initEvents : function() {
344         var me = this;
345         
346         me.mon(me.el, {
347             scope    : me,
348             mousedown: me.onMouseDown,
349             keydown  : me.onKeyDown,
350             change : me.onChange
351         });
352
353         me.focusEl.swallowEvent("click", true);
354     },
355
356     /**
357      * @private
358      * Mousedown handler for the slider. If the clickToChange is enabled and the click was not on the draggable 'thumb',
359      * this calculates the new value of the slider and tells the implementation (Horizontal or Vertical) to move the thumb
360      * @param {Ext.EventObject} e The click event
361      */
362     onMouseDown : function(e) {
363         var me = this,
364             thumbClicked = false,
365             i = 0,
366             thumbs = me.thumbs,
367             len = thumbs.length,
368             local;
369             
370         if (me.disabled) {
371             return;
372         }
373
374         //see if the click was on any of the thumbs
375         for (; i < len; i++) {
376             thumbClicked = thumbClicked || e.target == thumbs[i].el.dom;
377         }
378
379         if (me.clickToChange && !thumbClicked) {
380             local = me.innerEl.translatePoints(e.getXY());
381             me.onClickChange(local);
382         }
383         me.focus();
384     },
385
386     /**
387      * @private
388      * Moves the thumb to the indicated position. Note that a Vertical implementation is provided in Ext.slider.Multi.Vertical.
389      * Only changes the value if the click was within this.clickRange.
390      * @param {Object} local Object containing top and left values for the click event.
391      */
392     onClickChange : function(local) {
393         var me = this,
394             thumb, index;
395             
396         if (local.top > me.clickRange[0] && local.top < me.clickRange[1]) {
397             //find the nearest thumb to the click event
398             thumb = me.getNearest(local, 'left');
399             if (!thumb.disabled) {
400                 index = thumb.index;
401                 me.setValue(index, Ext.util.Format.round(me.reverseValue(local.left), me.decimalPrecision), undefined, true);
402             }
403         }
404     },
405
406     /**
407      * @private
408      * Returns the nearest thumb to a click event, along with its distance
409      * @param {Object} local Object containing top and left values from a click event
410      * @param {String} prop The property of local to compare on. Use 'left' for horizontal sliders, 'top' for vertical ones
411      * @return {Object} The closest thumb object and its distance from the click event
412      */
413     getNearest: function(local, prop) {
414         var me = this,
415             localValue = prop == 'top' ? me.innerEl.getHeight() - local[prop] : local[prop],
416             clickValue = me.reverseValue(localValue),
417             nearestDistance = (me.maxValue - me.minValue) + 5, //add a small fudge for the end of the slider
418             index = 0,
419             nearest = null,
420             thumbs = me.thumbs,
421             i = 0,
422             len = thumbs.length,
423             thumb,
424             value,
425             dist;
426
427         for (; i < len; i++) {
428             thumb = me.thumbs[i];
429             value = thumb.value;
430             dist  = Math.abs(value - clickValue);
431
432             if (Math.abs(dist <= nearestDistance)) {
433                 nearest = thumb;
434                 index = i;
435                 nearestDistance = dist;
436             }
437         }
438         return nearest;
439     },
440
441     /**
442      * @private
443      * Handler for any keypresses captured by the slider. If the key is UP or RIGHT, the thumb is moved along to the right
444      * by this.keyIncrement. If DOWN or LEFT it is moved left. Pressing CTRL moves the slider to the end in either direction
445      * @param {Ext.EventObject} e The Event object
446      */
447     onKeyDown : function(e) {
448         /*
449          * The behaviour for keyboard handling with multiple thumbs is currently undefined.
450          * There's no real sane default for it, so leave it like this until we come up
451          * with a better way of doing it.
452          */
453         var me = this,
454             k,
455             val;
456         
457         if(me.disabled || me.thumbs.length !== 1) {
458             e.preventDefault();
459             return;
460         }
461         k = e.getKey();
462         
463         switch(k) {
464             case e.UP:
465             case e.RIGHT:
466                 e.stopEvent();
467                 val = e.ctrlKey ? me.maxValue : me.getValue(0) + me.keyIncrement;
468                 me.setValue(0, val, undefined, true);
469             break;
470             case e.DOWN:
471             case e.LEFT:
472                 e.stopEvent();
473                 val = e.ctrlKey ? me.minValue : me.getValue(0) - me.keyIncrement;
474                 me.setValue(0, val, undefined, true);
475             break;
476             default:
477                 e.preventDefault();
478         }
479     },
480
481     /**
482      * @private
483      * If using snapping, this takes a desired new value and returns the closest snapped
484      * value to it
485      * @param {Number} value The unsnapped value
486      * @return {Number} The value of the nearest snap target
487      */
488     doSnap : function(value) {
489         var newValue = value,
490             inc = this.increment,
491             m;
492             
493         if (!(inc && value)) {
494             return value;
495         }
496         m = value % inc;
497         if (m !== 0) {
498             newValue -= m;
499             if (m * 2 >= inc) {
500                 newValue += inc;
501             } else if (m * 2 < -inc) {
502                 newValue -= inc;
503             }
504         }
505         return Ext.Number.constrain(newValue, this.minValue,  this.maxValue);
506     },
507
508     // private
509     afterRender : function() {
510         var me = this,
511             i = 0,
512             thumbs = me.thumbs,
513             len = thumbs.length,
514             thumb,
515             v;
516             
517         me.callParent(arguments);
518
519         for (; i < len; i++) {
520             thumb = thumbs[i];
521
522             if (thumb.value !== undefined) {
523                 v = me.normalizeValue(thumb.value);
524                 if (v !== thumb.value) {
525                     // delete this.value;
526                     me.setValue(i, v, false);
527                 } else {
528                     thumb.move(me.translateValue(v), false);
529                 }
530             }
531         }
532     },
533
534     /**
535      * @private
536      * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100,
537      * the ratio is 2
538      * @return {Number} The ratio of pixels to mapped values
539      */
540     getRatio : function() {
541         var w = this.innerEl.getWidth(),
542             v = this.maxValue - this.minValue;
543         return v === 0 ? w : (w/v);
544     },
545
546     /**
547      * @private
548      * Returns a snapped, constrained value when given a desired value
549      * @param {Number} value Raw number value
550      * @return {Number} The raw value rounded to the correct d.p. and constrained within the set max and min values
551      */
552     normalizeValue : function(v) {
553         var me = this;
554         
555         v = me.doSnap(v);
556         v = Ext.util.Format.round(v, me.decimalPrecision);
557         v = Ext.Number.constrain(v, me.minValue, me.maxValue);
558         return v;
559     },
560
561     /**
562      * Sets the minimum value for the slider instance. If the current value is less than the
563      * minimum value, the current value will be changed.
564      * @param {Number} val The new minimum value
565      */
566     setMinValue : function(val) {
567         var me = this,
568             i = 0,
569             thumbs = me.thumbs,
570             len = thumbs.length,
571             t;
572             
573         me.minValue = val;
574         me.inputEl.dom.setAttribute('aria-valuemin', val);
575
576         for (; i < len; ++i) {
577             t = thumbs[i];
578             t.value = t.value < val ? val : t.value;
579         }
580         me.syncThumbs();
581     },
582
583     /**
584      * Sets the maximum value for the slider instance. If the current value is more than the
585      * maximum value, the current value will be changed.
586      * @param {Number} val The new maximum value
587      */
588     setMaxValue : function(val) {
589         var me = this,
590             i = 0,
591             thumbs = me.thumbs,
592             len = thumbs.length,
593             t;
594             
595         me.maxValue = val;
596         me.inputEl.dom.setAttribute('aria-valuemax', val);
597
598         for (; i < len; ++i) {
599             t = thumbs[i];
600             t.value = t.value > val ? val : t.value;
601         }
602         me.syncThumbs();
603     },
604
605     /**
606      * Programmatically sets the value of the Slider. Ensures that the value is constrained within
607      * the minValue and maxValue.
608      * @param {Number} index Index of the thumb to move
609      * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
610      * @param {Boolean} animate Turn on or off animation, defaults to true
611      */
612     setValue : function(index, value, animate, changeComplete) {
613         var me = this,
614             thumb = me.thumbs[index];
615
616         // ensures value is contstrained and snapped
617         value = me.normalizeValue(value);
618
619         if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) {
620             thumb.value = value;
621             if (me.rendered) {
622                 // TODO this only handles a single value; need a solution for exposing multiple values to aria.
623                 // Perhaps this should go on each thumb element rather than the outer element.
624                 me.inputEl.set({
625                     'aria-valuenow': value,
626                     'aria-valuetext': value
627                 });
628
629                 thumb.move(me.translateValue(value), Ext.isDefined(animate) ? animate !== false : me.animate);
630
631                 me.fireEvent('change', me, value, thumb);
632                 if (changeComplete) {
633                     me.fireEvent('changecomplete', me, value, thumb);
634                 }
635             }
636         }
637     },
638
639     /**
640      * @private
641      */
642     translateValue : function(v) {
643         var ratio = this.getRatio();
644         return (v * ratio) - (this.minValue * ratio) - this.halfThumb;
645     },
646
647     /**
648      * @private
649      * Given a pixel location along the slider, returns the mapped slider value for that pixel.
650      * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500, reverseValue(50)
651      * returns 200
652      * @param {Number} pos The position along the slider to return a mapped value for
653      * @return {Number} The mapped value for the given position
654      */
655     reverseValue : function(pos) {
656         var ratio = this.getRatio();
657         return (pos + (this.minValue * ratio)) / ratio;
658     },
659
660     // private
661     focus : function() {
662         this.focusEl.focus(10);
663     },
664
665     //private
666     onDisable: function() {
667         var me = this,
668             i = 0,
669             thumbs = me.thumbs,
670             len = thumbs.length,
671             thumb,
672             el,
673             xy;
674             
675         me.callParent();
676
677         for (; i < len; i++) {
678             thumb = thumbs[i];
679             el = thumb.el;
680
681             thumb.disable();
682
683             if(Ext.isIE) {
684                 //IE breaks when using overflow visible and opacity other than 1.
685                 //Create a place holder for the thumb and display it.
686                 xy = el.getXY();
687                 el.hide();
688
689                 me.innerEl.addCls(me.disabledCls).dom.disabled = true;
690
691                 if (!me.thumbHolder) {
692                     me.thumbHolder = me.endEl.createChild({cls: Ext.baseCSSPrefix + 'slider-thumb ' + me.disabledCls});
693                 }
694
695                 me.thumbHolder.show().setXY(xy);
696             }
697         }
698     },
699
700     //private
701     onEnable: function() {
702         var me = this,
703             i = 0,
704             thumbs = me.thumbs,
705             len = thumbs.length,
706             thumb,
707             el;
708             
709         this.callParent();
710
711         for (; i < len; i++) {
712             thumb = thumbs[i];
713             el = thumb.el;
714
715             thumb.enable();
716
717             if (Ext.isIE) {
718                 me.innerEl.removeCls(me.disabledCls).dom.disabled = false;
719
720                 if (me.thumbHolder) {
721                     me.thumbHolder.hide();
722                 }
723
724                 el.show();
725                 me.syncThumbs();
726             }
727         }
728     },
729
730     /**
731      * Synchronizes thumbs position to the proper proportion of the total component width based
732      * on the current slider {@link #value}.  This will be called automatically when the Slider
733      * is resized by a layout, but if it is rendered auto width, this method can be called from
734      * another resize handler to sync the Slider if necessary.
735      */
736     syncThumbs : function() {
737         if (this.rendered) {
738             var thumbs = this.thumbs,
739                 length = thumbs.length,
740                 i = 0;
741
742             for (; i < length; i++) {
743                 thumbs[i].move(this.translateValue(thumbs[i].value));
744             }
745         }
746     },
747
748     /**
749      * Returns the current value of the slider
750      * @param {Number} index The index of the thumb to return a value for
751      * @return {Number/Array} The current value of the slider at the given index, or an array of
752      * all thumb values if no index is given.
753      */
754     getValue : function(index) {
755         return Ext.isNumber(index) ? this.thumbs[index].value : this.getValues();
756     },
757
758     /**
759      * Returns an array of values - one for the location of each thumb
760      * @return {Array} The set of thumb values
761      */
762     getValues: function() {
763         var values = [],
764             i = 0,
765             thumbs = this.thumbs,
766             len = thumbs.length;
767
768         for (; i < len; i++) {
769             values.push(thumbs[i].value);
770         }
771
772         return values;
773     },
774
775     getSubmitValue: function() {
776         var me = this;
777         return (me.disabled || !me.submitValue) ? null : me.getValue();
778     },
779
780     reset: function() {
781         var me = this,
782             Array = Ext.Array;
783         Array.forEach(Array.from(me.originalValue), function(val, i) {
784             me.setValue(i, val);
785         });
786         me.clearInvalid();
787         // delete here so we reset back to the original state
788         delete me.wasValid;
789     },
790
791     // private
792     beforeDestroy : function() {
793         var me = this;
794         
795         Ext.destroyMembers(me.innerEl, me.endEl, me.focusEl);
796         Ext.each(me.thumbs, function(thumb) {
797             Ext.destroy(thumb);
798         }, me);
799
800         me.callParent();
801     },
802
803     statics: {
804         // Method overrides to support slider with vertical orientation
805         Vertical: {
806             getRatio : function() {
807                 var h = this.innerEl.getHeight(),
808                     v = this.maxValue - this.minValue;
809                 return h/v;
810             },
811
812             onClickChange : function(local) {
813                 var me = this,
814                     thumb, index, bottom;
815
816                 if (local.left > me.clickRange[0] && local.left < me.clickRange[1]) {
817                     thumb = me.getNearest(local, 'top');
818                     if (!thumb.disabled) {
819                         index = thumb.index;
820                         bottom =  me.reverseValue(me.innerEl.getHeight() - local.top);
821
822                         me.setValue(index, Ext.util.Format.round(me.minValue + bottom, me.decimalPrecision), undefined, true);
823                     }
824                 }
825             }
826         }
827     }
828 });