Truncating Styled Text to Fit a Container Using JQuery
Posted by M. David Green on September 12th, 2009 filed in CSS, HTML, Javascript, JQueryRecently I had the need to truncate a section of styled HTML text so it would fit inside a container with a specific pixel width. You might have thought this would be something pretty easy to make happen with a little JQuery and some .css() and .width() commands.
You would have been mistaken. I know I was.
Among the tricky aspects was my desire that the text retain its styling, regardless of the width of the container. I also wanted it to truncate at the appropriate complete letter, rather than chop a word off in the middle of a letter. I wanted to be able to specify a string to use to indicate truncation (eg: “…”). I wanted the complete text to be preserved, and added to the element as a title tag so that it would be available to readers who hovered over the truncated text. And of course, I wanted it to work seamlessly across browsers.
What I decided I needed was a temporary DOM object where I could copy the styled text. I needed something that would report its width based on the width of the text it contained. That ruled out both block elements such as divs (which take their width from their container) and inline elements such as spans (which report zero when queried about their width, regardless of their actual contents). What to do?
Then I realized that tables adjust their width to the width of their contents, AND are able to report that width when queried. Eureka! I just needed to add a temporary table to the DOM somewhere that it would be untouched by other styling, and make sure all the elements that made up the table had no padding, margin, or border settings. I didn’t need to worry about the semantics of using a table this way since I was going to remove it from the DOM as soon as I was done with it. You won’t hear me saying things like that very often, but in this case even I was satisfied.
To copy the styled text, I had to take a two-step approach. After creating a temporary table, I copied the contents of the original element I wanted to measure, and then copied each of the original element’s font-relates styles and applied them to the inner td of the table. Firefox immediately rewarded me with an accurate measurement, which I could use in a recursive loop to chop away one letter at a time until I had the width I desired. It looked like I was going to be done with this one quickly.
I replaced the contents of the original element with the truncated text easily. I tossed in a call to cache the full text and apply it as a title attribute if the text needed truncating. I even made a quick swap of the truncation indicator text (“…”) into the styled table cell from the start, so I could account for the pixels that added to the width. It was looking pretty sweet.
Until I tested it in Internet Explorer.
Now the creepy thing was that it even worked in Internet Explorer with the font size set to the default value. But as soon as I tried font sizes that were larger or smaller, I started seeing the width vary in odd ways. Larger fonts would result in a reported width that was wider by an ever-increasing factor, resulting in truncated strings that were too short for the space. Smaller fonts did the opposite, reporting a narrower width than they actually required, resulting in truncated strings too long to fit the desired width. It wasn’t a straight line rate of change; I could see there was some mysterious equation at work here.
I will admit it took me a few hours of fiddling, and I had to dust off that part of my brain that was paying attention during high school algebra. What I discovered (and this is the golden nugget in this chicken soup of a blog posting) was that Microsoft calculates the pixel size of rendered text based on the square root of the desired font size divided by a base font size of 16.
That works out like this in Internet Explorer:
Math.sqrt(parseFloat($('#element').css("font-size"))/16)
Whereas other browsers use a formula more like this:
parseFloat($('#element').css("font-size"))/16
Once I had that worked out, the rest of the project came together nicely. Here’s the code for the set of JQuery plugins I ended up with. One does the truncation, and relies on the other one to get the width of the text:
(function ($) {
// A plugin to truncate the text inside an element to a given width
// This plugin depends on the textWidth() plugin, which should be below
$.fn.textTruncate = function(width,marginText) {
var that = this;
var width = width || "250"; // default width of 250 px
var marginText = marginText || "..."; // default margin text of "..."
that.css("visibility","hidden"); // The element should be hidden in CSS
return this.each(function () {
// A table as a temporary dom element for measuring the text width
$('body').append('<table id="textWidthMeasurer" style="padding:0;margin:0;border:0;width:auto;zoom:1;position:relative;"><tr style="padding:0;margin:0;border:0;"><td style="padding:0;margin:0;border:0;white-space:nowrap;">' + marginText + '</td></tr></table>');
var measurer = $('#textWidthMeasurer');
var margin = measurer.textWidth(measurer);
if (that.textWidth(measurer) > width) {
var contentLength = that.text().length;
that.attr("title",that.text());
while (that.textWidth(measurer) >= width - margin) {
contentLength--;
that.text(that.text().substring(0,contentLength));
}
that.text($.trim(that.text()) + marginText);
}
// Make the element visible and remove the measuring table
that.css("visibility","visible");
$('#textWidthMeasurer').remove();
});
};
// A helper plugin to get the width of the text inside an element
$.fn.textWidth = function(context,css) {
var that = this;
var context = context || null;
var css = css || null;
// Optionally pass in an array of additional CSS properties to use for measuring
var properties = ['font-family','font-weight','font-style','letter-spacing'];
if ((css != null) && (css[0] != null)) {
properties.concat(css);
}
// Establish a default context if none is passed in (slow)
if (context == null) {
if ($('#textWidthMeasurer') == null) {
$('body').append('<table id="textWidthMeasurer" style="padding:0;margin:0;border:0;width:auto;zoom:1;position:relative;"><tr style="padding:0;margin:0;border:0;"><td style="padding:0;margin:0;border:0;white-space:nowrap;"></td></tr></table>');
}
var context = $('#textWidthMeasurer');
}
var target = $('td',context);
// IE uses a bizarre formula to calculate the pixel value of font sizes:
var fontSize = ($.browser.msie) ?
Math.sqrt(parseFloat(that.css("font-size"))/16) + "em" :
parseFloat(that.css("font-size"))/16 + "em";
target.text(that.text()).css('font-size',fontSize);
properties.forEach(function(property) {
target.css(property,that.css(property));
});
var width = context.width();
return width;
};
})(jQuery);
If you want a demo, you can check out a sample page with styled text that adjusts to match the width specified.
You can also download the source code for the plugin set. It’s open source, and free to use in commercial or personal projects under an MIT (X11) license.
Let me know what you think.
September 15th, 2009 at 7:57 am
Hi M.
Two days ago I was looking for a jQuery plugin that would do exactly what yours appeared to offer. However when testing I noticed a few bugs: The plugin is unable to handle multiple matches for a selector. All items end up with the same text and content-length, regardless of styling.
Also measuring the text width in a table appended to body isn’t a good idea. The text needs to be measured in the style context where it is to be displayed. For example: measuring text that is bold in the original context but non-bold outside will result in text that is too long).
So I started modifying. Then rewriting. The result is a plugin that handles more use cases, works in more situations and behaves more like a jQuery plugin. It can now
also be configured using a hash (an object) and it is now possible to predefine
new defaults for all subsequent calls to the plugin, etc. Last but not least it’s smaller and more efficient.
Instead of forking and creating a new plugin I’d rather show it to you and let you decide how to proceed… I really hope it still works in your use cases! I have not tested it thoroughly. Please get in touch with me if you like and I’ll show you the code.
September 29th, 2009 at 2:36 am
Exactly what I was looking for. Thanks.
One issue: in my very brief testing, it doesn’t seem to play well when applied to multiple elements. In this example http://pastie.org/634714 the last two paragraphs both end up reading (in Safari 4, OS X 10.6.1):
This is paragraph 1 of 2 which have tThis is paragraph 1 of 2 which have t…
Also it might be nice to have the default width be that of the containing element instead of an arbitrary 250px.
September 29th, 2009 at 2:41 am
Also, how about getting the code up on github?
October 4th, 2009 at 10:01 am
Cool! I knew I wasn’t the only one who needed something like this. Glad to have some help making it work in more cases, and improving the efficiency. (I’ve gotten some good suggestions offline about ways to pre-estimate the truncation length to avoid looping when the string is really long.)
October 14th, 2009 at 12:20 am
Hi, I’m curious as to how you arrive at the conclusion “inline elements such as spans (which report zero when queried about their width, regardless of their actual contents)”. I found your article searching for advice on how to implement a scrolling text effect, for this I also needed the width of the text inside the element. I was able, however, to simply wrap it in a span and query the width directly, it works perfectly in both Firefox and IE.
July 15th, 2010 at 4:42 pm
I’ve tried this with latest jquery and ie8. however, it doesnt work for me. i ended up waiting for the page to load the script(sort of like an infinite loop is introduced)
Mind taking your spare time to look back into the code?
I would appreciate it if you can.
Thanks