spinner.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. /*!
  2. * jQuery UI Spinner 1.11.2
  3. * http://jqueryui.com
  4. *
  5. * Copyright 2014 jQuery Foundation and other contributors
  6. * Released under the MIT license.
  7. * http://jquery.org/license
  8. *
  9. * http://api.jqueryui.com/spinner/
  10. */
  11. (function( factory ) {
  12. if ( typeof define === "function" && define.amd ) {
  13. // AMD. Register as an anonymous module.
  14. define([
  15. "jquery",
  16. "./core",
  17. "./widget",
  18. "./button"
  19. ], factory );
  20. } else {
  21. // Browser globals
  22. factory( jQuery );
  23. }
  24. }(function( $ ) {
  25. function spinner_modifier( fn ) {
  26. return function() {
  27. var previous = this.element.val();
  28. fn.apply( this, arguments );
  29. this._refresh();
  30. if ( previous !== this.element.val() ) {
  31. this._trigger( "change" );
  32. }
  33. };
  34. }
  35. return $.widget( "ui.spinner", {
  36. version: "1.11.2",
  37. defaultElement: "<input>",
  38. widgetEventPrefix: "spin",
  39. options: {
  40. culture: null,
  41. icons: {
  42. down: "ui-icon-triangle-1-s",
  43. up: "ui-icon-triangle-1-n"
  44. },
  45. incremental: true,
  46. max: null,
  47. min: null,
  48. numberFormat: null,
  49. page: 10,
  50. step: 1,
  51. change: null,
  52. spin: null,
  53. start: null,
  54. stop: null
  55. },
  56. _create: function() {
  57. // handle string values that need to be parsed
  58. this._setOption( "max", this.options.max );
  59. this._setOption( "min", this.options.min );
  60. this._setOption( "step", this.options.step );
  61. // Only format if there is a value, prevents the field from being marked
  62. // as invalid in Firefox, see #9573.
  63. if ( this.value() !== "" ) {
  64. // Format the value, but don't constrain.
  65. this._value( this.element.val(), true );
  66. }
  67. this._draw();
  68. this._on( this._events );
  69. this._refresh();
  70. // turning off autocomplete prevents the browser from remembering the
  71. // value when navigating through history, so we re-enable autocomplete
  72. // if the page is unloaded before the widget is destroyed. #7790
  73. this._on( this.window, {
  74. beforeunload: function() {
  75. this.element.removeAttr( "autocomplete" );
  76. }
  77. });
  78. },
  79. _getCreateOptions: function() {
  80. var options = {},
  81. element = this.element;
  82. $.each( [ "min", "max", "step" ], function( i, option ) {
  83. var value = element.attr( option );
  84. if ( value !== undefined && value.length ) {
  85. options[ option ] = value;
  86. }
  87. });
  88. return options;
  89. },
  90. _events: {
  91. keydown: function( event ) {
  92. if ( this._start( event ) && this._keydown( event ) ) {
  93. event.preventDefault();
  94. }
  95. },
  96. keyup: "_stop",
  97. focus: function() {
  98. this.previous = this.element.val();
  99. },
  100. blur: function( event ) {
  101. if ( this.cancelBlur ) {
  102. delete this.cancelBlur;
  103. return;
  104. }
  105. this._stop();
  106. this._refresh();
  107. if ( this.previous !== this.element.val() ) {
  108. this._trigger( "change", event );
  109. }
  110. },
  111. mousewheel: function( event, delta ) {
  112. if ( !delta ) {
  113. return;
  114. }
  115. if ( !this.spinning && !this._start( event ) ) {
  116. return false;
  117. }
  118. this._spin( (delta > 0 ? 1 : -1) * this.options.step, event );
  119. clearTimeout( this.mousewheelTimer );
  120. this.mousewheelTimer = this._delay(function() {
  121. if ( this.spinning ) {
  122. this._stop( event );
  123. }
  124. }, 100 );
  125. event.preventDefault();
  126. },
  127. "mousedown .ui-spinner-button": function( event ) {
  128. var previous;
  129. // We never want the buttons to have focus; whenever the user is
  130. // interacting with the spinner, the focus should be on the input.
  131. // If the input is focused then this.previous is properly set from
  132. // when the input first received focus. If the input is not focused
  133. // then we need to set this.previous based on the value before spinning.
  134. previous = this.element[0] === this.document[0].activeElement ?
  135. this.previous : this.element.val();
  136. function checkFocus() {
  137. var isActive = this.element[0] === this.document[0].activeElement;
  138. if ( !isActive ) {
  139. this.element.focus();
  140. this.previous = previous;
  141. // support: IE
  142. // IE sets focus asynchronously, so we need to check if focus
  143. // moved off of the input because the user clicked on the button.
  144. this._delay(function() {
  145. this.previous = previous;
  146. });
  147. }
  148. }
  149. // ensure focus is on (or stays on) the text field
  150. event.preventDefault();
  151. checkFocus.call( this );
  152. // support: IE
  153. // IE doesn't prevent moving focus even with event.preventDefault()
  154. // so we set a flag to know when we should ignore the blur event
  155. // and check (again) if focus moved off of the input.
  156. this.cancelBlur = true;
  157. this._delay(function() {
  158. delete this.cancelBlur;
  159. checkFocus.call( this );
  160. });
  161. if ( this._start( event ) === false ) {
  162. return;
  163. }
  164. this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event );
  165. },
  166. "mouseup .ui-spinner-button": "_stop",
  167. "mouseenter .ui-spinner-button": function( event ) {
  168. // button will add ui-state-active if mouse was down while mouseleave and kept down
  169. if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) {
  170. return;
  171. }
  172. if ( this._start( event ) === false ) {
  173. return false;
  174. }
  175. this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event );
  176. },
  177. // TODO: do we really want to consider this a stop?
  178. // shouldn't we just stop the repeater and wait until mouseup before
  179. // we trigger the stop event?
  180. "mouseleave .ui-spinner-button": "_stop"
  181. },
  182. _draw: function() {
  183. var uiSpinner = this.uiSpinner = this.element
  184. .addClass( "ui-spinner-input" )
  185. .attr( "autocomplete", "off" )
  186. .wrap( this._uiSpinnerHtml() )
  187. .parent()
  188. // add buttons
  189. .append( this._buttonHtml() );
  190. this.element.attr( "role", "spinbutton" );
  191. // button bindings
  192. this.buttons = uiSpinner.find( ".ui-spinner-button" )
  193. .attr( "tabIndex", -1 )
  194. .button()
  195. .removeClass( "ui-corner-all" );
  196. // IE 6 doesn't understand height: 50% for the buttons
  197. // unless the wrapper has an explicit height
  198. if ( this.buttons.height() > Math.ceil( uiSpinner.height() * 0.5 ) &&
  199. uiSpinner.height() > 0 ) {
  200. uiSpinner.height( uiSpinner.height() );
  201. }
  202. // disable spinner if element was already disabled
  203. if ( this.options.disabled ) {
  204. this.disable();
  205. }
  206. },
  207. _keydown: function( event ) {
  208. var options = this.options,
  209. keyCode = $.ui.keyCode;
  210. switch ( event.keyCode ) {
  211. case keyCode.UP:
  212. this._repeat( null, 1, event );
  213. return true;
  214. case keyCode.DOWN:
  215. this._repeat( null, -1, event );
  216. return true;
  217. case keyCode.PAGE_UP:
  218. this._repeat( null, options.page, event );
  219. return true;
  220. case keyCode.PAGE_DOWN:
  221. this._repeat( null, -options.page, event );
  222. return true;
  223. }
  224. return false;
  225. },
  226. _uiSpinnerHtml: function() {
  227. return "<span class='ui-spinner ui-widget ui-widget-content ui-corner-all'></span>";
  228. },
  229. _buttonHtml: function() {
  230. return "" +
  231. "<a class='ui-spinner-button ui-spinner-up ui-corner-tr'>" +
  232. "<span class='ui-icon " + this.options.icons.up + "'>&#9650;</span>" +
  233. "</a>" +
  234. "<a class='ui-spinner-button ui-spinner-down ui-corner-br'>" +
  235. "<span class='ui-icon " + this.options.icons.down + "'>&#9660;</span>" +
  236. "</a>";
  237. },
  238. _start: function( event ) {
  239. if ( !this.spinning && this._trigger( "start", event ) === false ) {
  240. return false;
  241. }
  242. if ( !this.counter ) {
  243. this.counter = 1;
  244. }
  245. this.spinning = true;
  246. return true;
  247. },
  248. _repeat: function( i, steps, event ) {
  249. i = i || 500;
  250. clearTimeout( this.timer );
  251. this.timer = this._delay(function() {
  252. this._repeat( 40, steps, event );
  253. }, i );
  254. this._spin( steps * this.options.step, event );
  255. },
  256. _spin: function( step, event ) {
  257. var value = this.value() || 0;
  258. if ( !this.counter ) {
  259. this.counter = 1;
  260. }
  261. value = this._adjustValue( value + step * this._increment( this.counter ) );
  262. if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false) {
  263. this._value( value );
  264. this.counter++;
  265. }
  266. },
  267. _increment: function( i ) {
  268. var incremental = this.options.incremental;
  269. if ( incremental ) {
  270. return $.isFunction( incremental ) ?
  271. incremental( i ) :
  272. Math.floor( i * i * i / 50000 - i * i / 500 + 17 * i / 200 + 1 );
  273. }
  274. return 1;
  275. },
  276. _precision: function() {
  277. var precision = this._precisionOf( this.options.step );
  278. if ( this.options.min !== null ) {
  279. precision = Math.max( precision, this._precisionOf( this.options.min ) );
  280. }
  281. return precision;
  282. },
  283. _precisionOf: function( num ) {
  284. var str = num.toString(),
  285. decimal = str.indexOf( "." );
  286. return decimal === -1 ? 0 : str.length - decimal - 1;
  287. },
  288. _adjustValue: function( value ) {
  289. var base, aboveMin,
  290. options = this.options;
  291. // make sure we're at a valid step
  292. // - find out where we are relative to the base (min or 0)
  293. base = options.min !== null ? options.min : 0;
  294. aboveMin = value - base;
  295. // - round to the nearest step
  296. aboveMin = Math.round(aboveMin / options.step) * options.step;
  297. // - rounding is based on 0, so adjust back to our base
  298. value = base + aboveMin;
  299. // fix precision from bad JS floating point math
  300. value = parseFloat( value.toFixed( this._precision() ) );
  301. // clamp the value
  302. if ( options.max !== null && value > options.max) {
  303. return options.max;
  304. }
  305. if ( options.min !== null && value < options.min ) {
  306. return options.min;
  307. }
  308. return value;
  309. },
  310. _stop: function( event ) {
  311. if ( !this.spinning ) {
  312. return;
  313. }
  314. clearTimeout( this.timer );
  315. clearTimeout( this.mousewheelTimer );
  316. this.counter = 0;
  317. this.spinning = false;
  318. this._trigger( "stop", event );
  319. },
  320. _setOption: function( key, value ) {
  321. if ( key === "culture" || key === "numberFormat" ) {
  322. var prevValue = this._parse( this.element.val() );
  323. this.options[ key ] = value;
  324. this.element.val( this._format( prevValue ) );
  325. return;
  326. }
  327. if ( key === "max" || key === "min" || key === "step" ) {
  328. if ( typeof value === "string" ) {
  329. value = this._parse( value );
  330. }
  331. }
  332. if ( key === "icons" ) {
  333. this.buttons.first().find( ".ui-icon" )
  334. .removeClass( this.options.icons.up )
  335. .addClass( value.up );
  336. this.buttons.last().find( ".ui-icon" )
  337. .removeClass( this.options.icons.down )
  338. .addClass( value.down );
  339. }
  340. this._super( key, value );
  341. if ( key === "disabled" ) {
  342. this.widget().toggleClass( "ui-state-disabled", !!value );
  343. this.element.prop( "disabled", !!value );
  344. this.buttons.button( value ? "disable" : "enable" );
  345. }
  346. },
  347. _setOptions: spinner_modifier(function( options ) {
  348. this._super( options );
  349. }),
  350. _parse: function( val ) {
  351. if ( typeof val === "string" && val !== "" ) {
  352. val = window.Globalize && this.options.numberFormat ?
  353. Globalize.parseFloat( val, 10, this.options.culture ) : +val;
  354. }
  355. return val === "" || isNaN( val ) ? null : val;
  356. },
  357. _format: function( value ) {
  358. if ( value === "" ) {
  359. return "";
  360. }
  361. return window.Globalize && this.options.numberFormat ?
  362. Globalize.format( value, this.options.numberFormat, this.options.culture ) :
  363. value;
  364. },
  365. _refresh: function() {
  366. this.element.attr({
  367. "aria-valuemin": this.options.min,
  368. "aria-valuemax": this.options.max,
  369. // TODO: what should we do with values that can't be parsed?
  370. "aria-valuenow": this._parse( this.element.val() )
  371. });
  372. },
  373. isValid: function() {
  374. var value = this.value();
  375. // null is invalid
  376. if ( value === null ) {
  377. return false;
  378. }
  379. // if value gets adjusted, it's invalid
  380. return value === this._adjustValue( value );
  381. },
  382. // update the value without triggering change
  383. _value: function( value, allowAny ) {
  384. var parsed;
  385. if ( value !== "" ) {
  386. parsed = this._parse( value );
  387. if ( parsed !== null ) {
  388. if ( !allowAny ) {
  389. parsed = this._adjustValue( parsed );
  390. }
  391. value = this._format( parsed );
  392. }
  393. }
  394. this.element.val( value );
  395. this._refresh();
  396. },
  397. _destroy: function() {
  398. this.element
  399. .removeClass( "ui-spinner-input" )
  400. .prop( "disabled", false )
  401. .removeAttr( "autocomplete" )
  402. .removeAttr( "role" )
  403. .removeAttr( "aria-valuemin" )
  404. .removeAttr( "aria-valuemax" )
  405. .removeAttr( "aria-valuenow" );
  406. this.uiSpinner.replaceWith( this.element );
  407. },
  408. stepUp: spinner_modifier(function( steps ) {
  409. this._stepUp( steps );
  410. }),
  411. _stepUp: function( steps ) {
  412. if ( this._start() ) {
  413. this._spin( (steps || 1) * this.options.step );
  414. this._stop();
  415. }
  416. },
  417. stepDown: spinner_modifier(function( steps ) {
  418. this._stepDown( steps );
  419. }),
  420. _stepDown: function( steps ) {
  421. if ( this._start() ) {
  422. this._spin( (steps || 1) * -this.options.step );
  423. this._stop();
  424. }
  425. },
  426. pageUp: spinner_modifier(function( pages ) {
  427. this._stepUp( (pages || 1) * this.options.page );
  428. }),
  429. pageDown: spinner_modifier(function( pages ) {
  430. this._stepDown( (pages || 1) * this.options.page );
  431. }),
  432. value: function( newVal ) {
  433. if ( !arguments.length ) {
  434. return this._parse( this.element.val() );
  435. }
  436. spinner_modifier( this._value ).call( this, newVal );
  437. },
  438. widget: function() {
  439. return this.uiSpinner;
  440. }
  441. });
  442. }));