search: Add a basic implementation of search pills.

Following points have been implemented in this commit:
1.) Add search pill on selecting typeahead.
2.) Re-narrow after removing a search pill.
3.) Add quiet optional parameter to removeLastPill.
4.) Pre populate search pills in narrow.activate.
5.) Clear existing search pills on narrow.deactivate.

Description of above points:
1.) I tried out using the description from suggestions.lookup_table
to append a pill using appendValidatedData so that the description
had not to be calculated again. But the description in the suggestions
lookup contains html due to highlighting. This html is escaped when
inputed in a pill. An attempt was also made to remove the higlighting
by replacing the tags. But other espaced characters like < also
popped up, so it was better to use append_search_string.
3.) If one wants to refresh the pill using pill.clear and wants to
repopulate them, evaluating the event_handler associated with the
action of removing the pill may not be desired.
4.) Pill population code is added to narrow.activate. Pills are not
populated if the narrow was triggered by search as search handles the
addition and removal of pill by itself. The reason for not handling
search too in narrow.activate is to avoid clearing the pills and
repopulating them. Example of some of the triggers for narrow.activate
include `restore draft`, `topic change`,`sidebar`.

Also modifies tests for search.js
This commit is contained in:
Shubham Padia 2018-07-14 19:40:00 +05:30 committed by Tim Abbott
parent 6ff13d0d01
commit 36707a33ca
9 changed files with 145 additions and 62 deletions

View File

@ -6,6 +6,7 @@ zrequire('Filter', 'js/filter');
zrequire('MessageListData', 'js/message_list_data'); zrequire('MessageListData', 'js/message_list_data');
zrequire('unread'); zrequire('unread');
zrequire('narrow'); zrequire('narrow');
zrequire('search_pill');
set_global('blueslip', {}); set_global('blueslip', {});
set_global('channel', {}); set_global('channel', {});
@ -24,6 +25,12 @@ set_global('stream_list', {});
set_global('top_left_corner', {}); set_global('top_left_corner', {});
set_global('ui_util', {}); set_global('ui_util', {});
set_global('unread_ops', {}); set_global('unread_ops', {});
set_global('search_pill_widget', {
my_pill: {
clear: function () {return true;},
appendValue: function () {return true;},
},
});
var noop = () => {}; var noop = () => {};

View File

@ -2,6 +2,10 @@ set_global('page_params', {
search_pills_enabled: true, search_pills_enabled: true,
}); });
zrequire('search'); zrequire('search');
zrequire('search_pill');
zrequire('util');
zrequire('Filter', 'js/filter');
zrequire('search_pill_widget');
const noop = () => {}; const noop = () => {};
const return_true = () => true; const return_true = () => true;
@ -14,7 +18,8 @@ set_global('ui_util', {
change_tab_to: noop, change_tab_to: noop,
}); });
set_global('narrow', {}); set_global('narrow', {});
set_global('Filter', {});
search_pill.append_search_string = noop;
global.patch_builtin('setTimeout', func => func()); global.patch_builtin('setTimeout', func => func());
@ -105,21 +110,25 @@ run_test('initizalize', () => {
{ {
let operators; let operators;
let is_blurred; let is_blurred;
let is_append_search_string_called;
search_query_box.blur = () => { search_query_box.blur = () => {
is_blurred = true; is_blurred = true;
}; };
search_pill.append_search_string = () => {
is_append_search_string_called = true;
};
/* Test updater */ /* Test updater */
const _setup = (search_box_val) => { const _setup = (search_box_val) => {
is_blurred = false; is_blurred = false;
is_append_search_string_called = false;
search_query_box.val(search_box_val); search_query_box.val(search_box_val);
Filter.parse = (search_string) => {
assert.equal(search_string, search_box_val);
return operators;
};
narrow.activate = (raw_operators, options) => { narrow.activate = (raw_operators, options) => {
assert.deepEqual(raw_operators, operators); assert.deepEqual(raw_operators, operators);
assert.deepEqual(options, {trigger: 'search'}); assert.deepEqual(options, {trigger: 'search'});
}; };
search_pill.get_search_string_for_current_filter = () => {
return '';
};
}; };
operators = [{ operators = [{
@ -128,8 +137,9 @@ run_test('initizalize', () => {
operand: 'ver', operand: 'ver',
}]; }];
_setup('ver'); _setup('ver');
assert.equal(opts.updater('ver'), 'ver'); opts.updater('ver');
assert(is_blurred); assert(is_blurred);
assert(is_append_search_string_called);
operators = [{ operators = [{
negated: false, negated: false,
@ -137,13 +147,15 @@ run_test('initizalize', () => {
operand: 'Verona', operand: 'Verona',
}]; }];
_setup('stream:Verona'); _setup('stream:Verona');
assert.equal(opts.updater('stream:Verona'), 'stream:Verona'); opts.updater('stream:Verona');
assert(is_blurred); assert(is_blurred);
assert(is_append_search_string_called);
search.is_using_input_method = true; search.is_using_input_method = true;
_setup('stream:Verona'); _setup('stream:Verona');
assert.equal(opts.updater('stream:Verona'), 'stream:Verona'); opts.updater('stream:Verona');
assert(!is_blurred); assert(!is_blurred);
assert(is_append_search_string_called);
} }
}; };
@ -177,22 +189,17 @@ run_test('initizalize', () => {
is_blurred = false; is_blurred = false;
search_button.prop('disabled', false); search_button.prop('disabled', false);
search_query_box.val(search_box_val); search_query_box.val(search_box_val);
Filter.parse = (search_string) => {
assert.equal(search_string, search_box_val);
return operators;
};
narrow.activate = (raw_operators, options) => { narrow.activate = (raw_operators, options) => {
assert.deepEqual(raw_operators, operators); assert.deepEqual(raw_operators, operators);
assert.deepEqual(options, {trigger: 'search'}); assert.deepEqual(options, {trigger: 'search'});
}; };
search_pill.get_search_string_for_current_filter = () => {
return '';
};
}; };
operators = [{ operators = [];
negated: false,
operator: 'search',
operand: '',
}];
_setup(''); _setup('');
ev.which = 15; ev.which = 15;
@ -215,7 +222,11 @@ run_test('initizalize', () => {
assert(is_blurred); assert(is_blurred);
assert(search_button.prop('disabled')); assert(search_button.prop('disabled'));
operators = [{
negated: false,
operator: 'search',
operand: 'ver',
}];
_setup('ver'); _setup('ver');
search.is_using_input_method = true; search.is_using_input_method = true;
func(ev); func(ev);

View File

@ -5,13 +5,13 @@ zrequire('Filter', 'js/filter');
zrequire('Handlebars', 'handlebars'); zrequire('Handlebars', 'handlebars');
var is_starred_item = { var is_starred_item = {
display_value: 'starred messages', display_value: 'is:starred',
search_string: 'is:starred', description: 'starred messages',
}; };
var is_private_item = { var is_private_item = {
display_value: 'private messages', display_value: 'is:private',
search_string: 'is:private', description: 'private messages',
}; };
run_test('create_item', () => { run_test('create_item', () => {
@ -34,7 +34,7 @@ run_test('append', () => {
function fake_append(search_string) { function fake_append(search_string) {
appended = true; appended = true;
assert.equal(search_string, is_starred_item.search_string); assert.equal(search_string, is_starred_item.display_value);
} }
function fake_clear() { function fake_clear() {
@ -46,7 +46,7 @@ run_test('append', () => {
clear_text: fake_clear, clear_text: fake_clear,
}; };
search_pill.append_search_string(is_starred_item.search_string, pill_widget); search_pill.append_search_string(is_starred_item.display_value, pill_widget);
assert(appended); assert(appended);
assert(cleared); assert(cleared);
@ -60,7 +60,7 @@ run_test('get_items', () => {
}; };
assert.deepEqual(search_pill.get_search_string_for_current_filter(pill_widget), assert.deepEqual(search_pill.get_search_string_for_current_filter(pill_widget),
is_starred_item.search_string + ' ' + is_private_item.search_string); is_starred_item.display_value + ' ' + is_private_item.display_value);
}); });
run_test('create_pills', () => { run_test('create_pills', () => {

View File

@ -245,6 +245,14 @@ exports.activate = function (raw_operators, opts) {
hashchange.save_narrow(operators); hashchange.save_narrow(operators);
} }
if (page_params.search_pills_enabled && opts.trigger !== 'search') {
search_pill_widget.my_pill.clear(true);
_.each(operators, function (operator) {
var search_string = Filter.unparse([operator]);
search_pill.append_search_string(search_string, search_pill_widget.my_pill);
});
}
// Put the narrow operators in the search bar. // Put the narrow operators in the search bar.
$('#search_query').val(Filter.unparse(operators)); $('#search_query').val(Filter.unparse(operators));
search.update_button_visibility(); search.update_button_visibility();
@ -655,6 +663,11 @@ exports.deactivate = function () {
compose_fade.update_message_list(); compose_fade.update_message_list();
// clear existing search pills
if (page_params.search_pills_enabled) {
search_pill_widget.my_pill.clear(true);
}
top_left_corner.handle_narrow_deactivated(); top_left_corner.handle_narrow_deactivated();
stream_list.handle_narrow_deactivated(); stream_list.handle_narrow_deactivated();

View File

@ -14,7 +14,20 @@ function narrow_or_search_for_term(search_string) {
return search_query_box.val(); return search_query_box.val();
} }
ui_util.change_tab_to('#home'); ui_util.change_tab_to('#home');
var operators = Filter.parse(search_string);
var operators;
if (page_params.search_pills_enabled) {
// search_string only contains the suggestion selected
// from the typeahead. base_query stores the query
// corresponding to the existing pills.
var base_query = search_pill.get_search_string_for_current_filter(
search_pill_widget.my_pill);
var base_operators = Filter.parse(base_query);
var suggestion_operator = Filter.parse(search_string);
operators = base_operators.concat(suggestion_operator);
} else {
operators = Filter.parse(search_string);
}
narrow.activate(operators, {trigger: 'search'}); narrow.activate(operators, {trigger: 'search'});
// It's sort of annoying that this is not in a position to // It's sort of annoying that this is not in a position to
@ -74,10 +87,28 @@ exports.initialize = function () {
matcher: function () { matcher: function () {
return true; return true;
}, },
updater: narrow_or_search_for_term, updater: function (search_string) {
// Order is important here. narrow_or_search_for_term
// gets a search string from existing pills and obtains
// existing operators. Newly selected suggestion is added
// to those operators. If narrow_or_search_for_term was
// called after append_search_string, the existing search
// pills at the time for calling that function would also
// have the newly selected suggestion, and appending it again
// would cause duplication.
var result = narrow_or_search_for_term(search_string);
if (page_params.search_pills_enabled) {
search_pill.append_search_string(search_string,
search_pill_widget.my_pill);
$("#search_query").focus();
} else {
return result;
}
},
sorter: function (items) { sorter: function (items) {
return items; return items;
}, },
stopAdvance: page_params.search_pills_enabled,
}); });
searchbox_form.on('compositionend', function () { searchbox_form.on('compositionend', function () {

View File

@ -5,13 +5,13 @@ exports.create_item_from_search_string = function (search_string) {
var operator = Filter.parse(search_string); var operator = Filter.parse(search_string);
var description = Filter.describe(operator); var description = Filter.describe(operator);
return { return {
display_value: description, display_value: search_string,
search_string: search_string, description: description,
}; };
}; };
exports.get_search_string_from_item = function (item) { exports.get_search_string_from_item = function (item) {
return item.search_string; return item.display_value;
}; };
exports.create_pills = function (pill_container) { exports.create_pills = function (pill_container) {
@ -32,7 +32,7 @@ exports.append_search_string = function (search_string, pill_widget) {
exports.get_search_string_for_current_filter = function (pill_widget) { exports.get_search_string_for_current_filter = function (pill_widget) {
var items = pill_widget.items(); var items = pill_widget.items();
var search_strings = _.pluck(items, 'search_string'); var search_strings = _.pluck(items, 'display_value');
return search_strings.join(' '); return search_strings.join(' ');
}; };

View File

@ -8,6 +8,12 @@ exports.initialize = function () {
} }
var container = $('#search_arrows'); var container = $('#search_arrows');
exports.my_pill = search_pill.create_pills(container); exports.my_pill = search_pill.create_pills(container);
exports.my_pill.onPillRemove(function () {
var base_query = search_pill.get_search_string_for_current_filter(exports.my_pill);
var operators = Filter.parse(base_query);
narrow.activate(operators, {trigger: 'search'});
});
}; };
return exports; return exports;

View File

@ -1855,30 +1855,24 @@ nav a .no-style {
} }
#searchbox { #searchbox {
display: flex;
width: 100%; width: 100%;
height: 40px; height: 40px;
border-right: 1px solid #e0e0e0;
.navbar-search { .navbar-search {
margin-top: 0px; margin-top: 0px;
width: auto; width: calc(100% - 66px);
float: none; float: none;
overflow: hidden; overflow: hidden;
border-right: 1px solid hsl(0, 0%, 87.8%);
height: 40px; height: 40px;
} }
.input-append { .input-append {
align-items: center;
display: flex;
position: relative; position: relative;
width: 100%; width: calc(100% - 28px);
.fa-search {
padding: 0px;
font-size: 20px;
position: absolute;
left: 10px;
top: 10px;
z-index: 5;
}
} }
#search_query { #search_query {
@ -1886,7 +1880,7 @@ nav a .no-style {
font-size: 16px; font-size: 16px;
height: 40px; height: 40px;
padding: 0px; padding: 0px;
padding-left: 35px; padding-left: 5px;
padding-right: 20px; padding-right: 20px;
border: none; border: none;
border-radius: 0px; border-radius: 0px;
@ -1895,20 +1889,11 @@ nav a .no-style {
line-height: 40px; line-height: 40px;
} }
#search_query:focus {
box-shadow: inset 0px 0px 0px 2px hsl(204, 20%, 74%);
}
.search_button, .search_button,
.search_button[disabled]:hover { .search_button[disabled]:hover {
position: absolute; position: relative;
right: 2px;
top: 6px;
background: none; background: none;
border-radius: 0px; height: 40px;
border: none;
height: 30px;
text-align: center;
padding: 4px; padding: 4px;
color: hsl(0, 0%, 80%); color: hsl(0, 0%, 80%);
font-size: 18px; font-size: 18px;
@ -1917,6 +1902,7 @@ nav a .no-style {
-moz-box-shadow: none; -moz-box-shadow: none;
text-shadow: none; text-shadow: none;
z-index: 5; z-index: 5;
float: right;
} }
.search_button:hover { .search_button:hover {
@ -1928,11 +1914,19 @@ nav a .no-style {
} }
a.search_icon { a.search_icon {
display: table;
height: 100%;
color: hsl(0, 0%, 80%); color: hsl(0, 0%, 80%);
text-decoration: none; text-decoration: none;
display: block; padding: 0 10px;
width: 1px; font-size: 20px;
height: 1px; z-index: 5;
float: left;
i {
display: table-cell;
vertical-align: middle;
}
} }
a.search_icon:hover { a.search_icon:hover {
@ -1942,7 +1936,28 @@ nav a .no-style {
#search_arrows { #search_arrows {
font-size: 90%; font-size: 90%;
letter-spacing: normal letter-spacing: normal;
border: none;
}
@media (min-width: 500px) {
.pill {
padding: 2px 18px 2px 3px !important;
font-size: 14px;
.exit {
top: 2px !important; /* pill's top padding + border */
}
}
}
@media (max-width: 500px) {
#search_arrows .pill {
line-height: 20px;
.exit {
top: 0;
}
}
} }
} }

View File

@ -48,15 +48,15 @@
<div id="searchbox" class="searchbox-rightmargin"> <div id="searchbox" class="searchbox-rightmargin">
<div id="tab_bar" class="notdisplayed"> <div id="tab_bar" class="notdisplayed">
</div> </div>
<a class="search_icon" href="#search-operators" data-overlay-trigger="search-operators" title="{{ _('Search help') }}"><i class="fa fa-search" aria-hidden="true"></i></a>
<form id="searchbox_form" class="form-search navbar-search"> <form id="searchbox_form" class="form-search navbar-search">
<div id="search_arrows" class="pill-container input-append"> <div id="search_arrows" class="pill-container input-append">
<div contenteditable="true" class="input search-query input-block-level" id="search_query" type="text" placeholder="{{ _('Search') }}" <div contenteditable="true" class="input search-query input-block-level" id="search_query" type="text" placeholder="{{ _('Search') }}"
autocomplete="off" aria-label="{{ _('Search') }}" title="{{ _('Search') }} (/)"> </div> autocomplete="off" aria-label="{{ _('Search') }}" title="{{ _('Search') }} (/)"> </div>
{# Start the button off disabled since there is no active search #}
<button class="btn search_button" type="button" id="search_exit" disabled="disabled" aria-label="{{ _('Exit search') }}"><i class="fa fa-remove" aria-hidden="true"></i></button>
<a class="search_icon" href="#search-operators" data-overlay-trigger="search-operators" title="{{ _('Search help') }}"><i class="fa fa-search" aria-hidden="true"></i></a>
</div> </div>
</form> </form>
{# Start the button off disabled since there is no active search #}
<button class="btn search_button" type="button" id="search_exit" disabled="disabled" aria-label="{{ _('Exit search') }}"><i class="fa fa-remove" aria-hidden="true"></i></button>
</div> </div>
{% else %} {% else %}
<div id="searchbox_legacy" class="searchbox-rightmargin"> <div id="searchbox_legacy" class="searchbox-rightmargin">