X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/2e847cf21b8ab9d15fa167b315ca5b2fa92638fc..6a7e4474cba9d8be4b2ec445e10f1691f7277c50:/src/widgets/Slider.js diff --git a/src/widgets/Slider.js b/src/widgets/Slider.js index 252fcd3e..57a0ee8c 100644 --- a/src/widgets/Slider.js +++ b/src/widgets/Slider.js @@ -1,461 +1,937 @@ /*! - * Ext JS Library 3.1.1 - * Copyright(c) 2006-2010 Ext JS, LLC + * Ext JS Library 3.2.0 + * Copyright(c) 2006-2010 Ext JS, Inc. * licensing@extjs.com * http://www.extjs.com/license */ -/** - * @class Ext.Slider - * @extends Ext.BoxComponent - * Slider which supports vertical or horizontal orientation, keyboard adjustments, - * configurable snapping, axis clicking and animation. Can be added as an item to - * any container. Example usage: -

-new Ext.Slider({
-    renderTo: Ext.getBody(),
-    width: 200,
-    value: 50,
-    increment: 10,
-    minValue: 0,
-    maxValue: 100
-});
-
- */ -Ext.Slider = Ext.extend(Ext.BoxComponent, { - /** - * @cfg {Number} value The value to initialize the slider with. Defaults to minValue. - */ - /** - * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false. - */ - vertical: false, - /** - * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0. - */ - minValue: 0, - /** - * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100. - */ - maxValue: 100, - /** - * @cfg {Number/Boolean} decimalPrecision. - *

The number of decimal places to which to round the Slider's value. Defaults to 0.

- *

To disable rounding, configure as false.

- */ - decimalPrecision: 0, - /** - * @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. - */ - keyIncrement: 1, - /** - * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'. - */ - increment: 0, - // private - clickRange: [5,15], - /** - * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true - */ - clickToChange : true, - /** - * @cfg {Boolean} animate Turn on or off animation. Defaults to true - */ - animate: true, - - /** - * True while the thumb is in a drag operation - * @type boolean - */ - dragging: false, - - // private override - initComponent : function(){ - if(!Ext.isDefined(this.value)){ - this.value = this.minValue; - } - Ext.Slider.superclass.initComponent.call(this); - this.keyIncrement = Math.max(this.increment, this.keyIncrement); - this.addEvents( - /** - * @event beforechange - * Fires before the slider value is changed. By returning false from an event handler, - * you can cancel the event and prevent the slider from changing. - * @param {Ext.Slider} slider The slider - * @param {Number} newValue The new value which the slider is being changed to. - * @param {Number} oldValue The old value which the slider was previously. - */ - 'beforechange', - /** - * @event change - * Fires when the slider value is changed. - * @param {Ext.Slider} slider The slider - * @param {Number} newValue The new value which the slider has been changed to. - */ - 'change', - /** - * @event changecomplete - * Fires when the slider value is changed by the user and any drag operations have completed. - * @param {Ext.Slider} slider The slider - * @param {Number} newValue The new value which the slider has been changed to. - */ - 'changecomplete', - /** - * @event dragstart - * Fires after a drag operation has started. - * @param {Ext.Slider} slider The slider - * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker - */ - 'dragstart', - /** - * @event drag - * Fires continuously during the drag operation while the mouse is moving. - * @param {Ext.Slider} slider The slider - * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker - */ - 'drag', - /** - * @event dragend - * Fires after the drag operation has completed. - * @param {Ext.Slider} slider The slider - * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker - */ - 'dragend' - ); - - if(this.vertical){ - Ext.apply(this, Ext.Slider.Vertical); - } - }, - - // private override - onRender : function(){ - this.autoEl = { - cls: 'x-slider ' + (this.vertical ? 'x-slider-vert' : 'x-slider-horz'), - cn:{cls:'x-slider-end',cn:{cls:'x-slider-inner',cn:[{cls:'x-slider-thumb'},{tag:'a', cls:'x-slider-focus', href:"#", tabIndex: '-1', hidefocus:'on'}]}} - }; - Ext.Slider.superclass.onRender.apply(this, arguments); - this.endEl = this.el.first(); - this.innerEl = this.endEl.first(); - this.thumb = this.innerEl.first(); - this.halfThumb = (this.vertical ? this.thumb.getHeight() : this.thumb.getWidth())/2; - this.focusEl = this.thumb.next(); - this.initEvents(); - }, - - // private override - initEvents : function(){ - this.thumb.addClassOnOver('x-slider-thumb-over'); - this.mon(this.el, { - scope: this, - mousedown: this.onMouseDown, - keydown: this.onKeyDown - }); - - this.focusEl.swallowEvent("click", true); - - this.tracker = new Ext.dd.DragTracker({ - onBeforeStart: this.onBeforeDragStart.createDelegate(this), - onStart: this.onDragStart.createDelegate(this), - onDrag: this.onDrag.createDelegate(this), - onEnd: this.onDragEnd.createDelegate(this), - tolerance: 3, - autoStart: 300 - }); - this.tracker.initEl(this.thumb); - }, - - // private override - onMouseDown : function(e){ - if(this.disabled){ - return; - } - if(this.clickToChange && e.target != this.thumb.dom){ - var local = this.innerEl.translatePoints(e.getXY()); - this.onClickChange(local); - } - this.focus(); - }, - - // private - onClickChange : function(local){ - if(local.top > this.clickRange[0] && local.top < this.clickRange[1]){ - this.setValue(Ext.util.Format.round(this.reverseValue(local.left), this.decimalPrecision), undefined, true); - } - }, - - // private - onKeyDown : function(e){ - if(this.disabled){e.preventDefault();return;} - var k = e.getKey(); - switch(k){ - case e.UP: - case e.RIGHT: - e.stopEvent(); - if(e.ctrlKey){ - this.setValue(this.maxValue, undefined, true); - }else{ - this.setValue(this.value+this.keyIncrement, undefined, true); - } - break; - case e.DOWN: - case e.LEFT: - e.stopEvent(); - if(e.ctrlKey){ - this.setValue(this.minValue, undefined, true); - }else{ - this.setValue(this.value-this.keyIncrement, undefined, true); - } - break; - default: - e.preventDefault(); - } - }, - - // private - doSnap : function(value){ - if(!(this.increment && value)){ - return value; - } - var newValue = value, - inc = this.increment, - m = value % inc; - if(m != 0){ - newValue -= m; - if(m * 2 > inc){ - newValue += inc; - }else if(m * 2 < -inc){ - newValue -= inc; - } - } - return newValue.constrain(this.minValue, this.maxValue); - }, - - // private - afterRender : function(){ - Ext.Slider.superclass.afterRender.apply(this, arguments); - if(this.value !== undefined){ - var v = this.normalizeValue(this.value); - if(v !== this.value){ - delete this.value; - this.setValue(v, false); - }else{ - this.moveThumb(this.translateValue(v), false); - } - } - }, - - // private - getRatio : function(){ - var w = this.innerEl.getWidth(), - v = this.maxValue - this.minValue; - return v == 0 ? w : (w/v); - }, - - // private - normalizeValue : function(v){ - v = this.doSnap(v); - v = Ext.util.Format.round(v, this.decimalPrecision); - v = v.constrain(this.minValue, this.maxValue); - return v; - }, - - /** - * Sets the minimum value for the slider instance. If the current value is less than the - * minimum value, the current value will be changed. - * @param {Number} val The new minimum value - */ - setMinValue : function(val){ - this.minValue = val; - this.syncThumb(); - if(this.value < val){ - this.setValue(val); - } - }, - - /** - * Sets the maximum value for the slider instance. If the current value is more than the - * maximum value, the current value will be changed. - * @param {Number} val The new maximum value - */ - setMaxValue : function(val){ - this.maxValue = val; - this.syncThumb(); - if(this.value > val){ - this.setValue(val); - } - }, - - /** - * Programmatically sets the value of the Slider. Ensures that the value is constrained within - * the minValue and maxValue. - * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue) - * @param {Boolean} animate Turn on or off animation, defaults to true - */ - setValue : function(v, animate, changeComplete){ - v = this.normalizeValue(v); - if(v !== this.value && this.fireEvent('beforechange', this, v, this.value) !== false){ - this.value = v; - this.moveThumb(this.translateValue(v), animate !== false); - this.fireEvent('change', this, v); - if(changeComplete){ - this.fireEvent('changecomplete', this, v); - } - } - }, - - // private - translateValue : function(v){ - var ratio = this.getRatio(); - return (v * ratio) - (this.minValue * ratio) - this.halfThumb; - }, - - reverseValue : function(pos){ - var ratio = this.getRatio(); - return (pos + (this.minValue * ratio)) / ratio; - }, - - // private - moveThumb: function(v, animate){ - if(!animate || this.animate === false){ - this.thumb.setLeft(v); - }else{ - this.thumb.shift({left: v, stopFx: true, duration:.35}); - } - }, - - // private - focus : function(){ - this.focusEl.focus(10); - }, - - // private - onBeforeDragStart : function(e){ - return !this.disabled; - }, - - // private - onDragStart: function(e){ - this.thumb.addClass('x-slider-thumb-drag'); - this.dragging = true; - this.dragStartValue = this.value; - this.fireEvent('dragstart', this, e); - }, - - // private - onDrag: function(e){ - var pos = this.innerEl.translatePoints(this.tracker.getXY()); - this.setValue(Ext.util.Format.round(this.reverseValue(pos.left), this.decimalPrecision), false); - this.fireEvent('drag', this, e); - }, - - // private - onDragEnd: function(e){ - this.thumb.removeClass('x-slider-thumb-drag'); - this.dragging = false; - this.fireEvent('dragend', this, e); - if(this.dragStartValue != this.value){ - this.fireEvent('changecomplete', this, this.value); - } - }, - - // private - onResize : function(w, h){ - this.innerEl.setWidth(w - (this.el.getPadding('l') + this.endEl.getPadding('r'))); - this.syncThumb(); - Ext.Slider.superclass.onResize.apply(this, arguments); - }, - - //private - onDisable: function(){ - Ext.Slider.superclass.onDisable.call(this); - this.thumb.addClass(this.disabledClass); - if(Ext.isIE){ - //IE breaks when using overflow visible and opacity other than 1. - //Create a place holder for the thumb and display it. - var xy = this.thumb.getXY(); - this.thumb.hide(); - this.innerEl.addClass(this.disabledClass).dom.disabled = true; - if (!this.thumbHolder){ - this.thumbHolder = this.endEl.createChild({cls: 'x-slider-thumb ' + this.disabledClass}); - } - this.thumbHolder.show().setXY(xy); - } - }, - - //private - onEnable: function(){ - Ext.Slider.superclass.onEnable.call(this); - this.thumb.removeClass(this.disabledClass); - if(Ext.isIE){ - this.innerEl.removeClass(this.disabledClass).dom.disabled = false; - if(this.thumbHolder){ - this.thumbHolder.hide(); - } - this.thumb.show(); - this.syncThumb(); - } - }, - - /** - * Synchronizes the thumb position to the proper proportion of the total component width based - * on the current slider {@link #value}. This will be called automatically when the Slider - * is resized by a layout, but if it is rendered auto width, this method can be called from - * another resize handler to sync the Slider if necessary. - */ - syncThumb : function(){ - if(this.rendered){ - this.moveThumb(this.translateValue(this.value)); - } - }, - - /** - * Returns the current value of the slider - * @return {Number} The current value of the slider - */ - getValue : function(){ - return this.value; - }, - - // private - beforeDestroy : function(){ - Ext.destroyMembers(this, 'endEl', 'innerEl', 'thumb', 'halfThumb', 'focusEl', 'tracker', 'thumbHolder'); - Ext.Slider.superclass.beforeDestroy.call(this); - } -}); -Ext.reg('slider', Ext.Slider); - -// private class to support vertical sliders -Ext.Slider.Vertical = { - onResize : function(w, h){ - this.innerEl.setHeight(h - (this.el.getPadding('t') + this.endEl.getPadding('b'))); - this.syncThumb(); - }, - - getRatio : function(){ - var h = this.innerEl.getHeight(), - v = this.maxValue - this.minValue; - return h/v; - }, - - moveThumb: function(v, animate){ - if(!animate || this.animate === false){ - this.thumb.setBottom(v); - }else{ - this.thumb.shift({bottom: v, stopFx: true, duration:.35}); - } - }, - - onDrag: function(e){ - var pos = this.innerEl.translatePoints(this.tracker.getXY()), - bottom = this.innerEl.getHeight()-pos.top; - this.setValue(this.minValue + Ext.util.Format.round(bottom/this.getRatio(), this.decimalPrecision), false); - this.fireEvent('drag', this, e); - }, - - onClickChange : function(local){ - if(local.left > this.clickRange[0] && local.left < this.clickRange[1]){ - var bottom = this.innerEl.getHeight() - local.top; - this.setValue(this.minValue + Ext.util.Format.round(bottom/this.getRatio(), this.decimalPrecision), undefined, true); - } - } -}; \ No newline at end of file +Ext.ns('Ext.slider'); + +/** + * @class Ext.slider.Thumb + * @extends Object + * Represents a single thumb element on a Slider. This would not usually be created manually and would instead + * be created internally by an {@link Ext.slider.MultiSlider Ext.Slider}. + */ +Ext.slider.Thumb = Ext.extend(Object, { + + /** + * @constructor + * @cfg {Ext.slider.MultiSlider} slider The Slider to render to (required) + */ + constructor: function(config) { + /** + * @property slider + * @type Ext.slider.MultiSlider + * The slider this thumb is contained within + */ + Ext.apply(this, config || {}, { + cls: 'x-slider-thumb', + + /** + * @cfg {Boolean} constrain True to constrain the thumb so that it cannot overlap its siblings + */ + constrain: false + }); + + Ext.slider.Thumb.superclass.constructor.call(this, config); + + if (this.slider.vertical) { + Ext.apply(this, Ext.slider.Thumb.Vertical); + } + }, + + /** + * Renders the thumb into a slider + */ + render: function() { + this.el = this.slider.innerEl.insertFirst({cls: this.cls}); + + this.initEvents(); + }, + + /** + * Enables the thumb if it is currently disabled + */ + enable: function() { + this.disabled = false; + this.el.removeClass(this.slider.disabledClass); + }, + + /** + * Disables the thumb if it is currently enabled + */ + disable: function() { + this.disabled = true; + this.el.addClass(this.slider.disabledClass); + }, + + /** + * Sets up an Ext.dd.DragTracker for this thumb + */ + initEvents: function() { + var el = this.el; + + el.addClassOnOver('x-slider-thumb-over'); + + this.tracker = new Ext.dd.DragTracker({ + onBeforeStart: this.onBeforeDragStart.createDelegate(this), + onStart : this.onDragStart.createDelegate(this), + onDrag : this.onDrag.createDelegate(this), + onEnd : this.onDragEnd.createDelegate(this), + tolerance : 3, + autoStart : 300 + }); + + this.tracker.initEl(el); + }, + + /** + * @private + * This is tied into the internal Ext.dd.DragTracker. If the slider is currently disabled, + * this returns false to disable the DragTracker too. + * @return {Boolean} False if the slider is currently disabled + */ + onBeforeDragStart : function(e) { + if (this.disabled) { + return false; + } else { + this.slider.promoteThumb(this); + return true; + } + }, + + /** + * @private + * This is tied into the internal Ext.dd.DragTracker's onStart template method. Adds the drag CSS class + * to the thumb and fires the 'dragstart' event + */ + onDragStart: function(e){ + this.el.addClass('x-slider-thumb-drag'); + this.dragging = true; + this.dragStartValue = this.value; + + this.slider.fireEvent('dragstart', this.slider, e, this); + }, + + /** + * @private + * This is tied into the internal Ext.dd.DragTracker's onDrag template method. This is called every time + * the DragTracker detects a drag movement. It updates the Slider's value using the position of the drag + */ + onDrag: function(e) { + var slider = this.slider, + index = this.index, + newValue = this.getNewValue(); + + if (this.constrain) { + var above = slider.thumbs[index + 1], + below = slider.thumbs[index - 1]; + + if (below != undefined && newValue <= below.value) newValue = below.value; + if (above != undefined && newValue >= above.value) newValue = above.value; + } + + slider.setValue(index, newValue, false); + slider.fireEvent('drag', slider, e, this); + }, + + getNewValue: function() { + var slider = this.slider, + pos = slider.innerEl.translatePoints(this.tracker.getXY()); + + return Ext.util.Format.round(slider.reverseValue(pos.left), slider.decimalPrecision); + }, + + /** + * @private + * This is tied to the internal Ext.dd.DragTracker's onEnd template method. Removes the drag CSS class and + * fires the 'changecomplete' event with the new value + */ + onDragEnd: function(e) { + var slider = this.slider, + value = this.value; + + this.el.removeClass('x-slider-thumb-drag'); + + this.dragging = false; + slider.fireEvent('dragend', slider, e); + + if (this.dragStartValue != value) { + slider.fireEvent('changecomplete', slider, value, this); + } + } +}); + +/** + * @class Ext.slider.MultiSlider + * @extends Ext.BoxComponent + * Slider which supports vertical or horizontal orientation, keyboard adjustments, configurable snapping, axis clicking and animation. Can be added as an item to any container. Example usage: +
+new Ext.Slider({
+    renderTo: Ext.getBody(),
+    width: 200,
+    value: 50,
+    increment: 10,
+    minValue: 0,
+    maxValue: 100
+});
+
+ * Sliders can be created with more than one thumb handle by passing an array of values instead of a single one: +
+new Ext.Slider({
+    renderTo: Ext.getBody(),
+    width: 200,
+    values: [25, 50, 75],
+    minValue: 0,
+    maxValue: 100,
+
+    //this defaults to true, setting to false allows the thumbs to pass each other
+    {@link #constrainThumbs}: false
+});
+
+ */ +Ext.slider.MultiSlider = Ext.extend(Ext.BoxComponent, { + /** + * @cfg {Number} value The value to initialize the slider with. Defaults to minValue. + */ + /** + * @cfg {Boolean} vertical Orient the Slider vertically rather than horizontally, defaults to false. + */ + vertical: false, + /** + * @cfg {Number} minValue The minimum value for the Slider. Defaults to 0. + */ + minValue: 0, + /** + * @cfg {Number} maxValue The maximum value for the Slider. Defaults to 100. + */ + maxValue: 100, + /** + * @cfg {Number/Boolean} decimalPrecision. + *

The number of decimal places to which to round the Slider's value. Defaults to 0.

+ *

To disable rounding, configure as false.

+ */ + decimalPrecision: 0, + /** + * @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. + */ + keyIncrement: 1, + /** + * @cfg {Number} increment How many units to change the slider when adjusting by drag and drop. Use this option to enable 'snapping'. + */ + increment: 0, + + /** + * @private + * @property clickRange + * @type Array + * 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], + * 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' + * 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 + */ + clickRange: [5,15], + + /** + * @cfg {Boolean} clickToChange Determines whether or not clicking on the Slider axis will change the slider. Defaults to true + */ + clickToChange : true, + /** + * @cfg {Boolean} animate Turn on or off animation. Defaults to true + */ + animate: true, + + /** + * True while the thumb is in a drag operation + * @type Boolean + */ + dragging: false, + + /** + * @cfg {Boolean} constrainThumbs True to disallow thumbs from overlapping one another. Defaults to true + */ + constrainThumbs: true, + + /** + * @private + * @property topThumbZIndex + * @type Number + * The number used internally to set the z index of the top thumb (see promoteThumb for details) + */ + topThumbZIndex: 10000, + + // private override + initComponent : function(){ + if(!Ext.isDefined(this.value)){ + this.value = this.minValue; + } + + /** + * @property thumbs + * @type Array + * Array containing references to each thumb + */ + this.thumbs = []; + + Ext.slider.MultiSlider.superclass.initComponent.call(this); + + this.keyIncrement = Math.max(this.increment, this.keyIncrement); + this.addEvents( + /** + * @event beforechange + * Fires before the slider value is changed. By returning false from an event handler, + * you can cancel the event and prevent the slider from changing. + * @param {Ext.Slider} slider The slider + * @param {Number} newValue The new value which the slider is being changed to. + * @param {Number} oldValue The old value which the slider was previously. + */ + 'beforechange', + + /** + * @event change + * Fires when the slider value is changed. + * @param {Ext.Slider} slider The slider + * @param {Number} newValue The new value which the slider has been changed to. + * @param {Ext.slider.Thumb} thumb The thumb that was changed + */ + 'change', + + /** + * @event changecomplete + * Fires when the slider value is changed by the user and any drag operations have completed. + * @param {Ext.Slider} slider The slider + * @param {Number} newValue The new value which the slider has been changed to. + * @param {Ext.slider.Thumb} thumb The thumb that was changed + */ + 'changecomplete', + + /** + * @event dragstart + * Fires after a drag operation has started. + * @param {Ext.Slider} slider The slider + * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker + */ + 'dragstart', + + /** + * @event drag + * Fires continuously during the drag operation while the mouse is moving. + * @param {Ext.Slider} slider The slider + * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker + */ + 'drag', + + /** + * @event dragend + * Fires after the drag operation has completed. + * @param {Ext.Slider} slider The slider + * @param {Ext.EventObject} e The event fired from Ext.dd.DragTracker + */ + 'dragend' + ); + + /** + * @property values + * @type Array + * Array of values to initalize the thumbs with + */ + if (this.values == undefined || Ext.isEmpty(this.values)) this.values = [0]; + + var values = this.values; + + for (var i=0; i < values.length; i++) { + this.addThumb(values[i]); + } + + if(this.vertical){ + Ext.apply(this, Ext.slider.Vertical); + } + }, + + /** + * Creates a new thumb and adds it to the slider + * @param {Number} value The initial value to set on the thumb. Defaults to 0 + */ + addThumb: function(value) { + var thumb = new Ext.slider.Thumb({ + value : value, + slider : this, + index : this.thumbs.length, + constrain: this.constrainThumbs + }); + this.thumbs.push(thumb); + + //render the thumb now if needed + if (this.rendered) thumb.render(); + }, + + /** + * @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.thumbs, + zIndex, thumb; + + for (var i = 0, j = thumbs.length; i < j; i++) { + thumb = thumbs[i]; + + if (thumb == topThumb) { + zIndex = this.topThumbZIndex; + } else { + zIndex = ''; + } + + thumb.el.setStyle('zIndex', zIndex); + } + }, + + // private override + onRender : function() { + this.autoEl = { + cls: 'x-slider ' + (this.vertical ? 'x-slider-vert' : 'x-slider-horz'), + cn : { + cls: 'x-slider-end', + cn : { + cls:'x-slider-inner', + cn : [{tag:'a', cls:'x-slider-focus', href:"#", tabIndex: '-1', hidefocus:'on'}] + } + } + }; + + Ext.slider.MultiSlider.superclass.onRender.apply(this, arguments); + + this.endEl = this.el.first(); + this.innerEl = this.endEl.first(); + this.focusEl = this.innerEl.child('.x-slider-focus'); + + //render each thumb + for (var i=0; i < this.thumbs.length; i++) { + this.thumbs[i].render(); + } + + //calculate the size of half a thumb + var thumb = this.innerEl.child('.x-slider-thumb'); + this.halfThumb = (this.vertical ? thumb.getHeight() : thumb.getWidth()) / 2; + + this.initEvents(); + }, + + /** + * @private + * Adds keyboard and mouse listeners on this.el. Ignores click events on the internal focus element. + * Creates a new DragTracker which is used to control what happens when the user drags the thumb around. + */ + initEvents : function(){ + this.mon(this.el, { + scope : this, + mousedown: this.onMouseDown, + keydown : this.onKeyDown + }); + + this.focusEl.swallowEvent("click", true); + }, + + /** + * @private + * Mousedown handler for the slider. If the clickToChange is enabled and the click was not on the draggable 'thumb', + * this calculates the new value of the slider and tells the implementation (Horizontal or Vertical) to move the thumb + * @param {Ext.EventObject} e The click event + */ + onMouseDown : function(e){ + if(this.disabled){ + return; + } + + //see if the click was on any of the thumbs + var thumbClicked = false; + for (var i=0; i < this.thumbs.length; i++) { + thumbClicked = thumbClicked || e.target == this.thumbs[i].el.dom; + } + + if (this.clickToChange && !thumbClicked) { + var local = this.innerEl.translatePoints(e.getXY()); + this.onClickChange(local); + } + this.focus(); + }, + + /** + * @private + * Moves the thumb to the indicated position. Note that a Vertical implementation is provided in Ext.slider.Vertical. + * Only changes the value if the click was within this.clickRange. + * @param {Object} local Object containing top and left values for the click event. + */ + onClickChange : function(local) { + if (local.top > this.clickRange[0] && local.top < this.clickRange[1]) { + //find the nearest thumb to the click event + var thumb = this.getNearest(local, 'left'), + index = thumb.index; + + this.setValue(index, Ext.util.Format.round(this.reverseValue(local.left), this.decimalPrecision), undefined, true); + } + }, + + /** + * @private + * Returns the nearest thumb to a click event, along with its distance + * @param {Object} local Object containing top and left values from a click event + * @param {String} prop The property of local to compare on. Use 'left' for horizontal sliders, 'top' for vertical ones + * @return {Object} The closest thumb object and its distance from the click event + */ + getNearest: function(local, prop) { + var localValue = prop == 'top' ? this.innerEl.getHeight() - local[prop] : local[prop], + clickValue = this.reverseValue(localValue), + nearestDistance = (this.maxValue - this.minValue) + 5, //add a small fudge for the end of the slider + index = 0, + nearest = null; + + for (var i=0; i < this.thumbs.length; i++) { + var thumb = this.thumbs[i], + value = thumb.value, + dist = Math.abs(value - clickValue); + + if (Math.abs(dist <= nearestDistance)) { + nearest = thumb; + index = i; + nearestDistance = dist; + } + } + return nearest; + }, + + /** + * @private + * Handler for any keypresses captured by the slider. If the key is UP or RIGHT, the thumb is moved along to the right + * by this.keyIncrement. If DOWN or LEFT it is moved left. Pressing CTRL moves the slider to the end in either direction + * @param {Ext.EventObject} e The Event object + */ + onKeyDown : function(e){ + if(this.disabled){e.preventDefault();return;} + var k = e.getKey(); + switch(k){ + case e.UP: + case e.RIGHT: + e.stopEvent(); + if(e.ctrlKey){ + this.setValue(this.maxValue, undefined, true); + }else{ + this.setValue(this.value+this.keyIncrement, undefined, true); + } + break; + case e.DOWN: + case e.LEFT: + e.stopEvent(); + if(e.ctrlKey){ + this.setValue(this.minValue, undefined, true); + }else{ + this.setValue(this.value-this.keyIncrement, undefined, true); + } + break; + default: + e.preventDefault(); + } + }, + + /** + * @private + * If using snapping, this takes a desired new value and returns the closest snapped + * value to it + * @param {Number} value The unsnapped value + * @return {Number} The value of the nearest snap target + */ + doSnap : function(value){ + if (!(this.increment && value)) { + return value; + } + var newValue = value, + inc = this.increment, + m = value % inc; + if (m != 0) { + newValue -= m; + if (m * 2 >= inc) { + newValue += inc; + } else if (m * 2 < -inc) { + newValue -= inc; + } + } + return newValue.constrain(this.minValue, this.maxValue); + }, + + // private + afterRender : function(){ + Ext.slider.MultiSlider.superclass.afterRender.apply(this, arguments); + + for (var i=0; i < this.thumbs.length; i++) { + var thumb = this.thumbs[i]; + + if (thumb.value !== undefined) { + var v = this.normalizeValue(thumb.value); + + if (v !== thumb.value) { + // delete this.value; + this.setValue(i, v, false); + } else { + this.moveThumb(i, this.translateValue(v), false); + } + } + }; + }, + + /** + * @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 w = this.innerEl.getWidth(), + v = this.maxValue - this.minValue; + return v == 0 ? w : (w/v); + }, + + /** + * @private + * Returns a snapped, constrained value when given a desired value + * @param {Number} value 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){ + v = this.doSnap(v); + v = Ext.util.Format.round(v, this.decimalPrecision); + v = v.constrain(this.minValue, this.maxValue); + return v; + }, + + /** + * Sets the minimum value for the slider instance. If the current value is less than the + * minimum value, the current value will be changed. + * @param {Number} val The new minimum value + */ + setMinValue : function(val){ + this.minValue = val; + this.syncThumb(); + + for (var i=0, j = this.thumbs.length; i < j; i++) { + if (this.thumbs[i].value < val) this.thumbs[i].value = val; + } + }, + + /** + * Sets the maximum value for the slider instance. If the current value is more than the + * maximum value, the current value will be changed. + * @param {Number} val The new maximum value + */ + setMaxValue : function(val){ + this.maxValue = val; + this.syncThumb(); + + for (var i=0; i < this.thumbs.length; i++) { + if (this.thumbs[i].value > val) this.thumbs[i].value = val; + } + }, + + /** + * Programmatically sets the value of the Slider. Ensures that the value is constrained within + * the minValue and maxValue. + * @param {Number} index Index of the thumb to move + * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue) + * @param {Boolean} animate Turn on or off animation, defaults to true + */ + setValue : function(index, v, animate, changeComplete) { + var thumb = this.thumbs[index], + el = thumb.el; + + v = this.normalizeValue(v); + + if (v !== thumb.value && this.fireEvent('beforechange', this, v, thumb.value) !== false) { + thumb.value = v; + this.moveThumb(index, this.translateValue(v), animate !== false); + this.fireEvent('change', this, v, thumb); + if(changeComplete){ + this.fireEvent('changecomplete', this, v, thumb); + } + } + }, + + /** + * @private + */ + translateValue : function(v) { + var ratio = this.getRatio(); + return (v * ratio) - (this.minValue * ratio) - this.halfThumb; + }, + + /** + * @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, reverseValue(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 + */ + reverseValue : function(pos){ + var ratio = this.getRatio(); + return (pos + (this.minValue * ratio)) / ratio; + }, + + /** + * @private + * @param {Number} index Index of the thumb to move + */ + moveThumb: function(index, v, animate){ + var thumb = this.thumbs[index].el; + + if(!animate || this.animate === false){ + thumb.setLeft(v); + }else{ + thumb.shift({left: v, stopFx: true, duration:.35}); + } + }, + + // private + focus : function(){ + this.focusEl.focus(10); + }, + + // private + onResize : function(w, h){ + var thumbs = this.thumbs, + len = thumbs.length, + i = 0; + + /* + * If we happen to be animating during a resize, the position of the thumb will likely be off + * when the animation stops. As such, just stop any animations before syncing the thumbs. + */ + for(; i < len; ++i){ + thumbs[i].el.stopFx(); + } + this.innerEl.setWidth(w - (this.el.getPadding('l') + this.endEl.getPadding('r'))); + this.syncThumb(); + Ext.slider.MultiSlider.superclass.onResize.apply(this, arguments); + }, + + //private + onDisable: function(){ + Ext.slider.MultiSlider.superclass.onDisable.call(this); + + for (var i=0; i < this.thumbs.length; i++) { + var thumb = this.thumbs[i], + el = thumb.el; + + thumb.disable(); + + if(Ext.isIE){ + //IE breaks when using overflow visible and opacity other than 1. + //Create a place holder for the thumb and display it. + var xy = el.getXY(); + el.hide(); + + this.innerEl.addClass(this.disabledClass).dom.disabled = true; + + if (!this.thumbHolder) { + this.thumbHolder = this.endEl.createChild({cls: 'x-slider-thumb ' + this.disabledClass}); + } + + this.thumbHolder.show().setXY(xy); + } + } + }, + + //private + onEnable: function(){ + Ext.slider.MultiSlider.superclass.onEnable.call(this); + + for (var i=0; i < this.thumbs.length; i++) { + var thumb = this.thumbs[i], + el = thumb.el; + + thumb.enable(); + + if (Ext.isIE) { + this.innerEl.removeClass(this.disabledClass).dom.disabled = false; + + if (this.thumbHolder) this.thumbHolder.hide(); + + el.show(); + this.syncThumb(); + } + } + }, + + /** + * Synchronizes the thumb position to the proper proportion of the total component width based + * on the current slider {@link #value}. This will be called automatically when the Slider + * is resized by a layout, but if it is rendered auto width, this method can be called from + * another resize handler to sync the Slider if necessary. + */ + syncThumb : function() { + if (this.rendered) { + for (var i=0; i < this.thumbs.length; i++) { + this.moveThumb(i, this.translateValue(this.thumbs[i].value)); + } + } + }, + + /** + * Returns the current value of the slider + * @param {Number} index The index of the thumb to return a value for + * @return {Number} The current value of the slider + */ + getValue : function(index) { + return this.thumbs[index].value; + }, + + /** + * Returns an array of values - one for the location of each thumb + * @return {Array} The set of thumb values + */ + getValues: function() { + var values = []; + + for (var i=0; i < this.thumbs.length; i++) { + values.push(this.thumbs[i].value); + } + + return values; + }, + + // private + beforeDestroy : function(){ + Ext.destroyMembers(this, 'endEl', 'innerEl', 'thumb', 'halfThumb', 'focusEl', 'tracker', 'thumbHolder'); + Ext.slider.MultiSlider.superclass.beforeDestroy.call(this); + } +}); + +Ext.reg('multislider', Ext.slider.MultiSlider); + +/** + * @class Ext.slider.SingleSlider + * @extends Ext.slider.MultiSlider + * Slider which supports vertical or horizontal orientation, keyboard adjustments, + * configurable snapping, axis clicking and animation. Can be added as an item to + * any container. Example usage: +

+new Ext.slider.SingleSlider({
+    renderTo: Ext.getBody(),
+    width: 200,
+    value: 50,
+    increment: 10,
+    minValue: 0,
+    maxValue: 100
+});
+
+ * The class Ext.slider.SingleSlider is aliased to Ext.Slider for backwards compatibility. + */ +Ext.slider.SingleSlider = Ext.extend(Ext.slider.MultiSlider, { + constructor: function(config) { + config = config || {}; + + Ext.applyIf(config, { + values: [config.value || 0] + }); + + Ext.slider.SingleSlider.superclass.constructor.call(this, config); + }, + + /** + * Returns the current value of the slider + * @return {Number} The current value of the slider + */ + getValue: function() { + //just returns the value of the first thumb, which should be the only one in a single slider + return Ext.slider.SingleSlider.superclass.getValue.call(this, 0); + }, + + /** + * Programmatically sets the value of the Slider. Ensures that the value is constrained within + * the minValue and maxValue. + * @param {Number} value The value to set the slider to. (This will be constrained within minValue and maxValue) + * @param {Boolean} animate Turn on or off animation, defaults to true + */ + setValue: function(value, animate) { + var args = Ext.toArray(arguments), + len = args.length; + + //this is to maintain backwards compatiblity for sliders with only one thunb. Usually you must pass the thumb + //index to setValue, but if we only have one thumb we inject the index here first if given the multi-slider + //signature without the required index. The index will always be 0 for a single slider + if (len == 1 || (len <= 3 && typeof arguments[1] != 'number')) { + args.unshift(0); + } + + return Ext.slider.SingleSlider.superclass.setValue.apply(this, args); + }, + + /** + * Synchronizes the thumb position to the proper proportion of the total component width based + * on the current slider {@link #value}. This will be called automatically when the Slider + * is resized by a layout, but if it is rendered auto width, this method can be called from + * another resize handler to sync the Slider if necessary. + */ + syncThumb : function() { + return Ext.slider.SingleSlider.superclass.syncThumb.apply(this, [0].concat(arguments)); + }, + + // private + getNearest : function(){ + // Since there's only 1 thumb, it's always the nearest + return this.thumbs[0]; + } +}); + +//backwards compatibility +Ext.Slider = Ext.slider.SingleSlider; + +Ext.reg('slider', Ext.slider.SingleSlider); + +// private class to support vertical sliders +Ext.slider.Vertical = { + onResize : function(w, h){ + this.innerEl.setHeight(h - (this.el.getPadding('t') + this.endEl.getPadding('b'))); + this.syncThumb(); + }, + + getRatio : function(){ + var h = this.innerEl.getHeight(), + v = this.maxValue - this.minValue; + return h/v; + }, + + moveThumb: function(index, v, animate) { + var thumb = this.thumbs[index], + el = thumb.el; + + if (!animate || this.animate === false) { + el.setBottom(v); + } else { + el.shift({bottom: v, stopFx: true, duration:.35}); + } + }, + + onClickChange : function(local) { + if (local.left > this.clickRange[0] && local.left < this.clickRange[1]) { + var thumb = this.getNearest(local, 'top'), + index = thumb.index, + value = this.minValue + this.reverseValue(this.innerEl.getHeight() - local.top); + + this.setValue(index, Ext.util.Format.round(value, this.decimalPrecision), undefined, true); + } + } +}; + +//private class to support vertical dragging of thumbs within a slider +Ext.slider.Thumb.Vertical = { + getNewValue: function() { + var slider = this.slider, + innerEl = slider.innerEl, + pos = innerEl.translatePoints(this.tracker.getXY()), + bottom = innerEl.getHeight() - pos.top; + + return slider.minValue + Ext.util.Format.round(bottom / slider.getRatio(), slider.decimalPrecision); + } +};