mirror of
https://github.com/penpot/penpot.git
synced 2026-08-09 14:28:55 +00:00
🐛 Fix HTML paste never using the pasted HTML structure
The v2-html-paste path was broken since it was introduced. The paste handler called the formatted/plain dispatcher without the selection controller, so reading `clipboardData.types` threw on every paste while the feature was enabled. The HTML branch also never forwarded the allowHTMLPaste flag to mapContentFragmentFromHTML, and that function called mapContentFragmentFromDocument() without the root node, so createNodeIterator() threw and the silent fallback always degraded the paste to plain text. Pass the selection controller through the dispatcher, forward the flag from the formatted fragment helper, and iterate the parsed document from its body. AI-assisted-by: longcat-2.0-free
This commit is contained in:
parent
b0014d92aa
commit
222322dcb5
@ -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(
|
||||
"<div>Hello, <b>World!</b></div>",
|
||||
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("<div>Hello, <b>World!</b></div>");
|
||||
expect(root.dataset.itype).toBe("root");
|
||||
expect(root.children).toHaveLength(1);
|
||||
expect(root.firstElementChild.children).toHaveLength(1);
|
||||
expect(root.textContent).toBe("Hello, World!");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -5,6 +5,24 @@ import { paste } from "./paste.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
/**
|
||||
* Creates a minimal `ClipboardEvent`-like object carrying the given data.
|
||||
*
|
||||
* @param {Object.<string, string>} 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": "<div>Hello, <b>World!</b></div>",
|
||||
"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!");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
"<div>Hello, <b>World!</b></div>",
|
||||
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 = [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user