/* 
 * Spinbox plugin for jQuery
 * http://www.softwareunity.com/jquery/JQuerySpinBtn/
 *
 * Adds bells and whistles to any ordinary textbox to
 * make it look and feel like a SpinBox Control.
 *
 * Copyright (c) 2006-2010 Software Unity Ltd
 * Dual licensed under the MIT and GPL licenses:
 * http://www.softwareunity.com/jquery/MIT-LICENSE.txt
 * http://www.softwareunity.com/jquery/GPL-LICENSE.txt
 *
 * Originally written by George Adamson, Software Unity (george.jquery@softwareunity.com) August 2006.
 * Modifications made by Mark Gibson, (mgibson@designlinks.net) September 2006.
 * Rewritten and enhanced by George Adamson, Software Unity (george.jquery@softwareunity.com) October 2009.
 *
 * Tested in IE6, Opera9, Firefox 3.5.3
 * 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
 * v2.0	 20 Oct 2009 - George Adamson	- Major refactor with breaking changes to options.
 
 Sample usage:
 
	// Create group of settings to initialise spinbutton(s). (Optional)
	var myOptions = {
		min: 0,							// Set lower limit or null for no limit.
		max: 100,						// Set upper limit or null for no limit.
		step: 0.5,						// Set increment size.
		spinboxClass: "mySpinBoxClass",	// 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.
	}
 
	jQuery(document).ready(function($){

		// Initialise INPUT elements as SpinBoxes: (passing options if desired)
		$("INPUT.spinbox").spinbox(myOptions);

	});
 
 */

(function($){

$.fn.spinbox = function(options){

	// Tidy up when spinbox('destroy') is called:
	if( options && typeof(options)==="String" && options==="destroy" )
		return this.unbind(".spinbox")
			.removeClass(options.spinboxClass)
			.removeClass(options.upClass).removeClass(options.downClass)
			.removeClass(options.upHoverClass).removeClass(options.downHoverClass)
			.removeData("spinboxMin").removeData("spinboxMax").removeData("spinboxReset")
			.removeData("spinboxStep").removeData("spinboxBigStep")
	;


	// Apply specified options or defaults:
	var undefined;		
	options = $.extend( {}, $.fn.spinbox.defaults, options );


	// Store min, max & reset values for each element: (Reset defaults to element's initial value if option undefined)
	this.each(function(){

		var $box	= $(this),
			min		= classData( "spinboxMin", this ),
			max		= classData( "spinboxMax", this ),
			step	= classData( "spinboxStep", this ),
			bigStep	= classData( "spinboxBigStep", this ),
			reset	= classData( "spinboxReset", this ),
			scale	= classData( "spinboxScale", this );	// AKA: Number of decimal places.

		if( min===undefined )	min		= firstNum( $box.attr("min"), options.min );
		if( max===undefined )	max		= firstNum( $box.attr("max"), options.max );
		if( !step )				step	= firstNum( $box.attr("step"), options.step );
		if( !bigStep )			bigStep	= firstNum( $box.attr("bigStep"), options.bigStep );
		if( reset===undefined )	reset	= firstNum( options.reset, $box.val(), min, max );
		if( scale===undefined )	scale	= $box.data("spinboxScale"); scale = ( scale || isNum(scale) ) ? scale : options.scale;

		// When scale option is true, auto derive the number of decimalPlaces to round to:
		if( scale === true ) scale = ( step.toString().split(".")[1] || "" ).length;

		$box.data( "spinboxMin", min );
		$box.data( "spinboxMax", max );
		$box.data( "spinboxStep", step );
		$box.data( "spinboxBigStep", bigStep );
		$box.data( "spinboxReset", reset );
		$box.data( "spinboxScale", scale );

	});


	return this.each(function(){

		// Flags used while mouse is being moved or pressed:
		var hoverUp, hoverDown, delayId, repeatId;
		var $box = $(this);

		// Bind event handlers for the spinbox:
		$box.bind("mousemove.spinbox",function(e){

			// Change css class when mouse is over an up/down button:
			var $box	= $(this);			
			var offset	= $box.offset();		// Calculate element left & top;
			var middle	= $box.height() / 2;	// Calculate midpoint between top and bottom of element;
			var hover	= ( e.pageX > offset.left + $box.width() - options.buttonWidth );
			hoverUp		= hover && ( e.pageY <= offset.top + middle );
			hoverDown	= hover && ( e.pageY >  offset.top + middle );

			$box.toggleClass( options.upHoverClass, hoverUp )
				.toggleClass( options.downHoverClass, hoverDown );
			// TODO: Stop autorepeat when mouse moves away from button?

		})

		.bind("mouseout.spinbox",function(){

			// Reset up/down buttons to their normal state when mouse moves away:
			stopAutorepeat();
			$(this).removeClass( [ options.upClass, options.downClass, options.upHoverClass, options.downHoverClass ].join(" ") );
			hoverUp = hoverDown = null;

		})

		.bind("mousedown.spinbox",function(e){

			// Update the spinbox value and set up autorepeat to start after a short delay:
			if( hoverUp || hoverDown ){

				$(this).toggleClass( options.upClass, hoverUp ).toggleClass( options.downClass, hoverDown );
				adjustValue.apply( this, [e] );
				startAutorepeat(this,e);

			};
		})

		.bind("mouseup.spinbox", function(e){
			stopAutorepeat();
			$(this).removeClass(options.upClass).removeClass(options.downClass);
		})

		//.bind("dblclick", function(e) {
		//	if ($.browser.msie)
		//		adjustValue.apply( this, [e, options.step, 1] );
		//})

		.bind("keydown.spinbox",function(e){

			// Filter key press by allowable options.keys if specified:
			if( !options.keys ||
				$.grep(options.keys, function(key){
					return key === e.keyCode || ( key instanceof RegExp && key.test( String.fromCharCode(e.keyCode) ) );
				}).length ){

				// Define arry key codes and decide whether to use options.bigStep when Shift is pressed:
				var $box	= $(this);
				var key		= { up:38, down:40, pageUp:33, pageDown:34 };
				var bigStep	= $box.data("spinboxBigStep");
				var step	= e.shiftKey ? bigStep : $box.data("spinboxStep");

				// Respond to up/down arrow keys and pageUp/pageDown:
				switch(e.keyCode){
					case key.up: adjustValue.apply( this, [e, step, 1] );  break;
					case key.down: adjustValue.apply( this, [e, step, -1] ); break;
					case key.pageUp: adjustValue.apply( this, [e, bigStep, 1] );  break;
					case key.pageDown: adjustValue.apply( this, [e, bigStep, -1] ); break;
				};
			}
			else
				// Cancel event if keys filter was specified but pressed key is not on the list:
				return !options.keys;
		})

		.bind("change.spinbox", function(e){
			adjustValue.apply( this, [e,0] );
		})

		.addClass(options.spinboxClass);



		// React to mousewheel if desired:
		if( options.mousewheel ){

			$(this).bind("mousewheel.spinbox DOMMouseScroll.spinbox", function(e){

				var step = e.shiftKey ? $(this).data("spinboxBigStep") : $(this).data("spinboxStep");

				// Respond to mouse wheel: (Allow for IE/Opera e.wheelDelta and W3C e.detail)
				if ( e.detail < 0 || e.wheelDelta >= 120 )
					adjustValue.apply( this, [e, step, 1] );
				else if ( e.detail > 0 || e.wheelDelta <= -120 )
					adjustValue.apply( this, [e, step, -1] );

				return false;
			});

		}


		// Initialise the current value, ensuring it is within min/max etc:
		adjustValue.apply( this, [$.Event(),0] );



		// Helper called in response to click or mousewheel etc to apply step change:
		function adjustValue(e,step,direction){

			var $box	= $(this); if( options.ignore && $box.is(options.ignore) ) return;
			step		= firstNum( step, $box.data("spinboxStep"), 1 );
			direction	= direction || (hoverDown ? -1 : 1);
			var oldVal	= $box.val();
			var val		= firstNum( oldVal, $box.data("spinboxReset"), 0 );
			var min		= firstNum( $box.data("spinboxMin") );
			var max		= firstNum( $box.data("spinboxMax") );
			var data	= [val,step,min,max,direction,oldVal,options];

			if( direction > 0 ) val = options.increment.apply( this,data ); else
			if( direction < 0 ) val = options.decrement.apply( this,data );
			if( isNum($box.data("spinboxScale")) && options.round ) val = options.round(val,$box.data("spinboxScale"));
			if( isNum(min) ) val = Math.max(val,min);
			if( isNum(max) ) val = Math.min(val,max);

			var data	= [val,step,min,max,direction,oldVal,options];
			if( val != oldVal && $box.triggerHandler("beforeSpin",data) !== false ){
				$box.val(val);
				if( options.change ) $box.trigger("change",data);
				$box.triggerHandler("spin",data);
			};
		};


		// Helper to begin autorepeat when mouse is held down:
		function startAutorepeat(elem,e){
			stopAutorepeat();
			// Start timer for initial delay:
			delayId = window.setTimeout(function(){
				adjustValue.apply( elem, [e] );
				// Start timer for repeating:
				repeatId = window.setInterval(function(){
					adjustValue.apply( elem, [e] );
				},options.repeat);
			},options.delay);
		};

		// Helper to end autorepeat when mouse is released:
		function stopAutorepeat(){
			window.clearTimeout(delayId);
			window.clearInterval(repeatId);
		};

	});

};



// Helper functions:

	// More reliable alternative to isNaN and isFinite:
	function isNum(num){
		return !isNaN(parseFloat(num))
	}

	// Helper to return the first parameter that is a valid number:
	function firstNum(args){
		for( var i=0; i<arguments.length; i++ ){
			if( isNum(arguments[i]) ) return Number( parseFloat(arguments[i]) );
		};
		return;
	};

	// Helper to extract settings stored in css class attribute string: (Eg: when class="spinbox spinboxStep0.5")
	function classData(attr,elem){

		var classes	= $(elem||this).attr("class"),
			match	= new RegExp( "(\\b" + attr + ")(\\S*)" ).exec(classes),
			lookupBoolean = { "true":true, "True":true, "false":false, "False":false };

		// Convert "True" or "False" string to boolean:
		if( match && match.length >= 3 && lookupBoolean[match[3]] !== undefined )
			match[3] = lookupBoolean[ match[3] ];

		return	!match ? undefined :			// attr not found in class string.
				match.length >= 3 ? match[2] :	// Specific value.
				null;							// attr specified but value deliberately blank.
	};





$.fn.spinbox.defaults = {
	min				: 0,						// Lower limit or null.
	max				: null,						// Upper limit or null.
	step			: 1,						// Size of standard increment.
	bigStep			: 10,						// Size of increment when Shift key is held down or when pageUp/pagedown is pressed.
	keys			: [ /[0-9]/,9,13,8,46,33,34,37,38,39,40,109,188,190,96,97,98,99,100,101,102,103,104,105 ],	// Array of regular expressions and/or char codes to restrict key input. Default: 0-9, Tab, Enter, Backspace, Delete, PageUp, PageDown, Left, Up, Right, Down, Minus, Comma, Dot.
	ignore			: "[readonly],[disabled]",	// Spinbox will not respond on elements matching this CSS selector.
	spinboxClass	: 'spinbox-active',			// Added to element when spinbox is initialised. Typically used to apply button image through css.
	upClass			: 'spinbox-up',				// Added to element while mouse is depressed on the Up button.
	downClass		: 'spinbox-down',			// Added to element while mouse is depressed on the Down button.
	upHoverClass	: 'spinbox-up-hover',		// Added to element while mouse is over the Up button.
	downHoverClass	: 'spinbox-down-hover',		// Added to element while mouse is over the Down button.
	mousewheel		: false,						// When true, spinbox will react to mousehweel.
	change			: true,						// When true, spinbox will trigger change event as it spins. (Otherwise change event is fired in the same ways as any normal textbox)
	increment		: function(val,step,min,max,options){ return val + step; },	// Custom function to calculate the value increment.
	decrement		: function(val,step,min,max,options){ return val - step; },	// Custom function to calculate the value decrement.
	reset			: null,						// Value used when element value is invalid. Specify null to default to element's initial value;
	delay			: 500,						// Initial delay before auto-repeat when mouse button held down. (Milliseconds)
	repeat			: 100,						// Interval between auto-repeats when mouse button held down. (Milliseconds)
	buttonWidth		: 30,						// Width of the button sprite image (so we can decide when mouse is over it).
	scale			: true,						// Specify number of DP, or true to auto derive from options.step (Eg: when step is 0.125 then DP will be 3)
	round			: function round(num,dp) {	// Specify your own decimalPlaces rounding function if you don't like this default one.
		return Math.round( num * Math.pow(10,dp) ) / Math.pow(10,dp);
	}
};

})(jQuery);
