fix: support copying PNG to clipboard on Safari (#3746)
This commit is contained in:
parent
77d789ed8e
commit
cb6b7559b4
@ -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
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user