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:
10 Ext.create('Ext.slider.Multi', {
17 //this defaults to true, setting to false allows the thumbs to pass each other
18 {@link #constrainThumbs}: false,
19 renderTo: Ext.getBody()
24 Ext.define('Ext.slider.Multi', {
25 extend: 'Ext.form.field.Base',
26 alias: 'widget.multislider',
27 alternateClassName: 'Ext.slider.MultiSlider',
35 'Ext.layout.component.field.Slider'
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>',
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.
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.
66 * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false.
70 * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0.
74 * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100.
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>
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.
88 * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'.
94 * @property clickRange
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
103 * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true
105 clickToChange : true,
107 * @cfg {Boolean} animate Turn on or off animation. Defaults to true
112 * True while the thumb is in a drag operation
118 * @cfg {Boolean} constrainThumbs True to disallow thumbs from overlapping one another. Defaults to true
120 constrainThumbs: true,
122 componentLayout: 'sliderfield',
125 * @cfg {Boolean} useTips
126 * True to use an Ext.slider.Tip to display tips for the value. Defaults to <tt>true</tt>.
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.
140 initValue: function() {
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))]),
148 // Store for use in dirty check
149 me.originalValue = values;
151 // Add a thumb for each value
152 for (; i < len; i++) {
153 me.addThumb(values[i]);
158 initComponent : function() {
166 * Array containing references to each thumb
170 me.keyIncrement = Math.max(me.increment, me.keyIncrement);
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.
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
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
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
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
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
227 Ext.apply(me, Ext.slider.Multi.Vertical);
232 // only can use it if it exists.
234 tipPlug = me.tipText ? {getText: me.tipText} : {};
235 me.plugins = me.plugins || [];
236 Ext.each(me.plugins, function(plug){
237 if (plug.isSliderTip) {
243 me.plugins.push(Ext.create('Ext.slider.Tip', tipPlug));
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
253 addThumb: function(value) {
255 thumb = Ext.create('Ext.slider.Thumb', {
258 index : me.thumbs.length,
259 constrain: me.constrainThumbs
261 me.thumbs.push(thumb);
263 //render the thumb now if needed
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
279 promoteThumb: function(topThumb) {
280 var thumbs = this.thumbs,
284 for (i = 0; i < ln; i++) {
287 if (thumb == topThumb) {
288 thumb.bringToFront();
296 onRender : function() {
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,
310 Ext.applyIf(me.renderSelectors, {
311 endEl: '.' + Ext.baseCSSPrefix + 'slider-end',
312 innerEl: '.' + Ext.baseCSSPrefix + 'slider-inner',
313 focusEl: '.' + Ext.baseCSSPrefix + 'slider-focus'
316 me.callParent(arguments);
319 for (; i < len; i++) {
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;
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.
335 onChange : function(slider, v) {
336 this.setValue(v, undefined, true);
341 * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element.
343 initEvents : function() {
348 mousedown: me.onMouseDown,
349 keydown : me.onKeyDown,
353 me.focusEl.swallowEvent("click", true);
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
362 onMouseDown : function(e) {
364 thumbClicked = false,
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;
379 if (me.clickToChange && !thumbClicked) {
380 local = me.innerEl.translatePoints(e.getXY());
381 me.onClickChange(local);
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.
392 onClickChange : function(local) {
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) {
401 me.setValue(index, Ext.util.Format.round(me.reverseValue(local.left), me.decimalPrecision), undefined, true);
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
413 getNearest: function(local, prop) {
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
427 for (; i < len; i++) {
428 thumb = me.thumbs[i];
430 dist = Math.abs(value - clickValue);
432 if (Math.abs(dist <= nearestDistance)) {
435 nearestDistance = dist;
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
447 onKeyDown : function(e) {
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.
457 if(me.disabled || me.thumbs.length !== 1) {
467 val = e.ctrlKey ? me.maxValue : me.getValue(0) + me.keyIncrement;
468 me.setValue(0, val, undefined, true);
473 val = e.ctrlKey ? me.minValue : me.getValue(0) - me.keyIncrement;
474 me.setValue(0, val, undefined, true);
483 * If using snapping, this takes a desired new value and returns the closest snapped
485 * @param {Number} value The unsnapped value
486 * @return {Number} The value of the nearest snap target
488 doSnap : function(value) {
489 var newValue = value,
490 inc = this.increment,
493 if (!(inc && value)) {
501 } else if (m * 2 < -inc) {
505 return Ext.Number.constrain(newValue, this.minValue, this.maxValue);
509 afterRender : function() {
517 me.callParent(arguments);
519 for (; i < len; i++) {
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);
528 thumb.move(me.translateValue(v), false);
536 * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100,
538 * @return {Number} The ratio of pixels to mapped values
540 getRatio : function() {
541 var w = this.innerEl.getWidth(),
542 v = this.maxValue - this.minValue;
543 return v === 0 ? w : (w/v);
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
552 normalizeValue : function(v) {
556 v = Ext.util.Format.round(v, me.decimalPrecision);
557 v = Ext.Number.constrain(v, me.minValue, me.maxValue);
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
566 setMinValue : function(val) {
574 me.inputEl.dom.setAttribute('aria-valuemin', val);
576 for (; i < len; ++i) {
578 t.value = t.value < val ? val : t.value;
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
588 setMaxValue : function(val) {
596 me.inputEl.dom.setAttribute('aria-valuemax', val);
598 for (; i < len; ++i) {
600 t.value = t.value > val ? val : t.value;
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
612 setValue : function(index, value, animate, changeComplete) {
614 thumb = me.thumbs[index];
616 // ensures value is contstrained and snapped
617 value = me.normalizeValue(value);
619 if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) {
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.
625 'aria-valuenow': value,
626 'aria-valuetext': value
629 thumb.move(me.translateValue(value), Ext.isDefined(animate) ? animate !== false : me.animate);
631 me.fireEvent('change', me, value, thumb);
632 if (changeComplete) {
633 me.fireEvent('changecomplete', me, value, thumb);
642 translateValue : function(v) {
643 var ratio = this.getRatio();
644 return (v * ratio) - (this.minValue * ratio) - this.halfThumb;
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)
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
655 reverseValue : function(pos) {
656 var ratio = this.getRatio();
657 return (pos + (this.minValue * ratio)) / ratio;
662 this.focusEl.focus(10);
666 onDisable: function() {
677 for (; i < len; i++) {
684 //IE breaks when using overflow visible and opacity other than 1.
685 //Create a place holder for the thumb and display it.
689 me.innerEl.addCls(me.disabledCls).dom.disabled = true;
691 if (!me.thumbHolder) {
692 me.thumbHolder = me.endEl.createChild({cls: Ext.baseCSSPrefix + 'slider-thumb ' + me.disabledCls});
695 me.thumbHolder.show().setXY(xy);
701 onEnable: function() {
711 for (; i < len; i++) {
718 me.innerEl.removeCls(me.disabledCls).dom.disabled = false;
720 if (me.thumbHolder) {
721 me.thumbHolder.hide();
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.
736 syncThumbs : function() {
738 var thumbs = this.thumbs,
739 length = thumbs.length,
742 for (; i < length; i++) {
743 thumbs[i].move(this.translateValue(thumbs[i].value));
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.
754 getValue : function(index) {
755 return Ext.isNumber(index) ? this.thumbs[index].value : this.getValues();
759 * Returns an array of values - one for the location of each thumb
760 * @return {Array} The set of thumb values
762 getValues: function() {
765 thumbs = this.thumbs,
768 for (; i < len; i++) {
769 values.push(thumbs[i].value);
775 getSubmitValue: function() {
777 return (me.disabled || !me.submitValue) ? null : me.getValue();
783 Array.forEach(Array.from(me.originalValue), function(val, i) {
787 // delete here so we reset back to the original state
792 beforeDestroy : function() {
795 Ext.destroyMembers(me.innerEl, me.endEl, me.focusEl);
796 Ext.each(me.thumbs, function(thumb) {
804 // Method overrides to support slider with vertical orientation
806 getRatio : function() {
807 var h = this.innerEl.getHeight(),
808 v = this.maxValue - this.minValue;
812 onClickChange : function(local) {
814 thumb, index, bottom;
816 if (local.left > me.clickRange[0] && local.left < me.clickRange[1]) {
817 thumb = me.getNearest(local, 'top');
818 if (!thumb.disabled) {
820 bottom = me.reverseValue(me.innerEl.getHeight() - local.top);
822 me.setValue(index, Ext.util.Format.round(me.minValue + bottom, me.decimalPrecision), undefined, true);