excalidraw/src/clipboard.ts

145 lines
4.0 KiB
TypeScript
Raw Normal View History

2020-02-04 11:50:18 +01:00
import { ExcalidrawElement } from "./element/types";
let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false;
export const probablySupportsClipboardWriteText =
"clipboard" in navigator && "writeText" in navigator.clipboard;
export const probablySupportsClipboardBlob =
"clipboard" in navigator &&
"write" in navigator.clipboard &&
"ClipboardItem" in window &&
"toBlob" in HTMLCanvasElement.prototype;
export async function copyToAppClipboard(
elements: readonly ExcalidrawElement[],
) {
CLIPBOARD = JSON.stringify(
elements
.filter(element => element.isSelected)
.map(({ shape, ...el }) => el),
);
try {
// when copying to in-app clipboard, clear system clipboard so that if
// system clip contains text on paste we know it was copied *after* user
// copied elements, and thus we should prefer the text content.
await copyTextToSystemClipboard(null);
PREFER_APP_CLIPBOARD = false;
} catch (err) {
// if clearing system clipboard didn't work, we should prefer in-app
// clipboard even if there's text in system clipboard on paste, because
// we can't be sure of the order of copy operations
PREFER_APP_CLIPBOARD = true;
}
}
export function getAppClipboard(): {
elements?: readonly ExcalidrawElement[];
} {
try {
const clipboardElements = JSON.parse(CLIPBOARD);
if (
Array.isArray(clipboardElements) &&
clipboardElements.length > 0 &&
clipboardElements[0].type // need to implement a better check here...
) {
return { elements: clipboardElements };
}
} catch (err) {}
return {};
}
export function parseClipboardEvent(
e: ClipboardEvent,
): {
text?: string;
elements?: readonly ExcalidrawElement[];
} {
try {
const text = e.clipboardData?.getData("text/plain").trim();
if (text && !PREFER_APP_CLIPBOARD) {
return { text };
// eslint-disable-next-line no-else-return
} else {
return getAppClipboard();
}
} catch (e) {}
return {};
}
export async function copyCanvasToClipboardAsPng(canvas: HTMLCanvasElement) {
return new Promise((resolve, reject) => {
try {
canvas.toBlob(async function(blob: any) {
try {
await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blob }),
]);
resolve();
} catch (err) {
reject(err);
}
});
} catch (err) {
reject(err);
}
});
}
export async function copyTextToSystemClipboard(text: string | null) {
let copied = false;
if (probablySupportsClipboardWriteText) {
try {
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
// not focused
await navigator.clipboard.writeText(text || "");
copied = true;
} catch (err) {}
}
// Note that execCommand doesn't allow copying empty strings, so if we're
// clearing clipboard using this API, we must copy at least an empty char
if (!copied && !copyTextViaExecCommand(text || " ")) {
throw new Error("couldn't copy");
}
}
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
function copyTextViaExecCommand(text: string) {
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const textarea = document.createElement("textarea");
textarea.style.border = "0";
textarea.style.padding = "0";
textarea.style.margin = "0";
textarea.style.position = "absolute";
textarea.style[isRTL ? "right" : "left"] = "-9999px";
const yPosition = window.pageYOffset || document.documentElement.scrollTop;
textarea.style.top = `${yPosition}px`;
// Prevent zooming on iOS
textarea.style.fontSize = "12pt";
textarea.setAttribute("readonly", "");
textarea.value = text;
document.body.appendChild(textarea);
let success = false;
try {
textarea.select();
textarea.setSelectionRange(0, textarea.value.length);
success = document.execCommand("copy");
} catch (err) {}
textarea.remove();
return success;
}