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