spinner.js 13 KB

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