import { CODES, KEYS } from "../keys"; import { register } from "./register"; import { copyTextToSystemClipboard, copyToClipboard, createPasteEvent, probablySupportsClipboardBlob, probablySupportsClipboardWriteText, readSystemClipboard, } from "../clipboard"; import { actionDeleteSelected } from "./actionDeleteSelected"; import { exportCanvas, prepareElementsForExport } from "../data/index"; import { isTextElement } from "../element"; import { t } from "../i18n"; import { isFirefox } from "../constants"; export const actionCopy = register({ name: "copy", trackEvent: { category: "element" }, perform: async (elements, appState, event: ClipboardEvent | null, app) => { const elementsToCopy = app.scene.getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: true, includeElementsInFrames: true, }); try { await copyToClipboard(elementsToCopy, app.files, event); } catch (error: any) { return { commitToHistory: false, appState: { ...appState, errorMessage: error.message, }, }; } return { commitToHistory: false, }; }, contextItemLabel: "labels.copy", // don't supply a shortcut since we handle this conditionally via onCopy event keyTest: undefined, }); export const actionPaste = register({ name: "paste", trackEvent: { category: "element" }, perform: async (elements, appState, data, app) => { let types; try { types = await readSystemClipboard(); } catch (error: any) { if (error.name === "AbortError" || error.name === "NotAllowedError") { // user probably aborted the action. Though not 100% sure, it's best // to not annoy them with an error message. return false; } console.error(`actionPaste ${error.name}: ${error.message}`); if (isFirefox) { return { commitToHistory: false, appState: { ...appState, errorMessage: t("hints.firefox_clipboard_write"), }, }; } return { commitToHistory: false, appState: { ...appState, errorMessage: t("errors.asyncPasteFailedOnRead"), }, }; } try { app.pasteFromClipboard(createPasteEvent({ types })); } catch (error: any) { console.error(error); return { commitToHistory: false, appState: { ...appState, errorMessage: t("errors.asyncPasteFailedOnParse"), }, }; } return { commitToHistory: false, }; }, contextItemLabel: "labels.paste", // don't supply a shortcut since we handle this conditionally via onCopy event keyTest: undefined, }); export const actionCut = register({ name: "cut", trackEvent: { category: "element" }, perform: (elements, appState, event: ClipboardEvent | null, app) => { actionCopy.perform(elements, appState, event, app); return actionDeleteSelected.perform(elements, appState); }, contextItemLabel: "labels.cut", keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X, }); export const actionCopyAsSvg = register({ name: "copyAsSvg", trackEvent: { category: "element" }, perform: async (elements, appState, _data, app) => { if (!app.canvas) { return { commitToHistory: false, }; } const { exportedElements, exportingFrame } = prepareElementsForExport( elements, appState, true, ); try { await exportCanvas( "clipboard-svg", exportedElements, appState, app.files, { ...appState, exportingFrame, }, ); return { commitToHistory: false, }; } catch (error: any) { console.error(error); return { appState: { ...appState, errorMessage: error.message, }, commitToHistory: false, }; } }, predicate: (elements) => { return probablySupportsClipboardWriteText && elements.length > 0; }, contextItemLabel: "labels.copyAsSvg", }); export const actionCopyAsPng = register({ name: "copyAsPng", trackEvent: { category: "element" }, perform: async (elements, appState, _data, app) => { if (!app.canvas) { return { commitToHistory: false, }; } const selectedElements = app.scene.getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: true, includeElementsInFrames: true, }); const { exportedElements, exportingFrame } = prepareElementsForExport( elements, appState, true, ); try { await exportCanvas("clipboard", exportedElements, appState, app.files, { ...appState, exportingFrame, }); return { appState: { ...appState, toast: { message: t("toast.copyToClipboardAsPng", { exportSelection: selectedElements.length ? t("toast.selection") : t("toast.canvas"), exportColorScheme: appState.exportWithDarkMode ? t("buttons.darkMode") : t("buttons.lightMode"), }), }, }, commitToHistory: false, }; } catch (error: any) { console.error(error); return { appState: { ...appState, errorMessage: error.message, }, commitToHistory: false, }; } }, predicate: (elements) => { return probablySupportsClipboardBlob && elements.length > 0; }, contextItemLabel: "labels.copyAsPng", keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey, }); export const copyText = register({ name: "copyText", trackEvent: { category: "element" }, perform: (elements, appState, _, app) => { const selectedElements = app.scene.getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: true, }); const text = selectedElements .reduce((acc: string[], element) => { if (isTextElement(element)) { acc.push(element.text); } return acc; }, []) .join("\n\n"); copyTextToSystemClipboard(text); return { commitToHistory: false, }; }, predicate: (elements, appState, _, app) => { return ( probablySupportsClipboardWriteText && app.scene .getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: true, }) .some(isTextElement) ); }, contextItemLabel: "labels.copyText", });