compose: Format button for spoilers.

Note that toggling off spoiler formatting works if either all the
content inside, or the header (if it exists) or both are selected.

Co-authored-by: N-Shar-ma <bablinaneh@gmail.com>
This commit is contained in:
Julia Bichler
2022-02-02 16:01:50 +01:00
committed by Tim Abbott
parent df143137ef
commit 2db8563a7e
3 changed files with 280 additions and 0 deletions

View File

@@ -580,6 +580,151 @@ export function format_text($textarea, type, inserted_content) {
wrapSelection(field, syntax_start, syntax_end);
};
const format_spoiler = () => {
let spoiler_syntax_start = "```spoiler \n";
const spoiler_syntax_start_without_break = "```spoiler ";
let spoiler_syntax_end = "\n```";
// For when the entire spoiler block (with no header) is selected.
if (is_inner_text_formatted(spoiler_syntax_start, spoiler_syntax_end)) {
text =
text.slice(0, range.start) +
text.slice(
range.start + spoiler_syntax_start.length,
range.end - spoiler_syntax_end.length,
) +
text.slice(range.end);
if (text.startsWith("\n")) {
text = text.slice(1);
}
set(field, text);
field.setSelectionRange(
range.start,
range.end - spoiler_syntax_start.length - spoiler_syntax_end.length,
);
return;
}
// For when the entire spoiler block (with a header) is selected.
if (is_inner_text_formatted(spoiler_syntax_start_without_break, spoiler_syntax_end)) {
text =
text.slice(0, range.start) +
text.slice(
range.start + spoiler_syntax_start_without_break.length,
range.end - spoiler_syntax_end.length,
) +
text.slice(range.end);
if (text.startsWith("\n")) {
text = text.slice(1);
}
set(field, text);
field.setSelectionRange(
range.start,
range.end - spoiler_syntax_start_without_break.length - spoiler_syntax_end.length,
);
return;
}
// For when the text (including the header) inside a spoiler block is selected.
if (is_selection_formatted(spoiler_syntax_start_without_break, spoiler_syntax_end)) {
text =
text.slice(0, range.start - spoiler_syntax_start_without_break.length) +
selected_text +
text.slice(range.end + spoiler_syntax_end.length);
set(field, text);
field.setSelectionRange(
range.start - spoiler_syntax_start_without_break.length,
range.end - spoiler_syntax_start_without_break.length,
);
return;
}
// For when only the text inside a spoiler block (without a header) is selected.
if (is_selection_formatted(spoiler_syntax_start, spoiler_syntax_end)) {
text =
text.slice(0, range.start - spoiler_syntax_start.length) +
selected_text +
text.slice(range.end + spoiler_syntax_end.length);
set(field, text);
field.setSelectionRange(
range.start - spoiler_syntax_start.length,
range.end - spoiler_syntax_start.length,
);
return;
}
const is_inner_content_selected = () =>
range.start >= spoiler_syntax_start.length &&
text.length - range.end >= spoiler_syntax_end.length &&
text.slice(range.end, range.end + spoiler_syntax_end.length) === spoiler_syntax_end &&
text[range.start - 1] === "\n" &&
text.lastIndexOf(spoiler_syntax_start_without_break, range.start - 1) ===
text.lastIndexOf("\n", range.start - 2) + 1;
// For when only the text inside a spoiler block (with a header) is selected.
if (is_inner_content_selected()) {
const new_selection_start = text.lastIndexOf(
spoiler_syntax_start_without_break,
range.start,
);
text =
text.slice(0, new_selection_start) +
text.slice(
new_selection_start + spoiler_syntax_start_without_break.length,
range.start,
) +
selected_text +
text.slice(range.end + spoiler_syntax_end.length);
set(field, text);
field.setSelectionRange(
new_selection_start,
range.end - spoiler_syntax_start_without_break.length,
);
return;
}
const is_header_selected = () =>
range.start >= spoiler_syntax_start_without_break.length &&
text.slice(range.start - spoiler_syntax_start_without_break.length, range.start) ===
spoiler_syntax_start_without_break &&
text.length - range.end >= spoiler_syntax_end.length &&
text[range.end] === "\n";
// For when only the header of a spoiler block is selected.
if (is_header_selected()) {
const header = range.text;
const new_range_end = text.indexOf(spoiler_syntax_end, range.start);
const new_range_start = header ? range.start : range.start + 1;
text =
text.slice(0, range.start - spoiler_syntax_start_without_break.length) +
text.slice(new_range_start, new_range_end) +
text.slice(new_range_end + spoiler_syntax_end.length);
set(field, text);
field.setSelectionRange(
new_range_start - spoiler_syntax_start_without_break.length - (header ? 0 : 1),
new_range_end - spoiler_syntax_start_without_break.length - (header ? 0 : 1),
);
return;
}
if (range.start > 0 && text[range.start - 1] !== "\n") {
spoiler_syntax_start = "\n" + spoiler_syntax_start;
}
if (range.end < text.length && text[range.end] !== "\n") {
spoiler_syntax_end = spoiler_syntax_end + "\n";
}
const spoiler_syntax_start_with_header = spoiler_syntax_start_without_break + "Header\n";
// Otherwise, we don't have spoiler syntax, so we add it.
wrapSelection(field, spoiler_syntax_start_with_header, spoiler_syntax_end);
field.setSelectionRange(
range.start + spoiler_syntax_start.length - 1,
range.start + spoiler_syntax_start_with_header.length - 1,
);
};
switch (type) {
case "bold":
// Ctrl + B: Toggle bold syntax on selection.
@@ -739,6 +884,9 @@ export function format_text($textarea, type, inserted_content) {
format(quote_syntax_start, quote_syntax_end);
break;
}
case "spoiler":
format_spoiler();
break;
case "latex": {
const inline_latex_syntax = "$$";
let block_latex_syntax_start = "```math\n";

View File

@@ -8,6 +8,7 @@
<a role="button" data-format-type="bulleted" class="compose_control_button fa fa-list-ul formatting_button" aria-label="{{t 'Bulleted list' }}" {{#unless preview_mode_on}} tabindex=0 {{/unless}} data-tippy-content="{{t 'Bulleted list' }}"></a>
<div class="divider">|</div>
<a role="button" data-format-type="quote" class="compose_control_button fa fa-quote-left formatting_button" aria-label="{{t 'Quote' }}" {{#unless preview_mode_on}} tabindex=0 {{/unless}} data-tippy-content="{{t 'Quote' }}"></a>
<a role="button" data-format-type="spoiler" class="compose_control_button fa fa-minus formatting_button" aria-label="{{t 'Spoiler' }}" {{#unless preview_mode_on}} tabindex=0 {{/unless}} data-tippy-content="{{t 'Spoiler' }}"></a>
<a role="button" data-format-type="code" class="compose_control_button fa fa-code formatting_button" aria-label="{{t 'Code' }}" {{#unless preview_mode_on}} tabindex=0 {{/unless}} data-tippy-content="{{t 'Code' }}"></a>
<a role="button" data-format-type="latex" class="compose_control_button fa fa-superscript formatting_button" aria-label="{{t 'LaTeX' }}" {{#unless preview_mode_on}} tabindex=0 {{/unless}} data-tippy-content="{{t 'LaTeX' }}"></a>
<div class="divider">|</div>

View File

@@ -1167,6 +1167,137 @@ run_test("format_text - quote", ({override}) => {
assert.equal(wrap_selection_called, false);
});
run_test("format_text - spoiler", ({override}) => {
override(text_field_edit, "set", (_field, text) => {
set_text = text;
});
override(text_field_edit, "wrapSelection", (_field, syntax_start, syntax_end) => {
wrap_selection_called = true;
wrap_syntax_start = syntax_start;
wrap_syntax_end = syntax_end;
});
const spoiler_syntax_start_with_header = "```spoiler Header\n";
const spoiler_syntax_end = "\n```";
// Spoiler selected text
reset_state();
init_textarea("abc", {
start: 0,
end: 3,
text: "abc",
length: 3,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "");
assert.equal(wrap_selection_called, true);
assert.equal(wrap_syntax_start, spoiler_syntax_start_with_header);
assert.equal(wrap_syntax_end, spoiler_syntax_end);
// Undo spoiler, header selected
reset_state();
init_textarea("```spoiler Header\nabc\n```", {
start: 11,
end: 17,
text: "Header",
length: 6,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "Header\nabc");
assert.equal(wrap_selection_called, false);
reset_state();
init_textarea("abc\n```spoiler \ndef\n```\nghi", {
start: 15,
end: 15,
text: "",
length: 0,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\ndef\nghi");
assert.equal(wrap_selection_called, false);
// Undo spoiler selected text, only content selected
reset_state();
init_textarea("```spoiler \nabc\n```", {
start: 12,
end: 15,
text: "abc",
length: 3,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc");
assert.equal(wrap_selection_called, false);
reset_state();
init_textarea("```spoiler abc\ndef\n```", {
start: 15,
end: 18,
text: "def",
length: 3,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\ndef");
assert.equal(wrap_selection_called, false);
reset_state();
init_textarea("abc\n```spoiler d\nef\n```\nghi", {
start: 17,
end: 19,
text: "ef",
length: 2,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\nd\nef\nghi");
assert.equal(wrap_selection_called, false);
// Undo spoiler selected text, content and title selected
reset_state();
init_textarea("```spoiler abc\ndef\n```", {
start: 11,
end: 18,
text: "abc\ndef",
length: 7,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\ndef");
assert.equal(wrap_selection_called, false);
reset_state();
init_textarea("abc\n```spoiler d\nef\n```\nghi", {
start: 15,
end: 19,
text: "d\nef",
length: 4,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\nd\nef\nghi");
assert.equal(wrap_selection_called, false);
// Undo spoiler selected text, syntax selected
reset_state();
init_textarea("```spoiler abc\ndef\n```", {
start: 0,
end: 22,
text: "```spoiler abc\ndef\n```",
length: 22,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\ndef");
assert.equal(wrap_selection_called, false);
reset_state();
init_textarea("abc\n```spoiler d\nef\n```\nghi", {
start: 4,
end: 23,
text: "```spoiler d\nef\n```",
length: 19,
});
compose_ui.format_text($textarea, "spoiler");
assert.equal(set_text, "abc\nd\nef\nghi");
assert.equal(wrap_selection_called, false);
});
run_test("markdown_shortcuts", ({override_rewire}) => {
let format_text_type;
override_rewire(compose_ui, "format_text", (_$textarea, type) => {