Compare commits
1 Commits
master
...
dwelle/cli
Author | SHA1 | Date | |
---|---|---|---|
![]() |
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…
x
Reference in New Issue
Block a user