diff --git a/web/src/compose_actions.js b/web/src/compose_actions.js index b516af5e59..ec74a5aa98 100644 --- a/web/src/compose_actions.js +++ b/web/src/compose_actions.js @@ -536,25 +536,17 @@ export function quote_and_reply(opts) { const message = message_lists.current.selected_message(); const quoting_placeholder = $t({defaultMessage: "[Quoting…]"}); - if (compose_state.has_message_content()) { - // The user already started typing a message, - // so we won't re-open the compose box. + if (!compose_state.has_message_content()) { + // The user has not started typing a message, + // so we will re-open the compose box. // (If you did re-open the compose box, you // are prone to glitches where you select the // text, plus it's a complicated codepath that // can have other unintended consequences.) - - if ($textarea.caret() !== 0) { - // Insert a newline before quoted message if there is - // already some content in the compose box and quoted - // message is not being inserted at the beginning. - $textarea.caret("\n"); - } - } else { respond_to_message(opts); } - compose_ui.insert_syntax_and_focus(quoting_placeholder + "\n", $textarea); + compose_ui.insert_syntax_and_focus(quoting_placeholder, $textarea, "block"); function replace_content(message) { // Final message looks like: diff --git a/web/src/compose_ui.js b/web/src/compose_ui.js index 8e7ca68cb5..14e84fe19a 100644 --- a/web/src/compose_ui.js +++ b/web/src/compose_ui.js @@ -30,7 +30,7 @@ export function autosize_textarea($textarea) { } } -export function smart_insert($textarea, syntax) { +export function smart_insert_inline($textarea, syntax) { function is_space(c) { return c === " " || c === "\t" || c === "\n"; } @@ -68,11 +68,69 @@ export function smart_insert($textarea, syntax) { autosize_textarea($textarea); } -export function insert_syntax_and_focus(syntax, $textarea = $("#compose-textarea")) { +export function smart_insert_block($textarea, syntax) { + const pos = $textarea.caret(); + 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 + // already some content in the compose box and the content + // block is not being inserted at the beginning, such + // that there are at least 2 new lines between the content + // and start of the content block. + let new_lines_before_count = 0; + let current_pos = pos - 1; + while ( + current_pos >= 0 && + before_str.charAt(current_pos) === "\n" && + new_lines_before_count < 2 + ) { + // count up to 2 new lines before cursor + current_pos -= 1; + new_lines_before_count += 1; + } + const new_lines_needed_before_count = 2 - new_lines_before_count; + syntax = "\n".repeat(new_lines_needed_before_count) + syntax; + } + + let new_lines_after_count = 0; + let current_pos = 0; + while ( + current_pos < after_str.length && + after_str.charAt(current_pos) === "\n" && + new_lines_after_count < 2 + ) { + // count up to 2 new lines after cursor + current_pos += 1; + new_lines_after_count += 1; + } + // Insert newline/s after the content block, such that there + // are at least 2 new lines between the content block and + // the content after the cursor, if any. + const new_lines_needed_after_count = 2 - new_lines_after_count; + syntax = syntax + "\n".repeat(new_lines_needed_after_count); + + // text-field-edit ensures `$textarea` is focused before inserting + // the new syntax. + insert($textarea[0], syntax); + + autosize_textarea($textarea); +} + +export function insert_syntax_and_focus( + syntax, + $textarea = $("#compose-textarea"), + mode = "inline", +) { // 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. - smart_insert($textarea, syntax); + // wrapper around smart_insert_inline and smart_inline_block. + if (mode === "inline") { + smart_insert_inline($textarea, syntax); + } else if (mode === "block") { + smart_insert_block($textarea, syntax); + } } export function replace_syntax(old_syntax, new_syntax, $textarea = $("#compose-textarea")) { diff --git a/web/tests/compose_actions.test.js b/web/tests/compose_actions.test.js index 234734bf78..d4fd72c229 100644 --- a/web/tests/compose_actions.test.js +++ b/web/tests/compose_actions.test.js @@ -359,8 +359,9 @@ test("quote_and_reply", ({disallow, override, override_rewire}) => { override(message_lists.current, "selected_id", () => 100); - override(compose_ui, "insert_syntax_and_focus", (syntax) => { - assert.equal(syntax, "translated: [Quoting…]\n"); + override(compose_ui, "insert_syntax_and_focus", (syntax, $textarea, mode) => { + assert.equal(syntax, "translated: [Quoting…]"); + assert.equal(mode, "block"); }); const opts = { diff --git a/web/tests/compose_ui.test.js b/web/tests/compose_ui.test.js index 398ca8018b..2992e77302 100644 --- a/web/tests/compose_ui.test.js +++ b/web/tests/compose_ui.test.js @@ -120,34 +120,34 @@ run_test("smart_insert", ({override}) => { }); } override_with_expected_syntax(" :smile: "); - compose_ui.smart_insert($textbox, ":smile:"); + compose_ui.smart_insert_inline($textbox, ":smile:"); override_with_expected_syntax(" :airplane: "); - compose_ui.smart_insert($textbox, ":airplane:"); + compose_ui.smart_insert_inline($textbox, ":airplane:"); $textbox.caret(0); override_with_expected_syntax(":octopus: "); - compose_ui.smart_insert($textbox, ":octopus:"); + compose_ui.smart_insert_inline($textbox, ":octopus:"); $textbox.caret($textbox.val().length); override_with_expected_syntax(" :heart: "); - compose_ui.smart_insert($textbox, ":heart:"); + compose_ui.smart_insert_inline($textbox, ":heart:"); // Test handling of spaces for ```quote $textbox = make_textbox(""); $textbox.caret(0); - override_with_expected_syntax("```quote\nquoted message\n```\n"); - compose_ui.smart_insert($textbox, "```quote\nquoted message\n```\n"); + override_with_expected_syntax("```quote\nquoted message\n```\n\n"); + compose_ui.smart_insert_block($textbox, "```quote\nquoted message\n```"); $textbox = make_textbox(""); $textbox.caret(0); - override_with_expected_syntax("translated: [Quoting…]\n"); - compose_ui.smart_insert($textbox, "translated: [Quoting…]\n"); + override_with_expected_syntax("translated: [Quoting…]\n\n"); + compose_ui.smart_insert_block($textbox, "translated: [Quoting…]"); $textbox = make_textbox("abc"); $textbox.caret(3); - override_with_expected_syntax(" test with space "); - compose_ui.smart_insert($textbox, " test with space"); + override_with_expected_syntax("\n\n test with space\n\n"); + compose_ui.smart_insert_block($textbox, " test with space"); // Note that we don't have any special logic for strings that are // already surrounded by spaces, since we are usually inserting things @@ -281,23 +281,28 @@ run_test("quote_and_reply", ({override, override_rewire}) => { textarea_caret_pos = arg; return this; } - /* istanbul ignore if */ - if (typeof arg !== "string") { - console.info(arg); - throw new Error("We expected the actual code to pass in a string."); + + /* This next block of mocking code is currently unused, but + is preserved, since it may be useful in the future. */ + /* istanbul ignore next */ + { + if (typeof arg !== "string") { + console.info(arg); + throw new Error("We expected the actual code to pass in a string."); + } + + const before = textarea_val.slice(0, textarea_caret_pos); + const after = textarea_val.slice(textarea_caret_pos); + + textarea_val = before + arg + after; + textarea_caret_pos += arg.length; + return this; } - - const before = textarea_val.slice(0, textarea_caret_pos); - const after = textarea_val.slice(textarea_caret_pos); - - textarea_val = before + arg + after; - textarea_caret_pos += arg.length; - return this; }; $("#compose-textarea")[0] = "compose-textarea"; override(text_field_edit, "insert", (elt, syntax) => { assert.equal(elt, "compose-textarea"); - assert.equal(syntax, "translated: [Quoting…]\n"); + assert.equal(syntax, "\n\ntranslated: [Quoting…]\n\n"); }); function set_compose_content_with_caret(content) { @@ -343,7 +348,11 @@ run_test("quote_and_reply", ({override, override_rewire}) => { reset_test_state(); // If the caret is initially positioned at 0, it should not - // add a newline before the quoted message. + // add newlines before the quoted message. + override(text_field_edit, "insert", (elt, syntax) => { + assert.equal(elt, "compose-textarea"); + assert.equal(syntax, "translated: [Quoting…]\n\n"); + }); set_compose_content_with_caret("%hello there"); compose_actions.quote_and_reply(); @@ -387,6 +396,40 @@ run_test("quote_and_reply", ({override, override_rewire}) => { success_function({ raw_content: quote_text, }); + + reset_test_state(); + + // When there is already 1 newline before and after the caret, + // only 1 newline is added before and after the quoted message. + override(text_field_edit, "insert", (elt, syntax) => { + assert.equal(elt, "compose-textarea"); + assert.equal(syntax, "\ntranslated: [Quoting…]\n"); + }); + set_compose_content_with_caret("1st line\n%\n2nd line"); + compose_actions.quote_and_reply(); + + quote_text = "Testing with caret on a new line between 2 lines of text."; + override_with_quote_text(quote_text); + success_function({ + raw_content: quote_text, + }); + + reset_test_state(); + + // When there are many (>=2) newlines before and after the caret, + // no newline is added before or after the quoted message. + override(text_field_edit, "insert", (elt, syntax) => { + assert.equal(elt, "compose-textarea"); + assert.equal(syntax, "translated: [Quoting…]"); + }); + set_compose_content_with_caret("lots of\n\n\n\n%\n\n\nnewlines"); + compose_actions.quote_and_reply(); + + quote_text = "Testing with caret on a new line between many empty newlines."; + override_with_quote_text(quote_text); + success_function({ + raw_content: quote_text, + }); }); run_test("set_compose_box_top", () => {