/******************************************************************************************************

	jQuery.NobleCount

	Author Jeremy Horn
	Version 1.0
	Date: 3/21/2010

	Copyright (c) 2010 Jeremy Horn- jeremydhorn(at)gmail(dot)c0m | http://tpgblog.com
	Dual licensed under MIT and GPL.

	DESCRIPTION
		NobleCount... for a more 'proper' count of the characters remaining.
		
		NobleCount is a customizable jQuery plugin for a more the improved counting of the remaining 
		characters, and resulting behaviors, of a text entry object, e.g. input textfield, textarea.

		As text is entered into the target text area an object for the purposes of tracking
		the total number of characters remaining, defined as the maximum number of characters
		minus the current total number of characters within the text entry object, and storing
		that information visually and/or within the DOM as an HTML 5 compliant data-* attribute.
		
		Events and CSS Class alterations, if defined, are triggered based on current user 
		interaction with the target text entry object as well as the current state (positive or 
		negative) of the character remaining value.

		NobleCount supports pre-existing text within the text object.
		NobleCount supports jQuery chaining.

		Within NobleCount context...
			NEGATIVE is defined as Integers < 0
			POSITIVE is defined as Integers >= 0		[on_positive will fire when char_rem == 0]

		BY DEFAULT
		 - maximum characters EQUAL 140 characters
		 - no events defined
		 - no class changes defined
		 - no DOM attributes are created/altered
		 - user permitted to type past the maximum number of characters limit, resulting in
		   negative number of characters remaining
	
	IMPLEMENTATION

		$('#textarea1').NobleCount('#characters_remaining1');
		$('#textfield2').NobleCount('#characters_remaining2', { / * OPTIONS * / });
		
	COMPATIBILITY

		Tested in FF3.5, IE7
		With jQuery 1.3.x, 1.4.x

	METHOD(S)
		To properly intialize, both the text entry object and the object that will store the
		total number of characters remaining must exist and be passed to NobleCount.
		
			$(TEXT_ENTRY_OBJECT).NobleCount(CHARACTERS_REMAINING_OBJECT);
		
		Any callback functions assigned to any of the availale events are passed the following
		parameters: t_obj, char_area, c_settings, char_rem
		
			t_obj			text entry object
			
			char_area		selection of the characters remaining object
			
			c_settings		result of the options passed into NobleCount at time of 
							initialization merged with the default options
							
							** this is a GREAT way to pass in and remember other state
							information that will be needed upon the triggering of
							NobleCount events **
			
			char_rem		integer representation of the total number of characters
							remaining resulting from the calculated difference between
							the target maximum number of characters and the current
							number of characters currently within t_obj
		
		Both TEXT_ENTRY_OBJECT and CHARACTERS_REMAINING_OBJECT must be specified and valid.
		
		Upon successful initialization, all appropriate events and classes are applied to
		the CHARACTERS_REMAINING_OBJECT, including the storage (if not disabled) visually
		or only in the DOM (if enabled) of the integer representing the number of characters
		remaining.
		
		The target maximum number of characters (max_chars) are determined by the following 
		precedence rules....
		
				if max_chars passed via constructor
					max_chars = max_chars passed
				else if number exists within characters_remaining object and number > 0
					max_chars = number within the text() of characters_remaining object
				else use the NobleCount's default max_chars
		
	CUSTOMIZATION

		NobleCount(c_obj, <OPTIONS>)
		e.g. $(t_obj).NobleCount(c_obj, {max_chars:100px});


		on_negative				class (STRING) or FUNCTION that is applied/called 
								when characters remaining is negative IF DEFINED
									
		on_positive				class (STRING) or FUNCTION that is applied/called 
								when characters remaining is positive IF DEFINED
									
		on_update				FUNCTION that is called when characters remaining changes
								
		max_chars				target maximum number of characters
		
		block_negative			if TRUE, then all attempts are made to block entering 
									more than max_characters; not effective against user
									pasting in blocks of text that exceed the max_chars value
								otherwise, text area will let individual entering the text
									to exceed max_chars limit (characters remaining becomes
									negative)
								
		cloak: false,			if TRUE, then no visual updates of characters remaining 
									object (c_obj) will occur; this does not have any effect
									on the char_rem value returned via any event callbacks
								otherwise, the text within c_obj is constantly updated to
									represent the total number of characters remaining until
									the max_chars limit has been reached
								  
		in_dom: false			if TRUE and cloak is ALSO TRUE, then the number of characters
									remaining are stored as the attribute of c_obj
									named 'data-noblecount'
									
									!NOTE: if enabled, due to constant updating of a DOM element
										attribute user experience can appear sluggish while
										the individual is modifying the text entry object (t_obj)
								

		EXAMPLE	OPTIONS = 
			{
				on_negative: 'go_red',
				on_positive: 'go_green',
				max_chars: 25,
				on_update: function(t_obj, char_area, c_settings, char_rem){
					if ((char_rem % 10) == 0) {
						char_area.css('font-weight', 'bold');
						char_area.css('font-size', '300%');
					} else {
						char_area.css('font-weight', 'normal');
						char_area.css('font-size', '100%');
					}
				}
			};

	MORE

		For more details about NobleCount, its implementation, usage, and examples, go to:
		http://tpgblog.com/noblecount/

******************************************************************************************************/

(function($) {

	/**********************************************************************************

		FUNCTION
			NobleCount

		DESCRIPTION
			NobleCount method constructor
			
			allows for customization of maximum length and related update/length
			behaviors
			
				e.g. $(text_obj).NobleCount(characters_remaining_obj);

			REQUIRED: c_obj
			OPTIONAL: options

	**********************************************************************************/

	$.fn.NobleCount = function(c_obj, options) {
		var c_settings;
		var mc_passed = false;

		// if c_obj is not specified, then nothing to do here
		if (typeof c_obj == 'string') {
			// check for new & valid options
			c_settings = $.extend({}, $.fn.NobleCount.settings, options);

			// was max_chars passed via options parameter? 
			if (typeof options != 'undefined') {
				mc_passed = ((typeof options.max_chars == 'number') ? true : false);
			}

			// process all provided objects
			return this.each(function(){
				var $this = $(this);

				// attach events to c_obj
				attach_nobility($this, c_obj, c_settings, mc_passed);
			});
		}
		
		return this;
	};


	/**********************************************************************************

		FUNCTION
			NobleCount.settings

		DESCRIPTION
			publically accessible data stucture containing the max_chars and 
			event handling specifications for NobleCount
			
			can be directly accessed by	'$.fn.NobleCount.settings = ... ;'

	**********************************************************************************/
	$.fn.NobleCount.settings = {

		on_negative: null,		// class (STRING) or FUNCTION that is applied/called 
								// 		when characters remaining is negative
		on_positive: null,		// class (STRING) or FUNCTION that is applied/called 
								// 		when characters remaining is positive
		on_update: null,		// FUNCTION that is called when characters remaining 
								// 		changes
		max_chars: 140,			// maximum number of characters
		block_negative: false,  // if true, then all attempts are made to block entering 
								//		more than max_characters
		cloak: false,			// if true, then no visual updates of characters 
								// 		remaining (c_obj) occur
		in_dom: false			// if true and cloak == true, then number of characters
								//		remaining are stored as the attribute
								//		'data-noblecount' of c_obj
				
	};


	//////////////////////////////////////////////////////////////////////////////////

	// private functions and settings

	/**********************************************************************************

		FUNCTION
			attach_nobility
	
		DESCRIPTION
			performs all initialization routines and display initiation
			
			assigns both the keyup and keydown events to the target text entry
			object; both keyup and keydown are used to provide the smoothest
			user experience
	
				if max_chars_passed via constructor
					max_chars = max_chars_passed
				else if number exists within counting_object (and number > 0)
					max_chars = counting_object.number
				else use default max_chars
	
		PRE
			t_obj and c_obj EXIST
			c_settings and mc_passed initialized
			
		POST
			maximum number of characters for t_obj calculated and stored in max_char
			key events attached to t_obj
	
	**********************************************************************************/

	function attach_nobility(t_obj, c_obj, c_settings, mc_passed){
		var max_char 	= c_settings.max_chars;
		var char_area	= $(c_obj);

		// first determine if max_char needs adjustment
		if (!mc_passed) {
			var tmp_num = char_area.text();
			var isPosNumber = (/^[1-9]\d*$/).test(tmp_num);

			if (isPosNumber) {
				max_char = tmp_num;
			}
		}

		// initialize display of characters remaining
		// * note: initializing should not trigger on_update
		event_internals(t_obj, char_area, c_settings, max_char, true);

		// then attach the events -- seem to work better than keypress
		$(t_obj).keydown(function(e) {
			event_internals(t_obj, char_area, c_settings, max_char, false);

			// to block text entry, return false
			if (check_block_negative(e, t_obj, c_settings, max_char) == false) {
				return false;
			} 
		});

		$(t_obj).keyup(function(e) {
			event_internals(t_obj, char_area, c_settings, max_char, false);
			
			// to block text entry, return false
			if (check_block_negative(e, t_obj, c_settings, max_char) == false) {
				return false;
			} 
		});
	}


	/**********************************************************************************

		FUNCTION
			check_block_negative
	
		DESCRIPTION
			determines whether or not text entry within t_obj should be prevented
			
		PRE
			e EXISTS
			t_obj VALID
			c_settings and max_char initialized / calculated
			
		POST
			if t_obj text entry should be prevented FALSE is returned
				otherwise TRUE returned
	
		TODO
			improve selection detection and permissible behaviors experience
			ALSO
				doesnt CURRENTLY block from the pasting of large chunks of text that 
				exceed max_char
		
	**********************************************************************************/

	function check_block_negative(e, t_obj, c_settings, max_char){
		if (c_settings.block_negative) {
			var char_code = e.which;
			var selected;

			// goofy handling required to work in both IE and FF
			if (typeof document.selection != 'undefined') {
				selected = (document.selection.createRange().text.length > 0);
			} else {
				selected = (t_obj[0].selectionStart != t_obj[0].selectionEnd);
			}

			//return false if can't write more  
			if ((!((find_remaining(t_obj, max_char) < 1) &&
				(char_code > 47 || char_code == 32 || char_code == 0 || char_code == 13) &&
				!e.ctrlKey &&
				!e.altKey &&
				!selected)) == false) {
				
				// block text entry
				return false;
			}
		}
		
		// allow text entry
		return true;
	}


	/**********************************************************************************

		FUNCTION
			find_remaining
	
		DESCRIPTION
			determines of the number of characters permitted (max_char), the number of 
			characters remaining until that limit has been reached  
	
		PRE
			t_obj and max_char EXIST and are VALID
			
		POST
			returns integer of the difference between max_char and total number of
			characters within the text entry object (t_obj)
	
	**********************************************************************************/

	function find_remaining(t_obj, max_char){
		return max_char - ($(t_obj).val()).length;
	}


	/**********************************************************************************

		FUNCTION
			event_internals
	
		DESCRIPTION
			primarily used for the calculation of appropriate behavior resulting from
			any event attached to the text entry object (t_obj)
	
			whenever the char_rem and related display and/or DOM information needs
			updating this function is called

			if cloaking is being used, then no visual representation of the characters
			remaining, nor attempt by this plugin to change any of its visual 
			characteristics will occur
			
			if cloaking and in_dom are both TRUE, then the number of characters 
			remaining are stored within the HTML 5 compliant attribute of the
			character count remaining object (c_obj) labeled 'data-noblecount'
			
		PRE
			c_settings, init_disp initialized
			
		POST
			performs all updates to the DOM visual and otherwise required
			performs all relevant function calls
	
	**********************************************************************************/

	function event_internals(t_obj, char_area, c_settings, max_char, init_disp) {
		var char_rem	= find_remaining(t_obj, max_char);

		// is chararacters remaining positive or negative
		if (char_rem < 0) {
			toggle_states(c_settings.on_negative, c_settings.on_positive, t_obj, char_area, c_settings, char_rem);
		} else {
			toggle_states(c_settings.on_positive, c_settings.on_negative, t_obj, char_area, c_settings, char_rem);
		}
	
		// determine whether or not to update the text of the char_area (or c_obj)
		if (c_settings.cloak) {
			// this slows stuff down quite a bit; TODO: implement better method of publically accessible data storage
			if (c_settings.in_dom) {
				char_area.attr('data-noblecount', char_rem);
			}
		} else {
			// show the numbers of characters remaining 
			char_area.text(char_rem);
		}

		// if event_internals isn't being called for initialization purposes and
		// on_update is a properly defined function then call it on this update
		if (!init_disp && jQuery.isFunction(c_settings.on_update)) {
			c_settings.on_update(t_obj, char_area, c_settings, char_rem);
		} 
	}


	/**********************************************************************************

		FUNCTION
			toggle_states
	
		DESCRIPTION
			performs the toggling operations between the watched positive and negative
			characteristics
			
			first, enables/triggers/executes the toggle_on behavior/class
			second, disables the trigger_off class
	
		PRE
			toggle_on, toggle_off
				IF DEFINED, 
					must be a string representation of a VALID class
					OR
					must be a VALID function 
			
		POST
			toggle_on objects have been applied/executed
			toggle_off class has been removed (if it is a class)
	
	**********************************************************************************/

	function toggle_states(toggle_on, toggle_off, t_obj, char_area, c_settings, char_rem){
		if (toggle_on != null) {
			if (typeof toggle_on == 'string') {
				char_area.addClass(toggle_on);				
			} else if (jQuery.isFunction(toggle_on)) {
				toggle_on(t_obj, char_area, c_settings, char_rem);
			}
		}
		
		if (toggle_off != null) {
			if (typeof toggle_off == 'string') {
				char_area.removeClass(toggle_off);				
			}
		}		
	}
})(jQuery);
