import clsx from "clsx"; import React, { useCallback } from "react"; import { ActionManager } from "../actions/manager"; import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants"; import { exportCanvas } from "../data"; import { isTextElement, showSelectedShapeActions } from "../element"; import { NonDeletedExcalidrawElement } from "../element/types"; import { Language, t } from "../i18n"; import { calculateScrollCenter, getSelectedElements } from "../scene"; import { ExportType } from "../scene/types"; import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; import { muteFSAbortError } from "../utils"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import CollabButton from "./CollabButton"; import { ErrorDialog } from "./ErrorDialog"; import { ExportCB, ImageExportDialog } from "./ImageExportDialog"; import { FixedSideContainer } from "./FixedSideContainer"; import { HintViewer } from "./HintViewer"; import { Island } from "./Island"; import { LoadingMessage } from "./LoadingMessage"; import { LockButton } from "./LockButton"; import { MobileMenu } from "./MobileMenu"; import { PasteChartDialog } from "./PasteChartDialog"; import { Section } from "./Section"; import { HelpDialog } from "./HelpDialog"; import Stack from "./Stack"; import { UserList } from "./UserList"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library"; import { JSONExportDialog } from "./JSONExportDialog"; import { LibraryButton } from "./LibraryButton"; import { isImageFileHandle } from "../data/blob"; import { LibraryMenu } from "./LibraryMenu"; import "./LayerUI.scss"; import "./Toolbar.scss"; import { PenModeButton } from "./PenModeButton"; import { trackEvent } from "../analytics"; import { useDevice } from "../components/App"; import { Stats } from "./Stats"; import { actionToggleStats } from "../actions/actionToggleStats"; import Footer from "./Footer"; interface LayerUIProps { actionManager: ActionManager; appState: AppState; files: BinaryFiles; canvas: HTMLCanvasElement | null; setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; onCollabButtonClick?: () => void; onLockToggle: () => void; onPenModeToggle: () => void; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; showExitZenModeBtn: boolean; showThemeBtn: boolean; langCode: Language["code"]; isCollaborating: boolean; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderCustomFooter?: ExcalidrawProps["renderFooter"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; UIOptions: AppProps["UIOptions"]; focusContainer: () => void; library: Library; id: string; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; } const LayerUI = ({ actionManager, appState, files, setAppState, elements, canvas, onCollabButtonClick, onLockToggle, onPenModeToggle, onInsertElements, showExitZenModeBtn, showThemeBtn, isCollaborating, renderTopRightUI, renderCustomFooter, renderCustomStats, libraryReturnUrl, UIOptions, focusContainer, library, id, onImageAction, }: LayerUIProps) => { const device = useDevice(); const renderJSONExportDialog = () => { if (!UIOptions.canvasActions.export) { return null; } return ( ); }; const renderImageExportDialog = () => { if (!UIOptions.canvasActions.saveAsImage) { return null; } const createExporter = (type: ExportType): ExportCB => async (exportedElements) => { trackEvent("export", type, "ui"); const fileHandle = await exportCanvas( type, exportedElements, appState, files, { exportBackground: appState.exportBackground, name: appState.name, viewBackgroundColor: appState.viewBackgroundColor, }, ) .catch(muteFSAbortError) .catch((error) => { console.error(error); setAppState({ errorMessage: error.message }); }); if ( appState.exportEmbedScene && fileHandle && isImageFileHandle(fileHandle) ) { setAppState({ fileHandle }); } }; return ( ); }; const Separator = () => { return
; }; const renderViewModeCanvasActions = () => { return (
{/* the zIndex ensures this menu has higher stacking order, see https://github.com/excalidraw/excalidraw/pull/1445 */} {renderJSONExportDialog()} {renderImageExportDialog()}
); }; const renderCanvasActions = () => (
{/* the zIndex ensures this menu has higher stacking order, see https://github.com/excalidraw/excalidraw/pull/1445 */} {actionManager.renderAction("clearCanvas")} {actionManager.renderAction("loadScene")} {renderJSONExportDialog()} {renderImageExportDialog()} {onCollabButtonClick && ( )} {appState.fileHandle && ( <>{actionManager.renderAction("saveToActiveFile")} )}
); const renderSelectedShapeActions = () => (
); const closeLibrary = useCallback(() => { const isDialogOpen = !!document.querySelector(".Dialog"); // Prevent closing if any dialog is open if (isDialogOpen) { return; } setAppState({ isLibraryOpen: false }); }, [setAppState]); const deselectItems = useCallback(() => { setAppState({ selectedElementIds: {}, selectedGroupIds: {}, }); }, [setAppState]); const libraryMenu = appState.isLibraryOpen ? ( { onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems)); }} onAddToLibrary={deselectItems} setAppState={setAppState} libraryReturnUrl={libraryReturnUrl} focusContainer={focusContainer} library={library} files={files} id={id} appState={appState} /> ) : null; const renderFixedSideContainer = () => { const shouldRenderSelectedShapeActions = showSelectedShapeActions( appState, elements, ); return (
{appState.viewModeEnabled ? renderViewModeCanvasActions() : renderCanvasActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()} {!appState.viewModeEnabled && (
{(heading: React.ReactNode) => ( onLockToggle()} title={t("toolBar.lock")} /> {heading} { onImageAction({ insertOnCanvasDirectly: pointerType !== "mouse", }); }} /> )}
)}
{renderTopRightUI?.(device.isMobile, appState)}
); }; return ( <> {appState.isLoading && } {appState.errorMessage && ( setAppState({ errorMessage: null })} /> )} {appState.showHelpDialog && ( { setAppState({ showHelpDialog: false }); }} /> )} {appState.pasteDialog.shown && ( setAppState({ pasteDialog: { shown: false, data: null }, }) } /> )} {device.isMobile && ( onLockToggle()} onPenModeToggle={onPenModeToggle} canvas={canvas} isCollaborating={isCollaborating} renderCustomFooter={renderCustomFooter} showThemeBtn={showThemeBtn} onImageAction={onImageAction} renderTopRightUI={renderTopRightUI} renderCustomStats={renderCustomStats} /> )} {!device.isMobile && (
{renderFixedSideContainer()}
{appState.showStats && ( { actionManager.executeAction(actionToggleStats); }} renderCustomStats={renderCustomStats} /> )} {appState.scrolledOutside && ( )} {appState.isLibraryOpen && (
{libraryMenu}
)}
)} ); }; const areEqual = (prev: LayerUIProps, next: LayerUIProps) => { const getNecessaryObj = (appState: AppState): Partial => { const { suggestedBindings, startBoundElement: boundElement, ...ret } = appState; return ret; }; const prevAppState = getNecessaryObj(prev.appState); const nextAppState = getNecessaryObj(next.appState); const keys = Object.keys(prevAppState) as (keyof Partial)[]; return ( prev.renderCustomFooter === next.renderCustomFooter && prev.langCode === next.langCode && prev.elements === next.elements && prev.files === next.files && keys.every((key) => prevAppState[key] === nextAppState[key]) ); }; export default React.memo(LayerUI, areEqual);