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: '… ', + + // 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: '… ', - - // 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