mirror of
https://github.com/discourse/discourse.git
synced 2025-04-26 01:34:33 +08:00
FEATURE: add code-block rich editor extension (#31179)
Continues the work done on https://github.com/discourse/discourse/pull/30815. Extends the ProseMirror-markdown `code-block` node by integrating our existing HighlightJS pipeline for code highlighting and adding a node view with a `<select>` to change the language of the block. We're also adding the markdown paste extension, which handles converting pasted text/plain to rich content if it contains Markdown. --------- Co-authored-by: Martin Brennan <martin@discourse.org>
This commit is contained in:
parent
991379b9a4
commit
a5cacde681
@ -58,7 +58,7 @@ export default async function highlightSyntax(elem, siteSettings, session) {
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureHighlightJs(langFile) {
|
||||
export async function ensureHighlightJs(langFile) {
|
||||
try {
|
||||
if (!hljsLoadPromise) {
|
||||
hljsLoadPromise = loadHighlightJs(langFile);
|
||||
|
@ -73,7 +73,7 @@ export default class ProsemirrorEditor extends Component {
|
||||
|
||||
get pluginParams() {
|
||||
return {
|
||||
utils,
|
||||
utils: { ...utils, convertFromMarkdown: this.convertFromMarkdown },
|
||||
schema: this.schema,
|
||||
pmState: ProsemirrorState,
|
||||
pmModel: ProsemirrorModel,
|
||||
|
@ -0,0 +1,137 @@
|
||||
import { highlightPlugin } from "prosemirror-highlightjs";
|
||||
import { ensureHighlightJs } from "discourse/lib/highlight-syntax";
|
||||
|
||||
// cached hljs instance with custom plugins/languages
|
||||
let hljs;
|
||||
|
||||
class CodeBlockWithLangSelectorNodeView {
|
||||
#selectAdded = false;
|
||||
|
||||
constructor(node, view, getPos) {
|
||||
this.node = node;
|
||||
this.view = view;
|
||||
this.getPos = getPos;
|
||||
|
||||
const code = document.createElement("code");
|
||||
const pre = document.createElement("pre");
|
||||
pre.appendChild(code);
|
||||
|
||||
this.dom = pre;
|
||||
this.contentDOM = code;
|
||||
|
||||
this.appendSelect();
|
||||
}
|
||||
|
||||
changeListener(e) {
|
||||
this.view.dispatch(
|
||||
this.view.state.tr.setNodeMarkup(this.getPos(), null, {
|
||||
params: e.target.value,
|
||||
})
|
||||
);
|
||||
|
||||
if (e.target.firstChild.textContent) {
|
||||
e.target.firstChild.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
appendSelect() {
|
||||
if (!hljs || this.#selectAdded) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#selectAdded = true;
|
||||
|
||||
const select = document.createElement("select");
|
||||
select.contentEditable = false;
|
||||
select.addEventListener("change", (e) => this.changeListener(e));
|
||||
select.classList.add("code-language-select");
|
||||
|
||||
const languages = hljs.listLanguages();
|
||||
|
||||
const empty = document.createElement("option");
|
||||
empty.textContent = languages.includes(this.node.attrs.params)
|
||||
? ""
|
||||
: this.node.attrs.params;
|
||||
select.appendChild(empty);
|
||||
|
||||
languages.forEach((lang) => {
|
||||
const option = document.createElement("option");
|
||||
option.textContent = lang;
|
||||
option.selected = lang === this.node.attrs.params;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
this.dom.appendChild(select);
|
||||
}
|
||||
|
||||
update(node) {
|
||||
this.appendSelect();
|
||||
|
||||
return node.type === this.node.type;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.dom.removeEventListener("change", (e) => this.changeListener(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {RichEditorExtension} */
|
||||
const extension = {
|
||||
nodeViews: { code_block: CodeBlockWithLangSelectorNodeView },
|
||||
plugins({ pmState: { Plugin }, getContext }) {
|
||||
return [
|
||||
async () =>
|
||||
highlightPlugin(
|
||||
(hljs = await ensureHighlightJs(
|
||||
getContext().session.highlightJsPath
|
||||
)),
|
||||
["code_block", "html_block"],
|
||||
|
||||
// NOTE: If the language has not been set with the code block, we default to plain
|
||||
// text rather than autodetecting. This is to work around an infinite loop issue
|
||||
// in prosemirror-highlightjs when autodetecting which hangs the browser sometimes
|
||||
// for > 10 seconds, for example:
|
||||
//
|
||||
// https://github.com/b-kelly/prosemirror-highlightjs/issues/21
|
||||
//
|
||||
// We can remove this if we find some other workaround.
|
||||
(node) => node.attrs.params || "text"
|
||||
),
|
||||
new Plugin({
|
||||
props: {
|
||||
// Handles removal of the code_block when it's at the start of the document
|
||||
handleKeyDown(view, event) {
|
||||
if (
|
||||
event.key === "Backspace" &&
|
||||
view.state.selection.$from.parent.type ===
|
||||
view.state.schema.nodes.code_block &&
|
||||
view.state.selection.$from.start() === 1 &&
|
||||
view.state.selection.$from.parentOffset === 0
|
||||
) {
|
||||
const { tr } = view.state;
|
||||
|
||||
const codeBlock = view.state.selection.$from.parent;
|
||||
const paragraph = view.state.schema.nodes.paragraph.create(
|
||||
null,
|
||||
codeBlock.content
|
||||
);
|
||||
tr.replaceWith(
|
||||
view.state.selection.$from.before(),
|
||||
view.state.selection.$from.after(),
|
||||
paragraph
|
||||
);
|
||||
tr.setSelection(
|
||||
new view.state.selection.constructor(tr.doc.resolve(1))
|
||||
);
|
||||
|
||||
view.dispatch(tr);
|
||||
return true;
|
||||
}
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default extension;
|
@ -0,0 +1,20 @@
|
||||
/** @type {RichEditorExtension} */
|
||||
const extension = {
|
||||
plugins({
|
||||
pmState: { Plugin },
|
||||
pmModel: { Fragment, Slice },
|
||||
utils: { convertFromMarkdown },
|
||||
}) {
|
||||
return new Plugin({
|
||||
props: {
|
||||
clipboardTextParser(text) {
|
||||
const { content } = convertFromMarkdown(text);
|
||||
|
||||
return Slice.maxOpen(Fragment.from(content));
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default extension;
|
@ -1,9 +1,11 @@
|
||||
import { registerRichEditorExtension } from "discourse/lib/composer/rich-editor-extensions";
|
||||
import codeBlock from "./code-block";
|
||||
import emoji from "./emoji";
|
||||
import hashtag from "./hashtag";
|
||||
import heading from "./heading";
|
||||
import image from "./image";
|
||||
import link from "./link";
|
||||
import markdownPaste from "./markdown-paste";
|
||||
import mention from "./mention";
|
||||
import quote from "./quote";
|
||||
import strikethrough from "./strikethrough";
|
||||
@ -21,12 +23,14 @@ const defaultExtensions = [
|
||||
image,
|
||||
link,
|
||||
heading,
|
||||
codeBlock,
|
||||
quote,
|
||||
hashtag,
|
||||
mention,
|
||||
strikethrough,
|
||||
underline,
|
||||
table,
|
||||
markdownPaste,
|
||||
];
|
||||
|
||||
defaultExtensions.forEach(registerRichEditorExtension);
|
||||
|
@ -25,10 +25,15 @@ export async function testMarkdown(
|
||||
/>
|
||||
</template>
|
||||
);
|
||||
|
||||
// ensure toggling to rich editor and back works
|
||||
await click(".composer-toggle-switch");
|
||||
await click(".composer-toggle-switch");
|
||||
await click(".composer-toggle-switch");
|
||||
|
||||
await waitFor(".ProseMirror");
|
||||
await settled();
|
||||
|
||||
const editor = document.querySelector(".ProseMirror");
|
||||
|
||||
// typeIn for contentEditable isn't reliable, and is slower
|
||||
|
@ -0,0 +1,58 @@
|
||||
import { module, test } from "qunit";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
|
||||
|
||||
module(
|
||||
"Integration | Component | prosemirror-editor - code-block extension",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.siteSettings.rich_editor = true;
|
||||
});
|
||||
|
||||
const select = (lang = "") =>
|
||||
`<select contenteditable="false" class="code-language-select"><option>${lang}</option><option>javascript</option><option>ruby</option><option>sql</option></select>`;
|
||||
|
||||
Object.entries({
|
||||
"basic code block": [
|
||||
"```plaintext\nconsole.log('Hello, world!');\n```",
|
||||
`<pre><code>console.log('Hello, world!');</code>${select(
|
||||
"plaintext"
|
||||
)}</pre>`,
|
||||
"```plaintext\nconsole.log('Hello, world!');\n```",
|
||||
],
|
||||
"basic code block without a lanuage": [
|
||||
"```\nconsole.log('Hello, world!');\n```",
|
||||
`<pre><code>console.log('Hello, world!');</code>${select()}</pre>`,
|
||||
"```\nconsole.log('Hello, world!');\n```",
|
||||
],
|
||||
"code block within list item": [
|
||||
"- ```plaintext\n console.log('Hello, world!');\n ```",
|
||||
`<ul><li><pre><code>console.log('Hello, world!');</code>${select(
|
||||
"plaintext"
|
||||
)}</pre></li></ul>`,
|
||||
"* ```plaintext\n console.log('Hello, world!');\n ```",
|
||||
],
|
||||
"code block with language": [
|
||||
'```javascript\nconsole.log("Hello, world!");\n```',
|
||||
`<pre><code><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">"Hello, world!"</span>);</code>${select()}</pre>`,
|
||||
'```javascript\nconsole.log("Hello, world!");\n```',
|
||||
],
|
||||
"code block with 4 spaces": [
|
||||
" print('Hello, world!')",
|
||||
`<pre><code>print('Hello, world!')</code>${select()}</pre>`,
|
||||
"```\nprint('Hello, world!')\n```",
|
||||
],
|
||||
"code block with 4 spaces within list item": [
|
||||
"- print('Hello, world!')",
|
||||
`<ul><li><pre><code>print('Hello, world!')</code>${select()}</pre></li></ul>`,
|
||||
"* ```\n print('Hello, world!')\n ```",
|
||||
],
|
||||
}).forEach(([name, [markdown, html, expectedMarkdown]]) => {
|
||||
test(name, async function (assert) {
|
||||
await testMarkdown(assert, markdown, html, expectedMarkdown);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
@ -12,13 +12,8 @@ import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
|
||||
module("Integration | Component | prosemirror-editor", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
clearRichEditorExtensions();
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
resetRichEditorExtensions();
|
||||
});
|
||||
hooks.beforeEach(() => clearRichEditorExtensions());
|
||||
hooks.afterEach(() => resetRichEditorExtensions());
|
||||
|
||||
test("renders the editor", async function (assert) {
|
||||
await render(<template><ProsemirrorEditor /></template>);
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { module, test } from "qunit";
|
||||
import {
|
||||
clearRichEditorExtensions,
|
||||
resetRichEditorExtensions,
|
||||
} from "discourse/lib/composer/rich-editor-extensions";
|
||||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import { testMarkdown } from "discourse/tests/helpers/rich-editor-helper";
|
||||
|
||||
@ -7,6 +11,9 @@ module(
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(() => clearRichEditorExtensions());
|
||||
hooks.afterEach(() => resetRichEditorExtensions());
|
||||
|
||||
const testCases = {
|
||||
"paragraphs and hard breaks": [
|
||||
["Hello", "<p>Hello</p>", "Hello"],
|
||||
|
@ -61,6 +61,12 @@ h6 code {
|
||||
color: var(--hljs-number);
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-tag .hljs-title {
|
||||
color: var(--hljs-tag);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-tag .hljs-string,
|
||||
.hljs-template-tag,
|
||||
@ -73,10 +79,6 @@ h6 code {
|
||||
color: var(--hljs-title);
|
||||
}
|
||||
|
||||
.hljs-name {
|
||||
color: var(--hljs-name);
|
||||
}
|
||||
|
||||
.hljs-quote,
|
||||
.hljs-operator,
|
||||
.hljs-selector-pseudo,
|
||||
@ -94,10 +96,8 @@ h6 code {
|
||||
color: var(--hljs-title);
|
||||
}
|
||||
|
||||
.hljs-tag,
|
||||
.hljs-tag .hljs-title {
|
||||
color: var(--hljs-tag);
|
||||
font-weight: normal;
|
||||
.hljs-name {
|
||||
color: var(--hljs-name);
|
||||
}
|
||||
|
||||
.hljs-punctuation {
|
||||
|
@ -134,10 +134,6 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.code-language-select {
|
||||
position: absolute;
|
||||
right: 0.25rem;
|
||||
@ -149,8 +145,11 @@
|
||||
font-size: var(--font-down-1-rem);
|
||||
}
|
||||
|
||||
.html-block {
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.html-block {
|
||||
border: 1px dashed var(--primary-low-mid);
|
||||
|
||||
&::after {
|
||||
@ -169,13 +168,14 @@
|
||||
Section below from prosemirror-view/style/prosemirror.css
|
||||
********************************************************/
|
||||
|
||||
/* stylelint-disable-next-line no-duplicate-selectors */
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
.ProseMirror {
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line no-duplicate-selectors
|
||||
.ProseMirror pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
describe "Composer - ProseMirror editor", type: :system do
|
||||
fab!(:user) { Fabricate(:user, refresh_auto_groups: true) }
|
||||
fab!(:tag)
|
||||
let(:cdp) { PageObjects::CDP.new }
|
||||
let(:composer) { PageObjects::Components::Composer.new }
|
||||
let(:rich) { composer.rich_editor }
|
||||
|
||||
@ -11,6 +12,12 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
SiteSetting.rich_editor = true
|
||||
end
|
||||
|
||||
def open_composer_and_toggle_rich_editor
|
||||
page.visit "/new-topic"
|
||||
expect(composer).to be_opened
|
||||
composer.toggle_rich_editor
|
||||
end
|
||||
|
||||
it "hides the Composer container's preview button" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
@ -24,34 +31,21 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
|
||||
context "with autocomplete" do
|
||||
it "triggers an autocomplete on mention" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("@#{user.username}")
|
||||
|
||||
expect(composer).to have_mention_autocomplete
|
||||
end
|
||||
|
||||
it "triggers an autocomplete on hashtag" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
find(".composer-toggle-switch").click
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("##{tag.name}")
|
||||
|
||||
expect(composer).to have_hashtag_autocomplete
|
||||
end
|
||||
|
||||
it "triggers an autocomplete on emoji" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content(":smile")
|
||||
|
||||
expect(composer).to have_emoji_autocomplete
|
||||
@ -60,22 +54,14 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
|
||||
context "with inputRules" do
|
||||
it "supports > to create a blockquote" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("> This is a blockquote")
|
||||
|
||||
expect(rich).to have_css("blockquote", text: "This is a blockquote")
|
||||
end
|
||||
|
||||
it "supports n. to create an ordered list" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("1. Item 1\n5. Item 2")
|
||||
|
||||
expect(rich).to have_css("ol li", text: "Item 1")
|
||||
@ -83,11 +69,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports *, - or + to create an unordered list" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("* Item 1\n")
|
||||
composer.type_content("- Item 2\n")
|
||||
composer.type_content("+ Item 3")
|
||||
@ -96,11 +78,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports ``` or 4 spaces to create a code block" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("```\nThis is a code block")
|
||||
composer.send_keys(%i[shift enter])
|
||||
composer.type_content(" This is a code block")
|
||||
@ -109,11 +87,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports 1-6 #s to create a heading" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("# Heading 1\n")
|
||||
composer.type_content("## Heading 2\n")
|
||||
composer.type_content("### Heading 3\n")
|
||||
@ -130,11 +104,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports _ or * to create an italic text" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("_This is italic_\n")
|
||||
composer.type_content("*This is italic*")
|
||||
|
||||
@ -142,11 +112,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports __ or ** to create a bold text" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("__This is bold__\n")
|
||||
composer.type_content("**This is bold**")
|
||||
|
||||
@ -154,11 +120,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports ` to create a code text" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("`This is code`")
|
||||
|
||||
expect(rich).to have_css("code", text: "This is code")
|
||||
@ -168,11 +130,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
context "with keymap" do
|
||||
PLATFORM_KEY_MODIFIER = SystemHelpers::PLATFORM_KEY_MODIFIER
|
||||
it "supports Ctrl + B to create a bold text" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content([PLATFORM_KEY_MODIFIER, "b"])
|
||||
composer.type_content("This is bold")
|
||||
|
||||
@ -180,11 +138,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + I to create an italic text" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content([PLATFORM_KEY_MODIFIER, "i"])
|
||||
composer.type_content("This is italic")
|
||||
|
||||
@ -192,12 +146,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
xit "supports Ctrl + K to create a link" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
page.send_keys([PLATFORM_KEY_MODIFIER, "k"])
|
||||
open_composer_and_toggle_rich_editor page.send_keys([PLATFORM_KEY_MODIFIER, "k"])
|
||||
page.send_keys("https://www.example.com\t")
|
||||
page.send_keys("This is a link")
|
||||
page.send_keys(:enter)
|
||||
@ -206,11 +155,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + Shift + 7 to create an ordered list" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("Item 1")
|
||||
composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "7"])
|
||||
|
||||
@ -218,11 +163,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + Shift + 8 to create a bullet list" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("Item 1")
|
||||
composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "8"])
|
||||
|
||||
@ -230,11 +171,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + Shift + 9 to create a blockquote" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("This is a blockquote")
|
||||
composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "9"])
|
||||
|
||||
@ -242,12 +179,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + Shift + 1-6 for headings, 0 for reset" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
|
||||
open_composer_and_toggle_rich_editor
|
||||
(1..6).each do |i|
|
||||
composer.type_content("\nHeading #{i}")
|
||||
composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, i.to_s])
|
||||
@ -260,11 +192,7 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + Z and Ctrl + Shift + Z to undo and redo" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("This is a test")
|
||||
composer.send_keys([PLATFORM_KEY_MODIFIER, "z"])
|
||||
|
||||
@ -276,15 +204,32 @@ describe "Composer - ProseMirror editor", type: :system do
|
||||
end
|
||||
|
||||
it "supports Ctrl + Shift + _ to create a horizontal rule" do
|
||||
page.visit "/new-topic"
|
||||
|
||||
expect(composer).to be_opened
|
||||
|
||||
composer.toggle_rich_editor
|
||||
open_composer_and_toggle_rich_editor
|
||||
composer.type_content("This is a test")
|
||||
composer.send_keys([PLATFORM_KEY_MODIFIER, :shift, "_"])
|
||||
|
||||
expect(rich).to have_css("hr")
|
||||
end
|
||||
end
|
||||
|
||||
describe "pasting content" do
|
||||
it "does not freeze the editor when pasting markdown code blocks without a language" do
|
||||
cdp.allow_clipboard
|
||||
open_composer_and_toggle_rich_editor
|
||||
|
||||
# The example is a bit convoluted, but it's the simplest way to reproduce the issue.
|
||||
cdp.write_clipboard <<~MARKDOWN
|
||||
```
|
||||
puts SiteSetting.all_settings(filter_categories: ["uncategorized"]).map { |setting| setting[:setting] }.join("\n")
|
||||
```
|
||||
MARKDOWN
|
||||
composer.type_content("This is a test\n\n")
|
||||
page.send_keys([PLATFORM_KEY_MODIFIER, "v"])
|
||||
expect(page.driver.browser.logs.get(:browser)).not_to include(
|
||||
"Maximum call stack size exceeded",
|
||||
)
|
||||
expect(rich).to have_css("pre code", wait: 1)
|
||||
expect(rich).to have_css("select.code-language-select", wait: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -30,6 +30,13 @@ module PageObjects
|
||||
page.evaluate_async_script("navigator.clipboard.readText().then(arguments[0])")
|
||||
end
|
||||
|
||||
def write_clipboard(text)
|
||||
page.evaluate_async_script(
|
||||
"navigator.clipboard.writeText(arguments[0]).then(arguments[1])",
|
||||
text,
|
||||
)
|
||||
end
|
||||
|
||||
def clipboard_has_text?(text, chomp: false, strict: true)
|
||||
try_until_success do
|
||||
clipboard_text = chomp ? read_clipboard.chomp : read_clipboard
|
||||
|
Loading…
x
Reference in New Issue
Block a user