You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

579 lines
14 KiB

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