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. ╳
  88. </button>
  89. <h2>Export</h2>
  90. <div className="ExportDialog__preview" ref={previeRef}></div>
  91. <div className="ExportDialog__actions">
  92. <Stack.Row gap={2}>
  93. <ToolIcon
  94. type="button"
  95. icon={downloadFile}
  96. title="Export to PNG"
  97. aria-label="Export to PNG"
  98. onClick={() => onExportToPng(exportedElements, scale)}
  99. />
  100. {probablySupportsClipboard && (
  101. <ToolIcon
  102. type="button"
  103. icon={clipboard}
  104. title="Copy to clipboard"
  105. aria-label="Copy to clipboard"
  106. onClick={() =>
  107. onExportToClipboard(exportedElements, scale)
  108. }
  109. />
  110. )}
  111. </Stack.Row>
  112. {actionManager.renderAction(
  113. "changeProjectName",
  114. elements,
  115. appState,
  116. syncActionResult
  117. )}
  118. <Stack.Col gap={1}>
  119. <div className="ExportDialog__scales">
  120. <Stack.Row gap={1} align="baseline">
  121. {scales.map(s => (
  122. <ToolIcon
  123. size="s"
  124. type="radio"
  125. icon={"x" + s}
  126. name="export-canvas-scale"
  127. id="export-canvas-scale"
  128. checked={scale === s}
  129. onChange={() => setScale(s)}
  130. />
  131. ))}
  132. </Stack.Row>
  133. </div>
  134. {actionManager.renderAction(
  135. "changeExportBackground",
  136. elements,
  137. appState,
  138. syncActionResult
  139. )}
  140. {someElementIsSelected && (
  141. <div>
  142. <label>
  143. <input
  144. type="checkbox"
  145. checked={exportSelected}
  146. onChange={e =>
  147. setExportSelected(e.currentTarget.checked)
  148. }
  149. />{" "}
  150. Only selected
  151. </label>
  152. </div>
  153. )}
  154. </Stack.Col>
  155. </div>
  156. </Island>
  157. </div>
  158. </Modal>
  159. )}
  160. </>
  161. );
  162. }