2020-04-08 09:49:52 -07:00
|
|
|
import {
|
|
|
|
ExcalidrawElement,
|
|
|
|
NonDeletedExcalidrawElement,
|
|
|
|
} from "./element/types";
|
2020-02-16 22:54:50 +01:00
|
|
|
import { getSelectedElements } from "./scene";
|
2020-03-08 10:20:55 -07:00
|
|
|
import { AppState } from "./types";
|
2020-04-06 23:02:17 +02:00
|
|
|
import { SVG_EXPORT_TAG } from "./scene/export";
|
2020-06-06 13:09:04 -07:00
|
|
|
import { tryParseSpreadsheet, renderSpreadsheet } from "./charts";
|
2020-02-04 11:50:18 +01:00
|
|
|
|
|
|
|
let CLIPBOARD = "";
|
|
|
|
let PREFER_APP_CLIPBOARD = false;
|
|
|
|
|
2020-02-07 18:42:24 +01:00
|
|
|
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;
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const copyToAppClipboard = async (
|
2020-04-08 09:49:52 -07:00
|
|
|
elements: readonly NonDeletedExcalidrawElement[],
|
2020-03-08 10:20:55 -07:00
|
|
|
appState: AppState,
|
2020-05-20 16:21:37 +03:00
|
|
|
) => {
|
2020-03-08 10:20:55 -07:00
|
|
|
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;
|
2020-03-07 10:20:38 -05:00
|
|
|
} 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-05-20 16:21:37 +03:00
|
|
|
};
|
2020-02-04 11:50:18 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const getAppClipboard = (): {
|
2020-02-04 11:50:18 +01:00
|
|
|
elements?: readonly ExcalidrawElement[];
|
2020-05-20 16:21:37 +03:00
|
|
|
} => {
|
2020-04-06 23:02:17 +02:00
|
|
|
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 };
|
|
|
|
}
|
2020-02-28 23:03:53 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
2020-02-07 18:42:24 +01:00
|
|
|
}
|
2020-02-04 11:50:18 +01:00
|
|
|
|
|
|
|
return {};
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-02-04 11:50:18 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const getClipboardContent = async (
|
2020-06-06 13:09:04 -07:00
|
|
|
appState: AppState,
|
|
|
|
cursorX: number,
|
|
|
|
cursorY: number,
|
2020-02-28 23:03:53 +01:00
|
|
|
event: ClipboardEvent | null,
|
2020-02-07 18:42:24 +01:00
|
|
|
): Promise<{
|
2020-02-04 11:50:18 +01:00
|
|
|
text?: string;
|
|
|
|
elements?: readonly ExcalidrawElement[];
|
2020-06-06 13:09:04 -07:00
|
|
|
error?: string;
|
2020-05-20 16:21:37 +03:00
|
|
|
}> => {
|
2020-02-04 11:50:18 +01:00
|
|
|
try {
|
2020-02-28 23:03:53 +01:00
|
|
|
const text = event
|
|
|
|
? event.clipboardData?.getData("text/plain").trim()
|
2020-02-07 18:42:24 +01:00
|
|
|
: probablySupportsClipboardReadText &&
|
|
|
|
(await navigator.clipboard.readText());
|
|
|
|
|
2020-04-06 23:02:17 +02:00
|
|
|
if (text && !PREFER_APP_CLIPBOARD && !text.includes(SVG_EXPORT_TAG)) {
|
2020-06-06 13:09:04 -07:00
|
|
|
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 };
|
|
|
|
}
|
2020-02-28 23:03:53 +01:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
2020-02-07 18:42:24 +01:00
|
|
|
}
|
2020-02-04 11:50:18 +01:00
|
|
|
|
2020-02-07 18:42:24 +01:00
|
|
|
return getAppClipboard();
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-02-04 11:50:18 +01:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) =>
|
|
|
|
new Promise((resolve, reject) => {
|
2020-02-04 11:50:18 +01:00
|
|
|
try {
|
2020-05-20 16:21:37 +03:00
|
|
|
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();
|
2020-02-28 23:03:53 +01:00
|
|
|
} catch (error) {
|
|
|
|
reject(error);
|
2020-02-04 11:50:18 +01:00
|
|
|
}
|
|
|
|
});
|
2020-02-28 23:03:53 +01:00
|
|
|
} catch (error) {
|
|
|
|
reject(error);
|
2020-02-04 11:50:18 +01:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const copyCanvasToClipboardAsSvg = async (svgroot: SVGSVGElement) => {
|
2020-04-05 16:13:17 -07:00
|
|
|
try {
|
|
|
|
await navigator.clipboard.writeText(svgroot.outerHTML);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-04-05 16:13:17 -07:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
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;
|
2020-03-07 10:20:38 -05:00
|
|
|
} 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-05-20 16:21:37 +03:00
|
|
|
};
|
2020-02-04 11:50:18 +01:00
|
|
|
|
|
|
|
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
|
2020-05-20 16:21:37 +03:00
|
|
|
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");
|
2020-03-07 10:20:38 -05:00
|
|
|
} catch (error) {
|
|
|
|
console.error(error);
|
|
|
|
}
|
2020-02-04 11:50:18 +01:00
|
|
|
|
|
|
|
textarea.remove();
|
|
|
|
|
|
|
|
return success;
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|