From e903068cb97332ce8b810677fa06be8881556710 Mon Sep 17 00:00:00 2001 From: Yamilet P Date: Wed, 22 Apr 2026 22:07:27 -0500 Subject: [PATCH 1/3] fix: include non-editable blocks in Ctrl+A selection and fix nested paste. --- .../src/api/clipboard/toClipboard/copyExtension.ts | 13 +++++++++---- .../KeyboardShortcuts/KeyboardShortcutsExtension.ts | 13 ++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 3a6aeaffd5..5e36fb8f1a 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -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"; @@ -127,13 +127,18 @@ export function selectedFragmentToHTML< ); } + let selectedFragment = view.state.selection.content().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, 0, 0) ).dom.innerHTML; - const selectedFragment = view.state.selection.content().content; - const externalHTML = fragmentToExternalHTML( view, selectedFragment, diff --git a/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts b/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts index c0578d7f38..6ca1961ddf 100644 --- a/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts +++ b/packages/core/src/extensions/tiptap-extensions/KeyboardShortcuts/KeyboardShortcutsExtension.ts @@ -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, @@ -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; + }, }; }, }); From cc90389a945667a0072f6249cf2813aec531e954 Mon Sep 17 00:00:00 2001 From: Yamilet P Date: Wed, 22 Apr 2026 22:30:15 -0500 Subject: [PATCH 2/3] fix: failing copy/paste equality tests --- .../core/src/api/clipboard/toClipboard/copyExtension.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts index 5e36fb8f1a..47dd18fc9a 100644 --- a/packages/core/src/api/clipboard/toClipboard/copyExtension.ts +++ b/packages/core/src/api/clipboard/toClipboard/copyExtension.ts @@ -127,7 +127,12 @@ export function selectedFragmentToHTML< ); } - let selectedFragment = view.state.selection.content().content; + 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") { @@ -136,7 +141,7 @@ export function selectedFragmentToHTML< // Uses default ProseMirror clipboard serialization. const clipboardHTML: string = view.serializeForClipboard( - new Slice(selectedFragment, 0, 0) + new Slice(selectedFragment, originalSlice.openStart, originalSlice.openEnd) ).dom.innerHTML; const externalHTML = fragmentToExternalHTML( From 6cb73a1186fde42f43fe353385bdc72a11d37ed0 Mon Sep 17 00:00:00 2001 From: Yamilet P Date: Wed, 22 Apr 2026 22:48:23 -0500 Subject: [PATCH 3/3] test: add playwright test for copying image using ctrl+a command --- .../end-to-end/copypaste/copypaste.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/src/end-to-end/copypaste/copypaste.test.ts b/tests/src/end-to-end/copypaste/copypaste.test.ts index aaf36897d6..e796b9d707 100644 --- a/tests/src/end-to-end/copypaste/copypaste.test.ts +++ b/tests/src/end-to-end/copypaste/copypaste.test.ts @@ -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"); + }); + });