ExportDialog.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import "./ExportDialog.css";
  2. import React, { useState, useEffect, useRef } from "react";
  3. import { Modal } from "./Modal";
  4. import { ToolIcon } from "./ToolIcon";
  5. import { clipboard, exportFile, downloadFile, link } from "./icons";
  6. import { Island } from "./Island";
  7. import { ExcalidrawElement } from "../element/types";
  8. import { AppState } from "../types";
  9. import { getExportCanvasPreview } from "../scene/getExportCanvasPreview";
  10. import { ActionsManagerInterface, UpdaterFn } from "../actions/types";
  11. import Stack from "./Stack";
  12. import { useTranslation } from "react-i18next";
  13. const probablySupportsClipboard =
  14. "toBlob" in HTMLCanvasElement.prototype &&
  15. "clipboard" in navigator &&
  16. "write" in navigator.clipboard &&
  17. "ClipboardItem" in window;
  18. const scales = [1, 2, 3];
  19. const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
  20. type ExportCB = (elements: readonly ExcalidrawElement[], scale: number) => void;
  21. export function ExportDialog({
  22. elements,
  23. appState,
  24. exportPadding = 10,
  25. actionManager,
  26. syncActionResult,
  27. onExportToPng,
  28. onExportToClipboard,
  29. onExportToBackend
  30. }: {
  31. appState: AppState;
  32. elements: readonly ExcalidrawElement[];
  33. exportPadding?: number;
  34. actionManager: ActionsManagerInterface;
  35. syncActionResult: UpdaterFn;
  36. onExportToPng: ExportCB;
  37. onExportToClipboard: ExportCB;
  38. onExportToBackend: ExportCB;
  39. }) {
  40. const { t } = useTranslation();
  41. const someElementIsSelected = elements.some(element => element.isSelected);
  42. const [modalIsShown, setModalIsShown] = useState(false);
  43. const [scale, setScale] = useState(defaultScale);
  44. const [exportSelected, setExportSelected] = useState(someElementIsSelected);
  45. const previeRef = useRef<HTMLDivElement>(null);
  46. const { exportBackground, viewBackgroundColor } = appState;
  47. const exportedElements = exportSelected
  48. ? elements.filter(element => element.isSelected)
  49. : elements;
  50. useEffect(() => {
  51. setExportSelected(someElementIsSelected);
  52. }, [someElementIsSelected]);
  53. useEffect(() => {
  54. const previewNode = previeRef.current;
  55. const canvas = getExportCanvasPreview(exportedElements, {
  56. exportBackground,
  57. viewBackgroundColor,
  58. exportPadding,
  59. scale
  60. });
  61. previewNode?.appendChild(canvas);
  62. return () => {
  63. previewNode?.removeChild(canvas);
  64. };
  65. }, [
  66. modalIsShown,
  67. exportedElements,
  68. exportBackground,
  69. exportPadding,
  70. viewBackgroundColor,
  71. scale
  72. ]);
  73. function handleClose() {
  74. setModalIsShown(false);
  75. setExportSelected(someElementIsSelected);
  76. }
  77. return (
  78. <>
  79. <ToolIcon
  80. onClick={() => setModalIsShown(true)}
  81. icon={exportFile}
  82. type="button"
  83. aria-label="Show export dialog"
  84. title={t("buttons.export")}
  85. />
  86. {modalIsShown && (
  87. <Modal maxWidth={640} onCloseRequest={handleClose}>
  88. <div className="ExportDialog__dialog">
  89. <Island padding={4}>
  90. <button className="ExportDialog__close" onClick={handleClose}>
  91. </button>
  92. <h2>{t("buttons.export")}</h2>
  93. <div className="ExportDialog__preview" ref={previeRef}></div>
  94. <div className="ExportDialog__actions">
  95. <Stack.Row gap={2}>
  96. <ToolIcon
  97. type="button"
  98. icon={downloadFile}
  99. title={t("buttons.exportToPng")}
  100. aria-label={t("buttons.exportToPng")}
  101. onClick={() => onExportToPng(exportedElements, scale)}
  102. />
  103. {probablySupportsClipboard && (
  104. <ToolIcon
  105. type="button"
  106. icon={clipboard}
  107. title={t("buttons.copyToClipboard")}
  108. aria-label={t("buttons.copyToClipboard")}
  109. onClick={() =>
  110. onExportToClipboard(exportedElements, scale)
  111. }
  112. />
  113. )}
  114. <ToolIcon
  115. type="button"
  116. icon={link}
  117. title={t("buttons.getShareableLink")}
  118. aria-label={t("buttons.getShareableLink")}
  119. onClick={() => onExportToBackend(exportedElements, 1)}
  120. />
  121. </Stack.Row>
  122. {actionManager.renderAction(
  123. "changeProjectName",
  124. elements,
  125. appState,
  126. syncActionResult,
  127. t
  128. )}
  129. <Stack.Col gap={1}>
  130. <div className="ExportDialog__scales">
  131. <Stack.Row gap={1} align="baseline">
  132. {scales.map(s => (
  133. <ToolIcon
  134. key={s}
  135. size="s"
  136. type="radio"
  137. icon={"x" + s}
  138. name="export-canvas-scale"
  139. id="export-canvas-scale"
  140. checked={scale === s}
  141. onChange={() => setScale(s)}
  142. />
  143. ))}
  144. </Stack.Row>
  145. </div>
  146. {actionManager.renderAction(
  147. "changeExportBackground",
  148. elements,
  149. appState,
  150. syncActionResult,
  151. t
  152. )}
  153. {someElementIsSelected && (
  154. <div>
  155. <label>
  156. <input
  157. type="checkbox"
  158. checked={exportSelected}
  159. onChange={e =>
  160. setExportSelected(e.currentTarget.checked)
  161. }
  162. />{" "}
  163. {t("labels.onlySelected")}
  164. </label>
  165. </div>
  166. )}
  167. </Stack.Col>
  168. </div>
  169. </Island>
  170. </div>
  171. </Modal>
  172. )}
  173. </>
  174. );
  175. }