hotkey: Convert module to typescript.

This commit is contained in:
Evy Kassirer
2025-09-19 11:42:32 -07:00
committed by Tim Abbott
parent 7c90d2cee5
commit ce56be02bc
8 changed files with 74 additions and 47 deletions

View File

@@ -121,7 +121,7 @@ EXEMPT_FILES = make_set(
"web/src/hash_util.ts",
"web/src/hashchange.ts",
"web/src/hbs.d.ts",
"web/src/hotkey.js",
"web/src/hotkey.ts",
"web/src/inbox_ui.ts",
"web/src/inbox_util.ts",
"web/src/info_overlay.ts",

View File

@@ -231,7 +231,7 @@ function get_quote_target(opts: {message_id?: number; quote_content?: string | u
}
export function quote_message(opts: {
message_id: number;
message_id?: number;
quote_content?: string | undefined;
keep_composebox_empty?: boolean;
reply_type?: "personal";

View File

@@ -1,7 +1,7 @@
import $ from "jquery";
// Save the compose content cursor position and restore when we
// shift-tab back in (see hotkey.js).
// shift-tab back in (see hotkey.ts).
let saved_compose_cursor = 0;
function set_compose_textarea_handlers(): void {

View File

@@ -75,7 +75,7 @@ export function show_flatpickr(
assert(target !== undefined);
target.focus();
} else {
// Prevent keypresses from propagating to our general hotkey.js
// Prevent keypresses from propagating to our general hotkey.ts
// logic. Without this, `Up` will navigate both in the
// flatpickr instance and in the message feed behind
// it.

View File

@@ -65,8 +65,16 @@ import * as user_card_popover from "./user_card_popover.ts";
import * as user_group_popover from "./user_group_popover.ts";
import {user_settings} from "./user_settings.ts";
import * as user_topics_ui from "./user_topics_ui.ts";
import * as util from "./util.ts";
function do_narrow_action(action) {
function do_narrow_action(
action: (
target_id: number,
opts: {
trigger: string;
},
) => void,
): boolean {
if (message_lists.current === undefined) {
return false;
}
@@ -107,6 +115,11 @@ const KNOWN_IGNORE_SHIFT_MODIFIER_KEYS = new Set([
"@",
]);
type Hotkey = {
name: string;
message_view_only: boolean;
};
// 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
@@ -117,7 +130,7 @@ const KNOWN_IGNORE_SHIFT_MODIFIER_KEYS = new Set([
// in the main message view with a selected message.
// `message_view_only` hotkeys, as a group, are not processed if any
// overlays are open (e.g. settings, streams, etc.).
const KEYDOWN_MAPPINGS = {
const KEYDOWN_MAPPINGS: Record<string, Hotkey | Hotkey[]> = {
"Alt+P": {name: "toggle_compose_preview", message_view_only: true},
"Ctrl+[": {name: "escape", message_view_only: false},
"Cmd+Enter": {name: "action_with_enter", message_view_only: true},
@@ -239,7 +252,7 @@ const KNOWN_NAMED_KEY_ATTRIBUTE_VALUES = new Set([
"Tab",
]);
const CODE_TO_QWERTY_CHAR = {
const CODE_TO_QWERTY_CHAR: Record<string, string> = {
KeyA: "a",
KeyB: "b",
KeyC: "c",
@@ -310,7 +323,7 @@ const CODE_TO_QWERTY_CHAR = {
// Keyboard Event Viewer tool:
// https://w3c.github.io/uievents/tools/key-event-viewer.html
export function get_keydown_hotkey(e) {
export function get_keydown_hotkey(e: JQuery.KeyDownEvent): Hotkey | Hotkey[] | undefined {
// Determine the key pressed in a consistent way.
//
// For keyboard layouts (e.g. QWERTY) where `e.key` results
@@ -327,7 +340,11 @@ export function get_keydown_hotkey(e) {
let key = e.key;
if (!use_event_key) {
const code = `${e.shiftKey ? "Shift+" : ""}${e.code}`;
key = CODE_TO_QWERTY_CHAR[code];
if (CODE_TO_QWERTY_CHAR[code]) {
key = CODE_TO_QWERTY_CHAR[code];
} else {
return undefined;
}
}
if (common.has_mac_keyboard() && e.ctrlKey && key !== "[") {
@@ -362,7 +379,7 @@ export function get_keydown_hotkey(e) {
return KEYDOWN_MAPPINGS[key];
}
export let processing_text = () => {
export let processing_text = (): boolean => {
const $focused_elt = $(":focus");
return (
$focused_elt.is("input") ||
@@ -374,16 +391,13 @@ export let processing_text = () => {
);
};
export function rewire_processing_text(value) {
export function rewire_processing_text(value: typeof processing_text): void {
processing_text = value;
}
export function in_content_editable_widget(e) {
return $(e.target).is(".editable-section");
}
// Returns true if we handled it, false if the browser should.
export function process_escape_key(e) {
function process_escape_key(e: JQuery.KeyDownEvent): boolean {
assert(e.target instanceof HTMLElement);
if (
recent_view_ui.is_in_focus() &&
// This will return false if `e.target` is not
@@ -514,7 +528,7 @@ export function process_escape_key(e) {
return false;
}
function handle_popover_events(event_name) {
function handle_popover_events(event_name: string): boolean {
const popover_menu_visible_instance = popover_menus.get_visible_instance();
if (popover_menus.is_stream_actions_popover_displayed()) {
@@ -570,7 +584,7 @@ function handle_popover_events(event_name) {
}
// Returns true if we handled it, false if the browser should.
export function process_enter_key(e) {
function process_enter_key(e: JQuery.KeyDownEvent): boolean {
if ($(e.target).hasClass("trigger-click-on-enter")) {
// If the target has the class "trigger-click-on-enter", explicitly
// trigger a click event on it to call the associated click handler.
@@ -583,6 +597,7 @@ export function process_enter_key(e) {
// If a popover is open and we pressed Enter on a menu item,
// call click directly on the item to navigate to the `href`.
// trigger("click") doesn't work for them to navigate to `href`.
assert(e.target instanceof HTMLElement);
e.target.click();
e.preventDefault();
popovers.hide_all();
@@ -636,6 +651,7 @@ export function process_enter_key(e) {
// Transfer the enter keypress from button to the `<i>` tag inside
// it since it is the trigger for the popover. <button> is already used
// to trigger the tooltip so it cannot be used to trigger the popover.
assert(e.target instanceof HTMLElement);
if (e.target.id === "send_later") {
compose_send_menu_popover.toggle();
return true;
@@ -705,7 +721,7 @@ export function process_enter_key(e) {
return false;
}
window.location = hash_util.by_conversation_and_time_url(message);
window.location.href = hash_util.by_conversation_and_time_url(message);
return true;
}
@@ -713,7 +729,7 @@ export function process_enter_key(e) {
return true;
}
export function process_cmd_or_ctrl_enter_key() {
export function process_cmd_or_ctrl_enter_key(): boolean {
if ($("#compose").hasClass("preview_mode")) {
const cmd_or_ctrl_pressed = true;
compose.handle_enter_key_with_preview_open(cmd_or_ctrl_pressed);
@@ -723,7 +739,7 @@ export function process_cmd_or_ctrl_enter_key() {
return false;
}
export function process_tab_key() {
export function process_tab_key(): boolean {
// Returns true if we handled it, false if the browser should.
// TODO: See if browsers like Safari can now handle tabbing correctly
// without our intervention.
@@ -752,7 +768,7 @@ export function process_tab_key() {
return false;
}
export function process_shift_tab_key() {
export function process_shift_tab_key(): boolean {
// Returns true if we handled it, false if the browser should.
// TODO: See if browsers like Safari can now handle tabbing correctly
// without our intervention.
@@ -796,7 +812,7 @@ export function process_shift_tab_key() {
// Process a keydown event.
//
// Returns true if we handled it, false if the browser should.
export function process_hotkey(e, hotkey) {
function process_hotkey(e: JQuery.KeyDownEvent, hotkey: Hotkey): boolean {
const event_name = hotkey.name;
// This block needs to be before the `Tab` handler.
@@ -815,6 +831,7 @@ export function process_hotkey(e, hotkey) {
case "shift_tab":
case "open_recent_view":
if (recent_view_ui.is_in_focus()) {
assert(e.target instanceof HTMLElement);
return recent_view_ui.change_focused_element($(e.target), event_name);
}
}
@@ -844,7 +861,7 @@ export function process_hotkey(e, hotkey) {
case "enter":
return process_enter_key(e);
case "action_with_enter":
return process_cmd_or_ctrl_enter_key(e);
return process_cmd_or_ctrl_enter_key();
case "tab":
return process_tab_key();
case "shift_tab":
@@ -952,9 +969,11 @@ export function process_hotkey(e, hotkey) {
}
if (event_name === "toggle_compose_preview") {
const $last_focused_compose_type_input = $(
compose_state.get_last_focused_compose_type_input(),
);
const last_focused_compose_type_input = compose_state.get_last_focused_compose_type_input();
if (last_focused_compose_type_input === undefined) {
return false;
}
const $last_focused_compose_type_input = $(last_focused_compose_type_input);
if ($last_focused_compose_type_input.hasClass("message_edit_content")) {
if ($last_focused_compose_type_input.closest(".preview_mode").length > 0) {
@@ -1011,7 +1030,7 @@ export function process_hotkey(e, hotkey) {
if (event_name === "open_saved_snippet_dropdown") {
const $messagebox = $(":focus").parents(".messagebox");
if ($messagebox.length === 1) {
$messagebox.find(".saved_snippets_widget")[0].click();
util.the($messagebox.find(".saved_snippets_widget")).click();
}
}
@@ -1029,7 +1048,7 @@ export function process_hotkey(e, hotkey) {
if (event_name === "up_arrow" && $(":focus").attr("id") === "search_query") {
$("#search_query").trigger("blur");
message_scroll_state.set_keyboard_triggered_current_scroll(true);
navigate.up(true);
navigate.up();
}
if (
@@ -1047,7 +1066,7 @@ export function process_hotkey(e, hotkey) {
return true;
case "page_down": {
// so that it always goes to the end of the text box.
const height = $(":focus")[0].scrollHeight;
const height = util.the($(":focus")).scrollHeight;
$(":focus")
.caret(Number.POSITIVE_INFINITY)
.animate({scrollTop: height}, "fast");
@@ -1188,9 +1207,9 @@ export function process_hotkey(e, hotkey) {
if (inbox_ui.is_in_focus()) {
return inbox_ui.toggle_topic_visibility_policy();
}
if (message_lists.current.selected_message()) {
if (message_lists.current!.selected_message()) {
user_topics_ui.toggle_topic_visibility_policy(
message_lists.current.selected_message(),
message_lists.current!.selected_message()!,
);
return true;
}
@@ -1322,6 +1341,7 @@ export function process_hotkey(e, hotkey) {
}
const msg = message_lists.current.selected_message();
assert(msg !== undefined);
// Shortcuts that operate on a message
switch (event_name) {
@@ -1364,9 +1384,9 @@ export function process_hotkey(e, hotkey) {
$emoji_icon?.length !== 0 &&
$emoji_icon.closest(".message_control_button").css("display") !== "none"
) {
emoji_picker_reference = $emoji_icon[0];
emoji_picker_reference = util.the($emoji_icon);
} else {
emoji_picker_reference = $row.find(".message-actions-menu-button")[0];
emoji_picker_reference = util.the($row.find(".message-actions-menu-button"));
}
emoji_picker.toggle_emoji_popover(emoji_picker_reference, msg.id, {
@@ -1378,7 +1398,7 @@ export function process_hotkey(e, hotkey) {
// '+': reacts with thumbs up emoji on selected message
// Use canonical name.
const thumbs_up_emoji_code = "1f44d";
const canonical_name = emoji.get_emoji_name(thumbs_up_emoji_code);
const canonical_name = emoji.get_emoji_name(thumbs_up_emoji_code)!;
reactions.toggle_emoji_reaction(msg, canonical_name);
return true;
}
@@ -1434,8 +1454,13 @@ export function process_hotkey(e, hotkey) {
if (!message_edit.can_move_message(msg)) {
return false;
}
stream_popover.build_move_topic_to_stream_popover(msg.stream_id, msg.topic, false, msg);
assert(msg.type === "stream");
void stream_popover.build_move_topic_to_stream_popover(
msg.stream_id,
msg.topic,
false,
msg,
);
return true;
}
case "toggle_read_receipts": {
@@ -1476,7 +1501,7 @@ export function process_hotkey(e, hotkey) {
return false;
}
export function process_keydown(e) {
export function process_keydown(e: JQuery.KeyDownEvent): boolean {
activity.set_new_user_input(true);
const result = get_keydown_hotkey(e);
if (!result) {
@@ -1489,7 +1514,7 @@ export function process_keydown(e) {
return process_hotkey(e, result);
}
export function initialize() {
export function initialize(): void {
$(document).on("keydown", (e) => {
if (e.key === undefined) {
/* Some browsers trigger a 'keydown' event with `key === undefined`

View File

@@ -1,5 +1,5 @@
/*
See hotkey.js for handlers that are more app-wide.
See hotkey.ts for handlers that are more app-wide.
*/
export const vim_left = "h";

View File

@@ -55,7 +55,7 @@ import * as giphy from "./giphy.ts";
import * as giphy_state from "./giphy_state.ts";
import * as group_permission_settings from "./group_permission_settings.ts";
import * as hashchange from "./hashchange.ts";
import * as hotkey from "./hotkey.js";
import * as hotkey from "./hotkey.ts";
import * as i18n from "./i18n.ts";
import * as inbox_ui from "./inbox_ui.ts";
import * as information_density from "./information_density.ts";

View File

@@ -17,7 +17,7 @@ const {page_params} = require("./lib/zpage_params.cjs");
// functions (like overlays.settings_open). Within that context, to
// test whether a given key (e.g. `x`) results in a specific function
// (e.g. `ui.foo()`), we fail to import any modules other than
// hotkey.js so that accessing them will result in a ReferenceError.
// hotkey.ts so that accessing them will result in a ReferenceError.
//
// Then we create a stub `ui.foo`, and call the hotkey function. If
// it calls any external module other than `ui.foo`, it'll crash.
@@ -37,7 +37,7 @@ set_global("document", {
const activity_ui = mock_esm("../src/activity_ui");
const activity = zrequire("../src/activity");
const browser_history = mock_esm("../src/browser_history");
const browser_history = mock_esm("../src/browser_history", {go_to_location() {}});
const compose_actions = mock_esm("../src/compose_actions");
const compose_reply = mock_esm("../src/compose_reply");
const condense = mock_esm("../src/condense");
@@ -106,7 +106,7 @@ message_lists.current = {
},
selected_row() {
const $row = $.create("selected-row-stub");
$row.set_find_results(".message-actions-menu-button", []);
$row.set_find_results(".message-actions-menu-button", ["<menu-button-stub>"]);
$row.set_find_results(".emoji-message-control-button-container", {
closest: () => ({css: () => "none"}),
});
@@ -114,6 +114,8 @@ message_lists.current = {
},
selected_message() {
return {
type: "stream",
stream_id: 2,
sent_by_me: true,
flags: ["read", "starred"],
};
@@ -355,11 +357,11 @@ function test_normal_typing() {
assert_unmapped('~!@#$%^*()_+{}:"<>');
}
test_while_not_editing_text("unmapped keys return false easily", () => {
run_test("unmapped keys return false easily", () => {
// Unmapped keys should immediately return false, without
// calling any functions outside of hotkey.js.
// calling any functions outside of hotkey.ts.
// (unless we are editing text)
assert_unmapped("bfoyz");
assert_unmapped("bfo");
assert_unmapped("BEFLOQTWXYZ");
});