compose: Allow user to undo formatting.

For text that already has the formatting that the user is
trying to apply, we undo the formatting. This gives a nice
experience of applying and removing the formatting from text
on the same button press.
This commit is contained in:
Aman Agrawal 2021-09-04 21:17:27 +05:30 committed by Tim Abbott
parent 18b36e5b8c
commit 58172fe21a
2 changed files with 294 additions and 5 deletions

View File

@ -460,6 +460,163 @@ run_test("test_compose_height_changes", ({override}) => {
assert.ok(!compose_box_top_set); assert.ok(!compose_box_top_set);
}); });
run_test("format_text", () => {
let set_text = "";
let wrap_selection_called = false;
let wrap_syntax = "";
mock_esm("text-field-edit", {
set: (field, text) => {
set_text = text;
},
wrapSelection: (field, syntax) => {
wrap_selection_called = true;
wrap_syntax = syntax;
},
});
function reset_state() {
set_text = "";
wrap_selection_called = false;
wrap_syntax = "";
}
const textarea = $("#compose-textarea");
textarea.get = () => ({
setSelectionRange: () => {},
});
function init_textarea(val, range) {
textarea.val = () => val;
textarea.range = () => range;
}
const italic_syntax = "*";
const bold_syntax = "**";
// Bold selected text
reset_state();
init_textarea("abc", {
start: 0,
end: 3,
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "bold");
assert.equal(set_text, "");
assert.equal(wrap_selection_called, true);
assert.equal(wrap_syntax, bold_syntax);
// Undo bold selected text, syntax not selected
reset_state();
init_textarea("**abc**", {
start: 2,
end: 5,
text: "abc",
length: 7,
});
compose_ui.format_text(textarea, "bold");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
// Undo bold selected text, syntax selected
reset_state();
init_textarea("**abc**", {
start: 0,
end: 7,
text: "**abc**",
length: 7,
});
compose_ui.format_text(textarea, "bold");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
// Italic selected text
reset_state();
init_textarea("abc", {
start: 0,
end: 3,
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "italic");
assert.equal(set_text, "");
assert.equal(wrap_selection_called, true);
assert.equal(wrap_syntax, italic_syntax);
// Undo italic selected text, syntax not selected
reset_state();
init_textarea("*abc*", {
start: 1,
end: 4,
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "italic");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
// Undo italic selected text, syntax selected
reset_state();
init_textarea("*abc*", {
start: 0,
end: 5,
text: "*abc*",
length: 5,
});
compose_ui.format_text(textarea, "italic");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
// Undo bold selected text, text is both italic and bold, syntax not selected.
reset_state();
init_textarea("***abc***", {
start: 3,
end: 6,
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "bold");
assert.equal(set_text, "*abc*");
assert.equal(wrap_selection_called, false);
// Undo bold selected text, text is both italic and bold, syntax selected.
reset_state();
init_textarea("***abc***", {
start: 0,
end: 9,
text: "***abc***",
length: 9,
});
compose_ui.format_text(textarea, "bold");
assert.equal(set_text, "*abc*");
assert.equal(wrap_selection_called, false);
// Undo italic selected text, text is both italic and bold, syntax not selected.
reset_state();
init_textarea("***abc***", {
start: 3,
end: 6,
text: "abc",
length: 3,
});
compose_ui.format_text(textarea, "italic");
assert.equal(set_text, "**abc**");
assert.equal(wrap_selection_called, false);
// Undo italic selected text, text is both italic and bold, syntax selected.
reset_state();
init_textarea("***abc***", {
start: 0,
end: 9,
text: "***abc***",
length: 9,
});
compose_ui.format_text(textarea, "italic");
assert.equal(set_text, "**abc**");
assert.equal(wrap_selection_called, false);
});
run_test("markdown_shortcuts", ({override}) => { run_test("markdown_shortcuts", ({override}) => {
let format_text_type; let format_text_type;
override(compose_ui, "format_text", (textarea, type) => { override(compose_ui, "format_text", (textarea, type) => {

View File

@ -1,6 +1,6 @@
import autosize from "autosize"; import autosize from "autosize";
import $ from "jquery"; import $ from "jquery";
import {wrapSelection} from "text-field-edit"; import {set, wrapSelection} from "text-field-edit";
import * as common from "./common"; import * as common from "./common";
import {$t} from "./i18n"; import {$t} from "./i18n";
@ -221,17 +221,149 @@ export function handle_keyup(event, textarea) {
} }
export function format_text(textarea, type) { export function format_text(textarea, type) {
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);
const range = textarea.range(); const range = textarea.range();
let text = textarea.val();
const selected_text = range.text;
const is_selection_bold = () =>
// First check if there are enough characters before/after selection.
range.start >= bold_syntax.length &&
text.length - range.end >= bold_syntax.length &&
// And then if the characters have bold_syntax around them.
text.slice(range.start - bold_syntax.length, range.start) === bold_syntax &&
text.slice(range.end, range.end + bold_syntax.length) === bold_syntax;
const is_inner_text_bold = () =>
// Check if selected text itself has bold_syntax inside it.
range.length > 4 &&
selected_text.slice(0, bold_syntax.length) === bold_syntax &&
selected_text.slice(-bold_syntax.length) === bold_syntax;
switch (type) { switch (type) {
case "bold": case "bold":
// Ctrl + B: Convert selected text to bold text // Ctrl + B: Toggle bold syntax on selection.
wrapSelection(field, "**");
// If the selection is already surrounded by bold syntax,
// remove it rather than adding another copy.
if (is_selection_bold()) {
// Remove the bold_syntax from text.
text =
text.slice(0, range.start - bold_syntax.length) +
text.slice(range.start, range.end) +
text.slice(range.end + bold_syntax.length);
set(field, text);
field.setSelectionRange(
range.start - bold_syntax.length,
range.end - bold_syntax.length,
);
break;
} else if (is_inner_text_bold()) {
// Remove bold syntax inside the selection, if present.
text =
text.slice(0, range.start) +
text.slice(range.start + bold_syntax.length, range.end - bold_syntax.length) +
text.slice(range.end);
set(field, text);
field.setSelectionRange(range.start, range.end - bold_syntax.length * 2);
break;
}
// Otherwise, we don't have bold syntax, so we add it.
wrapSelection(field, bold_syntax);
break; break;
case "italic": case "italic":
// Ctrl + I: Convert selected text to italic text // Ctrl + I: Toggle italic syntax on selection. This is
wrapSelection(field, "*"); // much more complex than toggling bold syntax, because of
// the following subtle detail: If our selection is
// **foo**, toggling italics should add italics, since in
// fact it's just bold syntax, even though with *foo* and
// ***foo*** should remove italics.
// If the text is already italic, we remove the italic_syntax from text.
if (range.start >= 1 && text.length - range.end >= italic_syntax.length) {
// If text has italic_syntax around it.
const has_italic_syntax =
text.slice(range.start - italic_syntax.length, range.start) === italic_syntax &&
text.slice(range.end, range.end + italic_syntax.length) === italic_syntax;
if (is_selection_bold()) {
// If text has bold_syntax around it.
if (
range.start >= 3 &&
text.length - range.end >= bold_and_italic_syntax.length
) {
// If text is both bold and italic.
const has_bold_and_italic_syntax =
text.slice(range.start - bold_and_italic_syntax.length, range.start) ===
bold_and_italic_syntax &&
text.slice(range.end, range.end + bold_and_italic_syntax.length) ===
bold_and_italic_syntax;
if (has_bold_and_italic_syntax) {
is_selected_text_italic = true;
}
}
} else if (has_italic_syntax) {
// If text is only italic.
is_selected_text_italic = true;
}
}
if (is_selected_text_italic) {
// If text has italic syntax around it, we remove the italic syntax.
text =
text.slice(0, range.start - italic_syntax.length) +
text.slice(range.start, range.end) +
text.slice(range.end + italic_syntax.length);
set(field, text);
field.setSelectionRange(
range.start - italic_syntax.length,
range.end - italic_syntax.length,
);
break;
} else if (
selected_text.length > italic_syntax.length * 2 &&
// If the selected text contains italic syntax
selected_text.slice(0, italic_syntax.length) === italic_syntax &&
selected_text.slice(-italic_syntax.length) === italic_syntax
) {
if (is_inner_text_bold()) {
if (
selected_text.length > bold_and_italic_syntax.length * 2 &&
selected_text.slice(0, bold_and_italic_syntax.length) ===
bold_and_italic_syntax &&
selected_text.slice(-bold_and_italic_syntax.length) ===
bold_and_italic_syntax
) {
// If selected text is both bold and italic.
is_inner_text_italic = true;
}
} else {
// If selected text is only italic.
is_inner_text_italic = true;
}
}
if (is_inner_text_italic) {
// We remove the italic_syntax from within the selected text.
text =
text.slice(0, range.start) +
text.slice(
range.start + italic_syntax.length,
range.end - italic_syntax.length,
) +
text.slice(range.end);
set(field, text);
field.setSelectionRange(range.start, range.end - italic_syntax.length * 2);
break;
}
wrapSelection(field, italic_syntax);
break; break;
case "link": { case "link": {
// Ctrl + L: Insert a link to selected text // Ctrl + L: Insert a link to selected text