compose_ui: Convert module to typescript.

This commit is contained in:
Joydeep Bhattacharjee 2024-01-23 11:17:58 +00:00 committed by Tim Abbott
parent 91bdcc9596
commit 264a9a1057
2 changed files with 97 additions and 59 deletions

View File

@ -76,7 +76,7 @@ EXEMPT_FILES = make_set(
"web/src/compose_state.ts", "web/src/compose_state.ts",
"web/src/compose_textarea.ts", "web/src/compose_textarea.ts",
"web/src/compose_tooltips.js", "web/src/compose_tooltips.js",
"web/src/compose_ui.js", "web/src/compose_ui.ts",
"web/src/compose_validate.js", "web/src/compose_validate.js",
"web/src/composebox_typeahead.js", "web/src/composebox_typeahead.js",
"web/src/condense.js", "web/src/condense.js",

View File

@ -17,20 +17,36 @@ import * as stream_data from "./stream_data";
import * as user_status from "./user_status"; import * as user_status from "./user_status";
import * as util from "./util"; import * as util from "./util";
// TODO: Refactor to push this into a field of ComposeTriggeredOptions.
type messageType = "stream" | "private";
type ComposeTriggeredOptions = {
trigger: string;
private_message_recipient: string;
topic: string;
stream_id: number;
};
type SelectedLinesSections = {
before_lines: string;
separating_new_line_before: boolean;
selected_lines: string;
separating_new_line_after: boolean;
after_lines: string;
};
export let compose_spinner_visible = false; export let compose_spinner_visible = false;
export let shift_pressed = false; // true or false export let shift_pressed = false; // true or false
let full_size_status = false; // true or false let full_size_status = false; // true or false
// Some functions to handle the full size status explicitly // Some functions to handle the full size status explicitly
export function set_full_size(is_full) { export function set_full_size(is_full: boolean): void {
full_size_status = is_full; full_size_status = is_full;
} }
export function is_full_size() { export function is_full_size(): boolean {
return full_size_status; return full_size_status;
} }
export function autosize_textarea($textarea) { export function autosize_textarea($textarea: JQuery<HTMLTextAreaElement>): void {
// Since this supports both compose and file upload, one must pass // Since this supports both compose and file upload, one must pass
// in the text area to autosize. // in the text area to autosize.
if (!is_full_size()) { if (!is_full_size()) {
@ -38,7 +54,10 @@ export function autosize_textarea($textarea) {
} }
} }
export function insert_and_scroll_into_view(content, $textarea) { export function insert_and_scroll_into_view(
content: string,
$textarea: JQuery<HTMLTextAreaElement>,
): void {
insert($textarea[0], content); insert($textarea[0], content);
// Blurring and refocusing ensures the cursor / selection is in view. // Blurring and refocusing ensures the cursor / selection is in view.
$textarea.trigger("blur"); $textarea.trigger("blur");
@ -46,7 +65,7 @@ export function insert_and_scroll_into_view(content, $textarea) {
autosize_textarea($textarea); autosize_textarea($textarea);
} }
function get_focus_area(msg_type, opts) { function get_focus_area(msg_type: messageType, opts: ComposeTriggeredOptions): string {
// Set focus to "Topic" when narrowed to a stream+topic // Set focus to "Topic" when narrowed to a stream+topic
// and "Start new conversation" button clicked. // and "Start new conversation" button clicked.
if (msg_type === "stream" && opts.stream_id && !opts.topic) { if (msg_type === "stream" && opts.stream_id && !opts.topic) {
@ -70,24 +89,24 @@ function get_focus_area(msg_type, opts) {
// Export for testing // Export for testing
export const _get_focus_area = get_focus_area; export const _get_focus_area = get_focus_area;
export function set_focus(msg_type, opts) { export function set_focus(msg_type: messageType, opts: ComposeTriggeredOptions): void {
// Called mainly when opening the compose box or switching the // Called mainly when opening the compose box or switching the
// message type to set the focus in the first empty input in the // message type to set the focus in the first empty input in the
// compose box. // compose box.
if (window.getSelection().toString() === "" || opts.trigger !== "message click") { if (window.getSelection()!.toString() === "" || opts.trigger !== "message click") {
const focus_area = get_focus_area(msg_type, opts); const focus_area = get_focus_area(msg_type, opts);
$(focus_area).trigger("focus"); $(focus_area).trigger("focus");
} }
} }
export function smart_insert_inline($textarea, syntax) { export function smart_insert_inline($textarea: JQuery<HTMLTextAreaElement>, syntax: string): void {
function is_space(c) { function is_space(c: string): boolean {
return c === " " || c === "\t" || c === "\n"; return c === " " || c === "\t" || c === "\n";
} }
const pos = $textarea.caret(); const pos = $textarea.caret();
const before_str = $textarea.val().slice(0, pos); const before_str = $textarea.val()!.slice(0, pos);
const after_str = $textarea.val().slice(pos); const after_str = $textarea.val()!.slice(pos);
if ( if (
pos > 0 && pos > 0 &&
@ -114,10 +133,14 @@ export function smart_insert_inline($textarea, syntax) {
insert_and_scroll_into_view(syntax, $textarea); insert_and_scroll_into_view(syntax, $textarea);
} }
export function smart_insert_block($textarea, syntax, padding_newlines = 2) { export function smart_insert_block(
$textarea: JQuery<HTMLTextAreaElement>,
syntax: string,
padding_newlines = 2,
): void {
const pos = $textarea.caret(); const pos = $textarea.caret();
const before_str = $textarea.val().slice(0, pos); const before_str = $textarea.val()!.slice(0, pos);
const after_str = $textarea.val().slice(pos); const after_str = $textarea.val()!.slice(pos);
if (pos > 0) { if (pos > 0) {
// Insert newline/s before the content block if there is // Insert newline/s before the content block if there is
@ -161,11 +184,11 @@ export function smart_insert_block($textarea, syntax, padding_newlines = 2) {
} }
export function insert_syntax_and_focus( export function insert_syntax_and_focus(
syntax, syntax: string,
$textarea = $("textarea#compose-textarea"), $textarea = $<HTMLTextAreaElement>("textarea#compose-textarea"),
mode = "inline", mode = "inline",
padding_newlines, padding_newlines: number,
) { ): void {
// Generic helper for inserting syntax into the main compose box // Generic helper for inserting syntax into the main compose box
// where the cursor was and focusing the area. Mostly a thin // where the cursor was and focusing the area. Mostly a thin
// wrapper around smart_insert_inline and smart_inline_block. // wrapper around smart_insert_inline and smart_inline_block.
@ -187,11 +210,15 @@ export function insert_syntax_and_focus(
} }
} }
export function replace_syntax(old_syntax, new_syntax, $textarea = $("textarea#compose-textarea")) { export function replace_syntax(
old_syntax: string,
new_syntax: string,
$textarea = $<HTMLTextAreaElement>("textarea#compose-textarea"),
): boolean {
// The following couple lines are needed to later restore the initial // The following couple lines are needed to later restore the initial
// logical position of the cursor after the replacement // logical position of the cursor after the replacement
const prev_caret = $textarea.caret(); const prev_caret = $textarea.caret();
const replacement_offset = $textarea.val().indexOf(old_syntax); const replacement_offset = $textarea.val()!.indexOf(old_syntax);
// Replaces `old_syntax` with `new_syntax` text in the compose box. Due to // Replaces `old_syntax` with `new_syntax` text in the compose box. Due to
// the way that JavaScript handles string replacements, if `old_syntax` is // the way that JavaScript handles string replacements, if `old_syntax` is
@ -228,7 +255,9 @@ export function replace_syntax(old_syntax, new_syntax, $textarea = $("textarea#c
return old_text !== new_text; return old_text !== new_text;
} }
export function compute_placeholder_text(opts) { export function compute_placeholder_text(
opts: {message_type: messageType} & ComposeTriggeredOptions,
): string {
// Computes clear placeholder text for the compose box, depending // Computes clear placeholder text for the compose box, depending
// on what heading values have already been filled out. // on what heading values have already been filled out.
// //
@ -254,17 +283,17 @@ export function compute_placeholder_text(opts) {
const recipient_list = opts.private_message_recipient.split(","); const recipient_list = opts.private_message_recipient.split(",");
const recipient_parts = recipient_list.map((recipient) => { const recipient_parts = recipient_list.map((recipient) => {
const user = people.get_by_email(recipient); const user = people.get_by_email(recipient);
if (people.should_add_guest_user_indicator(user.user_id)) { if (people.should_add_guest_user_indicator(user!.user_id)) {
return $t({defaultMessage: "{name} (guest)"}, {name: user.full_name}); return $t({defaultMessage: "{name} (guest)"}, {name: user!.full_name});
} }
return user.full_name; return user!.full_name;
}); });
const recipient_names = util.format_array_as_list(recipient_parts, "long", "conjunction"); const recipient_names = util.format_array_as_list(recipient_parts, "long", "conjunction");
if (recipient_list.length === 1) { if (recipient_list.length === 1) {
// If it's a single user, display status text if available // If it's a single user, display status text if available
const user = people.get_by_email(recipient_list[0]); const user = people.get_by_email(recipient_list[0]);
const status = user_status.get_status_text(user.user_id); const status = user_status.get_status_text(user!.user_id);
if (status) { if (status) {
return $t( return $t(
{defaultMessage: "Message {recipient_name} ({recipient_status})"}, {defaultMessage: "Message {recipient_name} ({recipient_status})"},
@ -277,7 +306,7 @@ export function compute_placeholder_text(opts) {
return $t({defaultMessage: "Compose your message here"}); return $t({defaultMessage: "Compose your message here"});
} }
export function set_compose_box_top(set_top) { export function set_compose_box_top(set_top: boolean): void {
if (set_top) { if (set_top) {
// As `#compose` has `position: fixed` property, we cannot // As `#compose` has `position: fixed` property, we cannot
// make the compose-box to attain the correct height just by // make the compose-box to attain the correct height just by
@ -291,7 +320,7 @@ export function set_compose_box_top(set_top) {
} }
} }
export function make_compose_box_full_size() { export function make_compose_box_full_size(): void {
set_full_size(true); set_full_size(true);
// The autosize should be destroyed for the full size compose // The autosize should be destroyed for the full size compose
@ -309,7 +338,7 @@ export function make_compose_box_full_size() {
$("textarea#compose-textarea").trigger("focus"); $("textarea#compose-textarea").trigger("focus");
} }
export function make_compose_box_original_size() { export function make_compose_box_original_size(): void {
set_full_size(false); set_full_size(false);
$("#compose").removeClass("compose-fullscreen"); $("#compose").removeClass("compose-fullscreen");
@ -326,7 +355,10 @@ export function make_compose_box_original_size() {
$("textarea#compose-textarea").trigger("focus"); $("textarea#compose-textarea").trigger("focus");
} }
export function handle_keydown(event, $textarea) { export function handle_keydown(
event: JQuery.KeyboardEventBase,
$textarea: JQuery<HTMLTextAreaElement>,
): void {
if (event.key === "Shift") { if (event.key === "Shift") {
shift_pressed = true; shift_pressed = true;
} }
@ -354,7 +386,10 @@ export function handle_keydown(event, $textarea) {
} }
} }
export function handle_keyup(_event, $textarea) { export function handle_keyup(
_event: JQuery.KeyboardEventBase,
$textarea: JQuery<HTMLTextAreaElement>,
): void {
if (_event?.key === "Shift") { if (_event?.key === "Shift") {
shift_pressed = false; shift_pressed = false;
} }
@ -362,11 +397,11 @@ export function handle_keyup(_event, $textarea) {
rtl.set_rtl_class_for_textarea($textarea); rtl.set_rtl_class_for_textarea($textarea);
} }
export function cursor_inside_code_block($textarea) { export function cursor_inside_code_block($textarea: JQuery<HTMLTextAreaElement>): boolean {
// Returns whether the cursor is at a point that would be inside // Returns whether the cursor is at a point that would be inside
// a code block on rendering the textarea content as markdown. // a code block on rendering the textarea content as markdown.
const cursor_position = $textarea.caret(); const cursor_position = $textarea.caret();
const current_content = $textarea.val(); const current_content = $textarea.val()!;
let unique_insert = "UNIQUEINSERT:" + Math.random(); let unique_insert = "UNIQUEINSERT:" + Math.random();
while (current_content.includes(unique_insert)) { while (current_content.includes(unique_insert)) {
@ -379,19 +414,22 @@ export function cursor_inside_code_block($textarea) {
const rendered_content = markdown.parse_non_message(content); const rendered_content = markdown.parse_non_message(content);
const rendered_html = new DOMParser().parseFromString(rendered_content, "text/html"); const rendered_html = new DOMParser().parseFromString(rendered_content, "text/html");
const code_blocks = rendered_html.querySelectorAll("pre > code"); const code_blocks = rendered_html.querySelectorAll("pre > code");
return [...code_blocks].some((code_block) => code_block.textContent.includes(unique_insert)); return [...code_blocks].some((code_block) => code_block?.textContent?.includes(unique_insert));
} }
export function format_text($textarea, type, inserted_content) { export function format_text(
$textarea: JQuery<HTMLTextAreaElement>,
type: string,
inserted_content = "",
): void {
const italic_syntax = "*"; const italic_syntax = "*";
const bold_syntax = "**"; const bold_syntax = "**";
const bold_and_italic_syntax = "***"; const bold_and_italic_syntax = "***";
let is_selected_text_italic = false; let is_selected_text_italic = false;
let is_inner_text_italic = false; let is_inner_text_italic = false;
const field = $textarea.get(0); const field = $textarea.get(0)!;
let range = $textarea.range(); let range = $textarea.range();
let text = $textarea.val(); let text = $textarea.val()!;
// Remove new line and space around selected text, except list formatting, // Remove new line and space around selected text, except list formatting,
// where we want to especially preserve any selected new line character // where we want to especially preserve any selected new line character
// before the selected text, as it is conventionally depicted with a highlight // before the selected text, as it is conventionally depicted with a highlight
@ -410,19 +448,19 @@ export function format_text($textarea, type, inserted_content) {
const selected_text = range.text; const selected_text = range.text;
// Check if the selection is already surrounded by syntax // Check if the selection is already surrounded by syntax
const is_selection_formatted = (syntax_start, syntax_end = syntax_start) => const is_selection_formatted = (syntax_start: string, syntax_end = syntax_start): boolean =>
range.start >= syntax_start.length && range.start >= syntax_start.length &&
text.length - range.end >= syntax_end.length && text.length - range.end >= syntax_end.length &&
text.slice(range.start - syntax_start.length, range.start) === syntax_start && text.slice(range.start - syntax_start.length, range.start) === syntax_start &&
text.slice(range.end, range.end + syntax_end.length) === syntax_end; text.slice(range.end, range.end + syntax_end.length) === syntax_end;
// Check if selected text itself has syntax inside it. // Check if selected text itself has syntax inside it.
const is_inner_text_formatted = (syntax_start, syntax_end = syntax_start) => const is_inner_text_formatted = (syntax_start: string, syntax_end = syntax_start): boolean =>
range.length >= syntax_start.length + syntax_end.length && range.length >= syntax_start.length + syntax_end.length &&
selected_text.startsWith(syntax_start) && selected_text.startsWith(syntax_start) &&
selected_text.endsWith(syntax_end); selected_text.endsWith(syntax_end);
const section_off_selected_lines = () => { const section_off_selected_lines = (): SelectedLinesSections => {
// Divide all lines of text (separated by `\n`) into those entirely or // Divide all lines of text (separated by `\n`) into those entirely or
// partially selected, and those before and after these selected lines. // partially selected, and those before and after these selected lines.
const before = text.slice(0, range.start); const before = text.slice(0, range.start);
@ -468,13 +506,13 @@ export function format_text($textarea, type, inserted_content) {
}; };
}; };
const format_list = (type) => { const format_list = (type: string): void => {
let is_marked; let is_marked: (line: string) => boolean;
let mark; let mark: (line: string, i: number) => string;
let strip_marking; let strip_marking: (line: string) => string;
if (type === "bulleted") { if (type === "bulleted") {
is_marked = bulleted_numbered_list_util.is_bulleted; is_marked = bulleted_numbered_list_util.is_bulleted;
mark = (line) => "- " + line; mark = (line: string) => "- " + line;
strip_marking = bulleted_numbered_list_util.strip_bullet; strip_marking = bulleted_numbered_list_util.strip_bullet;
} else { } else {
is_marked = bulleted_numbered_list_util.is_numbered; is_marked = bulleted_numbered_list_util.is_numbered;
@ -536,7 +574,7 @@ export function format_text($textarea, type, inserted_content) {
} }
}; };
const format = (syntax_start, syntax_end = syntax_start) => { const format = (syntax_start: string, syntax_end = syntax_start): void => {
let linebreak_start = ""; let linebreak_start = "";
let linebreak_end = ""; let linebreak_end = "";
if (syntax_start.startsWith("\n")) { if (syntax_start.startsWith("\n")) {
@ -578,7 +616,7 @@ export function format_text($textarea, type, inserted_content) {
wrapSelection(field, syntax_start, syntax_end); wrapSelection(field, syntax_start, syntax_end);
}; };
const format_spoiler = () => { const format_spoiler = (): void => {
const spoiler_syntax_start = "```spoiler \n"; const spoiler_syntax_start = "```spoiler \n";
let spoiler_syntax_start_without_break = "```spoiler "; let spoiler_syntax_start_without_break = "```spoiler ";
let spoiler_syntax_end = "\n```"; let spoiler_syntax_end = "\n```";
@ -651,7 +689,7 @@ export function format_text($textarea, type, inserted_content) {
return; return;
} }
const is_inner_content_selected = () => const is_inner_content_selected = (): boolean =>
range.start >= spoiler_syntax_start.length && range.start >= spoiler_syntax_start.length &&
text.length - range.end >= spoiler_syntax_end.length && text.length - range.end >= spoiler_syntax_end.length &&
text.slice(range.end, range.end + spoiler_syntax_end.length) === spoiler_syntax_end && text.slice(range.end, range.end + spoiler_syntax_end.length) === spoiler_syntax_end &&
@ -681,7 +719,7 @@ export function format_text($textarea, type, inserted_content) {
return; return;
} }
const is_header_selected = () => const is_header_selected = (): boolean =>
range.start >= spoiler_syntax_start_without_break.length && range.start >= spoiler_syntax_start_without_break.length &&
text.slice(range.start - spoiler_syntax_start_without_break.length, range.start) === text.slice(range.start - spoiler_syntax_start_without_break.length, range.start) ===
spoiler_syntax_start_without_break && spoiler_syntax_start_without_break &&
@ -726,18 +764,18 @@ export function format_text($textarea, type, inserted_content) {
// Links have to be formatted differently because formatting is not only // Links have to be formatted differently because formatting is not only
// at the beginning and end of the text, but also in the middle // at the beginning and end of the text, but also in the middle
// Therefore more checks are necessary if selected text is already formatted // Therefore more checks are necessary if selected text is already formatted
const format_link = () => { const format_link = (): void => {
const link_syntax_start = "["; const link_syntax_start = "[";
const link_syntax_end = "](url)"; const link_syntax_end = "](url)";
const space_between_description_and_url = (descr, url) => { const space_between_description_and_url = (descr: string, url: string): string => {
if (descr === "" || url === "" || url === "url") { if (descr === "" || url === "" || url === "url") {
return ""; return "";
} }
return " "; return " ";
}; };
const url_to_retain = (url) => { const url_to_retain = (url: string): string => {
if (url === "" || url === "url") { if (url === "" || url === "url") {
return ""; return "";
} }
@ -747,7 +785,7 @@ export function format_text($textarea, type, inserted_content) {
// Captures: // Captures:
// [<description>](<url>) // [<description>](<url>)
// with just <url> selected // with just <url> selected
const is_selection_url = () => const is_selection_url = (): boolean =>
range.start >= "[](".length && range.start >= "[](".length &&
text.length - range.end >= ")".length && text.length - range.end >= ")".length &&
text.slice(range.start - 2, range.start) === "](" && text.slice(range.start - 2, range.start) === "](" &&
@ -778,7 +816,7 @@ export function format_text($textarea, type, inserted_content) {
// Captures: // Captures:
// [<description>](<url>) // [<description>](<url>)
// with just <description> selected // with just <description> selected
const is_selection_description_of_link = () => const is_selection_description_of_link = (): boolean =>
range.start >= "[".length && range.start >= "[".length &&
text.length - range.end >= "]()".length && text.length - range.end >= "]()".length &&
text.slice(range.start - 1, range.start) === "[" && text.slice(range.start - 1, range.start) === "[" &&
@ -805,7 +843,7 @@ export function format_text($textarea, type, inserted_content) {
// Captures: // Captures:
// [<description>](<url>) // [<description>](<url>)
// with [<description>](<url>) selected // with [<description>](<url>) selected
const is_selection_link = () => const is_selection_link = (): boolean =>
range.length >= "[]()".length && range.length >= "[]()".length &&
text[range.start] === "[" && text[range.start] === "[" &&
text[range.end - 1] === ")" && text[range.end - 1] === ")" &&
@ -1025,14 +1063,14 @@ export function format_text($textarea, type, inserted_content) {
/* TODO: This functions don't belong in this module, as they have /* TODO: This functions don't belong in this module, as they have
* nothing to do with the compose textarea. */ * nothing to do with the compose textarea. */
export function hide_compose_spinner() { export function hide_compose_spinner(): void {
compose_spinner_visible = false; compose_spinner_visible = false;
$(".compose-submit-button .loader").hide(); $(".compose-submit-button .loader").hide();
$(".compose-submit-button .zulip-icon-send").show(); $(".compose-submit-button .zulip-icon-send").show();
$(".compose-submit-button").removeClass("disable-btn"); $(".compose-submit-button").removeClass("disable-btn");
} }
export function show_compose_spinner() { export function show_compose_spinner(): void {
compose_spinner_visible = true; compose_spinner_visible = true;
// Always use white spinner. // Always use white spinner.
loading.show_button_spinner($(".compose-submit-button .loader"), true); loading.show_button_spinner($(".compose-submit-button .loader"), true);
@ -1040,7 +1078,7 @@ export function show_compose_spinner() {
$(".compose-submit-button").addClass("disable-btn"); $(".compose-submit-button").addClass("disable-btn");
} }
export function get_compose_click_target(e) { export function get_compose_click_target(e: JQuery.ClickEvent): Element {
const compose_control_buttons_popover = popover_menus.get_compose_control_buttons_popover(); const compose_control_buttons_popover = popover_menus.get_compose_control_buttons_popover();
if ( if (
compose_control_buttons_popover && compose_control_buttons_popover &&