Forráskód Böngészése

fix: support copying PNG to clipboard on Safari (#3746)

David Luzar 3 éve
szülő
commit
cb6b7559b4
4 módosított fájl, 53 hozzáadás és 8 törlés
  1. 30 4
      src/clipboard.ts
  2. 10 3
      src/data/index.ts
  3. 1 1
      src/locales/en.json
  4. 12 0
      src/utils.ts

+ 30 - 4
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<Blob>) => {
+  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) => {

+ 10 - 3
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<ExportType, "backend">,
   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");
   }
 };

+ 1 - 1
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?",

+ 12 - 0
src/utils.ts

@@ -625,3 +625,15 @@ export const getFrame = () => {
     return "iframe";
   }
 };
+
+export const isPromiseLike = (
+  value: any,
+): value is Promise<ResolutionType<typeof value>> => {
+  return (
+    !!value &&
+    typeof value === "object" &&
+    "then" in value &&
+    "catch" in value &&
+    "finally" in value
+  );
+};