diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 2400a67a3f3..317106eaaf5 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -8,6 +8,10 @@ **/ export default Em.ArrayController.extend(Discourse.Presence, { + contextChanged: function(){ + this.setProperties({ term: "", content: [], resultCount: 0, urls: [] }); + }.observes("searchContext"), + // If we need to perform another search newSearchNeeded: function() { this.set('noResults', false); diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js index 9189466e7fd..b31636df6d6 100644 --- a/app/assets/javascripts/discourse/controllers/topic_controller.js +++ b/app/assets/javascripts/discourse/controllers/topic_controller.js @@ -8,13 +8,31 @@ **/ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.SelectedPostsCount, { multiSelect: false, - needs: ['header', 'modal', 'composer', 'quote-button'], + needs: ['header', 'modal', 'composer', 'quote-button', 'search'], allPostsSelected: false, editingTopic: false, selectedPosts: null, selectedReplies: null, queryParams: ['filter', 'username_filters'], + contextChanged: function(){ + this.set('controllers.search.searchContext', this.get('model.searchContext')); + }.observes('topic'), + + termChanged: function(){ + var dropdown = this.get('controllers.header.visibleDropdown'); + var term = this.get('controllers.search.term'); + + if(dropdown === 'search-dropdown' && term){ + this.set('searchHighlight', term); + } else { + if(this.get('searchHighlight')){ + this.set('searchHighlight', null); + } + } + + }.observes('controllers.search.term', 'controllers.header.visibleDropdown'), + filter: function(key, value) { if (arguments.length > 1) { this.set('postStream.summary', value === "summary"); diff --git a/app/assets/javascripts/discourse/lib/highlight.js b/app/assets/javascripts/discourse/lib/highlight.js new file mode 100644 index 00000000000..f35e8546fcb --- /dev/null +++ b/app/assets/javascripts/discourse/lib/highlight.js @@ -0,0 +1,109 @@ +// forked cause we may want to amend the logic a bit +/* + * jQuery Highlight plugin + * + * Based on highlight v3 by Johann Burkard + * http://johannburkard.de/blog/programming/javascript/highlight-javascript-text-higlighting-jquery-plugin.html + * + * Code a little bit refactored and cleaned (in my humble opinion). + * Most important changes: + * - has an option to highlight only entire words (wordsOnly - false by default), + * - has an option to be case sensitive (caseSensitive - false by default) + * - highlight element tag and class names can be specified in options + * + * Usage: + * // wrap every occurrance of text 'lorem' in content + * // with (default options) + * $('#content').highlight('lorem'); + * + * // search for and highlight more terms at once + * // so you can save some time on traversing DOM + * $('#content').highlight(['lorem', 'ipsum']); + * $('#content').highlight('lorem ipsum'); + * + * // search only for entire word 'lorem' + * $('#content').highlight('lorem', { wordsOnly: true }); + * + * // don't ignore case during search of term 'lorem' + * $('#content').highlight('lorem', { caseSensitive: true }); + * + * // wrap every occurrance of term 'ipsum' in content + * // with + * $('#content').highlight('ipsum', { element: 'em', className: 'important' }); + * + * // remove default highlight + * $('#content').unhighlight(); + * + * // remove custom highlight + * $('#content').unhighlight({ element: 'em', className: 'important' }); + * + * + * Copyright (c) 2009 Bartek Szopka + * + * Licensed under MIT license. + * + */ + +jQuery.extend({ + highlight: function (node, re, nodeName, className) { + if (node.nodeType === 3) { + var match = node.data.match(re); + if (match) { + var highlight = document.createElement(nodeName || 'span'); + highlight.className = className || 'highlight'; + var wordNode = node.splitText(match.index); + wordNode.splitText(match[0].length); + var wordClone = wordNode.cloneNode(true); + highlight.appendChild(wordClone); + wordNode.parentNode.replaceChild(highlight, wordNode); + return 1; //skip added node in parent + } + } else if ((node.nodeType === 1 && node.childNodes) && // only element nodes that have children + !/(script|style)/i.test(node.tagName) && // ignore script and style nodes + !(node.tagName === nodeName.toUpperCase() && node.className === className)) { // skip if already highlighted + for (var i = 0; i < node.childNodes.length; i++) { + i += jQuery.highlight(node.childNodes[i], re, nodeName, className); + } + } + return 0; + } +}); + +jQuery.fn.unhighlight = function (options) { + var settings = { className: 'highlight', element: 'span' }; + jQuery.extend(settings, options); + + return this.find(settings.element + "." + settings.className).each(function () { + var parent = this.parentNode; + parent.replaceChild(this.firstChild, this); + parent.normalize(); + }).end(); +}; + +jQuery.fn.highlight = function (words, options) { + var settings = { className: 'highlight', element: 'span', caseSensitive: false, wordsOnly: false }; + jQuery.extend(settings, options); + + if (words.constructor === String) { + words = [words]; + } + words = jQuery.grep(words, function(word){ + return word !== ''; + }); + words = jQuery.map(words, function(word) { + return word.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); + }); + if (words.length === 0) { return this; } + + var flag = settings.caseSensitive ? "" : "i"; + var pattern = "(" + words.join("|") + ")"; + if (settings.wordsOnly) { + pattern = "\\b" + pattern + "\\b"; + } + var re = new RegExp(pattern, flag); + + return this.each(function () { + jQuery.highlight(this, re, settings.element, settings.className); + }); +}; + diff --git a/app/assets/javascripts/discourse/lib/url.js b/app/assets/javascripts/discourse/lib/url.js index eeac7345821..6134da19121 100644 --- a/app/assets/javascripts/discourse/lib/url.js +++ b/app/assets/javascripts/discourse/lib/url.js @@ -86,8 +86,6 @@ Discourse.URL = Em.Object.createWithMixins({ path = path.replace(rootURL, ''); } - // Schedule a DOM cleanup event - Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM'); // Rewrite /my/* urls if (path.indexOf('/my/') === 0) { @@ -100,9 +98,12 @@ Discourse.URL = Em.Object.createWithMixins({ } } + if (this.navigatedToPost(oldPath, path)) { return; } + // Schedule a DOM cleanup event + Em.run.scheduleOnce('afterRender', Discourse.Route, 'cleanDOM'); + // TODO: Extract into rules we can inject into the URL handler if (this.navigatedToHome(oldPath, path)) { return; } - if (this.navigatedToPost(oldPath, path)) { return; } if (path.match(/^\/?users\/[^\/]+$/)) { path += "/activity"; diff --git a/app/assets/javascripts/discourse/views/header_view.js b/app/assets/javascripts/discourse/views/header_view.js index e6d5be2c286..2d25a8a70e8 100644 --- a/app/assets/javascripts/discourse/views/header_view.js +++ b/app/assets/javascripts/discourse/views/header_view.js @@ -11,15 +11,16 @@ Discourse.HeaderView = Discourse.View.extend({ classNames: ['d-header', 'clearfix'], classNameBindings: ['editingTopic'], templateName: 'header', - topicBinding: 'Discourse.router.topicController.content', showDropdown: function($target) { var elementId = $target.data('dropdown') || $target.data('notifications'), $dropdown = $("#" + elementId), $li = $target.closest('li'), $ul = $target.closest('ul'), - $html = $('html'); + $html = $('html'), + self = this; + self.set('controller.visibleDropdown', elementId); // we need to ensure we are rendered, // this optimises the speed of the initial render var render = $target.data('render'); @@ -27,7 +28,7 @@ Discourse.HeaderView = Discourse.View.extend({ if(!this.get(render)){ this.set(render, true); Em.run.next(this, function(){ - this.showDropdown($target); + this.showDropdown.apply(self, [$target]); }); return; } @@ -37,6 +38,7 @@ Discourse.HeaderView = Discourse.View.extend({ $dropdown.fadeOut('fast'); $li.removeClass('active'); $html.data('hide-dropdown', null); + self.set('controller.visibleDropdown', null); return $html.off('click.d-dropdown'); }; @@ -53,7 +55,7 @@ Discourse.HeaderView = Discourse.View.extend({ $dropdown.find('input[type=text]').focus().select(); $html.on('click.d-dropdown', function(e) { - return $(e.target).closest('.d-dropdown').length > 0 ? true : hideDropdown(); + return $(e.target).closest('.d-dropdown').length > 0 ? true : hideDropdown.apply(self); }); $html.data('hide-dropdown', hideDropdown); @@ -107,36 +109,37 @@ Discourse.HeaderView = Discourse.View.extend({ didInsertElement: function() { - var headerView = this; + var self = this; + this.$('a[data-dropdown]').on('click.dropdown', function(e) { - headerView.showDropdown($(e.currentTarget)); + self.showDropdown.apply(self, [$(e.currentTarget)]); return false; }); this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').on('click.notifications', function(e) { - headerView.showNotifications(e); + self.showNotifications(e); return false; }); $(window).bind('scroll.discourse-dock', function() { - headerView.examineDockHeader(); + self.examineDockHeader(); }); $(document).bind('touchmove.discourse-dock', function() { - headerView.examineDockHeader(); + self.examineDockHeader(); }); - this.examineDockHeader(); + self.examineDockHeader(); // Delegate ESC to the composer $('body').on('keydown.header', function(e) { // Hide dropdowns if (e.which === 27) { - headerView.$('li').removeClass('active'); - headerView.$('.d-dropdown').fadeOut('fast'); + self.$('li').removeClass('active'); + self.$('.d-dropdown').fadeOut('fast'); } - if (headerView.get('editingTopic')) { + if (self.get('editingTopic')) { if (e.which === 13) { - headerView.finishedEdit(); + self.finishedEdit(); } if (e.which === 27) { - return headerView.cancelEdit(); + return self.cancelEdit(); } } }); diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js index 367b6d2de10..7376b23f076 100644 --- a/app/assets/javascripts/discourse/views/post_view.js +++ b/app/assets/javascripts/discourse/views/post_view.js @@ -257,5 +257,26 @@ Discourse.PostView = Discourse.GroupedView.extend(Ember.Evented, { // Find all the quotes Em.run.scheduleOnce('afterRender', this, 'insertQuoteControls'); - } + + this.applySearchHighlight(); + }, + + applySearchHighlight: function(){ + var highlight = this.get('controller.searchHighlight'); + var cooked = this.$('.cooked'); + + if(!cooked){ return; } + + if(highlight && highlight.length > 2){ + if(this._highlighted){ + cooked.unhighlight(); + } + cooked.highlight(highlight); + this._highlighted = true; + + } else if(this._highlighted){ + cooked.unhighlight(); + this._highlighted = false; + } + }.observes('controller.searchHighlight', 'cooked') }); diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 9397643fdc0..420e7519680 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -19,3 +19,9 @@ color: scale-color($primary, $lightness: 50%); } } + +.cooked .highlight{ + background-color: scale-color($highlight, $lightness: 40%); + padding: 2px; + margin: -2px; +}