mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 10:54:33 +01:00 
			
		
		
		
	fix: support copying PNG to clipboard on Safari (#3746)
This commit is contained in:
		| @@ -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) => { | ||||
|   | ||||
| @@ -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"); | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -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
									
								
								src/utils.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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 | ||||
|   ); | ||||
| }; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 David Luzar
					David Luzar