mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
parent
7ab0c1aba8
commit
6428b59ccb
18 changed files with 599 additions and 20 deletions
|
@ -1,10 +1,20 @@
|
|||
import React from "react";
|
||||
import React, {
|
||||
useRef,
|
||||
useState,
|
||||
RefObject,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
import { calculateScrollCenter } from "../scene";
|
||||
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
||||
import { exportCanvas } from "../data";
|
||||
|
||||
import { AppState } from "../types";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { AppState, LibraryItems } from "../types";
|
||||
import {
|
||||
NonDeletedExcalidrawElement,
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
} from "../element/types";
|
||||
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { Island } from "./Island";
|
||||
|
@ -32,6 +42,8 @@ import { GitHubCorner } from "./GitHubCorner";
|
|||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
import "./LayerUI.scss";
|
||||
import { LibraryUnit } from "./LibraryUnit";
|
||||
import { loadLibrary, saveLibrary } from "../data/localStorage";
|
||||
|
||||
interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
|
@ -43,11 +55,182 @@ interface LayerUIProps {
|
|||
onUsernameChange: (username: string) => void;
|
||||
onRoomDestroy: () => void;
|
||||
onLockToggle: () => void;
|
||||
onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void;
|
||||
zenModeEnabled: boolean;
|
||||
toggleZenMode: () => void;
|
||||
lng: string;
|
||||
}
|
||||
|
||||
function useOnClickOutside(
|
||||
ref: RefObject<HTMLElement>,
|
||||
cb: (event: MouseEvent) => void,
|
||||
) {
|
||||
useEffect(() => {
|
||||
const listener = (event: MouseEvent) => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
(ref.current.contains(event.target) ||
|
||||
!document.body.contains(event.target))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
cb(event);
|
||||
};
|
||||
document.addEventListener("pointerdown", listener, false);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("pointerdown", listener);
|
||||
};
|
||||
}, [ref, cb]);
|
||||
}
|
||||
|
||||
const LibraryMenuItems = ({
|
||||
library,
|
||||
onRemoveFromLibrary,
|
||||
onAddToLibrary,
|
||||
onInsertShape,
|
||||
pendingElements,
|
||||
}: {
|
||||
library: LibraryItems;
|
||||
pendingElements: NonDeleted<ExcalidrawElement>[];
|
||||
onClickOutside: (event: MouseEvent) => void;
|
||||
onRemoveFromLibrary: (index: number) => void;
|
||||
onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void;
|
||||
onAddToLibrary: (elements: NonDeleted<ExcalidrawElement>[]) => void;
|
||||
}) => {
|
||||
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
||||
const CELLS_PER_ROW = 3;
|
||||
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
|
||||
const rows = [];
|
||||
let addedPendingElements = false;
|
||||
|
||||
for (let row = 0; row < numRows; row++) {
|
||||
const i = CELLS_PER_ROW * row;
|
||||
const children = [];
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const shouldAddPendingElements: boolean =
|
||||
pendingElements.length > 0 &&
|
||||
!addedPendingElements &&
|
||||
i + j >= library.length;
|
||||
addedPendingElements = addedPendingElements || shouldAddPendingElements;
|
||||
|
||||
children.push(
|
||||
<Stack.Col key={j}>
|
||||
<LibraryUnit
|
||||
elements={library[i + j]}
|
||||
pendingElements={
|
||||
shouldAddPendingElements ? pendingElements : undefined
|
||||
}
|
||||
onRemoveFromLibrary={onRemoveFromLibrary.bind(null, i + j)}
|
||||
onClick={
|
||||
shouldAddPendingElements
|
||||
? onAddToLibrary.bind(null, pendingElements)
|
||||
: onInsertShape.bind(null, library[i + j])
|
||||
}
|
||||
/>
|
||||
</Stack.Col>,
|
||||
);
|
||||
}
|
||||
rows.push(
|
||||
<Stack.Row align="center" gap={1} key={row}>
|
||||
{children}
|
||||
</Stack.Row>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack.Col align="center" gap={1} className="layer-ui__library-items">
|
||||
{rows}
|
||||
</Stack.Col>
|
||||
);
|
||||
};
|
||||
|
||||
const LibraryMenu = ({
|
||||
onClickOutside,
|
||||
onInsertShape,
|
||||
pendingElements,
|
||||
onAddToLibrary,
|
||||
}: {
|
||||
pendingElements: NonDeleted<ExcalidrawElement>[];
|
||||
onClickOutside: (event: MouseEvent) => void;
|
||||
onInsertShape: (elements: readonly NonDeleted<ExcalidrawElement>[]) => void;
|
||||
onAddToLibrary: () => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useOnClickOutside(ref, onClickOutside);
|
||||
|
||||
const [libraryItems, setLibraryItems] = useState<LibraryItems>([]);
|
||||
|
||||
const [loadingState, setIsLoading] = useState<
|
||||
"preloading" | "loading" | "ready"
|
||||
>("preloading");
|
||||
|
||||
const loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
Promise.race([
|
||||
new Promise((resolve) => {
|
||||
loadingTimerRef.current = setTimeout(() => {
|
||||
resolve("loading");
|
||||
}, 100);
|
||||
}),
|
||||
loadLibrary().then((items) => {
|
||||
setLibraryItems(items);
|
||||
setIsLoading("ready");
|
||||
}),
|
||||
]).then((data) => {
|
||||
if (data === "loading") {
|
||||
setIsLoading("loading");
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
clearTimeout(loadingTimerRef.current!);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const removeFromLibrary = useCallback(async (indexToRemove) => {
|
||||
const items = await loadLibrary();
|
||||
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
||||
saveLibrary(nextItems);
|
||||
setLibraryItems(nextItems);
|
||||
}, []);
|
||||
|
||||
const addToLibrary = useCallback(
|
||||
async (elements: NonDeleted<ExcalidrawElement>[]) => {
|
||||
const items = await loadLibrary();
|
||||
const nextItems = [...items, elements];
|
||||
onAddToLibrary();
|
||||
saveLibrary(nextItems);
|
||||
setLibraryItems(nextItems);
|
||||
},
|
||||
[onAddToLibrary],
|
||||
);
|
||||
|
||||
return loadingState === "preloading" ? null : (
|
||||
<Island padding={1} ref={ref} className="layer-ui__library">
|
||||
{loadingState === "loading" ? (
|
||||
<div className="layer-ui__library-message">
|
||||
{t("labels.libraryLoadingMessage")}
|
||||
</div>
|
||||
) : (
|
||||
<LibraryMenuItems
|
||||
library={libraryItems}
|
||||
onClickOutside={onClickOutside}
|
||||
onRemoveFromLibrary={removeFromLibrary}
|
||||
onAddToLibrary={addToLibrary}
|
||||
onInsertShape={onInsertShape}
|
||||
pendingElements={pendingElements}
|
||||
/>
|
||||
)}
|
||||
</Island>
|
||||
);
|
||||
};
|
||||
|
||||
const LayerUI = ({
|
||||
actionManager,
|
||||
appState,
|
||||
|
@ -58,6 +241,7 @@ const LayerUI = ({
|
|||
onUsernameChange,
|
||||
onRoomDestroy,
|
||||
onLockToggle,
|
||||
onInsertShape,
|
||||
zenModeEnabled,
|
||||
toggleZenMode,
|
||||
}: LayerUIProps) => {
|
||||
|
@ -167,11 +351,33 @@ const LayerUI = ({
|
|||
</Section>
|
||||
);
|
||||
|
||||
const closeLibrary = useCallback(
|
||||
(event) => {
|
||||
setAppState({ isLibraryOpen: false });
|
||||
},
|
||||
[setAppState],
|
||||
);
|
||||
|
||||
const deselectItems = useCallback(() => {
|
||||
setAppState({
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
});
|
||||
}, [setAppState]);
|
||||
|
||||
const renderFixedSideContainer = () => {
|
||||
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
||||
appState,
|
||||
elements,
|
||||
);
|
||||
const libraryMenu = appState.isLibraryOpen ? (
|
||||
<LibraryMenu
|
||||
pendingElements={getSelectedElements(elements, appState)}
|
||||
onClickOutside={closeLibrary}
|
||||
onInsertShape={onInsertShape}
|
||||
onAddToLibrary={deselectItems}
|
||||
/>
|
||||
) : null;
|
||||
return (
|
||||
<FixedSideContainer side="top">
|
||||
<HintViewer appState={appState} elements={elements} />
|
||||
|
@ -193,6 +399,7 @@ const LayerUI = ({
|
|||
<ShapesSwitcher
|
||||
elementType={appState.elementType}
|
||||
setAppState={setAppState}
|
||||
isLibraryOpen={appState.isLibraryOpen}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
|
@ -203,6 +410,7 @@ const LayerUI = ({
|
|||
title={t("toolBar.lock")}
|
||||
/>
|
||||
</Stack.Row>
|
||||
{libraryMenu}
|
||||
</Stack.Col>
|
||||
)}
|
||||
</Section>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue