ソースを参照

feat: support pasting file contents & always prefer system clip (#3257)

David Luzar 4 年 前
コミット
94ad8eaa19
7 ファイル変更41 行追加28 行削除
  1. 14 15
      src/clipboard.ts
  2. 7 1
      src/constants.ts
  3. 2 2
      src/data/blob.ts
  4. 9 3
      src/data/image.ts
  5. 5 5
      src/data/json.ts
  6. 2 1
      src/tests/appState.test.tsx
  7. 2 1
      src/tests/history.test.tsx

+ 14 - 15
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

+ 7 - 1
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;

+ 2 - 2
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;

+ 9 - 3
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");

+ 5 - 5
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,
     },

+ 2 - 1
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",
             },

+ 2 - 1
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",