mirror of https://github.com/zulip/zulip.git
compose_ui: Convert module to typescript.
This commit is contained in:
parent
91bdcc9596
commit
264a9a1057
|
@ -76,7 +76,7 @@ EXEMPT_FILES = make_set(
|
|||
"web/src/compose_state.ts",
|
||||
"web/src/compose_textarea.ts",
|
||||
"web/src/compose_tooltips.js",
|
||||
"web/src/compose_ui.js",
|
||||
"web/src/compose_ui.ts",
|
||||
"web/src/compose_validate.js",
|
||||
"web/src/composebox_typeahead.js",
|
||||
"web/src/condense.js",
|
||||
|
|
|
@ -17,20 +17,36 @@ import * as stream_data from "./stream_data";
|
|||
import * as user_status from "./user_status";
|
||||
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 shift_pressed = false; // true or false
|
||||
let full_size_status = false; // true or false
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export function is_full_size() {
|
||||
export function is_full_size(): boolean {
|
||||
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
|
||||
// in the text area to autosize.
|
||||
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);
|
||||
// Blurring and refocusing ensures the cursor / selection is in view.
|
||||
$textarea.trigger("blur");
|
||||
|
@ -46,7 +65,7 @@ export function insert_and_scroll_into_view(content, $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
|
||||
// and "Start new conversation" button clicked.
|
||||
if (msg_type === "stream" && opts.stream_id && !opts.topic) {
|
||||
|
@ -70,24 +89,24 @@ function get_focus_area(msg_type, opts) {
|
|||
// Export for testing
|
||||
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
|
||||
// message type to set the focus in the first empty input in the
|
||||
// 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);
|
||||
$(focus_area).trigger("focus");
|
||||
}
|
||||
}
|
||||
|
||||
export function smart_insert_inline($textarea, syntax) {
|
||||
function is_space(c) {
|
||||
export function smart_insert_inline($textarea: JQuery<HTMLTextAreaElement>, syntax: string): void {
|
||||
function is_space(c: string): boolean {
|
||||
return c === " " || c === "\t" || c === "\n";
|
||||
}
|
||||
|
||||
const pos = $textarea.caret();
|
||||
const before_str = $textarea.val().slice(0, pos);
|
||||
const after_str = $textarea.val().slice(pos);
|
||||
const before_str = $textarea.val()!.slice(0, pos);
|
||||
const after_str = $textarea.val()!.slice(pos);
|
||||
|
||||
if (
|
||||
pos > 0 &&
|
||||
|
@ -114,10 +133,14 @@ export function smart_insert_inline($textarea, syntax) {
|
|||
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 before_str = $textarea.val().slice(0, pos);
|
||||
const after_str = $textarea.val().slice(pos);
|
||||
const before_str = $textarea.val()!.slice(0, pos);
|
||||
const after_str = $textarea.val()!.slice(pos);
|
||||
|
||||
if (pos > 0) {
|
||||
// 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(
|
||||
syntax,
|
||||
$textarea = $("textarea#compose-textarea"),
|
||||
syntax: string,
|
||||
$textarea = $<HTMLTextAreaElement>("textarea#compose-textarea"),
|
||||
mode = "inline",
|
||||
padding_newlines,
|
||||
) {
|
||||
padding_newlines: number,
|
||||
): void {
|
||||
// Generic helper for inserting syntax into the main compose box
|
||||
// where the cursor was and focusing the area. Mostly a thin
|
||||
// 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
|
||||
// logical position of the cursor after the replacement
|
||||
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
|
||||
// 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;
|
||||
}
|
||||
|
||||
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
|
||||
// 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_parts = recipient_list.map((recipient) => {
|
||||
const user = people.get_by_email(recipient);
|
||||
if (people.should_add_guest_user_indicator(user.user_id)) {
|
||||
return $t({defaultMessage: "{name} (guest)"}, {name: user.full_name});
|
||||
if (people.should_add_guest_user_indicator(user!.user_id)) {
|
||||
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");
|
||||
|
||||
if (recipient_list.length === 1) {
|
||||
// If it's a single user, display status text if available
|
||||
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) {
|
||||
return $t(
|
||||
{defaultMessage: "Message {recipient_name} ({recipient_status})"},
|
||||
|
@ -277,7 +306,7 @@ export function compute_placeholder_text(opts) {
|
|||
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) {
|
||||
// As `#compose` has `position: fixed` property, we cannot
|
||||
// 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);
|
||||
|
||||
// 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");
|
||||
}
|
||||
|
||||
export function make_compose_box_original_size() {
|
||||
export function make_compose_box_original_size(): void {
|
||||
set_full_size(false);
|
||||
|
||||
$("#compose").removeClass("compose-fullscreen");
|
||||
|
@ -326,7 +355,10 @@ export function make_compose_box_original_size() {
|
|||
$("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") {
|
||||
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") {
|
||||
shift_pressed = false;
|
||||
}
|
||||
|
@ -362,11 +397,11 @@ export function handle_keyup(_event, $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
|
||||
// a code block on rendering the textarea content as markdown.
|
||||
const cursor_position = $textarea.caret();
|
||||
const current_content = $textarea.val();
|
||||
const current_content = $textarea.val()!;
|
||||
|
||||
let unique_insert = "UNIQUEINSERT:" + Math.random();
|
||||
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_html = new DOMParser().parseFromString(rendered_content, "text/html");
|
||||
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 bold_syntax = "**";
|
||||
const bold_and_italic_syntax = "***";
|
||||
let is_selected_text_italic = false;
|
||||
let is_inner_text_italic = false;
|
||||
const field = $textarea.get(0);
|
||||
const field = $textarea.get(0)!;
|
||||
let range = $textarea.range();
|
||||
let text = $textarea.val();
|
||||
|
||||
let text = $textarea.val()!;
|
||||
// Remove new line and space around selected text, except list formatting,
|
||||
// where we want to especially preserve any selected new line character
|
||||
// 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;
|
||||
|
||||
// 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 &&
|
||||
text.length - range.end >= syntax_end.length &&
|
||||
text.slice(range.start - syntax_start.length, range.start) === syntax_start &&
|
||||
text.slice(range.end, range.end + syntax_end.length) === syntax_end;
|
||||
|
||||
// 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 &&
|
||||
selected_text.startsWith(syntax_start) &&
|
||||
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
|
||||
// partially selected, and those before and after these selected lines.
|
||||
const before = text.slice(0, range.start);
|
||||
|
@ -468,13 +506,13 @@ export function format_text($textarea, type, inserted_content) {
|
|||
};
|
||||
};
|
||||
|
||||
const format_list = (type) => {
|
||||
let is_marked;
|
||||
let mark;
|
||||
let strip_marking;
|
||||
const format_list = (type: string): void => {
|
||||
let is_marked: (line: string) => boolean;
|
||||
let mark: (line: string, i: number) => string;
|
||||
let strip_marking: (line: string) => string;
|
||||
if (type === "bulleted") {
|
||||
is_marked = bulleted_numbered_list_util.is_bulleted;
|
||||
mark = (line) => "- " + line;
|
||||
mark = (line: string) => "- " + line;
|
||||
strip_marking = bulleted_numbered_list_util.strip_bullet;
|
||||
} else {
|
||||
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_end = "";
|
||||
if (syntax_start.startsWith("\n")) {
|
||||
|
@ -578,7 +616,7 @@ export function format_text($textarea, type, inserted_content) {
|
|||
wrapSelection(field, syntax_start, syntax_end);
|
||||
};
|
||||
|
||||
const format_spoiler = () => {
|
||||
const format_spoiler = (): void => {
|
||||
const spoiler_syntax_start = "```spoiler \n";
|
||||
let spoiler_syntax_start_without_break = "```spoiler ";
|
||||
let spoiler_syntax_end = "\n```";
|
||||
|
@ -651,7 +689,7 @@ export function format_text($textarea, type, inserted_content) {
|
|||
return;
|
||||
}
|
||||
|
||||
const is_inner_content_selected = () =>
|
||||
const is_inner_content_selected = (): boolean =>
|
||||
range.start >= spoiler_syntax_start.length &&
|
||||
text.length - range.end >= spoiler_syntax_end.length &&
|
||||
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;
|
||||
}
|
||||
|
||||
const is_header_selected = () =>
|
||||
const is_header_selected = (): boolean =>
|
||||
range.start >= spoiler_syntax_start_without_break.length &&
|
||||
text.slice(range.start - spoiler_syntax_start_without_break.length, range.start) ===
|
||||
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
|
||||
// at the beginning and end of the text, but also in the middle
|
||||
// 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_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") {
|
||||
return "";
|
||||
}
|
||||
return " ";
|
||||
};
|
||||
|
||||
const url_to_retain = (url) => {
|
||||
const url_to_retain = (url: string): string => {
|
||||
if (url === "" || url === "url") {
|
||||
return "";
|
||||
}
|
||||
|
@ -747,7 +785,7 @@ export function format_text($textarea, type, inserted_content) {
|
|||
// Captures:
|
||||
// [<description>](<url>)
|
||||
// with just <url> selected
|
||||
const is_selection_url = () =>
|
||||
const is_selection_url = (): boolean =>
|
||||
range.start >= "[](".length &&
|
||||
text.length - range.end >= ")".length &&
|
||||
text.slice(range.start - 2, range.start) === "](" &&
|
||||
|
@ -778,7 +816,7 @@ export function format_text($textarea, type, inserted_content) {
|
|||
// Captures:
|
||||
// [<description>](<url>)
|
||||
// with just <description> selected
|
||||
const is_selection_description_of_link = () =>
|
||||
const is_selection_description_of_link = (): boolean =>
|
||||
range.start >= "[".length &&
|
||||
text.length - range.end >= "]()".length &&
|
||||
text.slice(range.start - 1, range.start) === "[" &&
|
||||
|
@ -805,7 +843,7 @@ export function format_text($textarea, type, inserted_content) {
|
|||
// Captures:
|
||||
// [<description>](<url>)
|
||||
// with [<description>](<url>) selected
|
||||
const is_selection_link = () =>
|
||||
const is_selection_link = (): boolean =>
|
||||
range.length >= "[]()".length &&
|
||||
text[range.start] === "[" &&
|
||||
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
|
||||
* nothing to do with the compose textarea. */
|
||||
export function hide_compose_spinner() {
|
||||
export function hide_compose_spinner(): void {
|
||||
compose_spinner_visible = false;
|
||||
$(".compose-submit-button .loader").hide();
|
||||
$(".compose-submit-button .zulip-icon-send").show();
|
||||
$(".compose-submit-button").removeClass("disable-btn");
|
||||
}
|
||||
|
||||
export function show_compose_spinner() {
|
||||
export function show_compose_spinner(): void {
|
||||
compose_spinner_visible = true;
|
||||
// Always use white spinner.
|
||||
loading.show_button_spinner($(".compose-submit-button .loader"), true);
|
||||
|
@ -1040,7 +1078,7 @@ export function show_compose_spinner() {
|
|||
$(".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();
|
||||
if (
|
||||
compose_control_buttons_popover &&
|
Loading…
Reference in New Issue