ExportDialog.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import "./ExportDialog.scss";
  2. import React, { useState, useEffect, useRef } from "react";
  3. import { ToolButton } from "./ToolButton";
  4. import { clipboard, exportFile, link } from "./icons";
  5. import { NonDeletedExcalidrawElement } from "../element/types";
  6. import { AppState } from "../types";
  7. import { exportToCanvas } from "../scene/export";
  8. import { ActionsManagerInterface } from "../actions/types";
  9. import Stack from "./Stack";
  10. import { t } from "../i18n";
  11. import { probablySupportsClipboardBlob } from "../clipboard";
  12. import { getSelectedElements, isSomeElementSelected } from "../scene";
  13. import useIsMobile from "../is-mobile";
  14. import { Dialog } from "./Dialog";
  15. const scales = [1, 2, 3];
  16. const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
  17. export type ExportCB = (
  18. elements: readonly NonDeletedExcalidrawElement[],
  19. scale?: number,
  20. ) => void;
  21. function ExportModal({
  22. elements,
  23. appState,
  24. exportPadding = 10,
  25. actionManager,
  26. onExportToPng,
  27. onExportToSvg,
  28. onExportToClipboard,
  29. onExportToBackend,
  30. }: {
  31. appState: AppState;
  32. elements: readonly NonDeletedExcalidrawElement[];
  33. exportPadding?: number;
  34. actionManager: ActionsManagerInterface;
  35. onExportToPng: ExportCB;
  36. onExportToSvg: ExportCB;
  37. onExportToClipboard: ExportCB;
  38. onExportToBackend: ExportCB;
  39. onCloseRequest: () => void;
  40. }) {
  41. const someElementIsSelected = isSomeElementSelected(elements, appState);
  42. const [scale, setScale] = useState(defaultScale);
  43. const [exportSelected, setExportSelected] = useState(someElementIsSelected);
  44. const previewRef = useRef<HTMLDivElement>(null);
  45. const { exportBackground, viewBackgroundColor } = appState;
  46. const exportedElements = exportSelected
  47. ? getSelectedElements(elements, appState)
  48. : elements;
  49. useEffect(() => {
  50. setExportSelected(someElementIsSelected);
  51. }, [someElementIsSelected]);
  52. useEffect(() => {
  53. const previewNode = previewRef.current;
  54. const canvas = exportToCanvas(exportedElements, appState, {
  55. exportBackground,
  56. viewBackgroundColor,
  57. exportPadding,
  58. scale,
  59. });
  60. previewNode?.appendChild(canvas);
  61. return () => {
  62. previewNode?.removeChild(canvas);
  63. };
  64. }, [
  65. appState,
  66. exportedElements,
  67. exportBackground,
  68. exportPadding,
  69. viewBackgroundColor,
  70. scale,
  71. ]);
  72. return (
  73. <div className="ExportDialog">
  74. <div className="ExportDialog__preview" ref={previewRef}></div>
  75. <Stack.Col gap={2} align="center">
  76. <div className="ExportDialog__actions">
  77. <Stack.Row gap={2}>
  78. <ToolButton
  79. type="button"
  80. label="PNG"
  81. title={t("buttons.exportToPng")}
  82. aria-label={t("buttons.exportToPng")}
  83. onClick={() => onExportToPng(exportedElements, scale)}
  84. />
  85. <ToolButton
  86. type="button"
  87. label="SVG"
  88. title={t("buttons.exportToSvg")}
  89. aria-label={t("buttons.exportToSvg")}
  90. onClick={() => onExportToSvg(exportedElements, scale)}
  91. />
  92. {probablySupportsClipboardBlob && (
  93. <ToolButton
  94. type="button"
  95. icon={clipboard}
  96. title={t("buttons.copyPngToClipboard")}
  97. aria-label={t("buttons.copyPngToClipboard")}
  98. onClick={() => onExportToClipboard(exportedElements, scale)}
  99. />
  100. )}
  101. <ToolButton
  102. type="button"
  103. icon={link}
  104. title={t("buttons.getShareableLink")}
  105. aria-label={t("buttons.getShareableLink")}
  106. onClick={() => onExportToBackend(exportedElements)}
  107. />
  108. </Stack.Row>
  109. <div className="ExportDialog__name">
  110. {actionManager.renderAction("changeProjectName")}
  111. </div>
  112. <Stack.Row gap={2}>
  113. {scales.map((s) => (
  114. <ToolButton
  115. key={s}
  116. size="s"
  117. type="radio"
  118. icon={`x${s}`}
  119. name="export-canvas-scale"
  120. aria-label={`Scale ${s} x`}
  121. id="export-canvas-scale"
  122. checked={s === scale}
  123. onChange={() => setScale(s)}
  124. />
  125. ))}
  126. </Stack.Row>
  127. </div>
  128. {actionManager.renderAction("changeExportBackground")}
  129. {someElementIsSelected && (
  130. <div>
  131. <label>
  132. <input
  133. type="checkbox"
  134. checked={exportSelected}
  135. onChange={(event) =>
  136. setExportSelected(event.currentTarget.checked)
  137. }
  138. />{" "}
  139. {t("labels.onlySelected")}
  140. </label>
  141. </div>
  142. )}
  143. </Stack.Col>
  144. </div>
  145. );
  146. }
  147. export function ExportDialog({
  148. elements,
  149. appState,
  150. exportPadding = 10,
  151. actionManager,
  152. onExportToPng,
  153. onExportToSvg,
  154. onExportToClipboard,
  155. onExportToBackend,
  156. }: {
  157. appState: AppState;
  158. elements: readonly NonDeletedExcalidrawElement[];
  159. exportPadding?: number;
  160. actionManager: ActionsManagerInterface;
  161. onExportToPng: ExportCB;
  162. onExportToSvg: ExportCB;
  163. onExportToClipboard: ExportCB;
  164. onExportToBackend: ExportCB;
  165. }) {
  166. const [modalIsShown, setModalIsShown] = useState(false);
  167. const triggerButton = useRef<HTMLButtonElement>(null);
  168. const handleClose = React.useCallback(() => {
  169. setModalIsShown(false);
  170. triggerButton.current?.focus();
  171. }, []);
  172. return (
  173. <>
  174. <ToolButton
  175. onClick={() => setModalIsShown(true)}
  176. icon={exportFile}
  177. type="button"
  178. aria-label={t("buttons.export")}
  179. showAriaLabel={useIsMobile()}
  180. title={t("buttons.export")}
  181. ref={triggerButton}
  182. />
  183. {modalIsShown && (
  184. <Dialog
  185. maxWidth={800}
  186. onCloseRequest={handleClose}
  187. title={t("buttons.export")}
  188. >
  189. <ExportModal
  190. elements={elements}
  191. appState={appState}
  192. exportPadding={exportPadding}
  193. actionManager={actionManager}
  194. onExportToPng={onExportToPng}
  195. onExportToSvg={onExportToSvg}
  196. onExportToClipboard={onExportToClipboard}
  197. onExportToBackend={onExportToBackend}
  198. onCloseRequest={handleClose}
  199. />
  200. </Dialog>
  201. )}
  202. </>
  203. );
  204. }