123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- import rough from "roughjs/bin/rough";
- import { ExcalidrawElement } from "../element/types";
- import { getElementAbsoluteCoords } from "../element";
- import { getDefaultAppState } from "../appState";
- import { renderScene } from "../renderer";
- import { AppState } from "../types";
- import { ExportType } from "./types";
- import nanoid from "nanoid";
- const LOCAL_STORAGE_KEY = "excalidraw";
- const LOCAL_STORAGE_KEY_STATE = "excalidraw-state";
- // TODO: Defined globally, since file handles aren't yet serializable.
- // Once `FileSystemFileHandle` can be serialized, make this
- // part of `AppState`.
- (window as any).handle = null;
- function saveFile(name: string, data: string) {
- // create a temporary <a> elem which we'll use to download the image
- const link = document.createElement("a");
- link.setAttribute("download", name);
- link.setAttribute("href", data);
- link.click();
- // clean up
- link.remove();
- }
- async function saveFileNative(name: string, data: Blob) {
- const options = {
- type: "saveFile",
- accepts: [
- {
- description: `Excalidraw ${
- data.type === "image/png" ? "image" : "file"
- }`,
- extensions: [data.type.split("/")[1]],
- mimeTypes: [data.type]
- }
- ]
- };
- try {
- let handle;
- if (data.type === "application/json") {
- // For Excalidraw files (i.e., `application/json` files):
- // If it exists, write back to a previously opened file.
- // Else, create a new file.
- if ((window as any).handle) {
- handle = (window as any).handle;
- } else {
- handle = await (window as any).chooseFileSystemEntries(options);
- (window as any).handle = handle;
- }
- } else {
- // For image export files (i.e., `image/png` files):
- // Always create a new file.
- handle = await (window as any).chooseFileSystemEntries(options);
- }
- const writer = await handle.createWriter();
- await writer.truncate(0);
- await writer.write(0, data, data.type);
- await writer.close();
- } catch (err) {
- if (err.name !== "AbortError") {
- console.error(err.name, err.message);
- }
- throw err;
- }
- }
- interface DataState {
- elements: readonly ExcalidrawElement[];
- appState: AppState;
- }
- 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 name = `${appState.name}.json`;
- if ("chooseFileSystemEntries" in window) {
- await saveFileNative(
- name,
- new Blob([serialized], { type: "application/json" })
- );
- } else {
- saveFile(
- name,
- "data:text/plain;charset=utf-8," + encodeURIComponent(serialized)
- );
- }
- }
- export async function loadFromJSON() {
- const updateAppState = (contents: string) => {
- const defaultAppState = getDefaultAppState();
- let elements = [];
- let appState = defaultAppState;
- try {
- const data = JSON.parse(contents);
- elements = data.elements || [];
- appState = { ...defaultAppState, ...data.appState };
- } catch (e) {
- // Do nothing because elements array is already empty
- }
- return { elements, appState };
- };
- if ("chooseFileSystemEntries" in window) {
- try {
- (window as any).handle = await (window as any).chooseFileSystemEntries({
- accepts: [
- {
- description: "Excalidraw files",
- extensions: ["json"],
- mimeTypes: ["application/json"]
- }
- ]
- });
- const file = await (window as any).handle.getFile();
- const contents = await file.text();
- const { elements, appState } = updateAppState(contents);
- return new Promise<DataState>(resolve => {
- resolve(restore(elements, appState));
- });
- } catch (err) {
- if (err.name !== "AbortError") {
- console.error(err.name, err.message);
- }
- throw err;
- }
- } else {
- const input = document.createElement("input");
- const reader = new FileReader();
- input.type = "file";
- input.accept = ".json";
- input.onchange = () => {
- if (!input.files!.length) {
- alert("A file was not selected.");
- return;
- }
- reader.readAsText(input.files![0], "utf8");
- };
- input.click();
- return new Promise<DataState>(resolve => {
- reader.onloadend = () => {
- if (reader.readyState === FileReader.DONE) {
- const { elements, appState } = updateAppState(
- reader.result as string
- );
- resolve(restore(elements, appState));
- }
- };
- });
- }
- }
- export function getExportCanvasPreview(
- elements: readonly ExcalidrawElement[],
- {
- exportBackground,
- exportPadding = 10,
- viewBackgroundColor
- }: {
- exportBackground: boolean;
- exportPadding?: number;
- viewBackgroundColor: string;
- }
- ) {
- // calculate smallest area to fit the contents in
- let subCanvasX1 = Infinity;
- let subCanvasX2 = 0;
- let subCanvasY1 = Infinity;
- let subCanvasY2 = 0;
- elements.forEach(element => {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
- subCanvasX1 = Math.min(subCanvasX1, x1);
- subCanvasY1 = Math.min(subCanvasY1, y1);
- subCanvasX2 = Math.max(subCanvasX2, x2);
- subCanvasY2 = Math.max(subCanvasY2, y2);
- });
- function distance(x: number, y: number) {
- return Math.abs(x > y ? x - y : y - x);
- }
- const tempCanvas = document.createElement("canvas");
- tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
- tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
- renderScene(
- elements,
- rough.canvas(tempCanvas),
- tempCanvas,
- {
- viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
- scrollX: 0,
- scrollY: 0
- },
- {
- offsetX: -subCanvasX1 + exportPadding,
- offsetY: -subCanvasY1 + exportPadding,
- renderScrollbars: false,
- renderSelection: false
- }
- );
- return tempCanvas;
- }
- export async function exportCanvas(
- type: ExportType,
- elements: readonly ExcalidrawElement[],
- canvas: HTMLCanvasElement,
- {
- exportBackground,
- exportPadding = 10,
- viewBackgroundColor,
- name
- }: {
- exportBackground: boolean;
- exportPadding?: number;
- viewBackgroundColor: string;
- scrollX: number;
- scrollY: number;
- name: string;
- }
- ) {
- if (!elements.length) return window.alert("Cannot export empty canvas.");
- // calculate smallest area to fit the contents in
- let subCanvasX1 = Infinity;
- let subCanvasX2 = 0;
- let subCanvasY1 = Infinity;
- let subCanvasY2 = 0;
- elements.forEach(element => {
- const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
- subCanvasX1 = Math.min(subCanvasX1, x1);
- subCanvasY1 = Math.min(subCanvasY1, y1);
- subCanvasX2 = Math.max(subCanvasX2, x2);
- subCanvasY2 = Math.max(subCanvasY2, y2);
- });
- function distance(x: number, y: number) {
- return Math.abs(x > y ? x - y : y - x);
- }
- const tempCanvas = document.createElement("canvas");
- tempCanvas.style.display = "none";
- document.body.appendChild(tempCanvas);
- tempCanvas.width = distance(subCanvasX1, subCanvasX2) + exportPadding * 2;
- tempCanvas.height = distance(subCanvasY1, subCanvasY2) + exportPadding * 2;
- renderScene(
- elements,
- rough.canvas(tempCanvas),
- tempCanvas,
- {
- viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
- scrollX: 0,
- scrollY: 0
- },
- {
- offsetX: -subCanvasX1 + exportPadding,
- offsetY: -subCanvasY1 + exportPadding,
- renderScrollbars: false,
- renderSelection: false
- }
- );
- if (type === "png") {
- const fileName = `${name}.png`;
- if ("chooseFileSystemEntries" in window) {
- tempCanvas.toBlob(async blob => {
- if (blob) {
- await saveFileNative(fileName, blob);
- }
- });
- } else {
- saveFile(fileName, tempCanvas.toDataURL("image/png"));
- }
- } else if (type === "clipboard") {
- try {
- tempCanvas.toBlob(async function(blob) {
- try {
- await navigator.clipboard.write([
- new window.ClipboardItem({ "image/png": blob })
- ]);
- } catch (err) {
- window.alert("Couldn't copy to clipboard. Try using Chrome browser.");
- }
- });
- } catch (err) {
- window.alert("Couldn't copy to clipboard. Try using Chrome browser.");
- }
- }
- // clean up the DOM
- if (tempCanvas !== canvas) tempCanvas.remove();
- }
- function restore(
- savedElements: readonly ExcalidrawElement[],
- savedState: AppState
- ): DataState {
- return {
- elements: savedElements.map(element => ({
- ...element,
- id: element.id || nanoid(),
- fillStyle: element.fillStyle || "hachure",
- strokeWidth: element.strokeWidth || 1,
- roughness: element.roughness || 1,
- opacity:
- element.opacity === null || element.opacity === undefined
- ? 100
- : element.opacity
- })),
- appState: savedState
- };
- }
- export function restoreFromLocalStorage() {
- const savedElements = localStorage.getItem(LOCAL_STORAGE_KEY);
- const savedState = localStorage.getItem(LOCAL_STORAGE_KEY_STATE);
- let elements = [];
- if (savedElements) {
- try {
- elements = JSON.parse(savedElements).map(
- ({ shape, ...element }: ExcalidrawElement) => element
- );
- } catch (e) {
- // Do nothing because elements array is already empty
- }
- }
- let appState = null;
- if (savedState) {
- try {
- appState = JSON.parse(savedState);
- } catch (e) {
- // Do nothing because appState is already null
- }
- }
- return restore(elements, appState);
- }
- export function saveToLocalStorage(
- elements: readonly ExcalidrawElement[],
- state: AppState
- ) {
- localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(elements));
- localStorage.setItem(LOCAL_STORAGE_KEY_STATE, JSON.stringify(state));
- }
|