Library MVP (#1787)

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Pete Hunt 2020-07-10 02:20:23 -07:00 committed by GitHub
parent 7ab0c1aba8
commit 6428b59ccb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 599 additions and 20 deletions

View file

@ -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>