/** * @license jQuery UI Spinner 1.20 * * Copyright (c) 2009-2010 Brant Burnett * Dual licensed under the MIT or GPL Version 2 licenses. */ (function($, undefined) { var // constants active = 'ui-state-active', hover = 'ui-state-hover', disabled = 'ui-state-disabled', keyCode = $.ui.keyCode, up = keyCode.UP, down = keyCode.DOWN, right = keyCode.RIGHT, left = keyCode.LEFT, pageUp = keyCode.PAGE_UP, pageDown = keyCode.PAGE_DOWN, home = keyCode.HOME, end = keyCode.END, msie = $.browser.msie, mouseWheelEventName = $.browser.mozilla ? 'DOMMouseScroll' : 'mousewheel', // namespace for events on input eventNamespace = '.uispinner', // only these special keys will be accepted, all others will be ignored unless CTRL or ALT are pressed validKeys = [up, down, right, left, pageUp, pageDown, home, end, keyCode.BACKSPACE, keyCode.DELETE, keyCode.TAB], // stores the currently focused spinner // Note: due to oddities in the focus/blur events, this is part of a two-part system for confirming focus // this must set to the control, and the focus variable must be true // this is because hitting up/down arrows with mouse causes focus to change, but blur event for previous control doesn't fire focusCtrl; $.widget('ui.spinner', { options: { min: null, max: null, allowNull: false, group: '', point: '.', prefix: '', suffix: '', places: null, // null causes it to detect the number of places in step 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 largeStep: 10, mouseWheel: true, increment: 'slow', className: null, showOn: 'always', width: 16, upIconClass: "ui-icon-triangle-1-n", downIconClass: "ui-icon-triangle-1-s", format: function(num, places) { var options = this, regex = /(\d+)(\d{3})/, result = ((isNaN(num) ? 0 : Math.abs(num)).toFixed(places)) + ''; for (result = result.replace('.', options.point); regex.test(result) && options.group; result=result.replace(regex, '$1'+options.group+'$2')) {}; return (num < 0 ? '-' : '') + options.prefix + result + options.suffix; }, parse: function(val) { var options = this; if (options.group == '.') val = val.replace('.', ''); if (options.point != '.') val = val.replace(options.point, '.'); return parseFloat(val.replace(/[^0-9\-\.]/g, '')); } }, // * Widget fields * // curvalue - current value // places - currently effective number of decimal places // oWidth - original input width (used for destroy) // oMargin - original input right margin (used for destroy) // counter - number of spins at the current spin speed // incCounter - index within options.increment of the current spin speed // selfChange - indicates that change event is being fired by the widget, so don't reprocess input value // inputMaxLength - initial maxLength value on the input // focused - this spinner currently has the focus _create: function() { // shortcuts var self = this, input = self.element, type = input.attr('type'); if (!input.is('input') || ((type != 'text') && (type != 'number'))) { console.error('Invalid target for ui.spinner'); return; } self._procOptions(true); self._createButtons(input); if (!input.is(':enabled')) self.disable(); }, _createButtons: function(input) { function getMargin(margin) { // IE8 returns auto if no margin specified return margin == 'auto' ? 0 : parseInt(margin); } var self = this, options = self.options, className = options.className, buttonWidth = options.width, showOn = options.showOn, box = $.support.boxModel, height = input.outerHeight(), rightMargin = self.oMargin = getMargin(input.css('margin-right')), // store original width and right margin for later destroy wrapper = self.wrapper = input.css({ width: (self.oWidth = (box ? input.width() : input.outerWidth())) - buttonWidth, marginRight: rightMargin + buttonWidth, textAlign: 'right' }) .after('').next(), btnContainer = self.btnContainer = $( '
' + '
 
' + '
 
' + '
'), // object shortcuts upButton, downButton, buttons, icons, hoverDelay, hoverDelayCallback, // current state booleans hovered, inKeyDown, inSpecialKey, inMouseDown, // used to reverse left/right key directions rtl = input[0].dir == 'rtl'; // apply className before doing any calculations because it could affect them if (className) wrapper.addClass(className); wrapper.append(btnContainer.css({ height: height, left: -buttonWidth-rightMargin, // use offset calculation to fix vertical position in Firefox top: (input.offset().top - wrapper.offset().top) + 'px' })); buttons = self.buttons = btnContainer.find('.ui-spinner-button'); buttons.css({ width: buttonWidth - (box ? buttons.outerWidth() - buttons.width() : 0), height: height/2 - (box ? buttons.outerHeight() - buttons.height() : 0) }); upButton = buttons[0]; downButton = buttons[1]; // fix icon centering icons = buttons.find('.ui-icon'); icons.css({ marginLeft: (buttons.innerWidth() - icons.width()) / 2, marginTop: (buttons.innerHeight() - icons.height()) / 2 }); // set width of btnContainer to be the same as the buttons btnContainer.width(buttons.outerWidth()); if (showOn != 'always') btnContainer.css('opacity', 0); /* Event Bindings */ // bind hover events to show/hide buttons if (showOn == 'hover' || showOn == 'both') buttons.add(input) .bind('mouseenter' + eventNamespace, function() { setHoverDelay(function() { hovered = true; if (!self.focused || (showOn == 'hover')) // ignore focus flag if show on hover only self.showButtons(); }); }) .bind('mouseleave' + eventNamespace, function hoverOut() { setHoverDelay(function() { hovered = false; if (!self.focused || (showOn == 'hover')) // ignore focus flag if show on hover only self.hideButtons(); }); }); buttons.hover(function() { // ensure that both buttons have hover removed, sometimes they get left on self.buttons.removeClass(hover); if (!options.disabled) $(this).addClass(hover); }, function() { $(this).removeClass(hover); }) .mousedown(mouseDown) .mouseup(mouseUp) .mouseout(mouseUp); if (msie) // fixes dbl click not firing second mouse down in IE buttons.dblclick(function() { if (!options.disabled) { // make sure any changes are posted self._change(); self._doSpin((this === upButton ? 1 : -1) * options.step); } return false; }) // fixes IE8 dbl click selection highlight .bind('selectstart', function() {return false;}); input.bind('keydown' + eventNamespace, function(e) { var dir, large, limit, keyCode = e.keyCode; // shortcut for minimization if (e.ctrl || e.alt) return true; // ignore these events if (isSpecialKey(keyCode)) inSpecialKey = true; if (inKeyDown) return false; // only one direction at a time, and suppress invalid keys switch (keyCode) { case up: case pageUp: dir = 1; large = keyCode == pageUp; break; case down: case pageDown: dir = -1; large = keyCode == pageDown; break; case right: case left: dir = (keyCode == right) ^ rtl ? 1 : -1; break; case home: limit = self.options.min; if (limit != null) self._setValue(limit); return false; case end: limit = self.options.max; limit = self.options.max; if (limit != null) self._setValue(limit); return false; } if (dir) { // only process if dir was set above if (!inKeyDown && !options.disabled) { keyDir = dir; $(dir > 0 ? upButton : downButton).addClass(active); inKeyDown = true; self._startSpin(dir, large); } return false; } }) .bind('keyup' + eventNamespace, function(e) { if (e.ctrl || e.alt) return true; // ignore these events if (isSpecialKey(keyCode)) inSpecialKey = false; switch (e.keyCode) { case up: case right: case pageUp: case down: case left: case pageDown: buttons.removeClass(active) self._stopSpin(); inKeyDown = false; return false; } }) .bind('keypress' + eventNamespace, function(e) { if (invalidKey(e.keyCode, e.charCode)) return false; }) .bind('change' + eventNamespace, function() { self._change(); }) .bind('focus' + eventNamespace, function() { function selectAll() { self.element.select(); } msie ? selectAll() : setTimeout(selectAll, 0); // add delay for Chrome, but breaks IE8 self.focused = true; focusCtrl = self; if (!hovered && (showOn == 'focus' || showOn == 'both')) // hovered will only be set if hover affects show self.showButtons(); }) .bind('blur' + eventNamespace, function() { self.focused = false; if (!hovered && (showOn == 'focus' || showOn == 'both')) // hovered will only be set if hover affects show self.hideButtons(); }); function isSpecialKey(keyCode) { for (var i=0; i= '0') && (ch <= '9') || (ch == '-')) return false; if (((self.places > 0) && (ch == options.point)) || (ch == options.group)) return false; return true; } // used to delay start of hover show/hide by 100 milliseconds function setHoverDelay(callback) { if (hoverDelay) { // don't do anything if trying to set the same callback again if (callback === hoverDelayCallback) return; clearTimeout(hoverDelay); } hoverDelayCallback = callback; hoverDelay = setTimeout(execute, 100); function execute() { hoverDelay = 0; callback(); } } function mouseDown() { if (!options.disabled) { var input = self.element[0], dir = (this === upButton ? 1 : -1); input.focus(); input.select(); $(this).addClass(active); inMouseDown = true; self._startSpin(dir); } return false; } function mouseUp() { if (inMouseDown) { $(this).removeClass(active); self._stopSpin(); inMouseDown = false; } return false; } }, _procOptions: function(init) { var self = this, input = self.element, options = self.options, min = options.min, max = options.max, step = options.step, places = options.places, maxlength = -1, temp; // setup increment based on speed string if (options.increment == 'slow') options.increment = [{count: 1, mult: 1, delay: 250}, {count: 3, mult: 1, delay: 100}, {count: 0, mult: 1, delay: 50}]; else if (options.increment == 'fast') options.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 ((min == null) && ((temp = input.attr('min')) != null)) min = parseFloat(temp); if ((max == null) && ((temp = input.attr('max')) != null)) max = parseFloat(temp); if (!step && ((temp = input.attr('step')) != null)) if (temp != 'any') { step = parseFloat(temp); options.largeStep *= step; } options.step = step = step || options.defaultStep; // Process step for decimal places if none are specified if ((places == null) && ((temp = step + '').indexOf('.') != -1)) places = temp.length - temp.indexOf('.') - 1; self.places = places; if ((max != null) && (min != null)) { // ensure that min is less than or equal to max if (min > max) min = max; // set maxlength based on min/max maxlength = Math.max(Math.max(maxlength, options.format(max, places, input).length), options.format(min, places, input).length); } // only lookup input maxLength on init if (init) self.inputMaxLength = input[0].maxLength; temp = self.inputMaxLength; if (temp > 0) { maxlength = maxlength > 0 ? Math.min(temp, maxlength) : temp; temp = Math.pow(10, maxlength) - 1; if ((max == null) || (max > temp)) max = temp; temp = -(temp + 1) / 10 + 1; if ((min == null) || (min < temp)) min = temp; } if (maxlength > 0) input.attr('maxlength', maxlength); options.min = min; options.max = max; // ensures that current value meets constraints self._change(); input.unbind(mouseWheelEventName + eventNamespace); if (options.mouseWheel) input.bind(mouseWheelEventName + eventNamespace, self._mouseWheel); }, _mouseWheel: function(e) { var self = $.data(this, 'spinner'); if (!self.options.disabled && self.focused && (focusCtrl === self)) { // make sure changes are posted self._change(); self._doSpin(((e.wheelDelta || -e.detail) > 0 ? 1 : -1) * self.options.step); return false; } }, // sets an interval to call the _spin function _setTimer: function(delay, dir, large) { var self = this; self._stopSpin(); self.timer = setInterval(fire, delay); function fire() { self._spin(dir, large); } }, // stops the spin timer _stopSpin: function() { if (this.timer) { clearInterval(this.timer); this.timer = 0; } }, // performs first step, and starts the spin timer if increment is set _startSpin: function(dir, large) { // shortcuts var self = this, options = self.options, increment = options.increment; // make sure any changes are posted self._change(); self._doSpin(dir * (large ? self.options.largeStep : self.options.step)); if (increment && increment.length > 0) { self.counter = 0; self.incCounter = 0; self._setTimer(increment[0].delay, dir, large); } }, // called by timer for each step in the spin _spin: function(dir, large) { // shortcuts var self = this, increment = self.options.increment, curIncrement = increment[self.incCounter]; self._doSpin(dir * curIncrement.mult * (large ? self.options.largeStep : self.options.step)); self.counter++; if ((self.counter > curIncrement.count) && (self.incCounter < increment.length-1)) { self.counter = 0; curIncrement = increment[++self.incCounter]; self._setTimer(curIncrement.delay, dir, large); } }, // actually spins the timer by a step _doSpin: function(step) { // shortcut var self = this, value = self.curvalue; if (value == null) value = (step > 0 ? self.options.min : self.options.max) || 0; self._setValue(value + step); }, // Parse the value currently in the field _parseValue: function() { var value = this.element.val(); return value ? this.options.parse(value, this.element) : null; }, _validate: function(value) { var options = this.options, min = options.min, max = options.max; if ((value == null) && !options.allowNull) value = this.curvalue != null ? this.curvalue : min || max || 0; // must confirm not null in case just initializing and had blank value if ((max != null) && (value > max)) return max; else if ((min != null) && (value < min)) return min; else return value; }, _change: function() { var self = this, // shortcut value = self._parseValue(), min = self.options.min, max = self.options.max; // don't reprocess if change was self triggered if (!self.selfChange) { if (isNaN(value)) value = self.curvalue; self._setValue(value, true); } }, // overrides _setData to force option parsing _setOption: function(key, value) { $.Widget.prototype._setOption.call(this, key, value); this._procOptions(); }, increment: function() { this._doSpin(this.options.step); }, decrement: function() { this._doSpin(-this.options.step); }, showButtons: function(immediate) { var btnContainer = this.btnContainer.stop(); if (immediate) btnContainer.css('opacity', 1); else btnContainer.fadeTo('fast', 1); }, hideButtons: function(immediate) { var btnContainer = this.btnContainer.stop(); if (immediate) btnContainer.css('opacity', 0); else btnContainer.fadeTo('fast', 0); this.buttons.removeClass(hover); }, // Set the value directly _setValue: function(value, suppressFireEvent) { var self = this; self.curvalue = value = self._validate(value); self.element.val(value != null ? self.options.format(value, self.places, self.element) : ''); if (!suppressFireEvent) { self.selfChange = true; self.element.change(); self.selfChange = false; } }, // Set or retrieve the value value: function(newValue) { if (arguments.length) { this._setValue(newValue); // maintains chaining return this.element; } return this.curvalue; }, enable: function() { this.buttons.removeClass(disabled); this.element[0].disabled = false; $.Widget.prototype.enable.call(this); }, disable: function() { this.buttons.addClass(disabled) // in case hover class got left on .removeClass(hover); this.element[0].disabled = true; $.Widget.prototype.disable.call(this); }, destroy: function(target) { this.wrapper.remove(); this.element.unbind(eventNamespace).css({ width: this.oWidth, marginRight: this.oMargin }); $.Widget.prototype.destroy.call(this); } }); })( jQuery );