Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Truncate a string nicely to fit within a given pixel width

Sometimes you have strings that must fit within a certain pixel width. This function attempts to do so efficiently. Please post your suggestions or refactorings below :)

function fitStringToSize(str,len) {
    var shortStr = str;
    var f = document.createElement("span");
    f.style.display = 'hidden';
    f.style.padding = '0px';
    document.body.appendChild(f);

    // on first run, check if string fits into the length already.
    f.innerHTML = str;
    diff = f.offsetWidth - len;

    // if string is too long, shorten it by the approximate 
    // difference in characters (to make for fewer iterations). 
    while(diff > 0)
    {
        shortStr = substring(str,0,(str.length - Math.ceil(diff / 5))) + '…';
        f.innerHTML = shortStr;
        diff = f.offsetWidth - len;
    }

    while(f.lastChild) {
        f.removeChild(f.lastChild);
    }
    document.body.removeChild(f);

    // if the string was too long, put the original string 
    // in the title element of the abbr, and append an ellipsis
    if(shortStr.length < str.length)
    {
        return '<abbr title="' + str + '">' + shortStr + '</abbr>';
    }
    // if the string was short enough in the first place, just return it.
    else
    {
        return str;
    }
}

UPDATE: @some's solution below is much better; please use that.

Update 2: Code now posted as a gist; feel free to fork and submit patches :)

like image 204
Aeon Avatar asked Nov 12 '08 01:11

Aeon


2 Answers

There are a couple of problems with your code.

  • Why / 5 ? The width of the characters depends on font-family and font-size.
  • You must escape str in the abbr title (or else an " will make the code invalid).
  • diff is not declared and ends up in the global scope
  • The substring is not supposed to work like that. What browser are you using?
  • hidden is not a valid value of style.display. To hide it you should use the value none but then the browser don't calculate the offsetWidth. Use style.visibility="hidden" instead.
  • The search for the right length is very inefficient.
  • Must escape &lt;/abbr&gt;"

I rewrote it for you and added className so you can use a style to set the font-family and font-size. Mr Fooz suggested that you use a mouseover to show the whole string. That is not necessary since modern browsers do that for you (tested with FF, IE, Opera and Chrome)

    function fitStringToSize(str,len,className) {
    var result = str; // set the result to the whole string as default
    var span = document.createElement("span");
    span.className=className; //Allow a classname to be set to get the right font-size.
    span.style.visibility = 'hidden';
    span.style.padding = '0px';
    document.body.appendChild(span);


    // check if the string don't fit 
    span.innerHTML = result;
    if (span.offsetWidth > len) {
        var posStart = 0, posMid, posEnd = str.length;
        while (true) {
            // Calculate the middle position
            posMid = posStart + Math.ceil((posEnd - posStart) / 2);
            // Break the loop if this is the last round
            if (posMid==posEnd || posMid==posStart) break;

            span.innerHTML = str.substring(0,posMid) + '&hellip;';

            // Test if the width at the middle position is
            // too wide (set new end) or too narrow (set new start).
            if ( span.offsetWidth > len ) posEnd = posMid; else posStart=posMid;
        }
        //Escape
        var title = str.replace("\"","&#34;");
        //Escape < and >
        var body = str.substring(0,posStart).replace("<","&lt;").replace(">","&gt;");
        result = '<abbr title="' + title + '">' + body + '&hellip;<\/abbr>';
    }
    document.body.removeChild(span);
    return result;
    }

Edit: While testing a little more I found a couple of bugs.

  • I used Math.ceil instead of the intended Math.floor (I blame this on that English isn't my native language)

  • If the input string had html-tags then the result would be undefined (it's not good to truncate a tag in the middle or to leave open tags)

Improvements:

  • Escape the string that is copied to the span on all places. You can still use html-entities, but no tags are allowed (< and > will be displayed)
  • Rewrote the while-statement (it is a little faster, but the main reason was to get rid of the bug that caused extra rounds and to get rid of the break-statement)
  • Renamed the function to fitStringToWidth

Version 2:

function fitStringToWidth(str,width,className) {
  // str    A string where html-entities are allowed but no tags.
  // width  The maximum allowed width in pixels
  // className  A CSS class name with the desired font-name and font-size. (optional)
  // ----
  // _escTag is a helper to escape 'less than' and 'greater than'
  function _escTag(s){ return s.replace("<","&lt;").replace(">","&gt;");}

  //Create a span element that will be used to get the width
  var span = document.createElement("span");
  //Allow a classname to be set to get the right font-size.
  if (className) span.className=className;
  span.style.display='inline';
  span.style.visibility = 'hidden';
  span.style.padding = '0px';
  document.body.appendChild(span);

  var result = _escTag(str); // default to the whole string
  span.innerHTML = result;
  // Check if the string will fit in the allowed width. NOTE: if the width
  // can't be determined (offsetWidth==0) the whole string will be returned.
  if (span.offsetWidth > width) {
    var posStart = 0, posMid, posEnd = str.length, posLength;
    // Calculate (posEnd - posStart) integer division by 2 and
    // assign it to posLength. Repeat until posLength is zero.
    while (posLength = (posEnd - posStart) >> 1) {
      posMid = posStart + posLength;
      //Get the string from the beginning up to posMid;
      span.innerHTML = _escTag(str.substring(0,posMid)) + '&hellip;';

      // Check if the current width is too wide (set new end)
      // or too narrow (set new start)
      if ( span.offsetWidth > width ) posEnd = posMid; else posStart=posMid;
    }

    result = '<abbr title="' +
      str.replace("\"","&quot;") + '">' +
      _escTag(str.substring(0,posStart)) +
      '&hellip;<\/abbr>';
  }
  document.body.removeChild(span);
  return result;
}
like image 53
some Avatar answered Sep 28 '22 03:09

some


At a quick glance, it looks good to me. Here are some minor suggestions:

  • Use a binary search to find the optimal size instead of a linear one.

  • (optionally) add a mouseover so that a tooltip would give the full string.

like image 30
Mr Fooz Avatar answered Sep 28 '22 03:09

Mr Fooz