summaryrefslogtreecommitdiff
path: root/web-ui
diff options
context:
space:
mode:
authorFelix Hammerl <fhammerl@thoughtworks.com>2016-03-03 12:25:25 +0100
committerFelix Hammerl <fhammerl@thoughtworks.com>2016-03-09 15:36:10 +0100
commit23b175742f20d96e5b5d3d9cfcc0ed7067197f92 (patch)
tree224e5d536b9a6c4859671b2cff57f1f60236ef9f /web-ui
parentd2d6fb98b909021efa435411ec95f554769fa9c9 (diff)
Issue #617: Highlight search terms by altering mail content
Diffstat (limited to 'web-ui')
-rw-r--r--web-ui/app/js/helpers/sanitizer.js32
-rw-r--r--web-ui/app/js/mail_view/ui/mail_view.js10
-rw-r--r--web-ui/app/js/page/events.js2
-rw-r--r--web-ui/app/js/search/results_highlighter.js29
-rw-r--r--web-ui/app/scss/sandbox.scss6
-rw-r--r--web-ui/test/spec/helpers/sanitizer.spec.js6
-rw-r--r--web-ui/test/spec/mail_view/ui/mail_view.spec.js6
-rw-r--r--web-ui/test/spec/search/results_highlighter.spec.js14
8 files changed, 94 insertions, 11 deletions
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: '&#x3C;&#x65;&#x6D;&#x20;&#x63;&#x6C;&#x61;&#x73;&#x73;&#x3D;&#x22;&#x73;&#x65;&#x61;&#x72;&#x63;&#x68;&#x2D;&#x68;&#x69;&#x67;&#x68;&#x6C;&#x69;&#x67;&#x68;&#x74;&#x22;&#x3E;',
+ post: '<em class="search-highlight">'
+ }, {
+ // highlight tag close
+ pre: '&#x3C;&#x2F;&#x65;&#x6D;&#x3E;',
+ post: '</em>'
+ }];
+
/**
* 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, '<em class="search-highlight">$1</em>');
+ });
+ 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('123<a>asd</a>');
expect(output).toEqual(expectedOutput);
});
+
+ it('should leave highlighted text untouched', function () {
+ var expectedOutput = '<em class="search-highlight">&#x31;&#x32;&#x33;&#x3C;&#x61;&#x3E;&#x61;&#x73;&#x64;&#x3C;&#x2F;&#x61;&#x3E;</em>';
+ var output = sanitizer.purifyText('<em class="search-highlight">123<a>asd</a></em>');
+ 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('<div id="text">Any one seeing too many open bugs</div>');
+ });
+ 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('<div id="text">Any one seeing too many open bugs</div>');
+ it('highlights a string with the keywords given', function () {
+ this.component.attr = {keywords: ['foo']};
+ var expectedString = 'the <em class="search-highlight">foo</em> 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);