From 94ad8eaa195ce9188b7695ec806c4f9a8fa34ea1 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Sat, 20 Mar 2021 20:20:47 +0100 Subject: [PATCH] feat: support pasting file contents & always prefer system clip (#3257) --- src/clipboard.ts | 29 ++++++++++++++--------------- src/constants.ts | 8 +++++++- src/data/blob.ts | 4 ++-- src/data/image.ts | 12 +++++++++--- src/data/json.ts | 10 +++++----- src/tests/appState.test.tsx | 3 ++- src/tests/history.test.tsx | 3 ++- 7 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/clipboard.ts b/src/clipboard.ts index 9807972bf..f1654b25f 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -7,12 +7,10 @@ import { AppState } from "./types"; import { SVG_EXPORT_TAG } from "./scene/export"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { canvasToBlob } from "./data/blob"; - -const TYPE_ELEMENTS = "excalidraw/elements"; +import { EXPORT_DATA_TYPES } from "./constants"; type ElementsClipboard = { - type: typeof TYPE_ELEMENTS; - created: number; + type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; elements: ExcalidrawElement[]; }; @@ -31,8 +29,16 @@ export const probablySupportsClipboardBlob = "ClipboardItem" in window && "toBlob" in HTMLCanvasElement.prototype; -const isElementsClipboard = (contents: any): contents is ElementsClipboard => { - if (contents?.type === TYPE_ELEMENTS) { +const clipboardContainsElements = ( + contents: any, +): contents is { elements: ExcalidrawElement[] } => { + if ( + [ + EXPORT_DATA_TYPES.excalidraw, + EXPORT_DATA_TYPES.excalidrawClipboard, + ].includes(contents?.type) && + Array.isArray(contents.elements) + ) { return true; } return false; @@ -43,8 +49,7 @@ export const copyToClipboard = async ( appState: AppState, ) => { const contents: ElementsClipboard = { - type: TYPE_ELEMENTS, - created: Date.now(), + type: EXPORT_DATA_TYPES.excalidrawClipboard, elements: getSelectedElements(elements, appState), }; const json = JSON.stringify(contents); @@ -131,15 +136,9 @@ export const parseClipboard = async ( try { const systemClipboardData = JSON.parse(systemClipboard); - // system clipboard elements are newer than in-app clipboard - if ( - isElementsClipboard(systemClipboardData) && - (!appClipboardData?.created || - appClipboardData.created < systemClipboardData.created) - ) { + if (clipboardContainsElements(systemClipboardData)) { return { elements: systemClipboardData.elements }; } - // in-app clipboard is newer than system clipboard return appClipboardData; } catch { // system clipboard doesn't contain excalidraw elements → return plaintext diff --git a/src/constants.ts b/src/constants.ts index aff57e309..5569d302a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -84,9 +84,15 @@ export const MIME_TYPES = { excalidrawlib: "application/vnd.excalidrawlib+json", }; +export const EXPORT_DATA_TYPES = { + excalidraw: "excalidraw", + excalidrawClipboard: "excalidraw/clipboard", + excalidrawLibrary: "excalidrawlib", +} as const; + export const STORAGE_KEYS = { LOCAL_STORAGE_LIBRARY: "excalidraw-library", -}; +} as const; // time in milliseconds export const TAP_TWICE_TIMEOUT = 300; diff --git a/src/data/blob.ts b/src/data/blob.ts index 81e01dded..21bb9f208 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -1,5 +1,5 @@ import { cleanAppStateForExport } from "../appState"; -import { MIME_TYPES } from "../constants"; +import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { clearElementsForExport } from "../element"; import { CanvasError } from "../errors"; import { t } from "../i18n"; @@ -121,7 +121,7 @@ export const loadFromBlob = async ( export const loadLibraryFromBlob = async (blob: Blob) => { const contents = await parseFileContents(blob); const data: LibraryData = JSON.parse(contents); - if (data.type !== "excalidrawlib") { + if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) { throw new Error(t("alerts.couldNotLoadInvalidFile")); } return data; diff --git a/src/data/image.ts b/src/data/image.ts index 08db76495..c46a4e2f9 100644 --- a/src/data/image.ts +++ b/src/data/image.ts @@ -2,7 +2,7 @@ import decodePng from "png-chunks-extract"; import tEXt from "png-chunk-text"; import encodePng from "png-chunks-encode"; import { stringToBase64, encode, decode, base64ToString } from "./encode"; -import { MIME_TYPES } from "../constants"; +import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; // ----------------------------------------------------------------------------- // PNG @@ -67,7 +67,10 @@ export const decodePngMetadata = async (blob: Blob) => { const encodedData = JSON.parse(metadata.text); if (!("encoded" in encodedData)) { // legacy, un-encoded scene JSON - if ("type" in encodedData && encodedData.type === "excalidraw") { + if ( + "type" in encodedData && + encodedData.type === EXPORT_DATA_TYPES.excalidraw + ) { return metadata.text; } throw new Error("FAILED"); @@ -115,7 +118,10 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => { const encodedData = JSON.parse(json); if (!("encoded" in encodedData)) { // legacy, un-encoded scene JSON - if ("type" in encodedData && encodedData.type === "excalidraw") { + if ( + "type" in encodedData && + encodedData.type === EXPORT_DATA_TYPES.excalidraw + ) { return json; } throw new Error("FAILED"); diff --git a/src/data/json.ts b/src/data/json.ts index 7f9a6de18..4ecaccb3d 100644 --- a/src/data/json.ts +++ b/src/data/json.ts @@ -1,6 +1,6 @@ import { fileOpen, fileSave } from "browser-fs-access"; import { cleanAppStateForExport } from "../appState"; -import { MIME_TYPES } from "../constants"; +import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants"; import { clearElementsForExport } from "../element"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; @@ -14,7 +14,7 @@ export const serializeAsJSON = ( ): string => JSON.stringify( { - type: "excalidraw", + type: EXPORT_DATA_TYPES.excalidraw, version: 2, source: window.location.origin, elements: clearElementsForExport(elements), @@ -69,7 +69,7 @@ export const isValidExcalidrawData = (data?: { appState?: any; }): data is ImportedDataState => { return ( - data?.type === "excalidraw" && + data?.type === EXPORT_DATA_TYPES.excalidraw && (!data.elements || (Array.isArray(data.elements) && (!data.appState || typeof data.appState === "object"))) @@ -80,7 +80,7 @@ export const isValidLibrary = (json: any) => { return ( typeof json === "object" && json && - json.type === "excalidrawlib" && + json.type === EXPORT_DATA_TYPES.excalidrawLibrary && json.version === 1 ); }; @@ -89,7 +89,7 @@ export const saveLibraryAsJSON = async () => { const library = await Library.loadLibrary(); const serialized = JSON.stringify( { - type: "excalidrawlib", + type: EXPORT_DATA_TYPES.excalidrawLibrary, version: 1, library, }, diff --git a/src/tests/appState.test.tsx b/src/tests/appState.test.tsx index f14a8d36b..368e91407 100644 --- a/src/tests/appState.test.tsx +++ b/src/tests/appState.test.tsx @@ -3,6 +3,7 @@ import { render, waitFor } from "./test-utils"; import ExcalidrawApp from "../excalidraw-app"; import { API } from "./helpers/api"; import { getDefaultAppState } from "../appState"; +import { EXPORT_DATA_TYPES } from "../constants"; const { h } = window; @@ -29,7 +30,7 @@ describe("appState", () => { new Blob( [ JSON.stringify({ - type: "excalidraw", + type: EXPORT_DATA_TYPES.excalidraw, appState: { viewBackgroundColor: "#000", }, diff --git a/src/tests/history.test.tsx b/src/tests/history.test.tsx index 4da33439a..aeadaa946 100644 --- a/src/tests/history.test.tsx +++ b/src/tests/history.test.tsx @@ -6,6 +6,7 @@ import { API } from "./helpers/api"; import { getDefaultAppState } from "../appState"; import { waitFor } from "@testing-library/react"; import { createUndoAction, createRedoAction } from "../actions/actionHistory"; +import { EXPORT_DATA_TYPES } from "../constants"; const { h } = window; @@ -76,7 +77,7 @@ describe("history", () => { new Blob( [ JSON.stringify({ - type: "excalidraw", + type: EXPORT_DATA_TYPES.excalidraw, appState: { ...getDefaultAppState(), viewBackgroundColor: "#000",