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 { exportCanvas } from "../data/index";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
import { t } from "../i18n"; import { t } from "../i18n";
import { isFirefox } from "../constants";
export const actionCopy = register({ export const actionCopy = register({
name: "copy", name: "copy",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: async (elements, appState, _, app) => {
const elementsToCopy = app.scene.getSelectedElements({ const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: 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 { return {
commitToHistory: false, commitToHistory: false,
}; };
}, },
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.copy", contextItemLabel: "labels.copy",
// don't supply a shortcut since we handle this conditionally via onCopy event // don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: undefined, keyTest: undefined,
@ -38,15 +46,91 @@ export const actionCopy = register({
export const actionPaste = register({ export const actionPaste = register({
name: "paste", name: "paste",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements: any, appStates: any, data, app) => { perform: async (elements, appState, data, app) => {
app.pasteFromClipboard(null); 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 { return {
commitToHistory: false, commitToHistory: false,
}; };
}, },
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.paste", contextItemLabel: "labels.paste",
// don't supply a shortcut since we handle this conditionally via onCopy event // don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: undefined, keyTest: undefined,

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

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

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

Loading…
Cancel
Save