├── README ├── example └── example.html ├── ui.spinner.css ├── ui.spinner.js └── ui.spinner.min.js /README: -------------------------------------------------------------------------------- 1 | jQuery.ui.spinner 1.20 2 | jQuery.ui extension for a numeric spinner 3 | 4 | Licensing 5 | ---------- 6 | 7 | Written by Brant Burnett 8 | Copyright (c) 2009-2010 Brant Burnett 9 | Dual licensed under the MIT or GPL Version 2 licenses. 10 | 11 | jQuery.ui.spinner is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Lesser General Public License for more details. 15 | 16 | Source Control 17 | --------------- 18 | 19 | The root branch for jQuery.ui.spinner is hosted on GitHub at 20 | . 21 | 22 | Overview 23 | --------- 24 | 25 | jQuery.ui.spinner is based upon jQuery UI 1.8 , and provides a new 26 | spinner widget that can be added to any text box. It fully uses the theme settings created by the 27 | ThemeRoller, and has been tested for compatibility with IE 6/7/8, Firefox 2/3, Safari 3.1, and 28 | Opera 9. Supports both mouse and keyboard input, and validates input directly into the text box. 29 | It requires only minimal CSS to be supplied, mostly relying on the CSS built into the jQuery UI 30 | framework. 31 | 32 | I did a lot of experimenting to try to find the best method that would work across a variety of 33 | web browswers while still using the jQuery UI theme framework rather than images. I finally 34 | settled on this method of spans with display: inline-block set on them, which is then 35 | positioned next to the text box using absolute positioning, relative to an outer wrapper that 36 | is relative positioned. The only restriction I know of with this system is that if you put 37 | a right margin on the text box, you must specify it in pixels. 38 | 39 | To use, add the CSS found in ui.spinner.css to your jQuery UI CSS files, or link to it 40 | separately. Then include the ui.spinner.js or ui.spinner.min.js script file beneath your 41 | include for jQuery UI. Finally, during or after $(document).ready call $("#myinput").spinner() 42 | to add the spinner to your text box. 43 | 44 | Options 45 | -------- 46 | 47 | The spinner also support a variety of options which can be passed in an object to the 48 | constructor. Page wide defaults can be assigned to $.ui.spinner.defaults. 49 | 50 | min (float) 51 | Minimum allowed value of the spinner. If left with the default, null, there will be no 52 | minimum unless the input has a maximum length. Then the minimum will be calculated. For 53 | a textbox with a maximum length of 3 characters, the minimum will be -99, etc. 54 | 55 | max (float) 56 | Maximum allowed value of the spinner. If left with the default, null, there will be no 57 | maximum unless the input has a maximum length. Then the maximum will be calculated. For 58 | a textbox with a maximum length of 3 characters, the maximum will be 999, etc. 59 | 60 | places (integer) 61 | Number of decimal places to display. Defaults to use the number of places found in step. 62 | 63 | step (float) 64 | A positive number that the spinner should be incremented by when up is clicked or pressed. 65 | The spinner will be decremented by this amount for down. Defaults to 1. 66 | 67 | largeStep (float) 68 | Used like step, but for when page up or page down are pressed to jump in larger 69 | amounts. Defaults to 10. 70 | 71 | group (string) 72 | Grouping separator. Would commonly be set to ',', defaults to ''. 73 | 74 | point (string) 75 | Decimal point character. Defaults to '.'. 76 | 77 | prefix (string) 78 | Character prefix before the number. Commonly used for currency symbols. Defaults to ''. 79 | 80 | suffix (string) 81 | Character suffix after the number. Commonly used for percentage signs. Defaults to ''. 82 | 83 | className (string) 84 | Optional class name that should be applied to the container span that is created around 85 | the input and the buttons 86 | 87 | showOn (string) 88 | Defines when the spin buttons are visible 89 | 'always': Spin buttons are always visible 90 | 'focus': Spin buttons are only visible when the input has focus 91 | 'hover': Spin buttons are only visible when the mouse is hovering over the input 92 | 'both': Spin buttons are visible when the input has focus or is being hovered over 93 | 94 | width (integer) 95 | Width in pixels of the spinner. Defaults to 16. 96 | 97 | increment (array or string) 98 | Controls the speed of the incremental spin when you hold a button or key down. Can be set 99 | to 'fast' or 'slow' to use predefined speeds. However, you can also supply an array of 100 | objects to control the speed increments, or null to stop continuous spinning. The default 101 | is 'slow'. The objects in the array must have the following properties: 102 | 103 | count (integer) 104 | Number of times to increment at this speed before moving to the next speed. 105 | 106 | mult (integer) 107 | Number to multiply the current step by. This increases the amount of each increment. 108 | 109 | delay (integer) 110 | Number of milliseconds to delay between each increment. 111 | 112 | mouseWheel (boolean) 113 | If true then mouse wheel events will be attached. Defaults to true. 114 | 115 | allowNull (boolean) 116 | If true then the control will allow a blank (null) value. Defaults to false. 117 | 118 | format ( function(num, places, element) ) 119 | Function that returns a formatted number. By default formats using the prefix, suffix, 120 | places, and point options. Note that this may be called during initialization before 121 | the spinner is fully constructed. 122 | 123 | this 124 | options object, should be treated as read-only 125 | 126 | num (float) 127 | Number to be formatted 128 | 129 | places (integer) 130 | Number of decimal places to display 131 | 132 | element (jQuery) 133 | input element 134 | 135 | 136 | parse ( function(num, element) ) 137 | Function that returns a number parsed from an input string. Will only be called if the 138 | input is non-null. By default parses using the prefix, suffix, and point options. 139 | 140 | this 141 | options object, should be treated as read-only 142 | 143 | num (string) 144 | Number to be parsed 145 | 146 | element (jQuery) 147 | input element 148 | 149 | Example Option Usage 150 | --------------------- 151 | 152 | 0 - 100 incrementing by 2: 153 | $("#myinput").spinner({min: 0, max: 100, increment: 2}); 154 | 155 | Show on hover/focus only: 156 | $("#myinput").spinner({showOn: 'both'}); 157 | 158 | Other Commands 159 | --------------- 160 | 161 | The spinner also support commands after they are created, using the standard UI widget method 162 | of passing strings. 163 | 164 | $("#myinput").spinner("value") 165 | Returns the integer value of the input. Returns null if the input is blank. 166 | 167 | $("#myinput").spinner("value", value) 168 | Sets the integer value of the input. Still validates against min and max, null blanks the 169 | input if nulls are allowed. 170 | 171 | $("#myinput").spinner("enable") 172 | Enables the spinner and the input 173 | 174 | $("#myinput").spinner("disable") 175 | Disables the spinner and the input 176 | 177 | $("#myinput").spinner("destroy") 178 | Destroys the spinner, restoring the input to its previous state 179 | 180 | $("#myinput").spinner("increment") 181 | Increments the spinner, just like you hit the button 182 | 183 | $("#myinput").spinner("decrement") 184 | Decrements the spinner, just like you hit the button 185 | 186 | $("#myinput").spinner("showButtons", [immediate]) 187 | Shows the buttons if they are hidden. If immediate is passed as true, the show won't be 188 | animated. 189 | 190 | $("#myinput").spinner("hideButtons", [immediate]) 191 | Hides the buttons if they are visible. If immediate is passed as true, the hide won't be 192 | animated. 193 | 194 | Version History 195 | ---------------- 196 | 197 | 1.20 198 | Updated for the UI 1.8 widget factory 199 | 200 | 1.10 201 | Added support for mouse wheel 202 | Added keypress filtering for numbers, special keys only 203 | Fixed input always focusing on spinner init (caused error in IE) 204 | Added event namespacing to improve destroy 205 | Added allowNull option 206 | Renamed increment option to step 207 | Will now load HTML5 min, max, and step attributes 208 | Now sets the maxlength of the input based on min and max 209 | Changed incremental speed increase to use an array, or be passed 'slow' and 'fast' options 210 | Changing options after the spinner is created now works 211 | Added support for decimal places and currency prefix/suffixes 212 | Added support for custom formatting 213 | 214 | 1.01 215 | Renamed from spinbuttons to spinner 216 | Implemented right/left and home/end keys per the DHTML Style Guide 217 | Fixed destroy implementation 218 | Made setValue call change event on input 219 | 220 | 1.0 221 | Initial release -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | jQuery UI Spinner Example 4 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 46 | 47 | 48 |
49 |

This is a simple example/test page for my jQuery UI 1.7 Spinner Widget v1.10. You can find the current release version online at GitHub.

50 | 51 |
52 | 53 |

Here is an example of a spinner based editor:

54 | 55 |

Here is a fast spinner:

56 | 57 |

Here is an example of one that hides: with some text after it

58 | 59 |

Here is an example of one that allows nulls:

60 | 61 |

Here is a disabled example:

62 | 63 |

Here is an example with maxlength:

64 | 65 |

Here is an example with HTML5 attributes:

66 | 67 |

Here is a currency spinner:

68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 83 | 84 | 85 |
Configurable spinner:
Maximum:
Minimum:
Step:
Speed:
Mouse Wheel:
86 |
87 | 88 | -------------------------------------------------------------------------------- /ui.spinner.css: -------------------------------------------------------------------------------- 1 | .ui-spinner {position: relative; border: 0px solid white; } 2 | .ui-spinner-buttons {position: absolute} 3 | .ui-spinner-button {overflow: hidden} 4 | -------------------------------------------------------------------------------- /ui.spinner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license jQuery UI Spinner 1.20 3 | * 4 | * Copyright (c) 2009-2010 Brant Burnett 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | */ 7 | (function($, undefined) { 8 | 9 | var 10 | // constants 11 | active = 'ui-state-active', 12 | hover = 'ui-state-hover', 13 | disabled = 'ui-state-disabled', 14 | 15 | keyCode = $.ui.keyCode, 16 | up = keyCode.UP, 17 | down = keyCode.DOWN, 18 | right = keyCode.RIGHT, 19 | left = keyCode.LEFT, 20 | pageUp = keyCode.PAGE_UP, 21 | pageDown = keyCode.PAGE_DOWN, 22 | home = keyCode.HOME, 23 | end = keyCode.END, 24 | 25 | msie = $.browser.msie, 26 | mouseWheelEventName = $.browser.mozilla ? 'DOMMouseScroll' : 'mousewheel', 27 | 28 | // namespace for events on input 29 | eventNamespace = '.uispinner', 30 | 31 | // only these special keys will be accepted, all others will be ignored unless CTRL or ALT are pressed 32 | validKeys = [up, down, right, left, pageUp, pageDown, home, end, keyCode.BACKSPACE, keyCode.DELETE, keyCode.TAB], 33 | 34 | // stores the currently focused spinner 35 | // Note: due to oddities in the focus/blur events, this is part of a two-part system for confirming focus 36 | // this must set to the control, and the focus variable must be true 37 | // this is because hitting up/down arrows with mouse causes focus to change, but blur event for previous control doesn't fire 38 | focusCtrl; 39 | 40 | $.widget('ui.spinner', { 41 | options: { 42 | min: null, 43 | max: null, 44 | allowNull: false, 45 | 46 | group: '', 47 | point: '.', 48 | prefix: '', 49 | suffix: '', 50 | places: null, // null causes it to detect the number of places in step 51 | 52 | defaultStep: 1, // real value is 'step', and should be passed as such. This value is used to detect if passed value should override HTML5 attribute 53 | largeStep: 10, 54 | mouseWheel: true, 55 | increment: 'slow', 56 | className: null, 57 | showOn: 'always', 58 | width: 16, 59 | upIconClass: "ui-icon-triangle-1-n", 60 | downIconClass: "ui-icon-triangle-1-s", 61 | 62 | format: function(num, places) { 63 | var options = this, 64 | regex = /(\d+)(\d{3})/, 65 | result = ((isNaN(num) ? 0 : Math.abs(num)).toFixed(places)) + ''; 66 | 67 | for (result = result.replace('.', options.point); regex.test(result) && options.group; result=result.replace(regex, '$1'+options.group+'$2')) {}; 68 | return (num < 0 ? '-' : '') + options.prefix + result + options.suffix; 69 | }, 70 | 71 | parse: function(val) { 72 | var options = this; 73 | 74 | if (options.group == '.') 75 | val = val.replace('.', ''); 76 | if (options.point != '.') 77 | val = val.replace(options.point, '.'); 78 | return parseFloat(val.replace(/[^0-9\-\.]/g, '')); 79 | } 80 | }, 81 | 82 | // * Widget fields * 83 | // curvalue - current value 84 | // places - currently effective number of decimal places 85 | // oWidth - original input width (used for destroy) 86 | // oMargin - original input right margin (used for destroy) 87 | // counter - number of spins at the current spin speed 88 | // incCounter - index within options.increment of the current spin speed 89 | // selfChange - indicates that change event is being fired by the widget, so don't reprocess input value 90 | // inputMaxLength - initial maxLength value on the input 91 | // focused - this spinner currently has the focus 92 | 93 | _create: function() { 94 | // shortcuts 95 | var self = this, 96 | input = self.element, 97 | type = input.attr('type'); 98 | 99 | if (!input.is('input') || ((type != 'text') && (type != 'number'))) { 100 | console.error('Invalid target for ui.spinner'); 101 | return; 102 | } 103 | 104 | self._procOptions(true); 105 | self._createButtons(input); 106 | 107 | if (!input.is(':enabled')) 108 | self.disable(); 109 | }, 110 | 111 | _createButtons: function(input) { 112 | function getMargin(margin) { 113 | // IE8 returns auto if no margin specified 114 | return margin == 'auto' ? 0 : parseInt(margin); 115 | } 116 | 117 | var self = this, 118 | options = self.options, 119 | className = options.className, 120 | buttonWidth = options.width, 121 | showOn = options.showOn, 122 | box = $.support.boxModel, 123 | height = input.outerHeight(), 124 | rightMargin = self.oMargin = getMargin(input.css('margin-right')), // store original width and right margin for later destroy 125 | wrapper = self.wrapper = input.css({ width: (self.oWidth = (box ? input.width() : input.outerWidth())) - buttonWidth, 126 | marginRight: rightMargin + buttonWidth, textAlign: 'right' }) 127 | .after('').next(), 128 | btnContainer = self.btnContainer = $( 129 | '
' + 130 | '
 
' + 131 | '
 
' + 132 | '
'), 133 | 134 | // object shortcuts 135 | upButton, downButton, buttons, icons, 136 | 137 | hoverDelay, 138 | hoverDelayCallback, 139 | 140 | // current state booleans 141 | hovered, inKeyDown, inSpecialKey, inMouseDown, 142 | 143 | // used to reverse left/right key directions 144 | rtl = input[0].dir == 'rtl'; 145 | 146 | // apply className before doing any calculations because it could affect them 147 | if (className) wrapper.addClass(className); 148 | 149 | wrapper.append(btnContainer.css({ height: height, left: -buttonWidth-rightMargin, 150 | // use offset calculation to fix vertical position in Firefox 151 | top: (input.offset().top - wrapper.offset().top) + 'px' })); 152 | 153 | buttons = self.buttons = btnContainer.find('.ui-spinner-button'); 154 | buttons.css({ width: buttonWidth - (box ? buttons.outerWidth() - buttons.width() : 0), height: height/2 - (box ? buttons.outerHeight() - buttons.height() : 0) }); 155 | upButton = buttons[0]; 156 | downButton = buttons[1]; 157 | 158 | // fix icon centering 159 | icons = buttons.find('.ui-icon'); 160 | icons.css({ marginLeft: (buttons.innerWidth() - icons.width()) / 2, marginTop: (buttons.innerHeight() - icons.height()) / 2 }); 161 | 162 | // set width of btnContainer to be the same as the buttons 163 | btnContainer.width(buttons.outerWidth()); 164 | if (showOn != 'always') 165 | btnContainer.css('opacity', 0); 166 | 167 | /* Event Bindings */ 168 | 169 | // bind hover events to show/hide buttons 170 | if (showOn == 'hover' || showOn == 'both') 171 | buttons.add(input) 172 | .bind('mouseenter' + eventNamespace, function() { 173 | setHoverDelay(function() { 174 | hovered = true; 175 | if (!self.focused || (showOn == 'hover')) // ignore focus flag if show on hover only 176 | self.showButtons(); 177 | }); 178 | }) 179 | 180 | .bind('mouseleave' + eventNamespace, function hoverOut() { 181 | setHoverDelay(function() { 182 | hovered = false; 183 | if (!self.focused || (showOn == 'hover')) // ignore focus flag if show on hover only 184 | self.hideButtons(); 185 | }); 186 | }); 187 | 188 | 189 | buttons.hover(function() { 190 | // ensure that both buttons have hover removed, sometimes they get left on 191 | self.buttons.removeClass(hover); 192 | 193 | if (!options.disabled) 194 | $(this).addClass(hover); 195 | }, function() { 196 | $(this).removeClass(hover); 197 | }) 198 | .mousedown(mouseDown) 199 | .mouseup(mouseUp) 200 | .mouseout(mouseUp); 201 | 202 | if (msie) 203 | // fixes dbl click not firing second mouse down in IE 204 | buttons.dblclick(function() { 205 | if (!options.disabled) { 206 | // make sure any changes are posted 207 | self._change(); 208 | self._doSpin((this === upButton ? 1 : -1) * options.step); 209 | } 210 | 211 | return false; 212 | }) 213 | 214 | // fixes IE8 dbl click selection highlight 215 | .bind('selectstart', function() {return false;}); 216 | 217 | input.bind('keydown' + eventNamespace, function(e) { 218 | var dir, large, limit, 219 | keyCode = e.keyCode; // shortcut for minimization 220 | if (e.ctrl || e.alt) return true; // ignore these events 221 | 222 | if (isSpecialKey(keyCode)) 223 | inSpecialKey = true; 224 | 225 | if (inKeyDown) return false; // only one direction at a time, and suppress invalid keys 226 | 227 | switch (keyCode) { 228 | case up: 229 | case pageUp: 230 | dir = 1; 231 | large = keyCode == pageUp; 232 | break; 233 | 234 | case down: 235 | case pageDown: 236 | dir = -1; 237 | large = keyCode == pageDown; 238 | break; 239 | 240 | case right: 241 | case left: 242 | dir = (keyCode == right) ^ rtl ? 1 : -1; 243 | break; 244 | 245 | case home: 246 | limit = self.options.min; 247 | if (limit != null) self._setValue(limit); 248 | return false; 249 | 250 | case end: 251 | limit = self.options.max; 252 | limit = self.options.max; 253 | if (limit != null) self._setValue(limit); 254 | return false; 255 | } 256 | 257 | if (dir) { // only process if dir was set above 258 | if (!inKeyDown && !options.disabled) { 259 | keyDir = dir; 260 | 261 | $(dir > 0 ? upButton : downButton).addClass(active); 262 | inKeyDown = true; 263 | self._startSpin(dir, large); 264 | } 265 | 266 | return false; 267 | } 268 | }) 269 | 270 | .bind('keyup' + eventNamespace, function(e) { 271 | if (e.ctrl || e.alt) return true; // ignore these events 272 | 273 | if (isSpecialKey(keyCode)) 274 | inSpecialKey = false; 275 | 276 | switch (e.keyCode) { 277 | case up: 278 | case right: 279 | case pageUp: 280 | case down: 281 | case left: 282 | case pageDown: 283 | buttons.removeClass(active) 284 | self._stopSpin(); 285 | inKeyDown = false; 286 | return false; 287 | } 288 | }) 289 | 290 | .bind('keypress' + eventNamespace, function(e) { 291 | if (invalidKey(e.keyCode, e.charCode)) return false; 292 | }) 293 | 294 | .bind('change' + eventNamespace, function() { self._change(); }) 295 | 296 | .bind('focus' + eventNamespace, function() { 297 | function selectAll() { 298 | self.element.select(); 299 | } 300 | 301 | msie ? selectAll() : setTimeout(selectAll, 0); // add delay for Chrome, but breaks IE8 302 | self.focused = true; 303 | focusCtrl = self; 304 | if (!hovered && (showOn == 'focus' || showOn == 'both')) // hovered will only be set if hover affects show 305 | self.showButtons(); 306 | }) 307 | 308 | .bind('blur' + eventNamespace, function() { 309 | self.focused = false; 310 | if (!hovered && (showOn == 'focus' || showOn == 'both')) // hovered will only be set if hover affects show 311 | self.hideButtons(); 312 | }); 313 | 314 | function isSpecialKey(keyCode) { 315 | for (var i=0; i= '0') && (ch <= '9') || (ch == '-')) return false; 328 | if (((self.places > 0) && (ch == options.point)) 329 | || (ch == options.group)) return false; 330 | 331 | return true; 332 | } 333 | 334 | // used to delay start of hover show/hide by 100 milliseconds 335 | function setHoverDelay(callback) { 336 | if (hoverDelay) { 337 | // don't do anything if trying to set the same callback again 338 | if (callback === hoverDelayCallback) return; 339 | 340 | clearTimeout(hoverDelay); 341 | } 342 | 343 | hoverDelayCallback = callback; 344 | hoverDelay = setTimeout(execute, 100); 345 | 346 | function execute() { 347 | hoverDelay = 0; 348 | callback(); 349 | } 350 | } 351 | 352 | function mouseDown() { 353 | if (!options.disabled) { 354 | var input = self.element[0], 355 | dir = (this === upButton ? 1 : -1); 356 | 357 | input.focus(); 358 | input.select(); 359 | $(this).addClass(active); 360 | 361 | inMouseDown = true; 362 | self._startSpin(dir); 363 | } 364 | 365 | return false; 366 | } 367 | 368 | function mouseUp() { 369 | if (inMouseDown) { 370 | $(this).removeClass(active); 371 | self._stopSpin(); 372 | inMouseDown = false; 373 | } 374 | return false; 375 | } 376 | }, 377 | 378 | _procOptions: function(init) { 379 | var self = this, 380 | input = self.element, 381 | options = self.options, 382 | min = options.min, 383 | max = options.max, 384 | step = options.step, 385 | places = options.places, 386 | maxlength = -1, temp; 387 | 388 | // setup increment based on speed string 389 | if (options.increment == 'slow') 390 | options.increment = [{count: 1, mult: 1, delay: 250}, 391 | {count: 3, mult: 1, delay: 100}, 392 | {count: 0, mult: 1, delay: 50}]; 393 | else if (options.increment == 'fast') 394 | options.increment = [{count: 1, mult: 1, delay: 250}, 395 | {count: 19, mult: 1, delay: 100}, 396 | {count: 80, mult: 1, delay: 20}, 397 | {count: 100, mult: 10, delay: 20}, 398 | {count: 0, mult: 100, delay: 20}]; 399 | 400 | if ((min == null) && ((temp = input.attr('min')) != null)) 401 | min = parseFloat(temp); 402 | 403 | if ((max == null) && ((temp = input.attr('max')) != null)) 404 | max = parseFloat(temp); 405 | 406 | if (!step && ((temp = input.attr('step')) != null)) 407 | if (temp != 'any') { 408 | step = parseFloat(temp); 409 | options.largeStep *= step; 410 | } 411 | options.step = step = step || options.defaultStep; 412 | 413 | // Process step for decimal places if none are specified 414 | if ((places == null) && ((temp = step + '').indexOf('.') != -1)) 415 | places = temp.length - temp.indexOf('.') - 1; 416 | self.places = places; 417 | 418 | if ((max != null) && (min != null)) { 419 | // ensure that min is less than or equal to max 420 | if (min > max) min = max; 421 | 422 | // set maxlength based on min/max 423 | maxlength = Math.max(Math.max(maxlength, options.format(max, places, input).length), options.format(min, places, input).length); 424 | } 425 | 426 | // only lookup input maxLength on init 427 | if (init) self.inputMaxLength = input[0].maxLength; 428 | temp = self.inputMaxLength; 429 | 430 | if (temp > 0) { 431 | maxlength = maxlength > 0 ? Math.min(temp, maxlength) : temp; 432 | temp = Math.pow(10, maxlength) - 1; 433 | if ((max == null) || (max > temp)) 434 | max = temp; 435 | temp = -(temp + 1) / 10 + 1; 436 | if ((min == null) || (min < temp)) 437 | min = temp; 438 | } 439 | 440 | if (maxlength > 0) 441 | input.attr('maxlength', maxlength); 442 | 443 | options.min = min; 444 | options.max = max; 445 | 446 | // ensures that current value meets constraints 447 | self._change(); 448 | 449 | input.unbind(mouseWheelEventName + eventNamespace); 450 | if (options.mouseWheel) 451 | input.bind(mouseWheelEventName + eventNamespace, self._mouseWheel); 452 | }, 453 | 454 | _mouseWheel: function(e) { 455 | var self = $.data(this, 'spinner'); 456 | if (!self.options.disabled && self.focused && (focusCtrl === self)) { 457 | // make sure changes are posted 458 | self._change(); 459 | self._doSpin(((e.wheelDelta || -e.detail) > 0 ? 1 : -1) * self.options.step); 460 | return false; 461 | } 462 | }, 463 | 464 | // sets an interval to call the _spin function 465 | _setTimer: function(delay, dir, large) { 466 | var self = this; 467 | self._stopSpin(); 468 | self.timer = setInterval(fire, delay); 469 | 470 | function fire() { 471 | self._spin(dir, large); 472 | } 473 | }, 474 | 475 | // stops the spin timer 476 | _stopSpin: function() { 477 | if (this.timer) { 478 | clearInterval(this.timer); 479 | this.timer = 0; 480 | } 481 | }, 482 | 483 | // performs first step, and starts the spin timer if increment is set 484 | _startSpin: function(dir, large) { 485 | // shortcuts 486 | var self = this, 487 | options = self.options, 488 | increment = options.increment; 489 | 490 | // make sure any changes are posted 491 | self._change(); 492 | self._doSpin(dir * (large ? self.options.largeStep : self.options.step)); 493 | 494 | if (increment && increment.length > 0) { 495 | self.counter = 0; 496 | self.incCounter = 0; 497 | self._setTimer(increment[0].delay, dir, large); 498 | } 499 | }, 500 | 501 | // called by timer for each step in the spin 502 | _spin: function(dir, large) { 503 | // shortcuts 504 | var self = this, 505 | increment = self.options.increment, 506 | curIncrement = increment[self.incCounter]; 507 | 508 | self._doSpin(dir * curIncrement.mult * (large ? self.options.largeStep : self.options.step)); 509 | self.counter++; 510 | 511 | if ((self.counter > curIncrement.count) && (self.incCounter < increment.length-1)) { 512 | self.counter = 0; 513 | curIncrement = increment[++self.incCounter]; 514 | self._setTimer(curIncrement.delay, dir, large); 515 | } 516 | }, 517 | 518 | // actually spins the timer by a step 519 | _doSpin: function(step) { 520 | // shortcut 521 | var self = this, 522 | value = self.curvalue; 523 | 524 | if (value == null) 525 | value = (step > 0 ? self.options.min : self.options.max) || 0; 526 | 527 | self._setValue(value + step); 528 | }, 529 | 530 | // Parse the value currently in the field 531 | _parseValue: function() { 532 | var value = this.element.val(); 533 | return value ? this.options.parse(value, this.element) : null; 534 | }, 535 | 536 | _validate: function(value) { 537 | var options = this.options, 538 | min = options.min, 539 | max = options.max; 540 | 541 | if ((value == null) && !options.allowNull) 542 | value = this.curvalue != null ? this.curvalue : min || max || 0; // must confirm not null in case just initializing and had blank value 543 | 544 | if ((max != null) && (value > max)) 545 | return max; 546 | else if ((min != null) && (value < min)) 547 | return min; 548 | else 549 | return value; 550 | }, 551 | 552 | _change: function() { 553 | var self = this, // shortcut 554 | value = self._parseValue(), 555 | min = self.options.min, 556 | max = self.options.max; 557 | 558 | // don't reprocess if change was self triggered 559 | if (!self.selfChange) { 560 | if (isNaN(value)) 561 | value = self.curvalue; 562 | 563 | self._setValue(value, true); 564 | } 565 | }, 566 | 567 | // overrides _setData to force option parsing 568 | _setOption: function(key, value) { 569 | $.Widget.prototype._setOption.call(this, key, value); 570 | this._procOptions(); 571 | }, 572 | 573 | increment: function() { 574 | this._doSpin(this.options.step); 575 | }, 576 | 577 | decrement: function() { 578 | this._doSpin(-this.options.step); 579 | }, 580 | 581 | showButtons: function(immediate) { 582 | var btnContainer = this.btnContainer.stop(); 583 | if (immediate) 584 | btnContainer.css('opacity', 1); 585 | else 586 | btnContainer.fadeTo('fast', 1); 587 | }, 588 | 589 | hideButtons: function(immediate) { 590 | var btnContainer = this.btnContainer.stop(); 591 | if (immediate) 592 | btnContainer.css('opacity', 0); 593 | else 594 | btnContainer.fadeTo('fast', 0); 595 | this.buttons.removeClass(hover); 596 | }, 597 | 598 | // Set the value directly 599 | _setValue: function(value, suppressFireEvent) { 600 | var self = this; 601 | 602 | self.curvalue = value = self._validate(value); 603 | self.element.val(value != null ? 604 | self.options.format(value, self.places, self.element) : 605 | ''); 606 | 607 | if (!suppressFireEvent) { 608 | self.selfChange = true; 609 | self.element.change(); 610 | self.selfChange = false; 611 | } 612 | }, 613 | 614 | // Set or retrieve the value 615 | value: function(newValue) { 616 | if (arguments.length) { 617 | this._setValue(newValue); 618 | 619 | // maintains chaining 620 | return this.element; 621 | } 622 | 623 | return this.curvalue; 624 | }, 625 | 626 | enable: function() { 627 | this.buttons.removeClass(disabled); 628 | this.element[0].disabled = false; 629 | $.Widget.prototype.enable.call(this); 630 | }, 631 | 632 | disable: function() { 633 | this.buttons.addClass(disabled) 634 | // in case hover class got left on 635 | .removeClass(hover); 636 | 637 | this.element[0].disabled = true; 638 | $.Widget.prototype.disable.call(this); 639 | }, 640 | 641 | destroy: function(target) { 642 | this.wrapper.remove(); 643 | this.element.unbind(eventNamespace).css({ width: this.oWidth, marginRight: this.oMargin }); 644 | 645 | $.Widget.prototype.destroy.call(this); 646 | } 647 | }); 648 | 649 | })( jQuery ); -------------------------------------------------------------------------------- /ui.spinner.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | jQuery UI Spinner 1.20 3 | 4 | Copyright (c) 2009-2010 Brant Burnett 5 | Dual licensed under the MIT or GPL Version 2 licenses. 6 | */ 7 | (function(j){var s="ui-state-active",l=j.ui.keyCode,C=l.UP,D=l.DOWN,t=l.RIGHT,E=l.LEFT,u=l.PAGE_UP,v=l.PAGE_DOWN,J=l.HOME,K=l.END,L=j.browser.msie,M=j.browser.mozilla?"DOMMouseScroll":"mousewheel",N=[C,D,t,E,u,v,J,K,l.BACKSPACE,l.DELETE,l.TAB],O;j.widget("ui.spinner",{options:{min:null,max:null,allowNull:false,group:"",point:".",prefix:"",suffix:"",places:null,defaultStep:1,largeStep:10,mouseWheel:true,increment:"slow",className:null,showOn:"always",width:16,upIconClass:"ui-icon-triangle-1-n",downIconClass:"ui-icon-triangle-1-s", 8 | format:function(a,b){var d=/(\d+)(\d{3})/,g=(isNaN(a)?0:Math.abs(a)).toFixed(b)+"";for(g=g.replace(".",this.point);d.test(g)&&this.group;g=g.replace(d,"$1"+this.group+"$2"));return(a<0?"-":"")+this.prefix+g+this.suffix},parse:function(a){if(this.group==".")a=a.replace(".","");if(this.point!=".")a=a.replace(this.point,".");return parseFloat(a.replace(/[^0-9\-\.]/g,""))}},_create:function(){var a=this.element,b=a.attr("type");if(!a.is("input")||b!="text"&&b!="number")console.error("Invalid target for ui.spinner"); 9 | else{this._procOptions(true);this._createButtons(a);a.is(":enabled")||this.disable()}},_createButtons:function(a){function b(e){return e=="auto"?0:parseInt(e)}function d(e){for(var h=0;h="0"&&m<="9"||m=="-")return false;if(c.places>0&&m==o.point||m==o.group)return false;return true}function i(e){function h(){w=0;e()}if(w){if(e===P)return;clearTimeout(w)}P=e;w=setTimeout(h, 10 | 100)}function p(){if(!f.disabled){var e=c.element[0],h=this===x?1:-1;e.focus();e.select();j(this).addClass(s);G=true;c._startSpin(h)}return false}function q(){if(G){j(this).removeClass(s);c._stopSpin();G=false}return false}var c=this,f=c.options,r=f.className,y=f.width,n=f.showOn,H=j.support.boxModel,Q=a.outerHeight(),R=c.oMargin=b(a.css("margin-right")),I=c.wrapper=a.css({width:(c.oWidth=H?a.width():a.outerWidth())-y,marginRight:R+y,textAlign:"right"}).after('').next(), 11 | z=c.btnContainer=j('
 
 
'),x,S,k,w,P,A,B,F,G,T=a[0].dir=="rtl";r&&I.addClass(r);I.append(z.css({height:Q,left:-y-R,top:a.offset().top-I.offset().top+"px"}));k=c.buttons=z.find(".ui-spinner-button"); 12 | k.css({width:y-(H?k.outerWidth()-k.width():0),height:Q/2-(H?k.outerHeight()-k.height():0)});x=k[0];S=k[1];r=k.find(".ui-icon");r.css({marginLeft:(k.innerWidth()-r.width())/2,marginTop:(k.innerHeight()-r.height())/2});z.width(k.outerWidth());n!="always"&&z.css("opacity",0);if(n=="hover"||n=="both")k.add(a).bind("mouseenter.uispinner",function(){i(function(){A=true;if(!c.focused||n=="hover")c.showButtons()})}).bind("mouseleave.uispinner",function(){i(function(){A=false;if(!c.focused||n=="hover")c.hideButtons()})}); 13 | k.hover(function(){c.buttons.removeClass("ui-state-hover");f.disabled||j(this).addClass("ui-state-hover")},function(){j(this).removeClass("ui-state-hover")}).mousedown(p).mouseup(q).mouseout(q);L&&k.dblclick(function(){if(!f.disabled){c._change();c._doSpin((this===x?1:-1)*f.step)}return false}).bind("selectstart",function(){return false});a.bind("keydown.uispinner",function(e){var h,m,o=e.keyCode;if(e.ctrl||e.alt)return true;if(d(o))F=true;if(B)return false;switch(o){case C:case u:h=1;m=o==u;break; 14 | case D:case v:h=-1;m=o==v;break;case t:case E:h=o==t^T?1:-1;break;case J:e=c.options.min;e!=null&&c._setValue(e);return false;case K:e=c.options.max;e!=null&&c._setValue(e);return false}if(h){if(!B&&!f.disabled){keyDir=h;j(h>0?x:S).addClass(s);B=true;c._startSpin(h,m)}return false}}).bind("keyup.uispinner",function(e){if(e.ctrl||e.alt)return true;if(d(l))F=false;switch(e.keyCode){case C:case t:case u:case D:case E:case v:k.removeClass(s);c._stopSpin();return B=false}}).bind("keypress.uispinner",function(e){if(g(e.keyCode, 15 | e.charCode))return false}).bind("change.uispinner",function(){c._change()}).bind("focus.uispinner",function(){function e(){c.element.select()}L?e():setTimeout(e,0);c.focused=true;O=c;if(!A&&(n=="focus"||n=="both"))c.showButtons()}).bind("blur.uispinner",function(){c.focused=false;if(!A&&(n=="focus"||n=="both"))c.hideButtons()})},_procOptions:function(a){var b=this.element,d=this.options,g=d.min,i=d.max,p=d.step,q=d.places,c=-1,f;if(d.increment=="slow")d.increment=[{count:1,mult:1,delay:250},{count:3, 16 | mult:1,delay:100},{count:0,mult:1,delay:50}];else if(d.increment=="fast")d.increment=[{count:1,mult:1,delay:250},{count:19,mult:1,delay:100},{count:80,mult:1,delay:20},{count:100,mult:10,delay:20},{count:0,mult:100,delay:20}];if(g==null&&(f=b.attr("min"))!=null)g=parseFloat(f);if(i==null&&(f=b.attr("max"))!=null)i=parseFloat(f);if(!p&&(f=b.attr("step"))!=null)if(f!="any"){p=parseFloat(f);d.largeStep*=p}d.step=p=p||d.defaultStep;if(q==null&&(f=p+"").indexOf(".")!=-1)q=f.length-f.indexOf(".")-1;this.places= 17 | q;if(i!=null&&g!=null){if(g>i)g=i;c=Math.max(Math.max(c,d.format(i,q,b).length),d.format(g,q,b).length)}if(a)this.inputMaxLength=b[0].maxLength;f=this.inputMaxLength;if(f>0){c=c>0?Math.min(f,c):f;f=Math.pow(10,c)-1;if(i==null||i>f)i=f;f=-(f+1)/10+1;if(g==null||g0&&b.attr("maxlength",c);d.min=g;d.max=i;this._change();b.unbind(M+".uispinner");d.mouseWheel&&b.bind(M+".uispinner",this._mouseWheel)},_mouseWheel:function(a){var b=j.data(this,"spinner");if(!b.options.disabled&&b.focused&&O===b){b._change(); 18 | b._doSpin(((a.wheelDelta||-a.detail)>0?1:-1)*b.options.step);return false}},_setTimer:function(a,b,d){function g(){i._spin(b,d)}var i=this;i._stopSpin();i.timer=setInterval(g,a)},_stopSpin:function(){if(this.timer){clearInterval(this.timer);this.timer=0}},_startSpin:function(a,b){var d=this.options.increment;this._change();this._doSpin(a*(b?this.options.largeStep:this.options.step));if(d&&d.length>0){this.incCounter=this.counter=0;this._setTimer(d[0].delay,a,b)}},_spin:function(a,b){var d=this.options.increment, 19 | g=d[this.incCounter];this._doSpin(a*g.mult*(b?this.options.largeStep:this.options.step));this.counter++;if(this.counter>g.count&&this.incCounter0?this.options.min:this.options.max)||0;this._setValue(b+a)},_parseValue:function(){var a=this.element.val();return a?this.options.parse(a,this.element):null},_validate:function(a){var b=this.options,d=b.min,g=b.max;if(a== 20 | null&&!b.allowNull)a=this.curvalue!=null?this.curvalue:d||g||0;return g!=null&&a>g?g:d!=null&&a