diff --git a/app/views/comments/_comment.html.haml b/app/views/comments/_comment.html.haml index 342a6bce0..78dcc265f 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 193b13b66..f86d4c783 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 37ed03c14..e8001ba8f 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) - "
#{text}
".html_safe - end end end end diff --git a/public/javascripts/vendor/jquery.expander.js b/public/javascripts/vendor/jquery.expander.js index d1d2ed66b..812057095 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 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); - startTags = startText.match(/<\w[^>]*>/g); - - if (startTags) { - startText = allText.slice(0,o.slicePoint + startTags.join('').length).replace(/(&([^;]+;)?|\w+)$/,''); + // bail out if we've already set up the expander on this element + if ( $.data(this, 'expander') ) { + return; } + $.data(this, 'expander', true); - if (startText.lastIndexOf('<') > startText.lastIndexOf('>') ) { - startText = startText.slice(0,startText.lastIndexOf('<')); - } - - var defined = {}; + // determine which callback functions are defined $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) { defined[val] = $.isFunction(o[val]); }); - 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++) { + // back up if we're in the middle of a tag or word + summaryText = backup(summaryText); - 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; - } - } - } - } - } + // summary text sans tags length + summTagless = summaryText.replace(rOpenCloseTag,'').length; - endText = cleanedTag && cleanedTag + endText || endText; + // 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]; } - $this.html([ - startText, - '', - o.expandPrefix, - '', - o.expandText, - '', - '', - '', - endText, - '' - ].join('') - ); + summaryText += newChar; + summTagless++; } - var $thisDetails = $(this).find('span.details'), - $readMore = $(this).find('span.read-more'); + 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 detail has fewer words than widow option + detailText = allHtml.slice(summaryText.length); + if ( detailText.split(/\s+/).length < o.widow && !hasDetails ) { + 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').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('' + o.userCollapsePrefix + '' + o.userCollapseText + ''); + } + + $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('' + o.userCollapsePrefix + '' + o.userCollapseText + ''); - $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); - } - }); } - }); - function reCollapse(el) { - el.hide() - .prev('span.read-more').show(); + }); // 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 = '