Import and export library from/to a file (#1940)
Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
7eff6893c5
commit
ee8fa6aaad
@ -7,14 +7,7 @@ import { t } from "../i18n";
|
|||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
import { muteFSAbortError } from "../utils";
|
||||||
const muteFSAbortError = (error?: Error) => {
|
|
||||||
// if user cancels, ignore the error
|
|
||||||
if (error?.name === "AbortError") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const actionChangeProjectName = register({
|
export const actionChangeProjectName = register({
|
||||||
name: "changeProjectName",
|
name: "changeProjectName",
|
||||||
|
@ -143,6 +143,7 @@ import { actionFinalize, actionDeleteSelected } from "../actions";
|
|||||||
import {
|
import {
|
||||||
restoreUsernameFromLocalStorage,
|
restoreUsernameFromLocalStorage,
|
||||||
saveUsernameToLocalStorage,
|
saveUsernameToLocalStorage,
|
||||||
|
loadLibrary,
|
||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
|
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
@ -153,6 +154,7 @@ import {
|
|||||||
isElementInGroup,
|
isElementInGroup,
|
||||||
getSelectedGroupIdForElement,
|
getSelectedGroupIdForElement,
|
||||||
} from "../groups";
|
} from "../groups";
|
||||||
|
import { Library } from "../data/library";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param func handler taking at most single parameter (event).
|
* @param func handler taking at most single parameter (event).
|
||||||
@ -3206,7 +3208,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
|
|
||||||
private handleCanvasOnDrop = (event: React.DragEvent<HTMLCanvasElement>) => {
|
private handleCanvasOnDrop = (event: React.DragEvent<HTMLCanvasElement>) => {
|
||||||
const libraryShapes = event.dataTransfer.getData(
|
const libraryShapes = event.dataTransfer.getData(
|
||||||
"application/vnd.excalidraw.json",
|
"application/vnd.excalidrawlib+json",
|
||||||
);
|
);
|
||||||
if (libraryShapes !== "") {
|
if (libraryShapes !== "") {
|
||||||
this.addElementsFromPasteOrLibrary(
|
this.addElementsFromPasteOrLibrary(
|
||||||
@ -3237,6 +3239,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.setState({ isLoading: false, errorMessage: error.message });
|
this.setState({ isLoading: false, errorMessage: error.message });
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
file?.type === "application/vnd.excalidrawlib+json" ||
|
||||||
|
file?.name.endsWith(".excalidrawlib")
|
||||||
|
) {
|
||||||
|
Library.importLibrary(file)
|
||||||
|
.then(() => {
|
||||||
|
this.setState({ isLibraryOpen: false });
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
this.setState({ isLoading: false, errorMessage: error.message }),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@ -3484,6 +3497,7 @@ declare global {
|
|||||||
setState: React.Component<any, AppState>["setState"];
|
setState: React.Component<any, AppState>["setState"];
|
||||||
history: SceneHistory;
|
history: SceneHistory;
|
||||||
app: InstanceType<typeof App>;
|
app: InstanceType<typeof App>;
|
||||||
|
library: ReturnType<typeof loadLibrary>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3506,6 +3520,9 @@ if (
|
|||||||
history: {
|
history: {
|
||||||
get: () => history,
|
get: () => history,
|
||||||
},
|
},
|
||||||
|
library: {
|
||||||
|
get: () => loadLibrary(),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,12 +9,8 @@ import { showSelectedShapeActions } from "../element";
|
|||||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||||
import { exportCanvas } from "../data";
|
import { exportCanvas } from "../data";
|
||||||
|
|
||||||
import { AppState, LibraryItems } from "../types";
|
import { AppState, LibraryItems, LibraryItem } from "../types";
|
||||||
import {
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeleted,
|
|
||||||
} from "../element/types";
|
|
||||||
|
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
@ -37,13 +33,16 @@ import { ErrorDialog } from "./ErrorDialog";
|
|||||||
import { ShortcutsDialog } from "./ShortcutsDialog";
|
import { ShortcutsDialog } from "./ShortcutsDialog";
|
||||||
import { LoadingMessage } from "./LoadingMessage";
|
import { LoadingMessage } from "./LoadingMessage";
|
||||||
import { CLASSES } from "../constants";
|
import { CLASSES } from "../constants";
|
||||||
import { shield } from "./icons";
|
import { shield, exportFile, load } from "./icons";
|
||||||
import { GitHubCorner } from "./GitHubCorner";
|
import { GitHubCorner } from "./GitHubCorner";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
|
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import { LibraryUnit } from "./LibraryUnit";
|
import { LibraryUnit } from "./LibraryUnit";
|
||||||
import { loadLibrary, saveLibrary } from "../data/localStorage";
|
import { loadLibrary, saveLibrary } from "../data/localStorage";
|
||||||
|
import { ToolButton } from "./ToolButton";
|
||||||
|
import { saveLibraryAsJSON, importLibraryFromJSON } from "../data/json";
|
||||||
|
import { muteFSAbortError } from "../utils";
|
||||||
|
|
||||||
interface LayerUIProps {
|
interface LayerUIProps {
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -55,7 +54,7 @@ interface LayerUIProps {
|
|||||||
onUsernameChange: (username: string) => void;
|
onUsernameChange: (username: string) => void;
|
||||||
onRoomDestroy: () => void;
|
onRoomDestroy: () => void;
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
zenModeEnabled: boolean;
|
zenModeEnabled: boolean;
|
||||||
toggleZenMode: () => void;
|
toggleZenMode: () => void;
|
||||||
lng: string;
|
lng: string;
|
||||||
@ -95,13 +94,15 @@ const LibraryMenuItems = ({
|
|||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
|
setAppState,
|
||||||
}: {
|
}: {
|
||||||
library: LibraryItems;
|
library: LibraryItems;
|
||||||
pendingElements: NonDeleted<ExcalidrawElement>[];
|
pendingElements: LibraryItem;
|
||||||
onClickOutside: (event: MouseEvent) => void;
|
onClickOutside: (event: MouseEvent) => void;
|
||||||
onRemoveFromLibrary: (index: number) => void;
|
onRemoveFromLibrary: (index: number) => void;
|
||||||
onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: (elements: NonDeleted<ExcalidrawElement>[]) => void;
|
onAddToLibrary: (elements: LibraryItem) => void;
|
||||||
|
setAppState: any;
|
||||||
}) => {
|
}) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
||||||
@ -110,6 +111,44 @@ const LibraryMenuItems = ({
|
|||||||
const rows = [];
|
const rows = [];
|
||||||
let addedPendingElements = false;
|
let addedPendingElements = false;
|
||||||
|
|
||||||
|
rows.push(
|
||||||
|
<Stack.Row align="center" gap={1} key={"actions"}>
|
||||||
|
<ToolButton
|
||||||
|
key="import"
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.load")}
|
||||||
|
aria-label={t("buttons.load")}
|
||||||
|
icon={load}
|
||||||
|
onClick={() => {
|
||||||
|
importLibraryFromJSON()
|
||||||
|
.then(() => {
|
||||||
|
// Maybe we should close and open the menu so that the items get updated.
|
||||||
|
// But for now we just close the menu.
|
||||||
|
setAppState({ isLibraryOpen: false });
|
||||||
|
})
|
||||||
|
.catch(muteFSAbortError)
|
||||||
|
.catch((error) => {
|
||||||
|
setAppState({ errorMessage: error.message });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ToolButton
|
||||||
|
key="export"
|
||||||
|
type="button"
|
||||||
|
title={t("buttons.export")}
|
||||||
|
aria-label={t("buttons.export")}
|
||||||
|
icon={exportFile}
|
||||||
|
onClick={() => {
|
||||||
|
saveLibraryAsJSON()
|
||||||
|
.catch(muteFSAbortError)
|
||||||
|
.catch((error) => {
|
||||||
|
setAppState({ errorMessage: error.message });
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Row>,
|
||||||
|
);
|
||||||
|
|
||||||
for (let row = 0; row < numRows; row++) {
|
for (let row = 0; row < numRows; row++) {
|
||||||
const i = CELLS_PER_ROW * row;
|
const i = CELLS_PER_ROW * row;
|
||||||
const children = [];
|
const children = [];
|
||||||
@ -156,11 +195,13 @@ const LibraryMenu = ({
|
|||||||
onInsertShape,
|
onInsertShape,
|
||||||
pendingElements,
|
pendingElements,
|
||||||
onAddToLibrary,
|
onAddToLibrary,
|
||||||
|
setAppState,
|
||||||
}: {
|
}: {
|
||||||
pendingElements: NonDeleted<ExcalidrawElement>[];
|
pendingElements: LibraryItem;
|
||||||
onClickOutside: (event: MouseEvent) => void;
|
onClickOutside: (event: MouseEvent) => void;
|
||||||
onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void;
|
onInsertShape: (elements: LibraryItem) => void;
|
||||||
onAddToLibrary: () => void;
|
onAddToLibrary: () => void;
|
||||||
|
setAppState: any;
|
||||||
}) => {
|
}) => {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
useOnClickOutside(ref, onClickOutside);
|
useOnClickOutside(ref, onClickOutside);
|
||||||
@ -202,7 +243,7 @@ const LibraryMenu = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addToLibrary = useCallback(
|
const addToLibrary = useCallback(
|
||||||
async (elements: NonDeleted<ExcalidrawElement>[]) => {
|
async (elements: LibraryItem) => {
|
||||||
const items = await loadLibrary();
|
const items = await loadLibrary();
|
||||||
const nextItems = [...items, elements];
|
const nextItems = [...items, elements];
|
||||||
onAddToLibrary();
|
onAddToLibrary();
|
||||||
@ -226,6 +267,7 @@ const LibraryMenu = ({
|
|||||||
onAddToLibrary={addToLibrary}
|
onAddToLibrary={addToLibrary}
|
||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertShape}
|
||||||
pendingElements={pendingElements}
|
pendingElements={pendingElements}
|
||||||
|
setAppState={setAppState}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Island>
|
</Island>
|
||||||
@ -372,6 +414,7 @@ const LayerUI = ({
|
|||||||
onClickOutside={closeLibrary}
|
onClickOutside={closeLibrary}
|
||||||
onInsertShape={onInsertShape}
|
onInsertShape={onInsertShape}
|
||||||
onAddToLibrary={deselectItems}
|
onAddToLibrary={deselectItems}
|
||||||
|
setAppState={setAppState}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { useRef, useEffect, useState } from "react";
|
import React, { useRef, useEffect, useState } from "react";
|
||||||
import { exportToSvg } from "../scene/export";
|
import { exportToSvg } from "../scene/export";
|
||||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
|
||||||
import { close } from "../components/icons";
|
import { close } from "../components/icons";
|
||||||
|
|
||||||
import "./LibraryUnit.scss";
|
import "./LibraryUnit.scss";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import useIsMobile from "../is-mobile";
|
import useIsMobile from "../is-mobile";
|
||||||
|
import { LibraryItem } from "../types";
|
||||||
|
|
||||||
// fa-plus
|
// fa-plus
|
||||||
const PLUS_ICON = (
|
const PLUS_ICON = (
|
||||||
@ -20,8 +20,8 @@ export const LibraryUnit = ({
|
|||||||
onRemoveFromLibrary,
|
onRemoveFromLibrary,
|
||||||
onClick,
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
elements?: NonDeleted<ExcalidrawElement>[];
|
elements?: LibraryItem;
|
||||||
pendingElements?: NonDeleted<ExcalidrawElement>[];
|
pendingElements?: LibraryItem;
|
||||||
onRemoveFromLibrary: () => void;
|
onRemoveFromLibrary: () => void;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
@ -75,7 +75,7 @@ export const LibraryUnit = ({
|
|||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
setIsHovered(false);
|
setIsHovered(false);
|
||||||
event.dataTransfer.setData(
|
event.dataTransfer.setData(
|
||||||
"application/vnd.excalidraw.json",
|
"application/vnd.excalidrawlib+json",
|
||||||
JSON.stringify(elements),
|
JSON.stringify(elements),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -2,17 +2,11 @@ import { getDefaultAppState, cleanAppStateForExport } from "../appState";
|
|||||||
import { restore } from "./restore";
|
import { restore } from "./restore";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { LibraryData } from "./types";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
|
|
||||||
/**
|
const loadFileContents = async (blob: any) => {
|
||||||
* @param blob
|
let contents: string;
|
||||||
* @param appState if provided, used for centering scroll to restored scene
|
|
||||||
*/
|
|
||||||
export const loadFromBlob = async (blob: any, appState?: AppState) => {
|
|
||||||
if (blob.handle) {
|
|
||||||
(window as any).handle = blob.handle;
|
|
||||||
}
|
|
||||||
let contents;
|
|
||||||
if ("text" in Blob) {
|
if ("text" in Blob) {
|
||||||
contents = await blob.text();
|
contents = await blob.text();
|
||||||
} else {
|
} else {
|
||||||
@ -26,7 +20,19 @@ export const loadFromBlob = async (blob: any, appState?: AppState) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return contents;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param blob
|
||||||
|
* @param appState if provided, used for centering scroll to restored scene
|
||||||
|
*/
|
||||||
|
export const loadFromBlob = async (blob: any, appState?: AppState) => {
|
||||||
|
if (blob.handle) {
|
||||||
|
(window as any).handle = blob.handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents = await loadFileContents(blob);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
let elements = [];
|
let elements = [];
|
||||||
let _appState = appState || defaultAppState;
|
let _appState = appState || defaultAppState;
|
||||||
@ -47,3 +53,12 @@ export const loadFromBlob = async (blob: any, appState?: AppState) => {
|
|||||||
|
|
||||||
return restore(elements, _appState);
|
return restore(elements, _appState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const loadLibraryFromBlob = async (blob: any) => {
|
||||||
|
const contents = await loadFileContents(blob);
|
||||||
|
const data: LibraryData = JSON.parse(contents);
|
||||||
|
if (data.type !== "excalidrawlib") {
|
||||||
|
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
@ -4,6 +4,8 @@ import { cleanAppStateForExport } from "../appState";
|
|||||||
|
|
||||||
import { fileOpen, fileSave } from "browser-nativefs";
|
import { fileOpen, fileSave } from "browser-nativefs";
|
||||||
import { loadFromBlob } from "./blob";
|
import { loadFromBlob } from "./blob";
|
||||||
|
import { loadLibrary } from "./localStorage";
|
||||||
|
import { Library } from "./library";
|
||||||
|
|
||||||
export const serializeAsJSON = (
|
export const serializeAsJSON = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
@ -50,3 +52,34 @@ export const loadFromJSON = async (appState: AppState) => {
|
|||||||
});
|
});
|
||||||
return loadFromBlob(blob, appState);
|
return loadFromBlob(blob, appState);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const saveLibraryAsJSON = async () => {
|
||||||
|
const library = await loadLibrary();
|
||||||
|
const serialized = JSON.stringify(
|
||||||
|
{
|
||||||
|
type: "excalidrawlib",
|
||||||
|
version: 1,
|
||||||
|
library,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
const fileName = `library.excalidrawlib`;
|
||||||
|
const blob = new Blob([serialized], {
|
||||||
|
type: "application/vnd.excalidrawlib+json",
|
||||||
|
});
|
||||||
|
await fileSave(blob, {
|
||||||
|
fileName,
|
||||||
|
description: "Excalidraw library file",
|
||||||
|
extensions: ["excalidrawlib"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importLibraryFromJSON = async () => {
|
||||||
|
const blob = await fileOpen({
|
||||||
|
description: "Excalidraw library files",
|
||||||
|
extensions: ["json", "excalidrawlib"],
|
||||||
|
mimeTypes: ["application/json"],
|
||||||
|
});
|
||||||
|
Library.importLibrary(blob);
|
||||||
|
};
|
||||||
|
43
src/data/library.ts
Normal file
43
src/data/library.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { loadLibraryFromBlob } from "./blob";
|
||||||
|
import { LibraryItems, LibraryItem } from "../types";
|
||||||
|
import { loadLibrary, saveLibrary } from "./localStorage";
|
||||||
|
|
||||||
|
export class Library {
|
||||||
|
/** imports library (currently merges, removing duplicates) */
|
||||||
|
static async importLibrary(blob: any) {
|
||||||
|
const libraryFile = await loadLibraryFromBlob(blob);
|
||||||
|
if (!libraryFile || !libraryFile.library) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks if library item does not exist already in current library
|
||||||
|
*/
|
||||||
|
const isUniqueitem = (
|
||||||
|
existingLibraryItems: LibraryItems,
|
||||||
|
targetLibraryItem: LibraryItem,
|
||||||
|
) => {
|
||||||
|
return !existingLibraryItems.find((libraryItem) => {
|
||||||
|
if (libraryItem.length !== targetLibraryItem.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// detect z-index difference by checking the excalidraw elements
|
||||||
|
// are in order
|
||||||
|
return libraryItem.every((libItemExcalidrawItem, idx) => {
|
||||||
|
return (
|
||||||
|
libItemExcalidrawItem.id === targetLibraryItem[idx].id &&
|
||||||
|
libItemExcalidrawItem.versionNonce ===
|
||||||
|
targetLibraryItem[idx].versionNonce
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingLibraryItems = await loadLibrary();
|
||||||
|
const filtered = libraryFile.library!.filter((libraryItem) =>
|
||||||
|
isUniqueitem(existingLibraryItems, libraryItem),
|
||||||
|
);
|
||||||
|
saveLibrary([...existingLibraryItems, ...filtered]);
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,7 @@ export const loadLibrary = (): Promise<LibraryItems> => {
|
|||||||
return resolve([]);
|
return resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = (JSON.parse(data) as ExcalidrawElement[][]).map(
|
const items = (JSON.parse(data) as LibraryItems).map(
|
||||||
(elements) => restore(elements, null).elements,
|
(elements) => restore(elements, null).elements,
|
||||||
) as Mutable<LibraryItems>;
|
) as Mutable<LibraryItems>;
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState } from "../types";
|
import { AppState, LibraryItems } from "../types";
|
||||||
|
|
||||||
export interface DataState {
|
export interface DataState {
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -8,3 +8,10 @@ export interface DataState {
|
|||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
|
appState: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LibraryData {
|
||||||
|
type?: string;
|
||||||
|
version?: number;
|
||||||
|
source?: string;
|
||||||
|
library?: LibraryItems;
|
||||||
|
}
|
||||||
|
@ -108,7 +108,8 @@ export type SocketUpdateData = SocketUpdateDataSource[keyof SocketUpdateDataSour
|
|||||||
_brand: "socketUpdateData";
|
_brand: "socketUpdateData";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LibraryItems = readonly NonDeleted<ExcalidrawElement>[][];
|
export type LibraryItem = NonDeleted<ExcalidrawElement>[];
|
||||||
|
export type LibraryItems = readonly LibraryItem[];
|
||||||
|
|
||||||
export interface ExcalidrawProps {
|
export interface ExcalidrawProps {
|
||||||
width: number;
|
width: number;
|
||||||
|
@ -246,3 +246,11 @@ export function tupleToCoors(
|
|||||||
const [x, y] = xyTuple;
|
const [x, y] = xyTuple;
|
||||||
return { x, y };
|
return { x, y };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** use as a rejectionHandler to mute filesystem Abort errors */
|
||||||
|
export const muteFSAbortError = (error?: Error) => {
|
||||||
|
if (error?.name === "AbortError") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user