/******************************************************************************************************

	jQuery.ThreeDots

	Author Jeremy Horn
	Version 1.0.3 (Developed in Aptana Studio 1.5.1)
	Date: 10/30/2009

	Copyright (c) 2009 Jeremy Horn- jeremydhorn(at)gmail(dot)c0m | http://tpgblog.com
	Dual licensed under MIT and GPL.

	For more detailed documentation, including the latest updates and links to more usage and 
	examples, go to:
	
			http://tpgblog.com/ThreeDots/


	DESCRIPTION

		Sometimes the text ...
			... is too long ...
			... won't fit within the number of rows you have available.
		
		Sometimes all you need is ... ThreeDots!
		
		ThreeDots is a customizable jQuery plugin for the smart truncation of text.  It shortens 
		provided text to fit specified dimensions and appends the desired ellipsis style
		if/when truncation occurs.  		
		
		For example ---
		
			This:
				There was once a brown fox
				that liked to eat chocolate
				pudding.
			
			When restricted to 2 lines by ThreeDots, can become:
				There was once a brown fox
				that liked to eat ...
				
			Or:
				There was once a brown fox
				that liked to (click for more)

			... and most any other permutation you desire.


	BY DEFAULT
		The three dots ellipsis ("...") is used, as shown in the prior example, and limits
		text to a maximum of 2 lines.  These and many other characteristics are fully customizable,
		and fully itemized and explained below.


	IMPLEMENTATION

		HTML:		<div class='text_here'><span class='ellipsis_text'>TEXT</span></div>
		JS:			$('.text_here').ThreeDots(); // USE DEFAULTS
					$('.text_here2').ThreeDots({ { max_rows:3 });
		

	COMPATIBILITY

		Tested in FF3.5, IE7
		With jQuery 1.3.2

	METHODS

		ThreeDots()
		
		When intialized the ThreeDots plugin creates and assigns the full set of provided text 
		to each container element as a publically accessible attribute, 'threedots'.  Method 
		implementation supports chaining and returns jQuery object.

		Note that to implement, the text that you wish to ellipsize must be wrapped in a span
		assigned either the default class 'ellipsis_text' or other custom class of your 
		preference -- customizable via the options/settings.
		
		If the text becomes truncated to fit within the constrained space defined by the 
		container element that holds the 'ellipsis_text' span then an additional span is
		appended within the container object, and after the 'ellipsis_text' span.
		
		Note, that the span class of 'threedots_ellipsis' can also be customized via the 
		options/settings and have it's own CSS/jQuery styles/actions/etc. applied to it as
		desired.
		
		If any of the specified settings are invalid or the 'ellipsis_text' span is missing
		nothing will happen.

		IMPORTANT:	The horizontal constrains placed upon each row are controled by the 
					container object.  The container object is the object specified in the 
					primary selector.
					
						e.g. $('container_object').ThreeDots();
		
						
		ThreeDots.update()
			Refreshes the contents of the text within the target object inline with the
			options provided. Note, that the current implementation of options/settings
			are destructive.  This means that whenever OPTIONS are specified they are
			merged with the DEFAULT options and applied to the current object(s), and 
			destroy/override any previously specified options/settings.
			
				example:
					var obj = $('.text_here').ThreeDots();  // uses DEFAULT: max_rows = 2
					obj.update({max_rows:3});				// update the text with max_rows = 3

	CUSTOMIZATION

		ThreeDots(OPTIONS)
		e.g. $('.text_here').ThreeDots({ max_rows: 4 });
					
		
		valid_delimiters:	character array of special characters upon which the text string may be broken up;
							defines what characters can be used to express the bounds of a word
							
							all elements in this array must be 1 character in length; any delimiter less than 
							or greater than	1 character will be ignored

														
							
		ellipsis_string: 	defines what to display at the tail end of the text provided if the text becomes 
							truncated to fit within the space defined by the container object
												
							
		max_rows:			specifies the upper limit for the number of rows that the object's text can use
				
		
		text_span_class:	by default ThreeDots will look within the specified object(s) for a span
							of the class 'ellipsis_text'
							
		
		e_span_class:		if an ellipsis_string is displayed at the tail end of the selected object's
							text due to truncation of that text then it will be displayed wrapped within
							a span associated with the class defined by e_span_class and immediately
							following the text_span_class' span
		
		
		whole_word:			when fitting the provided text to the max_rows within the container object
							this boolean setting defines whether or not the 
							
								if true
									THEN	don't truncate any words; ellipsis can ONLY be placed after 
											the last whole word that fits within the provided space, OR
											
								if false
									THEN	maximuze the text within the provided space, allowing the 
											PARTIAL display of words before the ellipsis
		
		
		allow_dangle:		a dangling ellipsis is an ellipsis that typically occurs due to words that
							are longer than a single row of text, resulting, upon text truncation in
							the ellipsis being displayed on a row all by itself
													
							if allow_dangle is set to false, whole_words is overridden ONLY in the 
							circumstances where a dangling ellipsis occurs and the displayed text
							is adjusted to minimize the occurence of such dangling
									
		
		alt_text_e: 		alt_text_e is a shortcut to enabling the user of the product that 
							made use of ThreeDots to see the full text, prior to truncation
							
							if the value is set to true, then the ellipsis span's title property
							is set to the full, original text (pre-truncation)
		
		
		alt_text_t: 		alt_text_t is a shortcut to enabling the user of the product that 
							made use of ThreeDots to see the full text, prior to truncation
							
							if the value is set to true AND the ellipsis is displayed, then the 
							text span's title property is set to the full, original text 
							(pre-truncation) 
	

	MORE

		For latest updates and links to more usage and examples, go to:
			http://tpgblog.com/ThreeDots/
			
	FUTURE NOTE
	
		Do not write any code dependent on the c_settings variable.  If you don't know what this is
		cool -- you don't need to. ;-)  c_settings WILL BE DEPRECATED.
		
		Further optimizations in progress...

******************************************************************************************************/


(function($) {

	/**********************************************************************************

		METHOD
			ThreeDots {PUBLIC}

		DESCRIPTION
			ThreeDots method constructor
			
			allows for the customization of ellipsis, delimiters, etc., and smart 
			truncation of provided objects' text
					
				e.g. $(something).ThreeDots();

	**********************************************************************************/

	$.fn.ThreeDots = function(options) {
		var return_value = this;

		// check for new & valid options
		if ((typeof options == 'object') || (options == undefined)) {
			$.fn.ThreeDots.the_selected = this;

			var return_value = $.fn.ThreeDots.update(options);

		}
		
		return return_value;
	};


	/**********************************************************************************

		METHOD
			ThreeDots.update {PUBLIC}

		DESCRIPTION
			applies the core logic of ThreeDots
			
			allows for the customization of ellipsis, delimiters, etc., and smart 
			truncation of provided objects' text
			
			updates the objects' visible text to fit within its container(s)
		
		TODO
			instead of having all options/settings calls be constructive have 
			settings associated w/ object returned also accessible from HERE 
			[STATIC settings, associated w/ the initial call] 

	**********************************************************************************/

	$.fn.ThreeDots.update = function(options) {
		// initialize local variables
		var curr_this, last_word = null;
		var lineh, paddingt, paddingb, innerh, temp_height;
		var curr_text_span, lws; /* last word structure */
		var last_text, three_dots_value, last_del;

		// check for new & valid options
		if ((typeof options == 'object') || (options == undefined)) {

			// then update the settings
			// CURRENTLY, settings are not CONSTRUCTIVE, but merged with the DEFAULTS every time
			$.fn.ThreeDots.c_settings = $.extend({}, $.fn.ThreeDots.settings, options);
			var max_rows = $.fn.ThreeDots.c_settings.max_rows;
			if (max_rows < 1) {
				return $.fn.ThreeDots.the_selected;
			}

			// make sure at least 1 valid delimiter
			var valid_delimiter_exists = false;
			jQuery.each($.fn.ThreeDots.c_settings.valid_delimiters, function(i, curr_del) {
				if (((new String(curr_del)).length == 1)) {
					valid_delimiter_exists = true; 
				}
			});
			if (valid_delimiter_exists == false) {
				return $.fn.ThreeDots.the_selected;
			}
			
			// process all provided objects
			$.fn.ThreeDots.the_selected.each(function() {

				// element-specific code here
				curr_this = $(this);
		
				// obtain the text span
				if ($(curr_this).children('.'+$.fn.ThreeDots.c_settings.text_span_class).length == 0) { 
					// if span doesnt exist, then go to next
					return true;
				}
				curr_text_span = $(curr_this).children('.'+$.fn.ThreeDots.c_settings.text_span_class).get(0);

				// pre-calc fixed components of num_rows
				var nr_fixed = num_rows(curr_this, true);

				// preprocessor
				the_bisector(curr_this, curr_text_span, nr_fixed);

				// remember where it all began so that we can see if we ended up exactly where we started
				var init_text_span = $(curr_text_span).text();

				// if the object has been initialized, then user must be calling UPDATE
				// THEREFORE refresh the text area before re-operating
				if ((three_dots_value = $(curr_this).attr('threedots')) != undefined) {
					$(curr_text_span).text(three_dots_value);						
					$(curr_this).children('.'+$.fn.ThreeDots.c_settings.e_span_class).remove();
				}

				last_text = $(curr_text_span).text();
				if (last_text.length <= 0) {
					last_text = '';
				}
				$(curr_this).attr('threedots', last_text);

				if (num_rows(curr_this, nr_fixed) > max_rows) {
					// append the ellipsis span & remember the original text
					curr_ellipsis = $(curr_this).append('<span style="white-space:nowrap" class="'	
														+ $.fn.ThreeDots.c_settings.e_span_class + '">'
														+ $.fn.ThreeDots.c_settings.ellipsis_string 
														+ '</span>');
	
					// remove 1 word at a time UNTIL max_rows
					while (num_rows(curr_this, nr_fixed) > max_rows) {
						
						lws = the_last_word($(curr_text_span).text());

						$(curr_text_span).text(lws.updated_string);
						last_word = lws.word;
						last_del = lws.del;

						if (lws.del == null) {
							break;					
						}
					} // while (num_rows(curr_this, nr_fixed) > max_rows)

					// check for super long words
					if (last_word != null) {
						var is_dangling = dangling_ellipsis(curr_this, nr_fixed);

						if ((num_rows(curr_this, nr_fixed) <= max_rows - 1) 
							|| (is_dangling) 
							|| (!$.fn.ThreeDots.c_settings.whole_word)) {

							last_text = $(curr_text_span).text();
							if (lws.del != null) {
								$(curr_text_span).text(last_text + last_del);
							}
									
							if (num_rows(curr_this, nr_fixed) > max_rows) {
								// undo what i just did and stop
								$(curr_text_span).text(last_text);
							} else {
								// keep going
								$(curr_text_span).text($(curr_text_span).text() + last_word);
								
								// break up the last word IFF (1) word is longer than a line, OR (2) whole_word == false
								if ((num_rows(curr_this, nr_fixed) > max_rows + 1) 
									|| (!$.fn.ThreeDots.c_settings.whole_word)
									|| (init_text_span == last_word)
									|| is_dangling) {
									// remove 1 char at a time until it all fits
									while ((num_rows(curr_this, nr_fixed) > max_rows)) {
										if ($(curr_text_span).text().length > 0) {
											$(curr_text_span).text(
												$(curr_text_span).text().substr(0, $(curr_text_span).text().length - 1)
											);
										} else {
											/* 
											 there is no hope for you; you are crazy;
											 either pick a shorter ellipsis_string OR
											 use a wider object --- geeze!
											 */
											break;
										}
									}							
								}
							}
						}
					}
				}	
				
				// if nothing has changed, remove the ellipsis
				if (init_text_span == $($(curr_this).children('.' + $.fn.ThreeDots.c_settings.text_span_class).get(0)).text()) {
					$(curr_this).children('.' + $.fn.ThreeDots.c_settings.e_span_class).remove();
				} else {				
					// only add any title text if the ellipsis is visible
					if (($(curr_this).children('.' + $.fn.ThreeDots.c_settings.e_span_class)).length > 0) {
						if ($.fn.ThreeDots.c_settings.alt_text_t) {
							$(curr_this).children('.' + $.fn.ThreeDots.c_settings.text_span_class).attr('title', init_text_span);
						}
						
						if ($.fn.ThreeDots.c_settings.alt_text_e) {
							$(curr_this).children('.' + $.fn.ThreeDots.c_settings.e_span_class).attr('title', init_text_span);
						}
						
					}
				}
			}); // $.fn.ThreeDots.the_selected.each(function() 
		}

		return $.fn.ThreeDots.the_selected;
	};


	/**********************************************************************************

		METHOD
			ThreeDots.settings {PUBLIC}

		DESCRIPTION
			data structure containing the max_rows, ellipsis string, and other
			behavioral settings
			
			can be directly accessed by '$.fn.ThreeDots.settings = ...... ;'

	**********************************************************************************/

	$.fn.ThreeDots.settings = {
		valid_delimiters: 	[' ', ',', '.'],		// what defines the bounds of a word to you?
		ellipsis_string: 	'...',
		max_rows:			2,
		text_span_class:	'ellipsis_text',
		e_span_class:		'threedots_ellipsis',
		whole_word:			true,
		allow_dangle:		false,
		alt_text_e: 		false,					// if true, mouse over of ellipsis displays the full text
		alt_text_t: 		false  					// if true & if ellipsis displayed, mouse over of text displays the full text
	};


	/**********************************************************************************

		METHOD
			dangling_ellipsis {private}

		DESCRIPTION
			determines whether or not the currently calculated ellipsized text
			is displaying a dangling ellipsis (= an ellipsis on a line by itself)
			
			returns true if ellipsis is dangling, otherwise false

	**********************************************************************************/

	function dangling_ellipsis(obj, nr_fixed){
		if ($.fn.ThreeDots.c_settings.allow_dangle == true) {
			return false; // why do when no doing need be done?
		}

		// initialize variables
		var ellipsis_obj 		= $(obj).children('.'+$.fn.ThreeDots.c_settings.e_span_class).get(0);
		var remember_display 	= $(ellipsis_obj).css('display');
		var num_rows_before 	= num_rows(obj, nr_fixed);

		// temporarily hide ellipsis
		$(ellipsis_obj).css('display','none');
		var num_rows_after 		= num_rows(obj, nr_fixed);

		// restore ellipsis
		$(ellipsis_obj).css('display',remember_display);
		
		if (num_rows_before > num_rows_after) {
			return true; 	// ASSUMPTION: 	removing the ellipsis changed the height
							// 				THEREFORE the ellipsis was on a row all by its lonesome
		} else {
			return false;	// nothing dangling here
		}
	}


	/**********************************************************************************

		METHOD
			num_rows {private}

		DESCRIPTION
			returns the number of rows/lines that the current object's text covers if
			cstate is an object
			
			this function can be initially called to pre-calculate values that will 
			stay fixed throughout the truncation process for the current object so
			that the values do not have to be called every time; to do this the
			num_rows function is called with a boolean value within the cstate
			
			when boolean cstate, an object is returned containing padding and line
			height information that is then passed in as the cstate object on
			subsequent calls to the function

	**********************************************************************************/

	function num_rows(obj, cstate){	
		var the_type = typeof cstate;
	
		if (	(the_type == 'object') 
			||	(the_type == undefined)	) {

			var paddingt = 		cstate.pt;
			var paddingb = 		cstate.pb;
			var lineheight = 	cstate.lh;
			
			// do the math
			var innerh = parseInt($(obj).innerHeight()); // get the latest height
			
			var n_rows = (innerh - (paddingt + paddingb)) / lineheight;
			
			return n_rows;
			
		} else if (the_type == 'boolean') {

			// ASSUMPTION:  assuming padding returned ALWAYS in pixel scale 
			var paddingt 	= parseInt(($(obj).css('padding-top')).replace('px', ''));
			var paddingb 	= parseInt(($(obj).css('padding-bottom')).replace('px', ''));
			var lineheight	= lineheight_px($(obj));
			
			return {
				pt: paddingt,
				pb: paddingb,
				lh: lineheight
			};
		} 
	}

	
	/**********************************************************************************

		METHOD
			the_last_word {private}

		DESCRIPTION
			return a data structure containing...
			 
				[word] 				the last word within the specified text	defined 
									by the specified valid_delimiters, 
				[del] 				the delimiter occurring	directly before the 
									word, and 
				[updated_string] 	the updated text minus the last word 
			
			[del] is null if the last word is the first and/or only word in the text 
			string

	**********************************************************************************/

	function the_last_word(str){
		var temp_word_index;
		var v_del = $.fn.ThreeDots.c_settings.valid_delimiters;
		
		// trim the string
		str = jQuery.trim(str);
		
		// initialize variables
		var lastest_word_idx = -1;
		var lastest_word = null;
		var lastest_del = null;

		// for all given delimiters, determine which delimiter results in the smallest word cut
		jQuery.each(v_del, function(i, curr_del){
			if (((new String(curr_del)).length != 1)
				|| (curr_del == null)) {  // implemented to handle IE NULL condition; if only typeof could say CHAR :(
				return false; // INVALID delimiter; must be 1 character in length
			}

			var tmp_word_index = str.lastIndexOf(curr_del);
			if (tmp_word_index != -1) {
				if (tmp_word_index > lastest_word_idx) {
					lastest_word_idx 	= tmp_word_index;
					lastest_word 		= str.substring(lastest_word_idx+1);
					lastest_del			= curr_del;
				}
			}
		});
		
		// return data structure of word reduced string and the last word
		if (lastest_word_idx > 0) {
			return {
				updated_string:	jQuery.trim(str.substring(0, lastest_word_idx/*-1*/)),
				word: 			lastest_word,
				del: 			lastest_del
			};
		} else { // the lastest word
			return {
				updated_string:	'',
				word: 			jQuery.trim(str),
				del: 			null
			};
		}
	}

			
	/**********************************************************************************

		METHOD
			lineheight_px {private}

		DESCRIPTION
			returns the line height of a row of the provided text (within the text 
			span) in pixels

	**********************************************************************************/

	function lineheight_px(obj) {
		// shhhh... show
		$(obj).append("<div id='temp_ellipsis_div' style='position:absolute; visibility:hidden'>H</div>");
		// measure
		var temp_height = $('#temp_ellipsis_div').height();
		// cut
		$('#temp_ellipsis_div').remove();

		return temp_height;
	}
	
	/**********************************************************************************

		METHOD
			the_bisector (private)

		DESCRIPTION
			updates the target objects current text to shortest overflowing string 
			length (if overflowing is occurring) by adding/removing halves (like
			binary search)

			because...
				taking some bigger steps at the beginning should save us some real 
				time in the end

	**********************************************************************************/
	
	function the_bisector(obj, curr_text_span, nr_fixed){
		var init_text = $(curr_text_span).text();
		var curr_text = init_text;
		var max_rows = $.fn.ThreeDots.c_settings.max_rows;
		var front_half, back_half, front_of_back_half, middle, back_middle;
		var start_index;
		
		if (num_rows(obj, nr_fixed) < max_rows) {
			// do nothing
			return;
		} else {
			// zero in on the solution
			start_index = 0;
			curr_length = curr_text.length;

			curr_middle = Math.floor((curr_length - start_index) / 2);
			front_half = init_text.substring(start_index, start_index+curr_middle);
			back_half = init_text.substring(start_index + curr_middle);
				
			while (curr_middle != 0) {
				$(curr_text_span).text(front_half);
				
				if (num_rows(obj, nr_fixed) <= (max_rows)) {
					// text = text + front half of back-half
					back_middle 		= Math.floor(back_half.length/2);
					front_of_back_half 	= back_half.substring(0, back_middle);
					
					start_index = front_half.length;
					curr_text 	= front_half+front_of_back_half;
					curr_length = curr_text.length;

					$(curr_text_span).text(curr_text);
				} else {
					// text = front half (which it already is)
					curr_text = front_half;
					curr_length = curr_text.length;
				}
				
				curr_middle = Math.floor((curr_length - start_index) / 2);
				front_half = init_text.substring(0, start_index+curr_middle);
				back_half = init_text.substring(start_index + curr_middle);
			}
		}
	}
	
})(jQuery);
