excalidraw/src/clipboard.ts

189 lines
5.2 KiB
TypeScript
Raw Normal View History

import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
} from "./element/types";
import { getSelectedElements } from "./scene";
import { AppState } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, renderSpreadsheet } from "./charts";
2020-02-04 11:50:18 +01:00
let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false;
export const probablySupportsClipboardReadText =
"clipboard" in navigator && "readText" in navigator.clipboard;
2020-02-04 11:50:18 +01:00
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 const copyToAppClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) => {
CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
2020-02-04 11:50:18 +01:00
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 {
2020-02-04 11:50:18 +01:00
// 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;
}
};
2020-02-04 11:50:18 +01:00
export const getAppClipboard = (): {
2020-02-04 11:50:18 +01:00
elements?: readonly ExcalidrawElement[];
} => {
if (!CLIPBOARD) {
return {};
}
2020-02-04 11:50:18 +01:00
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 (error) {
console.error(error);
}
2020-02-04 11:50:18 +01:00
return {};
};
2020-02-04 11:50:18 +01:00
export const getClipboardContent = async (
appState: AppState,
cursorX: number,
cursorY: number,
event: ClipboardEvent | null,
): Promise<{
2020-02-04 11:50:18 +01:00
text?: string;
elements?: readonly ExcalidrawElement[];
error?: string;
}> => {
2020-02-04 11:50:18 +01:00
try {
const text = event
? event.clipboardData?.getData("text/plain").trim()
: probablySupportsClipboardReadText &&
(await navigator.clipboard.readText());
if (text && !PREFER_APP_CLIPBOARD && !text.includes(SVG_EXPORT_TAG)) {
const result = tryParseSpreadsheet(text);
if (result.type === "spreadsheet") {
return {
elements: renderSpreadsheet(
appState,
result.spreadsheet,
cursorX,
cursorY,
),
};
} else if (result.type === "malformed spreadsheet") {
return { error: result.error };
}
2020-02-04 11:50:18 +01:00
return { text };
}
} catch (error) {
console.error(error);
}
2020-02-04 11:50:18 +01:00
return getAppClipboard();
};
2020-02-04 11:50:18 +01:00
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
new Promise((resolve, reject) => {
2020-02-04 11:50:18 +01:00
try {
canvas.toBlob(async (blob: any) => {
2020-02-04 11:50:18 +01:00
try {
await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blob }),
]);
resolve();
} catch (error) {
reject(error);
2020-02-04 11:50:18 +01:00
}
});
} catch (error) {
reject(error);
2020-02-04 11:50:18 +01:00
}
});
export const copyCanvasToClipboardAsSvg = async (svgroot: SVGSVGElement) => {
try {
await navigator.clipboard.writeText(svgroot.outerHTML);
} catch (error) {
console.error(error);
}
};
export const copyTextToSystemClipboard = async (text: string | null) => {
2020-02-04 11:50:18 +01:00
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 (error) {
console.error(error);
}
2020-02-04 11:50:18 +01:00
}
// 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");
}
};
2020-02-04 11:50:18 +01:00
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
const copyTextViaExecCommand = (text: string) => {
2020-02-04 11:50:18 +01:00
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 (error) {
console.error(error);
}
2020-02-04 11:50:18 +01:00
textarea.remove();
return success;
};