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);
|
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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue