Browse Source

feat: export exportToClipboard util from package (#5103)

* feat: export copyToClipboard from package

* use promise constructor for better browser supprt

* add type to exportToClipboard

* update docs

* use json instead of text and use selected element in actionCopy

* pass `files` in example `exportToClipboard`

* fix bad access

Co-authored-by: dwelle <luzar.david@gmail.com>
Aakansha Doshi 3 years ago
parent
commit
6a0f800716

+ 3 - 1
src/actions/actionClipboard.tsx

@@ -15,7 +15,9 @@ export const actionCopy = register({
   name: "copy",
   trackEvent: { category: "element" },
   perform: (elements, appState, _, app) => {
-    copyToClipboard(getNonDeletedElements(elements), appState, app.files);
+    const selectedElements = getSelectedElements(elements, appState, true);
+
+    copyToClipboard(selectedElements, appState, app.files);
 
     return {
       commitToHistory: false,

+ 11 - 11
src/clipboard.ts

@@ -2,7 +2,6 @@ import {
   ExcalidrawElement,
   NonDeletedExcalidrawElement,
 } from "./element/types";
-import { getSelectedElements } from "./scene";
 import { AppState, BinaryFiles } from "./types";
 import { SVG_EXPORT_TAG } from "./scene/export";
 import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
@@ -12,7 +11,7 @@ import { isPromiseLike } from "./utils";
 
 type ElementsClipboard = {
   type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
-  elements: ExcalidrawElement[];
+  elements: readonly NonDeletedExcalidrawElement[];
   files: BinaryFiles | undefined;
 };
 
@@ -57,19 +56,20 @@ const clipboardContainsElements = (
 export const copyToClipboard = async (
   elements: readonly NonDeletedExcalidrawElement[],
   appState: AppState,
-  files: BinaryFiles,
+  files: BinaryFiles | null,
 ) => {
   // select binded text elements when copying
-  const selectedElements = getSelectedElements(elements, appState, true);
   const contents: ElementsClipboard = {
     type: EXPORT_DATA_TYPES.excalidrawClipboard,
-    elements: selectedElements,
-    files: selectedElements.reduce((acc, element) => {
-      if (isInitializedImageElement(element) && files[element.fileId]) {
-        acc[element.fileId] = files[element.fileId];
-      }
-      return acc;
-    }, {} as BinaryFiles),
+    elements,
+    files: files
+      ? elements.reduce((acc, element) => {
+          if (isInitializedImageElement(element) && files[element.fileId]) {
+            acc[element.fileId] = files[element.fileId];
+          }
+          return acc;
+        }, {} as BinaryFiles)
+      : undefined,
   };
   const json = JSON.stringify(contents);
   CLIPBOARD = json;

+ 1 - 0
src/packages/excalidraw/CHANGELOG.md

@@ -17,6 +17,7 @@ Please add the latest change on the top under the correct section.
 
 #### Features
 
+- Expose util `exportToClipboard`[https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#exportToClipboard] which allows to copy the scene contents to clipboard as `svg`, `png` or `json` [#5103](https://github.com/excalidraw/excalidraw/pull/5103).
 - Expose `window.EXCALIDRAW_EXPORT_SOURCE` which you can use to overwrite the `source` field in exported data [#5095](https://github.com/excalidraw/excalidraw/pull/5095).
 - The `exportToBlob` utility now supports the `exportEmbedScene` option when generating a png image [#5047](https://github.com/excalidraw/excalidraw/pull/5047).
 - Exported [`restoreLibraryItems`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreLibraryItems) API [#4995](https://github.com/excalidraw/excalidraw/pull/4995).

+ 29 - 1
src/packages/excalidraw/README_NEXT.md

@@ -857,7 +857,7 @@ This function returns the canvas with the exported elements, appState and dimens
 
 <pre>
 exportToBlob(
-  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L10">ExportOpts</a> & {
+  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
   mimeType?: string,
   quality?: number;
 })
@@ -900,6 +900,34 @@ exportToSvg({
 
 This function returns a promise which resolves to svg of the exported drawing.
 
+#### `exportToClipboard`
+
+**_Signature_**
+
+<pre>
+exportToClipboard(
+  opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & {
+  mimeType?: string,
+  quality?: number;
+  type: 'png' | 'svg' |'json'
+})
+</pre>
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- | --- | --- |
+| opts |  |  | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exportToCanvas). |
+| mimeType | string | "image/png" | Indicates the image format, this will be used when exporting as `png`. |
+| quality | number | 0.92 | A value between 0 and 1 indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. This will be used when exporting as `png`. |
+| type | 'png' | 'svg' | 'json' |  | This determines the format to which the scene data should be exported. |
+
+**How to use**
+
+```js
+import { exportToClipboard } from "@excalidraw/excalidraw-next";
+```
+
+Copies the scene data in the specified format (determined by `type`) to clipboard.
+
 ##### Additional attributes of appState for `export\*` APIs
 
 | Name | Type | Default | Description |

+ 28 - 2
src/packages/excalidraw/example/App.js

@@ -10,8 +10,13 @@ import { MIME_TYPES } from "../../../constants";
 // This is so that we use the bundled excalidraw.development.js file instead
 // of the actual source code
 
-const { exportToCanvas, exportToSvg, exportToBlob, Excalidraw } =
-  window.ExcalidrawLib;
+const {
+  exportToCanvas,
+  exportToSvg,
+  exportToBlob,
+  exportToClipboard,
+  Excalidraw,
+} = window.ExcalidrawLib;
 const resolvablePromise = () => {
   let resolve;
   let reject;
@@ -141,6 +146,15 @@ export default function App() {
     }
   }, []);
 
+  const onCopy = async (type) => {
+    await exportToClipboard({
+      elements: excalidrawRef.current.getSceneElements(),
+      appState: excalidrawRef.current.getAppState(),
+      files: excalidrawRef.current.getFiles(),
+      type,
+    });
+    window.alert(`Copied to clipboard as ${type} sucessfully`);
+  };
   return (
     <div className="App">
       <h1> Excalidraw Example</h1>
@@ -175,6 +189,7 @@ export default function App() {
           >
             Update Library
           </button>
+
           <label>
             <input
               type="checkbox"
@@ -213,6 +228,17 @@ export default function App() {
             />
             Switch to Dark Theme
           </label>
+          <div>
+            <button onClick={onCopy.bind(null, "png")}>
+              Copy to Clipboard as PNG
+            </button>
+            <button onClick={onCopy.bind(null, "svg")}>
+              Copy to Clipboard as SVG
+            </button>
+            <button onClick={onCopy.bind(null, "json")}>
+              Copy to Clipboard as JSON
+            </button>
+          </div>
         </div>
         <div className="excalidraw-wrapper">
           <Excalidraw

+ 1 - 0
src/packages/excalidraw/index.tsx

@@ -197,6 +197,7 @@ export {
   loadLibraryFromBlob,
   loadFromBlob,
   getFreeDrawSvgPath,
+  exportToClipboard,
 } from "../../packages/utils";
 export { isLinearElement } from "../../element/typeChecks";
 

+ 38 - 3
src/packages/utils.ts

@@ -10,6 +10,11 @@ import { restore } from "../data/restore";
 import { MIME_TYPES } from "../constants";
 import { encodePngMetadata } from "../data/image";
 import { serializeAsJSON } from "../data/json";
+import {
+  copyBlobToClipboardAsPng,
+  copyTextToSystemClipboard,
+  copyToClipboard,
+} from "../clipboard";
 
 type ExportOpts = {
   elements: readonly NonDeleted<ExcalidrawElement>[];
@@ -81,7 +86,7 @@ export const exportToBlob = async (
     mimeType?: string;
     quality?: number;
   },
-): Promise<Blob | null> => {
+): Promise<Blob> => {
   let { mimeType = MIME_TYPES.png, quality } = opts;
 
   if (mimeType === MIME_TYPES.png && typeof quality === "number") {
@@ -107,9 +112,12 @@ export const exportToBlob = async (
 
   quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
 
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
     canvas.toBlob(
-      async (blob: Blob | null) => {
+      async (blob) => {
+        if (!blob) {
+          return reject(new Error("couldn't export to blob"));
+        }
         if (
           blob &&
           mimeType === MIME_TYPES.png &&
@@ -156,6 +164,33 @@ export const exportToSvg = async ({
   );
 };
 
+export const exportToClipboard = async (
+  opts: ExportOpts & {
+    mimeType?: string;
+    quality?: number;
+    type: "png" | "svg" | "json";
+  },
+) => {
+  if (opts.type === "svg") {
+    const svg = await exportToSvg(opts);
+    await copyTextToSystemClipboard(svg.outerHTML);
+  } else if (opts.type === "png") {
+    await copyBlobToClipboardAsPng(exportToBlob(opts));
+  } else if (opts.type === "json") {
+    const appState = {
+      offsetTop: 0,
+      offsetLeft: 0,
+      width: 0,
+      height: 0,
+      ...getDefaultAppState(),
+      ...opts.appState,
+    };
+    await copyToClipboard(opts.elements, appState, opts.files);
+  } else {
+    throw new Error("Invalid export type");
+  }
+};
+
 export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json";
 export { loadFromBlob, loadLibraryFromBlob } from "../data/blob";
 export { getFreeDrawSvgPath } from "../renderer/renderElement";