diff --git a/frontend/text-editor/src/editor/TextEditor.test.js b/frontend/text-editor/src/editor/TextEditor.test.js index cb78558a16..3bfcc22ae8 100644 --- a/frontend/text-editor/src/editor/TextEditor.test.js +++ b/frontend/text-editor/src/editor/TextEditor.test.js @@ -1,5 +1,5 @@ import { describe, test, expect } from "vitest"; -import { TextEditor } from "./TextEditor.js"; +import { TextEditor, createRootFromHTML } from "./TextEditor.js"; /* @vitest-environment jsdom */ describe("TextEditor", () => { @@ -97,4 +97,24 @@ describe("TextEditor", () => { expect(document.activeElement).toBe(textEditor.element); expect(selection.containsNode(textEditor.root)); }); + + test("createRootFromHTML should keep the HTML structure when HTML paste is allowed", () => { + const root = createRootFromHTML( + "
Hello, World!
", + undefined, + true, + ); + expect(root.dataset.itype).toBe("root"); + expect(root.children).toHaveLength(1); + expect(root.firstElementChild.children).toHaveLength(2); + expect(root.textContent).toBe("Hello, World!"); + }); + + test("createRootFromHTML should flatten to plain text when HTML paste is not allowed", () => { + const root = createRootFromHTML("
Hello, World!
"); + expect(root.dataset.itype).toBe("root"); + expect(root.children).toHaveLength(1); + expect(root.firstElementChild.children).toHaveLength(1); + expect(root.textContent).toBe("Hello, World!"); + }); }); diff --git a/frontend/text-editor/src/editor/clipboard/paste.js b/frontend/text-editor/src/editor/clipboard/paste.js index ea699349a9..683d40edb0 100644 --- a/frontend/text-editor/src/editor/clipboard/paste.js +++ b/frontend/text-editor/src/editor/clipboard/paste.js @@ -15,6 +15,7 @@ import { TextEditor } from "../TextEditor.js"; /** * Returns a DocumentFragment from text/html. * + * @param {SelectionController} selectionController * @param {DataTransfer} clipboardData * @returns {DocumentFragment} */ @@ -25,12 +26,14 @@ function getFormattedFragmentFromClipboardData( return mapContentFragmentFromHTML( clipboardData.getData("text/html"), selectionController.currentStyle, + true, ); } /** * Returns a DocumentFragment from text/plain. * + * @param {SelectionController} selectionController * @param {DataTransfer} clipboardData * @returns {DocumentFragment} */ @@ -44,6 +47,7 @@ function getPlainFragmentFromClipboardData(selectionController, clipboardData) { /** * Returns a document fragment of html data. * + * @param {SelectionController} selectionController * @param {DataTransfer} clipboardData * @returns {DocumentFragment} */ @@ -83,6 +87,7 @@ export function paste(event, editor, selectionController) { let fragment = null; if (editor?.options?.allowHTMLPaste) { fragment = getFormattedOrPlainFragmentFromClipboardData( + selectionController, event.clipboardData, ); } else { diff --git a/frontend/text-editor/src/editor/clipboard/paste.test.js b/frontend/text-editor/src/editor/clipboard/paste.test.js index 14dd7a9ac0..53658cac7c 100644 --- a/frontend/text-editor/src/editor/clipboard/paste.test.js +++ b/frontend/text-editor/src/editor/clipboard/paste.test.js @@ -5,6 +5,24 @@ import { paste } from "./paste.js"; /* @vitest-environment jsdom */ +/** + * Creates a minimal `ClipboardEvent`-like object carrying the given data. + * + * @param {Object.} data + * @returns {object} + */ +function createClipboardEvent(data) { + return { + preventDefault() {}, + clipboardData: { + types: Object.keys(data), + getData(type) { + return data[type] ?? ""; + }, + }, + }; +} + /** * Creates a minimal `ClipboardEvent`-like object carrying plain text. * @@ -12,15 +30,7 @@ import { paste } from "./paste.js"; * @returns {object} */ function createPlainTextClipboardEvent(text) { - return { - preventDefault() {}, - clipboardData: { - types: ["text/plain"], - getData(type) { - return type === "text/plain" ? text : ""; - }, - }, - }; + return createClipboardEvent({ "text/plain": text }); } describe("paste", () => { @@ -64,4 +74,45 @@ describe("paste", () => { expect(root.textContent).toBe("Hello, World!"); }); + + test("should insert the HTML contents when the editor allows HTML paste", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWithText(""); + textEditorMock.options = { allowHTMLPaste: true }; + const selection = document.getSelection(); + const selectionController = new SelectionController( + textEditorMock, + selection, + ); + textEditorMock.element.focus(); + + paste( + createClipboardEvent({ + "text/html": "
Hello, World!
", + "text/plain": "ignored", + }), + textEditorMock, + selectionController, + ); + + expect(textEditorMock.root.textContent).toBe("Hello, World!"); + }); + + test("should fall back to plain text when the editor allows HTML paste but there is no HTML", () => { + const textEditorMock = TextEditorMock.createTextEditorMockWithText(""); + textEditorMock.options = { allowHTMLPaste: true }; + const selection = document.getSelection(); + const selectionController = new SelectionController( + textEditorMock, + selection, + ); + textEditorMock.element.focus(); + + paste( + createPlainTextClipboardEvent("Hello, World!"), + textEditorMock, + selectionController, + ); + + expect(textEditorMock.root.textContent).toBe("Hello, World!"); + }); }); diff --git a/frontend/text-editor/src/editor/content/dom/Content.js b/frontend/text-editor/src/editor/content/dom/Content.js index 0cb742992d..b04dd7636b 100644 --- a/frontend/text-editor/src/editor/content/dom/Content.js +++ b/frontend/text-editor/src/editor/content/dom/Content.js @@ -207,7 +207,11 @@ export function mapContentFragmentFromHTML( try { const parser = new DOMParser(); const document = parser.parseFromString(html, "text/html"); - return mapContentFragmentFromDocument(document, styleDefaults); + return mapContentFragmentFromDocument( + document, + document.body, + styleDefaults, + ); } catch (error) { console.error("Couldn't parse HTML", html, error); const plainText = htmlToText(html); diff --git a/frontend/text-editor/src/editor/content/dom/Content.test.js b/frontend/text-editor/src/editor/content/dom/Content.test.js index 577e41d66b..7480a1642c 100644 --- a/frontend/text-editor/src/editor/content/dom/Content.test.js +++ b/frontend/text-editor/src/editor/content/dom/Content.test.js @@ -43,6 +43,19 @@ describe("Content", () => { expect(contentFragment.textContent).toBe("Hello, World!"); }); + test("mapContentFragmentFromHTML should map the HTML structure when HTML paste is allowed", () => { + const inertElement = document.createElement("div"); + const contentFragment = mapContentFragmentFromHTML( + "
Hello, World!
", + inertElement.style, + true, + ); + expect(contentFragment).toBeInstanceOf(DocumentFragment); + expect(contentFragment.children).toHaveLength(1); + expect(contentFragment.firstElementChild.children).toHaveLength(2); + expect(contentFragment.textContent).toBe("Hello, World!"); + }); + /* test("mapContentFragmentFromHTML should return a valid content for the editor (multiple paragraphs)", () => { const paragraphs = [