mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 14:03:30 +00:00 
			
		
		
		
	Previously, if you pressed the escape key with various modals open (keyboard shortcuts, markdown help, etc.), the modals would close but also the compose box would close and the user would be unnarrowed. This changes makes it so all that happens is the modal closes. Fixes #3472.
		
			
				
	
	
		
			432 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			432 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
var hotkeys = (function () {
 | 
						|
 | 
						|
var exports = {};
 | 
						|
 | 
						|
function do_narrow_action(action) {
 | 
						|
    action(current_msg_list.selected_id(), {trigger: 'hotkey'});
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
var actions_dropdown_hotkeys = [
 | 
						|
    'down_arrow',
 | 
						|
    'up_arrow',
 | 
						|
    'vim_up',
 | 
						|
    'vim_down',
 | 
						|
    'enter',
 | 
						|
];
 | 
						|
 | 
						|
// Note that multiple keys can map to the same event_name, which
 | 
						|
// we'll do in cases where they have the exact same semantics.
 | 
						|
// DON'T FORGET: update keyboard_shortcuts.html
 | 
						|
 | 
						|
var hotkeys_shift = {
 | 
						|
    // these can be triggered by shift + key only
 | 
						|
    9: {name: 'shift_tab', message_view_only: false}, // tab
 | 
						|
    32: {name: 'page_up', message_view_only: true},  // space bar
 | 
						|
};
 | 
						|
var hotkeys_no_modifiers = {
 | 
						|
    // these can be triggered by key only (without shift)
 | 
						|
    9: {name: 'tab', message_view_only: false}, // tab
 | 
						|
    32: {name: 'page_down', message_view_only: true}, // space bar
 | 
						|
    33: {name: 'page_up', message_view_only: true}, // page up
 | 
						|
    34: {name: 'page_down', message_view_only: true}, // page down
 | 
						|
    35: {name: 'end', message_view_only: true}, // end
 | 
						|
    36: {name: 'home', message_view_only: true}, // home
 | 
						|
    38: {name: 'up_arrow', message_view_only: true}, // up arrow
 | 
						|
    40: {name: 'down_arrow', message_view_only: true}, // down arrow
 | 
						|
};
 | 
						|
var hotkeys_shift_insensitive = {
 | 
						|
    // these can be triggered by key or shift + key
 | 
						|
    // Note that codes for letters are still case sensitive!
 | 
						|
    8: {name: 'backspace', message_view_only: true}, // backspace
 | 
						|
    13: {name: 'enter', message_view_only: false}, // enter
 | 
						|
    27: {name: 'escape', message_view_only: false}, // escape
 | 
						|
    47: {name: 'search', message_view_only: false}, // '/'
 | 
						|
    63: {name: 'show_shortcuts', message_view_only: false}, // '?'
 | 
						|
    64: {name: 'compose_reply_with_mention', message_view_only: true}, // '@'
 | 
						|
    65: {name: 'stream_cycle_backward', message_view_only: true}, // 'A'
 | 
						|
    67: {name: 'compose_private_message', message_view_only: true}, // 'C'
 | 
						|
    68: {name: 'stream_cycle_forward', message_view_only: true}, // 'D'
 | 
						|
    74: {name: 'page_down', message_view_only: true}, // 'J'
 | 
						|
    75: {name: 'page_up', message_view_only: true}, // 'K'
 | 
						|
    82: {name: 'respond_to_author', message_view_only: true}, // 'R'
 | 
						|
    83: {name: 'narrow_by_subject', message_view_only: true}, //'S'
 | 
						|
    99: {name: 'compose', message_view_only: true}, // 'c'
 | 
						|
    105: {name: 'message_actions', message_view_only: true}, // 'i'
 | 
						|
    106: {name: 'vim_down', message_view_only: true}, // 'j'
 | 
						|
    107: {name: 'vim_up', message_view_only: true}, // 'k'
 | 
						|
    113: {name: 'query_users', message_view_only: false}, // 'q'
 | 
						|
    114: {name: 'reply_message', message_view_only: true}, // 'r'
 | 
						|
    115: {name: 'narrow_by_recipient', message_view_only: true}, // 's'
 | 
						|
    118: {name: 'narrow_private', message_view_only: true}, // 'v'
 | 
						|
    119: {name: 'query_streams', message_view_only: false}, // 'w'
 | 
						|
};
 | 
						|
 | 
						|
var tab_up_down = (function () {
 | 
						|
    var list = ["#group-pm-list", "#stream_filters", "#global_filters", "#user_presences"];
 | 
						|
 | 
						|
    return function (e) {
 | 
						|
        var $target = $(e.target);
 | 
						|
        var flag = $target.closest(list.join(", ")).length > 0;
 | 
						|
 | 
						|
        return {
 | 
						|
            flag: flag,
 | 
						|
            next: function () {
 | 
						|
                return $target.closest("li").next().find("a");
 | 
						|
            },
 | 
						|
            prev: function () {
 | 
						|
                return $target.closest("li").prev().find("a");
 | 
						|
            },
 | 
						|
        };
 | 
						|
    };
 | 
						|
}());
 | 
						|
 | 
						|
function get_hotkey_from_event(e) {
 | 
						|
 | 
						|
    // We're in the middle of a combo; stop processing because
 | 
						|
    // we want the browser to handle it (to avoid breaking
 | 
						|
    // things like Ctrl-C or Command-C for copy).
 | 
						|
    if (e.metaKey || e.ctrlKey) {
 | 
						|
        return {name: 'ignore', message_view_only: false};
 | 
						|
    }
 | 
						|
 | 
						|
    if (e.shiftKey && hotkeys_shift[e.which] !== undefined) {
 | 
						|
        return hotkeys_shift[e.which];
 | 
						|
    } else if (!e.shiftKey && hotkeys_no_modifiers[e.which] !== undefined) {
 | 
						|
        return hotkeys_no_modifiers[e.which];
 | 
						|
    } else if (hotkeys_shift_insensitive[e.which] !== undefined) {
 | 
						|
        return hotkeys_shift_insensitive[e.which];
 | 
						|
    }
 | 
						|
 | 
						|
    return {name: 'ignore', message_view_only: false};
 | 
						|
}
 | 
						|
 | 
						|
// Process a keydown or keypress event.
 | 
						|
//
 | 
						|
// Returns true if we handled it, false if the browser should.
 | 
						|
function process_hotkey(e) {
 | 
						|
    var row;
 | 
						|
    var alert_words_content;
 | 
						|
    var focused_message_edit_content;
 | 
						|
    var focused_message_edit_save;
 | 
						|
    var message_edit_form;
 | 
						|
    var hotkey = get_hotkey_from_event(e);
 | 
						|
    var event_name = hotkey.name;
 | 
						|
    activity.new_user_input = true;
 | 
						|
 | 
						|
    if (ui.home_tab_obscured() && hotkey.message_view_only) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if ($(e.target).is(".editable-section")) {
 | 
						|
        if (event_name === "enter") {
 | 
						|
            $(e.target).parent().find(".checkmark").click();
 | 
						|
        }
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    var tab_list = tab_up_down(e);
 | 
						|
    if (tab_list.flag) {
 | 
						|
        if (hotkey.name === "up_arrow") {
 | 
						|
            tab_list.prev().focus();
 | 
						|
            return true;
 | 
						|
        } else if (hotkey.name === "down_arrow") {
 | 
						|
            tab_list.next().focus();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (event_name === 'ignore') {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    if (popovers.actions_popped() && actions_dropdown_hotkeys.indexOf(event_name) !== -1) {
 | 
						|
        popovers.actions_menu_handle_keyboard(event_name);
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // Handle a few keys specially when the send button is focused.
 | 
						|
    if ($('#compose-send-button').is(':focus')) {
 | 
						|
        if (event_name === 'backspace') {
 | 
						|
            // Ignore backspace; don't navigate back a page.
 | 
						|
            return true;
 | 
						|
        } else if (event_name === 'shift_tab') {
 | 
						|
            // Shift-Tab: go back to content textarea and restore
 | 
						|
            // cursor position.
 | 
						|
            ui.restore_compose_cursor();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // In Safari and the desktop app, we can't tab to buttons. Intercept the
 | 
						|
    // tab from the message edit content box to Save and then Cancel.
 | 
						|
    if (event_name === "tab") {
 | 
						|
        alert_words_content = $(".edit-alert-word").filter(":focus");
 | 
						|
        if (alert_words_content.length > 0) {
 | 
						|
            var add_word_li = alert_words_content.closest(".alert-word-item");
 | 
						|
            add_word_li.find(".add-alert-word").focus();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        focused_message_edit_content = $(".message_edit_content").filter(":focus");
 | 
						|
        if (focused_message_edit_content.length > 0) {
 | 
						|
            message_edit_form = focused_message_edit_content.closest(".message_edit_form");
 | 
						|
            message_edit_form.find(".message_edit_save").focus();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        focused_message_edit_save = $(".message_edit_save").filter(":focus");
 | 
						|
        if (focused_message_edit_save.length > 0) {
 | 
						|
            message_edit_form = focused_message_edit_save.closest(".message_edit_form");
 | 
						|
            message_edit_form.find(".message_edit_cancel").focus();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    if (event_name === "shift_tab") {
 | 
						|
        // Shift-tabbing from the edit message cancel button takes you to save.
 | 
						|
        if ($(".message_edit_cancel").filter(":focus").length > 0) {
 | 
						|
            $(".message_edit_save").focus();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
 | 
						|
        // Shift-tabbing from the edit message save button takes you to the content.
 | 
						|
        focused_message_edit_save = $(".message_edit_save").filter(":focus");
 | 
						|
        if (focused_message_edit_save.length > 0) {
 | 
						|
            focused_message_edit_save.closest(".message_edit_form")
 | 
						|
                                     .find(".message_edit_content").focus();
 | 
						|
            return true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (event_name === "escape") {
 | 
						|
        if ($("#overlay").hasClass("show")) {
 | 
						|
            ui.exit_lightbox_photo();
 | 
						|
            return true;
 | 
						|
        } else if ($("#subscription_overlay").is(":visible")) {
 | 
						|
            subs.close();
 | 
						|
            return true;
 | 
						|
        } else if ($('#markdown-help').hasClass('in') ||
 | 
						|
            $('#keyboard-shortcuts').hasClass('in') ||
 | 
						|
            $('#search-operators').hasClass('in') ||
 | 
						|
            $('#invite-user').hasClass('in')) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // Process hotkeys specially when in an input, select, textarea, or send button
 | 
						|
    if ($('input:focus,select:focus,textarea:focus,#compose-send-button:focus').length > 0) {
 | 
						|
        if (event_name === 'escape') {
 | 
						|
            // emoji window should trap escape before it is able to close the compose box
 | 
						|
            if ($('.emoji_popover').css('display') === 'inline-block') {
 | 
						|
                popovers.hide_emoji_map_popover();
 | 
						|
                return;
 | 
						|
            }
 | 
						|
            // If one of our typeaheads is open, do nothing so that the Esc
 | 
						|
            // will go to close it
 | 
						|
            if ($("#subject").data().typeahead.shown ||
 | 
						|
                $("#stream").data().typeahead.shown ||
 | 
						|
                $("#private_message_recipient").data().typeahead.shown ||
 | 
						|
                $("#new_message_content").data().typeahead.shown ||
 | 
						|
                $("#search_query").data().typeahead.shown) {
 | 
						|
                // For some reason this code is only needed in Firefox;
 | 
						|
                // in Chrome our typeahead is able to intercept the Esc
 | 
						|
                // event before we even get it.
 | 
						|
                // Regardless, we do nothing in this case.
 | 
						|
                return true;
 | 
						|
            } else if ($(".message_edit_content").filter(":focus").length > 0) {
 | 
						|
                row = $(".message_edit_content").filter(":focus").closest(".message_row");
 | 
						|
                message_edit.end(row);
 | 
						|
            } else if (activity.searching()) {
 | 
						|
                activity.escape_search();
 | 
						|
                return true;
 | 
						|
            } else if (stream_list.searching()) {
 | 
						|
                stream_list.escape_search();
 | 
						|
                return true;
 | 
						|
            } else if (compose.composing()) {
 | 
						|
                // If the user hit the escape key, cancel the current compose
 | 
						|
                compose.cancel();
 | 
						|
                return true;
 | 
						|
            } else {
 | 
						|
                // We pressed Esc and something was focused, and the composebox
 | 
						|
                // wasn't open. In that case, we should blur the input.
 | 
						|
                // (this is almost certainly the searchbar)
 | 
						|
                $("input:focus,textarea:focus").blur();
 | 
						|
                return true;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (event_name === 'enter') {
 | 
						|
            if (activity.searching()) {
 | 
						|
                activity.blur_search();
 | 
						|
                return true;
 | 
						|
            } else if (stream_list.searching()) {
 | 
						|
                stream_list.clear_and_hide_search();
 | 
						|
                return true;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if ((event_name === 'up_arrow' || event_name === 'down_arrow')
 | 
						|
            && compose.composing()
 | 
						|
            && compose.message_content() === ""
 | 
						|
            && $('#new_message_content').is(':focus')) {
 | 
						|
                compose.cancel();
 | 
						|
                // don't return, as we still want it to be picked up by the code below
 | 
						|
        } else {
 | 
						|
            // Let the browser handle the key normally.
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    // If we're on a button or a link and have pressed enter, let the
 | 
						|
    // browser handle the keypress
 | 
						|
    //
 | 
						|
    // This is subtle and here's why: Suppose you have the focus on a
 | 
						|
    // stream name in your left sidebar. j and k will still move your
 | 
						|
    // cursor up and down, but Enter won't reply -- it'll just trigger
 | 
						|
    // the link on the sidebar! So you keep pressing enter over and
 | 
						|
    // over again. Until you click somewhere or press r.
 | 
						|
    if ($('a:focus,button:focus').length > 0 && event_name === 'enter') {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Shortcuts that don't require a message
 | 
						|
    switch (event_name) {
 | 
						|
        case 'narrow_private':
 | 
						|
            return do_narrow_action(function (target, opts) {
 | 
						|
                narrow.by('is', 'private', opts);
 | 
						|
            });
 | 
						|
        case 'escape': // Esc: close actions popup, cancel compose, clear a find, or un-narrow
 | 
						|
            if ($('.emoji_popover').css('display') === 'inline-block') {
 | 
						|
                popovers.hide_emoji_map_popover();
 | 
						|
            } else if (popovers.any_active()) {
 | 
						|
                popovers.hide_all();
 | 
						|
            } else if (compose.composing()) {
 | 
						|
                compose.cancel();
 | 
						|
            } else {
 | 
						|
                search.clear_search();
 | 
						|
            }
 | 
						|
            return true;
 | 
						|
        case 'compose': // 'c': compose
 | 
						|
            compose.start('stream', {trigger: "compose_hotkey"});
 | 
						|
            return true;
 | 
						|
        case 'compose_private_message':
 | 
						|
            compose.start('private', {trigger: "compose_hotkey"});
 | 
						|
            return true;
 | 
						|
        case 'query_users':
 | 
						|
            activity.initiate_search();
 | 
						|
            return true;
 | 
						|
        case 'query_streams':
 | 
						|
            stream_list.initiate_search();
 | 
						|
            return true;
 | 
						|
        case 'search':
 | 
						|
            search.initiate_search();
 | 
						|
            return true;
 | 
						|
        case 'show_shortcuts': // Show keyboard shortcuts page
 | 
						|
            $('#keyboard-shortcuts').modal('show');
 | 
						|
            return true;
 | 
						|
        case 'stream_cycle_backward':
 | 
						|
            navigate.cycle_stream('backward');
 | 
						|
            return true;
 | 
						|
        case 'stream_cycle_forward':
 | 
						|
            navigate.cycle_stream('forward');
 | 
						|
            return true;
 | 
						|
    }
 | 
						|
 | 
						|
    if (current_msg_list.empty()) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Navigation shortcuts
 | 
						|
    switch (event_name) {
 | 
						|
        case 'down_arrow':
 | 
						|
        case 'vim_down':
 | 
						|
            navigate.down(true); // with_centering
 | 
						|
            return true;
 | 
						|
        case 'up_arrow':
 | 
						|
        case 'vim_up':
 | 
						|
            navigate.up();
 | 
						|
            return true;
 | 
						|
        case 'home':
 | 
						|
            navigate.to_home();
 | 
						|
            return true;
 | 
						|
        case 'end':
 | 
						|
            navigate.to_end();
 | 
						|
            return true;
 | 
						|
        case 'page_up':
 | 
						|
            navigate.page_up();
 | 
						|
            return true;
 | 
						|
        case 'page_down':
 | 
						|
            navigate.page_down();
 | 
						|
            return true;
 | 
						|
    }
 | 
						|
 | 
						|
    // Shortcuts that operate on a message
 | 
						|
    switch (event_name) {
 | 
						|
        case 'message_actions':
 | 
						|
            return popovers.open_message_menu();
 | 
						|
        case 'narrow_by_recipient':
 | 
						|
            return do_narrow_action(narrow.by_recipient);
 | 
						|
        case 'narrow_by_subject':
 | 
						|
            return do_narrow_action(narrow.by_subject);
 | 
						|
        case  'enter': // Enter: respond to message (unless we need to do something else)
 | 
						|
            compose.respond_to_message({trigger: 'hotkey enter'});
 | 
						|
            return true;
 | 
						|
        case 'reply_message': // 'r': respond to message
 | 
						|
            compose.respond_to_message({trigger: 'hotkey'});
 | 
						|
            return true;
 | 
						|
        case 'respond_to_author': // 'R': respond to author
 | 
						|
            compose.respond_to_message({reply_type: "personal", trigger: 'hotkey pm'});
 | 
						|
            return true;
 | 
						|
        case 'compose_reply_with_mention': // '@': respond to message with mention to author
 | 
						|
            compose.respond_to_message({trigger: 'hotkey'});
 | 
						|
            var message = current_msg_list.selected_message();
 | 
						|
            $("#new_message_content").val('@**' + message.sender_full_name + '** ');
 | 
						|
            return true;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
}
 | 
						|
 | 
						|
/* We register both a keydown and a keypress function because
 | 
						|
   we want to intercept pgup/pgdn, escape, etc, and process them
 | 
						|
   as they happen on the keyboard. However, if we processed
 | 
						|
   letters/numbers in keydown, we wouldn't know what the case of
 | 
						|
   the letters were.
 | 
						|
 | 
						|
   We want case-sensitive hotkeys (such as in the case of r vs R)
 | 
						|
   so we bail in .keydown if the event is a letter or number and
 | 
						|
   instead just let keypress go for it. */
 | 
						|
 | 
						|
$(document).keydown(function (e) {
 | 
						|
    // Restrict to non-alphanumeric keys
 | 
						|
    // check if 27 (esc) because it doesn't register under .keypress()
 | 
						|
    if (e.which < 48 || e.which > 90 || e.which === 27) {
 | 
						|
        if (process_hotkey(e)) {
 | 
						|
            e.preventDefault();
 | 
						|
        }
 | 
						|
    }
 | 
						|
    resize.resize_bottom_whitespace();
 | 
						|
});
 | 
						|
 | 
						|
$(document).keypress(function (e) {
 | 
						|
    // What exactly triggers .keypress may vary by browser.
 | 
						|
    // Welcome to compatability hell.
 | 
						|
    //
 | 
						|
    // In particular, when you press tab in Firefox, it fires a
 | 
						|
    // keypress event with keycode 0 after processing the original
 | 
						|
    // event.
 | 
						|
    if (e.which !== 0 && e.charCode !== 0) {
 | 
						|
        if (process_hotkey(e)) {
 | 
						|
            e.preventDefault();
 | 
						|
        }
 | 
						|
    }
 | 
						|
});
 | 
						|
 | 
						|
return exports;
 | 
						|
 | 
						|
}());
 | 
						|
 | 
						|
if (typeof module !== 'undefined') {
 | 
						|
    module.exports = hotkeys;
 | 
						|
}
 |