truncate vs dotdotdot

JavaScript performance comparison

Test case created by Jeff

Preparation code

<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>
      
<script>
Benchmark.prototype.setup = function() {
  (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);
  

};
</script>

Preparation code output

<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>

Test runner

Warning! For accurate results, please disable Firebug before running the tests. (Why?)

Java applet disabled.

Testing in CCBot 2.0.0 / Other 0.0.0
Test Ops/sec
truncate old
new window.Truncate($('#test3')[0], {
lines: 10,
lineHeight: 20
});
pending…
dotdotdot
$('#test1').dotdotdot({
height: 200
});
pending…
truncate jquery
$('#test2').truncate({
lines: 10,
lineHeight: 20
});
pending…

Compare results of other browsers

Revisions

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

0 Comments

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.
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.
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.