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;
+};