Share excalidrawings as links! (#356)
* shareable links * fix * review comments * json-excaliber (#464) * draw * Boom * backend * Remove local Co-authored-by: Lipis <lipiridis@gmail.com>
This commit is contained in:
parent
ad865907a6
commit
6ad596e9f1
@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
|
||||
import { Modal } from "./Modal";
|
||||
import { ToolIcon } from "./ToolIcon";
|
||||
import { clipboard, exportFile, downloadFile } from "./icons";
|
||||
import { clipboard, exportFile, downloadFile, link } from "./icons";
|
||||
import { Island } from "./Island";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
@ -30,7 +30,8 @@ export function ExportDialog({
|
||||
actionManager,
|
||||
syncActionResult,
|
||||
onExportToPng,
|
||||
onExportToClipboard
|
||||
onExportToClipboard,
|
||||
onExportToBackend
|
||||
}: {
|
||||
appState: AppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
@ -39,6 +40,7 @@ export function ExportDialog({
|
||||
syncActionResult: UpdaterFn;
|
||||
onExportToPng: ExportCB;
|
||||
onExportToClipboard: ExportCB;
|
||||
onExportToBackend: ExportCB;
|
||||
}) {
|
||||
const someElementIsSelected = elements.some(element => element.isSelected);
|
||||
const [modalIsShown, setModalIsShown] = useState(false);
|
||||
@ -108,7 +110,6 @@ export function ExportDialog({
|
||||
aria-label="Export to PNG"
|
||||
onClick={() => onExportToPng(exportedElements, scale)}
|
||||
/>
|
||||
|
||||
{probablySupportsClipboard && (
|
||||
<ToolIcon
|
||||
type="button"
|
||||
@ -120,6 +121,13 @@ export function ExportDialog({
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<ToolIcon
|
||||
type="button"
|
||||
icon={link}
|
||||
title="Get shareable link"
|
||||
aria-label="Get shareable link"
|
||||
onClick={() => onExportToBackend(exportedElements, 1)}
|
||||
/>
|
||||
</Stack.Row>
|
||||
|
||||
{actionManager.renderAction(
|
||||
|
@ -5,6 +5,15 @@
|
||||
|
||||
import React from "react";
|
||||
|
||||
export const link = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 512 512">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const save = (
|
||||
<svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 448 512">
|
||||
<path
|
||||
|
@ -27,7 +27,8 @@ import {
|
||||
hasBackground,
|
||||
hasStroke,
|
||||
hasText,
|
||||
exportCanvas
|
||||
exportCanvas,
|
||||
importFromBackend
|
||||
} from "./scene";
|
||||
|
||||
import { renderScene } from "./renderer";
|
||||
@ -210,7 +211,7 @@ export class App extends React.Component<{}, AppState> {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
public async componentDidMount() {
|
||||
document.addEventListener("copy", this.onCopy);
|
||||
document.addEventListener("paste", this.onPaste);
|
||||
document.addEventListener("cut", this.onCut);
|
||||
@ -219,14 +220,22 @@ export class App extends React.Component<{}, AppState> {
|
||||
document.addEventListener("mousemove", this.getCurrentCursorPosition);
|
||||
window.addEventListener("resize", this.onResize, false);
|
||||
|
||||
const { elements: newElements, appState } = restoreFromLocalStorage();
|
||||
let data;
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (newElements) {
|
||||
elements = newElements;
|
||||
if (searchParams.get("json") != null) {
|
||||
data = await importFromBackend(searchParams.get("json"));
|
||||
window.history.replaceState({}, "Excalidraw", window.location.origin);
|
||||
} else {
|
||||
data = restoreFromLocalStorage();
|
||||
}
|
||||
|
||||
if (appState) {
|
||||
this.setState(appState);
|
||||
if (data.elements) {
|
||||
elements = data.elements;
|
||||
}
|
||||
|
||||
if (data.appState) {
|
||||
this.setState(data.appState);
|
||||
} else {
|
||||
this.forceUpdate();
|
||||
}
|
||||
@ -510,6 +519,15 @@ export class App extends React.Component<{}, AppState> {
|
||||
scale
|
||||
});
|
||||
}}
|
||||
onExportToBackend={exportedElements => {
|
||||
if (this.canvas)
|
||||
exportCanvas(
|
||||
"backend",
|
||||
exportedElements,
|
||||
this.canvas,
|
||||
this.state
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{this.actionManager.renderAction(
|
||||
"clearCanvas",
|
||||
|
@ -9,6 +9,8 @@ import nanoid from "nanoid";
|
||||
|
||||
const LOCAL_STORAGE_KEY = "excalidraw";
|
||||
const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
|
||||
const BACKEND_POST = "https://json.excalidraw.com/api/v1/post/";
|
||||
const BACKEND_GET = "https://json.excalidraw.com/api/v1/";
|
||||
|
||||
// TODO: Defined globally, since file handles aren't yet serializable.
|
||||
// Once `FileSystemFileHandle` can be serialized, make this
|
||||
@ -73,16 +75,23 @@ interface DataState {
|
||||
appState: AppState;
|
||||
}
|
||||
|
||||
export function serializeAsJSON(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState?: AppState
|
||||
): string {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
source: window.location.origin,
|
||||
elements: elements.map(({ shape, ...el }) => el),
|
||||
appState: appState || getDefaultAppState()
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveAsJSON(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState
|
||||
) {
|
||||
const serialized = JSON.stringify({
|
||||
version: 1,
|
||||
source: window.location.origin,
|
||||
elements: elements.map(({ shape, ...el }) => el),
|
||||
appState: appState
|
||||
});
|
||||
const serialized = serializeAsJSON(elements, appState);
|
||||
|
||||
const name = `${appState.name}.json`;
|
||||
if ("chooseFileSystemEntries" in window) {
|
||||
@ -166,6 +175,44 @@ export async function loadFromJSON() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportToBackend(elements: readonly ExcalidrawElement[]) {
|
||||
const response = await fetch(BACKEND_POST, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: serializeAsJSON(elements)
|
||||
});
|
||||
const json = await response.json();
|
||||
if (json.hash) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.append("json", json.hash);
|
||||
|
||||
await navigator.clipboard.writeText(url.toString());
|
||||
window.alert("Copied shareable link " + url.toString() + " to clipboard");
|
||||
} else {
|
||||
window.alert("Couldn't create shareable link");
|
||||
}
|
||||
}
|
||||
|
||||
export async function importFromBackend(hash: string | null) {
|
||||
let elements: readonly ExcalidrawElement[] = [];
|
||||
let appState: AppState = getDefaultAppState();
|
||||
const response = await fetch(`${BACKEND_GET}${hash}.json`).then(data =>
|
||||
data.clone().json()
|
||||
);
|
||||
if (response != null) {
|
||||
try {
|
||||
elements = response.elements || elements;
|
||||
appState = response.appState || appState;
|
||||
} catch (error) {
|
||||
window.alert("Importing from backend failed");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return restore(elements, appState);
|
||||
}
|
||||
|
||||
export async function exportCanvas(
|
||||
type: ExportType,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -221,6 +268,8 @@ export async function exportCanvas(
|
||||
} catch (err) {
|
||||
window.alert("Couldn't copy to clipboard. Try using Chrome browser.");
|
||||
}
|
||||
} else if (type === "backend") {
|
||||
exportToBackend(elements);
|
||||
}
|
||||
|
||||
// clean up the DOM
|
||||
|
@ -12,7 +12,9 @@ export {
|
||||
loadFromJSON,
|
||||
saveAsJSON,
|
||||
restoreFromLocalStorage,
|
||||
saveToLocalStorage
|
||||
saveToLocalStorage,
|
||||
exportToBackend,
|
||||
importFromBackend
|
||||
} from "./data";
|
||||
export {
|
||||
hasBackground,
|
||||
|
@ -16,4 +16,4 @@ export interface Scene {
|
||||
elements: ExcalidrawTextElement[];
|
||||
}
|
||||
|
||||
export type ExportType = "png" | "clipboard";
|
||||
export type ExportType = "png" | "clipboard" | "backend";
|
||||
|
Loading…
x
Reference in New Issue
Block a user