/** * Slider component used by Ext.field.Slider. */Ext.define('Ext.slider.Slider', { extend: 'Ext.Component', xtype: 'slider', requires: [ 'Ext.slider.Thumb', 'Ext.fx.easing.EaseOut' ], /** * @event change * Fires when the value changes * @param {Ext.slider.Slider} this * @param {Ext.slider.Thumb} thumb The thumb being changed * @param {Number} newValue The new value * @param {Number} oldValue The old value */ /** * @event dragstart * Fires when the slider thumb starts a drag * @param {Ext.slider.Slider} this * @param {Ext.slider.Thumb} thumb The thumb being dragged * @param {Array} value The start value * @param {Ext.EventObject} e */ /** * @event drag * Fires when the slider thumb starts a drag * @param {Ext.slider.Slider} this * @param {Ext.slider.Thumb} thumb The thumb being dragged * @param {Ext.EventObject} e */ /** * @event dragend * Fires when the slider thumb starts a drag * @param {Ext.slider.Slider} this * @param {Ext.slider.Thumb} thumb The thumb being dragged * @param {Array} value The end value * @param {Ext.EventObject} e */ config: { /** * @cfg {Object} thumbDefaults The config object to factory {@link Ext.slider.Thumb} instances * @accessor * @cmd-auto-dependency { defaultType: "Ext.slider.Thumb", aliasPrefix:'widget.',typeProperty: 'xtype' } */ thumbDefaults: { xtype: 'thumb', draggable: { translatable: { easingX: { duration: 300, type: 'ease-out' } } } }, /** * @cfg {Number} increment The increment by which to snap each thumb when its value changes. Any thumb movement * will be snapped to the nearest value that is a multiple of the increment (e.g. if increment is 10 and the user * tries to move the thumb to 67, it will be snapped to 70 instead) * @accessor */ increment: 1, /** * @cfg {Number/Number[]} value The value(s) of this slider's thumbs. If you pass * a number, it will assume you have just 1 thumb. * @accessor */ value: 0, /** * @cfg {Boolean} [valueIsArray=false] * If `false` the {@link #value} will be a number when a single value is given * (even if it's an array containing that single value), and an array, * when an array of values was given. * If `true`, the {@link #value} will always be converted to an array. */ valueIsArray: false, /** * @cfg {Number} minValue The lowest value any thumb on this slider can be set to. * @accessor */ minValue: 0, /** * @cfg {Number} maxValue The highest value any thumb on this slider can be set to. * @accessor */ maxValue: 100, /** * @cfg {Boolean} allowThumbsOverlapping Whether or not to allow multiple thumbs to overlap each other. * Setting this to true guarantees the ability to select every possible value in between {@link #minValue} * and {@link #maxValue} that satisfies {@link #increment} * @accessor */ allowThumbsOverlapping: false, /** * @cfg {Boolean/Object} animation * The animation to use when moving the slider. Possible properties are: * * - duration * - easingX * - easingY * * @accessor */ animation: true, /** * Will make this field read only, meaning it cannot be changed from the user interface. * @cfg {Boolean} readOnly * @accessor */ readOnly: false }, /** * @cfg {Number/Number[]} values Alias to {@link #value} */ classCls: Ext.baseCSSPrefix + 'slider', elementWidth: 0, offsetValueRatio: 0, activeThumb: null, isThumbAnimating: 0, template: [{ reference: 'trackElement', cls: Ext.baseCSSPrefix + 'track-el' }, { reference: 'thumbWrapElement', cls: Ext.baseCSSPrefix + 'thumb-wrap-el' }], fillSelector: '.' + Ext.baseCSSPrefix + 'fill-el', constructor: function(config) { config = config || {}; if (config.hasOwnProperty('values')) { config.value = config.values; } this.thumbs = []; this.callParent([config]); }, /** * @private */ initialize: function() { var element = this.element, thumb; this.callParent(); element.on({ scope: this, tap: 'onTap', resize: 'onResize' }); thumb = this.thumbs[0]; if (thumb) { thumb.on('resize', 'onThumbResize', this); } }, applyThumbDefaults: function(defaults) { defaults.slider = this; return defaults; }, /** * @private */ factoryThumb: function() { return Ext.factory(this.getThumbDefaults(), Ext.slider.Thumb); }, /** * Returns the Thumb instances bound to this Slider * @return {Ext.slider.Thumb[]} The thumb instances */ getThumbs: function() { return this.thumbs; }, /** * Returns the Thumb instance bound to this Slider * @param {Number} [index=0] The index of Thumb to return. * @return {Ext.slider.Thumb} The thumb instance */ getThumb: function(index) { if (typeof index !== 'number') { index = 0; } return this.thumbs[index]; }, refreshOffsetValueRatio: function() { var me = this, valueRange = me.getMaxValue() - me.getMinValue(), trackWidth = me.elementWidth - me.thumbWidth; me.offsetValueRatio = valueRange === 0 ? 0 : trackWidth / valueRange; }, onThumbResize: function(){ var thumb = this.thumbs[0]; if (thumb) { this.thumbWidth = thumb.getElementWidth(); } this.refresh(); }, onResize: function(element, info) { this.elementWidth = info.width; this.refresh(); }, refresh: function() { this.refreshing = true; this.refreshValue(); this.refreshing = false; }, setActiveThumb: function(thumb) { var oldActiveThumb = this.activeThumb; if (oldActiveThumb && oldActiveThumb !== thumb) { oldActiveThumb.setZIndex(null); } this.activeThumb = thumb; thumb.setZIndex(2); return this; }, onThumbBeforeDragStart: function(thumb, e) { if (this.offsetValueRatio === 0 || e.absDeltaX <= e.absDeltaY || this.getReadOnly()) { return false; } }, onThumbDragStart: function(thumb, e) { var me = this; me.refreshAllThumbConstraints(); e.claimGesture(); if (me.getAllowThumbsOverlapping()) { me.setActiveThumb(thumb); } me.dragStartValue = me.getArrayValues()[me.getThumbIndex(thumb)]; me.fireEvent('dragstart', me, thumb, me.dragStartValue, e); }, onThumbDrag: function(thumb, e, offsetX) { var me = this, index = me.getThumbIndex(thumb), offsetValueRatio = me.offsetValueRatio, constrainedValue = me.constrainValue(me.getMinValue() + offsetX / offsetValueRatio); e.stopPropagation(); me.setIndexValue(index, constrainedValue); me.fireEvent('drag', me, thumb, me.getArrayValues(), e); return false; }, setIndexValue: function(index, value, animation) { var me = this, thumb = me.thumbs[index], values = me.getArrayValues(), minValue = me.getMinValue(), offsetValueRatio = me.offsetValueRatio, increment = me.getIncrement(), draggable = thumb.getDraggable(); draggable.setOffset((value - minValue) * offsetValueRatio, null, animation); values[index] = minValue + Math.round((draggable.offset.x / offsetValueRatio) / increment) * increment; me.setValue(values); }, onThumbDragEnd: function(thumb, e) { var me = this, index = me.getThumbIndex(thumb), newValue = me.getArrayValues()[index], oldValue = me.dragStartValue; me.snapThumbPosition(thumb, newValue); me.fireEvent('dragend', me, thumb, me.getArrayValues(), e); if (oldValue !== newValue) { me.fireEvent('change', me, thumb, newValue, oldValue); } }, getThumbIndex: function(thumb) { return this.thumbs.indexOf(thumb); }, refreshThumbConstraints: function(thumb) { var me = this, allowThumbsOverlapping = me.getAllowThumbsOverlapping(), offsetX = thumb.getDraggable().getOffset().x, thumbs = me.thumbs, index = me.getThumbIndex(thumb), previousThumb = thumbs[index - 1], nextThumb = thumbs[index + 1], thumbWidth = me.thumbWidth; if (previousThumb) { previousThumb.getDraggable().addExtraConstraint({ max: { x: offsetX - ((allowThumbsOverlapping) ? 0 : thumbWidth) } }); } if (nextThumb) { nextThumb.getDraggable().addExtraConstraint({ min: { x: offsetX + ((allowThumbsOverlapping) ? 0 : thumbWidth) } }); } }, /** * @private */ onTap: function(e) { var me = this, element = me.element, minDistance = Infinity, i, absDistance, testValue, closestIndex, oldValue, thumb, ln, values, value, offset, elementX, targetElement, touchPointX; if (me.offsetValueRatio === 0 || me.isDisabled() || me.getReadOnly()) { return; } targetElement = Ext.get(e.target); if (!targetElement || (Ext.browser.engineName == 'WebKit' && targetElement.hasCls('x-thumb'))) { return; } touchPointX = e.touch.point.x; elementX = element.getX(); offset = touchPointX - elementX - (me.thumbWidth / 2); value = me.constrainValue(me.getMinValue() + offset / me.offsetValueRatio); values = me.getArrayValues(); ln = values.length; if (ln === 1) { closestIndex = 0; } else { for (i = 0; i < ln; i++) { testValue = values[i]; absDistance = Math.abs(testValue - value); if (absDistance < minDistance) { minDistance = absDistance; closestIndex = i; } } } oldValue = values[closestIndex]; thumb = me.thumbs[closestIndex]; me.setIndexValue(closestIndex, value, me.getAnimation()); me.refreshThumbConstraints(thumb); if (oldValue !== value) { me.fireEvent('change', me, thumb, value, oldValue); } }, /** * @private */ updateThumbs: function(newThumbs) { this.add(newThumbs); }, applyValue: function(value, oldValue) { var me = this, values = Ext.Array.from(value || 0), valueIsArray = me.getValueIsArray(), filteredValues = [], previousFilteredValue = me.getMinValue(), filteredValue, i, ln, result; for (i = 0,ln = values.length; i < ln; i++) { filteredValue = me.constrainValue(values[i]); if (filteredValue < previousFilteredValue) { //<debug> Ext.Logger.warn("Invalid values of '"+Ext.encode(values)+"', values at smaller indexes must " + "be smaller than or equal to values at greater indexes"); //</debug> filteredValue = previousFilteredValue; } filteredValues.push(filteredValue); previousFilteredValue = filteredValue; } if (!me.refreshing && oldValue) { if (Ext.Array.equals(values, oldValue)) { filteredValues = undefined; } } if (filteredValues) { me.values = filteredValues; } if (valueIsArray) { result = filteredValues; } else { if (value && value.length === 1) { result = value[0]; } else { result = value; } } return result; }, /** * Updates the sliders thumbs with their new value(s) */ updateValue: function() { var me = this, thumbs = me.thumbs, values = me.getArrayValues(), len = values.length, i; me.setThumbsCount(len); if (!this.isThumbAnimating) { for (i = 0; i < len; i++) { me.snapThumbPosition(thumbs[i], values[i]); } } }, /** * @private */ refreshValue: function() { this.refreshOffsetValueRatio(); this.updateValue(); }, /** * @private * Takes a desired value of a thumb and returns the nearest snap value. e.g if minValue = 0, maxValue = 100, increment = 10 and we * pass a value of 67 here, the returned value will be 70. The returned number is constrained within {@link #minValue} and {@link #maxValue}, * so in the above example 68 would be returned if {@link #maxValue} was set to 68. * @param {Number} value The value to snap * @return {Number} The snapped value */ constrainValue: function(value) { var me = this, minValue = me.getMinValue(), maxValue = me.getMaxValue(), increment = me.getIncrement(), remainder; value = parseFloat(value); if (isNaN(value)) { value = minValue; } remainder = (value - minValue) % increment; value -= remainder; if (Math.abs(remainder) >= (increment / 2)) { value += (remainder > 0) ? increment : -increment; } value = Math.max(minValue, value); value = Math.min(maxValue, value); return value; }, setThumbsCount: function(count) { var me = this, thumbs = me.thumbs, thumbsCount = thumbs.length, i, ln, thumb; if (thumbsCount > count) { for (i = 0,ln = thumbsCount - count; i < ln; i++) { thumb = thumbs[thumbs.length - 1]; thumb.destroy(); Ext.Array.remove(thumbs, thumb); } } else if (thumbsCount < count) { for (i = thumbsCount, ln = count; i < ln; i++) { thumb = me.factoryThumb(); thumbs.push(thumb); me.trackElement.appendChild(thumb.fillElement); me.thumbWrapElement.appendChild(thumb.element); me.element.appendChild(thumb.sizerElement); } } if (thumbsCount !== count) { for (i = 0; i < count; i++) { // Default fill behavior is as follows: // - if only one thumb, fill is on // - if 2 thumbs, fill is off for first thumb, on for 2nd thumb // - 3 or more thumbs - no fill // TODO: allow the user to configure thumbs in initial slider config thumb = thumbs[i]; if (count > 2) { thumb.setFillTrack(false); } else if (count === 2) { thumb.setFillTrack(i === 1); } else { thumb.setFillTrack(true); } } } return this; }, /** * Convenience method. Calls {@link #setValue}. */ setValues: function(value) { this.setValue(value); }, /** * Convenience method. Calls {@link #getValue}. * @return {Object} */ getValues: function() { return this.getValue(); }, /** * @private */ getArrayValues: function () { return this.values; }, /** * Sets the {@link #increment} configuration. * @param {Number} increment * @return {Number} */ applyIncrement: function(increment) { if (increment === 0) { increment = 1; } return Math.abs(increment); }, /** * @private */ updateAllowThumbsOverlapping: function(newValue, oldValue) { if (typeof oldValue != 'undefined') { this.refreshValue(); } }, /** * @private */ updateMinValue: function(newValue, oldValue) { if (typeof oldValue != 'undefined') { this.refreshValue(); } }, /** * @private */ updateMaxValue: function(newValue, oldValue) { if (typeof oldValue != 'undefined') { this.refreshValue(); } }, /** * @private */ updateIncrement: function(newValue, oldValue) { if (typeof oldValue != 'undefined') { this.refreshValue(); } }, updateDisabled: function(disabled) { this.callParent(arguments); var thumbs = this.thumbs, ln = thumbs.length, i; for (i = 0; i < ln; i++) { thumbs[i].setDisabled(disabled); } }, doDestroy: function() { this.thumbs = Ext.destroy(this.thumbs); this.callParent(); }, privates: { refreshAllThumbConstraints: function() { var thumbs = this.thumbs, len = thumbs.length, i; for (i = 0; i < len; i++) { this.refreshThumbConstraints(thumbs[i]); } }, snapThumbPosition: function(thumb, value) { var ratio = this.offsetValueRatio, draggable = thumb.getDraggable(), offset; if (isFinite(ratio)) { offset = Ext.Number.correctFloat((value - this.getMinValue()) * ratio); draggable.refreshContainerSize().setExtraConstraint(null).setOffset(offset); } }, syncFill: function(thumb, offset) { var me = this, thumbs = me.thumbs, values = me.getArrayValues(), ln = values.length, prevOffset = 0, fillElements = me.trackElement.query(me.fillSelector, false), thumbIndex = thumbs.indexOf(thumb), thumbOffset, fillElement, i; offset = offset + Math.ceil(thumb.element.getWidth() / 2); for (i = 0; i < ln; i++) { thumb = thumbs[i]; fillElement = fillElements[i]; // during animation offset may be different from what the draggable reports thumbOffset = (i === thumbIndex) ? offset : thumb.getDraggable().getOffset().x + (thumb.element.getWidth() / 2); fillElement.setWidth(thumbOffset - prevOffset); fillElement.setLocalX(prevOffset); prevOffset = thumbOffset; } }, onThumbAnimationStart: function() { this.isThumbAnimating++; }, onThumbAnimationEnd: function() { this.isThumbAnimating--; } }});