aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/actions/actionExport.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/actions/actionExport.tsx')
-rw-r--r--packages/excalidraw/actions/actionExport.tsx309
1 files changed, 309 insertions, 0 deletions
diff --git a/packages/excalidraw/actions/actionExport.tsx b/packages/excalidraw/actions/actionExport.tsx
new file mode 100644
index 0000000..8d18acd
--- /dev/null
+++ b/packages/excalidraw/actions/actionExport.tsx
@@ -0,0 +1,309 @@
+import { ExportIcon, questionCircle, saveAs } from "../components/icons";
+import { ProjectName } from "../components/ProjectName";
+import { ToolButton } from "../components/ToolButton";
+import { Tooltip } from "../components/Tooltip";
+import { DarkModeToggle } from "../components/DarkModeToggle";
+import { loadFromJSON, saveAsJSON } from "../data";
+import { resaveAsImageWithScene } from "../data/resave";
+import { t } from "../i18n";
+import { useDevice } from "../components/App";
+import { KEYS } from "../keys";
+import { register } from "./register";
+import { CheckboxItem } from "../components/CheckboxItem";
+import { getExportSize } from "../scene/export";
+import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
+import { getSelectedElements, isSomeElementSelected } from "../scene";
+import { getNonDeletedElements } from "../element";
+import { isImageFileHandle } from "../data/blob";
+import { nativeFileSystemSupported } from "../data/filesystem";
+import type { Theme } from "../element/types";
+
+import "../components/ToolIcon.scss";
+import { CaptureUpdateAction } from "../store";
+
+export const actionChangeProjectName = register({
+ name: "changeProjectName",
+ label: "labels.fileTitle",
+ trackEvent: false,
+ perform: (_elements, appState, value) => {
+ return {
+ appState: { ...appState, name: value },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+ PanelComponent: ({ appState, updateData, appProps, data, app }) => (
+ <ProjectName
+ label={t("labels.fileTitle")}
+ value={app.getName()}
+ onChange={(name: string) => updateData(name)}
+ ignoreFocus={data?.ignoreFocus ?? false}
+ />
+ ),
+});
+
+export const actionChangeExportScale = register({
+ name: "changeExportScale",
+ label: "imageExportDialog.scale",
+ trackEvent: { category: "export", action: "scale" },
+ perform: (_elements, appState, value) => {
+ return {
+ appState: { ...appState, exportScale: value },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+ PanelComponent: ({ elements: allElements, appState, updateData }) => {
+ const elements = getNonDeletedElements(allElements);
+ const exportSelected = isSomeElementSelected(elements, appState);
+ const exportedElements = exportSelected
+ ? getSelectedElements(elements, appState)
+ : elements;
+
+ return (
+ <>
+ {EXPORT_SCALES.map((s) => {
+ const [width, height] = getExportSize(
+ exportedElements,
+ DEFAULT_EXPORT_PADDING,
+ s,
+ );
+
+ const scaleButtonTitle = `${t(
+ "imageExportDialog.label.scale",
+ )} ${s}x (${width}x${height})`;
+
+ return (
+ <ToolButton
+ key={s}
+ size="small"
+ type="radio"
+ icon={`${s}x`}
+ name="export-canvas-scale"
+ title={scaleButtonTitle}
+ aria-label={scaleButtonTitle}
+ id="export-canvas-scale"
+ checked={s === appState.exportScale}
+ onChange={() => updateData(s)}
+ />
+ );
+ })}
+ </>
+ );
+ },
+});
+
+export const actionChangeExportBackground = register({
+ name: "changeExportBackground",
+ label: "imageExportDialog.label.withBackground",
+ trackEvent: { category: "export", action: "toggleBackground" },
+ perform: (_elements, appState, value) => {
+ return {
+ appState: { ...appState, exportBackground: value },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+ PanelComponent: ({ appState, updateData }) => (
+ <CheckboxItem
+ checked={appState.exportBackground}
+ onChange={(checked) => updateData(checked)}
+ >
+ {t("imageExportDialog.label.withBackground")}
+ </CheckboxItem>
+ ),
+});
+
+export const actionChangeExportEmbedScene = register({
+ name: "changeExportEmbedScene",
+ label: "imageExportDialog.tooltip.embedScene",
+ trackEvent: { category: "export", action: "embedScene" },
+ perform: (_elements, appState, value) => {
+ return {
+ appState: { ...appState, exportEmbedScene: value },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+ PanelComponent: ({ appState, updateData }) => (
+ <CheckboxItem
+ checked={appState.exportEmbedScene}
+ onChange={(checked) => updateData(checked)}
+ >
+ {t("imageExportDialog.label.embedScene")}
+ <Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
+ <div className="excalidraw-tooltip-icon">{questionCircle}</div>
+ </Tooltip>
+ </CheckboxItem>
+ ),
+});
+
+export const actionSaveToActiveFile = register({
+ name: "saveToActiveFile",
+ label: "buttons.save",
+ icon: ExportIcon,
+ trackEvent: { category: "export" },
+ predicate: (elements, appState, props, app) => {
+ return (
+ !!app.props.UIOptions.canvasActions.saveToActiveFile &&
+ !!appState.fileHandle &&
+ !appState.viewModeEnabled
+ );
+ },
+ perform: async (elements, appState, value, app) => {
+ const fileHandleExists = !!appState.fileHandle;
+
+ try {
+ const { fileHandle } = isImageFileHandle(appState.fileHandle)
+ ? await resaveAsImageWithScene(
+ elements,
+ appState,
+ app.files,
+ app.getName(),
+ )
+ : await saveAsJSON(elements, appState, app.files, app.getName());
+
+ return {
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ appState: {
+ ...appState,
+ fileHandle,
+ toast: fileHandleExists
+ ? {
+ message: fileHandle?.name
+ ? t("toast.fileSavedToFilename").replace(
+ "{filename}",
+ `"${fileHandle.name}"`,
+ )
+ : t("toast.fileSaved"),
+ }
+ : null,
+ },
+ };
+ } catch (error: any) {
+ if (error?.name !== "AbortError") {
+ console.error(error);
+ } else {
+ console.warn(error);
+ }
+ return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
+ }
+ },
+ keyTest: (event) =>
+ event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
+});
+
+export const actionSaveFileToDisk = register({
+ name: "saveFileToDisk",
+ label: "exportDialog.disk_title",
+ icon: ExportIcon,
+ viewMode: true,
+ trackEvent: { category: "export" },
+ perform: async (elements, appState, value, app) => {
+ try {
+ const { fileHandle } = await saveAsJSON(
+ elements,
+ {
+ ...appState,
+ fileHandle: null,
+ },
+ app.files,
+ app.getName(),
+ );
+ return {
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ appState: {
+ ...appState,
+ openDialog: null,
+ fileHandle,
+ toast: { message: t("toast.fileSaved") },
+ },
+ };
+ } catch (error: any) {
+ if (error?.name !== "AbortError") {
+ console.error(error);
+ } else {
+ console.warn(error);
+ }
+ return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
+ }
+ },
+ keyTest: (event) =>
+ event.key === KEYS.S && event.shiftKey && event[KEYS.CTRL_OR_CMD],
+ PanelComponent: ({ updateData }) => (
+ <ToolButton
+ type="button"
+ icon={saveAs}
+ title={t("buttons.saveAs")}
+ aria-label={t("buttons.saveAs")}
+ showAriaLabel={useDevice().editor.isMobile}
+ hidden={!nativeFileSystemSupported}
+ onClick={() => updateData(null)}
+ data-testid="save-as-button"
+ />
+ ),
+});
+
+export const actionLoadScene = register({
+ name: "loadScene",
+ label: "buttons.load",
+ trackEvent: { category: "export" },
+ predicate: (elements, appState, props, app) => {
+ return (
+ !!app.props.UIOptions.canvasActions.loadScene && !appState.viewModeEnabled
+ );
+ },
+ perform: async (elements, appState, _, app) => {
+ try {
+ const {
+ elements: loadedElements,
+ appState: loadedAppState,
+ files,
+ } = await loadFromJSON(appState, elements);
+ return {
+ elements: loadedElements,
+ appState: loadedAppState,
+ files,
+ captureUpdate: CaptureUpdateAction.IMMEDIATELY,
+ };
+ } catch (error: any) {
+ if (error?.name === "AbortError") {
+ console.warn(error);
+ return false;
+ }
+ return {
+ elements,
+ appState: { ...appState, errorMessage: error.message },
+ files: app.files,
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ }
+ },
+ keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
+});
+
+export const actionExportWithDarkMode = register({
+ name: "exportWithDarkMode",
+ label: "imageExportDialog.label.darkMode",
+ trackEvent: { category: "export", action: "toggleTheme" },
+ perform: (_elements, appState, value) => {
+ return {
+ appState: { ...appState, exportWithDarkMode: value },
+ captureUpdate: CaptureUpdateAction.EVENTUALLY,
+ };
+ },
+ PanelComponent: ({ appState, updateData }) => (
+ <div
+ style={{
+ display: "flex",
+ justifyContent: "flex-end",
+ marginTop: "-45px",
+ marginBottom: "10px",
+ }}
+ >
+ <DarkModeToggle
+ value={appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT}
+ onChange={(theme: Theme) => {
+ updateData(theme === THEME.DARK);
+ }}
+ title={t("imageExportDialog.label.darkMode")}
+ />
+ </div>
+ ),
+});