click_handlers: Convert module to TypeScript.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg
2024-12-10 23:23:21 -08:00
committed by Tim Abbott
parent 4f297bee32
commit 26aa19db43
6 changed files with 147 additions and 100 deletions

View File

@@ -63,7 +63,7 @@ EXEMPT_FILES = make_set(
"web/src/bootstrap_typeahead.ts",
"web/src/browser_history.ts",
"web/src/buddy_list.ts",
"web/src/click_handlers.js",
"web/src/click_handlers.ts",
"web/src/compose.js",
"web/src/compose_actions.ts",
"web/src/compose_banner.ts",

View File

@@ -1,8 +1,9 @@
// You won't find every click handler here, but it's a good place to start!
import $ from "jquery";
import assert from "minimalistic-assert";
import * as tippy from "tippy.js";
// You won't find every click handler here, but it's a good place to start!
import {z} from "zod";
import render_buddy_list_tooltip_content from "../templates/buddy_list_tooltip_content.hbs";
@@ -41,12 +42,12 @@ import * as ui_util from "./ui_util.ts";
import {parse_html} from "./ui_util.ts";
import * as util from "./util.ts";
export function initialize() {
export function initialize(): void {
// MESSAGE CLICKING
function initialize_long_tap() {
function initialize_long_tap(): void {
const MS_DELAY = 750;
const meta = {
const meta: {touchdown: boolean; current_target: number | undefined; invalid?: boolean} = {
touchdown: false,
current_target: undefined,
};
@@ -69,7 +70,7 @@ export function initialize() {
// Later we check whether after MS_DELAY the user is still
// long touching the same message as it can be possible that
// user touched another message within MS_DELAY period.
if (meta.touchdown === true && !meta.invalid && id === meta.current_target) {
if (meta.touchdown && !meta.invalid && id === meta.current_target) {
$(this).trigger("longtap");
}
}, MS_DELAY);
@@ -95,7 +96,7 @@ export function initialize() {
initialize_long_tap();
}
function is_clickable_message_element($target) {
function is_clickable_message_element($target: JQuery<Element>): boolean {
// This function defines all the elements within a message
// body that have UI behavior other than starting a reply.
@@ -158,7 +159,8 @@ export function initialize() {
return false;
}
const select_message_function = function (e) {
const select_message_function = function (this: HTMLElement, e: JQuery.TriggeredEvent): void {
assert(e.target instanceof Element);
if (is_clickable_message_element($(e.target))) {
// If this click came from a hyperlink, don't trigger the
// reply action. The simple way of doing this is simply
@@ -172,7 +174,7 @@ export function initialize() {
return;
}
if (document.getSelection().type === "Range") {
if (document.getSelection()?.type === "Range") {
// Drags on the message (to copy message text) shouldn't trigger a reply.
return;
}
@@ -199,7 +201,7 @@ export function initialize() {
// This might happen for locally echoed messages, for example.
return;
}
window.location = hash_util.by_conversation_and_time_url(message);
window.location.href = hash_util.by_conversation_and_time_url(message);
return;
}
@@ -216,7 +218,7 @@ export function initialize() {
$("#main_div").on("click", ".messagebox", select_message_function);
// on the other hand, on mobile it should be done with a long tap.
} else {
$("#main_div").on("longtap", ".messagebox", function (e) {
$("#main_div").on("longtap", ".messagebox", function (this: HTMLElement, e) {
const sel = window.getSelection();
// if one matches, remove the current selections.
// after a longtap that is valid, there should be no text selected.
@@ -242,10 +244,11 @@ export function initialize() {
const message_id = rows.id($(this).closest(".message_row"));
const message = message_store.get(message_id);
assert(message !== undefined);
starred_messages_ui.toggle_starred_and_update_server(message);
});
$("#main_div").on("click", ".message_reaction", function (e) {
$("#main_div").on("click", ".message_reaction", function (this: HTMLElement, e) {
e.stopPropagation();
if (page_params.is_spectator) {
@@ -254,7 +257,7 @@ export function initialize() {
}
emoji_picker.hide_emoji_popover();
const local_id = $(this).attr("data-reaction-id");
const local_id = $(this).attr("data-reaction-id")!;
const message_id = rows.get_message_id(this);
reactions.process_reaction_click(message_id, local_id);
$(".tooltip").remove();
@@ -268,16 +271,16 @@ export function initialize() {
e.preventDefault();
});
$("#main_div").on("click", "a.stream", function (e) {
$("#main_div").on("click", "a.stream", function (this: HTMLAnchorElement, e) {
e.preventDefault();
// Note that we may have an href here, but we trust the stream id more,
// so we re-encode the hash.
const stream_id = Number.parseInt($(this).attr("data-stream-id"), 10);
const stream_id = Number.parseInt($(this).attr("data-stream-id")!, 10);
if (stream_id) {
browser_history.go_to_location(hash_util.by_stream_url(stream_id));
return;
}
window.location.href = $(this).attr("href");
window.location.href = this.href;
});
$("body").on("click", "#scroll-to-bottom-button-clickable-area", (e) => {
@@ -310,7 +313,8 @@ export function initialize() {
const $row = message_lists.current.get_row(rows.id($(this).closest(".message_row")));
const message_id = rows.id($row);
const message = message_lists.current.get(message_id);
stream_popover.build_move_topic_to_stream_popover(
assert(message?.type === "stream");
void stream_popover.build_move_topic_to_stream_popover(
message.stream_id,
message.topic,
false,
@@ -357,28 +361,36 @@ export function initialize() {
e.preventDefault();
const row_id = rows.id($(this).closest(".message_row"));
$(`#edit_form_${CSS.escape(row_id)} .file_input`).trigger("click");
$(`#edit_form_${CSS.escape(`${row_id}`)} .file_input`).trigger("click");
});
$("body").on("focus", ".message_edit_form .message_edit_content", (e) => {
compose_state.set_last_focused_compose_type_input(e.target);
$("body").on(
"focus",
".message_edit_form textarea.message_edit_content",
function (this: HTMLTextAreaElement, _event) {
compose_state.set_last_focused_compose_type_input(this);
},
);
$("body").on("click", ".message_edit_form .markdown_preview", function (this: HTMLElement, e) {
e.preventDefault();
message_edit.show_preview_area($(this));
});
$("body").on("click", ".message_edit_form .markdown_preview", (e) => {
$("body").on(
"click",
".message_edit_form .undo_markdown_preview",
function (this: HTMLElement, e) {
e.preventDefault();
message_edit.show_preview_area($(e.target));
});
$("body").on("click", ".message_edit_form .undo_markdown_preview", (e) => {
e.preventDefault();
message_edit.clear_preview_area($(e.target));
});
message_edit.clear_preview_area($(this));
},
);
// RESOLVED TOPICS
$("body").on("click", ".message_header .on_hover_topic_resolve", (e) => {
e.stopPropagation();
const $recipient_row = $(e.target).closest(".recipient_row");
const message_id = rows.id_for_recipient_row($recipient_row);
const topic_name = $(e.target).attr("data-topic-name");
const topic_name = $(e.target).attr("data-topic-name")!;
message_edit.toggle_resolve_topic(message_id, topic_name, false, $recipient_row);
});
@@ -386,35 +398,39 @@ export function initialize() {
e.stopPropagation();
const $recipient_row = $(e.target).closest(".recipient_row");
const message_id = rows.id_for_recipient_row($recipient_row);
const topic_name = $(e.target).attr("data-topic-name");
const topic_name = $(e.target).attr("data-topic-name")!;
message_edit.toggle_resolve_topic(message_id, topic_name, false, $recipient_row);
});
// RECIPIENT BARS
function get_row_id_for_narrowing(narrow_link_elem) {
function get_row_id_for_narrowing(narrow_link_elem: HTMLElement): number {
const $group = rows.get_closest_group(narrow_link_elem);
const msg_id = rows.id_for_recipient_row($group);
assert(message_lists.current !== undefined);
const nearest = message_lists.current.get(msg_id);
const nearest = message_lists.current.get(msg_id)!;
const selected = message_lists.current.selected_message();
if (util.same_recipient(nearest, selected)) {
if (selected !== undefined && util.same_recipient(nearest, selected)) {
return selected.id;
}
return nearest.id;
}
$("#message_feed_container").on("click", ".narrows_by_recipient", function (e) {
$("#message_feed_container").on(
"click",
".narrows_by_recipient",
function (this: HTMLElement, e) {
if (e.metaKey || e.ctrlKey || e.shiftKey) {
return;
}
e.preventDefault();
const row_id = get_row_id_for_narrowing(this);
message_view.narrow_by_recipient(row_id, {trigger: "message header"});
});
},
);
$("#message_feed_container").on("click", ".narrows_by_topic", function (e) {
$("#message_feed_container").on("click", ".narrows_by_topic", function (this: HTMLElement, e) {
if (e.metaKey || e.ctrlKey || e.shiftKey) {
return;
}
@@ -455,22 +471,25 @@ export function initialize() {
// Doesn't show tooltip on touch devices.
function do_render_buddy_list_tooltip(
$elem,
title_data,
get_target_node,
check_reference_removed,
$elem: JQuery,
title_data: buddy_data.TitleData,
get_target_node?: (tippy_instance: tippy.Instance) => HTMLElement,
check_reference_removed?: (
mutation: MutationRecord,
tippy_instance: tippy.Instance,
) => boolean,
subtree = false,
parent_element_to_append = null,
parent_element_to_append: HTMLElement | null = null,
is_custom_observer_needed = true,
) {
let placement = "left";
let observer;
): void {
let placement: tippy.Placement = "left";
let observer: MutationObserver;
if (window.innerWidth < media_breakpoints_num.md) {
// On small devices display tooltips based on available space.
// This will default to "bottom" placement for this tooltip.
placement = "auto";
}
tippy.default($elem[0], {
tippy.default(util.the($elem), {
// Quickly display and hide right sidebar tooltips
// so that they don't stick and overlap with
// each other.
@@ -494,12 +513,14 @@ export function initialize() {
if (!is_custom_observer_needed) {
return;
}
assert(get_target_node !== undefined);
assert(check_reference_removed !== undefined);
// We cannot use MutationObserver directly on the reference element because
// it will be removed and we need to attach it on an element which will remain in the DOM.
const target_node = get_target_node(instance);
// We only need to know if any of the `li` elements were removed.
const config = {attributes: false, childList: true, subtree};
const callback = function (mutationsList) {
const callback: MutationCallback = function (mutationsList) {
for (const mutation of mutationsList) {
// Hide instance if reference is in the removed node list.
if (check_reference_removed(mutation, instance)) {
@@ -510,7 +531,7 @@ export function initialize() {
observer = new MutationObserver(callback);
observer.observe(target_node, config);
},
appendTo: () => parent_element_to_append || document.body,
appendTo: () => parent_element_to_append ?? document.body,
});
}
@@ -519,15 +540,18 @@ export function initialize() {
e.stopPropagation();
const user_id_string = $(e.currentTarget)
.closest(".user_sidebar_entry")
.attr("data-user-id");
.attr("data-user-id")!;
const title_data = buddy_data.get_title_data(user_id_string, false);
// `target_node` is the `ul` element since it stays in DOM even after updates.
function get_target_node() {
return $(e.target).parents(".buddy-list-section")[0];
function get_target_node(): HTMLElement {
return util.the($(e.target).parents(".buddy-list-section"));
}
function check_reference_removed(mutation, instance) {
function check_reference_removed(
mutation: MutationRecord,
instance: tippy.Instance,
): boolean {
return Array.prototype.includes.call(
mutation.removedNodes,
instance.reference.parentElement,
@@ -546,8 +570,9 @@ export function initialize() {
*/
$(".user_sidebar_entry .status-emoji-name").off("mouseenter").off("mouseleave");
$(".user_sidebar_entry .status-emoji-name").on("mouseenter", () => {
const instance = $elem[0]._tippy;
if (instance && instance.state.isVisible) {
const element: tippy.ReferenceElement = util.the($elem);
const instance = element._tippy;
if (instance?.state.isVisible) {
instance.destroy();
}
});
@@ -562,22 +587,25 @@ export function initialize() {
});
// DIRECT MESSAGE LIST TOOLTIPS (not displayed on touch devices)
$("body").on("mouseenter", ".dm-user-status", (e) => {
$("body").on("mouseenter", ".dm-user-status", function (this: HTMLElement, e) {
e.stopPropagation();
const $elem = $(e.currentTarget);
const user_ids_string = $elem.attr("data-user-ids-string");
const $elem = $(this);
const user_ids_string = $elem.attr("data-user-ids-string")!;
// This converts from 'true' in the DOM to true.
const is_group = JSON.parse($elem.attr("data-is-group"));
const is_group = z.boolean().parse(JSON.parse($elem.attr("data-is-group")!));
const title_data = buddy_data.get_title_data(user_ids_string, is_group);
// Since anything inside `#left_sidebar_scroll_container` can be replaced, it is our target node here.
function get_target_node() {
return document.querySelector("#left_sidebar_scroll_container");
function get_target_node(): HTMLElement {
return document.querySelector("#left_sidebar_scroll_container")!;
}
// Whole list is just replaced, so we need to check for that.
function check_reference_removed(mutation, instance) {
function check_reference_removed(
mutation: MutationRecord,
instance: tippy.Instance,
): boolean {
return Array.prototype.includes.call(
mutation.removedNodes,
$(instance.reference).parents(".dm-list")[0],
@@ -600,8 +628,9 @@ export function initialize() {
*/
$(".dm-user-status .status-emoji-name").off("mouseenter").off("mouseleave");
$(".dm-user-status .status-emoji-name").on("mouseenter", () => {
const instance = $elem[0]._tippy;
if (instance && instance.state.isVisible) {
const element: tippy.ReferenceElement = util.the($elem);
const instance = element._tippy;
if (instance?.state.isVisible) {
instance.destroy();
}
});
@@ -616,10 +645,9 @@ export function initialize() {
});
// Left sidebar channel rows
$("body").on("click", ".channel-new-topic-button", (e) => {
$("body").on("click", ".channel-new-topic-button", function (this: HTMLElement, e) {
e.stopPropagation();
const elem = e.currentTarget;
const stream_id = Number.parseInt(elem.dataset.streamId, 10);
const stream_id = Number.parseInt(this.dataset.streamId!, 10);
compose_actions.start({
message_type: "stream",
stream_id,
@@ -630,18 +658,29 @@ export function initialize() {
});
// Recent conversations direct messages (Not displayed on small widths)
$("body").on("mouseenter", ".recent_topic_stream .pm_status_icon", (e) => {
$("body").on(
"mouseenter",
".recent_topic_stream .pm_status_icon",
function (this: HTMLElement, e) {
e.stopPropagation();
const $elem = $(e.currentTarget);
const $elem = $(this);
const user_ids_string = $elem.attr("data-user-ids-string");
// Don't show tooltip for group direct messages.
if (!user_ids_string || user_ids_string.split(",").length !== 1) {
return;
}
const title_data = recent_view_ui.get_pm_tooltip_data(user_ids_string);
const noop = () => {};
do_render_buddy_list_tooltip($elem, title_data, noop, noop, false, undefined, false);
});
do_render_buddy_list_tooltip(
$elem,
title_data,
undefined,
undefined,
false,
undefined,
false,
);
},
);
// MISC
@@ -652,7 +691,7 @@ export function initialize() {
"#buddy-list-users-matching-view",
].join(", ");
$(sel).on("click", "a", function () {
$(sel).on("click", "a", function (this: HTMLElement) {
this.blur();
});
}
@@ -687,11 +726,11 @@ export function initialize() {
});
$("body").on("click", "[data-overlay-trigger]", function () {
const target = $(this).attr("data-overlay-trigger");
const target = $(this).attr("data-overlay-trigger")!;
browser_history.go_to_location(target);
});
function handle_compose_click(e) {
function handle_compose_click(e: JQuery.ClickEvent): void {
const $target = $(e.target);
// Emoji clicks should be handled by their own click handler in emoji_picker.js
if ($target.is(".emoji_map, img.emoji, .drag, .compose_gif_icon, .compose_control_menu")) {
@@ -724,9 +763,13 @@ export function initialize() {
compose_actions.cancel();
});
$("body").on("focus", "#compose-textarea", (e) => {
compose_state.set_last_focused_compose_type_input(e.target);
});
$("body").on(
"focus",
"textarea#compose-textarea",
function (this: HTMLTextAreaElement, _event: JQuery.Event) {
compose_state.set_last_focused_compose_type_input(this);
},
);
// LEFT SIDEBAR
@@ -789,18 +832,22 @@ export function initialize() {
// Chrome focuses an element when dragging it which can be confusing when
// users involuntarily drag something and we show them the focus outline.
$("body").on("dragstart", "a", (e) => e.target.blur());
$("body").on("dragstart", "a", function (this: HTMLElement) {
this.blur();
});
// Don't focus links on middle click.
$("body").on("mouseup", "a", (e) => {
$("body").on("mouseup", "a", function (this: HTMLElement, e) {
if (e.button === 1) {
// middle click
e.target.blur();
this.blur();
}
});
// Don't focus links on context menu.
$("body").on("contextmenu", "a", (e) => e.target.blur());
$("body").on("contextmenu", "a", function (this: HTMLElement) {
this.blur();
});
$("body").on("click", ".language_selection_widget button", (e) => {
e.preventDefault();
@@ -844,7 +891,7 @@ export function initialize() {
$("textarea#compose-textarea").trigger("focus");
return;
} else if (
!window.getSelection().toString() &&
!window.getSelection()?.toString() &&
// Clicking any input or text area should not close
// the compose box; this means using the sidebar
// filters or search widgets won't unnecessarily close

View File

@@ -82,7 +82,7 @@ have a target of "_blank".
The "info:" items use our info overlay system
in web/src/info_overlay.ts. They are dispatched
using a click handler in web/src/click_handlers.js.
using a click handler in web/src/click_handlers.ts.
The click handler uses "[data-overlay-trigger]" as
the selector and then calls browser_history.go_to_location.
*/

View File

@@ -633,7 +633,7 @@ export function initialize(): void {
/*
Status emoji tooltips for most locations in the app. This
basic tooltip logic is overridden by separate logic in
click_handlers.js for the left and right sidebars, to
click_handlers.ts for the left and right sidebars, to
avoid problematic interactions with the main tooltips for
those regions.
*/

View File

@@ -19,7 +19,7 @@ import * as audible_notifications from "./audible_notifications.ts";
import * as blueslip from "./blueslip.ts";
import * as bot_data from "./bot_data.ts";
import * as channel from "./channel.ts";
import * as click_handlers from "./click_handlers.js";
import * as click_handlers from "./click_handlers.ts";
import * as common from "./common.ts";
import * as compose from "./compose.js";
import * as compose_closed_ui from "./compose_closed_ui.ts";

View File

@@ -376,7 +376,7 @@ test("handlers", ({override, override_rewire, mock_template}) => {
(function test_click_handler() {
init();
// We wire up the click handler in click_handlers.js,
// We wire up the click handler in click_handlers.ts,
// so this just tests the called function.
narrowed = false;
activity_ui.narrow_for_user({$li: $alice_li});