diff --git a/packages/super-editor/src/components/slash-menu/utils.js b/packages/super-editor/src/components/slash-menu/utils.js index 20d624934..d7683ba41 100644 --- a/packages/super-editor/src/components/slash-menu/utils.js +++ b/packages/super-editor/src/components/slash-menu/utils.js @@ -90,7 +90,7 @@ export async function getEditorContext(editor, event) { node = state.doc.nodeAt(pos); } - // We need to check if we have anything in the clipboard and request permission if needed + // We need to check if we have anything in the clipboard const clipboardContent = await readFromClipboard(state); return { diff --git a/packages/super-editor/src/core/Editor.js b/packages/super-editor/src/core/Editor.js index 5f55a7024..9131a5fa4 100644 --- a/packages/super-editor/src/core/Editor.js +++ b/packages/super-editor/src/core/Editor.js @@ -239,7 +239,6 @@ export class Editor extends EventEmitter { // async (file) => url; handleImageUpload: null, - // telemetry telemetry: null, // Docx xml updated by User diff --git a/packages/super-editor/src/core/utilities/clipboardUtils.js b/packages/super-editor/src/core/utilities/clipboardUtils.js index 67be48e6d..483db510e 100644 --- a/packages/super-editor/src/core/utilities/clipboardUtils.js +++ b/packages/super-editor/src/core/utilities/clipboardUtils.js @@ -1,48 +1,41 @@ -// @ts-nocheck // clipboardUtils.js -import { DOMParser } from 'prosemirror-model'; +import { DOMSerializer, DOMParser } from 'prosemirror-model'; /** - * Checks if clipboard read permission is granted and handles permission prompts. - * Returns true if clipboard-read permission is granted. If state is "prompt" it will - * proactively trigger a readText() call which will surface the browser permission - * dialog to the user. Falls back gracefully in older browsers that lack the - * Permissions API. - * @returns {Promise} Whether clipboard read permission is granted + * Serializes the current selection in the editor state to HTML and plain text for clipboard use. + * @param {EditorState} state - The ProseMirror editor state containing the current selection. + * @returns {{ htmlString: string, text: string }} An object with the HTML string and plain text of the selection. */ -export async function ensureClipboardPermission() { - if (typeof navigator === 'undefined' || !navigator.clipboard) { - return false; - } - - // Some older browsers do not expose navigator.permissions – assume granted - if (!navigator.permissions || typeof navigator.permissions.query !== 'function') { - return true; - } +export function serializeSelectionToClipboard(state) { + const { from, to } = state.selection; + const slice = state.selection.content(); + const htmlContainer = document.createElement('div'); + htmlContainer.appendChild(DOMSerializer.fromSchema(state.schema).serializeFragment(slice.content)); + const htmlString = htmlContainer.innerHTML; + const text = state.doc.textBetween(from, to); + return { htmlString, text }; +} +/** + * Writes HTML and plain text data to the system clipboard. + * Uses the Clipboard API if available, otherwise falls back to plain text. + * @param {{ htmlString: string, text: string }} param0 - The HTML and plain text to write to the clipboard. + * @returns {Promise} A promise that resolves when the clipboard write is complete. + */ +export async function writeToClipboard({ htmlString, text }) { try { - // @ts-ignore – string literal is valid at runtime; TS lib DOM typing not available in .js file - const status = await navigator.permissions.query({ name: 'clipboard-read' }); - - if (status.state === 'granted') { - return true; - } - - if (status.state === 'prompt') { - // Trigger a readText() to make the browser show its permission prompt. - try { - await navigator.clipboard.readText(); - return true; - } catch { - return false; - } + if (navigator.clipboard && window.ClipboardItem) { + const clipboardItem = new window.ClipboardItem({ + 'text/html': new Blob([htmlString], { type: 'text/html' }), + 'text/plain': new Blob([text], { type: 'text/plain' }), + }); + await navigator.clipboard.write([clipboardItem]); + } else { + await navigator.clipboard.writeText(text); } - - // If we hit this area this is state === 'denied' - return false; - } catch { - return false; + } catch (e) { + console.error('Error writing to clipboard', e); } } @@ -55,9 +48,7 @@ export async function ensureClipboardPermission() { export async function readFromClipboard(state) { let html = ''; let text = ''; - const hasPermission = await ensureClipboardPermission(); - - if (hasPermission && navigator.clipboard && navigator.clipboard.read) { + if (navigator.clipboard && navigator.clipboard.read) { try { const items = await navigator.clipboard.read(); for (const item of items) { @@ -69,13 +60,10 @@ export async function readFromClipboard(state) { } } } catch { - // Fallback to plain text read; may still fail if permission denied - try { - text = await navigator.clipboard.readText(); - } catch {} + text = await navigator.clipboard.readText(); } } else { - // permissions denied or API unavailable; leave content empty + text = await navigator.clipboard.readText(); } let content = null; if (html) { diff --git a/packages/super-editor/src/core/utilities/tests/clipboardUtils.test.js b/packages/super-editor/src/core/utilities/tests/clipboardUtils.test.js deleted file mode 100644 index 095909ee6..000000000 --- a/packages/super-editor/src/core/utilities/tests/clipboardUtils.test.js +++ /dev/null @@ -1,72 +0,0 @@ -import { describe, it, expect, vi, afterEach } from 'vitest'; - -import { ensureClipboardPermission, readFromClipboard } from '../clipboardUtils.js'; - -// Helper to restore globals after each test -const originalNavigator = global.navigator; -const originalWindowClipboardItem = globalThis.ClipboardItem; - -function restoreGlobals() { - if (typeof originalNavigator !== 'undefined') { - global.navigator = originalNavigator; - } else { - delete global.navigator; - } - - if (typeof originalWindowClipboardItem !== 'undefined') { - globalThis.ClipboardItem = originalWindowClipboardItem; - } else { - delete globalThis.ClipboardItem; - } -} - -afterEach(() => { - restoreGlobals(); - vi.restoreAllMocks(); -}); - -describe('clipboardUtils', () => { - describe('ensureClipboardPermission', () => { - it('navigator undefined returns false', async () => { - // Remove navigator entirely - delete global.navigator; - const result = await ensureClipboardPermission(); - expect(result).toBe(false); - }); - it('permissions absent but clipboard present returns true', async () => { - global.navigator = { - clipboard: {}, - }; - const result = await ensureClipboardPermission(); - expect(result).toBe(true); - }); - }); - - describe('readFromClipboard', () => { - it('navigator.clipboard undefined returns null (no throw)', async () => { - global.navigator = {}; - const mockState = { schema: { text: (t) => t } }; - const res = await readFromClipboard(mockState); - expect(res).toBeNull(); - }); - - it('read() fails so fallback readText() is used', async () => { - const readTextMock = vi.fn().mockResolvedValue('plain'); - global.navigator = { - clipboard: { - read: vi.fn().mockRejectedValue(new Error('fail')), - readText: readTextMock, - }, - permissions: { - query: vi.fn().mockResolvedValue({ state: 'granted' }), - }, - }; - - const mockState = { schema: { text: (t) => t } }; - const res = await readFromClipboard(mockState); - - expect(readTextMock).toHaveBeenCalled(); - expect(res).toBe('plain'); - }); - }); -}); diff --git a/packages/superdoc/package.json b/packages/superdoc/package.json index 747b4ae8d..8d0221b72 100644 --- a/packages/superdoc/package.json +++ b/packages/superdoc/package.json @@ -1,7 +1,7 @@ { "name": "@harbour-enterprises/superdoc", "type": "module", - "version": "0.16.0-next.9", + "version": "0.16.0", "license": "AGPL-3.0", "readme": "../../README.md", "files": [ diff --git a/packages/superdoc/src/core/SuperDoc.js b/packages/superdoc/src/core/SuperDoc.js index faf6cc0f4..b43e6c777 100644 --- a/packages/superdoc/src/core/SuperDoc.js +++ b/packages/superdoc/src/core/SuperDoc.js @@ -163,7 +163,6 @@ export class SuperDoc extends EventEmitter { isDev: false, - // telemetry config telemetry: null, // Events diff --git a/packages/superdoc/src/stores/comments-store.js b/packages/superdoc/src/stores/comments-store.js index e34bd0ba1..866a3afc4 100644 --- a/packages/superdoc/src/stores/comments-store.js +++ b/packages/superdoc/src/stores/comments-store.js @@ -472,11 +472,12 @@ export const useCommentsStore = defineStore('comments', () => { // Create comments for tracked changes // that do not have a corresponding comment (created in Word). - const { tr } = editor.view.state; - const { dispatch } = editor.view; - groupedChanges.forEach(({ insertedMark, deletionMark, formatMark }, index) => { console.debug(`Create comment for track change: ${index}`); + + const { dispatch } = editor.view; + const { tr } = editor.view.state; + const foundComment = commentsList.value.find( (i) => i.commentId === insertedMark?.mark.attrs.id || @@ -488,6 +489,7 @@ export const useCommentsStore = defineStore('comments', () => { if (foundComment) { if (isLastIteration) { tr.setMeta(CommentsPluginKey, { type: 'force' }); + dispatch(tr); } return; } @@ -502,10 +504,9 @@ export const useCommentsStore = defineStore('comments', () => { if (isLastIteration) tr.setMeta(CommentsPluginKey, { type: 'force' }); tr.setMeta(CommentsPluginKey, { type: 'forceTrackChanges' }); tr.setMeta(TrackChangesBasePluginKey, trackChangesPayload); + dispatch(tr); } }); - - dispatch(tr); }; const translateCommentsForExport = () => {