jquery.ui.spinner.js 12 KB

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