/******************************************************************************************************
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("H
");
// 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);