zulip/zephyr/static/js/search.js

395 lines
12 KiB
JavaScript
Raw Normal View History

var search = (function () {
var exports = {};
var cached_term = "";
var cached_matches = [];
var cached_index;
var cached_table = $('table.focused_table');
var current_search_term;
// Data storage for the typeahead -- to go from object to string representation and vice versa.
var labels = [];
var mapped = {};
function get_query(obj) {
return obj.query;
}
function get_person(obj) {
return typeahead_helper.render_person(obj.query);
}
function phrase_match(phrase, q) {
// match "tes" to "test" and "stream test" but not "hostess"
var i;
q = q.toLowerCase();
var parts = phrase.split(' ');
for (i = 0; i < parts.length; i++) {
if (parts[i].toLowerCase().indexOf(q) === 0) {
return true;
}
}
return false;
}
function person_matches_query(person, q) {
return phrase_match(person.full_name, q) || phrase_match(person.email, q);
}
function stream_matches_query(stream_name, q) {
return phrase_match(stream_name, q);
}
// Convert a list of operators to a human-readable description.
function describe(operators) {
return $.map(operators, function (elem) {
var operand = elem[1];
switch (elem[0]) {
case 'is':
if (operand === 'private') {
return 'Narrow to all private messages';
} else if (operand === 'starred') {
return 'Narrow to starred messages';
} else if (operand === 'mentioned') {
return 'Narrow to mentioned messages';
}
break;
case 'stream':
return 'Narrow to stream ' + operand;
case 'subject':
return 'Narrow to subject ' + operand;
case 'sender':
return 'Narrow to sender ' + operand;
case 'pm-with':
return 'Narrow to private messages with ' + operand;
case 'search':
return 'Search for ' + operand;
case 'in':
return 'Narrow to messages in ' + operand;
}
return 'Narrow to (unknown operator)';
}).join(', ');
}
function get_label(obj) {
switch (obj.action) {
case 'stream':
return 'stream:' + obj.query;
case 'private_message':
return 'pm-with:' + obj.query.email;
case 'sender':
return 'sender:' + obj.query.email;
case 'operators':
return obj.query;
}
}
exports.update_typeahead = function () {
var stream_names = subs.subscribed_streams();
stream_names.sort();
var streams = $.map(stream_names, function (elt,idx) {
return {action: 'stream', query: elt};
});
var people_names = page_params.people_list;
var people = $.map(people_names, function (elt,idx) {
return {action: 'private_message', query: elt};
});
var senders = $.map(people_names, function (elt,idx) {
return {action: 'sender', query: elt};
});
var options = streams.concat(people).concat(senders);
// The first slot is reserved for "search for x".
// (this is updated in the source function for our typeahead as well)
options.unshift({action: 'operators', query: '', operators: []});
mapped = {};
labels = [];
$.each(options, function (i, obj) {
var label = get_label(obj);
mapped[label] = obj;
obj.label = label;
labels.push(label);
});
};
function narrow_or_search_for_term(item) {
var search_query_box = $("#search_query");
var obj = mapped[item];
ui.change_tab_to('#home');
switch (obj.action) {
case 'stream':
narrow.by('stream', obj.query, {trigger: 'search'});
// It's sort of annoying that this is not in a position to
// blur the search box, because it means that Esc won't
// unnarrow, it'll leave the searchbox.
// Narrowing will have already put some operators in the search box,
// so leave the current text in.
search_query_box.blur();
return search_query_box.val();
case 'private_message':
narrow.by('pm-with', obj.query.email, {trigger: 'search'});
search_query_box.blur();
return search_query_box.val();
case 'sender':
narrow.by('sender', obj.query.email, {trigger: 'search'});
search_query_box.blur();
return search_query_box.val();
case 'operators':
narrow.activate(obj.operators, {trigger: 'search'});
search_query_box.blur();
return search_query_box.val();
}
return item;
}
function update_buttons_with_focus(focused) {
var search_query = $('#search_query');
// Show buttons iff the search input is focused, or has non-empty contents,
// or we are narrowed.
if (focused
|| search_query.val()
|| narrow.active()) {
$('.search_button').removeAttr('disabled');
} else {
$('.search_button').attr('disabled', 'disabled');
}
}
exports.update_button_visibility = function () {
update_buttons_with_focus($('#search_query').is(':focus'));
};
function highlight_person(query, person) {
var hilite = typeahead_helper.highlight_query_in_phrase;
return hilite(query, person.full_name) + " &lt;" + hilite(query, person.email) + "&gt;";
}
function get_stream_suggestions(query) {
var items = $.grep(labels, function (label) {
var obj = mapped[label];
if (obj.action === 'stream') {
return stream_matches_query(obj.query, query);
}
return false;
});
var objs = $.map(items, function (label) {
return mapped[label];
});
// streams are already sorted
objs = typeahead_helper.sorter(query, objs, get_query);
items = $.map(objs, function (obj) { return obj.label;});
return items;
}
function get_person_suggestions(query, action) {
var items = $.grep(labels, function (label) {
var obj = mapped[label];
if (obj.action === action) {
return person_matches_query(obj.query, query);
}
return false;
});
var objs = $.map(items, function (label) {
return mapped[label];
});
objs.sort(function (x, y) {
return typeahead_helper.compare_by_pms(get_query(x), get_query(y));
});
items = $.map(objs, function (obj) { return obj.label;});
return items;
}
exports.initialize = function () {
$( "#search_query" ).typeahead({
source: function (query, process) {
// Delete our old search queries (one for find-in-page, one for operators)
delete mapped[labels.shift()]; // Operators
var result = [];
// Add an entry for narrow by operators.
var operators = narrow.parse(query);
if (operators.length !== 0) {
var obj = {action: 'operators', query: query, operators: operators};
var label = get_label(obj);
mapped[label] = obj;
obj.label = label;
result = [label];
} else {
return [];
}
var stream_suggestions = get_stream_suggestions(query).slice(0,4);
result = result.concat(stream_suggestions);
var person_suggestions;
person_suggestions = get_person_suggestions(query, 'private_message').slice(0, 4);
result = result.concat(person_suggestions);
person_suggestions = get_person_suggestions(query, 'sender').slice(0, 4);
result = result.concat(person_suggestions);
return result;
},
items: 20,
highlighter: function (item) {
var query = this.query;
var obj = mapped[item];
var prefix;
var person;
var name;
var stream;
if (obj.action === 'private_message') {
prefix = 'Narrow to private messages with';
person = obj.query;
name = highlight_person(query, person);
return prefix + ' ' + name;
}
if (obj.action === 'sender') {
prefix = 'Narrow to messages sent by';
person = obj.query;
name = highlight_person(query, person);
return prefix + ' ' + name;
}
if (obj.action === 'stream') {
prefix = 'Narrow to stream';
stream = obj.query;
stream = typeahead_helper.highlight_query_in_phrase(query, stream);
return prefix + ' ' + stream;
}
var description = describe(obj.operators);
description = Handlebars.Utils.escapeExpression(description);
return description;
},
matcher: function (item) {
return true;
},
updater: narrow_or_search_for_term,
sorter: function (items) {
return items;
}
});
$("#searchbox_form").keydown(function (e) {
exports.update_button_visibility();
var code = e.which;
var search_query_box = $("#search_query");
if (code === 13 && search_query_box.is(":focus")) {
// Don't submit the form so that the typeahead can instead
// handle our Enter keypress. Any searching that needs
// to be done will be handled in the keyup.
e.preventDefault();
return false;
}
}).keyup(function (e) {
var code = e.which;
var search_query_box = $("#search_query");
if (code === 13 && search_query_box.is(":focus")) {
// We just pressed enter and the box had focus, which
// means we didn't use the typeahead at all. In that
// case, we should act as though we're searching by
// operators. (The reason the other actions don't call
// this codepath is that they first all blur the box to
// indicate that they've done what they need to do)
if (search_query_box.val().trim()) {
narrow.activate(narrow.parse(search_query_box.val()));
}
search_query_box.blur();
update_buttons_with_focus(false);
}
});
// Some of these functions don't actually need to be exported,
// but the code was moved here from elsewhere, and it would be
// more work to re-order everything and make them private.
$('#search_exit' ).on('click', exports.clear_search);
var query = $('#search_query');
query.on('focus', exports.focus_search)
.on('blur' , function () {
// The search query box is a visual cue as to
// whether search or narrowing is active. If
// neither is active, we should clear the box on
// blur.
//
// But we can't do this right away, because
// selecting something in the typeahead menu causes
// the box to lose focus a moment before. We would
// clear the thing we're about to search for.
//
// The workaround is to check 100ms later -- long
// enough for the search to have gone through, but
// short enough that the user won't notice (though
// really it would be OK if they did).
setTimeout(function () {
if (!(narrow.active())) {
query.val('');
}
exports.update_button_visibility();
}, 100);
});
};
function match_on_visible_text(row, search_term) {
// You can't select on :visible, since that includes hidden elements that
// take up space.
return row.find(".message_content, .message_header")
.text().toLowerCase().indexOf(search_term) !== -1;
}
exports.focus_search = function () {
// The search bar is not focused yet, but will be.
update_buttons_with_focus(true);
};
exports.initiate_search = function () {
$('#search_query').select();
};
exports.clear_search = function () {
narrow.deactivate();
$('table tr').removeHighlight();
$('#search_query').blur();
exports.update_button_visibility();
};
return exports;
}());