/**
* 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({
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();
}
});