// Exported for unit testing exports.is_using_input_method = false; exports.narrow_or_search_for_term = function (search_string) { const search_query_box = $("#search_query"); if (exports.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(); } ui_util.change_tab_to('#home'); 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.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); } } exports.update_button_visibility = function () { update_buttons_with_focus($('#search_query').is(':focus')); }; exports.initialize = function () { 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" 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: function (query) { let base_query = ''; if (page_params.search_pills_enabled) { base_query = search_pill.get_search_string_for_current_filter( search_pill_widget.widget); } 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: function (item) { const obj = search_map.get(item); return obj.description; }, matcher: function () { return true; }, updater: function (search_string) { if (page_params.search_pills_enabled) { search_pill.append_search_string(search_string, search_pill_widget.widget); if (search_query_box.is(':focus')) { // We usually allow the user to continue // typing until the enter key is pressed. // But we narrow when the user clicks on a // typeahead suggestion. This change in behaviour // is a workaround to be able to display the // navbar every time search_query_box loses focus. return search_query_box.val(); } } return exports.narrow_or_search_for_term(search_string); }, sorter: function (items) { return items; }, stopAdvance: page_params.search_pills_enabled, advanceKeyCodes: [8], on_move: function () { if (page_params.search_pills_enabled) { ui_util.place_caret_at_end(search_query_box[0]); return true; } }, // Use our custom typeahead `on_escape` hook to exit // the search bar as soon as the user hits Esc. on_escape: tab_bar.exit_search, }); searchbox_form.on('compositionend', () => { // Set `is_using_input_method` to true if enter is pressed to exit // the input tool popover and get the text in the search bar. Then // we suppress searching triggered by this enter key by checking // `is_using_input_method` before searching. // More details in the commit message that added this line. exports.is_using_input_method = true; }); searchbox_form.keydown((e) => { exports.update_button_visibility(); const code = e.which; 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. return false; } }).keyup((e) => { if (exports.is_using_input_method) { exports.is_using_input_method = false; return; } const code = e.which; 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) // Pill is already added during keydown event of input pills. exports.narrow_or_search_for_term(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_query_box.on('focus', exports.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 // 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 300ms 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 pill_id = $(e.relatedTarget).closest(".pill").data('id'); const search_pill = search_pill_widget.widget.getByID(pill_id); 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(() => { exports.update_button_visibility(); tab_bar.close_search_bar_and_open_narrow_description(); searchbox.css({"box-shadow": "unset"}); }, 300); }); }; exports.focus_search = function () { // The search bar is not focused yet, but will be. update_buttons_with_focus(true); }; exports.initiate_search = function () { tab_bar.open_search_bar_and_close_narrow_description(); $('#searchbox').css({"box-shadow": "inset 0px 0px 0px 2px hsl(204, 20%, 74%)"}); $('#search_query').typeahead('lookup').select(); if (page_params.search_pills_enabled) { $('#search_query').focus(); ui_util.place_caret_at_end($('#search_query')[0]); } }; exports.clear_search_form = function () { $('#search_query').val(''); $('#search_query').blur(); $('.search_button').prop('disabled', true); }; window.search = exports;