truncate vs dotdotdot

Benchmark created by Jeff on


Preparation HTML

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<div id="test1" style="width: 200px; line-height: 20px;">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</div>
<div id="test2" style="width: 200px; line-height: 20px;">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</div>
<div id="test3" style="width: 200px; line-height: 20px;">
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
</div>

Setup

(function ($, undefined) {
    
      var BLOCK_TAGS = ['table', 'thead', 'tbody', 'tfoot', 'tr', 'col', 'colgroup', 'object', 'embed', 'param', 'ol', 'ul', 'dl', 'blockquote', 'select', 'optgroup', 'option', 'textarea', 'script', 'style'];
    
      function setText(element, text) {
        if (element.innerText) {
          element.innerText = text;
        } else if (element.nodeValue) {
          element.nodeValue = text;
        } else if (element.textContent) {
          element.textContent = text;
        } else {
          return false;
        }
      }
    
      function truncateTextContent($element, $rootNode, $after, options) {
        var element = $element[0];
        var original = $element.text();
    
        var maxChunk = '';
        var mid, chunk;
        var low = 0;
        var high = original.length;
    
        // Binary Search
        while (low <= high) {
          mid = low + ((high - low) >> 1); // Integer division
    
          chunk = $.trim(original.substr(0, mid + 1)) + options.ellipsis;
          setText(element, chunk);
    
          if ($rootNode.height() > options.maxHeight) {
            high = mid - 1;
          } else {
            low = mid + 1;
            maxChunk = maxChunk.length > chunk.length ? maxChunk : chunk;
          }
        }
    
        if (maxChunk.length > 0) {
          setText(element, maxChunk);
          return true;
        } else {
    
          // Backtrack
          var $parent = $element.parent();
          $element.remove();
    
          var afterLength = $after ? $after.length : 0;
    
          if ($parent.contents().length > afterLength) {
    
            var $n = $parent.contents().eq(-1 - afterLength);
            return truncateTextContent($n, $rootNode, $after, options);
    
          } else {
    
            var $prev = $parent.prev();
            var $e = $prev.contents().eq(-1);
            var e = $e[0];
    
            if (e) {
              setText(e, $e.text() + options.ellipsis);
              $parent.remove();
    
              if ($after.length) {
                $prev.append($after);
              }
              return true;
            }
          }
          return false;
        }
      }
    
      function truncateNestedNode($element, $rootNode, $after, options) {
        var element = $element[0];
    
        var $children = $element.contents();
        var $child, child;
    
        var index = 0;
        var length = $children.length;
        var truncated = false;
    
        $element.empty();
    
        for (; index < length && !truncated; index++) {
    
          $child = $children.eq(index);
          child = $child[0];
    
          if (child.nodeType === child.COMMENT_NODE) {
            continue;
          }
    
          element.appendChild(child);
    
          if ($after.length) {
            if ($.inArray(element.tagName.toLowerCase(), BLOCK_TAGS) >= 0) {
              $element.after($after);
            } else {
              $element.append($after);
            }
          }
    
          if ($rootNode.height() > options.maxHeight) {
            if (child.nodeType === child.TEXT_NODE) {
              truncated = truncateTextContent($child, $rootNode, $after, options);
            } else {
              truncated = truncateNestedNode($child, $rootNode, $after, options);
            }
          }
    
          if (!truncated && $after.length) { $after.remove(); }
    
        }
    
        return truncated;
      }
    
      function Truncate(element, options) {
        this.element = element;
        this.$element = $(element);
    
        this._name = 'truncate';
        this._defaults = {
          lines: 1,
          ellipsis: '… ',
          showMore: '<a href="#">More</a>',
          showLess: '<a href="#">Less</a>'
        };
    
        this.options = $.extend({}, this._defaults, options);
        this.options.maxHeight = parseInt(this.options.lines, 10) * parseInt(this.options.lineHeight, 10);
    
        this.$after = $(this.options.showMore, this.$element);
    
        this.original = element.innerHTML;
        this.cached = null;
    
        this.update();
      }
    
      Truncate.prototype = {
    
        update: function (html) {
          // Update HTML if provided, otherwise default to current inner HTML.
          if (html) {
            this.original = this.element.innerHTML = html;
          }
    
          // Check if already meets height requirement
          if (this.$element.height() <= this.options.maxHeight) {
            return;
          }
    
          truncateNestedNode(this.$element, this.$element, this.$after, this.options);
          this.cachedHTML = this.element.innerHTML;
        },
    
        expand: function () {
          this.element.innerHTML = this.originalHTML + this.options.showLess;
        },
    
        collapse: function () {
          this.element.innerHTML = this.cachedHTML;
        }
    
      };
    
      // Lightweight plugin wrapper preventing multiple instantiations
      $.fn.truncate = function (options) {
        return this.each(function () {
          if (!$.data(this, 'jquery-truncate')) {
            $.data(this, 'jquery-truncate', new Truncate(this, options));
          }
        });
      };
    
    })(jQuery);
    
    
    /*
     *  jQuery dotdotdot 1.6.1
     *
     *  Copyright (c) 2013 Fred Heusschen
     *  www.frebsite.nl
     *
     *  Plugin website:
     *  dotdotdot.frebsite.nl
     *
     *  Dual licensed under the MIT and GPL licenses.
     *  http://en.wikipedia.org/wiki/MIT_License
     *  http://en.wikipedia.org/wiki/GNU_General_Public_License
     */
    
    (function( $ )
    {
        if ( $.fn.dotdotdot )
        {
                return;
        }
    
        $.fn.dotdotdot = function( o )
        {
                if ( this.length == 0 )
                {
                        if ( !o || o.debug !== false )
                        {
                                debug( true, 'No element found for "' + this.selector + '".' );
                        }
                        return this;
                }
                if ( this.length > 1 )
                {
                        return this.each(
                                function()
                                {
                                        $(this).dotdotdot( o );
                                }
                        );
                }
    
    
                var $dot = this;
    
                if ( $dot.data( 'dotdotdot' ) )
                {
                        $dot.trigger( 'destroy.dot' );
                }
    
                $dot.data( 'dotdotdot-style', $dot.attr( 'style' ) );
                $dot.css( 'word-wrap', 'break-word' );
                if ($dot.css( 'white-space' ) === 'nowrap')
                {
                        $dot.css( 'white-space', 'normal' );
                }
    
                $dot.bind_events = function()
                {
                        $dot.bind(
                                'update.dot',
                                function( e, c )
                                {
                                        e.preventDefault();
                                        e.stopPropagation();
    
                                        opts.maxHeight = ( typeof opts.height == 'number' )
                                                ? opts.height
                                                : getTrueInnerHeight( $dot );
    
                                        opts.maxHeight += opts.tolerance;
    
                                        if ( typeof c != 'undefined' )
                                        {
                                                if ( typeof c == 'string' || c instanceof HTMLElement )
                                                {
                                                        c = $('<div />').append( c ).contents();
                                                }
                                                if ( c instanceof $ )
                                                {
                                                        orgContent = c;
                                                }
                                        }
    
                                        $inr = $dot.wrapInner( '<div class="dotdotdot" />' ).children();
                                        $inr.empty()
                                                .append( orgContent.clone( true ) )
                                                .css({
                                                        'height'        : 'auto',
                                                        'width'         : 'auto',
                                                        'border'        : 'none',
                                                        'padding'       : 0,
                                                        'margin'        : 0
                                                });
    
                                        var after = false,
                                                trunc = false;
    
                                        if ( conf.afterElement )
                                        {
                                                after = conf.afterElement.clone( true );
                                                conf.afterElement.remove();
                                        }
                                        if ( test( $inr, opts ) )
                                        {
                                                if ( opts.wrap == 'children' )
                                                {
                                                        trunc = children( $inr, opts, after );
                                                }
                                                else
                                                {
                                                        trunc = ellipsis( $inr, $dot, $inr, opts, after );
                                                }
                                        }
                                        $inr.replaceWith( $inr.contents() );
                                        $inr = null;
    
                                        if ( $.isFunction( opts.callback ) )
                                        {
                                                opts.callback.call( $dot[ 0 ], trunc, orgContent );
                                        }
    
                                        conf.isTruncated = trunc;
                                        return trunc;
                                }
    
                        ).bind(
                                'isTruncated.dot',
                                function( e, fn )
                                {
                                        e.preventDefault();
                                        e.stopPropagation();
    
                                        if ( typeof fn == 'function' )
                                        {
                                                fn.call( $dot[ 0 ], conf.isTruncated );
                                        }
                                        return conf.isTruncated;
                                }
    
                        ).bind(
                                'originalContent.dot',
                                function( e, fn )
                                {
                                        e.preventDefault();
                                        e.stopPropagation();
    
                                        if ( typeof fn == 'function' )
                                        {
                                                fn.call( $dot[ 0 ], orgContent );
                                        }
                                        return orgContent;
                                }
    
                        ).bind(
                                'destroy.dot',
                                function( e )
                                {
                                        e.preventDefault();
                                        e.stopPropagation();
    
                                        $dot.unwatch()
                                                .unbind_events()
                                                .empty()
                                                .append( orgContent )
                                                .attr( 'style', $dot.data( 'dotdotdot-style' ) )
                                                .data( 'dotdotdot', false );
                                }
                        );
                        return $dot;
                };      //      /bind_events
    
                $dot.unbind_events = function()
                {
                        $dot.unbind('.dot');
                        return $dot;
                };      //      /unbind_events
    
                $dot.watch = function()
                {
                        $dot.unwatch();
                        if ( opts.watch == 'window' )
                        {
                                var $window = $(window),
                                        _wWidth = $window.width(),
                                        _wHeight = $window.height();
    
                                $window.bind(
                                        'resize.dot' + conf.dotId,
                                        function()
                                        {
                                                if ( _wWidth != $window.width() || _wHeight != $window.height() || !opts.windowResizeFix )
                                                {
                                                        _wWidth = $window.width();
                                                        _wHeight = $window.height();
    
                                                        if ( watchInt )
                                                        {
                                                                clearInterval( watchInt );
                                                        }
                                                        watchInt = setTimeout(
                                                                function()
                                                                {
                                                                        $dot.trigger( 'update.dot' );
                                                                }, 10
                                                        );
                                                }
                                        }
                                );
                        }
                        else
                        {
                                watchOrg = getSizes( $dot );
                                watchInt = setInterval(
                                        function()
                                        {
                                                var watchNew = getSizes( $dot );
                                                if ( watchOrg.width  != watchNew.width ||
                                                         watchOrg.height != watchNew.height )
                                                {
                                                        $dot.trigger( 'update.dot' );
                                                        watchOrg = getSizes( $dot );
                                                }
                                        }, 100
                                );
                        }
                        return $dot;
                };
                $dot.unwatch = function()
                {
                        $(window).unbind( 'resize.dot' + conf.dotId );
                        if ( watchInt )
                        {
                                clearInterval( watchInt );
                        }
                        return $dot;
                };
    
                var     orgContent      = $dot.contents(),
                        opts            = $.extend( true, {}, $.fn.dotdotdot.defaults, o ),
                        conf            = {},
                        watchOrg        = {},
                        watchInt        = null,
                        $inr            = null;
    
    
                if ( !( opts.lastCharacter.remove instanceof Array ) )
                {
                        opts.lastCharacter.remove = $.fn.dotdotdot.defaultArrays.lastCharacter.remove;
                }
                if ( !( opts.lastCharacter.noEllipsis instanceof Array ) )
                {
                        opts.lastCharacter.noEllipsis = $.fn.dotdotdot.defaultArrays.lastCharacter.noEllipsis;
                }
    
    
                conf.afterElement       = getElement( opts.after, $dot );
                conf.isTruncated        = false;
                conf.dotId                      = dotId++;
    
    
                $dot.data( 'dotdotdot', true )
                        .bind_events()
                        .trigger( 'update.dot' );
    
                if ( opts.watch )
                {
                        $dot.watch();
                }
    
                return $dot;
        };
    
    
        //      public
        $.fn.dotdotdot.defaults = {
                'ellipsis'                      : '... ',
                'wrap'                          : 'word',
                'fallbackToLetter'      : true,
                'lastCharacter'         : {},
                'tolerance'                     : 0,
                'callback'                      : null,
                'after'                         : null,
                'height'                        : null,
                'watch'                         : false,
                'windowResizeFix'       : true,
                'debug'                         : false
        };
        $.fn.dotdotdot.defaultArrays = {
                'lastCharacter'         : {
                        'remove'                        : [ ' ', '\u3000', ',', ';', '.', '!', '?' ],
                        'noEllipsis'            : []
                }
        };
    
    
        //      private
        var dotId = 1;
    
        function children( $elem, o, after )
        {
                var $elements   = $elem.children(),
                        isTruncated     = false;
    
                $elem.empty();
    
                for ( var a = 0, l = $elements.length; a < l; a++ )
                {
                        var $e = $elements.eq( a );
                        $elem.append( $e );
                        if ( after )
                        {
                                $elem.append( after );
                        }
                        if ( test( $elem, o ) )
                        {
                                $e.remove();
                                isTruncated = true;
                                break;
                        }
                        else
                        {
                                if ( after )
                                {
                                        after.detach();
                                }
                        }
                }
                return isTruncated;
        }
        function ellipsis( $elem, $d, $i, o, after )
        {
                var $elements   = $elem.contents(),
                        isTruncated     = false;
    
                $elem.empty();
    
                var notx = 'table, thead, tbody, tfoot, tr, col, colgroup, object, embed, param, ol, ul, dl, blockquote, select, optgroup, option, textarea, script, style';
                for ( var a = 0, l = $elements.length; a < l; a++ )
                {
    
                        if ( isTruncated )
                        {
                                break;
                        }
    
                        var e   = $elements[ a ],
                                $e      = $(e);
    
                        if ( typeof e == 'undefined' )
                        {
                                continue;
                        }
    
                        $elem.append( $e );
                        if ( after )
                        {
                                $elem[ ( $elem.is( notx ) ) ? 'after' : 'append' ]( after );
                        }
                        if ( e.nodeType == 3 )
                        {
                                if ( test( $i, o ) )
                                {
                                        isTruncated = ellipsisElement( $e, $d, $i, o, after );
                                }
                        }
                        else
                        {
                                isTruncated = ellipsis( $e, $d, $i, o, after );
                        }
    
                        if ( !isTruncated )
                        {
                                if ( after )
                                {
                                        after.detach();
                                }
                        }
                }
                return isTruncated;
        }
        function ellipsisElement( $e, $d, $i, o, after )
        {
                var isTruncated = false,
                        e = $e[ 0 ];
    
                if ( typeof e == 'undefined' )
                {
                        return false;
                }
    
                var txt                 = getTextContent( e ),
                        space           = ( txt.indexOf(' ') !== -1 ) ? ' ' : '\u3000',
                        separator       = ( o.wrap == 'letter' ) ? '' : space,
                        textArr         = txt.split( separator ),
                        position        = -1,
                        midPos          = -1,
                        startPos        = 0,
                        endPos          = textArr.length - 1;
    
                while ( startPos <= endPos && !( startPos == 0 && endPos == 0 ) )
                {
                        var m = Math.floor( ( startPos + endPos ) / 2 );
                        if ( m == midPos )
                        {
                                break;
                        }
                        midPos = m;
    
                        setTextContent( e, textArr.slice( 0, midPos + 1 ).join( separator ) + o.ellipsis );
    
                        if ( !test( $i, o ) )
                        {
                                position = midPos;
                                startPos = midPos;
                        }
                        else
                        {
                                endPos = midPos;
                        }
    
          if( endPos == startPos && endPos == 0 && o.fallbackToLetter )
          {
                                separator       = '';
                                textArr         = textArr[0].split(separator);
                                position        = -1;
                                midPos          = -1;
                                startPos        = 0;
                                endPos          = textArr.length - 1;
          }
        }
    
                if ( position != -1 && !( textArr.length == 1 && textArr[ 0 ].length == 0 ) )
                {
                        txt = addEllipsis( textArr.slice( 0, position + 1 ).join( separator ), o );
                        isTruncated = true;
                        setTextContent( e, txt );
                }
                else
                {
                        var $w = $e.parent();
                        $e.remove();
    
                        var afterLength = ( after ) ? after.length : 0 ;
    
                        if ( $w.contents().size() > afterLength )
                        {
                                var $n = $w.contents().eq( -1 - afterLength );
                                isTruncated = ellipsisElement( $n, $d, $i, o, after );
                        }
                        else
                        {
                                var $p = $w.prev()
                                var e = $p.contents().eq( -1 )[ 0 ];
    
                                if ( typeof e != 'undefined' )
                                {
                                        var txt = addEllipsis( getTextContent( e ), o );
                                        setTextContent( e, txt );
                                        if ( after )
                                        {
                                                $p.append( after );
                                        }
                                        $w.remove();
                                        isTruncated = true;
                                }
    
                        }
                }
    
                return isTruncated;
        }
        function test( $i, o )
        {
                return $i.innerHeight() > o.maxHeight;
        }
        function addEllipsis( txt, o )
        {
                while( $.inArray( txt.slice( -1 ), o.lastCharacter.remove ) > -1 )
                {
                        txt = txt.slice( 0, -1 );
                }
                if ( $.inArray( txt.slice( -1 ), o.lastCharacter.noEllipsis ) < 0 )
                {
                        txt += o.ellipsis;
                }
                return txt;
        }
        function getSizes( $d )
        {
                return {
                        'width' : $d.innerWidth(),
                        'height': $d.innerHeight()
                };
        }
        function setTextContent( e, content )
        {
                if ( e.innerText )
                {
                        e.innerText = content;
                }
                else if ( e.nodeValue )
                {
                        e.nodeValue = content;
                }
                else if (e.textContent)
                {
                        e.textContent = content;
                }
    
        }
        function getTextContent( e )
        {
                if ( e.innerText )
                {
                        return e.innerText;
                }
                else if ( e.nodeValue )
                {
                        return e.nodeValue;
                }
                else if ( e.textContent )
                {
                        return e.textContent;
                }
                else
                {
                        return "";
                }
        }
        function getElement( e, $i )
        {
                if ( typeof e == 'undefined' )
                {
                        return false;
                }
                if ( !e )
                {
                        return false;
                }
                if ( typeof e == 'string' )
                {
                        e = $(e, $i);
                        return ( e.length )
                                ? e
                                : false;
                }
                if ( typeof e == 'object' )
                {
                        return ( typeof e.jquery == 'undefined' )
                                ? false
                                : e;
                }
                return false;
        }
        function getTrueInnerHeight( $el )
        {
                var h = $el.innerHeight(),
                        a = [ 'paddingTop', 'paddingBottom' ];
    
                for ( var z = 0, l = a.length; z < l; z++ ) {
                        var m = parseInt( $el.css( a[ z ] ), 10 );
                        if ( isNaN( m ) )
                        {
                                m = 0;
                        }
                        h -= m;
                }
                return h;
        }
        function debug( d, m )
        {
                if ( !d )
                {
                        return false;
                }
                if ( typeof m == 'string' )
                {
                        m = 'dotdotdot: ' + m;
                }
                else
                {
                        m = [ 'dotdotdot:', m ];
                }
    
                if ( typeof window.console != 'undefined' )
                {
                        if ( typeof window.console.log != 'undefined' )
                        {
                                window.console.log( m );
                        }
                }
                return false;
        }
    
    
        //      override jQuery.html
        var _orgHtml = $.fn.html;
        $.fn.html = function( str ) {
                if ( typeof str != 'undefined' )
                {
                        if ( this.data( 'dotdotdot' ) )
                        {
                                if ( typeof str != 'function' )
                                {
                                        return this.trigger( 'update', [ str ] );
                                }
                        }
                        return _orgHtml.call( this, str );
                }
                return _orgHtml.call( this );
        };
    
    
        //      override jQuery.text
        var _orgText = $.fn.text;
        $.fn.text = function( str ) {
                if ( typeof str != 'undefined' )
                {
                        if ( this.data( 'dotdotdot' ) )
                        {
                                var temp = $( '<div />' );
                                temp.text( str );
                                str = temp.html();
                                temp.remove();
                                return this.trigger( 'update', [ str ] );
                        }
                        return _orgText.call( this, str );
                }
            return _orgText.call( this );
        };
    
    
    })( jQuery );
    (function (module, undefined) {
    
      /*
       *
       */
      function getStyle(element, property) {
        // IE8 and below does not support getComputedStyle. Use currentStyle instead.
        var styles = (window.getComputedStyle && window.getComputedStyle(element) || element.currentStyle);
        return parseFloat(styles[property]);
      }
    
      /*
       *
       */
      function height(element) {
        var total = getStyle(element, 'height');
        var boxModel = getStyle(element, 'boxSizing');
        if (boxModel === 'border-box') {
          return total - getStyle(element, 'paddingTop') - getStyle(element, 'paddingBottom');
        } else {
          // Assume content-box model
          return total;
        }
      }
    
      /* Trims leading and trailing whitespace. Regular expression taken from jQuery.
       * See https://github.com/jquery/jquery/blob/master/speed/jquery-basis.js
       *
       * str - The original string to trim.
       *
       * Returns trimmed string.
       */
      function trim(str) {
        return (str || '').replace(/^(\s|\u00A0)+|(\s|\u00A0)+$/g, '');
      }
    
      /*
       *
       */
      function getHTMLInRange(node, startIndex, endIndex) {
        var index, childNode;
        var childNodes = node.childNodes,
            length = childNodes.length,
            html = '';
    
        for (index = startIndex; index <= endIndex && index < length; index++) {
          childNode = childNodes[index];
          if (childNode.nodeType === childNode.COMMENT_NODE) {
            // Need the commend node HTML in order to reconstruct original DOM structure
            // This manual way is the only way to get a comment node's HTML
            html += '<!--' + childNode.nodeValue + '-->';
          } else {
            html += childNode.outerHTML || childNode.nodeValue;
          }
        }
        return html;
      }
    
      /* Truncates a text node using binary search.
       *
       * textNode - The text node to truncate.
       * rootNode - The root node (ancestor of the textNode) to measure the truncated height.
       * options  - An object containing:
       *            showMore  - The HTML string to append at the end of the string.
       *            maxHeight - The maximum height for the root node.
       *
       * Returns nothing.
       */
      function truncateTextNode(textNode, rootNode, options) {
        var originalHTML = textNode.nodeValue,
            mid,
            low = 0,
            high = originalHTML.length,
            chunk,
            maxChunk = '';
    
        // Binary Search
        while (low <= high) {
          mid = low + ((high - low) >> 1); // Integer division
    
          chunk = trim(originalHTML.substr(0, mid + 1)) + options.showMore;
          textNode.nodeValue = chunk;
    
          if (height(rootNode) > options.maxHeight) {
            high = mid - 1;
          } else {
            low = mid + 1;
            maxChunk = maxChunk.length > chunk.length ? maxChunk : chunk;
          }
        }
    
        textNode.nodeValue = maxChunk;
      }
    
      function truncateNestedNode(element, rootNode, options) {
    
        var childNodes = element.childNodes,
            length = childNodes.length;
    
        if (length === 0) {
    
          // Base case: single element remaining
    
          if (element.nodeType === element.TEXT_NODE) {
            // Truncate the text node
            truncateTextNode(element, rootNode, options);
          } else {
            // Remove the node itself
            element.parentNode.removeChild(element);
          }
    
          return;
    
        } else {
    
          // Recursive case: iterate backwards on children nodes until tipping node is found
    
          var index, node, chunk;
          var originalHTML = element.innerHTML;
    
          for (index = length - 1; index >= 0; index--) {
            node = childNodes[index];
    
            chunk = getHTMLInRange(element, 0, index);
            element.innerHTML = chunk;
    
            if (height(rootNode) <= options.maxHeight) {
    
              // Check if element is not the last child
              if (index + 1 <= length - 1) {
                // Reset HTML so original childNodes tree is available
                element.innerHTML = originalHTML;
    
                chunk += getHTMLInRange(element, index + 1, index + 1);
                element.innerHTML = chunk;
    
                index += 1;
              }
    
              return truncateNestedNode(childNodes[index], rootNode, options);
            }
          }
    
          return truncateNestedNode(childNodes[0], rootNode, options);
    
        }
      }
    
      /* Public: Creates an instance of Truncate.
       *
       * element - A DOM element to be truncated.
       * options - An Object literal containing setup options.
       *
       * Examples:
       *
       *   var element = document.createElement('span');
       *   element.innerHTML = 'This is<br>odd.';
       *   var truncated = new Truncate(element, {
       *     lines: 1,
       *     lineHeight: 16,
       *     showMore: '<a class="show-more">Show More</a>',
       *     showLess: '<a class="show-more">Show Less</a>'
       *   });
       *
       *   // Update HTML
       *   truncated.update('This is not very odd.');
       *
       *   // Undo truncation
       *   truncated.expand();
       *
       *   // Redo truncation
       *   truncated.collapse();
       */
      function Truncate(element, options) {
        this.options = options || {};
        options.showMore = typeof options.showMore !== 'undefined' ? options.showMore : '…';
        options.showLess = typeof options.showLess !== 'undefined' ? options.showLess : '';
    
        this.options.maxHeight = options.lines * options.lineHeight;
    
        this.element = element;
        this.originalHTML = element.innerHTML;
        this.cachedHTML = null;
    
        this.update();
      }
    
      /* Public: Updates the inner HTML of the element and re-truncates.
       *
       * newHTML - The new HTML.
       *
       * Returns nothing.
       */
      Truncate.prototype.update = function (newHTML) {
        // Update HTML if provided, otherwise default to current inner HTML.
        if (newHTML) {
          this.originalHTML = this.element.innerHTML = newHTML;
        }
    
        // Already meets height requirement
        if (height(this.element) <= this.options.maxHeight) {
          return;
        }
    
        var visibility = this.element.style.visibility;
        this.element.style.visibility = 'hidden';
    
        truncateNestedNode(this.element, this.element, this.options);
        this.cachedHTML = this.element.innerHTML;
    
        this.element.style.visibility = visibility;
      };
    
      /* Public: Expands the element to show content in full.
       *
       * Returns nothing.
       */
      Truncate.prototype.expand = function () {
        this.element.innerHTML = this.originalHTML + this.options.showLess;
      };
    
      /* Public: Collapses the element to the truncated state.
       * Uses the cached HTML from .update().
       *
       * Returns nothing.
       */
      Truncate.prototype.collapse = function () {
        this.element.innerHTML = this.cachedHTML;
      };
    
      module.Truncate = Truncate;
    
    })(window);

Test runner

Ready to run.

Testing in
TestOps/sec
dotdotdot
$('#test1').dotdotdot({
height: 200
});
ready
truncate jquery
$('#test2').truncate({
lines: 10,
lineHeight: 20
});
ready
truncate old
new window.Truncate($('#test3')[0], {
lines: 10,
lineHeight: 20
});
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.