2021-01-05 20:06:14 +02:00
|
|
|
import clsx from "clsx";
|
2020-07-10 02:20:23 -07:00
|
|
|
import React, {
|
|
|
|
RefObject,
|
|
|
|
useCallback,
|
2021-01-05 20:06:14 +02:00
|
|
|
useEffect,
|
|
|
|
useRef,
|
|
|
|
useState,
|
2020-07-10 02:20:23 -07:00
|
|
|
} from "react";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { ActionManager } from "../actions/manager";
|
|
|
|
import { CLASSES } from "../constants";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { exportCanvas } from "../data";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
|
|
|
|
import { Library } from "../data/library";
|
|
|
|
import { showSelectedShapeActions } from "../element";
|
2020-07-27 15:29:19 +03:00
|
|
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
2021-01-04 02:21:52 +05:30
|
|
|
import { Language, t } from "../i18n";
|
2020-03-07 10:20:38 -05:00
|
|
|
import useIsMobile from "../is-mobile";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { calculateScrollCenter, getSelectedElements } from "../scene";
|
2020-03-07 10:20:38 -05:00
|
|
|
import { ExportType } from "../scene/types";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { AppState, LibraryItem, LibraryItems } from "../types";
|
|
|
|
import { muteFSAbortError } from "../utils";
|
|
|
|
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
|
|
|
|
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
|
2020-12-05 20:00:53 +05:30
|
|
|
import CollabButton from "./CollabButton";
|
2020-04-03 12:50:51 +01:00
|
|
|
import { ErrorDialog } from "./ErrorDialog";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { ExportCB, ExportDialog } from "./ExportDialog";
|
|
|
|
import { FixedSideContainer } from "./FixedSideContainer";
|
2020-04-18 02:09:15 +05:30
|
|
|
import { GitHubCorner } from "./GitHubCorner";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { HintViewer } from "./HintViewer";
|
|
|
|
import { exportFile, load, shield } from "./icons";
|
|
|
|
import { Island } from "./Island";
|
2020-04-18 02:09:15 +05:30
|
|
|
import "./LayerUI.scss";
|
2020-07-10 02:20:23 -07:00
|
|
|
import { LibraryUnit } from "./LibraryUnit";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { LoadingMessage } from "./LoadingMessage";
|
|
|
|
import { LockIcon } from "./LockIcon";
|
|
|
|
import { MobileMenu } from "./MobileMenu";
|
2020-12-27 18:26:30 +02:00
|
|
|
import { PasteChartDialog } from "./PasteChartDialog";
|
2021-01-05 20:06:14 +02:00
|
|
|
import { Section } from "./Section";
|
2021-01-17 17:46:23 +01:00
|
|
|
import { HelpDialog } from "./HelpDialog";
|
2021-01-05 20:06:14 +02:00
|
|
|
import Stack from "./Stack";
|
|
|
|
import { ToolButton } from "./ToolButton";
|
|
|
|
import { Tooltip } from "./Tooltip";
|
|
|
|
import { UserList } from "./UserList";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
interface LayerUIProps {
|
|
|
|
actionManager: ActionManager;
|
|
|
|
appState: AppState;
|
|
|
|
canvas: HTMLCanvasElement | null;
|
2020-10-16 11:53:40 +02:00
|
|
|
setAppState: React.Component<any, AppState>["setState"];
|
2020-04-08 09:49:52 -07:00
|
|
|
elements: readonly NonDeletedExcalidrawElement[];
|
2020-12-05 20:00:53 +05:30
|
|
|
onCollabButtonClick?: () => void;
|
2020-03-24 19:51:49 +09:00
|
|
|
onLockToggle: () => void;
|
2020-12-27 18:26:30 +02:00
|
|
|
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
|
2020-04-25 18:43:02 +05:30
|
|
|
zenModeEnabled: boolean;
|
|
|
|
toggleZenMode: () => void;
|
2021-01-04 02:21:52 +05:30
|
|
|
langCode: Language["code"];
|
2020-12-05 20:00:53 +05:30
|
|
|
isCollaborating: boolean;
|
2020-12-20 19:44:04 +05:30
|
|
|
onExportToBackend?: (
|
|
|
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
|
|
|
appState: AppState,
|
|
|
|
canvas: HTMLCanvasElement | null,
|
|
|
|
) => void;
|
2021-01-04 02:21:52 +05:30
|
|
|
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
const useOnClickOutside = (
|
2020-07-10 02:20:23 -07:00
|
|
|
ref: RefObject<HTMLElement>,
|
|
|
|
cb: (event: MouseEvent) => void,
|
2020-11-06 22:06:39 +02:00
|
|
|
) => {
|
2020-07-10 02:20:23 -07:00
|
|
|
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]);
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-07-10 02:20:23 -07:00
|
|
|
|
|
|
|
const LibraryMenuItems = ({
|
|
|
|
library,
|
|
|
|
onRemoveFromLibrary,
|
|
|
|
onAddToLibrary,
|
|
|
|
onInsertShape,
|
|
|
|
pendingElements,
|
2020-07-27 15:29:19 +03:00
|
|
|
setAppState,
|
2020-07-10 02:20:23 -07:00
|
|
|
}: {
|
|
|
|
library: LibraryItems;
|
2020-07-27 15:29:19 +03:00
|
|
|
pendingElements: LibraryItem;
|
2020-07-10 02:20:23 -07:00
|
|
|
onRemoveFromLibrary: (index: number) => void;
|
2020-07-27 15:29:19 +03:00
|
|
|
onInsertShape: (elements: LibraryItem) => void;
|
|
|
|
onAddToLibrary: (elements: LibraryItem) => void;
|
2020-10-16 11:53:40 +02:00
|
|
|
setAppState: React.Component<any, AppState>["setState"];
|
2020-07-10 02:20:23 -07:00
|
|
|
}) => {
|
2020-07-20 00:12:56 +03:00
|
|
|
const isMobile = useIsMobile();
|
2020-07-10 02:20:23 -07:00
|
|
|
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
|
2020-07-20 00:12:56 +03:00
|
|
|
const CELLS_PER_ROW = isMobile ? 4 : 6;
|
2020-07-10 02:20:23 -07:00
|
|
|
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
|
|
|
|
const rows = [];
|
|
|
|
let addedPendingElements = false;
|
|
|
|
|
2020-07-27 15:29:19 +03:00
|
|
|
rows.push(
|
2021-01-02 13:13:48 -03:00
|
|
|
<div className="layer-ui__library-header">
|
|
|
|
<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 });
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
|
2021-01-05 20:06:14 +02:00
|
|
|
<a href="https://libraries.excalidraw.com" target="_excalidraw_libraries">
|
2020-12-07 19:24:55 +02:00
|
|
|
{t("labels.libraries")}
|
|
|
|
</a>
|
2021-01-02 13:13:48 -03:00
|
|
|
</div>,
|
2020-07-27 15:29:19 +03:00
|
|
|
);
|
|
|
|
|
2020-07-10 02:20:23 -07:00
|
|
|
for (let row = 0; row < numRows; row++) {
|
2020-12-07 19:24:55 +02:00
|
|
|
const y = CELLS_PER_ROW * row;
|
2020-07-10 02:20:23 -07:00
|
|
|
const children = [];
|
2020-12-07 19:24:55 +02:00
|
|
|
for (let x = 0; x < CELLS_PER_ROW; x++) {
|
2020-07-10 02:20:23 -07:00
|
|
|
const shouldAddPendingElements: boolean =
|
|
|
|
pendingElements.length > 0 &&
|
|
|
|
!addedPendingElements &&
|
2020-12-07 19:24:55 +02:00
|
|
|
y + x >= library.length;
|
2020-07-10 02:20:23 -07:00
|
|
|
addedPendingElements = addedPendingElements || shouldAddPendingElements;
|
|
|
|
|
|
|
|
children.push(
|
2020-12-07 19:24:55 +02:00
|
|
|
<Stack.Col key={x}>
|
2020-07-10 02:20:23 -07:00
|
|
|
<LibraryUnit
|
2020-12-07 19:24:55 +02:00
|
|
|
elements={library[y + x]}
|
2020-07-10 02:20:23 -07:00
|
|
|
pendingElements={
|
|
|
|
shouldAddPendingElements ? pendingElements : undefined
|
|
|
|
}
|
2020-12-07 19:24:55 +02:00
|
|
|
onRemoveFromLibrary={onRemoveFromLibrary.bind(null, y + x)}
|
2020-07-10 02:20:23 -07:00
|
|
|
onClick={
|
|
|
|
shouldAddPendingElements
|
|
|
|
? onAddToLibrary.bind(null, pendingElements)
|
2020-12-07 19:24:55 +02:00
|
|
|
: onInsertShape.bind(null, library[y + x])
|
2020-07-10 02:20:23 -07:00
|
|
|
}
|
|
|
|
/>
|
|
|
|
</Stack.Col>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
rows.push(
|
|
|
|
<Stack.Row align="center" gap={1} key={row}>
|
|
|
|
{children}
|
|
|
|
</Stack.Row>,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2020-12-12 18:53:59 +05:30
|
|
|
<Stack.Col align="start" gap={1} className="layer-ui__library-items">
|
2020-07-10 02:20:23 -07:00
|
|
|
{rows}
|
|
|
|
</Stack.Col>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const LibraryMenu = ({
|
|
|
|
onClickOutside,
|
|
|
|
onInsertShape,
|
|
|
|
pendingElements,
|
|
|
|
onAddToLibrary,
|
2020-07-27 15:29:19 +03:00
|
|
|
setAppState,
|
2020-07-10 02:20:23 -07:00
|
|
|
}: {
|
2020-07-27 15:29:19 +03:00
|
|
|
pendingElements: LibraryItem;
|
2020-07-10 02:20:23 -07:00
|
|
|
onClickOutside: (event: MouseEvent) => void;
|
2020-07-27 15:29:19 +03:00
|
|
|
onInsertShape: (elements: LibraryItem) => void;
|
2020-07-10 02:20:23 -07:00
|
|
|
onAddToLibrary: () => void;
|
2020-10-16 11:53:40 +02:00
|
|
|
setAppState: React.Component<any, AppState>["setState"];
|
2020-07-10 02:20:23 -07:00
|
|
|
}) => {
|
|
|
|
const ref = useRef<HTMLDivElement | null>(null);
|
2020-11-27 03:02:40 +08:00
|
|
|
useOnClickOutside(ref, (event) => {
|
|
|
|
// If click on the library icon, do nothing.
|
|
|
|
if ((event.target as Element).closest(".ToolIcon_type_button__library")) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
onClickOutside(event);
|
|
|
|
});
|
2020-07-10 02:20:23 -07:00
|
|
|
|
|
|
|
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);
|
|
|
|
}),
|
2020-10-30 21:01:41 +01:00
|
|
|
Library.loadLibrary().then((items) => {
|
2020-07-10 02:20:23 -07:00
|
|
|
setLibraryItems(items);
|
|
|
|
setIsLoading("ready");
|
|
|
|
}),
|
|
|
|
]).then((data) => {
|
|
|
|
if (data === "loading") {
|
|
|
|
setIsLoading("loading");
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return () => {
|
|
|
|
clearTimeout(loadingTimerRef.current!);
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const removeFromLibrary = useCallback(async (indexToRemove) => {
|
2020-10-30 21:01:41 +01:00
|
|
|
const items = await Library.loadLibrary();
|
2020-07-10 02:20:23 -07:00
|
|
|
const nextItems = items.filter((_, index) => index !== indexToRemove);
|
2020-10-30 21:01:41 +01:00
|
|
|
Library.saveLibrary(nextItems);
|
2020-07-10 02:20:23 -07:00
|
|
|
setLibraryItems(nextItems);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const addToLibrary = useCallback(
|
2020-07-27 15:29:19 +03:00
|
|
|
async (elements: LibraryItem) => {
|
2020-10-30 21:01:41 +01:00
|
|
|
const items = await Library.loadLibrary();
|
2020-07-10 02:20:23 -07:00
|
|
|
const nextItems = [...items, elements];
|
|
|
|
onAddToLibrary();
|
2020-10-30 21:01:41 +01:00
|
|
|
Library.saveLibrary(nextItems);
|
2020-07-10 02:20:23 -07:00
|
|
|
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}
|
|
|
|
onRemoveFromLibrary={removeFromLibrary}
|
|
|
|
onAddToLibrary={addToLibrary}
|
|
|
|
onInsertShape={onInsertShape}
|
|
|
|
pendingElements={pendingElements}
|
2020-07-27 15:29:19 +03:00
|
|
|
setAppState={setAppState}
|
2020-07-10 02:20:23 -07:00
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</Island>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
const LayerUI = ({
|
|
|
|
actionManager,
|
|
|
|
appState,
|
|
|
|
setAppState,
|
|
|
|
canvas,
|
|
|
|
elements,
|
2020-12-05 20:00:53 +05:30
|
|
|
onCollabButtonClick,
|
2020-04-18 01:54:19 +05:30
|
|
|
onLockToggle,
|
2020-12-27 18:26:30 +02:00
|
|
|
onInsertElements,
|
2020-04-25 18:43:02 +05:30
|
|
|
zenModeEnabled,
|
|
|
|
toggleZenMode,
|
2020-12-05 20:00:53 +05:30
|
|
|
isCollaborating,
|
2020-12-20 19:44:04 +05:30
|
|
|
onExportToBackend,
|
2021-01-04 02:21:52 +05:30
|
|
|
renderCustomFooter,
|
2020-04-18 01:54:19 +05:30
|
|
|
}: LayerUIProps) => {
|
|
|
|
const isMobile = useIsMobile();
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-04-18 02:09:15 +05:30
|
|
|
const renderEncryptedIcon = () => (
|
|
|
|
<a
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("encrypted-icon tooltip zen-mode-visibility", {
|
|
|
|
"zen-mode-visibility--hidden": zenModeEnabled,
|
|
|
|
})}
|
2020-04-18 02:09:15 +05:30
|
|
|
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
|
|
|
target="_blank"
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
>
|
2020-12-14 18:54:54 +05:30
|
|
|
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
|
|
|
|
{shield}
|
|
|
|
</Tooltip>
|
2020-04-18 02:09:15 +05:30
|
|
|
</a>
|
|
|
|
);
|
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
const renderExportDialog = () => {
|
2020-10-16 11:53:40 +02:00
|
|
|
const createExporter = (type: ExportType): ExportCB => async (
|
2020-04-18 01:54:19 +05:30
|
|
|
exportedElements,
|
|
|
|
scale,
|
|
|
|
) => {
|
|
|
|
if (canvas) {
|
2020-10-28 20:52:53 +01:00
|
|
|
await exportCanvas(type, exportedElements, appState, canvas, {
|
|
|
|
exportBackground: appState.exportBackground,
|
2020-12-01 14:00:13 +01:00
|
|
|
name: appState.name,
|
2020-10-28 20:52:53 +01:00
|
|
|
viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
|
scale,
|
|
|
|
shouldAddWatermark: appState.shouldAddWatermark,
|
|
|
|
})
|
|
|
|
.catch(muteFSAbortError)
|
|
|
|
.catch((error) => {
|
|
|
|
console.error(error);
|
|
|
|
setAppState({ errorMessage: error.message });
|
2020-10-16 11:53:40 +02:00
|
|
|
});
|
2020-04-18 01:54:19 +05:30
|
|
|
}
|
|
|
|
};
|
2020-12-20 19:44:04 +05:30
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
return (
|
|
|
|
<ExportDialog
|
2020-03-07 10:20:38 -05:00
|
|
|
elements={elements}
|
2020-04-18 01:54:19 +05:30
|
|
|
appState={appState}
|
2020-03-07 10:20:38 -05:00
|
|
|
actionManager={actionManager}
|
2020-04-18 01:54:19 +05:30
|
|
|
onExportToPng={createExporter("png")}
|
|
|
|
onExportToSvg={createExporter("svg")}
|
|
|
|
onExportToClipboard={createExporter("clipboard")}
|
2020-12-20 19:44:04 +05:30
|
|
|
onExportToBackend={
|
|
|
|
onExportToBackend
|
|
|
|
? (elements) => {
|
|
|
|
onExportToBackend &&
|
|
|
|
onExportToBackend(elements, appState, canvas);
|
2020-10-28 20:52:53 +01:00
|
|
|
}
|
2020-12-20 19:44:04 +05:30
|
|
|
: undefined
|
|
|
|
}
|
2020-03-07 10:20:38 -05:00
|
|
|
/>
|
2020-04-18 01:54:19 +05:30
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const renderCanvasActions = () => (
|
2020-04-25 18:43:02 +05:30
|
|
|
<Section
|
|
|
|
heading="canvasActions"
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("zen-mode-transition", {
|
|
|
|
"transition-left": zenModeEnabled,
|
|
|
|
})}
|
2020-04-25 18:43:02 +05:30
|
|
|
>
|
2020-04-18 01:54:19 +05:30
|
|
|
{/* the zIndex ensures this menu has higher stacking order,
|
|
|
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
2020-08-21 01:24:46 +03:00
|
|
|
<Island padding={2} style={{ zIndex: 1 }}>
|
2020-04-18 01:54:19 +05:30
|
|
|
<Stack.Col gap={4}>
|
|
|
|
<Stack.Row gap={1} justifyContent="space-between">
|
|
|
|
{actionManager.renderAction("loadScene")}
|
|
|
|
{actionManager.renderAction("saveScene")}
|
2020-06-12 18:35:04 +02:00
|
|
|
{actionManager.renderAction("saveAsScene")}
|
2020-04-18 01:54:19 +05:30
|
|
|
{renderExportDialog()}
|
|
|
|
{actionManager.renderAction("clearCanvas")}
|
2020-12-05 20:00:53 +05:30
|
|
|
{onCollabButtonClick && (
|
|
|
|
<CollabButton
|
|
|
|
isCollaborating={isCollaborating}
|
|
|
|
collaboratorCount={appState.collaborators.size}
|
|
|
|
onClick={onCollabButtonClick}
|
|
|
|
/>
|
|
|
|
)}
|
2020-04-18 01:54:19 +05:30
|
|
|
</Stack.Row>
|
2020-08-13 04:35:31 -07:00
|
|
|
<BackgroundPickerAndDarkModeToggle
|
|
|
|
actionManager={actionManager}
|
|
|
|
appState={appState}
|
|
|
|
setAppState={setAppState}
|
|
|
|
/>
|
2020-04-18 01:54:19 +05:30
|
|
|
</Stack.Col>
|
|
|
|
</Island>
|
|
|
|
</Section>
|
|
|
|
);
|
|
|
|
|
|
|
|
const renderSelectedShapeActions = () => (
|
2020-04-25 18:43:02 +05:30
|
|
|
<Section
|
|
|
|
heading="selectedShapeActions"
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("zen-mode-transition", {
|
|
|
|
"transition-left": zenModeEnabled,
|
|
|
|
})}
|
2020-04-25 18:43:02 +05:30
|
|
|
>
|
2020-08-21 01:24:46 +03:00
|
|
|
<Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={2}>
|
2020-04-18 01:54:19 +05:30
|
|
|
<SelectedShapeActions
|
|
|
|
appState={appState}
|
|
|
|
elements={elements}
|
|
|
|
renderAction={actionManager.renderAction}
|
|
|
|
elementType={appState.elementType}
|
|
|
|
/>
|
|
|
|
</Island>
|
|
|
|
</Section>
|
|
|
|
);
|
|
|
|
|
2020-07-10 02:20:23 -07:00
|
|
|
const closeLibrary = useCallback(
|
|
|
|
(event) => {
|
|
|
|
setAppState({ isLibraryOpen: false });
|
|
|
|
},
|
|
|
|
[setAppState],
|
|
|
|
);
|
|
|
|
|
|
|
|
const deselectItems = useCallback(() => {
|
|
|
|
setAppState({
|
|
|
|
selectedElementIds: {},
|
|
|
|
selectedGroupIds: {},
|
|
|
|
});
|
|
|
|
}, [setAppState]);
|
|
|
|
|
2020-07-20 00:12:56 +03:00
|
|
|
const libraryMenu = appState.isLibraryOpen ? (
|
|
|
|
<LibraryMenu
|
|
|
|
pendingElements={getSelectedElements(elements, appState)}
|
|
|
|
onClickOutside={closeLibrary}
|
2020-12-27 18:26:30 +02:00
|
|
|
onInsertShape={onInsertElements}
|
2020-07-20 00:12:56 +03:00
|
|
|
onAddToLibrary={deselectItems}
|
2020-07-27 15:29:19 +03:00
|
|
|
setAppState={setAppState}
|
2020-07-20 00:12:56 +03:00
|
|
|
/>
|
|
|
|
) : null;
|
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
const renderFixedSideContainer = () => {
|
|
|
|
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
|
|
|
appState,
|
|
|
|
elements,
|
|
|
|
);
|
2020-07-20 00:12:56 +03:00
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
return (
|
|
|
|
<FixedSideContainer side="top">
|
|
|
|
<div className="App-menu App-menu_top">
|
2020-04-29 22:49:36 +02:00
|
|
|
<Stack.Col
|
|
|
|
gap={4}
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
|
2020-04-29 22:49:36 +02:00
|
|
|
>
|
2020-04-18 01:54:19 +05:30
|
|
|
{renderCanvasActions()}
|
|
|
|
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
|
|
|
</Stack.Col>
|
|
|
|
<Section heading="shapes">
|
|
|
|
{(heading) => (
|
|
|
|
<Stack.Col gap={4} align="start">
|
|
|
|
<Stack.Row gap={1}>
|
2020-10-19 17:14:28 +03:00
|
|
|
<Island
|
|
|
|
padding={1}
|
|
|
|
className={clsx({ "zen-mode": zenModeEnabled })}
|
|
|
|
>
|
2020-08-31 14:13:34 +03:00
|
|
|
<HintViewer appState={appState} elements={elements} />
|
2020-04-18 01:54:19 +05:30
|
|
|
{heading}
|
|
|
|
<Stack.Row gap={1}>
|
|
|
|
<ShapesSwitcher
|
|
|
|
elementType={appState.elementType}
|
|
|
|
setAppState={setAppState}
|
2020-07-10 02:20:23 -07:00
|
|
|
isLibraryOpen={appState.isLibraryOpen}
|
2020-03-11 19:42:18 +01:00
|
|
|
/>
|
2020-03-07 10:20:38 -05:00
|
|
|
</Stack.Row>
|
|
|
|
</Island>
|
2020-04-18 01:54:19 +05:30
|
|
|
<LockIcon
|
2020-04-25 18:43:02 +05:30
|
|
|
zenModeEnabled={zenModeEnabled}
|
2020-04-18 01:54:19 +05:30
|
|
|
checked={appState.elementLocked}
|
|
|
|
onChange={onLockToggle}
|
|
|
|
title={t("toolBar.lock")}
|
2020-03-07 10:20:38 -05:00
|
|
|
/>
|
2020-04-18 01:54:19 +05:30
|
|
|
</Stack.Row>
|
2020-07-10 02:20:23 -07:00
|
|
|
{libraryMenu}
|
2020-04-18 01:54:19 +05:30
|
|
|
</Stack.Col>
|
|
|
|
)}
|
|
|
|
</Section>
|
2020-06-19 11:36:49 +01:00
|
|
|
<UserList
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("zen-mode-transition", {
|
|
|
|
"transition-right": zenModeEnabled,
|
|
|
|
})}
|
2020-06-19 11:36:49 +01:00
|
|
|
>
|
|
|
|
{Array.from(appState.collaborators)
|
|
|
|
// Collaborator is either not initialized or is actually the current user.
|
|
|
|
.filter(([_, client]) => Object.keys(client).length !== 0)
|
|
|
|
.map(([clientId, client]) => (
|
|
|
|
<Tooltip
|
|
|
|
label={client.username || "Unknown user"}
|
|
|
|
key={clientId}
|
|
|
|
>
|
|
|
|
{actionManager.renderAction("goToCollaborator", clientId)}
|
|
|
|
</Tooltip>
|
|
|
|
))}
|
|
|
|
</UserList>
|
2020-04-18 01:54:19 +05:30
|
|
|
</div>
|
|
|
|
</FixedSideContainer>
|
2020-03-07 10:20:38 -05:00
|
|
|
);
|
2020-04-18 01:54:19 +05:30
|
|
|
};
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-07-02 18:57:47 +05:30
|
|
|
const renderBottomAppMenu = () => {
|
|
|
|
return (
|
|
|
|
<div
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("App-menu App-menu_bottom zen-mode-transition", {
|
|
|
|
"App-menu_bottom--transition-left": zenModeEnabled,
|
|
|
|
})}
|
2020-07-02 18:57:47 +05:30
|
|
|
>
|
|
|
|
<Stack.Col gap={2}>
|
|
|
|
<Section heading="canvasActions">
|
|
|
|
<Island padding={1}>
|
|
|
|
<ZoomActions
|
|
|
|
renderAction={actionManager.renderAction}
|
|
|
|
zoom={appState.zoom}
|
|
|
|
/>
|
|
|
|
</Island>
|
|
|
|
{renderEncryptedIcon()}
|
|
|
|
</Section>
|
|
|
|
</Stack.Col>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-04-18 01:54:19 +05:30
|
|
|
const renderFooter = () => (
|
2020-04-25 18:43:02 +05:30
|
|
|
<footer role="contentinfo" className="layer-ui__wrapper__footer">
|
|
|
|
<div
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("zen-mode-transition", {
|
|
|
|
"transition-right disable-pointerEvents": zenModeEnabled,
|
|
|
|
})}
|
2020-04-25 18:43:02 +05:30
|
|
|
>
|
2021-01-04 02:21:52 +05:30
|
|
|
{renderCustomFooter?.(false)}
|
2020-04-25 18:43:02 +05:30
|
|
|
{actionManager.renderAction("toggleShortcuts")}
|
|
|
|
</div>
|
|
|
|
<button
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx("disable-zen-mode", {
|
|
|
|
"disable-zen-mode--visible": zenModeEnabled,
|
|
|
|
})}
|
2020-04-25 18:43:02 +05:30
|
|
|
onClick={toggleZenMode}
|
|
|
|
>
|
|
|
|
{t("buttons.exitZenMode")}
|
|
|
|
</button>
|
2020-04-18 01:54:19 +05:30
|
|
|
{appState.scrolledOutside && (
|
|
|
|
<button
|
|
|
|
className="scroll-back-to-content"
|
|
|
|
onClick={() => {
|
2020-05-30 17:32:32 +05:30
|
|
|
setAppState({
|
|
|
|
...calculateScrollCenter(elements, appState, canvas),
|
|
|
|
});
|
2020-04-18 01:54:19 +05:30
|
|
|
}}
|
|
|
|
>
|
|
|
|
{t("buttons.scrollBackToContent")}
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</footer>
|
|
|
|
);
|
2020-03-07 10:20:38 -05:00
|
|
|
|
2020-12-27 18:26:30 +02:00
|
|
|
const dialogs = (
|
|
|
|
<>
|
2020-04-18 01:54:19 +05:30
|
|
|
{appState.isLoading && <LoadingMessage />}
|
|
|
|
{appState.errorMessage && (
|
|
|
|
<ErrorDialog
|
|
|
|
message={appState.errorMessage}
|
|
|
|
onClose={() => setAppState({ errorMessage: null })}
|
|
|
|
/>
|
|
|
|
)}
|
2021-01-17 17:46:23 +01:00
|
|
|
{appState.showHelpDialog && (
|
|
|
|
<HelpDialog onClose={() => setAppState({ showHelpDialog: false })} />
|
2020-04-18 01:54:19 +05:30
|
|
|
)}
|
2020-12-27 18:26:30 +02:00
|
|
|
{appState.pasteDialog.shown && (
|
|
|
|
<PasteChartDialog
|
|
|
|
setAppState={setAppState}
|
|
|
|
appState={appState}
|
|
|
|
onInsertChart={onInsertElements}
|
|
|
|
onClose={() =>
|
|
|
|
setAppState({
|
|
|
|
pasteDialog: { shown: false, data: null },
|
|
|
|
})
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
|
|
|
return isMobile ? (
|
|
|
|
<>
|
|
|
|
{dialogs}
|
|
|
|
<MobileMenu
|
|
|
|
appState={appState}
|
|
|
|
elements={elements}
|
|
|
|
actionManager={actionManager}
|
|
|
|
libraryMenu={libraryMenu}
|
|
|
|
exportButton={renderExportDialog()}
|
|
|
|
setAppState={setAppState}
|
|
|
|
onCollabButtonClick={onCollabButtonClick}
|
|
|
|
onLockToggle={onLockToggle}
|
|
|
|
canvas={canvas}
|
|
|
|
isCollaborating={isCollaborating}
|
2021-01-04 02:21:52 +05:30
|
|
|
renderCustomFooter={renderCustomFooter}
|
2020-12-27 18:26:30 +02:00
|
|
|
/>
|
|
|
|
</>
|
|
|
|
) : (
|
2021-02-01 13:55:38 +01:00
|
|
|
<div
|
|
|
|
className={clsx("layer-ui__wrapper", {
|
|
|
|
"disable-pointerEvents": appState.cursorButton === "down",
|
|
|
|
})}
|
|
|
|
>
|
2020-12-27 18:26:30 +02:00
|
|
|
{dialogs}
|
2020-04-18 01:54:19 +05:30
|
|
|
{renderFixedSideContainer()}
|
2020-07-02 18:57:47 +05:30
|
|
|
{renderBottomAppMenu()}
|
2020-04-25 18:43:02 +05:30
|
|
|
{
|
|
|
|
<aside
|
2020-10-19 17:14:28 +03:00
|
|
|
className={clsx(
|
|
|
|
"layer-ui__wrapper__github-corner zen-mode-transition",
|
|
|
|
{
|
|
|
|
"transition-right": zenModeEnabled,
|
|
|
|
},
|
|
|
|
)}
|
2020-04-25 18:43:02 +05:30
|
|
|
>
|
2020-08-13 04:35:31 -07:00
|
|
|
<GitHubCorner appearance={appState.appearance} />
|
2020-04-25 18:43:02 +05:30
|
|
|
</aside>
|
|
|
|
}
|
2020-04-18 01:54:19 +05:30
|
|
|
{renderFooter()}
|
2020-04-18 02:09:15 +05:30
|
|
|
</div>
|
2020-04-18 01:54:19 +05:30
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
|
|
|
const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
2020-08-08 21:04:15 -07:00
|
|
|
const {
|
|
|
|
suggestedBindings,
|
|
|
|
startBoundElement: boundElement,
|
|
|
|
...ret
|
|
|
|
} = appState;
|
2020-04-18 01:54:19 +05:30
|
|
|
return ret;
|
|
|
|
};
|
|
|
|
const prevAppState = getNecessaryObj(prev.appState);
|
|
|
|
const nextAppState = getNecessaryObj(next.appState);
|
|
|
|
|
|
|
|
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
|
|
return (
|
2021-01-04 02:21:52 +05:30
|
|
|
prev.langCode === next.langCode &&
|
2020-04-18 01:54:19 +05:30
|
|
|
prev.elements === next.elements &&
|
|
|
|
keys.every((key) => prevAppState[key] === nextAppState[key])
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default React.memo(LayerUI, areEqual);
|