3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis clicking
17 * and animation. Can be added as an item to any container.
19 * Sliders can be created with more than one thumb handle by passing an array of values instead of a single one:
22 * Ext.create('Ext.slider.Multi', {
24 * values: [25, 50, 75],
29 * // this defaults to true, setting to false allows the thumbs to pass each other
30 * constrainThumbs: false,
31 * renderTo: Ext.getBody()
34 Ext.define('Ext.slider.Multi', {
35 extend: 'Ext.form.field.Base',
36 alias: 'widget.multislider',
37 alternateClassName: 'Ext.slider.MultiSlider',
45 'Ext.layout.component.field.Slider'
48 // note: {id} here is really {inputId}, but {cmpId} is available
50 '<div id="{id}" class="' + Ext.baseCSSPrefix + 'slider {fieldCls} {vertical}" aria-valuemin="{minValue}" aria-valuemax="{maxValue}" aria-valuenow="{value}" aria-valuetext="{value}">',
51 '<div id="{cmpId}-endEl" class="' + Ext.baseCSSPrefix + 'slider-end" role="presentation">',
52 '<div id="{cmpId}-innerEl" class="' + Ext.baseCSSPrefix + 'slider-inner" role="presentation">',
53 '<a id="{cmpId}-focusEl" class="' + Ext.baseCSSPrefix + 'slider-focus" href="#" tabIndex="-1" hidefocus="on" role="presentation"></a>',
65 * A value with which to initialize the slider. Defaults to minValue. Setting this will only result in the creation
66 * of a single slider thumb; if you want multiple thumbs then use the {@link #values} config instead.
70 * @cfg {Number[]} values
71 * Array of Number values with which to initalize the slider. A separate slider thumb will be created for each value
72 * in this array. This will take precedence over the single {@link #value} config.
76 * @cfg {Boolean} vertical
77 * Orient the Slider vertically rather than horizontally.
82 * @cfg {Number} minValue
83 * The minimum value for the Slider.
88 * @cfg {Number} maxValue
89 * The maximum value for the Slider.
94 * @cfg {Number/Boolean} decimalPrecision The number of decimal places to which to round the Slider's value.
96 * To disable rounding, configure as **false**.
101 * @cfg {Number} keyIncrement
102 * How many units to change the Slider when adjusting with keyboard navigation. If the increment
103 * config is larger, it will be used instead.
108 * @cfg {Number} increment
109 * How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.
115 * @property {Number[]} clickRange
116 * 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],
117 * 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'
118 * 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
123 * @cfg {Boolean} clickToChange
124 * Determines whether or not clicking on the Slider axis will change the slider.
126 clickToChange : true,
129 * @cfg {Boolean} animate
130 * Turn on or off animation.
135 * @property {Boolean} dragging
136 * True while the thumb is in a drag operation
141 * @cfg {Boolean} constrainThumbs
142 * True to disallow thumbs from overlapping one another.
144 constrainThumbs: true,
146 componentLayout: 'sliderfield',
149 * @cfg {Boolean} useTips
150 * True to use an Ext.slider.Tip to display tips for the value.
155 * @cfg {Function} tipText
156 * A function used to display custom text for the slider tip. Defaults to null, which will use the default on the
164 initValue: function() {
166 extValue = Ext.value,
167 // Fallback for initial values: values config -> value config -> minValue config -> 0
168 values = extValue(me.values, [extValue(me.value, extValue(me.minValue, 0))]),
172 // Store for use in dirty check
173 me.originalValue = values;
175 // Add a thumb for each value
176 for (; i < len; i++) {
177 me.addThumb(values[i]);
182 initComponent : function() {
188 * @property {Array} thumbs
189 * Array containing references to each thumb
193 me.keyIncrement = Math.max(me.increment, me.keyIncrement);
197 * @event beforechange
198 * Fires before the slider value is changed. By returning false from an event handler, you can cancel the
199 * event and prevent the slider from changing.
200 * @param {Ext.slider.Multi} slider The slider
201 * @param {Number} newValue The new value which the slider is being changed to.
202 * @param {Number} oldValue The old value which the slider was previously.
208 * Fires when the slider value is changed.
209 * @param {Ext.slider.Multi} slider The slider
210 * @param {Number} newValue The new value which the slider has been changed to.
211 * @param {Ext.slider.Thumb} thumb The thumb that was changed
216 * @event changecomplete
217 * Fires when the slider value is changed by the user and any drag operations have completed.
218 * @param {Ext.slider.Multi} slider The slider
219 * @param {Number} newValue The new value which the slider has been changed to.
220 * @param {Ext.slider.Thumb} thumb The thumb that was changed
226 * Fires after a drag operation has started.
227 * @param {Ext.slider.Multi} slider The slider
228 * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
234 * Fires continuously during the drag operation while the mouse is moving.
235 * @param {Ext.slider.Multi} slider The slider
236 * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
242 * Fires after the drag operation has completed.
243 * @param {Ext.slider.Multi} slider The slider
244 * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
250 Ext.apply(me, Ext.slider.Multi.Vertical);
255 // only can use it if it exists.
257 tipPlug = me.tipText ? {getText: me.tipText} : {};
258 me.plugins = me.plugins || [];
259 Ext.each(me.plugins, function(plug){
260 if (plug.isSliderTip) {
266 me.plugins.push(Ext.create('Ext.slider.Tip', tipPlug));
272 * Creates a new thumb and adds it to the slider
273 * @param {Number} value The initial value to set on the thumb. Defaults to 0
274 * @return {Ext.slider.Thumb} The thumb
276 addThumb: function(value) {
278 thumb = Ext.create('Ext.slider.Thumb', {
281 index : me.thumbs.length,
282 constrain: me.constrainThumbs
284 me.thumbs.push(thumb);
286 //render the thumb now if needed
296 * Moves the given thumb above all other by increasing its z-index. This is called when as drag
297 * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
298 * required when the thumbs are stacked on top of each other at one of the ends of the slider's
299 * range, which can result in the user not being able to move any of them.
300 * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
302 promoteThumb: function(topThumb) {
303 var thumbs = this.thumbs,
307 for (i = 0; i < ln; i++) {
310 if (thumb == topThumb) {
311 thumb.bringToFront();
319 onRender : function() {
326 Ext.applyIf(me.subTplData, {
327 vertical: me.vertical ? Ext.baseCSSPrefix + 'slider-vert' : Ext.baseCSSPrefix + 'slider-horz',
328 minValue: me.minValue,
329 maxValue: me.maxValue,
333 me.addChildEls('endEl', 'innerEl', 'focusEl');
335 me.callParent(arguments);
338 for (; i < len; i++) {
342 //calculate the size of half a thumb
343 thumb = me.innerEl.down('.' + Ext.baseCSSPrefix + 'slider-thumb');
344 me.halfThumb = (me.vertical ? thumb.getHeight() : thumb.getWidth()) / 2;
349 * Utility method to set the value of the field when the slider changes.
350 * @param {Object} slider The slider object.
351 * @param {Object} v The new value.
354 onChange : function(slider, v) {
355 this.setValue(v, undefined, true);
360 * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element.
362 initEvents : function() {
367 mousedown: me.onMouseDown,
368 keydown : me.onKeyDown,
372 me.focusEl.swallowEvent("click", true);
377 * Mousedown handler for the slider. If the clickToChange is enabled and the click was not on the draggable 'thumb',
378 * this calculates the new value of the slider and tells the implementation (Horizontal or Vertical) to move the thumb
379 * @param {Ext.EventObject} e The click event
381 onMouseDown : function(e) {
383 thumbClicked = false,
393 //see if the click was on any of the thumbs
394 for (; i < len; i++) {
395 thumbClicked = thumbClicked || e.target == thumbs[i].el.dom;
398 if (me.clickToChange && !thumbClicked) {
399 local = me.innerEl.translatePoints(e.getXY());
400 me.onClickChange(local);
407 * Moves the thumb to the indicated position. Note that a Vertical implementation is provided in Ext.slider.Multi.Vertical.
408 * Only changes the value if the click was within this.clickRange.
409 * @param {Object} local Object containing top and left values for the click event.
411 onClickChange : function(local) {
415 if (local.top > me.clickRange[0] && local.top < me.clickRange[1]) {
416 //find the nearest thumb to the click event
417 thumb = me.getNearest(local, 'left');
418 if (!thumb.disabled) {
420 me.setValue(index, Ext.util.Format.round(me.reverseValue(local.left), me.decimalPrecision), undefined, true);
427 * Returns the nearest thumb to a click event, along with its distance
428 * @param {Object} local Object containing top and left values from a click event
429 * @param {String} prop The property of local to compare on. Use 'left' for horizontal sliders, 'top' for vertical ones
430 * @return {Object} The closest thumb object and its distance from the click event
432 getNearest: function(local, prop) {
434 localValue = prop == 'top' ? me.innerEl.getHeight() - local[prop] : local[prop],
435 clickValue = me.reverseValue(localValue),
436 nearestDistance = (me.maxValue - me.minValue) + 5, //add a small fudge for the end of the slider
446 for (; i < len; i++) {
447 thumb = me.thumbs[i];
449 dist = Math.abs(value - clickValue);
451 if (Math.abs(dist <= nearestDistance)) {
454 nearestDistance = dist;
462 * Handler for any keypresses captured by the slider. If the key is UP or RIGHT, the thumb is moved along to the right
463 * by this.keyIncrement. If DOWN or LEFT it is moved left. Pressing CTRL moves the slider to the end in either direction
464 * @param {Ext.EventObject} e The Event object
466 onKeyDown : function(e) {
468 * The behaviour for keyboard handling with multiple thumbs is currently undefined.
469 * There's no real sane default for it, so leave it like this until we come up
470 * with a better way of doing it.
476 if(me.disabled || me.thumbs.length !== 1) {
486 val = e.ctrlKey ? me.maxValue : me.getValue(0) + me.keyIncrement;
487 me.setValue(0, val, undefined, true);
492 val = e.ctrlKey ? me.minValue : me.getValue(0) - me.keyIncrement;
493 me.setValue(0, val, undefined, true);
501 afterRender : function() {
509 me.callParent(arguments);
511 for (; i < len; i++) {
514 if (thumb.value !== undefined) {
515 v = me.normalizeValue(thumb.value);
516 if (v !== thumb.value) {
517 // delete this.value;
518 me.setValue(i, v, false);
520 thumb.move(me.translateValue(v), false);
528 * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100,
530 * @return {Number} The ratio of pixels to mapped values
532 getRatio : function() {
533 var w = this.innerEl.getWidth(),
534 v = this.maxValue - this.minValue;
535 return v === 0 ? w : (w/v);
540 * Returns a snapped, constrained value when given a desired value
541 * @param {Number} value Raw number value
542 * @return {Number} The raw value rounded to the correct d.p. and constrained within the set max and min values
544 normalizeValue : function(v) {
547 v = Ext.Number.snap(v, this.increment, this.minValue, this.maxValue);
548 v = Ext.util.Format.round(v, me.decimalPrecision);
549 v = Ext.Number.constrain(v, me.minValue, me.maxValue);
554 * Sets the minimum value for the slider instance. If the current value is less than the minimum value, the current
555 * value will be changed.
556 * @param {Number} val The new minimum value
558 setMinValue : function(val) {
567 me.inputEl.dom.setAttribute('aria-valuemin', val);
570 for (; i < len; ++i) {
572 t.value = t.value < val ? val : t.value;
578 * Sets the maximum value for the slider instance. If the current value is more than the maximum value, the current
579 * value will be changed.
580 * @param {Number} val The new maximum value
582 setMaxValue : function(val) {
591 me.inputEl.dom.setAttribute('aria-valuemax', val);
594 for (; i < len; ++i) {
596 t.value = t.value > val ? val : t.value;
602 * Programmatically sets the value of the Slider. Ensures that the value is constrained within the minValue and
604 * @param {Number} index Index of the thumb to move
605 * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
606 * @param {Boolean} [animate=true] Turn on or off animation
608 setValue : function(index, value, animate, changeComplete) {
610 thumb = me.thumbs[index];
612 // ensures value is contstrained and snapped
613 value = me.normalizeValue(value);
615 if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) {
618 // TODO this only handles a single value; need a solution for exposing multiple values to aria.
619 // Perhaps this should go on each thumb element rather than the outer element.
621 'aria-valuenow': value,
622 'aria-valuetext': value
625 thumb.move(me.translateValue(value), Ext.isDefined(animate) ? animate !== false : me.animate);
627 me.fireEvent('change', me, value, thumb);
628 if (changeComplete) {
629 me.fireEvent('changecomplete', me, value, thumb);
638 translateValue : function(v) {
639 var ratio = this.getRatio();
640 return (v * ratio) - (this.minValue * ratio) - this.halfThumb;
645 * Given a pixel location along the slider, returns the mapped slider value for that pixel.
646 * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500, reverseValue(50)
648 * @param {Number} pos The position along the slider to return a mapped value for
649 * @return {Number} The mapped value for the given position
651 reverseValue : function(pos) {
652 var ratio = this.getRatio();
653 return (pos + (this.minValue * ratio)) / ratio;
658 this.focusEl.focus(10);
662 onDisable: function() {
673 for (; i < len; i++) {
680 //IE breaks when using overflow visible and opacity other than 1.
681 //Create a place holder for the thumb and display it.
685 me.innerEl.addCls(me.disabledCls).dom.disabled = true;
687 if (!me.thumbHolder) {
688 me.thumbHolder = me.endEl.createChild({cls: Ext.baseCSSPrefix + 'slider-thumb ' + me.disabledCls});
691 me.thumbHolder.show().setXY(xy);
697 onEnable: function() {
707 for (; i < len; i++) {
714 me.innerEl.removeCls(me.disabledCls).dom.disabled = false;
716 if (me.thumbHolder) {
717 me.thumbHolder.hide();
727 * Synchronizes thumbs position to the proper proportion of the total component width based on the current slider
728 * {@link #value}. This will be called automatically when the Slider is resized by a layout, but if it is rendered
729 * auto width, this method can be called from another resize handler to sync the Slider if necessary.
731 syncThumbs : function() {
733 var thumbs = this.thumbs,
734 length = thumbs.length,
737 for (; i < length; i++) {
738 thumbs[i].move(this.translateValue(thumbs[i].value));
744 * Returns the current value of the slider
745 * @param {Number} index The index of the thumb to return a value for
746 * @return {Number/Number[]} The current value of the slider at the given index, or an array of all thumb values if
749 getValue : function(index) {
750 return Ext.isNumber(index) ? this.thumbs[index].value : this.getValues();
754 * Returns an array of values - one for the location of each thumb
755 * @return {Number[]} The set of thumb values
757 getValues: function() {
760 thumbs = this.thumbs,
763 for (; i < len; i++) {
764 values.push(thumbs[i].value);
770 getSubmitValue: function() {
772 return (me.disabled || !me.submitValue) ? null : me.getValue();
778 Array.forEach(Array.from(me.originalValue), function(val, i) {
782 // delete here so we reset back to the original state
787 beforeDestroy : function() {
790 Ext.destroy(me.innerEl, me.endEl, me.focusEl);
791 Ext.each(me.thumbs, function(thumb) {
799 // Method overrides to support slider with vertical orientation
801 getRatio : function() {
802 var h = this.innerEl.getHeight(),
803 v = this.maxValue - this.minValue;
807 onClickChange : function(local) {
809 thumb, index, bottom;
811 if (local.left > me.clickRange[0] && local.left < me.clickRange[1]) {
812 thumb = me.getNearest(local, 'top');
813 if (!thumb.disabled) {
815 bottom = me.reverseValue(me.innerEl.getHeight() - local.top);
817 me.setValue(index, Ext.util.Format.round(me.minValue + bottom, me.decimalPrecision), undefined, true);