diff --git a/src/components/App.tsx b/src/components/App.tsx index 1bf2d8e6..02e317f7 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -59,6 +59,7 @@ import { ELEMENT_TRANSLATE_AMOUNT, ENV, EVENT, + EXPORT_IMAGE_TYPES, GRID_SIZE, IMAGE_MIME_TYPES, IMAGE_RENDER_TIMEOUT, @@ -82,7 +83,7 @@ import { VERTICAL_ALIGN, ZOOM_STEP, } from "../constants"; -import { loadFromBlob } from "../data"; +import { exportCanvas, loadFromBlob } from "../data"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import { restore, restoreElements } from "../data/restore"; import { @@ -237,6 +238,7 @@ import { getShortcutKey, isTransparent, easeToValuesRAF, + muteFSAbortError, } from "../utils"; import { ContextMenu, @@ -251,6 +253,7 @@ import { generateIdFromFile, getDataURL, getFileFromEvent, + isImageFileHandle, isSupportedImageFile, loadSceneOrLibraryFromBlob, normalizeFile, @@ -618,6 +621,7 @@ class App extends React.Component { } UIOptions={this.props.UIOptions} onImageAction={this.onImageAction} + onExportImage={this.onExportImage} renderWelcomeScreen={ !this.state.isLoading && this.state.showWelcomeScreen && @@ -688,6 +692,37 @@ class App extends React.Component { }); }; + public onExportImage = async ( + type: keyof typeof EXPORT_IMAGE_TYPES, + elements: readonly NonDeletedExcalidrawElement[], + ) => { + trackEvent("export", type, "ui"); + const fileHandle = await exportCanvas( + type, + elements, + this.state, + this.files, + { + exportBackground: this.state.exportBackground, + name: this.state.name, + viewBackgroundColor: this.state.viewBackgroundColor, + }, + ) + .catch(muteFSAbortError) + .catch((error) => { + console.error(error); + this.setState({ errorMessage: error.message }); + }); + + if ( + this.state.exportEmbedScene && + fileHandle && + isImageFileHandle(fileHandle) + ) { + this.setState({ fileHandle }); + } + }; + private syncActionResult = withBatchedUpdates( (actionResult: ActionResult) => { if (this.unmounted || actionResult === false) { diff --git a/src/components/ImageExportDialog.tsx b/src/components/ImageExportDialog.tsx index 3f8a9679..38467d6e 100644 --- a/src/components/ImageExportDialog.tsx +++ b/src/components/ImageExportDialog.tsx @@ -4,13 +4,17 @@ import { canvasToBlob } from "../data/blob"; import { NonDeletedExcalidrawElement } from "../element/types"; import { t } from "../i18n"; import { getSelectedElements, isSomeElementSelected } from "../scene"; -import { BinaryFiles, UIAppState } from "../types"; +import { AppClassProperties, BinaryFiles, UIAppState } from "../types"; import { Dialog } from "./Dialog"; import { clipboard } from "./icons"; import Stack from "./Stack"; import OpenColor from "open-color"; import { CheckboxItem } from "./CheckboxItem"; -import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; +import { + DEFAULT_EXPORT_PADDING, + EXPORT_IMAGE_TYPES, + isFirefox, +} from "../constants"; import { nativeFileSystemSupported } from "../data/filesystem"; import { ActionManager } from "../actions/manager"; import { exportToCanvas } from "../packages/utils"; @@ -65,21 +69,14 @@ const ImageExportModal = ({ elements, appState, files, - exportPadding = DEFAULT_EXPORT_PADDING, actionManager, - onExportToPng, - onExportToSvg, - onExportToClipboard, + onExportImage, }: { appState: UIAppState; elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; - exportPadding?: number; actionManager: ActionManager; - onExportToPng: ExportCB; - onExportToSvg: ExportCB; - onExportToClipboard: ExportCB; - onCloseRequest: () => void; + onExportImage: AppClassProperties["onExportImage"]; }) => { const someElementIsSelected = isSomeElementSelected(elements, appState); const [exportSelected, setExportSelected] = useState(someElementIsSelected); @@ -90,10 +87,6 @@ const ImageExportModal = ({ ? getSelectedElements(elements, appState, true) : elements; - useEffect(() => { - setExportSelected(someElementIsSelected); - }, [someElementIsSelected]); - useEffect(() => { const previewNode = previewRef.current; if (!previewNode) { @@ -107,7 +100,7 @@ const ImageExportModal = ({ elements: exportedElements, appState, files, - exportPadding, + exportPadding: DEFAULT_EXPORT_PADDING, maxWidthOrHeight: maxWidth, }) .then((canvas) => { @@ -122,7 +115,7 @@ const ImageExportModal = ({ console.error(error); setRenderError(error); }); - }, [appState, files, exportedElements, exportPadding]); + }, [appState, files, exportedElements]); return (
@@ -177,7 +170,9 @@ const ImageExportModal = ({ color="indigo" title={t("buttons.exportToPng")} aria-label={t("buttons.exportToPng")} - onClick={() => onExportToPng(exportedElements)} + onClick={() => + onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements) + } > PNG @@ -185,7 +180,9 @@ const ImageExportModal = ({ color="red" title={t("buttons.exportToSvg")} aria-label={t("buttons.exportToSvg")} - onClick={() => onExportToSvg(exportedElements)} + onClick={() => + onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements) + } > SVG @@ -194,7 +191,9 @@ const ImageExportModal = ({ {(probablySupportsClipboardBlob || isFirefox) && ( onExportToClipboard(exportedElements)} + onClick={() => + onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements) + } color="gray" shade={7} > @@ -209,45 +208,31 @@ const ImageExportModal = ({ export const ImageExportDialog = ({ elements, appState, - setAppState, files, - exportPadding = DEFAULT_EXPORT_PADDING, actionManager, - onExportToPng, - onExportToSvg, - onExportToClipboard, + onExportImage, + onCloseRequest, }: { appState: UIAppState; - setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; files: BinaryFiles; - exportPadding?: number; actionManager: ActionManager; - onExportToPng: ExportCB; - onExportToSvg: ExportCB; - onExportToClipboard: ExportCB; + onExportImage: AppClassProperties["onExportImage"]; + onCloseRequest: () => void; }) => { - const handleClose = React.useCallback(() => { - setAppState({ openDialog: null }); - }, [setAppState]); + if (appState.openDialog !== "imageExport") { + return null; + } return ( - <> - {appState.openDialog === "imageExport" && ( - - - - )} - + + + ); }; diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 0e08eafa..f05f5df1 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -2,23 +2,22 @@ import clsx from "clsx"; import React from "react"; import { ActionManager } from "../actions/manager"; import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants"; -import { exportCanvas } from "../data"; import { isTextElement, showSelectedShapeActions } from "../element"; import { NonDeletedExcalidrawElement } from "../element/types"; import { Language, t } from "../i18n"; import { calculateScrollCenter } from "../scene"; -import { ExportType } from "../scene/types"; import { AppProps, AppState, ExcalidrawProps, BinaryFiles, UIAppState, + AppClassProperties, } from "../types"; -import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils"; +import { capitalizeString, isShallowEqual } from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { ErrorDialog } from "./ErrorDialog"; -import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; +import { ImageExportDialog } from "./ImageExportDialog"; import { FixedSideContainer } from "./FixedSideContainer"; import { HintViewer } from "./HintViewer"; import { Island } from "./Island"; @@ -31,7 +30,6 @@ import { HelpDialog } from "./HelpDialog"; import Stack from "./Stack"; import { UserList } from "./UserList"; import { JSONExportDialog } from "./JSONExportDialog"; -import { isImageFileHandle } from "../data/blob"; import { PenModeButton } from "./PenModeButton"; import { trackEvent } from "../analytics"; import { useDevice } from "../components/App"; @@ -69,6 +67,7 @@ interface LayerUIProps { renderCustomStats?: ExcalidrawProps["renderCustomStats"]; UIOptions: AppProps["UIOptions"]; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; + onExportImage: AppClassProperties["onExportImage"]; renderWelcomeScreen: boolean; children?: React.ReactNode; } @@ -114,6 +113,7 @@ const LayerUI = ({ renderCustomStats, UIOptions, onImageAction, + onExportImage, renderWelcomeScreen, children, }: LayerUIProps) => { @@ -143,47 +143,14 @@ const LayerUI = ({ return null; } - const createExporter = - (type: ExportType): ExportCB => - async (exportedElements) => { - trackEvent("export", type, "ui"); - const fileHandle = await exportCanvas( - type, - exportedElements, - // FIXME once we split UI canvas from element canvas - appState as AppState, - files, - { - exportBackground: appState.exportBackground, - name: appState.name, - viewBackgroundColor: appState.viewBackgroundColor, - }, - ) - .catch(muteFSAbortError) - .catch((error) => { - console.error(error); - setAppState({ errorMessage: error.message }); - }); - - if ( - appState.exportEmbedScene && - fileHandle && - isImageFileHandle(fileHandle) - ) { - setAppState({ fileHandle }); - } - }; - return ( setAppState({ openDialog: null })} /> ); }; diff --git a/src/constants.ts b/src/constants.ts index 5b59679c..e7241e17 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -131,6 +131,12 @@ export const MIME_TYPES = { ...IMAGE_MIME_TYPES, } as const; +export const EXPORT_IMAGE_TYPES = { + png: "png", + svg: "svg", + clipboard: "clipboard", +} as const; + export const EXPORT_DATA_TYPES = { excalidraw: "excalidraw", excalidrawClipboard: "excalidraw/clipboard", diff --git a/src/types.ts b/src/types.ts index 8aa07d3c..666f9e03 100644 --- a/src/types.ts +++ b/src/types.ts @@ -442,6 +442,7 @@ export type AppClassProperties = { pasteFromClipboard: App["pasteFromClipboard"]; id: App["id"]; onInsertElements: App["onInsertElements"]; + onExportImage: App["onExportImage"]; }; export type PointerDownState = Readonly<{