Sin descripción

spinner.js 14KB

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