/****************************************************************************************************** jQuery.ThreeDots Author Jeremy Horn Version 1.0.10 (Developed in Aptana Studio 1.5.1) Date: 1/25/2010 Copyright (c) 2010 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/ KNOWN BUGS None 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:
TEXT
JS: $('.text_here').ThreeDots(); // USE DEFAULTS $('.text_here2').ThreeDots({ { max_rows:3 }); COMPATIBILITY Tested in FF3.5, IE7, Chrome With jQuery 1.3.x, 1.4 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(); So, remember to set container_object's WIDTH. 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); // 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(); // preprocessor the_bisector(curr_this, curr_text_span, nr_fixed); var init_post_b = $(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', init_text_span); if (num_rows(curr_this, nr_fixed) > max_rows) { // append the ellipsis span & remember the original text curr_ellipsis = $(curr_this).append('' + $.fn.ThreeDots.c_settings.ellipsis_string + ''); // 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());// HERE $(curr_text_span).text(lws.updated_string); last_word = lws.word; last_del = lws.del; if (last_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_post_b == 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) ) { // do the math & return return $(obj).height() / cstate.lh; } else if (the_type == 'boolean') { var lineheight = lineheight_px($(obj)); return { 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(""); // 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);