123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229 |
- import {
- ExcalidrawElement,
- NonDeletedExcalidrawElement,
- } from "./element/types";
- import { getSelectedElements } from "./scene";
- import { AppState, BinaryFiles } from "./types";
- 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";
-
- type ElementsClipboard = {
- type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
- elements: ExcalidrawElement[];
- files: BinaryFiles | undefined;
- };
-
- export interface ClipboardData {
- spreadsheet?: Spreadsheet;
- elements?: readonly ExcalidrawElement[];
- files?: BinaryFiles;
- text?: string;
- errorMessage?: string;
- }
-
- let CLIPBOARD = "";
- let PREFER_APP_CLIPBOARD = false;
-
- export const probablySupportsClipboardReadText =
- "clipboard" in navigator && "readText" in navigator.clipboard;
-
- 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;
-
- const clipboardContainsElements = (
- contents: any,
- ): contents is { elements: ExcalidrawElement[]; files?: BinaryFiles } => {
- if (
- [
- EXPORT_DATA_TYPES.excalidraw,
- EXPORT_DATA_TYPES.excalidrawClipboard,
- ].includes(contents?.type) &&
- Array.isArray(contents.elements)
- ) {
- return true;
- }
- return false;
- };
-
- export const copyToClipboard = async (
- elements: readonly NonDeletedExcalidrawElement[],
- appState: AppState,
- files: BinaryFiles,
- ) => {
- const selectedElements = getSelectedElements(elements, appState);
- const contents: ElementsClipboard = {
- type: EXPORT_DATA_TYPES.excalidrawClipboard,
- elements: selectedElements,
- files: selectedElements.reduce((acc, element) => {
- if (isInitializedImageElement(element) && files[element.fileId]) {
- acc[element.fileId] = files[element.fileId];
- }
- return acc;
- }, {} as BinaryFiles),
- };
- const json = JSON.stringify(contents);
- CLIPBOARD = json;
- try {
- PREFER_APP_CLIPBOARD = false;
- await copyTextToSystemClipboard(json);
- } catch (error) {
- PREFER_APP_CLIPBOARD = true;
- console.error(error);
- }
- };
-
- const getAppClipboard = (): Partial<ElementsClipboard> => {
- if (!CLIPBOARD) {
- return {};
- }
-
- try {
- return JSON.parse(CLIPBOARD);
- } catch (error) {
- console.error(error);
- return {};
- }
- };
-
- const parsePotentialSpreadsheet = (
- text: string,
- ): { spreadsheet: Spreadsheet } | { errorMessage: string } | null => {
- const result = tryParseSpreadsheet(text);
- if (result.type === VALID_SPREADSHEET) {
- return { spreadsheet: result.spreadsheet };
- }
- return null;
- };
-
- /**
- * Retrieves content from system clipboard (either from ClipboardEvent or
- * via async clipboard API if supported)
- */
- const getSystemClipboard = async (
- event: ClipboardEvent | null,
- ): Promise<string> => {
- try {
- const text = event
- ? event.clipboardData?.getData("text/plain").trim()
- : probablySupportsClipboardReadText &&
- (await navigator.clipboard.readText());
-
- return text || "";
- } catch {
- return "";
- }
- };
-
- /**
- * Attemps to parse clipboard. Prefers system clipboard.
- */
- export const parseClipboard = async (
- event: ClipboardEvent | null,
- ): Promise<ClipboardData> => {
- const systemClipboard = await getSystemClipboard(event);
-
- // if system clipboard empty, couldn't be resolved, or contains previously
- // copied excalidraw scene as SVG, fall back to previously copied excalidraw
- // elements
- if (!systemClipboard || systemClipboard.includes(SVG_EXPORT_TAG)) {
- return getAppClipboard();
- }
-
- // if system clipboard contains spreadsheet, use it even though it's
- // technically possible it's staler than in-app clipboard
- const spreadsheetResult = parsePotentialSpreadsheet(systemClipboard);
- if (spreadsheetResult) {
- return spreadsheetResult;
- }
-
- const appClipboardData = getAppClipboard();
-
- try {
- const systemClipboardData = JSON.parse(systemClipboard);
- if (clipboardContainsElements(systemClipboardData)) {
- return {
- elements: systemClipboardData.elements,
- files: systemClipboardData.files,
- };
- }
- return appClipboardData;
- } catch {
- // system clipboard doesn't contain excalidraw elements → return plaintext
- // unless we set a flag to prefer in-app clipboard because browser didn't
- // support storing to system clipboard on copy
- return PREFER_APP_CLIPBOARD && appClipboardData.elements
- ? appClipboardData
- : { text: systemClipboard };
- }
- };
-
- export const copyBlobToClipboardAsPng = async (blob: Blob) => {
- await navigator.clipboard.write([
- new window.ClipboardItem({ [MIME_TYPES.png]: blob }),
- ]);
- };
-
- export const copyTextToSystemClipboard = async (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 (error) {
- console.error(error);
- }
- }
-
- // 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
- const 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 (error) {
- console.error(error);
- }
-
- textarea.remove();
-
- return success;
- };
|