/** * A Widget-based implementation of a slider. * @since 5.0.0 */Ext.define('Ext.slider.Widget', { extend: 'Ext.Widget', alias: 'widget.sliderwidget', // Required to pull in the styles requires: [ 'Ext.slider.Multi' ], cachedConfig: { /** * @cfg {Boolean} vertical * Orients the slider vertically rather than horizontally. */ vertical: false }, config: { /** * @cfg {Boolean} clickToChange * Determines whether or not clicking on the Slider axis will change the slider. */ clickToChange: true, ui: 'widget', /** * @cfg {Number/Number[]} value * One more values for the position of the slider's thumb(s). */ value: 0, /** * @cfg {Number} minValue * The minimum value for any slider thumb. */ minValue: 0, /** * @cfg {Number} maxValue * The maximum value for any slider thumb. */ maxValue: 100, /** * @cfg {Boolean} [publishOnComplete=true] * This controls when the value of the slider is published to the `ViewModel`. By * default this is done only when the thumb is released (the change is complete). To * cause this to happen on every change of the thumb position, specify `false`. This * setting is `true` by default for improved performance on slower devices (such as * older browsers or tablets). */ publishOnComplete: true, /** * @cfg {Object} twoWayBindable * This object is a map of config property names holding a `true` if changes to * that config should written back to its binding. Most commonly this is used to * indicate that the `value` config should be monitored and changes written back * to the bound value. */ twoWayBindable: { value: 1 } }, decimalPrecision: 0, defaultBindProperty: 'value', element: { reference: 'element', cls: Ext.baseCSSPrefix + 'slider', listeners: { mousedown: 'onMouseDown', dragstart: 'cancelDrag', drag: 'cancelDrag', dragend: 'cancelDrag' }, children: [{ reference: 'endEl', cls: Ext.baseCSSPrefix + 'slider-end', children: [{ reference: 'innerEl', cls: Ext.baseCSSPrefix + 'slider-inner' }] }] }, thumbCls: Ext.baseCSSPrefix + 'slider-thumb', horizontalProp: 'left', // This property is set to false onMouseDown and deleted onMouseUp. It is used only // by applyValue when it passes the animate parameter to setThumbValue. animateOnSetValue: undefined, applyValue: function(value) { var me = this, animate = me.animateOnSetValue, i, len; if (Ext.isArray(value)) { value = Ext.Array.from(value); for (i = 0, len = value.length; i < len; ++i) { me.setThumbValue(i, value[i] = me.normalizeValue(value[i]), animate, true); } } else { value = me.normalizeValue(value); me.setThumbValue(0, value, animate, true); } return value; }, updateVertical: function(vertical, oldVertical) { this.element.removeCls(Ext.baseCSSPrefix + 'slider-' + (oldVertical ? 'vert' : 'horz')); this.element.addCls( Ext.baseCSSPrefix + 'slider-' + (vertical ? 'vert' : 'horz')); }, updateHeight: function(height, oldHeight) { this.callParent([height, oldHeight]); this.endEl.dom.style.height = this.innerEl.dom.style.height = '100%'; }, cancelDrag: function(e) { // prevent the touch scroller from scrolling when the slider is being dragged e.stopPropagation(); }, getThumb: function(ordinal) { var me = this, thumbConfig, result = (me.thumbs || (me.thumbs = []))[ordinal], panDisable = me.getVertical() ? 'panY' : 'panX', touchAction = {}; if (!result) { thumbConfig = { cls: me.thumbCls, style: {} }; thumbConfig['data-thumbIndex'] = ordinal; result = me.thumbs[ordinal] = me.innerEl.createChild(thumbConfig); touchAction[panDisable] = false; result.setTouchAction(touchAction); } return result; }, getThumbPositionStyle: function() { return this.getVertical() ? 'bottom' : this.horizontalProp; }, // // TODO: RTL // getRenderTree: function() { // var me = this, // rtl = me.rtl; // // if (rtl && Ext.rtl) { // me.baseCls += ' ' + (Ext.rtl.util.Renderable.prototype._rtlCls); // me.horizontalProp = 'right'; // } else if (rtl === false) { // me.addCls(Ext.rtl.util.Renderable.prototype._ltrCls); // } // // return me.callParent(); // }, update: function() { var me = this, values = me.getValues(), len = values.length, i; for (i = 0; i < len; i++) { this.thumbs[i].dom.style[me.getThumbPositionStyle()] = me.calculateThumbPosition(values[i]) + '%'; } }, updateMaxValue: function (maxValue) { this.onRangeAdjustment(maxValue, 'min'); }, updateMinValue: function (minValue) { this.onRangeAdjustment(minValue, 'max'); }, /** * @private * Conditionally updates value of slider when minValue or maxValue are updated * @param {Number} rangeValue The new min or max value * @param {String} compareType The comparison type (e.g., min/max) */ onRangeAdjustment: function (rangeValue, compareType) { var value = this._value, newValue; if (!isNaN(value)) { newValue = Math[compareType](value, rangeValue); } if (newValue !== undefined) { this.setValue(newValue); } this.update(); }, onMouseDown: function(e) { var me = this, thumb, trackPoint = e.getXY(), delta; if (!me.disabled && e.button === 0) { // Stop any selection caused by mousedown + mousemove Ext.getDoc().on({ scope: me, capture: true, selectstart: me.stopSelect }); thumb = e.getTarget('.' + me.thumbCls, null, true); if (thumb) { me.animateOnSetValue = false; me.promoteThumb(thumb); me.captureMouse(me.onMouseMove, me.onMouseUp, [thumb], 1); delta = me.pointerOffset = thumb.getXY(); // Work out the delta of the pointer from the dead centre of the thumb. // Slider.getTrackPoint positions the centre of the slider at the reported // pointer position, so we have to correct for that in getValueFromTracker. delta[0] += Math.floor(thumb.getWidth() / 2) - trackPoint[0]; delta[1] += Math.floor(thumb.getHeight() / 2) - trackPoint[1]; } else { if (me.getClickToChange()) { trackPoint = me.getTrackpoint(trackPoint); if (trackPoint != null) { me.onClickChange(trackPoint); } } } } }, /** * @private * Moves the thumb to the indicated position. * Only changes the value if the click was within this.clickRange. * @param {Number} trackPoint local pixel offset **from the origin** (left for horizontal and bottom for vertical) along the Slider's axis at which the click event occured. */ onClickChange: function(trackPoint) { var me = this, thumb, index, value; // How far along the track *from the origin* was the click. // If vertical, the origin is the bottom of the slider track. //find the nearest thumb to the click event thumb = me.getNearest(trackPoint); index = parseInt(thumb.getAttribute('data-thumbIndex'), 10); value = Ext.util.Format.round(me.reversePixelValue(trackPoint), me.decimalPrecision); if (index) { me.setThumbValue(index, value, undefined, true); } else { me.setValue(value); } }, /** * @private * Returns the nearest thumb to a click event, along with its distance * @param {Number} trackPoint local pixel position along the Slider's axis to find the Thumb for * @return {Object} The closest thumb object and its distance from the click event */ getNearest: function(trackPoint) { var me = this, clickValue = me.reversePixelValue(trackPoint), nearestDistance = me.getRange() + 5, //add a small fudge for the end of the slider nearest = null, thumbs = me.thumbs, i = 0, len = thumbs.length, thumb, value, dist; for (; i < len; i++) { thumb = thumbs[i]; value = thumb.value; dist = Math.abs(value - clickValue); if (Math.abs(dist) <= nearestDistance) { nearest = thumb; nearestDistance = dist; } } return nearest; }, /** * @private * Moves the given thumb above all other by increasing its z-index. This is called when as drag * any thumb, so that the thumb that was just dragged is always at the highest z-index. This is * required when the thumbs are stacked on top of each other at one of the ends of the slider's * range, which can result in the user not being able to move any of them. * @param {Ext.slider.Thumb} topThumb The thumb to move to the top */ promoteThumb: function(topThumb) { var thumbs = this.thumbStack || (this.thumbStack = Ext.Array.slice(this.thumbs)), ln = thumbs.length, zIndex = 10000, i; // Move topthumb to position zero if (thumbs[0] !== topThumb) { Ext.Array.remove(thumbs, topThumb); thumbs.unshift(topThumb); } // Then shuffle the zIndices for (i = 0; i < ln; i++) { thumbs[i].el.setStyle('zIndex', zIndex); zIndex -= 1000; } }, doMouseMove: function (e, thumb, changeComplete) { var me = this, trackerXY = e.getXY(), newValue, thumbIndex, trackPoint; trackerXY[0] += me.pointerOffset[0]; trackerXY[1] += me.pointerOffset[1]; trackPoint = me.getTrackpoint(trackerXY); // If dragged out of range, value will be undefined if (trackPoint != null) { newValue = Ext.util.Format.round(me.reversePixelValue(trackPoint), me.decimalPrecision); thumbIndex = parseInt(thumb.getAttribute('data-thumbIndex'), 10); if (thumbIndex || (!changeComplete && me.getPublishOnComplete())) { me.setThumbValue(thumbIndex, newValue, false, changeComplete); } else { me.setValue(newValue); } } }, onMouseMove: function(e, thumb) { this.doMouseMove(e, thumb, false); }, onMouseUp: function(e, thumb) { var me = this; me.doMouseMove(e, thumb, true); Ext.getDoc().un({ scope: me, capture: true, selectstart: me.stopSelect }); delete me.animateOnSetValue; // expose "undefined" on prototype }, stopSelect: function(e) { e.stopEvent(); return false; }, /** * Programmatically sets the value of the Slider. Ensures that the value is constrained within the minValue and * maxValue. * * Setting a single value: * // Set the second slider value, don't animate * mySlider.setThumbValue(1, 50, false); * * Setting multiple values at once * // Set 3 thumb values, animate * mySlider.setThumbValue([20, 40, 60], true); * * @param {Number/Number[]} index Index of the thumb to move. Alternatively, it can be an array of values to set * for each thumb in the slider. * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue) * @param {Boolean} [animate=true] Turn on or off animation * @param {Boolean} changeComplete * @return {Ext.slider.Multi} this */ setThumbValue: function(index, value, animate, changeComplete) { var me = this, thumb, len, i, values; if (Ext.isArray(index)) { values = index; animate = value; for (i = 0, len = values.length; i < len; ++i) { me.setThumbValue(i, values[i], animate, changeComplete); } return me; } thumb = me.getThumb(index); // ensures value is contstrained and snapped value = me.normalizeValue(value); if (value !== thumb.value && me.fireEvent('beforechange', me, value, thumb.value, thumb) !== false) { thumb.value = value; if (me.element.dom) { // TODO this only handles a single value; need a solution for exposing multiple values to aria. // Perhaps this should go on each thumb element rather than the outer element. me.element.set({ 'aria-valuenow': value, 'aria-valuetext': value }); me.moveThumb(thumb, me.calculateThumbPosition(value), Ext.isDefined(animate) ? animate !== false : me.animate); me.fireEvent('change', me, value, thumb); } } return me; }, /** * Returns the current value of the slider * @param {Number} index The index of the thumb to return a value for * @return {Number/Number[]} The current value of the slider at the given index, or an array of all thumb values if * no index is given. */ getValue: function(index) { var me = this, value; if (Ext.isNumber(index)) { value = me.thumbs[index].value; } else { value = me.getValues(); if (value.length === 1) { value = value[0]; } } return value; }, /** * Returns an array of values - one for the location of each thumb * @return {Number[]} The set of thumb values */ getValues: function() { var me = this, values = [], i = 0, thumbs = me.thumbs, len = thumbs && thumbs.length; for (; i < len; i++) { values.push(me.thumbs[i].value); } return values; }, /** * @private * move the thumb */ moveThumb: function(thumb, v, animate) { var me = this, styleProp = me.getThumbPositionStyle(), to, from; v += '%'; if (!animate) { thumb.dom.style[styleProp] = v; } else { to = {}; to[styleProp] = v; if (!Ext.supports.GetPositionPercentage) { from = {}; from[styleProp] = thumb.dom.style[styleProp]; } new Ext.fx.Anim({ // jshint ignore:line target: thumb, duration: 350, from: from, to: to }); } }, /** * @private * Returns a snapped, constrained value when given a desired value * @param {Number} v Raw number value * @return {Number} The raw value rounded to the correct d.p. and constrained within the set max and min values */ normalizeValue: function(v) { var me = this, snapFn = me.zeroBasedSnapping ? 'snap' : 'snapInRange'; v = Ext.Number[snapFn](v, me.increment, me.minValue, me.maxValue); v = Ext.util.Format.round(v, me.decimalPrecision); v = Ext.Number.constrain(v, me.minValue, me.maxValue); return v; }, /** * @private * Given an `[x, y]` position within the slider's track (Points outside the slider's track are coerced to either the minimum or maximum value), * calculate how many pixels **from the slider origin** (left for horizontal Sliders and bottom for vertical Sliders) that point is. * * If the point is outside the range of the Slider's track, the return value is `undefined` * @param {Number[]} xy The point to calculate the track point for */ getTrackpoint: function(xy) { var me = this, vertical = me.getVertical(), sliderTrack = me.innerEl, trackLength, result, positionProperty; if (vertical) { positionProperty = 'top'; trackLength = sliderTrack.getHeight(); } else { positionProperty = me.horizontalProp; trackLength = sliderTrack.getWidth(); } xy = me.transformTrackPoints(sliderTrack.translatePoints(xy)); result = Ext.Number.constrain(xy[positionProperty], 0, trackLength); return vertical ? trackLength - result : result; }, transformTrackPoints: Ext.identityFn, /** * @private * Given a value within this Slider's range, calculates a Thumb's percentage CSS position to map that value. */ calculateThumbPosition: function(v) { var me = this, pos = (v - me.getMinValue()) / me.getRange() * 100; if (isNaN(pos)) { pos = 0; } return pos; }, /** * @private * Returns the ratio of pixels to mapped values. e.g. if the slider is 200px wide and maxValue - minValue is 100, * the ratio is 2 * @return {Number} The ratio of pixels to mapped values */ getRatio: function() { var me = this, innerEl = me.innerEl, trackLength = me.getVertical() ? innerEl.getHeight() : innerEl.getWidth(), valueRange = me.getRange(); return valueRange === 0 ? trackLength : (trackLength / valueRange); }, getRange: function() { return this.getMaxValue() - this.getMinValue(); }, /** * @private * Given a pixel location along the slider, returns the mapped slider value for that pixel. * E.g. if we have a slider 200px wide with minValue = 100 and maxValue = 500, reversePixelValue(50) * returns 200 * @param {Number} pos The position along the slider to return a mapped value for * @return {Number} The mapped value for the given position */ reversePixelValue: function(pos) { return this.getMinValue() + (pos / this.getRatio()); }, captureMouse: function(onMouseMove, onMouseUp, args, appendArgs) { var me = this, onMouseupWrap, listeners; onMouseMove = onMouseMove && Ext.Function.bind(onMouseMove, me, args, appendArgs); onMouseUp = onMouseUp && Ext.Function.bind(onMouseUp, me, args, appendArgs); onMouseupWrap = function() { Ext.getDoc().un(listeners); if (onMouseUp) { onMouseUp.apply(me, arguments); } }; listeners = { mousemove: onMouseMove, mouseup: onMouseupWrap }; // Funnel mousemove events and the final mouseup event back into the gadget Ext.getDoc().on(listeners); }, doDestroy: function() { Ext.destroy(this.thumbs); this.callParent(); }});