From ee04e063c6a3c104cee850febad800343ffc5722 Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Mon, 2 Feb 2026 16:17:00 +0200 Subject: [PATCH 1/3] fix: allow paste from context menu --- .../src/components/slash-menu/menuItems.js | 21 +++++++------- .../src/core/utilities/clipboardUtils.d.ts | 4 +++ .../src/core/utilities/clipboardUtils.js | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/super-editor/src/components/slash-menu/menuItems.js b/packages/super-editor/src/components/slash-menu/menuItems.js index 92bd438cf1..3b92461cd7 100644 --- a/packages/super-editor/src/components/slash-menu/menuItems.js +++ b/packages/super-editor/src/components/slash-menu/menuItems.js @@ -4,6 +4,8 @@ import TableActions from '../toolbar/TableActions.vue'; import LinkInput from '../toolbar/LinkInput.vue'; import { TEXTS, ICONS, TRIGGERS } from './constants.js'; import { isTrackedChangeActionAllowed } from '@extensions/track-changes/permission-helpers.js'; +import { readClipboardRaw } from '../../core/utilities/clipboardUtils.js'; +import { handleClipboardPaste } from '../../core/InputRule.js'; /** * Check if a module is enabled based on editor options @@ -257,17 +259,14 @@ export function getItems(context, customItems = [], includeDefaultItems = true) label: TEXTS.paste, icon: ICONS.paste, isDefault: true, - action: (editor) => { - // Use execCommand('paste') - triggers native paste without permission prompt - // This works because it's triggered by user interaction (clicking the menu item) - const editorDom = editor.view?.dom; - if (editorDom) { - editorDom.focus(); - // execCommand paste is allowed when triggered by user action - const success = document.execCommand('paste'); - if (!success) { - console.warn('[Paste] execCommand paste failed - clipboard may be empty or inaccessible'); - } + action: async (editor) => { + const { view } = editor ?? {}; + if (!view) return; + view.dom.focus(); + const { html, text } = await readClipboardRaw(); + const handled = html ? handleClipboardPaste({ editor, view }, html) : false; + if (!handled && text && editor.commands?.insertContent) { + editor.commands.insertContent(text, { contentType: 'text' }); } }, showWhen: (context) => { diff --git a/packages/super-editor/src/core/utilities/clipboardUtils.d.ts b/packages/super-editor/src/core/utilities/clipboardUtils.d.ts index a277ab6ae2..dacb2bc68d 100644 --- a/packages/super-editor/src/core/utilities/clipboardUtils.d.ts +++ b/packages/super-editor/src/core/utilities/clipboardUtils.d.ts @@ -7,6 +7,10 @@ * @returns {Promise} Whether clipboard read permission is granted */ export function ensureClipboardPermission(): Promise; +/** + * Reads raw HTML and text from the system clipboard (for use in paste actions). + */ +export function readClipboardRaw(): Promise<{ html: string; text: string }>; /** * Reads content from the system clipboard and parses it into a ProseMirror fragment. * Attempts to read HTML first, falling back to plain text if necessary. diff --git a/packages/super-editor/src/core/utilities/clipboardUtils.js b/packages/super-editor/src/core/utilities/clipboardUtils.js index 80a1ba7556..12ce729c17 100644 --- a/packages/super-editor/src/core/utilities/clipboardUtils.js +++ b/packages/super-editor/src/core/utilities/clipboardUtils.js @@ -52,6 +52,35 @@ export async function ensureClipboardPermission() { } } +/** + * Reads raw HTML and text from the system clipboard (for use in paste actions). + * @returns {Promise<{ html: string, text: string }>} + */ +export async function readClipboardRaw() { + let html = ''; + let text = ''; + const hasPermission = await ensureClipboardPermission(); + + if (hasPermission && navigator.clipboard && navigator.clipboard.read) { + try { + const items = await navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes('text/html')) { + html = await (await item.getType('text/html')).text(); + } + if (item.types.includes('text/plain')) { + text = await (await item.getType('text/plain')).text(); + } + } + } catch { + try { + text = await navigator.clipboard.readText(); + } catch {} + } + } + return { html, text: text || '' }; +} + /** * Reads content from the system clipboard and parses it into a ProseMirror fragment. * Attempts to read HTML first, falling back to plain text if necessary. From 07ef1beca823f120385646e14cb4cc5b39aa12f8 Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Mon, 2 Feb 2026 19:04:03 +0200 Subject: [PATCH 2/3] fix: additional check --- .../src/core/utilities/clipboardUtils.js | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/packages/super-editor/src/core/utilities/clipboardUtils.js b/packages/super-editor/src/core/utilities/clipboardUtils.js index 12ce729c17..2abdc7cac5 100644 --- a/packages/super-editor/src/core/utilities/clipboardUtils.js +++ b/packages/super-editor/src/core/utilities/clipboardUtils.js @@ -61,18 +61,24 @@ export async function readClipboardRaw() { let text = ''; const hasPermission = await ensureClipboardPermission(); - if (hasPermission && navigator.clipboard && navigator.clipboard.read) { - try { - const items = await navigator.clipboard.read(); - for (const item of items) { - if (item.types.includes('text/html')) { - html = await (await item.getType('text/html')).text(); - } - if (item.types.includes('text/plain')) { - text = await (await item.getType('text/plain')).text(); + if (hasPermission && navigator.clipboard) { + if (navigator.clipboard.read) { + try { + const items = await navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes('text/html')) { + html = await (await item.getType('text/html')).text(); + } + if (item.types.includes('text/plain')) { + text = await (await item.getType('text/plain')).text(); + } } + } catch { + try { + text = await navigator.clipboard.readText(); + } catch {} } - } catch { + } else { try { text = await navigator.clipboard.readText(); } catch {} From 94ecfc6de23035539b97796700a56808ff6f3ddc Mon Sep 17 00:00:00 2001 From: VladaHarbour Date: Wed, 4 Feb 2026 21:55:54 +0200 Subject: [PATCH 3/3] fix: optimise clipboard utils --- .../src/core/utilities/clipboardUtils.js | 25 +------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/packages/super-editor/src/core/utilities/clipboardUtils.js b/packages/super-editor/src/core/utilities/clipboardUtils.js index 2abdc7cac5..0e0342b70f 100644 --- a/packages/super-editor/src/core/utilities/clipboardUtils.js +++ b/packages/super-editor/src/core/utilities/clipboardUtils.js @@ -94,30 +94,7 @@ export async function readClipboardRaw() { * @returns {Promise} A promise that resolves to a ProseMirror fragment or text node, or null if reading fails. */ export async function readFromClipboard(state) { - let html = ''; - let text = ''; - const hasPermission = await ensureClipboardPermission(); - - if (hasPermission && navigator.clipboard && navigator.clipboard.read) { - try { - const items = await navigator.clipboard.read(); - for (const item of items) { - if (item.types.includes('text/html')) { - html = await (await item.getType('text/html')).text(); - break; - } else if (item.types.includes('text/plain')) { - text = await (await item.getType('text/plain')).text(); - } - } - } catch { - // Fallback to plain text read; may still fail if permission denied - try { - text = await navigator.clipboard.readText(); - } catch {} - } - } else { - // permissions denied or API unavailable; leave content empty - } + const { html, text } = await readClipboardRaw(); let content = null; if (html) { try {