diff --git a/web/src/compose_ui.js b/web/src/compose_ui.js index 2093e2fa06..809aa37627 100644 --- a/web/src/compose_ui.js +++ b/web/src/compose_ui.js @@ -358,7 +358,7 @@ export function format_text($textarea, type, inserted_content) { // where we want to especially preserve any selected new line character // before the selected text, as it is conventionally depicted with a highlight // at the end of the previous line, which we would like to format. - const TRIM_ONLY_END_TYPES = ["bulleted"]; + const TRIM_ONLY_END_TYPES = ["bulleted", "numbered"]; let start_trim_length; if (TRIM_ONLY_END_TYPES.includes(type)) { @@ -431,6 +431,74 @@ export function format_text($textarea, type, inserted_content) { }; }; + const format_list = (type) => { + let is_marked; + let mark; + let strip_marking; + if (type === "bulleted") { + is_marked = bulleted_numbered_list_util.is_bulleted; + mark = (line) => "- " + line; + strip_marking = bulleted_numbered_list_util.strip_bullet; + } else { + is_marked = bulleted_numbered_list_util.is_numbered; + mark = (line, i) => i + 1 + ". " + line; + strip_marking = bulleted_numbered_list_util.strip_numbering; + } + // We toggle complete lines even when they are partially selected (and just selecting the + // newline character after a line counts as partial selection too). + const sections = section_off_selected_lines(); + let {before_lines, selected_lines, after_lines} = sections; + const {separating_new_line_before, separating_new_line_after} = sections; + // If there is even a single unmarked line selected, we mark all. + const should_mark = selected_lines.split("\n").some((line) => !is_marked(line)); + if (should_mark) { + selected_lines = selected_lines + .split("\n") + .map((line, i) => mark(line, i)) + .join("\n"); + // We always ensure a blank line before and after the list, as we want + // a clean separation between the list and the rest of the text, especially + // when the markdown is rendered. + + // Add blank line between text before and list if not already present. + if (before_lines.length && before_lines.at(-1) !== "\n") { + before_lines += "\n"; + } + // Add blank line between list and rest of text if not already present. + if (after_lines.length && after_lines.at(0) !== "\n") { + after_lines = "\n" + after_lines; + } + } else { + // Unmark all marked lines by removing the marking syntax characters. + selected_lines = selected_lines + .split("\n") + .map((line) => strip_marking(line)) + .join("\n"); + } + // Restore the separating newlines that were removed by section_off_selected_lines. + if (separating_new_line_before) { + before_lines += "\n"; + } + if (separating_new_line_after) { + after_lines = "\n" + after_lines; + } + text = before_lines + selected_lines + after_lines; + set(field, text); + // If no text was selected, that is, marking was added to the line with the + // cursor, nothing will be selected and the cursor will remain as it was. + if (selected_text === "") { + field.setSelectionRange( + before_lines.length + selected_lines.length, + before_lines.length + selected_lines.length, + ); + } else { + field.setSelectionRange( + before_lines.length, + before_lines.length + selected_lines.length, + ); + } + }; + switch (type) { case "bold": // Ctrl + B: Toggle bold syntax on selection. @@ -551,64 +619,10 @@ export function format_text($textarea, type, inserted_content) { wrapSelection(field, italic_syntax); break; - case "bulleted": { - // We toggle complete lines even when they are partially selected (and just selecting the - // newline character after a line counts as partial selection too). - const sections = section_off_selected_lines(); - let {before_lines, selected_lines, after_lines} = sections; - const {separating_new_line_before, separating_new_line_after} = sections; - // If there is even a single unbulleted line selected, we bullet all. - const should_bullet = selected_lines - .split("\n") - .some((line) => !bulleted_numbered_list_util.is_bulleted(line)); - if (should_bullet) { - selected_lines = selected_lines - .split("\n") - .map((line) => "- " + line) - .join("\n"); - // We always ensure a blank line before and after the list, as we want - // a clean separation between the list and the rest of the text, especially - // when the markdown is rendered. - - // Add blank line between text before and list if not already present. - if (before_lines.length && before_lines.at(-1) !== "\n") { - before_lines += "\n"; - } - // Add blank line between list and rest of text if not already present. - if (after_lines.length && after_lines.at(0) !== "\n") { - after_lines = "\n" + after_lines; - } - } else { - // Unbullet all bulleted lines by removing the 2 bullet syntax characters. - selected_lines = selected_lines - .split("\n") - .map((line) => bulleted_numbered_list_util.strip_bullet(line)) - .join("\n"); - } - // Restore the separating newlines that were removed by section_off_selected_lines. - if (separating_new_line_before) { - before_lines += "\n"; - } - if (separating_new_line_after) { - after_lines = "\n" + after_lines; - } - text = before_lines + selected_lines + after_lines; - set(field, text); - // If no text was selected, that is, bullet was added to the line with the - // cursor, nothing will be selected and the cursor will remain as it was. - if (selected_text === "") { - field.setSelectionRange( - before_lines.length + selected_lines.length, - before_lines.length + selected_lines.length, - ); - } else { - field.setSelectionRange( - before_lines.length, - before_lines.length + selected_lines.length, - ); - } + case "bulleted": + case "numbered": + format_list(type); break; - } case "link": { // Ctrl + L: Insert a link to selected text wrapSelection(field, "[", "](url)"); diff --git a/web/templates/compose_control_buttons_in_popover.hbs b/web/templates/compose_control_buttons_in_popover.hbs index eb7ec7e94e..c59720651a 100644 --- a/web/templates/compose_control_buttons_in_popover.hbs +++ b/web/templates/compose_control_buttons_in_popover.hbs @@ -2,6 +2,7 @@ +
|
diff --git a/web/tests/compose_ui.test.js b/web/tests/compose_ui.test.js index 71e9e08fc0..05d22ef387 100644 --- a/web/tests/compose_ui.test.js +++ b/web/tests/compose_ui.test.js @@ -691,6 +691,57 @@ run_test("format_text", ({override}) => { compose_ui.format_text($textarea, "bulleted"); assert.equal(set_text, "first_item\nsecond_item"); assert.equal(wrap_selection_called, false); + + // Test numbered list toggling on + reset_state(); + init_textarea("first_item\nsecond_item", { + start: 0, + end: 22, + text: "first_item\nsecond_item", + length: 22, + }); + compose_ui.format_text($textarea, "numbered"); + assert.equal(set_text, "1. first_item\n2. second_item"); + assert.equal(wrap_selection_called, false); + + // Test numbered list toggling off + reset_state(); + init_textarea("1. first_item\n2. second_item", { + start: 0, + end: 28, + text: "1. first_item\n2. second_item", + length: 28, + }); + compose_ui.format_text($textarea, "numbered"); + assert.equal(set_text, "first_item\nsecond_item"); + assert.equal(wrap_selection_called, false); + + // Test numbered list toggling with newline at end + reset_state(); + init_textarea("first_item\nsecond_item\n", { + start: 0, + end: 23, + text: "first_item\nsecond_item\n", + length: 23, + }); + compose_ui.format_text($textarea, "numbered"); + assert.equal(set_text, "1. first_item\n2. second_item\n"); + assert.equal(wrap_selection_called, false); + + // Test numbered list toggling on with partially selected lines + reset_state(); + init_textarea("before_first\nfirst_item\nsecond_item\nafter_last", { + start: 15, + end: 33, + text: "rst_item\nsecond_it", + length: 18, + }); + compose_ui.format_text($textarea, "numbered"); + // Notice the blank lines inserted right before and after the list to visually demarcate it. + // Had the blank line after `second_item` not been inserted, `after_last` would have been + // (wrongly) indented as part of the list's last item too. + assert.equal(set_text, "before_first\n\n1. first_item\n2. second_item\n\nafter_last"); + assert.equal(wrap_selection_called, false); }); run_test("markdown_shortcuts", ({override_rewire}) => {