diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts index d604040974c5a..477765dc17f08 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/database-block.ts @@ -5,6 +5,7 @@ import { } from '@affine/core/components/hooks/affine/use-share-url'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { EditorService } from '@affine/core/modules/editor'; +import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard'; import { I18n } from '@affine/i18n'; import { track } from '@affine/track'; import type { @@ -63,18 +64,15 @@ function createCopyLinkToBlockMenuItem( const type = model.flavour; const page = editor.editorContainer$.value; - page?.host?.std.clipboard - .writeToClipboard(items => { - items['text/plain'] = str; - // wrap a link - items['text/html'] = `${str}`; - return items; - }) - .then(() => { - track.doc.editor.toolbar.copyBlockToLink({ type }); + copyLinkToBlockStdScopeClipboard(str, page?.host?.std.clipboard) + .then(success => { + if (!success) return; + notify.success({ title: I18n['Copied link to clipboard']() }); }) .catch(console.error); + + track.doc.editor.toolbar.copyBlockToLink({ type }); }, }); } diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts index 7380edf9b7834..8795acdf331f5 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts @@ -5,6 +5,7 @@ import { } from '@affine/core/components/hooks/affine/use-share-url'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { EditorService } from '@affine/core/modules/editor'; +import { copyLinkToBlockStdScopeClipboard } from '@affine/core/utils/clipboard'; import { I18n } from '@affine/i18n'; import { track } from '@affine/track'; import type { @@ -77,7 +78,7 @@ function createCopyLinkToBlockMenuItem( ) { return { ...item, - action: (ctx: MenuContext) => { + action: async (ctx: MenuContext) => { const baseUrl = getAffineCloudBaseUrl(); if (!baseUrl) { ctx.close(); @@ -114,18 +115,16 @@ function createCopyLinkToBlockMenuItem( return; } - ctx.std.clipboard - .writeToClipboard(items => { - items['text/plain'] = str; - // wrap a link - items['text/html'] = `${str}`; - return items; - }) - .then(() => { - track.doc.editor.toolbar.copyBlockToLink({ type }); - notify.success({ title: I18n['Copied link to clipboard']() }); - }) - .catch(console.error); + const success = await copyLinkToBlockStdScopeClipboard( + str, + ctx.std.clipboard + ); + + if (success) { + notify.success({ title: I18n['Copied link to clipboard']() }); + } + + track.doc.editor.toolbar.copyBlockToLink({ type }); ctx.close(); }, diff --git a/packages/frontend/core/src/components/hooks/affine/use-share-url.ts b/packages/frontend/core/src/components/hooks/affine/use-share-url.ts index 787b2b353a688..8dc9650856467 100644 --- a/packages/frontend/core/src/components/hooks/affine/use-share-url.ts +++ b/packages/frontend/core/src/components/hooks/affine/use-share-url.ts @@ -1,6 +1,7 @@ import { notify } from '@affine/component'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { toURLSearchParams } from '@affine/core/modules/navigation'; +import { copyTextToClipboard } from '@affine/core/utils/clipboard'; import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { type EditorHost } from '@blocksuite/affine/block-std'; @@ -145,23 +146,18 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => { elementIds, }); if (sharingUrl) { - navigator.clipboard - .writeText(sharingUrl) - .then(() => { - notify.success({ - title: t['Copied link to clipboard'](), - }); + copyTextToClipboard(sharingUrl) + .then(success => { + if (success) { + notify.success({ title: t['Copied link to clipboard']() }); + } }) .catch(err => { console.error(err); }); - track.$.sharePanel.$.copyShareLink({ - type, - }); + track.$.sharePanel.$.copyShareLink({ type }); } else { - notify.error({ - title: 'Network not available', - }); + notify.error({ title: 'Network not available' }); } }, [pageId, t, workspaceId] diff --git a/packages/frontend/core/src/utils/clipboard/fake.ts b/packages/frontend/core/src/utils/clipboard/fake.ts new file mode 100644 index 0000000000000..e240f34cf7432 --- /dev/null +++ b/packages/frontend/core/src/utils/clipboard/fake.ts @@ -0,0 +1,49 @@ +const createFakeElement = (text: string) => { + const isRTL = document.documentElement.getAttribute('dir') === 'rtl'; + const fakeElement = document.createElement('textarea'); + // Prevent zooming on iOS + fakeElement.style.fontSize = '12pt'; + // Reset box model + fakeElement.style.border = '0'; + fakeElement.style.padding = '0'; + fakeElement.style.margin = '0'; + // Move element out of screen horizontally + fakeElement.style.position = 'absolute'; + fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; + // Move element to the same position vertically + const yPosition = window.pageYOffset || document.documentElement.scrollTop; + fakeElement.style.top = `${yPosition}px`; + + fakeElement.setAttribute('readonly', ''); + fakeElement.value = text; + + return fakeElement; +}; + +function command(type: string) { + try { + return document.execCommand(type); + } catch (err) { + console.error(err); + return false; + } +} + +export const fakeCopyAction = (text: string, container = document.body) => { + let success = false; + + const fakeElement = createFakeElement(text); + container.append(fakeElement); + + try { + fakeElement.select(); + fakeElement.setSelectionRange(0, fakeElement.value.length); + success = command('copy'); + } catch (err) { + console.error(err); + } + + fakeElement.remove(); + + return success; +}; diff --git a/packages/frontend/core/src/utils/clipboard/index.ts b/packages/frontend/core/src/utils/clipboard/index.ts new file mode 100644 index 0000000000000..aa60ded16d3f9 --- /dev/null +++ b/packages/frontend/core/src/utils/clipboard/index.ts @@ -0,0 +1,54 @@ +import { type Clipboard as BlockStdScopeClipboard } from '@blocksuite/affine/block-std'; + +import { fakeCopyAction } from './fake'; + +const clipboardWriteIsSupported = + 'clipboard' in navigator && 'write' in navigator.clipboard; + +const clipboardWriteTextIsSupported = + 'clipboard' in navigator && 'writeText' in navigator.clipboard; + +export const copyTextToClipboard = async (text: string) => { + // 1. try using Async API first, works on HTTPS domain + if (clipboardWriteTextIsSupported) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (err) { + console.error(err); + } + } + + // 2. try using `document.execCommand` + // https://github.com/zenorocha/clipboard.js/blob/master/src/actions/copy.js + return fakeCopyAction(text); +}; + +export const copyLinkToBlockStdScopeClipboard = async ( + text: string, + clipboard?: BlockStdScopeClipboard +) => { + let success = false; + + if (!clipboard) return success; + + if (clipboardWriteIsSupported) { + try { + await clipboard.writeToClipboard(items => { + items['text/plain'] = text; + // wrap a link + items['text/html'] = `${text}`; + return items; + }); + success = true; + } catch (error) { + console.error(error); + } + } + + if (!success) { + success = await copyTextToClipboard(text); + } + + return success; +};