import React, { useEffect, useRef, useState } from "react";
import type { ActionManager } from "../actions/manager";
import type { AppClassProperties, BinaryFiles, UIAppState } from "../types";
import {
actionExportWithDarkMode,
actionChangeExportBackground,
actionChangeExportEmbedScene,
actionChangeExportScale,
actionChangeProjectName,
} from "../actions/actionExport";
import { probablySupportsClipboardBlob } from "../clipboard";
import {
DEFAULT_EXPORT_PADDING,
EXPORT_IMAGE_TYPES,
isFirefox,
EXPORT_SCALES,
} from "../constants";
import { canvasToBlob } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../../utils/export";
import { copyIcon, downloadIcon, helpIcon } from "./icons";
import { Dialog } from "./Dialog";
import { RadioGroup } from "./RadioGroup";
import { Switch } from "./Switch";
import { Tooltip } from "./Tooltip";
import "./ImageExportDialog.scss";
import { useAppProps } from "./App";
import { FilledButton } from "./FilledButton";
import { cloneJSON } from "../utils";
import { prepareElementsForExport } from "../data";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
export const ErrorCanvasPreview = () => {
return (
{t("canvasError.cannotShowPreview")}
{t("canvasError.canvasTooBig")}
({t("canvasError.canvasTooBigTip")})
);
};
type ImageExportModalProps = {
appStateSnapshot: Readonly;
elementsSnapshot: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
actionManager: ActionManager;
onExportImage: AppClassProperties["onExportImage"];
};
const ImageExportModal = ({
appStateSnapshot,
elementsSnapshot,
files,
actionManager,
onExportImage,
}: ImageExportModalProps) => {
const hasSelection = isSomeElementSelected(
elementsSnapshot,
appStateSnapshot,
);
const appProps = useAppProps();
const [projectName, setProjectName] = useState(appStateSnapshot.name);
const [exportSelectionOnly, setExportSelectionOnly] = useState(hasSelection);
const [exportWithBackground, setExportWithBackground] = useState(
appStateSnapshot.exportBackground,
);
const [exportDarkMode, setExportDarkMode] = useState(
appStateSnapshot.exportWithDarkMode,
);
const [embedScene, setEmbedScene] = useState(
appStateSnapshot.exportEmbedScene,
);
const [exportScale, setExportScale] = useState(appStateSnapshot.exportScale);
const previewRef = useRef(null);
const [renderError, setRenderError] = useState(null);
const { exportedElements, exportingFrame } = prepareElementsForExport(
elementsSnapshot,
appStateSnapshot,
exportSelectionOnly,
);
useEffect(() => {
const previewNode = previewRef.current;
if (!previewNode) {
return;
}
const maxWidth = previewNode.offsetWidth;
const maxHeight = previewNode.offsetHeight;
if (!maxWidth) {
return;
}
exportToCanvas({
elements: exportedElements,
appState: {
...appStateSnapshot,
name: projectName,
exportBackground: exportWithBackground,
exportWithDarkMode: exportDarkMode,
exportScale,
exportEmbedScene: embedScene,
},
files,
exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
exportingFrame,
})
.then((canvas) => {
setRenderError(null);
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
return canvasToBlob(canvas).then(() => {
previewNode.replaceChildren(canvas);
});
})
.catch((error) => {
console.error(error);
setRenderError(error);
});
}, [
appStateSnapshot,
files,
exportedElements,
exportingFrame,
projectName,
exportWithBackground,
exportDarkMode,
exportScale,
embedScene,
]);
return (
{t("imageExportDialog.header")}
{t("imageExportDialog.header")}
{hasSelection && (
{
setExportSelectionOnly(checked);
}}
/>
)}
{
setExportWithBackground(checked);
actionManager.executeAction(
actionChangeExportBackground,
"ui",
checked,
);
}}
/>
{supportsContextFilters && (
{
setExportDarkMode(checked);
actionManager.executeAction(
actionExportWithDarkMode,
"ui",
checked,
);
}}
/>
)}
{
setEmbedScene(checked);
actionManager.executeAction(
actionChangeExportEmbedScene,
"ui",
checked,
);
}}
/>
{
setExportScale(scale);
actionManager.executeAction(actionChangeExportScale, "ui", scale);
}}
choices={EXPORT_SCALES.map((scale) => ({
value: scale,
label: `${scale}\u00d7`,
}))}
/>
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements, {
exportingFrame,
})
}
startIcon={downloadIcon}
>
{t("imageExportDialog.button.exportToPng")}
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements, {
exportingFrame,
})
}
startIcon={downloadIcon}
>
{t("imageExportDialog.button.exportToSvg")}
{(probablySupportsClipboardBlob || isFirefox) && (
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements, {
exportingFrame,
})
}
startIcon={copyIcon}
>
{t("imageExportDialog.button.copyPngToClipboard")}
)}
);
};
type ExportSettingProps = {
label: string;
children: React.ReactNode;
tooltip?: string;
name?: string;
};
const ExportSetting = ({
label,
children,
tooltip,
name,
}: ExportSettingProps) => {
return (
{children}
);
};
export const ImageExportDialog = ({
elements,
appState,
files,
actionManager,
onExportImage,
onCloseRequest,
}: {
appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
actionManager: ActionManager;
onExportImage: AppClassProperties["onExportImage"];
onCloseRequest: () => void;
}) => {
// we need to take a snapshot so that the exported state can't be modified
// while the dialog is open
const [{ appStateSnapshot, elementsSnapshot }] = useState(() => {
return {
appStateSnapshot: cloneJSON(appState),
elementsSnapshot: cloneJSON(elements),
};
});
return (
);
};