diff --git a/src/clipboard.ts b/src/clipboard.ts index 94a2dfa6..5b61dae2 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -8,6 +8,7 @@ import { SVG_EXPORT_TAG } from "./scene/export"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants"; import { isInitializedImageElement } from "./element/typeChecks"; +import { isPromiseLike } from "./utils"; type ElementsClipboard = { type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; @@ -166,10 +167,35 @@ export const parseClipboard = async ( } }; -export const copyBlobToClipboardAsPng = async (blob: Blob) => { - await navigator.clipboard.write([ - new window.ClipboardItem({ [MIME_TYPES.png]: blob }), - ]); +export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { + let promise; + try { + // in Safari so far we need to construct the ClipboardItem synchronously + // (i.e. in the same tick) otherwise browser will complain for lack of + // user intent. Using a Promise ClipboardItem constructor solves this. + // https://bugs.webkit.org/show_bug.cgi?id=222262 + // + // not await so that we can detect whether the thrown error likely relates + // to a lack of support for the Promise ClipboardItem constructor + promise = navigator.clipboard.write([ + new window.ClipboardItem({ + [MIME_TYPES.png]: blob, + }), + ]); + } catch (error: any) { + // if we're using a Promise ClipboardItem, let's try constructing + // with resolution value instead + if (isPromiseLike(blob)) { + await navigator.clipboard.write([ + new window.ClipboardItem({ + [MIME_TYPES.png]: await blob, + }), + ]); + } else { + throw error; + } + } + await promise; }; export const copyTextToSystemClipboard = async (text: string | null) => { diff --git a/src/data/index.ts b/src/data/index.ts index 88966461..5cfaaa97 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -16,7 +16,7 @@ export { loadFromBlob } from "./blob"; export { loadFromJSON, saveAsJSON } from "./json"; export const exportCanvas = async ( - type: ExportType, + type: Omit, elements: readonly NonDeletedExcalidrawElement[], appState: AppState, files: BinaryFiles, @@ -73,10 +73,10 @@ export const exportCanvas = async ( }); tempCanvas.style.display = "none"; document.body.appendChild(tempCanvas); - let blob = await canvasToBlob(tempCanvas); - tempCanvas.remove(); if (type === "png") { + let blob = await canvasToBlob(tempCanvas); + tempCanvas.remove(); if (appState.exportEmbedScene) { blob = await ( await import(/* webpackChunkName: "image" */ "./image") @@ -94,12 +94,19 @@ export const exportCanvas = async ( }); } else if (type === "clipboard") { try { + const blob = canvasToBlob(tempCanvas); await copyBlobToClipboardAsPng(blob); } catch (error: any) { if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { throw error; } throw new Error(t("alerts.couldNotCopyToClipboard")); + } finally { + tempCanvas.remove(); } + } else { + tempCanvas.remove(); + // shouldn't happen + throw new Error("Unsupported export type"); } }; diff --git a/src/locales/en.json b/src/locales/en.json index 5bcaa850..def36a10 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -161,7 +161,7 @@ "couldNotLoadInvalidFile": "Couldn't load invalid file", "importBackendFailed": "Importing from backend failed.", "cannotExportEmptyCanvas": "Cannot export empty canvas.", - "couldNotCopyToClipboard": "Couldn't copy to clipboard. Try using Chrome browser.", + "couldNotCopyToClipboard": "Couldn't copy to clipboard.", "decryptFailed": "Couldn't decrypt data.", "uploadedSecurly": "The upload has been secured with end-to-end encryption, which means that Excalidraw server and third parties can't read the content.", "loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?", diff --git a/src/utils.ts b/src/utils.ts index 7cc08d07..91b67fd2 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -625,3 +625,15 @@ export const getFrame = () => { return "iframe"; } }; + +export const isPromiseLike = ( + value: any, +): value is Promise> => { + return ( + !!value && + typeof value === "object" && + "then" in value && + "catch" in value && + "finally" in value + ); +};