debug: clipboard

dwelle/clipboard-debug
dwelle 1 year ago
parent ec2de7205f
commit 348912f32f

@ -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,

@ -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);

@ -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;
}

@ -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");
});
}}

Loading…
Cancel
Save