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..0e0342b70f 100644 --- a/packages/super-editor/src/core/utilities/clipboardUtils.js +++ b/packages/super-editor/src/core/utilities/clipboardUtils.js @@ -53,36 +53,48 @@ export async function ensureClipboardPermission() { } /** - * 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. - * @param {EditorState} state - The ProseMirror editor state, used for schema and parsing. - * @returns {Promise} A promise that resolves to a ProseMirror fragment or text node, or null if reading fails. + * Reads raw HTML and text from the system clipboard (for use in paste actions). + * @returns {Promise<{ html: string, text: string }>} */ -export async function readFromClipboard(state) { +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(); - break; - } else 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 { - // Fallback to plain text read; may still fail if permission denied + } else { try { text = await navigator.clipboard.readText(); } catch {} } - } else { - // permissions denied or API unavailable; leave content empty } + 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. + * @param {EditorState} state - The ProseMirror editor state, used for schema and parsing. + * @returns {Promise} A promise that resolves to a ProseMirror fragment or text node, or null if reading fails. + */ +export async function readFromClipboard(state) { + const { html, text } = await readClipboardRaw(); let content = null; if (html) { try {