/* SpinButton control
 *
 * Adds bells and whistles to any ordinary textbox to
 * make it look and feel like a SpinButton Control.
 *
 * Originally written by George Adamson, Software Unity (george.jquery@softwareunity.com) August 2006.
 * - Added min/max options
 * - Added step size option
 * - Added bigStep (page up/down) option
 *
 * Modifications made by Mark Gibson, (mgibson@designlinks.net) September 2006:
 * - Converted to jQuery plugin
 * - Allow limited or unlimited min/max values
 * - Allow custom class names, and add class to input element
 * - Removed global vars
 * - Reset (to original or through config) when invalid value entered
 * - Repeat whilst holding mouse button down (with initial pause, like keyboard repeat)
 * - Support mouse wheel in Firefox
 * - Fix double click in IE
 * - Refactored some code and renamed some vars
 *
 * Modifications by Jeff Schiller, June 2009:
 * - provide callback function for when the value changes based on the following
 *   http://www.mail-archive.com/jquery-en@googlegroups.com/msg36070.html
 * Modifications by Jeff Schiller, July 2009:
 * - improve styling for widget in Opera
 * - consistent key-repeat handling cross-browser
 * Modifications by Alexis Deveria, October 2009:
 * - provide "stepfunc" callback option to allow custom function to run when changing a value
 * - Made adjustValue(0) only run on certain keyup events, not all.
 *
 * Tested in IE6, Opera9, Firefox 1.5
 * v1.0  11 Aug 2006 - George Adamson   - First release
 * v1.1     Aug 2006 - George Adamson   - Minor enhancements
 * v1.2  27 Sep 2006 - Mark Gibson              - Major enhancements
 * v1.3a 28 Sep 2006 - George Adamson   - Minor enhancements
 * v1.4  18 Jun 2009 - Jeff Schiller    - Added callback function
 * v1.5  06 Jul 2009 - Jeff Schiller    - Fixes for Opera.
 * v1.6  13 Oct 2009 - Alexis Deveria   - Added stepfunc function
 * v1.7  21 Oct 2009 - Alexis Deveria   - Minor fixes
 *                                        Fast-repeat for keys and live updating as you type.

 Sample usage:

        // Create group of settings to initialise spinbutton(s). (Optional)
        var myOptions = {
                                        min: 0,                                         // Set lower limit.
                                        max: 100,                                       // Set upper limit.
                                        step: 1,                                        // Set increment size.
                                        spinClass: mySpinBtnClass,      // CSS class to style the spinbutton. (Class also specifies url of the up/down button image.)
                                        upClass: mySpinUpClass,         // CSS class for style when mouse over up button.
                                        downClass: mySpinDnClass        // CSS class for style when mouse over down button.
                                        }

        $(document).ready(function(){

                // Initialise INPUT element(s) as SpinButtons: (passing options if desired)
                $("#myInputElement").SpinButton(myOptions);

        });

 */
$.fn.SpinButton = function(cfg){
    return this.each(function(){

        this.repeating = false;

        // Apply specified options or defaults:
        // (Ought to refactor this some day to use $.extend() instead)
        this.spinCfg = {
            //min: cfg && cfg.min ? Number(cfg.min) : null,
            //max: cfg && cfg.max ? Number(cfg.max) : null,
            min: cfg && !isNaN(parseFloat(cfg.min)) ? Number(cfg.min) : null,       // Fixes bug with min:0
            max: cfg && !isNaN(parseFloat(cfg.max)) ? Number(cfg.max) : null,
            step: cfg && cfg.step ? Number(cfg.step) : 1,
            stepfunc: cfg && cfg.stepfunc ? cfg.stepfunc : false,
            page: cfg && cfg.page ? Number(cfg.page) : 10,
            upClass: cfg && cfg.upClass ? cfg.upClass : 'up',
            downClass: cfg && cfg.downClass ? cfg.downClass : 'down',
            reset: cfg && cfg.reset ? cfg.reset : this.value,
            delay: cfg && cfg.delay ? Number(cfg.delay) : 500,
            interval: cfg && cfg.interval ? Number(cfg.interval) : 100,
            _btn_width: 20,
            _direction: null,
            _delay: null,
            _repeat: null,
            callback: cfg && cfg.callback ? cfg.callback : null
        };


        this.adjustValue = function(i){
            var v;
            if(isNaN(this.value)) {
                v = this.spinCfg.reset;
            } else if($.isFunction(this.spinCfg.stepfunc)) {
                v = this.spinCfg.stepfunc(this, i);
            } else {
                v = Number(this.value) + Number(i);
            }
            if (this.spinCfg.min !== null) v = Math.max(v, this.spinCfg.min);
            if (this.spinCfg.max !== null) v = Math.min(v, this.spinCfg.max);
            this.value = v;
            if ($.isFunction(this.spinCfg.callback)) this.spinCfg.callback(this);
        };

        $(this)
        .addClass(cfg && cfg.spinClass ? cfg.spinClass : 'spin-button')

        .mousemove(function(e){
            // Determine which button mouse is over, or not (spin direction):
            var x = e.pageX || e.x;
            var y = e.pageY || e.y;
            var el = e.target || e.srcElement;
            var height = $(el).outerHeight()/2;
            var direction =
            (x > coord(el,'offsetLeft') + el.offsetWidth - this.spinCfg._btn_width)
            ? ((y < coord(el,'offsetTop') + height) ? 1 : -1) : 0;

            if (direction !== this.spinCfg._direction) {
                // Style up/down buttons:
                switch(direction){
                    case 1: // Up arrow:
                        $(this).removeClass(this.spinCfg.downClass).addClass(this.spinCfg.upClass);
                        break;
                    case -1: // Down arrow:
                        $(this).removeClass(this.spinCfg.upClass).addClass(this.spinCfg.downClass);
                        break;
                    default: // Mouse is elsewhere in the textbox
                        $(this).removeClass(this.spinCfg.upClass).removeClass(this.spinCfg.downClass);
                }

                // Set spin direction:
                this.spinCfg._direction = direction;
            }
        })

        .mouseout(function(){
            // Reset up/down buttons to their normal appearance when mouse moves away:
            $(this).removeClass(this.spinCfg.upClass).removeClass(this.spinCfg.downClass);
            this.spinCfg._direction = null;
        })

        .mousedown(function(e){
            if (this.spinCfg._direction != 0) {
                // Respond to click on one of the buttons:
                var self = this;
                var adjust = function() {
                    self.adjustValue(self.spinCfg._direction * self.spinCfg.step);
                };

                adjust();

                // Initial delay before repeating adjustment
                self.spinCfg._delay = window.setTimeout(function() {
                    adjust();
                    // Repeat adjust at regular intervals
                    self.spinCfg._repeat = window.setInterval(adjust, self.spinCfg.interval);
                }, self.spinCfg.delay);
            }
        })

        .mouseup(function(e){
            // Cancel repeating adjustment
            window.clearInterval(this.spinCfg._repeat);
            window.clearTimeout(this.spinCfg._delay);
        })

        .dblclick(function(e) {
            if ($.browser.msie)
                this.adjustValue(this.spinCfg._direction * this.spinCfg.step);
        })

        .keydown(function(e){
            // Respond to up/down arrow keys.
            switch(e.keyCode){
                case 38:
                    this.adjustValue(this.spinCfg.step);
                    break; // Up
                case 40:
                    this.adjustValue(-this.spinCfg.step);
                    break; // Down
                case 33:
                    this.adjustValue(this.spinCfg.page);
                    break; // PageUp
                case 34:
                    this.adjustValue(-this.spinCfg.page);
                    break; // PageDown
            }
        })

        /*
                http://unixpapa.com/js/key.html describes the current state-of-affairs for
                key repeat events:
                - Safari 3.1 changed their model so that keydown is reliably repeated going forward
                - Firefox and Opera still only repeat the keypress event, not the keydown
                */
        .keypress(function(e){
            if (this.repeating) {
                // Respond to up/down arrow keys.
                switch(e.keyCode){
                    case 38:
                        this.adjustValue(this.spinCfg.step);
                        break; // Up
                    case 40:
                        this.adjustValue(-this.spinCfg.step);
                        break; // Down
                    case 33:
                        this.adjustValue(this.spinCfg.page);
                        break; // PageUp
                    case 34:
                        this.adjustValue(-this.spinCfg.page);
                        break; // PageDown
                }
            }
            // we always ignore the first keypress event (use the keydown instead)
            else {
                this.repeating = true;
            }
        })

        // clear the 'repeating' flag
        .keyup(function(e) {
            this.repeating = false;
            switch(e.keyCode){
                case 38: // Up
                case 40: // Down
                case 33: // PageUp
                case 34: // PageDown
                case 13:
                    this.adjustValue(0);
                    break; // Enter/Return
            }
        })

        .bind("mousewheel", function(e){
            // Respond to mouse wheel in IE. (It returns up/dn motion in multiples of 120)
            if (e.wheelDelta >= 120)
                this.adjustValue(this.spinCfg.step);
            else if (e.wheelDelta <= -120)
                this.adjustValue(-this.spinCfg.step);

            e.preventDefault();
        })

        .change(function(e){
            this.adjustValue(0);
        });

        if (this.addEventListener) {
            // Respond to mouse wheel in Firefox
            this.addEventListener('DOMMouseScroll', function(e) {
                if (e.detail > 0)
                    this.adjustValue(-this.spinCfg.step);
                else if (e.detail < 0)
                    this.adjustValue(this.spinCfg.step);

                e.preventDefault();
            }, false);
        }
    });

    function coord(el,prop) {
        var c = el[prop], b = document.body;

        while ((el = el.offsetParent) && (el != b)) {
            if (!$.browser.msie || (el.currentStyle.position != 'relative'))
                c += el[prop];
        }

        return c;
    }
};