/*!
* jQuery Expander Plugin v1.4
*
* Date: Sun Dec 11 15:08:42 2011 EST
* Requires: jQuery v1.3+
*
* Copyright 2011, Karl Swedberg
* Dual licensed under the MIT and GPL licenses (just like jQuery):
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*
*
*
*
*/

(function ($) {
    $.expander = {
        version: '1.4',
        defaults: {
            // the number of characters at which the contents will be sliced into two parts.
            slicePoint: 100,

            // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false)
            preserveWords: true,

            // a threshold of sorts for whether to initially hide/collapse part of the element's contents.
            // If after slicing the contents in two there are fewer words in the second part than
            // the value set by widow, we won't bother hiding/collapsing anything.
            widow: 4,

            // text displayed in a link instead of the hidden part of the element.
            // clicking this will expand/show the hidden/collapsed text
            expandText: 'read more',
            expandPrefix: '&hellip; ',

            expandAfterSummary: false,

            // class names for summary element and detail element
            summaryClass: 'summary',
            detailClass: 'details',

            // class names for <span> around "read-more" link and "read-less" link
            moreClass: 'read-more',
            lessClass: 'read-less',

            // number of milliseconds after text has been expanded at which to collapse the text again.
            // when 0, no auto-collapsing
            collapseTimer: 0,

            // effects for expanding and collapsing
            expandEffect: 'fadeIn',
            expandSpeed: 250,
            collapseEffect: 'fadeOut',
            collapseSpeed: 200,

            // allow the user to re-collapse the expanded text.
            userCollapse: true,

            // text to use for the link to re-collapse the text
            userCollapseText: 'read less',
            userCollapsePrefix: ' ',


            // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called

            onSlice: null, // function() {}
            beforeExpand: null, // function() {},
            afterExpand: null, // function() {},
            onCollapse: null // function(byUser) {}
        }
    };

    $.fn.expander = function (options) {
        var meth = 'init';

        if (typeof options == 'string') {
            meth = options;
            options = {};
        }

        var opts = $.extend({}, $.expander.defaults, options),
        rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i,
        rAmpWordEnd = /(&(?:[^;]+;)?|\w+)$/,
        rOpenCloseTag = /<\/?(\w+)[^>]*>/g,
        rOpenTag = /<(\w+)[^>]*>/g,
        rCloseTag = /<\/(\w+)>/g,
        rLastCloseTag = /(<\/[^>]+>)\s*$/,
        rTagPlus = /^<[^>]+>.?/,
        delayedCollapse;

        var methods = {
            init: function () {
                this.each(function () {
                    var i, l, tmp, summTagLess, summOpens, summCloses, lastCloseTag, detailText,
              $thisDetails, $readMore,
              openTagsForDetails = [],
              closeTagsForsummaryText = [],
              defined = {},
              thisEl = this,
              $this = $(this),
              $summEl = $([]),
              o = $.meta ? $.extend({}, opts, $this.data()) : opts,
              hasDetails = !!$this.find('.' + o.detailClass).length,
              hasBlocks = !!$this.find('*').filter(function () {
                  var display = $(this).css('display');
                  return (/^block|table|list/).test(display);
              }).length,
              el = hasBlocks ? 'div' : 'span',
              detailSelector = el + '.' + o.detailClass,
              moreSelector = 'span.' + o.moreClass,
              expandSpeed = o.expandSpeed || 0,
              allHtml = $.trim($this.html()),
              allText = $.trim($this.text()),
              summaryText = allHtml.slice(0, o.slicePoint);

                    // bail out if we've already set up the expander on this element
                    if ($.data(this, 'expander')) {
                        return;
                    }
                    $.data(this, 'expander', true);

                    // determine which callback functions are defined
                    $.each(['onSlice', 'beforeExpand', 'afterExpand', 'onCollapse'], function (index, val) {
                        defined[val] = $.isFunction(o[val]);
                    });

                    // back up if we're in the middle of a tag or word
                    summaryText = backup(summaryText);

                    // summary text sans tags length
                    summTagless = summaryText.replace(rOpenCloseTag, '').length;

                    // add more characters to the summary, one for each character in the tags
                    while (summTagless < o.slicePoint) {
                        newChar = allHtml.charAt(summaryText.length);
                        if (newChar == '<') {
                            newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0];
                        }
                        summaryText += newChar;
                        summTagless++;
                    }

                    summaryText = backup(summaryText, o.preserveWords);

                    // separate open tags from close tags and clean up the lists
                    summOpens = summaryText.match(rOpenTag) || [];
                    summCloses = summaryText.match(rCloseTag) || [];

                    // filter out self-closing tags
                    tmp = [];
                    $.each(summOpens, function (index, val) {
                        if (!rSelfClose.test(val)) {
                            tmp.push(val);
                        }
                    });
                    summOpens = tmp;

                    // strip close tags to just the tag name
                    l = summCloses.length;
                    for (i = 0; i < l; i++) {
                        summCloses[i] = summCloses[i].replace(rCloseTag, '$1');
                    }

                    // tags that start in summary and end in detail need:
                    // a). close tag at end of summary
                    // b). open tag at beginning of detail
                    $.each(summOpens, function (index, val) {
                        var thisTagName = val.replace(rOpenTag, '$1');
                        var closePosition = $.inArray(thisTagName, summCloses);
                        if (closePosition === -1) {
                            openTagsForDetails.push(val);
                            closeTagsForsummaryText.push('</' + thisTagName + '>');

                        } else {
                            summCloses.splice(closePosition, 1);
                        }
                    });

                    // reverse the order of the close tags for the summary so they line up right
                    closeTagsForsummaryText.reverse();

                    // create necessary summary and detail elements if they don't already exist
                    if (!hasDetails) {

                        // end script if there is no detail text or if detail has fewer words than widow option
                        detailText = allHtml.slice(summaryText.length);

                        if (detailText === '' || detailText.split(/\s+/).length < o.widow) {
                            return;
                        }

                        // otherwise, continue...
                        lastCloseTag = closeTagsForsummaryText.pop() || '';
                        summaryText += closeTagsForsummaryText.join('');
                        detailText = openTagsForDetails.join('') + detailText;

                    } else {
                        // assume that even if there are details, we still need readMore/readLess/summary elements
                        // (we already bailed out earlier when readMore el was found)
                        // but we need to create els differently

                        // remove the detail from the rest of the content
                        detailText = $this.find(detailSelector).remove().html();

                        // The summary is what's left
                        summaryText = $this.html();

                        // allHtml is the summary and detail combined (this is needed when content has block-level elements)
                        allHtml = summaryText + detailText;

                        lastCloseTag = '';
                    }
                    o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o);

                    if (hasBlocks) {
                        detailText = allHtml;
                    }
                    summaryText += lastCloseTag;

                    // onSlice callback
                    o.summary = summaryText;
                    o.details = detailText;
                    o.lastCloseTag = lastCloseTag;

                    if (defined.onSlice) {
                        // user can choose to return a modified options object
                        // one last chance for user to change the options. sneaky, huh?
                        // but could be tricky so use at your own risk.
                        tmp = o.onSlice.call(thisEl, o);

                        // so, if the returned value from the onSlice function is an object with a details property, we'll use that!
                        o = tmp && tmp.details ? tmp : o;
                    }

                    // build the html with summary and detail and use it to replace old contents
                    var html = buildHTML(o, hasBlocks);

                    $this.html(html);

                    // set up details and summary for expanding/collapsing
                    $thisDetails = $this.find(detailSelector);
                    $readMore = $this.find(moreSelector);
                    $thisDetails.hide();
                    $readMore.find('a').unbind('click.expander').bind('click.expander', expand);

                    $summEl = $this.find('div.' + o.summaryClass);

                    if (o.userCollapse && !$this.find('span.' + o.lessClass).length) {
                        $this
            .find(detailSelector)
            .append('<span class="' + o.lessClass + '">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>');
                    }

                    $this
          .find('span.' + o.lessClass + ' a')
          .unbind('click.expander')
          .bind('click.expander', function (event) {
              event.preventDefault();
              clearTimeout(delayedCollapse);
              var $detailsCollapsed = $(this).closest(detailSelector);
              reCollapse(o, $detailsCollapsed);
              if (defined.onCollapse) {
                  o.onCollapse.call(thisEl, true);
              }
          });

                    function expand(event) {
                        event.preventDefault();
                        $readMore.hide();
                        $summEl.hide();
                        if (defined.beforeExpand) {
                            o.beforeExpand.call(thisEl);
                        }

                        $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function () {
                            $thisDetails.css({ zoom: '' });
                            if (defined.afterExpand) { o.afterExpand.call(thisEl); }
                            delayCollapse(o, $thisDetails, thisEl);
                        });
                    }

                }); // this.each
            },
            destroy: function () {
                if (!this.data('expander')) {
                    return;
                }
                this.removeData('expander');
                this.each(function () {
                    var $this = $(this),
              o = $.meta ? $.extend({}, opts, $this.data()) : opts,
              details = $this.find('.' + o.detailClass).contents();

                    $this.find('.' + o.moreClass).remove();
                    $this.find('.' + o.summaryClass).remove();
                    $this.find('.' + o.detailClass).after(details).remove();
                    $this.find('.' + o.lessClass).remove();

                });
            }
        };

        // run the methods (almost always "init")
        if (methods[meth]) {
            methods[meth].call(this);
        }

        // utility functions
        function buildHTML(o, blocks) {
            var el = 'span',
          summary = o.summary;
            if (blocks) {
                el = 'div';
                // if summary ends with a close tag, tuck the moreLabel inside it
                if (rLastCloseTag.test(summary) && !o.expandAfterSummary) {
                    summary = summary.replace(rLastCloseTag, o.moreLabel + '$1');
                } else {
                    // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary
                    // fixes #19
                    summary += o.moreLabel;
                }

                // and wrap it in a div
                summary = '<div class="' + o.summaryClass + '">' + summary + '</div>';
            } else {
                summary += o.moreLabel;
            }

            return [
        summary,
        '<',
          el + ' class="' + o.detailClass + '"',
        '>',
          o.details,
        '</' + el + '>'
        ].join('');
        }

        function buildMoreLabel(o) {
            var ret = '<span class="' + o.moreClass + '">' + o.expandPrefix;
            ret += '<a href="#">' + o.expandText + '</a></span>';
            return ret;
        }

        function backup(txt, preserveWords) {
            if (txt.lastIndexOf('<') > txt.lastIndexOf('>')) {
                txt = txt.slice(0, txt.lastIndexOf('<'));
            }
            if (preserveWords) {
                txt = txt.replace(rAmpWordEnd, '');
            }
            return txt;
        }

        function reCollapse(o, el) {
            el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function () {
                var prevMore = el.prev('span.' + o.moreClass).show();
                if (!prevMore.length) {
                    el.parent().children('div.' + o.summaryClass).show()
            .find('span.' + o.moreClass).show();
                }
            });
        }

        function delayCollapse(option, $collapseEl, thisEl) {
            if (option.collapseTimer) {
                delayedCollapse = setTimeout(function () {
                    reCollapse(option, $collapseEl);
                    if ($.isFunction(option.onCollapse)) {
                        option.onCollapse.call(thisEl, false);
                    }
                }, option.collapseTimer);
            }
        }

        return this;
    };

    // plugin defaults
    $.fn.expander.defaults = $.expander.defaults;
})(jQuery);
