mirror of https://github.com/zulip/zulip.git
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:
parent
18b36e5b8c
commit
58172fe21a
|
@ -460,6 +460,163 @@ run_test("test_compose_height_changes", ({override}) => {
|
|||
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}) => {
|
||||
let format_text_type;
|
||||
override(compose_ui, "format_text", (textarea, type) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import autosize from "autosize";
|
||||
import $ from "jquery";
|
||||
import {wrapSelection} from "text-field-edit";
|
||||
import {set, wrapSelection} from "text-field-edit";
|
||||
|
||||
import * as common from "./common";
|
||||
import {$t} from "./i18n";
|
||||
|
@ -221,17 +221,149 @@ export function handle_keyup(event, textarea) {
|
|||
}
|
||||
|
||||
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 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) {
|
||||
case "bold":
|
||||
// Ctrl + B: Convert selected text to bold text
|
||||
wrapSelection(field, "**");
|
||||
// Ctrl + B: Toggle bold syntax on selection.
|
||||
|
||||
// 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;
|
||||
case "italic":
|
||||
// Ctrl + I: Convert selected text to italic text
|
||||
wrapSelection(field, "*");
|
||||
// Ctrl + I: Toggle italic syntax on selection. This is
|
||||
// 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;
|
||||
case "link": {
|
||||
// Ctrl + L: Insert a link to selected text
|
||||
|
|
Loading…
Reference in New Issue