ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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 } ) );