ExportDialog.tsx 5.4 KB

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