mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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:
parent
46624cc953
commit
37d513ad59
15 changed files with 189 additions and 77 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue