spinner.js (14182B)
1 /*! 2 * jQuery UI Spinner 1.12.1 3 * http://jqueryui.com 4 * 5 * Copyright jQuery Foundation and other contributors 6 * Released under the MIT license. 7 * http://jquery.org/license 8 */ 9 10 //>>label: Spinner 11 //>>group: Widgets 12 //>>description: Displays buttons to easily input numbers via the keyboard or mouse. 13 //>>docs: http://api.jqueryui.com/spinner/ 14 //>>demos: http://jqueryui.com/spinner/ 15 //>>css.structure: ../../themes/base/core.css 16 //>>css.structure: ../../themes/base/spinner.css 17 //>>css.theme: ../../themes/base/theme.css 18 19 ( function( factory ) { 20 if ( typeof define === "function" && define.amd ) { 21 22 // AMD. Register as an anonymous module. 23 define( [ 24 "jquery", 25 "./button", 26 "./core" 27 ], factory ); 28 } else { 29 30 // Browser globals 31 factory( jQuery ); 32 } 33 }( function( $ ) { 34 35 function spinnerModifer( fn ) { 36 return function() { 37 var previous = this.element.val(); 38 fn.apply( this, arguments ); 39 this._refresh(); 40 if ( previous !== this.element.val() ) { 41 this._trigger( "change" ); 42 } 43 }; 44 } 45 46 $.widget( "ui.spinner", { 47 version: "1.12.1", 48 defaultElement: "<input>", 49 widgetEventPrefix: "spin", 50 options: { 51 classes: { 52 "ui-spinner": "ui-corner-all", 53 "ui-spinner-down": "ui-corner-br", 54 "ui-spinner-up": "ui-corner-tr" 55 }, 56 culture: null, 57 icons: { 58 down: "ui-icon-triangle-1-s", 59 up: "ui-icon-triangle-1-n" 60 }, 61 incremental: true, 62 max: null, 63 min: null, 64 numberFormat: null, 65 page: 10, 66 step: 1, 67 68 change: null, 69 spin: null, 70 start: null, 71 stop: null 72 }, 73 74 _create: function() { 75 76 // handle string values that need to be parsed 77 this._setOption( "max", this.options.max ); 78 this._setOption( "min", this.options.min ); 79 this._setOption( "step", this.options.step ); 80 81 // Only format if there is a value, prevents the field from being marked 82 // as invalid in Firefox, see #9573. 83 if ( this.value() !== "" ) { 84 85 // Format the value, but don't constrain. 86 this._value( this.element.val(), true ); 87 } 88 89 this._draw(); 90 this._on( this._events ); 91 this._refresh(); 92 93 // Turning off autocomplete prevents the browser from remembering the 94 // value when navigating through history, so we re-enable autocomplete 95 // if the page is unloaded before the widget is destroyed. #7790 96 this._on( this.window, { 97 beforeunload: function() { 98 this.element.removeAttr( "autocomplete" ); 99 } 100 } ); 101 }, 102 103 _getCreateOptions: function() { 104 var options = this._super(); 105 var element = this.element; 106 107 $.each( [ "min", "max", "step" ], function( i, option ) { 108 var value = element.attr( option ); 109 if ( value != null && value.length ) { 110 options[ option ] = value; 111 } 112 } ); 113 114 return options; 115 }, 116 117 _events: { 118 keydown: function( event ) { 119 if ( this._start( event ) && this._keydown( event ) ) { 120 event.preventDefault(); 121 } 122 }, 123 keyup: "_stop", 124 focus: function() { 125 this.previous = this.element.val(); 126 }, 127 blur: function( event ) { 128 if ( this.cancelBlur ) { 129 delete this.cancelBlur; 130 return; 131 } 132 133 this._stop(); 134 this._refresh(); 135 if ( this.previous !== this.element.val() ) { 136 this._trigger( "change", event ); 137 } 138 }, 139 mousewheel: function( event, delta ) { 140 if ( !delta ) { 141 return; 142 } 143 if ( !this.spinning && !this._start( event ) ) { 144 return false; 145 } 146 147 this._spin( ( delta > 0 ? 1 : -1 ) * this.options.step, event ); 148 clearTimeout( this.mousewheelTimer ); 149 this.mousewheelTimer = this._delay( function() { 150 if ( this.spinning ) { 151 this._stop( event ); 152 } 153 }, 100 ); 154 event.preventDefault(); 155 }, 156 "mousedown .ui-spinner-button": function( event ) { 157 var previous; 158 159 // We never want the buttons to have focus; whenever the user is 160 // interacting with the spinner, the focus should be on the input. 161 // If the input is focused then this.previous is properly set from 162 // when the input first received focus. If the input is not focused 163 // then we need to set this.previous based on the value before spinning. 164 previous = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ) ? 165 this.previous : this.element.val(); 166 function checkFocus() { 167 var isActive = this.element[ 0 ] === $.ui.safeActiveElement( this.document[ 0 ] ); 168 if ( !isActive ) { 169 this.element.trigger( "focus" ); 170 this.previous = previous; 171 172 // support: IE 173 // IE sets focus asynchronously, so we need to check if focus 174 // moved off of the input because the user clicked on the button. 175 this._delay( function() { 176 this.previous = previous; 177 } ); 178 } 179 } 180 181 // Ensure focus is on (or stays on) the text field 182 event.preventDefault(); 183 checkFocus.call( this ); 184 185 // Support: IE 186 // IE doesn't prevent moving focus even with event.preventDefault() 187 // so we set a flag to know when we should ignore the blur event 188 // and check (again) if focus moved off of the input. 189 this.cancelBlur = true; 190 this._delay( function() { 191 delete this.cancelBlur; 192 checkFocus.call( this ); 193 } ); 194 195 if ( this._start( event ) === false ) { 196 return; 197 } 198 199 this._repeat( null, $( event.currentTarget ) 200 .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); 201 }, 202 "mouseup .ui-spinner-button": "_stop", 203 "mouseenter .ui-spinner-button": function( event ) { 204 205 // button will add ui-state-active if mouse was down while mouseleave and kept down 206 if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { 207 return; 208 } 209 210 if ( this._start( event ) === false ) { 211 return false; 212 } 213 this._repeat( null, $( event.currentTarget ) 214 .hasClass( "ui-spinner-up" ) ? 1 : -1, event ); 215 }, 216 217 // TODO: do we really want to consider this a stop? 218 // shouldn't we just stop the repeater and wait until mouseup before 219 // we trigger the stop event? 220 "mouseleave .ui-spinner-button": "_stop" 221 }, 222 223 // Support mobile enhanced option and make backcompat more sane 224 _enhance: function() { 225 this.uiSpinner = this.element 226 .attr( "autocomplete", "off" ) 227 .wrap( "<span>" ) 228 .parent() 229 230 // Add buttons 231 .append( 232 "<a></a><a></a>" 233 ); 234 }, 235 236 _draw: function() { 237 this._enhance(); 238 239 this._addClass( this.uiSpinner, "ui-spinner", "ui-widget ui-widget-content" ); 240 this._addClass( "ui-spinner-input" ); 241 242 this.element.attr( "role", "spinbutton" ); 243 244 // Button bindings 245 this.buttons = this.uiSpinner.children( "a" ) 246 .attr( "tabIndex", -1 ) 247 .attr( "aria-hidden", true ) 248 .button( { 249 classes: { 250 "ui-button": "" 251 } 252 } ); 253 254 // TODO: Right now button does not support classes this is already updated in button PR 255 this._removeClass( this.buttons, "ui-corner-all" ); 256 257 this._addClass( this.buttons.first(), "ui-spinner-button ui-spinner-up" ); 258 this._addClass( this.buttons.last(), "ui-spinner-button ui-spinner-down" ); 259 this.buttons.first().button( { 260 "icon": this.options.icons.up, 261 "showLabel": false 262 } ); 263 this.buttons.last().button( { 264 "icon": this.options.icons.down, 265 "showLabel": false 266 } ); 267 268 // IE 6 doesn't understand height: 50% for the buttons 269 // unless the wrapper has an explicit height 270 if ( this.buttons.height() > Math.ceil( this.uiSpinner.height() * 0.5 ) && 271 this.uiSpinner.height() > 0 ) { 272 this.uiSpinner.height( this.uiSpinner.height() ); 273 } 274 }, 275 276 _keydown: function( event ) { 277 var options = this.options, 278 keyCode = $.ui.keyCode; 279 280 switch ( event.keyCode ) { 281 case keyCode.UP: 282 this._repeat( null, 1, event ); 283 return true; 284 case keyCode.DOWN: 285 this._repeat( null, -1, event ); 286 return true; 287 case keyCode.PAGE_UP: 288 this._repeat( null, options.page, event ); 289 return true; 290 case keyCode.PAGE_DOWN: 291 this._repeat( null, -options.page, event ); 292 return true; 293 } 294 295 return false; 296 }, 297 298 _start: function( event ) { 299 if ( !this.spinning && this._trigger( "start", event ) === false ) { 300 return false; 301 } 302 303 if ( !this.counter ) { 304 this.counter = 1; 305 } 306 this.spinning = true; 307 return true; 308 }, 309 310 _repeat: function( i, steps, event ) { 311 i = i || 500; 312 313 clearTimeout( this.timer ); 314 this.timer = this._delay( function() { 315 this._repeat( 40, steps, event ); 316 }, i ); 317 318 this._spin( steps * this.options.step, event ); 319 }, 320 321 _spin: function( step, event ) { 322 var value = this.value() || 0; 323 324 if ( !this.counter ) { 325 this.counter = 1; 326 } 327 328 value = this._adjustValue( value + step * this._increment( this.counter ) ); 329 330 if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false ) { 331 this._value( value ); 332 this.counter++; 333 } 334 }, 335 336 _increment: function( i ) { 337 var incremental = this.options.incremental; 338 339 if ( incremental ) { 340 return $.isFunction( incremental ) ? 341 incremental( i ) : 342 Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 ); 343 } 344 345 return 1; 346 }, 347 348 _precision: function() { 349 var precision = this._precisionOf( this.options.step ); 350 if ( this.options.min !== null ) { 351 precision = Math.max( precision, this._precisionOf( this.options.min ) ); 352 } 353 return precision; 354 }, 355 356 _precisionOf: function( num ) { 357 var str = num.toString(), 358 decimal = str.indexOf( "." ); 359 return decimal === -1 ? 0 : str.length - decimal - 1; 360 }, 361 362 _adjustValue: function( value ) { 363 var base, aboveMin, 364 options = this.options; 365 366 // Make sure we're at a valid step 367 // - find out where we are relative to the base (min or 0) 368 base = options.min !== null ? options.min : 0; 369 aboveMin = value - base; 370 371 // - round to the nearest step 372 aboveMin = Math.round( aboveMin / options.step ) * options.step; 373 374 // - rounding is based on 0, so adjust back to our base 375 value = base + aboveMin; 376 377 // Fix precision from bad JS floating point math 378 value = parseFloat( value.toFixed( this._precision() ) ); 379 380 // Clamp the value 381 if ( options.max !== null && value > options.max ) { 382 return options.max; 383 } 384 if ( options.min !== null && value < options.min ) { 385 return options.min; 386 } 387 388 return value; 389 }, 390 391 _stop: function( event ) { 392 if ( !this.spinning ) { 393 return; 394 } 395 396 clearTimeout( this.timer ); 397 clearTimeout( this.mousewheelTimer ); 398 this.counter = 0; 399 this.spinning = false; 400 this._trigger( "stop", event ); 401 }, 402 403 _setOption: function( key, value ) { 404 var prevValue, first, last; 405 406 if ( key === "culture" || key === "numberFormat" ) { 407 prevValue = this._parse( this.element.val() ); 408 this.options[ key ] = value; 409 this.element.val( this._format( prevValue ) ); 410 return; 411 } 412 413 if ( key === "max" || key === "min" || key === "step" ) { 414 if ( typeof value === "string" ) { 415 value = this._parse( value ); 416 } 417 } 418 if ( key === "icons" ) { 419 first = this.buttons.first().find( ".ui-icon" ); 420 this._removeClass( first, null, this.options.icons.up ); 421 this._addClass( first, null, value.up ); 422 last = this.buttons.last().find( ".ui-icon" ); 423 this._removeClass( last, null, this.options.icons.down ); 424 this._addClass( last, null, value.down ); 425 } 426 427 this._super( key, value ); 428 }, 429 430 _setOptionDisabled: function( value ) { 431 this._super( value ); 432 433 this._toggleClass( this.uiSpinner, null, "ui-state-disabled", !!value ); 434 this.element.prop( "disabled", !!value ); 435 this.buttons.button( value ? "disable" : "enable" ); 436 }, 437 438 _setOptions: spinnerModifer( function( options ) { 439 this._super( options ); 440 } ), 441 442 _parse: function( val ) { 443 if ( typeof val === "string" && val !== "" ) { 444 val = window.Globalize && this.options.numberFormat ? 445 Globalize.parseFloat( val, 10, this.options.culture ) : +val; 446 } 447 return val === "" || isNaN( val ) ? null : val; 448 }, 449 450 _format: function( value ) { 451 if ( value === "" ) { 452 return ""; 453 } 454 return window.Globalize && this.options.numberFormat ? 455 Globalize.format( value, this.options.numberFormat, this.options.culture ) : 456 value; 457 }, 458 459 _refresh: function() { 460 this.element.attr( { 461 "aria-valuemin": this.options.min, 462 "aria-valuemax": this.options.max, 463 464 // TODO: what should we do with values that can't be parsed? 465 "aria-valuenow": this._parse( this.element.val() ) 466 } ); 467 }, 468 469 isValid: function() { 470 var value = this.value(); 471 472 // Null is invalid 473 if ( value === null ) { 474 return false; 475 } 476 477 // If value gets adjusted, it's invalid 478 return value === this._adjustValue( value ); 479 }, 480 481 // Update the value without triggering change 482 _value: function( value, allowAny ) { 483 var parsed; 484 if ( value !== "" ) { 485 parsed = this._parse( value ); 486 if ( parsed !== null ) { 487 if ( !allowAny ) { 488 parsed = this._adjustValue( parsed ); 489 } 490 value = this._format( parsed ); 491 } 492 } 493 this.element.val( value ); 494 this._refresh(); 495 }, 496 497 _destroy: function() { 498 this.element 499 .prop( "disabled", false ) 500 .removeAttr( "autocomplete role aria-valuemin aria-valuemax aria-valuenow" ); 501 502 this.uiSpinner.replaceWith( this.element ); 503 }, 504 505 stepUp: spinnerModifer( function( steps ) { 506 this._stepUp( steps ); 507 } ), 508 _stepUp: function( steps ) { 509 if ( this._start() ) { 510 this._spin( ( steps || 1 ) * this.options.step ); 511 this._stop(); 512 } 513 }, 514 515 stepDown: spinnerModifer( function( steps ) { 516 this._stepDown( steps ); 517 } ), 518 _stepDown: function( steps ) { 519 if ( this._start() ) { 520 this._spin( ( steps || 1 ) * -this.options.step ); 521 this._stop(); 522 } 523 }, 524 525 pageUp: spinnerModifer( function( pages ) { 526 this._stepUp( ( pages || 1 ) * this.options.page ); 527 } ), 528 529 pageDown: spinnerModifer( function( pages ) { 530 this._stepDown( ( pages || 1 ) * this.options.page ); 531 } ), 532 533 value: function( newVal ) { 534 if ( !arguments.length ) { 535 return this._parse( this.element.val() ); 536 } 537 spinnerModifer( this._value ).call( this, newVal ); 538 }, 539 540 widget: function() { 541 return this.uiSpinner; 542 } 543 } ); 544 545 // DEPRECATED 546 // TODO: switch return back to widget declaration at top of file when this is removed 547 if ( $.uiBackCompat !== false ) { 548 549 // Backcompat for spinner html extension points 550 $.widget( "ui.spinner", $.ui.spinner, { 551 _enhance: function() { 552 this.uiSpinner = this.element 553 .attr( "autocomplete", "off" ) 554 .wrap( this._uiSpinnerHtml() ) 555 .parent() 556 557 // Add buttons 558 .append( this._buttonHtml() ); 559 }, 560 _uiSpinnerHtml: function() { 561 return "<span>"; 562 }, 563 564 _buttonHtml: function() { 565 return "<a></a><a></a>"; 566 } 567 } ); 568 } 569 570 return $.ui.spinner; 571 572 } ) );