Introduction
Sencha Touch Mobile Framework has a Ext.form.Slider
class to show a slider for user to pick a value. The interface looks like this in the Height
field (click here for demo, you may have to use mobile device, or Chrome or Safari browsers on computers):
It works great, but I want to to show more information like the minimum value and the maximum value when it is setup, the current value when a user drags the thumb, and the value when a user stops dragging, just like this in fields Price
and Weight
:
Background
DevGuyDotOrg did a great job to show the current value when a user drags the thumb (see the following image):
Based on this idea, I made an Ext.form.Slider
class extension like this:
Ext.ns('Ext.ux');
Ext.ux.SliderTooltip = new Ext.Panel({
floating: true,
height: 30,
styleHtmlContent: true,
style: "background-color: #FFF; text-align: center"
});
Ext.ux.Slider = Ext.extend(Ext.form.Slider, {
listeners: {
drag: function (theSlider, theThumb, ThumbValue) {
Ext.ux.SliderTooltip.setWidth(ThumbValue.length * 1.5);
Ext.ux.SliderTooltip.showBy(theThumb);
Ext.ux.SliderTooltip.el.setHTML(ThumbValue);
},
dragend: function (theSlider, theThumb, ThumbValue) {
Ext.ux.SliderTooltip.hide();
},
scope: this
}
});
Ext.reg('uxSlider', Ext.ux.Slider);
Though it works well in terms of the tooltip, I prefer a plugin in this case. (There are only a few differences in some respects, see here.)
The Codes
For those who are new to Sencha plugins, please refer to Ref 1, Ref 2 and Ref 3.
First, let's take a look at all the codes for the plugin:
Ext.ns('Ext.ux');
Ext.ux.CustomSlider = Ext.extend(Object, {
valueTextClass: 'x-slider-value-text',
valueUnit: '',
valueUnitPos: 'before',
tooltipStyle: 'background-color: #FFF; text-align: center',
showSliderBothEndValue: true,
sliderEndValuePos: 'under',
sliderEndValueStyle: 'color: green',
constructor: function(config){
Ext.apply(this, config);
Ext.ux.CustomSlider.superclass.constructor.apply(this, arguments);
},
init: function(parent) {
var me = this;
parent.on({
drag: {
fn: function(slider, thumb, value) {
me.sliderTooltip.setWidth(value.length * 1.5);
me.sliderTooltip.showBy(thumb);
me.sliderTooltip.el.setHTML(value);
}
},
dragend: {
fn: function (slider, thumb, value) {
me.sliderTooltip.hide();
me.showSliderValue(this.valueTextEl, slider, thumb, value);
}
},
afterrender: {
fn: function(component) {
me.createSliderToolTip();
if (me.showSliderBothEndValue) me.showSliderEndValue(this);
if (!this.valueTextEl) {
this.valueTextEl = component.getEl().createChild({
cls: me.valueTextClass
});
}
}
}
});
},
showSliderValue: function(valueTextEl, slider, thumb, value) {
if (this.valueUnitPos == 'before') {
valueTextEl.setHTML(this.valueUnit + value);
} else {
if (parseFloat(value) > 1) {
valueTextEl.setHTML(value + '&nbsp' + this.valueUnit + 's');
} else {
valueTextEl.setHTML(value + '&nbsp' + this.valueUnit);
}
}
var left = thumb.getEl().getX(),
thumbWidth = thumb.getEl().getWidth(),
thumbHeight = thumb.getEl().getHeight(),
top = thumbHeight / 2,
textWidth = valueTextEl.getWidth(),
sliderLength = slider.getWidth();
if (left > sliderLength - textWidth - thumbWidth) {
left = left - textWidth - thumbWidth / 2;
} else {
left = left + thumbWidth / 2;
}
valueTextEl.setLeft(left);
valueTextEl.setTop(top);
},
showSliderEndValue: function(slider) {
var sliderPosX = slider.getThumb().getEl().getX();
var minValueEl = slider.getEl().createChild();
minValueEl.setHTML(slider.minValue);
minValueEl.applyStyles('overflow:hidden;position:absolute');
minValueEl.applyStyles(this.sliderEndValueStyle);
var thumbHeight = slider.getThumb().getEl().getHeight();
minValueEl.setLeft(sliderPosX - 2);
if (this.sliderEndValuePos == 'above') {
minValueEl.setTop(-4);
} else {
minValueEl.setTop(thumbHeight + 2);
}
var maxValueEl = slider.getEl().createChild();
maxValueEl.setHTML(slider.maxValue);
maxValueEl.applyStyles('overflow:hidden;position:absolute');
maxValueEl.applyStyles(this.sliderEndValueStyle);
var sliderLength = slider.getEl().getWidth();
var maxTextWidth = maxValueEl.getWidth();
maxValueEl.setLeft(sliderLength - maxTextWidth - 25);
if (this.sliderEndValuePos == 'above') {
maxValueEl.setTop(-4);
} else {
maxValueEl.setTop(thumbHeight + 2);
}
},
createSliderToolTip: function() {
if (! this.sliderTooltip) {
this.sliderTooltip = new Ext.Panel({
floating: true,
height: 30,
styleHtmlContent: true,
style: this.tooltipStyle
});
}
}
});
Lines 1 through 7 are default configurations and can be overwritten when this plugin is plugged into its container and if its constructor is set up in the above way.
All the features the plugin will add or modify lie in the function init()
. In this function, it is very important to know that in the parent.on({...})
code block, pronoun this refers to the plugin's parent (which is the Slider
class), while me refers to the plugin itself.
In the afterrender
event handler, a tooltip panel is prepared by function createSliderToolTip
for the drag event to use. Function createSliderToolTip
creates different instances of tooltip for different plugin instances, and the tooltip style can be changed by changing the plugin's tooltipStyle
option, see screenshots below:
If we don't want different tooltip styles, then we can re-write the function to create only one tooltip panel on one page and make a little bit gain in performance, like this:
createSliderToolTip: function() {
var sliderTooltip = Ext.getCmp('sliderTooltip');
if (! sliderTooltip) {
sliderTooltip = new Ext.Panel({
floating: true,
height: 30,
id: 'sliderTooltip',
styleHtmlContent: true,
style: this.tooltipStyle
});
}
this.sliderTooltip = sliderTooltip;
}
When changed in this way, the tooltip panel will take the first plugin's style configuration (in this case, only the tooltipStyle
option in Price
field takes effect. That in Weight
field will be simply ignored).
It is also very important that the CSS class for the slider value text should be defined at least with the position attribute being absolute, so that we can move and align the text to where we want it to be. My definition is:
.x-slider-value-text {
overflow:hidden;
position:absolute;
z-index:9999;
color:rgb(255,55,55);
}
With this in place, we can position those elements that are created in the afterrender
event handler, such as valueTextEl
, minValueEl
and maxValueEl
. Functions showSliderValue
and showSliderEndValue
deal with the positioning and styling hassles involved.
Using the Code
We can use the plugin in this way:
items: [{
xtype: 'sliderfield',
minValue: 0,
maxValue: 10000,
increment: 0.5,
id: 'price',
plugins: [new Ext.ux.CustomSlider({
valueUnit: '$',
tooltipStyle: 'background-color: #AAA; text-align: center',
showSliderBothEndValue: true,
sliderEndValuePos: 'above'
})],
label: 'Price ($)'
}, {
xtype: 'sliderfield',
increment: 0.5,
id: 'weight',
plugins: [new Ext.ux.CustomSlider({
valueUnit: 'carat',
tooltipStyle: 'background-color: #FFF; text-align: center',
valueUnitPos: 'after',
showSliderBothEndValue: true
})],
label: 'Weight (carat)'
},
...
}]
...
From this example, we can see that we can customize the options and style, like value unit and its position, tooltip style, show or not show the slider's minimum and maximum values.
If we don't like the plugin to work, just remove it from the plugins option if we have more than one plugin in place, or take away the plugins option completely.
Points of Interest
Writing a plugin is fun. It is not so difficult as one may think.