refactor: simplify ImageExportDialog
(#6578)
This commit is contained in:
parent
b1b325b9a7
commit
f6f9ed0396
@ -59,6 +59,7 @@ import {
|
|||||||
ELEMENT_TRANSLATE_AMOUNT,
|
ELEMENT_TRANSLATE_AMOUNT,
|
||||||
ENV,
|
ENV,
|
||||||
EVENT,
|
EVENT,
|
||||||
|
EXPORT_IMAGE_TYPES,
|
||||||
GRID_SIZE,
|
GRID_SIZE,
|
||||||
IMAGE_MIME_TYPES,
|
IMAGE_MIME_TYPES,
|
||||||
IMAGE_RENDER_TIMEOUT,
|
IMAGE_RENDER_TIMEOUT,
|
||||||
@ -82,7 +83,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
ZOOM_STEP,
|
ZOOM_STEP,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { loadFromBlob } from "../data";
|
import { exportCanvas, loadFromBlob } from "../data";
|
||||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
import { restore, restoreElements } from "../data/restore";
|
import { restore, restoreElements } from "../data/restore";
|
||||||
import {
|
import {
|
||||||
@ -237,6 +238,7 @@ import {
|
|||||||
getShortcutKey,
|
getShortcutKey,
|
||||||
isTransparent,
|
isTransparent,
|
||||||
easeToValuesRAF,
|
easeToValuesRAF,
|
||||||
|
muteFSAbortError,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@ -251,6 +253,7 @@ import {
|
|||||||
generateIdFromFile,
|
generateIdFromFile,
|
||||||
getDataURL,
|
getDataURL,
|
||||||
getFileFromEvent,
|
getFileFromEvent,
|
||||||
|
isImageFileHandle,
|
||||||
isSupportedImageFile,
|
isSupportedImageFile,
|
||||||
loadSceneOrLibraryFromBlob,
|
loadSceneOrLibraryFromBlob,
|
||||||
normalizeFile,
|
normalizeFile,
|
||||||
@ -618,6 +621,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
UIOptions={this.props.UIOptions}
|
UIOptions={this.props.UIOptions}
|
||||||
onImageAction={this.onImageAction}
|
onImageAction={this.onImageAction}
|
||||||
|
onExportImage={this.onExportImage}
|
||||||
renderWelcomeScreen={
|
renderWelcomeScreen={
|
||||||
!this.state.isLoading &&
|
!this.state.isLoading &&
|
||||||
this.state.showWelcomeScreen &&
|
this.state.showWelcomeScreen &&
|
||||||
@ -688,6 +692,37 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(
|
private syncActionResult = withBatchedUpdates(
|
||||||
(actionResult: ActionResult) => {
|
(actionResult: ActionResult) => {
|
||||||
if (this.unmounted || actionResult === false) {
|
if (this.unmounted || actionResult === false) {
|
||||||
|
@ -4,13 +4,17 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||||
import { BinaryFiles, UIAppState } from "../types";
|
import { AppClassProperties, BinaryFiles, UIAppState } from "../types";
|
||||||
import { Dialog } from "./Dialog";
|
import { Dialog } from "./Dialog";
|
||||||
import { clipboard } from "./icons";
|
import { clipboard } from "./icons";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import OpenColor from "open-color";
|
import OpenColor from "open-color";
|
||||||
import { CheckboxItem } from "./CheckboxItem";
|
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 { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { exportToCanvas } from "../packages/utils";
|
import { exportToCanvas } from "../packages/utils";
|
||||||
@ -65,21 +69,14 @@ const ImageExportModal = ({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
files,
|
files,
|
||||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportToPng,
|
onExportImage,
|
||||||
onExportToSvg,
|
|
||||||
onExportToClipboard,
|
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
exportPadding?: number;
|
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onExportToPng: ExportCB;
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
onExportToSvg: ExportCB;
|
|
||||||
onExportToClipboard: ExportCB;
|
|
||||||
onCloseRequest: () => void;
|
|
||||||
}) => {
|
}) => {
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
||||||
@ -90,10 +87,6 @@ const ImageExportModal = ({
|
|||||||
? getSelectedElements(elements, appState, true)
|
? getSelectedElements(elements, appState, true)
|
||||||
: elements;
|
: elements;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setExportSelected(someElementIsSelected);
|
|
||||||
}, [someElementIsSelected]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previewNode = previewRef.current;
|
const previewNode = previewRef.current;
|
||||||
if (!previewNode) {
|
if (!previewNode) {
|
||||||
@ -107,7 +100,7 @@ const ImageExportModal = ({
|
|||||||
elements: exportedElements,
|
elements: exportedElements,
|
||||||
appState,
|
appState,
|
||||||
files,
|
files,
|
||||||
exportPadding,
|
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||||
maxWidthOrHeight: maxWidth,
|
maxWidthOrHeight: maxWidth,
|
||||||
})
|
})
|
||||||
.then((canvas) => {
|
.then((canvas) => {
|
||||||
@ -122,7 +115,7 @@ const ImageExportModal = ({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
setRenderError(error);
|
setRenderError(error);
|
||||||
});
|
});
|
||||||
}, [appState, files, exportedElements, exportPadding]);
|
}, [appState, files, exportedElements]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ExportDialog">
|
<div className="ExportDialog">
|
||||||
@ -177,7 +170,9 @@ const ImageExportModal = ({
|
|||||||
color="indigo"
|
color="indigo"
|
||||||
title={t("buttons.exportToPng")}
|
title={t("buttons.exportToPng")}
|
||||||
aria-label={t("buttons.exportToPng")}
|
aria-label={t("buttons.exportToPng")}
|
||||||
onClick={() => onExportToPng(exportedElements)}
|
onClick={() =>
|
||||||
|
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
PNG
|
PNG
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
@ -185,7 +180,9 @@ const ImageExportModal = ({
|
|||||||
color="red"
|
color="red"
|
||||||
title={t("buttons.exportToSvg")}
|
title={t("buttons.exportToSvg")}
|
||||||
aria-label={t("buttons.exportToSvg")}
|
aria-label={t("buttons.exportToSvg")}
|
||||||
onClick={() => onExportToSvg(exportedElements)}
|
onClick={() =>
|
||||||
|
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
SVG
|
SVG
|
||||||
</ExportButton>
|
</ExportButton>
|
||||||
@ -194,7 +191,9 @@ const ImageExportModal = ({
|
|||||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
<ExportButton
|
<ExportButton
|
||||||
title={t("buttons.copyPngToClipboard")}
|
title={t("buttons.copyPngToClipboard")}
|
||||||
onClick={() => onExportToClipboard(exportedElements)}
|
onClick={() =>
|
||||||
|
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
||||||
|
}
|
||||||
color="gray"
|
color="gray"
|
||||||
shade={7}
|
shade={7}
|
||||||
>
|
>
|
||||||
@ -209,45 +208,31 @@ const ImageExportModal = ({
|
|||||||
export const ImageExportDialog = ({
|
export const ImageExportDialog = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
setAppState,
|
|
||||||
files,
|
files,
|
||||||
exportPadding = DEFAULT_EXPORT_PADDING,
|
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportToPng,
|
onExportImage,
|
||||||
onExportToSvg,
|
onCloseRequest,
|
||||||
onExportToClipboard,
|
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
exportPadding?: number;
|
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onExportToPng: ExportCB;
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
onExportToSvg: ExportCB;
|
onCloseRequest: () => void;
|
||||||
onExportToClipboard: ExportCB;
|
|
||||||
}) => {
|
}) => {
|
||||||
const handleClose = React.useCallback(() => {
|
if (appState.openDialog !== "imageExport") {
|
||||||
setAppState({ openDialog: null });
|
return null;
|
||||||
}, [setAppState]);
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog onCloseRequest={onCloseRequest} title={t("buttons.exportImage")}>
|
||||||
{appState.openDialog === "imageExport" && (
|
<ImageExportModal
|
||||||
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
|
elements={elements}
|
||||||
<ImageExportModal
|
appState={appState}
|
||||||
elements={elements}
|
files={files}
|
||||||
appState={appState}
|
actionManager={actionManager}
|
||||||
files={files}
|
onExportImage={onExportImage}
|
||||||
exportPadding={exportPadding}
|
/>
|
||||||
actionManager={actionManager}
|
</Dialog>
|
||||||
onExportToPng={onExportToPng}
|
|
||||||
onExportToSvg={onExportToSvg}
|
|
||||||
onExportToClipboard={onExportToClipboard}
|
|
||||||
onCloseRequest={handleClose}
|
|
||||||
/>
|
|
||||||
</Dialog>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -2,23 +2,22 @@ import clsx from "clsx";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
import { CLASSES, DEFAULT_SIDEBAR, LIBRARY_SIDEBAR_WIDTH } from "../constants";
|
||||||
import { exportCanvas } from "../data";
|
|
||||||
import { isTextElement, showSelectedShapeActions } from "../element";
|
import { isTextElement, showSelectedShapeActions } from "../element";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { Language, t } from "../i18n";
|
import { Language, t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { ExportType } from "../scene/types";
|
|
||||||
import {
|
import {
|
||||||
AppProps,
|
AppProps,
|
||||||
AppState,
|
AppState,
|
||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
UIAppState,
|
UIAppState,
|
||||||
|
AppClassProperties,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils";
|
import { capitalizeString, isShallowEqual } from "../utils";
|
||||||
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "./ErrorDialog";
|
||||||
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
|
import { ImageExportDialog } from "./ImageExportDialog";
|
||||||
import { FixedSideContainer } from "./FixedSideContainer";
|
import { FixedSideContainer } from "./FixedSideContainer";
|
||||||
import { HintViewer } from "./HintViewer";
|
import { HintViewer } from "./HintViewer";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
@ -31,7 +30,6 @@ import { HelpDialog } from "./HelpDialog";
|
|||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { UserList } from "./UserList";
|
import { UserList } from "./UserList";
|
||||||
import { JSONExportDialog } from "./JSONExportDialog";
|
import { JSONExportDialog } from "./JSONExportDialog";
|
||||||
import { isImageFileHandle } from "../data/blob";
|
|
||||||
import { PenModeButton } from "./PenModeButton";
|
import { PenModeButton } from "./PenModeButton";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
@ -69,6 +67,7 @@ interface LayerUIProps {
|
|||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -114,6 +113,7 @@ const LayerUI = ({
|
|||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
|
onExportImage,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
@ -143,47 +143,14 @@ const LayerUI = ({
|
|||||||
return null;
|
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 (
|
return (
|
||||||
<ImageExportDialog
|
<ImageExportDialog
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
setAppState={setAppState}
|
|
||||||
files={files}
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportToPng={createExporter("png")}
|
onExportImage={onExportImage}
|
||||||
onExportToSvg={createExporter("svg")}
|
onCloseRequest={() => setAppState({ openDialog: null })}
|
||||||
onExportToClipboard={createExporter("clipboard")}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -131,6 +131,12 @@ export const MIME_TYPES = {
|
|||||||
...IMAGE_MIME_TYPES,
|
...IMAGE_MIME_TYPES,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const EXPORT_IMAGE_TYPES = {
|
||||||
|
png: "png",
|
||||||
|
svg: "svg",
|
||||||
|
clipboard: "clipboard",
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const EXPORT_DATA_TYPES = {
|
export const EXPORT_DATA_TYPES = {
|
||||||
excalidraw: "excalidraw",
|
excalidraw: "excalidraw",
|
||||||
excalidrawClipboard: "excalidraw/clipboard",
|
excalidrawClipboard: "excalidraw/clipboard",
|
||||||
|
@ -442,6 +442,7 @@ export type AppClassProperties = {
|
|||||||
pasteFromClipboard: App["pasteFromClipboard"];
|
pasteFromClipboard: App["pasteFromClipboard"];
|
||||||
id: App["id"];
|
id: App["id"];
|
||||||
onInsertElements: App["onInsertElements"];
|
onInsertElements: App["onInsertElements"];
|
||||||
|
onExportImage: App["onExportImage"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user