import React, { useEffect, useRef, useState } from "react"; import { render, unmountComponentAtNode } from "react-dom"; import { ActionsManagerInterface } from "../actions/types"; import { probablySupportsClipboardBlob } from "../clipboard"; import { canvasToBlob } from "../data/blob"; import { NonDeletedExcalidrawElement } from "../element/types"; import { CanvasError } from "../errors"; import { t } from "../i18n"; import { useIsMobile } from "./App"; import { getSelectedElements, isSomeElementSelected } from "../scene"; import { exportToCanvas, getExportSize } from "../scene/export"; import { AppState } from "../types"; import { Dialog } from "./Dialog"; import { clipboard, exportImage } from "./icons"; import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; import "./ExportDialog.scss"; import { supported as fsSupported } from "browser-fs-access"; import OpenColor from "open-color"; import { CheckboxItem } from "./CheckboxItem"; const scales = [1, 2, 3]; const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1; const supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d")!; export const ErrorCanvasPreview = () => { return (

{t("canvasError.cannotShowPreview")}

{t("canvasError.canvasTooBig")}

({t("canvasError.canvasTooBigTip")})
); }; const renderPreview = ( content: HTMLCanvasElement | Error, previewNode: HTMLDivElement, ) => { unmountComponentAtNode(previewNode); previewNode.innerHTML = ""; if (content instanceof HTMLCanvasElement) { previewNode.appendChild(content); } else { render(, previewNode); } }; export type ExportCB = ( elements: readonly NonDeletedExcalidrawElement[], scale?: number, ) => void; const ExportButton: React.FC<{ color: keyof OpenColor; onClick: () => void; title: string; shade?: number; }> = ({ children, title, onClick, color, shade = 6 }) => { return ( ); }; const ImageExportModal = ({ elements, appState, exportPadding = 10, actionManager, onExportToPng, onExportToSvg, onExportToClipboard, }: { appState: AppState; elements: readonly NonDeletedExcalidrawElement[]; exportPadding?: number; actionManager: ActionsManagerInterface; onExportToPng: ExportCB; onExportToSvg: ExportCB; onExportToClipboard: ExportCB; onCloseRequest: () => void; }) => { const someElementIsSelected = isSomeElementSelected(elements, appState); const [scale, setScale] = useState(defaultScale); const [exportSelected, setExportSelected] = useState(someElementIsSelected); const previewRef = useRef(null); const { exportBackground, viewBackgroundColor } = appState; const exportedElements = exportSelected ? getSelectedElements(elements, appState) : elements; useEffect(() => { setExportSelected(someElementIsSelected); }, [someElementIsSelected]); useEffect(() => { const previewNode = previewRef.current; if (!previewNode) { return; } try { const canvas = exportToCanvas(exportedElements, appState, { exportBackground, viewBackgroundColor, exportPadding, scale, }); // if converting to blob fails, there's some problem that will // likely prevent preview and export (e.g. canvas too big) canvasToBlob(canvas) .then(() => { renderPreview(canvas, previewNode); }) .catch((error) => { console.error(error); renderPreview(new CanvasError(), previewNode); }); } catch (error) { console.error(error); renderPreview(new CanvasError(), previewNode); } }, [ appState, exportedElements, exportBackground, exportPadding, viewBackgroundColor, scale, ]); return (
{supportsContextFilters && actionManager.renderAction("exportWithDarkMode")}
{actionManager.renderAction("changeExportBackground")} {someElementIsSelected && ( setExportSelected(checked)} > {t("labels.onlySelected")} )} {actionManager.renderAction("changeExportEmbedScene")}
{scales.map((_scale) => { const [width, height] = getExportSize( exportedElements, exportPadding, _scale, ); const scaleButtonTitle = `${t( "buttons.scale", )} ${_scale}x (${width}x${height})`; return ( setScale(_scale)} /> ); })}

Scale

{!fsSupported && actionManager.renderAction("changeProjectName")}
onExportToPng(exportedElements, scale)} > PNG onExportToSvg(exportedElements, scale)} > SVG {probablySupportsClipboardBlob && ( onExportToClipboard(exportedElements, scale)} color="gray" shade={7} > {clipboard} )}
); }; export const ImageExportDialog = ({ elements, appState, exportPadding = 10, actionManager, onExportToPng, onExportToSvg, onExportToClipboard, }: { appState: AppState; elements: readonly NonDeletedExcalidrawElement[]; exportPadding?: number; actionManager: ActionsManagerInterface; onExportToPng: ExportCB; onExportToSvg: ExportCB; onExportToClipboard: ExportCB; }) => { const [modalIsShown, setModalIsShown] = useState(false); const handleClose = React.useCallback(() => { setModalIsShown(false); }, []); return ( <> { setModalIsShown(true); }} data-testid="image-export-button" icon={exportImage} type="button" aria-label={t("buttons.exportImage")} showAriaLabel={useIsMobile()} title={t("buttons.exportImage")} /> {modalIsShown && ( )} ); };