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 * @class Ext.slider.Multi
17 * @extends Ext.form.field.Base
18 * <p>Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis
19 * clicking and animation. Can be added as an item to any container. In addition,
20 * {@img Ext.slider.Multi/Ext.slider.Multi.png Ext.slider.Multi component}
21 * <p>Example usage:</p>
22 * Sliders can be created with more than one thumb handle by passing an array of values instead of a single one:
24 Ext.create('Ext.slider.Multi', {
31 //this defaults to true, setting to false allows the thumbs to pass each other
32 {@link #constrainThumbs}: false,
33 renderTo: Ext.getBody()
37 Ext.define('Ext.slider.Multi', {
38 extend: 'Ext.form.field.Base',
39 alias: 'widget.multislider',
40 alternateClassName: 'Ext.slider.MultiSlider',
48 'Ext.layout.component.field.Slider'
52 '<div class="' + Ext.baseCSSPrefix + 'slider {fieldCls} {vertical}" aria-valuemin="{minValue}" aria-valuemax="{maxValue}" aria-valuenow="{value}" aria-valuetext="{value}">',
53 '<div class="' + Ext.baseCSSPrefix + 'slider-end" role="presentation">',
54 '<div class="' + Ext.baseCSSPrefix + 'slider-inner" role="presentation">',
55 '<a class="' + Ext.baseCSSPrefix + 'slider-focus" href="#" tabIndex="-1" hidefocus="on" role="presentation"></a>',
67 * A value with which to initialize the slider. Defaults to minValue. Setting this will only
68 * result in the creation of a single slider thumb; if you want multiple thumbs then use the
69 * {@link #values} config instead.
74 * Array of Number values with which to initalize the slider. A separate slider thumb will be created for
75 * each value in this array. This will take precedence over the single {@link #value} config.
79 * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false.
83 * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0.
87 * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100.
91 * @cfg {Number/Boolean} decimalPrecision.
92 * <p>The number of decimal places to which to round the Slider's value. Defaults to 0.</p>
93 * <p>To disable rounding, configure as <tt><b>false</b></tt>.</p>
97 * @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.
101 * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.
107 * @property clickRange
109 * 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],
110 * 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'
111 * 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
116 * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true
118 clickToChange : true,
120 * @cfg {Boolean} animate Turn on or off animation. Defaults to true
125 * True while the thumb is in a drag operation
131 * @cfg {Boolean} constrainThumbs True to disallow thumbs from overlapping one another. Defaults to true
133 constrainThumbs: true,
135 componentLayout: 'sliderfield',
138 * @cfg {Boolean} useTips
139 * True to use an Ext.slider.Tip to display tips for the value. Defaults to <tt>true</tt>.
144 * @cfg {Function} tipText
145 * A function used to display custom text for the slider tip. Defaults to <tt>null</tt>, which will
146 * use the default on the plugin.
153 initValue: function() {
155 extValue = Ext.value,
156 // Fallback for initial values: values config -> value config -> minValue config -> 0
157 values = extValue(me.values, [extValue(me.value, extValue(me.minValue, 0))]),
161 // Store for use in dirty check
162 me.originalValue = values;
164 // Add a thumb for each value
165 for (; i < len; i++) {
166 me.addThumb(values[i]);
171 initComponent : function() {
179 * Array containing references to each thumb
183 me.keyIncrement = Math.max(me.increment, me.keyIncrement);
187 * @event beforechange
188 * Fires before the slider value is changed. By returning false from an event handler,
189 * you can cancel the event and prevent the slider from changing.
190 * @param {Ext.slider.Multi} slider The slider
191 * @param {Number} newValue The new value which the slider is being changed to.
192 * @param {Number} oldValue The old value which the slider was previously.
198 * Fires when the slider value is changed.
199 * @param {Ext.slider.Multi} slider The slider
200 * @param {Number} newValue The new value which the slider has been changed to.
201 * @param {Ext.slider.Thumb} thumb The thumb that was changed
206 * @event changecomplete
207 * Fires when the slider value is changed by the user and any drag operations have completed.
208 * @param {Ext.slider.Multi} slider The slider
209 * @param {Number} newValue The new value which the slider has been changed to.
210 * @param {Ext.slider.Thumb} thumb The thumb that was changed
216 * Fires after a drag operation has started.
217 * @param {Ext.slider.Multi} slider The slider
218 * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
224 * Fires continuously during the drag operation while the mouse is moving.
225 * @param {Ext.slider.Multi} slider The slider
226 * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
232 * Fires after the drag operation has completed.
233 * @param {Ext.slider.Multi} slider The slider
234 * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker
240 Ext.apply(me, Ext.slider.Multi.Vertical);
245 // only can use it if it exists.
247 tipPlug = me.tipText ? {getText: me.tipText} : {};
248 me.plugins = me.plugins || [];
249 Ext.each(me.plugins, function(plug){
250 if (plug.isSliderTip) {
256 me.plugins.push(Ext.create('Ext.slider.Tip', tipPlug));
262 * Creates a new thumb and adds it to the slider
263 * @param {Number} value The initial value to set on the thumb. Defaults to 0
264 * @return {Ext.slider.Thumb} The thumb
266 addThumb: function(value) {
268 thumb = Ext.create('Ext.slider.Thumb', {
271 index : me.thumbs.length,
272 constrain: me.constrainThumbs
274 me.thumbs.push(thumb);
276 //render the thumb now if needed
286 * Moves the given thumb above all other by increasing its z-index. This is called when as drag
287 * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is
288 * required when the thumbs are stacked on top of each other at one of the ends of the slider's
289 * range, which can result in the user not being able to move any of them.
290 * @param {Ext.slider.Thumb} topThumb The thumb to move to the top
292 promoteThumb: function(topThumb) {
293 var thumbs = this.thumbs,
297 for (i = 0; i < ln; i++) {
300 if (thumb == topThumb) {
301 thumb.bringToFront();
309 onRender : function() {
316 Ext.applyIf(me.subTplData, {
317 vertical: me.vertical ? Ext.baseCSSPrefix + 'slider-vert' : Ext.baseCSSPrefix + 'slider-horz',
318 minValue: me.minValue,
319 maxValue: me.maxValue,
323 Ext.applyIf(me.renderSelectors, {
324 endEl: '.' + Ext.baseCSSPrefix + 'slider-end',
325 innerEl: '.' + Ext.baseCSSPrefix + 'slider-inner',
326 focusEl: '.' + Ext.baseCSSPrefix + 'slider-focus'
329 me.callParent(arguments);
332 for (; i < len; i++) {
336 //calculate the size of half a thumb
337 thumb = me.innerEl.down('.' + Ext.baseCSSPrefix + 'slider-thumb');
338 me.halfThumb = (me.vertical ? thumb.getHeight() : thumb.getWidth()) / 2;
343 * Utility method to set the value of the field when the slider changes.
344 * @param {Object} slider The slider object.
345 * @param {Object} v The new value.
348 onChange : function(slider, v) {
349 this.setValue(v, undefined, true);
354 * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element.
356 initEvents : function() {
361 mousedown: me.onMouseDown,
362 keydown : me.onKeyDown,
366 me.focusEl.swallowEvent("click", true);
371 * Mousedown handler for the slider. If the clickToChange is enabled and the click was not on the draggable 'thumb',
372 * this calculates the new value of the slider and tells the implementation (Horizontal or Vertical) to move the thumb
373 * @param {Ext.EventObject} e The click event
375 onMouseDown : function(e) {
377 thumbClicked = false,
387 //see if the click was on any of the thumbs
388 for (; i < len; i++) {
389 thumbClicked = thumbClicked || e.target == thumbs[i].el.dom;
392 if (me.clickToChange && !thumbClicked) {
393 local = me.innerEl.translatePoints(e.getXY());
394 me.onClickChange(local);
401 * Moves the thumb to the indicated position. Note that a Vertical implementation is provided in Ext.slider.Multi.Vertical.
402 * Only changes the value if the click was within this.clickRange.
403 * @param {Object} local Object containing top and left values for the click event.
405 onClickChange : function(local) {
409 if (local.top > me.clickRange[0] && local.top < me.clickRange[1]) {
410 //find the nearest thumb to the click event
411 thumb = me.getNearest(local, 'left');
412 if (!thumb.disabled) {
414 me.setValue(index, Ext.util.Format.round(me.reverseValue(local.left), me.decimalPrecision), undefined, true);
421 * Returns the nearest thumb to a click event, along with its distance
422 * @param {Object} local Object containing top and left values from a click event
423 * @param {String} prop The property of local to compare on. Use 'left' for horizontal sliders, 'top' for vertical ones
424 * @return {Object} The closest thumb object and its distance from the click event
426 getNearest: function(local, prop) {
428 localValue = prop == 'top' ? me.innerEl.getHeight() - local[prop] : local[prop],
429 clickValue = me.reverseValue(localValue),
430 nearestDistance = (me.maxValue - me.minValue) + 5, //add a small fudge for the end of the slider
440 for (; i < len; i++) {
441 thumb = me.thumbs[i];
443 dist = Math.abs(value - clickValue);
445 if (Math.abs(dist <= nearestDistance)) {
448 nearestDistance = dist;
456 * Handler for any keypresses captured by the slider. If the key is UP or RIGHT, the thumb is moved along to the right
457 * by this.keyIncrement. If DOWN or LEFT it is moved left. Pressing CTRL moves the slider to the end in either direction
458 * @param {Ext.EventObject} e The Event object
460 onKeyDown : function(e) {
462 * The behaviour for keyboard handling with multiple thumbs is currently undefined.
463 * There's no real sane default for it, so leave it like this until we come up
464 * with a better way of doing it.
470 if(me.disabled || me.thumbs.length !== 1) {
480 val = e.ctrlKey ? me.maxValue : me.getValue(0) + me.keyIncrement;
481 me.setValue(0, val, undefined, true);
486 val = e.ctrlKey ? me.minValue : me.getValue(0) - me.keyIncrement;
487 me.setValue(0, val, undefined, true);
495 afterRender : function() {
503 me.callParent(arguments);
505 for (; i < len; i++) {
508 if (thumb.value !== undefined) {
509 v = me.normalizeValue(thumb.value);
510 if (v !== thumb.value) {
511 // delete this.value;
512 me.setValue(i, v, false);
514 thumb.move(me.translateValue(v), false);
522 * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100,
524 * @return {Number} The ratio of pixels to mapped values
526 getRatio : function() {
527 var w = this.innerEl.getWidth(),
528 v = this.maxValue - this.minValue;
529 return v === 0 ? w : (w/v);
534 * Returns a snapped, constrained value when given a desired value
535 * @param {Number} value Raw number value
536 * @return {Number} The raw value rounded to the correct d.p. and constrained within the set max and min values
538 normalizeValue : function(v) {
541 v = Ext.Number.snap(v, this.increment, this.minValue, this.maxValue);
542 v = Ext.util.Format.round(v, me.decimalPrecision);
543 v = Ext.Number.constrain(v, me.minValue, me.maxValue);
548 * Sets the minimum value for the slider instance. If the current value is less than the
549 * minimum value, the current value will be changed.
550 * @param {Number} val The new minimum value
552 setMinValue : function(val) {
560 me.inputEl.dom.setAttribute('aria-valuemin', val);
562 for (; i < len; ++i) {
564 t.value = t.value < val ? val : t.value;
570 * Sets the maximum value for the slider instance. If the current value is more than the
571 * maximum value, the current value will be changed.
572 * @param {Number} val The new maximum value
574 setMaxValue : function(val) {
582 me.inputEl.dom.setAttribute('aria-valuemax', val);
584 for (; i < len; ++i) {
586 t.value = t.value > val ? val : t.value;
592 * Programmatically sets the value of the Slider. Ensures that the value is constrained within
593 * the minValue and maxValue.
594 * @param {Number} index Index of the thumb to move
595 * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue)
596 * @param {Boolean} animate Turn on or off animation, defaults to true
598 setValue : function(index, value, animate, changeComplete) {
600 thumb = me.thumbs[index];
602 // ensures value is contstrained and snapped
603 value = me.normalizeValue(value);
605 if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) {
608 // TODO this only handles a single value; need a solution for exposing multiple values to aria.
609 // Perhaps this should go on each thumb element rather than the outer element.
611 'aria-valuenow': value,
612 'aria-valuetext': value
615 thumb.move(me.translateValue(value), Ext.isDefined(animate) ? animate !== false : me.animate);
617 me.fireEvent('change', me, value, thumb);
618 if (changeComplete) {
619 me.fireEvent('changecomplete', me, value, thumb);
628 translateValue : function(v) {
629 var ratio = this.getRatio();
630 return (v * ratio) - (this.minValue * ratio) - this.halfThumb;
635 * Given a pixel location along the slider, returns the mapped slider value for that pixel.
636 * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500, reverseValue(50)
638 * @param {Number} pos The position along the slider to return a mapped value for
639 * @return {Number} The mapped value for the given position
641 reverseValue : function(pos) {
642 var ratio = this.getRatio();
643 return (pos + (this.minValue * ratio)) / ratio;
648 this.focusEl.focus(10);
652 onDisable: function() {
663 for (; i < len; i++) {
670 //IE breaks when using overflow visible and opacity other than 1.
671 //Create a place holder for the thumb and display it.
675 me.innerEl.addCls(me.disabledCls).dom.disabled = true;
677 if (!me.thumbHolder) {
678 me.thumbHolder = me.endEl.createChild({cls: Ext.baseCSSPrefix + 'slider-thumb ' + me.disabledCls});
681 me.thumbHolder.show().setXY(xy);
687 onEnable: function() {
697 for (; i < len; i++) {
704 me.innerEl.removeCls(me.disabledCls).dom.disabled = false;
706 if (me.thumbHolder) {
707 me.thumbHolder.hide();
717 * Synchronizes thumbs position to the proper proportion of the total component width based
718 * on the current slider {@link #value}. This will be called automatically when the Slider
719 * is resized by a layout, but if it is rendered auto width, this method can be called from
720 * another resize handler to sync the Slider if necessary.
722 syncThumbs : function() {
724 var thumbs = this.thumbs,
725 length = thumbs.length,
728 for (; i < length; i++) {
729 thumbs[i].move(this.translateValue(thumbs[i].value));
735 * Returns the current value of the slider
736 * @param {Number} index The index of the thumb to return a value for
737 * @return {Number/Array} The current value of the slider at the given index, or an array of
738 * all thumb values if no index is given.
740 getValue : function(index) {
741 return Ext.isNumber(index) ? this.thumbs[index].value : this.getValues();
745 * Returns an array of values - one for the location of each thumb
746 * @return {Array} The set of thumb values
748 getValues: function() {
751 thumbs = this.thumbs,
754 for (; i < len; i++) {
755 values.push(thumbs[i].value);
761 getSubmitValue: function() {
763 return (me.disabled || !me.submitValue) ? null : me.getValue();
769 Array.forEach(Array.from(me.originalValue), function(val, i) {
773 // delete here so we reset back to the original state
778 beforeDestroy : function() {
781 Ext.destroyMembers(me.innerEl, me.endEl, me.focusEl);
782 Ext.each(me.thumbs, function(thumb) {
790 // Method overrides to support slider with vertical orientation
792 getRatio : function() {
793 var h = this.innerEl.getHeight(),
794 v = this.maxValue - this.minValue;
798 onClickChange : function(local) {
800 thumb, index, bottom;
802 if (local.left > me.clickRange[0] && local.left < me.clickRange[1]) {
803 thumb = me.getNearest(local, 'top');
804 if (!thumb.disabled) {
806 bottom = me.reverseValue(me.innerEl.getHeight() - local.top);
808 me.setValue(index, Ext.util.Format.round(me.minValue + bottom, me.decimalPrecision), undefined, true);