From 23b175742f20d96e5b5d3d9cfcc0ed7067197f92 Mon Sep 17 00:00:00 2001 From: Felix Hammerl Date: Thu, 3 Mar 2016 12:25:25 +0100 Subject: Issue #617: Highlight search terms by altering mail content --- web-ui/app/js/helpers/sanitizer.js | 32 +++++++++++++++++----- web-ui/app/js/mail_view/ui/mail_view.js | 10 ++++++- web-ui/app/js/page/events.js | 2 ++ web-ui/app/js/search/results_highlighter.js | 29 ++++++++++++++++++++ web-ui/app/scss/sandbox.scss | 6 ++++ web-ui/test/spec/helpers/sanitizer.spec.js | 6 ++++ web-ui/test/spec/mail_view/ui/mail_view.spec.js | 6 ++++ .../test/spec/search/results_highlighter.spec.js | 14 ++++++++-- 8 files changed, 94 insertions(+), 11 deletions(-) (limited to 'web-ui') diff --git a/web-ui/app/js/helpers/sanitizer.js b/web-ui/app/js/helpers/sanitizer.js index eea1f0f7..443e8602 100644 --- a/web-ui/app/js/helpers/sanitizer.js +++ b/web-ui/app/js/helpers/sanitizer.js @@ -23,6 +23,16 @@ define(['DOMPurify', 'he'], function (DOMPurify, he) { */ var sanitizer = {}; + sanitizer.whitelist = [{ + // highlight tag open + pre: '<em class="search-highlight">', + post: '' + }, { + // highlight tag close + pre: '</em>', + post: '' + }]; + /** * Adds html line breaks to a plaintext with line breaks (incl carriage return) * @@ -55,16 +65,24 @@ define(['DOMPurify', 'he'], function (DOMPurify, he) { }; /** - * Runs a given dirty body through he, thereby encoding everything - * as HTML entities. - * - * @param {string} dirtyBody The unsanitized string - * @return {string} Safe-to-display HTML string - */ + * Runs a given dirty body through he, thereby encoding everything + * as HTML entities. + * + * @param {string} dirtyBody The unsanitized string + * @return {string} Safe-to-display HTML string + */ sanitizer.purifyText = function (dirtyBody) { - return he.encode(dirtyBody, { + var escapedBody = he.encode(dirtyBody, { encodeEverything: true }); + + this.whitelist.forEach(function(entry) { + while (escapedBody.indexOf(entry.pre) > -1) { + escapedBody = escapedBody.replace(entry.pre, entry.post); + } + }); + + return escapedBody; }; /** diff --git a/web-ui/app/js/mail_view/ui/mail_view.js b/web-ui/app/js/mail_view/ui/mail_view.js index fbbba409..d952fed7 100644 --- a/web-ui/app/js/mail_view/ui/mail_view.js +++ b/web-ui/app/js/mail_view/ui/mail_view.js @@ -257,9 +257,17 @@ define( this.trigger(events.mail.want, {mail: this.attr.ident, caller: this}); }; + this.highlightMailContent = function (event, data) { + // we can't directly manipulate the iFrame to highlight the content + // so we need to take an indirection where we directly manipulate + // the mail content to accomodate the highlighting + this.trigger(document, events.mail.highlightMailContent, data); + }; + this.after('initialize', function () { - this.on(this, events.mail.here, this.displayMail); this.on(this, events.mail.notFound, this.openNoMessageSelectedPane); + this.on(this, events.mail.here, this.highlightMailContent); + this.on(document, events.mail.display, this.displayMail); this.on(document, events.dispatchers.rightPane.clear, this.teardown); this.on(document, events.mail.tags.updated, this.tagsUpdated); this.on(document, events.mail.deleted, this.mailDeleted); diff --git a/web-ui/app/js/page/events.js b/web-ui/app/js/page/events.js index 7a0dbf9d..ad15e76e 100644 --- a/web-ui/app/js/page/events.js +++ b/web-ui/app/js/page/events.js @@ -121,6 +121,8 @@ define(function () { mail: { here: 'mail:here', want: 'mail:want', + display: 'mail:display', + highlightMailContent: 'mail:highlightMailContent', send: 'mail:send', send_failed: 'mail:send_failed', sent: 'mail:sent', diff --git a/web-ui/app/js/search/results_highlighter.js b/web-ui/app/js/search/results_highlighter.js index 9e3ba167..831be0cd 100644 --- a/web-ui/app/js/search/results_highlighter.js +++ b/web-ui/app/js/search/results_highlighter.js @@ -40,6 +40,7 @@ define( var domIdent = data.where; if(this.attr.keywords) { _.each(this.attr.keywords, function (keyword) { + keyword = escapeRegExp(keyword); $(domIdent).highlightRegex(new RegExp(keyword, 'i'), { tagType: 'em', className: 'search-highlight' @@ -57,12 +58,40 @@ define( }); }; + this.highlightString = function (string) { + _.each(this.attr.keywords, function (keyword) { + keyword = escapeRegExp(keyword); + var regex = new RegExp('(' + keyword + ')', 'ig'); + string = string.replace(regex, '$1'); + }); + return string; + }; + + /* + * Alter data.mail.textPlainBody to highlight each of this.attr.keywords + * and pass it back to the mail_view when done + */ + this.highlightMailContent = function(ev, data){ + var mail = data.mail; + mail.textPlainBody = this.highlightString(mail.textPlainBody); + this.trigger(document, events.mail.display, data); + }; + + /* + * Escapes the special charaters used regular expressions that + * would cause problems with strings in the RegExp constructor + */ + function escapeRegExp(string){ + return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + } + this.after('initialize', function () { this.on(document, events.search.perform, this.getKeywordsSearch); this.on(document, events.ui.tag.select, this.clearHighlights); this.on(document, events.search.resetHighlight, this.clearHighlights); this.on(document, events.search.highlightResults, this.highlightResults); + this.on(document, events.mail.highlightMailContent, this.highlightMailContent); }); } }); diff --git a/web-ui/app/scss/sandbox.scss b/web-ui/app/scss/sandbox.scss index e722753d..3cb4c441 100644 --- a/web-ui/app/scss/sandbox.scss +++ b/web-ui/app/scss/sandbox.scss @@ -1,3 +1,5 @@ +$search-highlight: #FFEF29; + body { font-family: "Open Sans", "Microsoft YaHei", "Hiragino Sans GB", "Hiragino Sans GB W3", "微软雅黑", "Helvetica Neue", Arial, sans-serif; font-size: 13px; @@ -12,3 +14,7 @@ body { box-sizing: border-box; word-wrap: break-word; } + +.search-highlight { + background-color: $search-highlight; +} diff --git a/web-ui/test/spec/helpers/sanitizer.spec.js b/web-ui/test/spec/helpers/sanitizer.spec.js index acd4b2b2..b553583e 100644 --- a/web-ui/test/spec/helpers/sanitizer.spec.js +++ b/web-ui/test/spec/helpers/sanitizer.spec.js @@ -25,6 +25,12 @@ define(['helpers/sanitizer'], function (sanitizer) { var output = sanitizer.purifyText('123asd'); expect(output).toEqual(expectedOutput); }); + + it('should leave highlighted text untouched', function () { + var expectedOutput = '123<a>asd</a>'; + var output = sanitizer.purifyText('123asd'); + expect(output).toEqual(expectedOutput); + }); }); describe('sanitizer.sanitize', function () { diff --git a/web-ui/test/spec/mail_view/ui/mail_view.spec.js b/web-ui/test/spec/mail_view/ui/mail_view.spec.js index 9ed56023..9f1114a7 100644 --- a/web-ui/test/spec/mail_view/ui/mail_view.spec.js +++ b/web-ui/test/spec/mail_view/ui/mail_view.spec.js @@ -21,6 +21,12 @@ describeComponent('mail_view/ui/mail_view', function () { expect(spyEvent.mostRecentCall.data.mail).toEqual(1); }); + it('triggers mail.highlightMailContent when receiving mail.here', function () { + var hightlightEvent = spyOnEvent(document,Pixelated.events.mail.highlightMailContent); + this.component.trigger(this.component, Pixelated.events.mail.here); + expect(hightlightEvent).toHaveBeenTriggeredOn(document); + }); + it('triggers dispatchers.rightPane.openNoMessageSelected when getting mail.notFound', function () { var openNoMessageSelectedEvent = spyOnEvent(document, Pixelated.events.dispatchers.rightPane.openNoMessageSelected); diff --git a/web-ui/test/spec/search/results_highlighter.spec.js b/web-ui/test/spec/search/results_highlighter.spec.js index cfb61e9c..13131a8e 100644 --- a/web-ui/test/spec/search/results_highlighter.spec.js +++ b/web-ui/test/spec/search/results_highlighter.spec.js @@ -1,9 +1,11 @@ describeComponent('search/results_highlighter', function () { 'use strict'; - it('highlights words or parts of words that match with the keywords given', function () { + beforeEach(function () { this.setupComponent('
Any one seeing too many open bugs
'); + }); + it('highlights words or parts of words that match with the keywords given', function () { this.component.attr = {keywords: ['any']}; this.component.highlightResults(event, {where: '#text'}); @@ -12,9 +14,15 @@ describeComponent('search/results_highlighter', function () { expect(highlightedWords).toEqual(2); }); - it('resets highlights when a new search is performed', function() { - this.setupComponent('
Any one seeing too many open bugs
'); + it('highlights a string with the keywords given', function () { + this.component.attr = {keywords: ['foo']}; + var expectedString = 'the foo bar'; + var string = this.component.highlightString('the foo bar'); + + expect(string).toEqual(expectedString); + }); + it('resets highlights when a new search is performed', function() { this.component.attr = {keywords: ['any']}; this.component.highlightResults(event, {where: '#text'}); $(document).trigger(Pixelated.events.search.resetHighlight); -- cgit v1.2.3