Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions packages/core/src/api/clipboard/toClipboard/copyExtension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Extension } from "@tiptap/core";
import { Fragment, Node } from "prosemirror-model";
import { Fragment, Node, Slice } from "prosemirror-model";
import { NodeSelection, Plugin } from "prosemirror-state";
import { CellSelection } from "prosemirror-tables";
import type { EditorView } from "prosemirror-view";
Expand Down Expand Up @@ -127,13 +127,23 @@ export function selectedFragmentToHTML<
);
}

const originalSlice = view.state.selection.content();

// When AllSelection is used, ProseMirror wraps contents in blockGroup node.
// Unwrap it to avoid nested blocks when pasting.

let selectedFragment = originalSlice.content;

if (selectedFragment.childCount === 1 &&
selectedFragment.firstChild?.type.name === "blockGroup") {
selectedFragment = selectedFragment.firstChild.content;
}

// Uses default ProseMirror clipboard serialization.
const clipboardHTML: string = view.serializeForClipboard(
view.state.selection.content(),
new Slice(selectedFragment, originalSlice.openStart, originalSlice.openEnd)
).dom.innerHTML;

const selectedFragment = view.state.selection.content().content;

const externalHTML = fragmentToExternalHTML<BSchema, I, S>(
view,
selectedFragment,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Extension } from "@tiptap/core";
import { Fragment, Node } from "prosemirror-model";
import { TextSelection } from "prosemirror-state";
import { AllSelection, TextSelection } from "prosemirror-state";

import {
getBottomNestedBlockInfo,
Expand Down Expand Up @@ -953,6 +953,17 @@ export const KeyboardShortcutsExtension = Extension.create<{
"Mod-z": () => this.options.editor.undo(),
"Mod-y": () => this.options.editor.redo(),
"Shift-Mod-z": () => this.options.editor.redo(),
// Forces AllSelection from pos 0 to include non-editable blocks (e.g. images) that
// TextSelection would skip.
"Mod-a": () => {
const { doc } = this.options.editor.prosemirrorState;
this.options.editor.prosemirrorView?.dispatch(
this.options.editor.prosemirrorState.tr.setSelection(
new AllSelection(doc)
)
);
return true;
},
};
},
});
26 changes: 26 additions & 0 deletions tests/src/end-to-end/copypaste/copypaste.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,30 @@ test.describe("Check Copy/Paste Functionality", () => {

await compareDocToSnapshot(page, "images.json");
});

test("Image as first block should be able to be copied using Ctrl+A", async ({
page,
browserName,
}) => {
test.skip(
browserName === "firefox" || browserName === "webkit",
"Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33.",
);

await focusOnEditor(page);

const IMAGE_EMBED_URL = "https://placehold.co/800x540.png";
await executeSlashCommand(page, "image");

await page.click(`[data-test="embed-tab"]`);
await page.click(`[data-test="embed-input"]`);
await page.keyboard.type(IMAGE_EMBED_URL);
await page.click(`[data-test="embed-input-button"]`);
await page.waitForSelector(`img[src="${IMAGE_EMBED_URL}"]`);

await copyPasteAll(page);

await compareDocToSnapshot(page, "imageAsFirstBlock.json");
Comment on lines +191 to +213
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add coverage for the “image first, text follows” case.

This test covers the only-image scenario, but issue #2016 also reports that when text follows the leading image, Ctrl+A/C copies only the text. Please add a mixed-content case so the fix is locked down for both reported regressions.

Suggested additional coverage
+  test("Image as first block followed by text should be copied using Ctrl+A", async ({
+    page,
+    browserName,
+  }) => {
+    test.skip(
+      browserName === "firefox" || browserName === "webkit",
+      "Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33.",
+    );
+
+    await focusOnEditor(page);
+
+    const IMAGE_EMBED_URL = "https://placehold.co/800x540.png";
+    await executeSlashCommand(page, "image");
+
+    await page.click(`[data-test="embed-tab"]`);
+    await page.click(`[data-test="embed-input"]`);
+    await page.keyboard.type(IMAGE_EMBED_URL);
+    await page.click(`[data-test="embed-input-button"]`);
+    await page.waitForSelector(`img[src="${IMAGE_EMBED_URL}"]`);
+
+    await page.keyboard.press("ArrowDown");
+    await page.keyboard.press("Enter");
+    await page.keyboard.type("paragraph after image");
+
+    await copyPasteAll(page);
+
+    await compareDocToSnapshot(page, "imageAsFirstBlockWithText.json");
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("Image as first block should be able to be copied using Ctrl+A", async ({
page,
browserName,
}) => {
test.skip(
browserName === "firefox" || browserName === "webkit",
"Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33.",
);
await focusOnEditor(page);
const IMAGE_EMBED_URL = "https://placehold.co/800x540.png";
await executeSlashCommand(page, "image");
await page.click(`[data-test="embed-tab"]`);
await page.click(`[data-test="embed-input"]`);
await page.keyboard.type(IMAGE_EMBED_URL);
await page.click(`[data-test="embed-input-button"]`);
await page.waitForSelector(`img[src="${IMAGE_EMBED_URL}"]`);
await copyPasteAll(page);
await compareDocToSnapshot(page, "imageAsFirstBlock.json");
test("Image as first block should be able to be copied using Ctrl+A", async ({
page,
browserName,
}) => {
test.skip(
browserName === "firefox" || browserName === "webkit",
"Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33.",
);
await focusOnEditor(page);
const IMAGE_EMBED_URL = "https://placehold.co/800x540.png";
await executeSlashCommand(page, "image");
await page.click(`[data-test="embed-tab"]`);
await page.click(`[data-test="embed-input"]`);
await page.keyboard.type(IMAGE_EMBED_URL);
await page.click(`[data-test="embed-input-button"]`);
await page.waitForSelector(`img[src="${IMAGE_EMBED_URL}"]`);
await copyPasteAll(page);
await compareDocToSnapshot(page, "imageAsFirstBlock.json");
});
test("Image as first block followed by text should be copied using Ctrl+A", async ({
page,
browserName,
}) => {
test.skip(
browserName === "firefox" || browserName === "webkit",
"Firefox doesn't yet support the async clipboard API. Webkit copy/paste stopped working after updating to Playwright 1.33.",
);
await focusOnEditor(page);
const IMAGE_EMBED_URL = "https://placehold.co/800x540.png";
await executeSlashCommand(page, "image");
await page.click(`[data-test="embed-tab"]`);
await page.click(`[data-test="embed-input"]`);
await page.keyboard.type(IMAGE_EMBED_URL);
await page.click(`[data-test="embed-input-button"]`);
await page.waitForSelector(`img[src="${IMAGE_EMBED_URL}"]`);
await page.keyboard.press("ArrowDown");
await page.keyboard.press("Enter");
await page.keyboard.type("paragraph after image");
await copyPasteAll(page);
await compareDocToSnapshot(page, "imageAsFirstBlockWithText.json");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/end-to-end/copypaste/copypaste.test.ts` around lines 191 - 213, Add
a new end-to-end test that mirrors the existing "Image as first block should be
able to be copied using Ctrl+A" but inserts a text block after the image to
cover the "image first, text follows" regression; use the same helpers
(focusOnEditor, executeSlashCommand to insert the image via IMAGE_EMBED_URL,
waitForSelector `img[src="${IMAGE_EMBED_URL}"]`), then insert or type a text
block after the image (e.g., create a paragraph block via keyboard or
executeSlashCommand), call copyPasteAll(page) and assert with
compareDocToSnapshot using a new snapshot name like "imageThenText.json" to lock
in mixed-content behavior.

});

});