clipboard.ts 3.9 KB

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