ソースを参照

debug: clipboard

dwelle 1 年間 前
コミット
348912f32f
4 ファイル変更118 行追加27 行削除
  1. 94 10
      src/actions/actionClipboard.tsx
  2. 4 7
      src/clipboard.ts
  3. 15 1
      src/components/App.tsx
  4. 5 9
      src/components/ContextMenu.tsx

+ 94 - 10
src/actions/actionClipboard.tsx

@@ -10,26 +10,34 @@ import { actionDeleteSelected } from "./actionDeleteSelected";
 import { exportCanvas } from "../data/index";
 import { getNonDeletedElements, isTextElement } from "../element";
 import { t } from "../i18n";
+import { isFirefox } from "../constants";
 
 export const actionCopy = register({
   name: "copy",
   trackEvent: { category: "element" },
-  perform: (elements, appState, _, app) => {
+  perform: async (elements, appState, _, app) => {
     const elementsToCopy = app.scene.getSelectedElements({
       selectedElementIds: appState.selectedElementIds,
       includeBoundTextElement: true,
       includeElementsInFrames: true,
     });
 
-    copyToClipboard(elementsToCopy, app.files);
+    try {
+      await copyToClipboard(elementsToCopy, app.files);
+    } catch (error: any) {
+      return {
+        commitToHistory: false,
+        appState: {
+          ...appState,
+          errorMessage: error.message,
+        },
+      };
+    }
 
     return {
       commitToHistory: false,
     };
   },
-  predicate: (elements, appState, appProps, app) => {
-    return app.device.isMobile && !!navigator.clipboard;
-  },
   contextItemLabel: "labels.copy",
   // don't supply a shortcut since we handle this conditionally via onCopy event
   keyTest: undefined,
@@ -38,15 +46,91 @@ export const actionCopy = register({
 export const actionPaste = register({
   name: "paste",
   trackEvent: { category: "element" },
-  perform: (elements: any, appStates: any, data, app) => {
-    app.pasteFromClipboard(null);
+  perform: async (elements, appState, data, app) => {
+    const MIME_TYPES: Record<string, string> = {};
+    try {
+      try {
+        const clipboardItems = await navigator.clipboard?.read();
+        for (const item of clipboardItems) {
+          for (const type of item.types) {
+            try {
+              const blob = await item.getType(type);
+              MIME_TYPES[type] = await blob.text();
+            } catch (error: any) {
+              console.warn(
+                `Cannot retrieve ${type} from clipboardItem: ${error.message}`,
+              );
+            }
+          }
+        }
+        if (Object.keys(MIME_TYPES).length === 0) {
+          console.warn(
+            "No clipboard data found from clipboard.read(). Falling back to clipboard.readText()",
+          );
+          // throw so we fall back onto clipboard.readText()
+          throw new Error("No clipboard data found");
+        }
+      } catch (error: any) {
+        try {
+          MIME_TYPES["text/plain"] = await navigator.clipboard?.readText();
+        } catch (error: any) {
+          console.warn(`Cannot readText() from clipboard: ${error.message}`);
+          if (isFirefox) {
+            return {
+              commitToHistory: false,
+              appState: {
+                ...appState,
+                errorMessage: t("hints.firefox_clipboard_write"),
+              },
+            };
+          }
+          throw error;
+        }
+      }
+    } catch (error: any) {
+      console.error(`actionPaste: ${error.message}`);
+      return {
+        commitToHistory: false,
+        appState: {
+          ...appState,
+          errorMessage: error.message,
+        },
+      };
+    }
+    try {
+      console.log("actionPaste (1)", { MIME_TYPES });
+      const event = new ClipboardEvent("paste", {
+        clipboardData: new DataTransfer(),
+      });
+      for (const [type, value] of Object.entries(MIME_TYPES)) {
+        try {
+          event.clipboardData?.setData(type, value);
+        } catch (error: any) {
+          console.warn(
+            `Cannot set ${type} as clipboardData item: ${error.message}`,
+          );
+        }
+      }
+      event.clipboardData?.types.forEach((type) => {
+        console.log(
+          `actionPaste (2) event.clipboardData?.getData(${type})`,
+          event.clipboardData?.getData(type),
+        );
+      });
+      app.pasteFromClipboard(event);
+    } catch (error: any) {
+      return {
+        commitToHistory: false,
+        appState: {
+          ...appState,
+          errorMessage: error.message,
+        },
+      };
+    }
     return {
       commitToHistory: false,
     };
   },
-  predicate: (elements, appState, appProps, app) => {
-    return app.device.isMobile && !!navigator.clipboard;
-  },
   contextItemLabel: "labels.paste",
   // don't supply a shortcut since we handle this conditionally via onCopy event
   keyTest: undefined,

+ 4 - 7
src/clipboard.ts

@@ -118,7 +118,7 @@ export const copyToClipboard = async (
     await copyTextToSystemClipboard(json);
   } catch (error: any) {
     PREFER_APP_CLIPBOARD = true;
-    console.error(error);
+    throw error;
   }
 };
 
@@ -193,7 +193,7 @@ const maybeParseHTMLPaste = (event: ClipboardEvent) => {
  *  via async clipboard API if supported)
  */
 const getSystemClipboard = async (
-  event: ClipboardEvent | null,
+  event: ClipboardEvent,
   isPlainPaste = false,
 ): Promise<
   | { type: "text"; value: string }
@@ -205,10 +205,7 @@ const getSystemClipboard = async (
       return { type: "mixedContent", value: mixedContent };
     }
 
-    const text = event
-      ? event.clipboardData?.getData("text/plain")
-      : probablySupportsClipboardReadText &&
-        (await navigator.clipboard.readText());
+    const text = event.clipboardData?.getData("text/plain");
 
     return { type: "text", value: (text || "").trim() };
   } catch {
@@ -220,7 +217,7 @@ const getSystemClipboard = async (
  * Attempts to parse clipboard. Prefers system clipboard.
  */
 export const parseClipboard = async (
-  event: ClipboardEvent | null,
+  event: ClipboardEvent,
   isPlainPaste = false,
 ): Promise<ClipboardData> => {
   const systemClipboard = await getSystemClipboard(event, isPlainPaste);

+ 15 - 1
src/components/App.tsx

@@ -1275,6 +1275,12 @@ class App extends React.Component<AppProps, AppState> {
                             top={this.state.contextMenu.top}
                             left={this.state.contextMenu.left}
                             actionManager={this.actionManager}
+                            onClose={(cb) => {
+                              this.setState({ contextMenu: null }, () => {
+                                this.focusContainer();
+                                cb?.();
+                              });
+                            }}
                           />
                         )}
                         <StaticCanvas
@@ -2195,14 +2201,21 @@ class App extends React.Component<AppProps, AppState> {
   };
 
   public pasteFromClipboard = withBatchedUpdates(
-    async (event: ClipboardEvent | null) => {
+    async (event: ClipboardEvent) => {
       const isPlainPaste = !!(IS_PLAIN_PASTE && event);
 
+      console.warn(
+        "pasteFromClipboard",
+        event?.clipboardData?.types,
+        event?.clipboardData?.getData("text/plain"),
+      );
+
       // #686
       const target = document.activeElement;
       const isExcalidrawActive =
         this.excalidrawContainerRef.current?.contains(target);
       if (event && !isExcalidrawActive) {
+        console.log("exit (1)");
         return;
       }
 
@@ -2215,6 +2228,7 @@ class App extends React.Component<AppProps, AppState> {
         (!(elementUnderCursor instanceof HTMLCanvasElement) ||
           isWritableElement(target))
       ) {
+        console.log("exit (2)");
         return;
       }
 

+ 5 - 9
src/components/ContextMenu.tsx

@@ -9,11 +9,7 @@ import {
 } from "../actions/shortcuts";
 import { Action } from "../actions/types";
 import { ActionManager } from "../actions/manager";
-import {
-  useExcalidrawAppState,
-  useExcalidrawElements,
-  useExcalidrawSetAppState,
-} from "./App";
+import { useExcalidrawAppState, useExcalidrawElements } from "./App";
 import React from "react";
 
 export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
@@ -25,14 +21,14 @@ type ContextMenuProps = {
   items: ContextMenuItems;
   top: number;
   left: number;
+  onClose: (cb?: () => void) => void;
 };
 
 export const CONTEXT_MENU_SEPARATOR = "separator";
 
 export const ContextMenu = React.memo(
-  ({ actionManager, items, top, left }: ContextMenuProps) => {
+  ({ actionManager, items, top, left, onClose }: ContextMenuProps) => {
     const appState = useExcalidrawAppState();
-    const setAppState = useExcalidrawSetAppState();
     const elements = useExcalidrawElements();
 
     const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
@@ -54,7 +50,7 @@ export const ContextMenu = React.memo(
 
     return (
       <Popover
-        onCloseRequest={() => setAppState({ contextMenu: null })}
+        onCloseRequest={() => onClose()}
         top={top}
         left={left}
         fitInViewport={true}
@@ -102,7 +98,7 @@ export const ContextMenu = React.memo(
                   // we need update state before executing the action in case
                   // the action uses the appState it's being passed (that still
                   // contains a defined contextMenu) to return the next state.
-                  setAppState({ contextMenu: null }, () => {
+                  onClose(() => {
                     actionManager.executeAction(item, "contextMenu");
                   });
                 }}