1 (function( $, undefined ) {
3 $.widget( "mobile.slider", $.mobile.widget, {
4 widgetEventPrefix: "slide",
10 initSelector: "input[type='range'], :jqmData(type='range'), :jqmData(role='slider')",
16 // TODO: Each of these should have comments explain what they're for
19 control = this.element,
21 parentTheme = $.mobile.getInheritedTheme( control, "c" ),
23 theme = this.options.theme || parentTheme,
25 trackTheme = this.options.trackTheme || parentTheme,
27 cType = control[ 0 ].nodeName.toLowerCase(),
29 selectClass = ( cType === "select" ) ? "ui-slider-switch" : "",
31 controlID = control.attr( "id" ),
33 $label = $( "[for='" + controlID + "']" ),
35 labelID = $label.attr( "id" ) || controlID + "-label",
37 label = $label.attr( "id", labelID ),
40 return cType === "input" ? parseFloat( control.val() ) : control[0].selectedIndex;
43 min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0,
45 max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length-1,
47 step = window.parseFloat( control.attr( "step" ) || 1 ),
49 inlineClass = ( this.options.inline || $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "inline" ) === true ) ? " ui-slider-inline" : "",
51 miniClass = ( this.options.mini || $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "min" ) ) ? " ui-slider-mini" : "",
54 domHandle = document.createElement( 'a' ),
55 handle = $( domHandle ),
56 domSlider = document.createElement( 'div' ),
57 slider = $( domSlider ),
59 valuebg = $.mobile.getAttrFixed( control[0], "data-" + $.mobile.ns + "highlight" ) !== false && cType !== "select" ? (function() {
60 var bg = document.createElement('div');
61 bg.className = 'ui-slider-bg ' + $.mobile.activeBtnClass + ' ui-btn-corner-all';
62 return $( bg ).prependTo( slider );
69 domHandle.setAttribute( 'href', "#" );
70 domSlider.setAttribute('role','application');
71 domSlider.className = ['ui-slider ',selectClass," ui-btn-down-",trackTheme,' ui-btn-corner-all', inlineClass, miniClass].join( "" );
72 domHandle.className = 'ui-slider-handle';
73 domSlider.appendChild( domHandle );
75 if ( $( control ).find( "option" ).length && $( control ).find( "option" ).text() === "" ) {
76 $( domSlider ).addClass( "ui-toggle-switch" );
79 handle.buttonMarkup({ corners: true, theme: theme, shadow: true })
84 "aria-valuenow": val(),
85 "aria-valuetext": val(),
87 "aria-labelledby": labelID
100 if ( cType === "select" ) {
101 var wrapper = document.createElement('div');
102 wrapper.className = 'ui-slider-inneroffset';
104 for ( var j = 0,length = domSlider.childNodes.length;j < length;j++ ) {
105 wrapper.appendChild( domSlider.childNodes[j] );
108 domSlider.appendChild( wrapper );
110 // slider.wrapInner( "<div class='ui-slider-inneroffset'></div>" );
112 // make the handle move with a smooth transition
113 handle.addClass( "ui-slider-handle-snapping" );
115 options = control.find( "option" );
117 for ( var i = 0, optionsCount = options.length; i < optionsCount; i++ ) {
118 var side = !i ? "b" : "a",
119 sliderTheme = !i ? " ui-btn-down-" + trackTheme : ( " " + $.mobile.activeBtnClass ),
120 sliderLabel = document.createElement( 'div' ),
121 sliderImg = document.createElement( 'span' );
123 sliderImg.className = ['ui-slider-label ui-slider-label-',side,sliderTheme," ui-btn-corner-all"].join( "" );
124 sliderImg.setAttribute('role','img');
125 sliderImg.appendChild( document.createTextNode( options[i].innerHTML ) );
126 $( sliderImg ).html() ? $( sliderImg ).html( $( sliderImg ).text() ) : $( sliderImg ).html();
127 $(sliderImg).prependTo( slider );
130 self._labels = $( ".ui-slider-label", slider );
134 label.addClass( "ui-slider" );
136 // monitor the input for updated values
137 control.addClass( cType === "input" ? "ui-slider-input" : "ui-slider-switch" )
139 // if the user dragged the handle, the "change" event was triggered from inside refresh(); don't call refresh() again
140 if ( !self.mouseMoved ) {
141 self.refresh( val(), true );
144 .keyup(function() { // necessary?
145 self.refresh( val(), true, true );
148 self.refresh( val(), true );
151 this._preventDocumentDrag = function( event ) {
152 // NOTE: we don't do this in refresh because we still want to
153 // support programmatic alteration of disabled inputs
154 if ( self.dragging && !self.options.disabled && (!$(self.element).siblings(".ui-slider").is(".ui-toggle-switch") || (( $(event.target).find(".ui-slider-switch") === $(self.element) ) && slider.is(".ui-toggle-switch") && $(self.element).siblings(".ui-slider").find(".ui-slider-handle").is(".ui-btn-hover-s"))) ) {
156 // divide jQM and Tizen slider on dragging outside of toggle area
157 // self.mouseMoved must be updated before refresh() because it will be used in the control "change" event
158 self.mouseMoved = true;
160 if ( cType === "select" ) {
161 // make the handle move in sync with the mouse
162 handle.removeClass( "ui-slider-handle-snapping" );
165 self.refresh( event );
167 // only after refresh() you can calculate self.userModified
168 self.userModified = self.beforeStart !== control[0].selectedIndex;
173 this._on( $.mobile.$document, { "vmousemove": this._preventDocumentDrag });
175 // it appears the clicking the up and down buttons in chrome on
176 // range/number inputs doesn't trigger a change until the field is
177 // blurred. Here we check thif the value has changed and refresh
178 control.bind( "vmouseup", $.proxy( self._checkedRefresh, self));
180 slider.bind( "vmousedown", function( event ) {
181 // NOTE: we don't do this in refresh because we still want to
182 // support programmatic alteration of disabled inputs
183 if ( self.options.disabled ) {
187 self.dragging = true;
188 self.userModified = false;
189 self.mouseMoved = false;
191 if ( cType === "select" ) {
192 self.beforeStart = control[0].selectedIndex;
195 self.refresh( event );
196 self._trigger( "start" );
199 .bind( "vclick", false );
201 this._sliderMouseUp = function() {
202 if ( self.dragging ) {
203 self.dragging = false;
205 if ( cType === "select") {
206 // make the handle move with a smooth transition
207 handle.addClass( "ui-slider-handle-snapping" );
209 if ( self.mouseMoved ) {
210 // this is a drag, change the value only if user dragged enough
211 if ( self.userModified ) {
212 self.refresh( self.beforeStart === 0 ? 1 : 0 );
215 self.refresh( self.beforeStart );
219 // this is just a click, change the value
220 self.refresh( self.beforeStart === 0 ? 1 : 0 );
224 self.mouseMoved = false;
225 self._trigger( "stop" );
230 this._on( slider.add( document ), { "vmouseup": this._sliderMouseUp });
231 slider.insertAfter( control );
233 // Only add focus class to toggle switch, sliders get it automatically from ui-btn
234 if ( cType === 'select' ) {
237 slider.addClass( $.mobile.focusClass );
241 slider.removeClass( $.mobile.focusClass );
247 // NOTE force focus on handle
248 vmousedown: function() {
254 keydown: function( event ) {
257 if ( self.options.disabled ) {
261 // In all cases prevent the default and mark the handle as active
262 switch ( event.keyCode ) {
263 case $.mobile.keyCode.HOME:
264 case $.mobile.keyCode.END:
265 case $.mobile.keyCode.PAGE_UP:
266 case $.mobile.keyCode.PAGE_DOWN:
267 case $.mobile.keyCode.UP:
268 case $.mobile.keyCode.RIGHT:
269 case $.mobile.keyCode.DOWN:
270 case $.mobile.keyCode.LEFT:
271 event.preventDefault();
273 if ( !self._keySliding ) {
274 self._keySliding = true;
275 $( this ).addClass( "ui-state-active" );
280 // move the slider according to the keypress
281 switch ( event.keyCode ) {
282 case $.mobile.keyCode.HOME:
285 case $.mobile.keyCode.END:
288 case $.mobile.keyCode.PAGE_UP:
289 case $.mobile.keyCode.UP:
290 case $.mobile.keyCode.RIGHT:
291 self.refresh( index + step );
293 case $.mobile.keyCode.PAGE_DOWN:
294 case $.mobile.keyCode.DOWN:
295 case $.mobile.keyCode.LEFT:
296 self.refresh( index - step );
299 }, // remove active mark
301 keyup: function( event ) {
302 if ( self._keySliding ) {
303 self._keySliding = false;
304 $( this ).removeClass( "ui-state-active" );
309 this.refresh( undefined, undefined, true );
312 _checkedRefresh: function() {
313 if( this.value != this._value() ){
314 this.refresh( this._value() );
319 return this._type === "input" ?
320 parseFloat( this.element.val() ) : this.element[0].selectedIndex;
323 refresh: function( val, isfromControl, preventInputUpdate ) {
324 var imgToggle = false,
327 if ( $( this.handle ).parents().is( ".ui-toggle-switch" ) ) {
330 // NOTE: we don't return here because we want to support programmatic
331 // alteration of the input value, which should still update the slider
332 if ( this.options.disabled || this.element.attr('disabled')) {
336 // set the stored value for comparison later
337 this.value = this._value();
339 var control = this.element, percent,
340 cType = control[0].nodeName.toLowerCase(),
341 min = cType === "input" ? parseFloat( control.attr( "min" ) ) : 0,
342 max = cType === "input" ? parseFloat( control.attr( "max" ) ) : control.find( "option" ).length - 1,
343 step = ( cType === "input" && parseFloat( control.attr( "step" ) ) > 0 ) ? parseFloat( control.attr( "step" ) ) : 1;
345 if ( typeof val === "object" ) {
347 // a slight tolerance helped get to the ends of the slider
349 if ( !this.dragging ||
350 data.pageX < this.slider.offset().left - tol ||
351 data.pageX > this.slider.offset().left + this.slider.width() + tol ) {
354 percent = Math.round( ( ( data.pageX - this.slider.offset().left ) / this.slider.width() ) * 100 );
357 val = cType === "input" ? parseFloat( control.val() || 0 ) : control[0].selectedIndex;
359 percent = ( parseFloat( val ) - min ) / ( max - min ) * 100;
362 if ( isNaN( percent ) ) {
370 if ( percent > 100 ) {
374 var newval = ( percent / 100 ) * ( max - min ) + min;
376 //from jQuery UI slider, the following source will round to the nearest step
377 var valModStep = ( newval - min ) % step;
378 var alignValue = newval - valModStep;
380 if ( Math.abs( valModStep ) * 2 >= step ) {
381 alignValue += ( valModStep > 0 ) ? step : ( -step );
383 // Since JavaScript has problems with large floats, round
384 // the final value to 5 digits after the decimal point (see jQueryUI: #4124)
385 newval = parseFloat( alignValue.toFixed(5) );
387 if ( newval < min ) {
391 if ( newval > max ) {
395 this.handle.css( "left", percent + "%" );
397 "aria-valuenow": cType === "input" ? newval : control.find( "option" ).eq( newval ).attr( "value" ),
398 "aria-valuetext": cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText(),
399 title: cType === "input" ? newval : control.find( "option" ).eq( newval ).getEncodedText()
403 if ( this.valuebg ) {
404 this.valuebg.css( "width", percent + "%" );
408 // drag the label widths
410 appHandle = $( this.handle ).parents( ".ui-slider" );
411 if ( $( this.handle ).attr("aria-valuenow") === "on" || $( this.handle ).attr("aria-valuenow") === $( this.element ).children( "option:last" ).attr( "value" ) ) {
412 appHandle.children( "span.ui-slider-label-a" ).show();
413 appHandle.children( "span.ui-slider-label-b" ).hide();
415 appHandle.children( "span.ui-slider-label-b" ).show();
416 appHandle.children( "span.ui-slider-label-a" ).hide();
419 if ( this._labels ) {
420 var handlePercent = this.handle.width() / this.slider.width() * 100,
421 aPercent = percent && handlePercent + ( 100 - handlePercent ) * percent / 100,
422 bPercent = percent === 100 ? 0 : Math.min( handlePercent + 100 - aPercent, 100 );
424 this._labels.each(function() {
425 var ab = $( this ).is( ".ui-slider-label-a" );
426 $( this ).width( ( ab ? aPercent : bPercent ) + "%" );
431 if ( !preventInputUpdate ) {
432 var valueChanged = false;
434 // update control"s value
435 if ( cType === "input" ) {
436 valueChanged = control.val() !== newval;
437 control.val( newval );
439 valueChanged = control[ 0 ].selectedIndex !== newval;
440 control[ 0 ].selectedIndex = newval;
442 if ( !isfromControl && valueChanged ) {
443 control.trigger( "change" );
449 this.element.attr( "disabled", false );
450 this.slider.removeClass( "ui-disabled" ).attr( "aria-disabled", false );
451 return this._setOption( "disabled", false );
454 disable: function() {
455 this.element.attr( "disabled", true );
456 this.slider.addClass( "ui-disabled" ).attr( "aria-disabled", true );
457 return this._setOption( "disabled", true );
462 //auto self-init widgets
463 $.mobile.$document.bind( "pagecreate create", function( e ) {
464 $.mobile.slider.prototype.enhanceWithin( e.target, true );