diff --git a/config/assets.yml b/config/assets.yml
index 9d346d01a..8bf27bd77 100644
--- a/config/assets.yml
+++ b/config/assets.yml
@@ -21,7 +21,6 @@ javascripts:
- public/javascripts/vendor/jquery.autoresize.js
- public/javascripts/vendor/jquery-ui-1.8.9.custom.min.js
- public/javascripts/vendor/jquery.charcount.js
- - public/javascripts/vendor/jquery.expander.js
- public/javascripts/vendor/jquery.placeholder.js
- public/javascripts/vendor/timeago.js
- public/javascripts/vendor/facebox.js
diff --git a/features/show_more.feature b/features/show_more.feature
new file mode 100644
index 000000000..e842267e7
--- /dev/null
+++ b/features/show_more.feature
@@ -0,0 +1,24 @@
+@javascript
+Feature: collapsing and expanding long posts
+ In order to tame the lengths of posts in my stream
+ As a rocket scientist
+ I want long posts to be collapsed and expand on click
+
+ Background:
+ Given a user with username "bob"
+ And I sign in as "bob@bob.bob"
+ And I am on the home page
+
+ Scenario: post a very long message
+ Given I post an extremely long status message
+ And I go to the home page
+
+ Then the post should be collapsed
+
+ Scenario: expand a very long message
+ Given I post an extremely long status message
+ And I go to the home page
+ And I expand the post
+
+ Then the post should be expanded
+
diff --git a/features/step_definitions/posts_steps.rb b/features/step_definitions/posts_steps.rb
index 3e81c8840..ee7e2854f 100644
--- a/features/step_definitions/posts_steps.rb
+++ b/features/step_definitions/posts_steps.rb
@@ -2,6 +2,14 @@ Then /^the post "([^"]*)" should be marked nsfw$/ do |text|
assert_nsfw(text)
end
+Then /^the post should be collapsed$/ do
+ first_post_collapsed?
+end
+
+Then /^the post should be expanded$/ do
+ first_post_expanded?
+end
+
Then /^I should see an uploaded image within the photo drop zone$/ do
find("#photodropzone img")["src"].should include("uploads/images")
end
@@ -32,6 +40,9 @@ When /^I click on the first block button/ do
find(".block_user").click
end
+When /^I expand the post$/ do
+ expand_first_post
+end
Then /^I should see "([^"]*)" as the first post in my stream$/ do |text|
first_post_text.should include(text)
@@ -43,4 +54,8 @@ end
When /^I click the publisher and post "([^"]*)"$/ do |text|
click_and_post(text)
-end
\ No newline at end of file
+end
+
+When /^I post an extremely long status message$/ do
+ click_and_post("I am a very interesting message " * 64)
+end
diff --git a/features/support/publishing_cuke_helpers.rb b/features/support/publishing_cuke_helpers.rb
index 9895813a4..8f1762434 100644
--- a/features/support/publishing_cuke_helpers.rb
+++ b/features/support/publishing_cuke_helpers.rb
@@ -17,6 +17,22 @@ module PublishingCukeHelpers
')
end
+ def expand_first_post
+ find(".stream_element:first .expander").click
+ wait_until{ !find(".expander").visible? }
+ end
+
+ def first_post_collapsed?
+ find(".stream_element:first .collapsible").should have_css(".expander")
+ find(".stream_element:first .collapsible").has_selector?(".collapsed")
+ end
+
+ def first_post_expanded?
+ find(".stream_element:first .expander").should_not be_visible
+ find(".stream_element:first .collapsible").has_no_selector?(".collapsed")
+ find(".stream_element:first .collapsible").has_selector?(".opened")
+ end
+
def first_post_text
find('.stream_element:first .post-content').text()
end
diff --git a/public/javascripts/app/views/comment_view.js b/public/javascripts/app/views/comment_view.js
index 5c04227cb..c85faf7a2 100644
--- a/public/javascripts/app/views/comment_view.js
+++ b/public/javascripts/app/views/comment_view.js
@@ -4,8 +4,10 @@ app.views.Comment = app.views.Content.extend({
className : "comment media",
- events : {
- "click .comment_delete": "destroyModel"
+ events : function() {
+ return _.extend(app.views.Content.prototype.events, {
+ "click .comment_delete": "destroyModel"
+ });
},
presenter : function() {
diff --git a/public/javascripts/app/views/content_view.js b/public/javascripts/app/views/content_view.js
index 3bc7f0760..bb66c08f1 100644
--- a/public/javascripts/app/views/content_view.js
+++ b/public/javascripts/app/views/content_view.js
@@ -1,7 +1,8 @@
app.views.Content = app.views.StreamObject.extend({
events: {
- "click .oembed .thumb": "showOembedContent"
+ "click .oembed .thumb": "showOembedContent",
+ "click .expander": "expandPost"
},
presenter : function(){
@@ -41,7 +42,17 @@ app.views.Content = app.views.StreamObject.extend({
var paramSeparator = ( /\?/.test(insertHTML.attr("src")) ) ? "&" : "?";
insertHTML.attr("src", insertHTML.attr("src") + paramSeparator + "autoplay=1");
oembed.html( insertHTML );
+ },
+
+ expandPost: function(evt) {
+ var el = $(this.el).find('.collapsible');
+ el.removeClass('collapsed').addClass('opened');
+ el.animate({'height':el.data('orig-height')}, 550, function() {
+ el.css('height','auto');
+ });
+ $(evt.currentTarget).hide();
}
+
});
app.views.StatusMessage = app.views.Content.extend({
diff --git a/public/javascripts/app/views/stream_object_view.js b/public/javascripts/app/views/stream_object_view.js
index 054cdee88..1f46e732b 100644
--- a/public/javascripts/app/views/stream_object_view.js
+++ b/public/javascripts/app/views/stream_object_view.js
@@ -1,34 +1,5 @@
app.views.StreamObject = app.views.Base.extend({
- postRenderTemplate : function() {
- // collapse long posts
- this.$(".collapsible").expander({
- slicePoint: 400,
- widow: 12,
- expandPrefix: "",
- expandText: Diaspora.I18n.t("show_more"),
- userCollapse: false,
- beforeExpand: function() {
- if ($(this).find('.summary').length == 0) { // Sigh. See comments in the spec.
- var readMoreDiv = $(this).find('.read-more');
- var lastElementBeforeReadMore = readMoreDiv.prev();
- var firstElementAfterReadMore = readMoreDiv.next().children().first();
-
- if (lastElementBeforeReadMore.is('p')) {
- lastElementBeforeReadMore.append(firstElementAfterReadMore.html());
- firstElementAfterReadMore.remove();
-
- } else if (lastElementBeforeReadMore.is('ul') && firstElementAfterReadMore.is('ul')) {
- var firstBullet = firstElementAfterReadMore.children().first();
- lastElementBeforeReadMore.find('li').last().append(firstBullet.html());
- firstBullet.remove();
- }
- readMoreDiv.remove();
- }
- }
- });
- },
-
destroyModel: function(evt) {
if (evt) {
evt.preventDefault();
diff --git a/public/javascripts/app/views/stream_view.js b/public/javascripts/app/views/stream_view.js
index 0c3b71d74..b3095b540 100644
--- a/public/javascripts/app/views/stream_view.js
+++ b/public/javascripts/app/views/stream_view.js
@@ -16,6 +16,7 @@ app.views.Stream = Backbone.View.extend({
setupEvents : function(){
this.stream.bind("fetched", this.removeLoader, this)
+ this.stream.bind("fetched", this.postRender, this)
this.stream.bind("allPostsLoaded", this.unbindInfScroll, this)
this.collection.bind("add", this.addPost, this);
if(window.app.user()) {
@@ -52,6 +53,35 @@ app.views.Stream = Backbone.View.extend({
return this;
},
+
+ postRender : function() {
+ // collapse long posts
+ var collHeight = 190,
+ collElem = $(this.el).find(".collapsible");
+
+ _.each(collElem, function(elem) {
+ var elem = $(elem),
+ oembed = elem.find(".oembed"),
+ addHeight = 0;
+
+ if( $.trim(oembed.html()) != "" ) {
+ addHeight = oembed.height();
+ }
+
+ // only collapse if height exceeds collHeight+20%
+ if( elem.height() > ((collHeight*1.2)+addHeight) && !elem.is(".opened") ) {
+ elem.data("orig-height", elem.height() );
+ elem
+ .height( Math.max(collHeight, addHeight) )
+ .addClass("collapsed")
+ .append(
+ $('
')
+ .addClass('expander')
+ .text( Diaspora.I18n.t("show_more") )
+ );
+ }
+ });
+ },
appendLoader: function(){
$("#paginate").html($("
", {
diff --git a/public/javascripts/vendor/jquery.expander.js b/public/javascripts/vendor/jquery.expander.js
deleted file mode 100644
index 812057095..000000000
--- a/public/javascripts/vendor/jquery.expander.js
+++ /dev/null
@@ -1,338 +0,0 @@
-/*!
- * jQuery Expander Plugin v1.3
- *
- * 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
- *
- *
- *
- *
-*/
-
-(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({}, $.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 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 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').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.stop(false, true)[o.expandEffect](expandSpeed, function() {
- $thisDetails.css({zoom: ''});
- if (defined.afterExpand) {o.afterExpand.call(thisEl);}
- delayCollapse(o, $thisDetails, thisEl);
- });
- }
-
- }); // 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 = '' + summary + '
';
- } else {
- summary += o.moreLabel;
- }
-
- return [
- summary,
- '<',
- el + ' class="' + o.detailClass + '"',
- '>',
- o.details,
- '' + el + '>'
- ].join('');
- }
-
- function buildMoreLabel(o) {
- var ret = '' + o.expandPrefix;
- ret += '' + o.expandText + '';
- 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);
diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass
index 2a48d9bed..03cc59f24 100644
--- a/public/stylesheets/sass/application.sass
+++ b/public/stylesheets/sass/application.sass
@@ -1828,6 +1828,26 @@ ul#press_logos
:color #777
.collapsible
+ :overflow hidden
+ :position relative
+
+ .expander
+ :position absolute
+ :bottom 0
+ :left 0
+ :right 0
+ :height 30px
+ :text-align center
+ :line-height 48px
+ :font-size .8em
+ :color $grey
+ :text-shadow 0 0 7px #FFF
+ :cursor pointer
+ :border-bottom 2px solid #DDD
+ @include border-radius(0, 0, 3px, 3px)
+ @include linear-gradient(rgba(255,255,255,0) , #EEE, 0%, 95%)
+ :background-color transparent
+
.oembed
:background url('/images/ajax-loader2.gif') no-repeat center center
:display inline-block
diff --git a/spec/javascripts/app/views/stream_view_spec.js b/spec/javascripts/app/views/stream_view_spec.js
index b1b8b1630..7f14ae523 100644
--- a/spec/javascripts/app/views/stream_view_spec.js
+++ b/spec/javascripts/app/views/stream_view_spec.js
@@ -39,73 +39,6 @@ describe("app.views.Stream", function() {
});
});
- describe('clicking read more', function() {
- var readMoreLink;
-
- beforeEach(function() {
- this.statusMessage = this.stream.posts.models[0];
- this.statusElement = $(this.view.$(".stream_element")[0]);
- readMoreLink = this.statusElement.find('.read-more a');
- readMoreLink.text("read more");
- });
-
- it('expands the post', function() {
- expect(this.statusElement.find('.collapsible .details').attr('style')).toContain('display: none;');
- readMoreLink.click();
- expect(this.statusElement.find('.collapsible .details').attr('style')).not.toContain('display: none;');
- });
-
- describe('differences between firefox and webkit/IE', function() {
- // Firefox creates 2 divs - one with the summary and one with the whole post.
- // It hides the summary and shows the whole post when you click show more. Works great!
- // Webkit and IE also create 2 divs, but they split the post - the 1st has the summary and the 2nd has the rest
- // of the post. When you click read more, it just shows the 2nd div. This leaves whitespace in odd places.
- // So there's a callback that this is testing, that fixes the whitespace on webkit & IE.
- var weAreOnFirefox;
-
- beforeEach(function() {
- weAreOnFirefox = this.statusElement.find('.collapsible .summary').length > 0;
- });
-
- it('removes the read-more div on webkit/IE but leaves it on firefox', function() {
- expect(this.statusElement.find('.read-more').length).toEqual(1);
- readMoreLink.click();
- if (weAreOnFirefox === true) {
- expect(this.statusElement.find('.read-more').length).toEqual(1);
- } else {
- expect(this.statusElement.find('.read-more').length).toEqual(0);
- }
- });
-
- it('collapses p elements on webkit/IE but leaves them alone on firefox', function() {
- expect(this.statusElement.find('.collapsible p').length).toEqual(2);
- readMoreLink.click();
- if (weAreOnFirefox === true) {
- expect(this.statusElement.find('.collapsible p').length).toEqual(2);
- } else {
- expect(this.statusElement.find('.collapsible p').length).toEqual(1);
- }
- });
-
- it('collapses li elements on webkit/IE but leaves them alone on firefox', function() {
- this.statusMessage = this.stream.posts.models[3];
- this.statusElement = $(this.view.$(".stream_element")[3]);
- readMoreLink = this.statusElement.find('.read-more a');
- readMoreLink.text("read more");
-
- if (weAreOnFirefox === true) {
- expect(this.statusElement.find('.collapsible li').length).toEqual(12);
- readMoreLink.click();
- expect(this.statusElement.find('.collapsible li').length).toEqual(12);
- } else {
- expect(this.statusElement.find('.collapsible li').length).toEqual(9);
- readMoreLink.click();
- expect(this.statusElement.find('.collapsible li').length).toEqual(8);
- }
- });
- });
- });
-
describe("infScroll", function() {
// NOTE: inf scroll happens at 500px