zulip/static/js/search.js

236 lines
9.1 KiB
JavaScript
Raw Normal View History

import $ from "jquery";
import render_search_list_item from "../templates/search_list_item.hbs";
import {Filter} from "./filter";
import * as keydown_util from "./keydown_util";
import * as message_view_header from "./message_view_header";
import * as narrow from "./narrow";
import * as narrow_state from "./narrow_state";
import {page_params} from "./page_params";
import * as search_pill from "./search_pill";
import * as search_pill_widget from "./search_pill_widget";
import * as search_suggestion from "./search_suggestion";
import * as ui_util from "./ui_util";
// Exported for unit testing
export let is_using_input_method = false;
search: Fix "Enter to search" while using an input method. NOTE: To test this locally I've used Google Chrome input tool. This change will not affect users who don't use input tools. Here is the algorithm used to deal with this case and other important points: * Here I've used `compositionend` event which is triggered as soon as an input tool completes a word or user press "enter" to get the suggested text. (There was a situation where it is triggered even when input tool wasn't closed, that is when we press space, but it also triggers another `compositionstart` event simultaneously so our logic can't be affected by this.) * We are using a variable `is_using_input_method` which sets to `true` when `compositionend` event is triggered. * Basically our searching is initiated by `keyup` event which is triggered by the same keypress which triggers `compositionend` event to get the text, so our main goal is to suppress the searching triggered by this key pressing. * Observation shows that `compositionend` is triggered before the `keyup` and calling of callback `narrow_or_search_for_term` used by typeahead. i.e. chronological order of triggering of this event is `compositionend` > calling of `narrow_or_search_for_term` > `keyup`. * So the main logic is to set `is_using_input_method` to `false` by default and if used the input tool then when we press enter to get the suggested text we set it to `true` which indicate further events triggered after it to skip the searching and finally in `keyup` we set it to default `false` so when pressed enter again we have it set to false and we have a successful search. Fixes: #9396.
2018-06-09 11:10:56 +02:00
export function narrow_or_search_for_term(search_string) {
const $search_query_box = $("#search_query");
if (is_using_input_method) {
// Neither narrow nor search when using input tools as
// `updater` is also triggered when 'enter' is triggered
// while using input tool
return $search_query_box.val();
search: Fix "Enter to search" while using an input method. NOTE: To test this locally I've used Google Chrome input tool. This change will not affect users who don't use input tools. Here is the algorithm used to deal with this case and other important points: * Here I've used `compositionend` event which is triggered as soon as an input tool completes a word or user press "enter" to get the suggested text. (There was a situation where it is triggered even when input tool wasn't closed, that is when we press space, but it also triggers another `compositionstart` event simultaneously so our logic can't be affected by this.) * We are using a variable `is_using_input_method` which sets to `true` when `compositionend` event is triggered. * Basically our searching is initiated by `keyup` event which is triggered by the same keypress which triggers `compositionend` event to get the text, so our main goal is to suppress the searching triggered by this key pressing. * Observation shows that `compositionend` is triggered before the `keyup` and calling of callback `narrow_or_search_for_term` used by typeahead. i.e. chronological order of triggering of this event is `compositionend` > calling of `narrow_or_search_for_term` > `keyup`. * So the main logic is to set `is_using_input_method` to `false` by default and if used the input tool then when we press enter to get the suggested text we set it to `true` which indicate further events triggered after it to skip the searching and finally in `keyup` we set it to default `false` so when pressed enter again we have it set to false and we have a successful search. Fixes: #9396.
2018-06-09 11:10:56 +02:00
}
let operators;
if (page_params.search_pills_enabled) {
// We have to take care to append the new pill before calling this
// function, so that the base_query includes the suggestion selected
// along with query corresponding to the existing pills.
const base_query = search_pill.get_search_string_for_current_filter(
search_pill_widget.widget,
);
operators = Filter.parse(base_query);
} else {
operators = Filter.parse(search_string);
}
narrow.activate(operators, {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.
if (!page_params.search_pills_enabled) {
$search_query_box.trigger("blur");
}
return $search_query_box.val();
}
function update_buttons_with_focus(focused) {
const $search_query_box = $("#search_query");
// Show buttons iff the search input is focused, or has non-empty contents,
// or we are narrowed.
if (focused || $search_query_box.val() || narrow_state.active()) {
$(".search_button").prop("disabled", false);
}
}
export function update_button_visibility() {
update_buttons_with_focus($("#search_query").is(":focus"));
}
export function initialize() {
const $search_query_box = $("#search_query");
const $searchbox_form = $("#searchbox_form");
const $searchbox = $("#searchbox");
// Data storage for the typeahead.
// This maps a search string to an object with a "description_html" field.
// (It's a bit of legacy that we have an object with only one important
// field. There's also a "search_string" field on each element that actually
// just represents the key of the hash, so it's redundant.)
let search_map = new Map();
$search_query_box.typeahead({
source(query) {
let base_query = "";
if (page_params.search_pills_enabled) {
search suggestions: De-duplicate legacy search codepath. This merges the `exports.get_search_result_legacy` and `exports.get_search_result` function. The key differences between the two code paths are as follows: * We only want to generate suggestions for the queries which the user is typing or can edit. For the legacy version, suggestions are displayed for the entire search string in the searchbox. (`all_operators`) For the pills enabled version, suggestions are displayed only for the input which hasn't been converted to pills. (`query_operators`) `all_operators` = `base_query_operators` + " " + `query_operators`. trim is added at the end just to handle the legacy case where we pass the `base_query` as ''. * It is not possible to detect whether the user wants to continue typing in the legacy version. However if the the searchbox is still focused even after pill creation we can assume the user still wants to continue typing. To handle this we push an empty term as the `last` operator. This is possible since the previous queries have been completely entered as evident from it's generated pill. * When using the legacy version, `search_operators` are the same as `all_operators`, as mentioned in point 1. In the pills enabled version we perform most of the computations from the `query_operators`, but we do require all `all_operators`, only for filtering the last query's suggestion. * And there is just one block unique to the legacy search system. More details are mentioned in the comments of that block. We also refactor both the search suggestions node tests, mainly to make them similar and easier to detect differences when we switch over to the new version.
2020-06-01 15:00:42 +02:00
base_query = search_pill.get_search_string_for_current_filter(
search_pill_widget.widget,
);
}
search suggestions: De-duplicate legacy search codepath. This merges the `exports.get_search_result_legacy` and `exports.get_search_result` function. The key differences between the two code paths are as follows: * We only want to generate suggestions for the queries which the user is typing or can edit. For the legacy version, suggestions are displayed for the entire search string in the searchbox. (`all_operators`) For the pills enabled version, suggestions are displayed only for the input which hasn't been converted to pills. (`query_operators`) `all_operators` = `base_query_operators` + " " + `query_operators`. trim is added at the end just to handle the legacy case where we pass the `base_query` as ''. * It is not possible to detect whether the user wants to continue typing in the legacy version. However if the the searchbox is still focused even after pill creation we can assume the user still wants to continue typing. To handle this we push an empty term as the `last` operator. This is possible since the previous queries have been completely entered as evident from it's generated pill. * When using the legacy version, `search_operators` are the same as `all_operators`, as mentioned in point 1. In the pills enabled version we perform most of the computations from the `query_operators`, but we do require all `all_operators`, only for filtering the last query's suggestion. * And there is just one block unique to the legacy search system. More details are mentioned in the comments of that block. We also refactor both the search suggestions node tests, mainly to make them similar and easier to detect differences when we switch over to the new version.
2020-06-01 15:00:42 +02:00
const suggestions = search_suggestion.get_suggestions(base_query, query);
// Update our global search_map hash
search_map = suggestions.lookup_table;
return suggestions.strings;
},
fixed: true,
items: search_suggestion.max_num_of_search_results,
helpOnEmptyStrings: true,
naturalSearch: true,
highlighter(item) {
const obj = search_map.get(item);
return render_search_list_item(obj);
},
matcher() {
return true;
},
updater(search_string) {
if (page_params.search_pills_enabled) {
search_pill.append_search_string(search_string, search_pill_widget.widget);
return $search_query_box.val();
}
return narrow_or_search_for_term(search_string);
},
sorter(items) {
return items;
},
stopAdvance: page_params.search_pills_enabled,
advanceKeyCodes: [8],
on_move() {
if (page_params.search_pills_enabled) {
ui_util.place_caret_at_end($search_query_box[0]);
}
},
// Use our custom typeahead `on_escape` hook to exit
// the search bar as soon as the user hits Esc.
on_escape: message_view_header.exit_search,
});
$searchbox_form.on("compositionend", () => {
// Set `is_using_input_method` to true if Enter is pressed to exit
search: Fix "Enter to search" while using an input method. NOTE: To test this locally I've used Google Chrome input tool. This change will not affect users who don't use input tools. Here is the algorithm used to deal with this case and other important points: * Here I've used `compositionend` event which is triggered as soon as an input tool completes a word or user press "enter" to get the suggested text. (There was a situation where it is triggered even when input tool wasn't closed, that is when we press space, but it also triggers another `compositionstart` event simultaneously so our logic can't be affected by this.) * We are using a variable `is_using_input_method` which sets to `true` when `compositionend` event is triggered. * Basically our searching is initiated by `keyup` event which is triggered by the same keypress which triggers `compositionend` event to get the text, so our main goal is to suppress the searching triggered by this key pressing. * Observation shows that `compositionend` is triggered before the `keyup` and calling of callback `narrow_or_search_for_term` used by typeahead. i.e. chronological order of triggering of this event is `compositionend` > calling of `narrow_or_search_for_term` > `keyup`. * So the main logic is to set `is_using_input_method` to `false` by default and if used the input tool then when we press enter to get the suggested text we set it to `true` which indicate further events triggered after it to skip the searching and finally in `keyup` we set it to default `false` so when pressed enter again we have it set to false and we have a successful search. Fixes: #9396.
2018-06-09 11:10:56 +02:00
// the input tool popover and get the text in the search bar. Then
// we suppress searching triggered by this Enter key by checking
search: Fix "Enter to search" while using an input method. NOTE: To test this locally I've used Google Chrome input tool. This change will not affect users who don't use input tools. Here is the algorithm used to deal with this case and other important points: * Here I've used `compositionend` event which is triggered as soon as an input tool completes a word or user press "enter" to get the suggested text. (There was a situation where it is triggered even when input tool wasn't closed, that is when we press space, but it also triggers another `compositionstart` event simultaneously so our logic can't be affected by this.) * We are using a variable `is_using_input_method` which sets to `true` when `compositionend` event is triggered. * Basically our searching is initiated by `keyup` event which is triggered by the same keypress which triggers `compositionend` event to get the text, so our main goal is to suppress the searching triggered by this key pressing. * Observation shows that `compositionend` is triggered before the `keyup` and calling of callback `narrow_or_search_for_term` used by typeahead. i.e. chronological order of triggering of this event is `compositionend` > calling of `narrow_or_search_for_term` > `keyup`. * So the main logic is to set `is_using_input_method` to `false` by default and if used the input tool then when we press enter to get the suggested text we set it to `true` which indicate further events triggered after it to skip the searching and finally in `keyup` we set it to default `false` so when pressed enter again we have it set to false and we have a successful search. Fixes: #9396.
2018-06-09 11:10:56 +02:00
// `is_using_input_method` before searching.
// More details in the commit message that added this line.
is_using_input_method = true;
search: Fix "Enter to search" while using an input method. NOTE: To test this locally I've used Google Chrome input tool. This change will not affect users who don't use input tools. Here is the algorithm used to deal with this case and other important points: * Here I've used `compositionend` event which is triggered as soon as an input tool completes a word or user press "enter" to get the suggested text. (There was a situation where it is triggered even when input tool wasn't closed, that is when we press space, but it also triggers another `compositionstart` event simultaneously so our logic can't be affected by this.) * We are using a variable `is_using_input_method` which sets to `true` when `compositionend` event is triggered. * Basically our searching is initiated by `keyup` event which is triggered by the same keypress which triggers `compositionend` event to get the text, so our main goal is to suppress the searching triggered by this key pressing. * Observation shows that `compositionend` is triggered before the `keyup` and calling of callback `narrow_or_search_for_term` used by typeahead. i.e. chronological order of triggering of this event is `compositionend` > calling of `narrow_or_search_for_term` > `keyup`. * So the main logic is to set `is_using_input_method` to `false` by default and if used the input tool then when we press enter to get the suggested text we set it to `true` which indicate further events triggered after it to skip the searching and finally in `keyup` we set it to default `false` so when pressed enter again we have it set to false and we have a successful search. Fixes: #9396.
2018-06-09 11:10:56 +02:00
});
$searchbox_form
.on("keydown", (e) => {
update_button_visibility();
if (keydown_util.is_enter_event(e) && $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();
}
})
.on("keyup", (e) => {
if (is_using_input_method) {
is_using_input_method = false;
return;
}
if (keydown_util.is_enter_event(e) && $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)
// Pill is already added during keydown event of input pills.
narrow_or_search_for_term($search_query_box.val());
$search_query_box.trigger("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_query_box.on("focus", focus_search);
$search_query_box.on("blur", (e) => {
// The search query box is a visual cue as to
// whether search or narrowing is active. If
// the user blurs the search box, then we should
2018-06-08 21:14:55 +02:00
// update the search string to reflect the current
// narrow (or lack of narrow).
//
// But we can't do this right away, because
// selecting something in the typeahead menu causes
// the box to lose focus a moment before.
//
// 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).
if (page_params.search_pills_enabled) {
const $element = $(e.relatedTarget).closest(".pill");
const search_pill = search_pill_widget.widget.getByElement($element[0]);
if (search_pill) {
// The searchbox loses focus while the search
// pill element gains focus.
// We do not consider the searchbox to actually
// lose focus when a pill inside it gets selected
// or deleted by a click.
return;
}
}
setTimeout(() => {
update_button_visibility();
}, 100);
});
if (page_params.search_pills_enabled) {
// Uses jquery instead of pure css as the `:focus` event occurs on `#search_query`,
// while we want to add box-shadow to `#searchbox`. This could have been done
// with `:focus-within` CSS selector, but it is not supported in IE or Opera.
$searchbox.on("focusout", () => {
message_view_header.close_search_bar_and_open_narrow_description();
$searchbox.css({"box-shadow": "unset"});
});
}
}
export function focus_search() {
// The search bar is not focused yet, but will be.
update_buttons_with_focus(true);
}
export function initiate_search() {
message_view_header.open_search_bar_and_close_narrow_description();
$("#searchbox").css({"box-shadow": "inset 0px 0px 0px 2px hsl(204, 20%, 74%)"});
$("#search_query").typeahead("lookup").trigger("select");
if (page_params.search_pills_enabled) {
$("#search_query").trigger("focus");
ui_util.place_caret_at_end($("#search_query")[0]);
}
}
export function clear_search_form() {
$("#search_query").val("");
$("#search_query").trigger("blur");
$(".search_button").prop("disabled", true);
}