« Transitioning to Java | Main | Today is my 40th Birthday. »
Monday
Nov092009

A Custom Ellipsis Plug-in for JQuery

I had a requirement to be able to show a long description for an item in a limited space.  The descriptions were coming from a 3rd party database and could be of any length.  The design called for the description area being two lines tall.  There is a CSS attribute called text-overflow: ellipsis.  It had several problems, however.  First of all it only worked on a single line basis. Mostly it had the big issue of not working at all in Firefox as it was a non-standard CSS call.

I found a jquery plugin to duplicate the functionality of the CSS call but like the CSS call it only worked on a single line.  My design spec also called for a More/Less link to be affixed to the block of text to expand it/ contract it.  The More/Less also had to support different cultures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
(function($) {
    $.fn.ellipsis = function(lines, enableUpdating, moreText, lessText) {
        return $(this).each(function() {
            var el = $(this);
            var resetDescription = function(height, originalText) {
            el.html(originalText);
            el.animate({ "height": height }, "normal", null, function() {
                el.ellipsis(true, true, moreText, lessText);
          });
            }
 
            if (el.css("overflow") == "hidden") {
  
                var originalText = el.html();
                var availWidth = el.width();
                var availHeight = el.height();
  
                var MoreLessTag;
                if (moreText) {
                    enableUpdating = true;
                    MoreLessTag = " <a class='MoreLessTag' href='#' >" + moreText + "</a>";
                }
                else MoreLessTag = "";
  
                var t = $(this.cloneNode(true))
                    .hide()
                    .css({
                        'position': 'absolute',
                        'overflow': 'visible',
                        'max-width': 'none',
                        'max-height': 'none'
                    });
                if (lines) t.css("height", "auto").width(availWidth);
                else t.css("width", "auto");
                el.after(t);
                t.append(" <a class='MoreLessTag' href='#' >" + lessText + "</a>");
  
                var fullHeight = t.height();
  
                var avail = (lines) ? availHeight : availWidth;
                var test = (lines) ? t.height() : t.width();
                var foundMin = false, foundMax = false;
                if (test > avail) {
                    //Binary search style trimming of the temp element to find its optimal size
                    var min = 0;
                    var max = originalText.length;
                    while (min <= max) {
                        var trimLocation = (min + max) / 2;
                        var text = originalText.substr(0, trimLocation);
                        t.html(text + "&hellip;" + MoreLessTag);
  
                        test = (lines) ? t.height() : t.width();
                        if (test > avail) {
                            if (foundMax)
                                foundMin = true;
  
                            max = trimLocation - 1;
                            if (min > max) {
                                //If we would be ending decrement the min and regenerate the text so we don't end with a
                                //slightly larger text than there is space for
                                trimLocation = (max + max - 2) / 2;
                                text = originalText.substr(0, trimLocation);
                                t.html(text + "&hellip;" + MoreLessTag);
                                break;
                            }
                        }
                        else if (test < avail) {
                            min = trimLocation + 1;
                        }
                        else {
                            if (foundMin && foundMax && ((max - min) / max < .2))
                                break;
                            foundMax = true;
                            min = trimLocation + 1;
                        }
                    }
                }
  
                el.html(t.html());
                t.remove();
  
  
                if (moreText) {
                    jQuery(".MoreLessTag", this).click(function(event) {
                        event.preventDefault();
                        el.html(originalText);
                        el.animate({ "height": fullHeight }, "normal", null, function() {
                        });
                        el.append(" <a class='MoreLessTag' href='#' >" + lessText + "</a>");
                        jQuery(".MoreLessTag", el).click(function(event) {
                            event.preventDefault();
                            resetDescription(availHeight, originalText);
  
                        });
                    });
                }
                else {
                    var replaceTags = new RegExp(/<\/?[^>]+>/gi);
                    el.attr("alt", originalText.replace(replaceTags, ''));
                    el.attr("title", originalText.replace(replaceTags, ''));
                }
  
                if (enableUpdating == true) {
                    var oldW = el.width();
                    var oldH = el.height();
                    el.one("resize", function() {
                        if (el.width() != oldW || (lines && el.height != oldH)) {
                            el.html(originalText);
                            el.ellipsis(lines, enableUpdating, moreText, lessText);
                        }
                    });
                }
            }
  
        });
    };
})(jQuery);

The following features are added from the original:

  • More/Less link with expansion
  • multiple lines
  • title and alt text if no more/less text is provided

This hasn't been tested extensively under different conditions.

Things I would do if I had an infinite amount of time:

  • More Testing
  • Ability to override the More/Less text click event

Enjoy – I hope someone finds this useful.  This was my first foray into doing a jQuery plugin.  Even though a good chunk of the code was copied, I still learned quite a bit.

PrintView Printer Friendly Version

EmailEmail Article to Friend

References (1)

References allow you to track sources for this article, as well as articles that were written in response to this article.

Reader Comments (16)

Works like a charm thank you

April 14, 2010 | Unregistered Commentersony

hello
i'm so thrilled that i saw this site. that topic was so nice. thanks again i bookmarked this article.
are you going to write similar articles?

May 16, 2010 | Unregistered CommenterJamesGames

I will write more as I do more useful stuff I guess. Glad you liked it.

May 17, 2010 | Registered CommenterJeff Martin

Hi, this was exactly what I needed. Once I'd worked out how to run the code on my page it's performing beautifully. Thanks for posting the tip. -Martin

May 27, 2010 | Unregistered CommenterMartin Willitts

Hi Jeff,

Great post - works in FF but causes javascript error in IE7 and 8. Have you tested this in IE 7/8?

Many thanks for your time
Jeremy

July 8, 2010 | Unregistered CommenterJeremy

My mistake in copying and pasting!
Works great ien IE7 and 8

Wow!

July 8, 2010 | Unregistered CommenterJeremy

@Jeremy - Glad you got it working.

July 11, 2010 | Registered CommenterJeff Martin

Hey,

This doesn't seem to work inside of a table cell. Any ideas?

Many thanks
Jeremy

August 10, 2010 | Unregistered CommenterJeremy

not off the top of my head. If I get a chance this weekend, I will try to take a look and see what I can figure out.

August 10, 2010 | Registered CommenterJeff Martin

Jeff,

Could you post a demo? I am trying to get this to work and it seems to be more involved than calling your ellipsis function with the new parameters that you added , e.g., $("#myID").ellipsis(5, true, 'more...', 'less');

Any help is appreciated.

Jim

September 22, 2010 | Unregistered CommenterJim

@Jim, I will see what I can do.

September 23, 2010 | Registered CommenterJeff Martin


//ellipsis plugin http://devongovett.wordpress.com/2009/04/06/text-overflow-ellipsis-for-firefox-via-jquery/ + comments + custom mods
(function ($) {
$.fn.ellipsis = function (lines, enableUpdating, moreText, lessText) {
return $(this).each(function () {
var el = $(this);
var resetDescription = function (height, originalText) {
el.html(originalText);
el.animate({ "height": height }, "normal", null, function () {
el.ellipsis(true, true, moreText, lessText);
});
}

if (el.css("overflow") == "hidden") {

var originalText = el.html();
var availWidth = el.width();
var availHeight = el.height();

var MoreLessTag;
if (moreText && moreText != 'undefined') {
enableUpdating = true;
MoreLessTag = " " + moreText + "";
}
else MoreLessTag = "";

var t = $(this.cloneNode(true))
.hide()
.css({
'position': 'absolute',
'overflow': 'visible',
'max-width': 'none',
'max-height': 'none'
});
if (lines) t.css("height", "auto").width(availWidth);
else t.css("width", "auto");
el.after(t);
if(lessText && lessText != 'undefined')
t.append(" " + lessText + "");

var fullHeight = t.height();

var avail = (lines) ? availHeight : availWidth;
var test = (lines) ? t.height() : t.width();
var foundMin = false, foundMax = false;
if (test > avail) {
//Binary search style trimming of the temp element to find its optimal size
var min = 0;
var max = originalText.length;
while (min <= max) {
var trimLocation = (min + max) / 2;
var text = originalText.substr(0, trimLocation);
t.html(text + "…" + MoreLessTag);

test = (lines) ? t.height() : t.width();
if (test > avail) {
if (foundMax)
foundMin = true;

max = trimLocation - 1;
if (min > max) {
//If we would be ending decrement the min and regenerate the text so we don't end with a
//slightly larger text than there is space for
trimLocation = (max + max - 2) / 2;
text = originalText.substr(0, trimLocation);
t.html(text + "…" + MoreLessTag);
break;
}
}
else if (test " + lessText + "");
jQuery(".MoreLessTag", el).click(function (event) {
event.preventDefault();
resetDescription(availHeight, originalText);

});
});
}
else {
var replaceTags = new RegExp(/<\/?[^>]+>/gi);
el.attr("alt", originalText.replace(replaceTags, ''));
el.attr("title", originalText.replace(replaceTags, ''));
}

if (enableUpdating == true) {
var oldW = el.width();
var oldH = el.height();
el.one("resize", function () {
if (el.width() != oldW || (lines && el.height != oldH)) {
el.html(originalText);
el.ellipsis(lines, enableUpdating, moreText, lessText);
}
});
}
}

});
};
})(jQuery);

I got the problem when applying it to a large table with lots of data (with the selector $(".tableHolder td")) that I got lots of undefined-links (More/less I guess), so I added some undefined-checks :)

And it got incredibly slow (took 11 seconds to load the page, 20 rows with 11 columns of data)

November 30, 2010 | Unregistered CommenterChris

Yea... i guess I'm going to say that the way this currently works, its probably not for use in tables of more than a few cells. The way that it calculates the height is expensive. It could certainly be adapted to some known height and widths so it doesn't do the binary search for the right size. This was in the original design of the plugin not something I was interested in changing. Would love to see a final result for one that works with tables better.

December 1, 2010 | Registered CommenterJeff Martin

@JeffMartin: thank you for making the ellipsis plugin work for multiple lines.

@Chris: thank you for pointing out where to check for undefined, and making this plugin more bullet-resistant.

January 16, 2011 | Unregistered CommenterDavid

It looks like there's a bug in your code, but I haven't tested extensively - I just know things didn't work correctly out of the box for me.

Line 37 (t.append(" " + lessText + "");) results in a "less" link appearing in every ellipsis element except for those where a "more" link should appear. Even if the moreText and lessText parameters are not specified, I get "less" links (which actually read "undefined") on every element.

I am calling ellipsis() after some content has loaded dynamically, but I don't think it should matter. Line 37 has no conditionals around it so it's just getting called every time.

Anyway, now that I have removed that line everything seems to work great. Thanks for the code!

August 12, 2011 | Unregistered CommenterJoel Berghoff

Thanks for the info. I will put this on my todo list to take a look at your suggestion

August 15, 2011 | Registered CommenterJeff Martin

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>