clipboard.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. import { ExcalidrawElement } from "./element/types";
  2. import { getSelectedElements } from "./scene";
  3. import { AppState } from "./types";
  4. let CLIPBOARD = "";
  5. let PREFER_APP_CLIPBOARD = false;
  6. export const probablySupportsClipboardReadText =
  7. "clipboard" in navigator && "readText" in navigator.clipboard;
  8. export const probablySupportsClipboardWriteText =
  9. "clipboard" in navigator && "writeText" in navigator.clipboard;
  10. export const probablySupportsClipboardBlob =
  11. "clipboard" in navigator &&
  12. "write" in navigator.clipboard &&
  13. "ClipboardItem" in window &&
  14. "toBlob" in HTMLCanvasElement.prototype;
  15. export async function copyToAppClipboard(
  16. elements: readonly ExcalidrawElement[],
  17. appState: AppState,
  18. ) {
  19. CLIPBOARD = JSON.stringify(getSelectedElements(elements, appState));
  20. try {
  21. // when copying to in-app clipboard, clear system clipboard so that if
  22. // system clip contains text on paste we know it was copied *after* user
  23. // copied elements, and thus we should prefer the text content.
  24. await copyTextToSystemClipboard(null);
  25. PREFER_APP_CLIPBOARD = false;
  26. } catch {
  27. // if clearing system clipboard didn't work, we should prefer in-app
  28. // clipboard even if there's text in system clipboard on paste, because
  29. // we can't be sure of the order of copy operations
  30. PREFER_APP_CLIPBOARD = true;
  31. }
  32. }
  33. export function getAppClipboard(): {
  34. elements?: readonly ExcalidrawElement[];
  35. } {
  36. try {
  37. const clipboardElements = JSON.parse(CLIPBOARD);
  38. if (
  39. Array.isArray(clipboardElements) &&
  40. clipboardElements.length > 0 &&
  41. clipboardElements[0].type // need to implement a better check here...
  42. ) {
  43. return { elements: clipboardElements };
  44. }
  45. } catch (error) {
  46. console.error(error);
  47. }
  48. return {};
  49. }
  50. export async function getClipboardContent(
  51. event: ClipboardEvent | null,
  52. ): Promise<{
  53. text?: string;
  54. elements?: readonly ExcalidrawElement[];
  55. }> {
  56. try {
  57. const text = event
  58. ? event.clipboardData?.getData("text/plain").trim()
  59. : probablySupportsClipboardReadText &&
  60. (await navigator.clipboard.readText());
  61. if (text && !PREFER_APP_CLIPBOARD) {
  62. return { text };
  63. }
  64. } catch (error) {
  65. console.error(error);
  66. }
  67. return getAppClipboard();
  68. }
  69. export async function copyCanvasToClipboardAsPng(canvas: HTMLCanvasElement) {
  70. return new Promise((resolve, reject) => {
  71. try {
  72. canvas.toBlob(async function(blob: any) {
  73. try {
  74. await navigator.clipboard.write([
  75. new window.ClipboardItem({ "image/png": blob }),
  76. ]);
  77. resolve();
  78. } catch (error) {
  79. reject(error);
  80. }
  81. });
  82. } catch (error) {
  83. reject(error);
  84. }
  85. });
  86. }
  87. export async function copyTextToSystemClipboard(text: string | null) {
  88. let copied = false;
  89. if (probablySupportsClipboardWriteText) {
  90. try {
  91. // NOTE: doesn't work on FF on non-HTTPS domains, or when document
  92. // not focused
  93. await navigator.clipboard.writeText(text || "");
  94. copied = true;
  95. } catch (error) {
  96. console.error(error);
  97. }
  98. }
  99. // Note that execCommand doesn't allow copying empty strings, so if we're
  100. // clearing clipboard using this API, we must copy at least an empty char
  101. if (!copied && !copyTextViaExecCommand(text || " ")) {
  102. throw new Error("couldn't copy");
  103. }
  104. }
  105. // adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
  106. function copyTextViaExecCommand(text: string) {
  107. const isRTL = document.documentElement.getAttribute("dir") === "rtl";
  108. const textarea = document.createElement("textarea");
  109. textarea.style.border = "0";
  110. textarea.style.padding = "0";
  111. textarea.style.margin = "0";
  112. textarea.style.position = "absolute";
  113. textarea.style[isRTL ? "right" : "left"] = "-9999px";
  114. const yPosition = window.pageYOffset || document.documentElement.scrollTop;
  115. textarea.style.top = `${yPosition}px`;
  116. // Prevent zooming on iOS
  117. textarea.style.fontSize = "12pt";
  118. textarea.setAttribute("readonly", "");
  119. textarea.value = text;
  120. document.body.appendChild(textarea);
  121. let success = false;
  122. try {
  123. textarea.select();
  124. textarea.setSelectionRange(0, textarea.value.length);
  125. success = document.execCommand("copy");
  126. } catch (error) {
  127. console.error(error);
  128. }
  129. textarea.remove();
  130. return success;
  131. }