feat: Make library local to given excalidraw instance and allow consumer to control it (#3451)

* feat: dnt share library & attach to the excalidraw instance

* fix

* Add addToLibrary, resetLibrary and libraryItems in initialData

* remove comment

* handle errors & improve types

* remove resetLibrary and addToLibrary and add onLibraryChange prop

* set library cache to empty arrary on reset

* Add i18n for remove/add library

* Update src/locales/en.json

Co-authored-by: David Luzar <luzar.david@gmail.com>

* update docs

* Assign unique ID to
 each excalidraw component and remove csrfToken from library as its not needed

* tweaks

Co-authored-by: David Luzar <luzar.david@gmail.com>

* update

* tweak

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2021-04-21 23:38:24 +05:30 committed by GitHub
parent 46624cc953
commit 37d513ad59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 189 additions and 77 deletions

View file

@ -4,6 +4,7 @@ import { RoughCanvas } from "roughjs/bin/canvas";
import rough from "roughjs/bin/rough";
import clsx from "clsx";
import { supported } from "browser-fs-access";
import { nanoid } from "nanoid";
import {
actionAddToLibrary,
@ -68,7 +69,7 @@ import {
} from "../constants";
import { loadFromBlob } from "../data";
import { isValidLibrary } from "../data/json";
import { Library } from "../data/library";
import Library from "../data/library";
import { restore } from "../data/restore";
import {
dragNewElement,
@ -163,7 +164,14 @@ import Scene from "../scene/Scene";
import { SceneState, ScrollBars } from "../scene/types";
import { getNewZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes";
import { AppProps, AppState, Gesture, GestureEvent, SceneData } from "../types";
import {
AppProps,
AppState,
Gesture,
GestureEvent,
LibraryItems,
SceneData,
} from "../types";
import {
debounce,
distance,
@ -289,6 +297,7 @@ export type ExcalidrawImperativeAPI = {
setToastMessage: InstanceType<typeof App>["setToastMessage"];
readyPromise: ResolvablePromise<ExcalidrawImperativeAPI>;
ready: true;
id: string;
};
class App extends React.Component<AppProps, AppState> {
@ -309,6 +318,9 @@ class App extends React.Component<AppProps, AppState> {
private scene: Scene;
private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: Library;
public libraryItemsFromStorage: LibraryItems | undefined;
private id: string;
constructor(props: AppProps) {
super(props);
@ -334,6 +346,8 @@ class App extends React.Component<AppProps, AppState> {
height: window.innerHeight,
};
this.id = nanoid();
if (excalidrawRef) {
const readyPromise =
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
@ -354,6 +368,7 @@ class App extends React.Component<AppProps, AppState> {
refresh: this.refresh,
importLibrary: this.importLibraryFromUrl,
setToastMessage: this.setToastMessage,
id: this.id,
} as const;
if (typeof excalidrawRef === "function") {
excalidrawRef(api);
@ -363,6 +378,7 @@ class App extends React.Component<AppProps, AppState> {
readyPromise.resolve(api);
}
this.scene = new Scene();
this.library = new Library(this);
this.actionManager = new ActionManager(
this.syncActionResult,
@ -490,6 +506,8 @@ class App extends React.Component<AppProps, AppState> {
libraryReturnUrl={this.props.libraryReturnUrl}
UIOptions={this.props.UIOptions}
focusContainer={this.focusContainer}
library={this.library}
id={this.id}
/>
<div className="excalidraw-textEditorContainer" />
<div className="excalidraw-contextMenuContainer" />
@ -650,12 +668,12 @@ class App extends React.Component<AppProps, AppState> {
throw new Error();
}
if (
token === Library.csrfToken ||
token === this.id ||
window.confirm(
t("alerts.confirmAddLibrary", { numShapes: json.library.length }),
)
) {
await Library.importLibrary(blob);
await this.library.importLibrary(blob);
// hack to rerender the library items after import
if (this.state.isLibraryOpen) {
this.setState({ isLibraryOpen: false });
@ -724,6 +742,9 @@ class App extends React.Component<AppProps, AppState> {
let initialData = null;
try {
initialData = (await this.props.initialData) || null;
if (initialData?.libraryItems) {
this.libraryItemsFromStorage = initialData.libraryItems;
}
} catch (error) {
console.error(error);
initialData = {
@ -3713,7 +3734,8 @@ class App extends React.Component<AppProps, AppState> {
file?.type === MIME_TYPES.excalidrawlib ||
file?.name?.endsWith(".excalidrawlib")
) {
Library.importLibrary(file)
this.library
.importLibrary(file)
.then(() => {
// Close and then open to get the libraries updated
this.setState({ isLibraryOpen: false });
@ -4248,7 +4270,6 @@ declare global {
setState: React.Component<any, AppState>["setState"];
history: SceneHistory;
app: InstanceType<typeof App>;
library: typeof Library;
};
}
}
@ -4273,10 +4294,6 @@ if (
configurable: true,
get: () => history,
},
library: {
configurable: true,
value: Library,
},
});
}
export default App;

View file

@ -10,7 +10,6 @@ import { ActionManager } from "../actions/manager";
import { CLASSES } from "../constants";
import { exportCanvas } from "../data";
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
import { Library } from "../data/library";
import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
@ -47,6 +46,7 @@ import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import { UserList } from "./UserList";
import Library from "../data/library";
interface LayerUIProps {
actionManager: ActionManager;
@ -73,6 +73,8 @@ interface LayerUIProps {
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
focusContainer: () => void;
library: Library;
id: string;
}
const useOnClickOutside = (
@ -104,7 +106,7 @@ const useOnClickOutside = (
};
const LibraryMenuItems = ({
library,
libraryItems,
onRemoveFromLibrary,
onAddToLibrary,
onInsertShape,
@ -113,8 +115,10 @@ const LibraryMenuItems = ({
setLibraryItems,
libraryReturnUrl,
focusContainer,
library,
id,
}: {
library: LibraryItems;
libraryItems: LibraryItems;
pendingElements: LibraryItem;
onRemoveFromLibrary: (index: number) => void;
onInsertShape: (elements: LibraryItem) => void;
@ -123,9 +127,11 @@ const LibraryMenuItems = ({
setLibraryItems: (library: LibraryItems) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}) => {
const isMobile = useIsMobile();
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
const numCells = libraryItems.length + (pendingElements.length > 0 ? 1 : 0);
const CELLS_PER_ROW = isMobile ? 4 : 6;
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
const rows = [];
@ -143,7 +149,7 @@ const LibraryMenuItems = ({
aria-label={t("buttons.load")}
icon={load}
onClick={() => {
importLibraryFromJSON()
importLibraryFromJSON(library)
.then(() => {
// Close and then open to get the libraries updated
setAppState({ isLibraryOpen: false });
@ -155,7 +161,7 @@ const LibraryMenuItems = ({
});
}}
/>
{!!library.length && (
{!!libraryItems.length && (
<>
<ToolButton
key="export"
@ -164,7 +170,7 @@ const LibraryMenuItems = ({
aria-label={t("buttons.export")}
icon={exportFile}
onClick={() => {
saveLibraryAsJSON()
saveLibraryAsJSON(library)
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
@ -179,7 +185,7 @@ const LibraryMenuItems = ({
icon={trash}
onClick={() => {
if (window.confirm(t("alerts.resetLibrary"))) {
Library.resetLibrary();
library.resetLibrary();
setLibraryItems([]);
focusContainer();
}
@ -190,7 +196,7 @@ const LibraryMenuItems = ({
<a
href={`https://libraries.excalidraw.com?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${Library.csrfToken}`}
}&referrer=${referrer}&useHash=true&token=${id}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
@ -205,13 +211,13 @@ const LibraryMenuItems = ({
const shouldAddPendingElements: boolean =
pendingElements.length > 0 &&
!addedPendingElements &&
y + x >= library.length;
y + x >= libraryItems.length;
addedPendingElements = addedPendingElements || shouldAddPendingElements;
children.push(
<Stack.Col key={x}>
<LibraryUnit
elements={library[y + x]}
elements={libraryItems[y + x]}
pendingElements={
shouldAddPendingElements ? pendingElements : undefined
}
@ -219,7 +225,7 @@ const LibraryMenuItems = ({
onClick={
shouldAddPendingElements
? onAddToLibrary.bind(null, pendingElements)
: onInsertShape.bind(null, library[y + x])
: onInsertShape.bind(null, libraryItems[y + x])
}
/>
</Stack.Col>,
@ -247,6 +253,8 @@ const LibraryMenu = ({
setAppState,
libraryReturnUrl,
focusContainer,
library,
id,
}: {
pendingElements: LibraryItem;
onClickOutside: (event: MouseEvent) => void;
@ -255,6 +263,8 @@ const LibraryMenu = ({
setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => {
@ -280,7 +290,7 @@ const LibraryMenu = ({
resolve("loading");
}, 100);
}),
Library.loadLibrary().then((items) => {
library.loadLibrary().then((items) => {
setLibraryItems(items);
setIsLoading("ready");
}),
@ -292,24 +302,33 @@ const LibraryMenu = ({
return () => {
clearTimeout(loadingTimerRef.current!);
};
}, []);
}, [library]);
const removeFromLibrary = useCallback(async (indexToRemove) => {
const items = await Library.loadLibrary();
const nextItems = items.filter((_, index) => index !== indexToRemove);
Library.saveLibrary(nextItems);
setLibraryItems(nextItems);
}, []);
const removeFromLibrary = useCallback(
async (indexToRemove) => {
const items = await library.loadLibrary();
const nextItems = items.filter((_, index) => index !== indexToRemove);
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setLibraryItems(nextItems);
},
[library, setAppState],
);
const addToLibrary = useCallback(
async (elements: LibraryItem) => {
const items = await Library.loadLibrary();
const items = await library.loadLibrary();
const nextItems = [...items, elements];
onAddToLibrary();
Library.saveLibrary(nextItems);
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
setLibraryItems(nextItems);
},
[onAddToLibrary],
[onAddToLibrary, library, setAppState],
);
return loadingState === "preloading" ? null : (
@ -320,7 +339,7 @@ const LibraryMenu = ({
</div>
) : (
<LibraryMenuItems
library={libraryItems}
libraryItems={libraryItems}
onRemoveFromLibrary={removeFromLibrary}
onAddToLibrary={addToLibrary}
onInsertShape={onInsertShape}
@ -329,6 +348,8 @@ const LibraryMenu = ({
setLibraryItems={setLibraryItems}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
id={id}
/>
)}
</Island>
@ -355,6 +376,8 @@ const LayerUI = ({
libraryReturnUrl,
UIOptions,
focusContainer,
library,
id,
}: LayerUIProps) => {
const isMobile = useIsMobile();
@ -526,6 +549,8 @@ const LayerUI = ({
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
id={id}
/>
) : null;