From 01aa38fb82ef12e869a8f6105a47f185b620cee7 Mon Sep 17 00:00:00 2001 From: Waqas Khalid Date: Wed, 25 Jun 2014 17:11:58 +0500 Subject: [PATCH] Truncated markup can be displayed in forums user profile view FOR-581 --- ...discussion_thread_profile_view_spec.coffee | 113 ++++++++++++++++++ .../static/coffee/src/discussion/utils.coffee | 10 ++ .../discussion_thread_profile_view.coffee | 13 +- common/static/js/vendor/jquery.truncate.js | 107 +++++++++++++++++ common/static/js_test.yml | 1 + .../discussion/_js_head_dependencies.html | 1 + .../mustache/_profile_thread.mustache | 2 +- 7 files changed, 239 insertions(+), 8 deletions(-) create mode 100644 common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee create mode 100755 common/static/js/vendor/jquery.truncate.js diff --git a/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee b/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee new file mode 100644 index 0000000000..5fd0d4b783 --- /dev/null +++ b/common/static/coffee/spec/discussion/view/discussion_thread_profile_view_spec.coffee @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +describe "DiscussionThreadProfileView", -> + + beforeEach -> + + setFixtures """ +
+ + """ + @threadData = { + id: "1", + body: "dummy body", + discussion: new Discussion() + abuse_flaggers: [], + votes: {up_count: "42"} + } + @imageTag = '' + window.MathJax = { Hub: { Queue: -> } } + + makeView = (thread) -> + view = new DiscussionThreadProfileView(el: $("article#thread_#{thread.id}"), model: thread) + spyConvertMath(view) + return view + + makeThread = (threadData) -> + thread = new Thread(threadData) + thread.discussion = new Discussion() + return thread + + spyConvertMath = (view) -> + spyOn(view, "convertMath").andCallFake( -> + @model.set('markdownBody', @model.get('body')) + ) + + checkPostWithImages = (numberOfImages, truncatedText, threadData, imageTag) -> + expectedHtml = '

' + threadData.body = '

' + testText = '' + expectedText = '' + + if truncatedText + testText = new Array(100).join('test ') + expectedText = testText.substring(0, 139)+ '…' + else + testText = 'Test body' + expectedText = 'Test body' + + for i in [0..numberOfImages-1] + threadData.body = threadData.body + imageTag + if i == 0 + expectedHtml = expectedHtml + imageTag + else + expectedHtml = expectedHtml + 'image omitted' + + threadData.body = threadData.body + '' + testText + '

' + if numberOfImages > 1 + expectedHtml = expectedHtml + '' + expectedText + '

Some images in this post have been omitted

' + else + expectedHtml = expectedHtml + '' + expectedText + '

' + + view = makeView(makeThread(threadData)) + view.render() + expect(view.$el.find(".post-body").html()).toEqual(expectedHtml) + + checkBody = (truncated, view, threadData) -> + view.render() + if not truncated + expect(view.model.get("body")).toEqual(view.model.get("abbreviatedBody")) + expect(view.$el.find(".post-body").html()).toEqual(threadData.body) + else + expect(view.model.get("body")).not.toEqual(view.model.get("abbreviatedBody")) + expect(view.$el.find(".post-body").html()).not.toEqual(threadData.body) + outputHtmlStripped = view.$el.find(".post-body").html().replace(/(<([^>]+)>)/ig,""); + outputHtmlStripped = outputHtmlStripped.replace("Some images in this post have been omitted","") + outputHtmlStripped = outputHtmlStripped.replace("image omitted","") + inputHtmlStripped = threadData.body.replace(/(<([^>]+)>)/ig,""); + expectedOutput = inputHtmlStripped.substring(0, 139)+ '…' + expect(outputHtmlStripped).toEqual(expectedOutput) + expect(view.$el.find(".post-body").html().indexOf("…")).toBeGreaterThan(0) + + describe "Body markdown should be correct", -> + + it "untruncated text without markdown body", -> + @threadData.body = "Test body" + view = makeView(makeThread(@threadData)) + checkBody(false, view, @threadData) + + it "truncated text without markdown body", -> + @threadData.body = new Array(100).join("test ") + view = makeView(makeThread(@threadData)) + checkBody(true, view, @threadData) + + it "untruncated text with markdown body", -> + @threadData.body = '

' + @imageTag + 'Google top search engine

' + view = makeView(makeThread(@threadData)) + checkBody(false, view, @threadData) + + it "truncated text with markdown body", -> + testText = new Array(100).join("test ") + @threadData.body = '

' + @imageTag + @imageTag + '' + testText + '

' + view = makeView(makeThread(@threadData)) + checkBody(true, view, @threadData) + + for numImages in [1, 2, 10] + for truncatedText in [true, false] + it "body with #{numImages} images and #{if truncatedText then "truncated" else "untruncated"} text", -> + checkPostWithImages(numImages, truncatedText, @threadData, @imageTag) diff --git a/common/static/coffee/src/discussion/utils.coffee b/common/static/coffee/src/discussion/utils.coffee index 0835534d0b..50530622b2 100644 --- a/common/static/coffee/src/discussion/utils.coffee +++ b/common/static/coffee/src/discussion/utils.coffee @@ -309,6 +309,16 @@ class @DiscussionUtil minLength++ return text.substr(0, minLength) + gettext('…') + @abbreviateHTML: (html, minLength) -> + # Abbreviates the html to at least minLength characters, stopping at word boundaries + truncated_text = jQuery.truncate(html, {length: minLength, noBreaks: true, ellipsis: gettext('…')}) + $result = $("
" + truncated_text + "
") + imagesToReplace = $result.find("img:not(:first)") + if imagesToReplace.length > 0 + $result.append("

Some images in this post have been omitted

") + imagesToReplace.replaceWith("image omitted") + $result.html() + @getPaginationParams: (curPage, numPages, pageUrlFunc) => delta = 2 minPage = Math.max(curPage - delta, 1) diff --git a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee index 8fa3330076..51afd0bd24 100644 --- a/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee +++ b/common/static/coffee/src/discussion/views/discussion_thread_profile_view.coffee @@ -2,21 +2,20 @@ if Backbone? class @DiscussionThreadProfileView extends Backbone.View render: -> @template = DiscussionUtil.getTemplate("_profile_thread") - if not @model.has('abbreviatedBody') - @abbreviateBody() + @convertMath() + @abbreviateBody() params = $.extend(@model.toJSON(),{permalink: @model.urlFor('retrieve')}) if not @model.get('anonymous') params = $.extend(params, user:{username: @model.username, user_url: @model.user_url}) @$el.html(Mustache.render(@template, params)) @$("span.timeago").timeago() - @convertMath() + element = @$(".post-body") + MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] @ convertMath: -> - element = @$(".post-body") - element.html DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight element.text() - MathJax.Hub.Queue ["Typeset", MathJax.Hub, element[0]] + @model.set('markdownBody', DiscussionUtil.postMathJaxProcessor DiscussionUtil.markdownWithHighlight @model.get('body')) abbreviateBody: -> - abbreviated = DiscussionUtil.abbreviateString @model.get('body'), 140 + abbreviated = DiscussionUtil.abbreviateHTML @model.get('markdownBody'), 140 @model.set('abbreviatedBody', abbreviated) diff --git a/common/static/js/vendor/jquery.truncate.js b/common/static/js/vendor/jquery.truncate.js new file mode 100755 index 0000000000..22d33e0f06 --- /dev/null +++ b/common/static/js/vendor/jquery.truncate.js @@ -0,0 +1,107 @@ +/** +Copyright (c) 2010-2012 Pathable + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. +https://github.com/pathable/truncate +**/ +(function($) { + + // Matches trailing non-space characters. + var chop = /(\s*\S+|\s)$/; + + // Return a truncated html string. Delegates to $.fn.truncate. + $.truncate = function(html, options) { + return $('
').append(html).truncate(options).html(); + }; + + // Truncate the contents of an element in place. + $.fn.truncate = function(options) { + if ($.isNumeric(options)) options = {length: options}; + var o = $.extend({}, $.truncate.defaults, options); + + return this.each(function() { + var self = $(this); + + if (o.noBreaks) self.find('br').replaceWith(' '); + + var text = self.text(); + var excess = text.length - o.length; + + if (o.stripTags) self.text(text); + + // Chop off any partial words if appropriate. + if (o.words && excess > 0) { + excess = text.length - text.slice(0, o.length).replace(chop, '').length - 1; + } + + if (excess < 0 || !excess && !o.truncated) return; + + // Iterate over each child node in reverse, removing excess text. + $.each(self.contents().get().reverse(), function(i, el) { + var $el = $(el); + var text = $el.text(); + var length = text.length; + + // If the text is longer than the excess, remove the node and continue. + if (length <= excess) { + o.truncated = true; + excess -= length; + $el.remove(); + return; + } + + // Remove the excess text and append the ellipsis. + if (el.nodeType === 3) { + $(el.splitText(length - excess - 1)).replaceWith(o.ellipsis); + return false; + } + + // Recursively truncate child nodes. + $el.truncate($.extend(o, {length: length - excess})); + return false; + }); + }); + }; + + $.truncate.defaults = { + + // Strip all html elements, leaving only plain text. + stripTags: false, + + // Only truncate at word boundaries. + words: false, + + // Replace instances of
with a single space. + noBreaks: false, + + // The maximum length of the truncated html. + length: Infinity, + + // The character to use as the ellipsis. The word joiner (U+2060) can be + // used to prevent a hanging ellipsis, but displays incorrectly in Chrome + // on Windows 7. + // http://code.google.com/p/chromium/issues/detail?id=68323 + ellipsis: '\u2026' // '\u2060\u2026' + + }; + +})(jQuery); diff --git a/common/static/js_test.yml b/common/static/js_test.yml index ec4b061a7b..1532cccc1e 100644 --- a/common/static/js_test.yml +++ b/common/static/js_test.yml @@ -31,6 +31,7 @@ lib_paths: - js/vendor/jquery.min.js - js/vendor/jasmine-jquery.js - js/vendor/jasmine-imagediff.js + - js/vendor/jquery.truncate.js - js/vendor/mustache.js - js/vendor/underscore-min.js - js/vendor/backbone-min.js diff --git a/lms/templates/discussion/_js_head_dependencies.html b/lms/templates/discussion/_js_head_dependencies.html index 264a259933..dd437c33b3 100644 --- a/lms/templates/discussion/_js_head_dependencies.html +++ b/lms/templates/discussion/_js_head_dependencies.html @@ -1,6 +1,7 @@ <%namespace name='static' file='../static_content.html'/> + diff --git a/lms/templates/discussion/mustache/_profile_thread.mustache b/lms/templates/discussion/mustache/_profile_thread.mustache index 877251a90b..51cb6e1b36 100644 --- a/lms/templates/discussion/mustache/_profile_thread.mustache +++ b/lms/templates/discussion/mustache/_profile_thread.mustache @@ -18,7 +18,7 @@

-
{{abbreviatedBody}}
+
{{{abbreviatedBody}}}
${_("View discussion")}