﻿// HTML Truncator for jQuery
// by Henrik Nyh <http://henrik.nyh.se> 2008-02-28.
// Free to modify and redistribute with credit.

// 2010 :Dan Wellman
// added options:
//   linkClass - class name added to more and less links
//   addEllipses - Adds string of '...' after text in truncated element
//   fuzzyTruncate - Only truncates if there are more than the number of words specified after truncation
//   preserveWordBoundaries - Don;t truncate half-way though a word - count back to the next whitespace character and truncate to that instead

(function ($) {

    var trailing_whitespace = true;

    $.fn.truncate = function (options) {

        var opts = $.extend({}, $.fn.truncate.defaults, options);

        $(this).each(function () {

            var content_length = $.trim(squeeze($(this).text())).length;
            if (content_length <= opts.max_length)
                return;  // bail early if not overlong

            var actual_max_length = opts.max_length - opts.more.length - 3,  // 3 for " ()"
                truncated_node = recursivelyTruncate(this, actual_max_length),
                full_node = $(this),

            //count number of words before and after truncation
                wordCountBeforeTruncation = full_node.text().replace(/\s/g, ' ').split(' ').length,
                wordCountAfterTruncation = truncated_node.text().replace(/\s/g, ' ').split(' ').length;

            if (wordCountBeforeTruncation - wordCountAfterTruncation <= opts.fuzzyTruncate) {
                //don't truncate if less than fuzzyTruncate limit
                return;
            } else {
                full_node.hide();
                truncated_node.insertAfter(full_node);
            }

            if (opts.preserveWordBoundaries === true) {
                //avoid breaking half-way through a word
                var whitespace = false,
                    counter = opts.max_length;

                while (whitespace === false) {
                    if (truncated_node.html().charCodeAt(counter) != 10 && truncated_node.html().charCodeAt(counter) != 32) {
                        counter--;
                    } else {
                        truncated_node.html(truncated_node.html().slice(0, counter));
                        whitespace = true;
                    }
                }
            }

            findNodeForMore(truncated_node).append(' <a class="more-link ' + opts.linkClass + '" href="#more">' + opts.more + '</a>');
            findNodeForLess(full_node).append(' <a class=' + opts.linkClass + ' href="#less">' + opts.less + '</a>');

            truncated_node.find('a:last').click(function () {
                truncated_node.hide(); full_node.show(); return false;
            });
            full_node.find('a:last').click(function () {
                truncated_node.show(); full_node.hide(); return false;
            });

            //add ellipsis after truncated text
            if (opts.addEllipses === true) {
                
                $("<span></span>", {
                    text: "..."
                }).insertBefore(".more-link");
            }

        });
    }

    // Note that the " (…more)" bit counts towards the max length – so a max
    // length of 10 would truncate "1234567890" to "12 (…more)".
    $.fn.truncate.defaults = {
        max_length: 100,
        more: '…more',
        less: 'less',
        linkClass: "truncate-link",
        addEllipses: true,
        fuzzyTruncate: 10,
        preserveWordBoundaries: true
    };

    function recursivelyTruncate(node, max_length) {
        return (node.nodeType == 3) ? truncateText(node, max_length) : truncateNode(node, max_length);
    }

    function truncateNode(node, max_length) {
        var node = $(node);
        var new_node = node.clone().empty();
        var truncatedChild;
        node.contents().each(function () {
            var remaining_length = max_length - new_node.text().length;
            if (remaining_length == 0) return;  // breaks the loop
            truncatedChild = recursivelyTruncate(this, remaining_length);
            if (truncatedChild) new_node.append(truncatedChild);
        });
        return new_node;
    }

    function truncateText(node, max_length) {
        var text = squeeze(node.data);
        if (trailing_whitespace)  // remove initial whitespace if last text
            text = text.replace(/^ /, '');  // node had trailing whitespace.
        trailing_whitespace = !!text.match(/ $/);
        var text = text.slice(0, max_length);

        // Ensure HTML entities are encoded
        // http://debuggable.com/posts/encode-html-entities-with-jquery:480f4dd6-13cc-4ce9-8071-4710cbdd56cb
        text = $('<div/>').text(text).html();
        return text;

    }

    // Collapses a sequence of whitespace into a single space.
    function squeeze(string) {
        return string.replace(/\s+/g, ' ');
    }

    // Finds the last, innermost block-level element
    function findNodeForMore(node) {
        var $node = $(node);
        var last_child = $node.children(":last");
        if (!last_child) return node;
        var display = last_child.css('display');
        if (!display || display == 'inline') return $node;
        return findNodeForMore(last_child);
    };

    // Finds the last child if it's a p; otherwise the parent
    function findNodeForLess(node) {
        var $node = $(node);
        var last_child = $node.children(":last");
        if (last_child && last_child.is('p')) return last_child;
        return node;
    };

})(jQuery);

