5 ( function( $, undefined ) {
7 $.widget( "mobile.slider", $.mobile.widget, {
12 initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')"
17 // TODO: Each of these should have comments explain what they're for
20 control = this.element,
22 parentTheme = $.mobile.getInheritedTheme( control, "c" ),
24 theme = this.options.theme || parentTheme,
26 trackTheme = this.options.trackTheme || parentTheme,
28 cType = control[ 0 ].nodeName.toLowerCase(),
30 selectClass = ( cType == "select" ) ? "ui-slider-switch" : "",
32 controlID = control.attr( "id" ),
34 labelID = controlID + "-label",
36 label = $( "[for='"+ controlID +"']" ).attr( "id", labelID ),
39 return cType == "input" ? parseFloat( control.val() ) : control[0].selectedIndex;
42 min = cType == "input" ? parseFloat( control.attr( "min" ) ) : 0,
44 max = cType == "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1,
46 step = window.parseFloat( control.attr( "step" ) || 1 ),
48 slider = $( "<div class='ui-slider " + selectClass + " ui-btn-down-" + trackTheme +
49 " ui-btn-corner-all' role='application'></div>" ),
51 handle = $( "<a href='#' class='ui-slider-handle'></a>" )
53 .buttonMarkup({ corners: true, theme: theme, shadow: true })
58 "aria-valuenow": val(),
59 "aria-valuetext": val(),
61 "aria-labelledby": labelID
74 if ( cType == "select" ) {
76 slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" );
78 // make the handle move with a smooth transition
79 handle.addClass( "ui-slider-handle-snapping" );
81 options = control.find( "option" );
83 control.find( "option" ).each(function( i ) {
85 var side = !i ? "b":"a",
86 corners = !i ? "right" :"left",
87 theme = !i ? " ui-btn-down-" + trackTheme :( " " + $.mobile.activeBtnClass );
89 $( "<div class='ui-slider-labelbg ui-slider-labelbg-" + side + theme + " ui-btn-corner-" + corners + "'></div>" )
92 $( "<span class='ui-slider-label ui-slider-label-" + side + theme + " ui-btn-corner-" + corners + "' role='img'>" + $( this ).getEncodedText() + "</span>" )
98 label.addClass( "ui-slider" );
100 // monitor the input for updated values
101 control.addClass( cType === "input" ? "ui-slider-input" : "ui-slider-switch" )
102 .change( function() {
103 // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again
104 if (!self.mouseMoved) {
105 self.refresh( val(), true );
108 .keyup( function() { // necessary?
109 self.refresh( val(), true, true );
112 self.refresh( val(), true );
115 // prevent screen drag when slider activated
116 $( document ).bind( "vmousemove", function( event ) {
117 if ( self.dragging ) {
118 // self.mouseMoved must be updated before refresh() because it will be used in the control "change" event
119 self.mouseMoved = true;
121 if ( cType === "select" ) {
122 // make the handle move in sync with the mouse
123 handle.removeClass( "ui-slider-handle-snapping" );
126 self.refresh( event );
128 // only after refresh() you can calculate self.userModified
129 self.userModified = self.beforeStart !== control[0].selectedIndex;
134 slider.bind( "vmousedown", function( event ) {
135 self.dragging = true;
136 self.userModified = false;
137 self.mouseMoved = false;
139 if ( cType === "select" ) {
140 self.beforeStart = control[0].selectedIndex;
143 self.refresh( event );
147 slider.add( document )
148 .bind( "vmouseup", function() {
149 if ( self.dragging ) {
151 self.dragging = false;
153 if ( cType === "select") {
155 // make the handle move with a smooth transition
156 handle.addClass( "ui-slider-handle-snapping" );
158 if ( self.mouseMoved ) {
160 // this is a drag, change the value only if user dragged enough
161 if ( self.userModified ) {
162 self.refresh( self.beforeStart == 0 ? 1 : 0 );
165 self.refresh( self.beforeStart );
170 // this is just a click, change the value
171 self.refresh( self.beforeStart == 0 ? 1 : 0 );
176 self.mouseMoved = false;
182 slider.insertAfter( control );
184 // NOTE force focus on handle
186 .bind( "vmousedown", function() {
189 .bind( "vclick", false );
192 .bind( "keydown", function( event ) {
195 if ( self.options.disabled ) {
199 // In all cases prevent the default and mark the handle as active
200 switch ( event.keyCode ) {
201 case $.mobile.keyCode.HOME:
202 case $.mobile.keyCode.END:
203 case $.mobile.keyCode.PAGE_UP:
204 case $.mobile.keyCode.PAGE_DOWN:
205 case $.mobile.keyCode.UP:
206 case $.mobile.keyCode.RIGHT:
207 case $.mobile.keyCode.DOWN:
208 case $.mobile.keyCode.LEFT:
209 event.preventDefault();
211 if ( !self._keySliding ) {
212 self._keySliding = true;
213 $( this ).addClass( "ui-state-active" );
218 // move the slider according to the keypress
219 switch ( event.keyCode ) {
220 case $.mobile.keyCode.HOME:
223 case $.mobile.keyCode.END:
226 case $.mobile.keyCode.PAGE_UP:
227 case $.mobile.keyCode.UP:
228 case $.mobile.keyCode.RIGHT:
229 self.refresh( index + step );
231 case $.mobile.keyCode.PAGE_DOWN:
232 case $.mobile.keyCode.DOWN:
233 case $.mobile.keyCode.LEFT:
234 self.refresh( index - step );
237 }) // remove active mark
238 .keyup( function( event ) {
239 if ( self._keySliding ) {
240 self._keySliding = false;
241 $( this ).removeClass( "ui-state-active" );
245 this.refresh(undefined, undefined, true);
248 refresh: function( val, isfromControl, preventInputUpdate ) {
250 if ( this.options.disabled || this.element.attr('disabled')) {
254 var control = this.element, percent,
255 cType = control[0].nodeName.toLowerCase(),
256 min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0,
257 max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length - 1;
259 if ( typeof val === "object" ) {
261 // a slight tolerance helped get to the ends of the slider
263 if ( !this.dragging ||
264 data.pageX < this.slider.offset().left - tol ||
265 data.pageX > this.slider.offset().left + this.slider.width() + tol ) {
268 percent = Math.round( ( ( data.pageX - this.slider.offset().left ) / this.slider.width() ) * 100 );
271 val = cType === "input" ? parseFloat( control.val() ) : control[0].selectedIndex;
273 percent = ( parseFloat( val ) - min ) / ( max - min ) * 100;
276 if ( isNaN( percent ) ) {
284 if ( percent > 100 ) {
288 var newval = Math.round( ( percent / 100 ) * ( max - min ) ) + min;
290 if ( newval < min ) {
294 if ( newval > max ) {
298 // Flip the stack of the bg colors
299 if ( percent > 60 && cType === "select" ) {
302 this.handle.css( "left", percent + "%" );
304 "aria-valuenow": cType === "input" ? newval : control.find( "option" ).eq( newval ).attr( "value" ),
305 "aria-valuetext": cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText(),
309 // add/remove classes for flip toggle switch
310 if ( cType === "select" ) {
311 if ( newval === 0 ) {
312 this.slider.addClass( "ui-slider-switch-a" )
313 .removeClass( "ui-slider-switch-b" );
315 this.slider.addClass( "ui-slider-switch-b" )
316 .removeClass( "ui-slider-switch-a" );
320 if ( !preventInputUpdate ) {
321 var valueChanged = false;
323 // update control"s value
324 if ( cType === "input" ) {
325 valueChanged = control.val() !== newval;
326 control.val( newval );
328 valueChanged = control[ 0 ].selectedIndex !== newval;
329 control[ 0 ].selectedIndex = newval;
331 if ( !isfromControl && valueChanged ) {
332 control.trigger( "change" );
338 this.element.attr( "disabled", false );
339 this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
340 return this._setOption( "disabled", false );
343 disable: function() {
344 this.element.attr( "disabled", true );
345 this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true );
346 return this._setOption( "disabled", true );
351 //auto self-init widgets
352 $( document ).bind( "pagecreate create", function( e ){
353 $.mobile.slider.prototype.enhanceWithin( e.target );