FEATURE: search highlighting within topic

BUGFIX: fixed hiding of the search dialog when navigating within a topic
This commit is contained in:
Sam 2014-06-05 16:59:18 +10:00
parent 96fc5addc4
commit e7991cb803
7 changed files with 182 additions and 20 deletions

View File

@ -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);

View File

@ -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");

View File

@ -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 <span class='highlight'> (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 <em class='important'>
* $('#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);
});
};

View File

@ -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";

View File

@ -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();
}
}
});

View File

@ -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')
});

View File

@ -19,3 +19,9 @@
color: scale-color($primary, $lightness: 50%);
}
}
.cooked .highlight{
background-color: scale-color($highlight, $lightness: 40%);
padding: 2px;
margin: -2px;
}