From 1a521a792a31f3b955b9e7d7f4ffb85734156f7b Mon Sep 17 00:00:00 2001
From: Maxwell Salzberg <maxwell@joindiaspora.com>
Date: Tue, 25 Oct 2011 19:46:09 -0700
Subject: [PATCH] zomg text collapse is finally back #thankgod

---
 app/views/comments/_comment.html.haml         |   2 +-
 .../status_messages/_status_message.html.haml |   2 +-
 lib/diaspora/markdownify.rb                   |   3 -
 public/javascripts/vendor/jquery.expander.js  | 395 ++++++++++++------
 public/javascripts/widgets/comment.js         |  14 +-
 public/javascripts/widgets/stream-element.js  |  14 +-
 6 files changed, 290 insertions(+), 140 deletions(-)

diff --git a/app/views/comments/_comment.html.haml b/app/views/comments/_comment.html.haml
index 342a6bce0c..78dcc265f3 100644
--- a/app/views/comments/_comment.html.haml
+++ b/app/views/comments/_comment.html.haml
@@ -11,7 +11,7 @@
     %span.from
       = person_link(comment.author, :class => "hovercardable")
 
-    %span{:class => direction_for(comment.text)}
+    %span{:class => [direction_for(comment.text), 'collapsible']}
       = markdownify(comment, :oembed => true, :youtube_maps => comment.youtube_titles)
 
     .comment_info
diff --git a/app/views/status_messages/_status_message.html.haml b/app/views/status_messages/_status_message.html.haml
index 193b13b662..f86d4c7838 100644
--- a/app/views/status_messages/_status_message.html.haml
+++ b/app/views/status_messages/_status_message.html.haml
@@ -15,7 +15,7 @@
         - for photo in photos[1..photos.size]
           = link_to (image_tag photo.url(:thumb_small), :class => 'stream-photo thumb_small', 'data-small-photo' => photo.url(:thumb_medium), 'data-full-photo' => photo.url), photo_path(photo), :class => 'stream-photo-link'
 
-%div{:class => direction_for(post.text)}
+%div{:class => [direction_for(post.text), 'collapsible']}
   != markdownify(post, :youtube_maps => post[:youtube_titles])
   - if post.o_embed_cache_id.present?
     = o_embed_html(post.o_embed_cache)
diff --git a/lib/diaspora/markdownify.rb b/lib/diaspora/markdownify.rb
index 37ed03c144..e8001ba8f1 100644
--- a/lib/diaspora/markdownify.rb
+++ b/lib/diaspora/markdownify.rb
@@ -10,9 +10,6 @@ module Diaspora
         auto_link(link, :link => :urls, :html => { :target => "_blank" })
       end
 
-      def paragraph(text)
-         "<p>#{text} </p>".html_safe
-      end
     end
   end
 end
diff --git a/public/javascripts/vendor/jquery.expander.js b/public/javascripts/vendor/jquery.expander.js
index d1d2ed66b1..8120570955 100644
--- a/public/javascripts/vendor/jquery.expander.js
+++ b/public/javascripts/vendor/jquery.expander.js
@@ -1,140 +1,328 @@
 /*!
- * jQuery Expander Plugin v0.7
+ * jQuery Expander Plugin v1.3
  *
- * Date: Wed Aug 31 20:53:59 2011 EDT
+ * Date: Sat Sep 17 00:37:34 2011 EDT
  * 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
- * source: https://github.com/kswedberg/jquery-expander/
+ *
+ *
+ *
+ *
 */
 
 (function($) {
+  $.expander = {
+    version: '1.3',
+    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,
+
+      // widow: 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; ',
+
+      // 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 opts = $.extend({}, $.fn.expander.defaults, options),
-        rSlash = /\//,
+    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,
+        rTagPlus = /^<[^>]+>.?/,
         delayedCollapse;
 
     this.each(function() {
-      var cleanedTag, startTags, endTags,
+      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,
-          allText = $this.html(),
-          startText = allText.slice(0, o.slicePoint).replace(/(&([^;]+;)?|\w+)$/,'');
+          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);
 
-      startTags = startText.match(/<\w[^>]*>/g);
+      // summary text sans tags length
+      summTagless = summaryText.replace(rOpenCloseTag,'').length;
 
-      if (startTags) {
-        startText = allText.slice(0,o.slicePoint + startTags.join('').length).replace(/(&([^;]+;)?|\w+)$/,'');
+      // 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++;
       }
 
-      if (startText.lastIndexOf('<') > startText.lastIndexOf('>') ) {
-        startText = startText.slice(0,startText.lastIndexOf('<'));
+      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');
       }
 
-      var defined = {};
-      $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) {
-        defined[val] = $.isFunction(o[val]);
+      // 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);
+        }
       });
 
-      var endText = allText.slice(startText.length);
-      // create necessary expand/collapse elements if they don't already exist
-      if (!$(this).find('span.details').length) {
-        // end script if text length isn't long enough.
-        if ( endText.replace(/\s+$/,'').split(' ').length < o.widow || allText.length < o.slicePoint ) { return; }
-        // otherwise, continue...
-        if (defined.onSlice) { o.onSlice.call(thisEl); }
-        if (endText.indexOf('</') > -1) {
-          endTags = endText.match(/<(\/)?[^>]*>/g);
-          for (var i=0; i < endTags.length; i++) {
-
-            if (endTags[i].indexOf('</') > -1) {
-              var startTag, startTagExists = false;
-              for (var j=0; j < i; j++) {
-                startTag = endTags[j].slice(0, endTags[j].indexOf(' ')).replace(/\w$/,'$1>');
-                if (startTag == endTags[i].replace(rSlash,'')) {
-                  startTagExists = true;
-                }
-              }
-              if (!startTagExists) {
-                startText = startText + endTags[i];
-                var matched = false;
-                for (var s=startTags.length - 1; s >= 0; s--) {
-                  if (startTags[s].slice(0, startTags[s].indexOf(' ')).replace(/(\w)$/,'$1>') == endTags[i].replace(rSlash,'') &&
-                  !matched ) {
-                    cleanedTag = cleanedTag ? startTags[s] + cleanedTag : startTags[s];
-                    matched = true;
-                  }
-                }
-              }
-            }
-          }
+      // 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 ) {
 
-          endText = cleanedTag && cleanedTag + endText || endText;
+        // end script if detail has fewer words than widow option
+        detailText = allHtml.slice(summaryText.length);
+        if ( detailText.split(/\s+/).length < o.widow && !hasDetails ) {
+          return;
         }
-        $this.html([
-          startText,
-          '<span class="read-more">',
-            o.expandPrefix,
-            '<a href="#">',
-              o.expandText,
-            '</a>',
-          '</span>',
-          '<span class="details">',
-            endText,
-          '</span>'
-          ].join('')
-        );
+
+        // 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;
 
-      var $thisDetails = $(this).find('span.details'),
-          $readMore = $(this).find('span.read-more');
+      // 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').bind('click.expander', function(event) {
+      $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[o.expandEffect](expandSpeed, function() {
+        $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() {
           $thisDetails.css({zoom: ''});
           if (defined.afterExpand) {o.afterExpand.call(thisEl);}
           delayCollapse(o, $thisDetails, thisEl);
         });
-      });
+      }
 
-      if ( o.userCollapse && !$this.find('span.re-collapse').length ) {
-        $this
-        .find('span.details')
-        .append('<span class="re-collapse">' + o.userCollapsePrefix + '<a href="#">' + o.userCollapseText + '</a></span>');
-        $this.find('span.re-collapse a').bind('click.expander', function(event) {
-          event.preventDefault();
-          clearTimeout(delayedCollapse);
-          var $detailsCollapsed = $(this).parents('span.details');
-          reCollapse($detailsCollapsed);
-          if (defined.onCollapse) {
-            o.onCollapse.call(thisEl, true);
-          }
-        });
+    }); // this.each
+
+    function buildHTML(o, blocks) {
+      var el = 'span',
+          summary = o.summary;
+      if ( blocks ) {
+        el = 'div';
+        // tuck the moreLabel inside the last close tag
+        summary = summary.replace(/(<\/[^>]+>)\s*$/, o.moreLabel + '$1');
+
+        // and wrap it in a div
+        summary = '<div class="' + o.summaryClass + '">' + summary + '</div>';
+      } else {
+        summary += o.moreLabel;
       }
-    });
 
-    function reCollapse(el) {
-       el.hide()
-        .prev('span.read-more').show();
+      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($collapseEl);
+          reCollapse(option, $collapseEl);
           if ( $.isFunction(option.onCollapse) ) {
             option.onCollapse.call(thisEl, false);
           }
@@ -146,40 +334,5 @@
   };
 
   // plugin defaults
-  $.fn.expander.defaults = {
-    // slicePoint: the number of characters at which the contents will be sliced into two parts.
-    // Note: any tag names in the HTML that appear inside the sliced element before
-    // the slicePoint will be counted along with the text characters.
-    slicePoint: 100,
-
-    // widow: 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; ',
-
-    // number of milliseconds after text has been expanded at which to collapse the text again
-    collapseTimer: 0,
-    expandEffect: 'fadeIn',
-    expandSpeed: 250,
-
-    // allow the user to re-collapse the expanded text.
-    userCollapse: true,
-
-    // text to use for the link to re-collapse the text
-    userCollapseText: '[collapse expanded text]',
-    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.defaults = $.expander.defaults;
 })(jQuery);
diff --git a/public/javascripts/widgets/comment.js b/public/javascripts/widgets/comment.js
index 0fa6f1501e..862b5cfbf4 100644
--- a/public/javascripts/widgets/comment.js
+++ b/public/javascripts/widgets/comment.js
@@ -8,18 +8,18 @@
         deleteCommentLink: comment.find("a.comment_delete"),
         likes: self.instantiate("Likes", comment.find(".likes_container")),
         timeAgo: self.instantiate("TimeAgo", comment.find("abbr.timeago")),
-        content: comment.find(".content span")
+        content: comment.find(".content span .collapsible")
       });
 
       self.deleteCommentLink.click(self.removeComment);
       self.deleteCommentLink.twipsy({ trigger: "hover" });
 
-      // self.content.expander({
-      //   slicePoint: 200,
-      //   widow: 18,
-      //   expandText: Diaspora.I18n.t("show_more"),
-      //   userCollapse: false
-      // });
+      self.content.expander({
+        slicePoint: 200,
+        widow: 18,
+        expandText: Diaspora.I18n.t("show_more"),
+        userCollapse: false
+      });
 
       self.globalSubscribe("likes/" + self.comment.attr('id') + "/updated", function(){
         self.likes = self.instantiate("Likes", self.comment.find(".likes_container"));
diff --git a/public/javascripts/widgets/stream-element.js b/public/javascripts/widgets/stream-element.js
index 3878e3beb6..ac431f8914 100644
--- a/public/javascripts/widgets/stream-element.js
+++ b/public/javascripts/widgets/stream-element.js
@@ -12,7 +12,7 @@
         lightBox: self.instantiate("Lightbox", element),
         timeAgo: self.instantiate("TimeAgo", element.find(".timeago a abbr.timeago")),
 
-        content: element.find(".content p"),
+        content: element.find(".content .collapsible"),
         deletePostLink: element.find("a.stream_element_delete"),
         focusCommentLink: element.find("a.focus_comment_textarea"),
         hidePostLoader: element.find("img.hide_loader"),
@@ -26,12 +26,12 @@
       self.postScope.twipsy();
 
       // collapse long posts
-      // self.content.expander({
-      //   slicePoint: 400,
-      //   widow: 12,
-      //   expandText: Diaspora.I18n.t("show_more"),
-      //   userCollapse: false
-      // });
+      self.content.expander({
+        slicePoint: 400,
+        widow: 12,
+        expandText: Diaspora.I18n.t("show_more"),
+        userCollapse: false
+      });
 
       self.globalSubscribe("likes/" + self.postGuid + "/updated", function() {
         self.likes = self.instantiate("Likes", self.post.find(".likes_container:first"));
-- 
GitLab