aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/components/MobileMenu.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/excalidraw/components/MobileMenu.tsx')
-rw-r--r--packages/excalidraw/components/MobileMenu.tsx211
1 files changed, 211 insertions, 0 deletions
diff --git a/packages/excalidraw/components/MobileMenu.tsx b/packages/excalidraw/components/MobileMenu.tsx
new file mode 100644
index 0000000..bef99a9
--- /dev/null
+++ b/packages/excalidraw/components/MobileMenu.tsx
@@ -0,0 +1,211 @@
+import type { JSX } from "react";
+import React from "react";
+import type {
+ AppClassProperties,
+ AppProps,
+ AppState,
+ Device,
+ ExcalidrawProps,
+ UIAppState,
+} from "../types";
+import type { ActionManager } from "../actions/manager";
+import { t } from "../i18n";
+import Stack from "./Stack";
+import { showSelectedShapeActions } from "../element";
+import type { NonDeletedExcalidrawElement } from "../element/types";
+import { FixedSideContainer } from "./FixedSideContainer";
+import { Island } from "./Island";
+import { HintViewer } from "./HintViewer";
+import { calculateScrollCenter } from "../scene";
+import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
+import { Section } from "./Section";
+import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
+import { LockButton } from "./LockButton";
+import { PenModeButton } from "./PenModeButton";
+import { HandButton } from "./HandButton";
+import { isHandToolActive } from "../appState";
+import { useTunnels } from "../context/tunnels";
+
+type MobileMenuProps = {
+ appState: UIAppState;
+ actionManager: ActionManager;
+ renderJSONExportDialog: () => React.ReactNode;
+ renderImageExportDialog: () => React.ReactNode;
+ setAppState: React.Component<any, AppState>["setState"];
+ elements: readonly NonDeletedExcalidrawElement[];
+ onLockToggle: () => void;
+ onHandToolToggle: () => void;
+ onPenModeToggle: AppClassProperties["togglePenMode"];
+
+ renderTopRightUI?: (
+ isMobile: boolean,
+ appState: UIAppState,
+ ) => JSX.Element | null;
+ renderCustomStats?: ExcalidrawProps["renderCustomStats"];
+ renderSidebars: () => JSX.Element | null;
+ device: Device;
+ renderWelcomeScreen: boolean;
+ UIOptions: AppProps["UIOptions"];
+ app: AppClassProperties;
+};
+
+export const MobileMenu = ({
+ appState,
+ elements,
+ actionManager,
+ setAppState,
+ onLockToggle,
+ onHandToolToggle,
+ onPenModeToggle,
+
+ renderTopRightUI,
+ renderCustomStats,
+ renderSidebars,
+ device,
+ renderWelcomeScreen,
+ UIOptions,
+ app,
+}: MobileMenuProps) => {
+ const {
+ WelcomeScreenCenterTunnel,
+ MainMenuTunnel,
+ DefaultSidebarTriggerTunnel,
+ } = useTunnels();
+ const renderToolbar = () => {
+ return (
+ <FixedSideContainer side="top" className="App-top-bar">
+ {renderWelcomeScreen && <WelcomeScreenCenterTunnel.Out />}
+ <Section heading="shapes">
+ {(heading: React.ReactNode) => (
+ <Stack.Col gap={4} align="center">
+ <Stack.Row gap={1} className="App-toolbar-container">
+ <Island padding={1} className="App-toolbar App-toolbar--mobile">
+ {heading}
+ <Stack.Row gap={1}>
+ <ShapesSwitcher
+ appState={appState}
+ activeTool={appState.activeTool}
+ UIOptions={UIOptions}
+ app={app}
+ />
+ </Stack.Row>
+ </Island>
+ {renderTopRightUI && renderTopRightUI(true, appState)}
+ <div className="mobile-misc-tools-container">
+ {!appState.viewModeEnabled &&
+ appState.openDialog?.name !== "elementLinkSelector" && (
+ <DefaultSidebarTriggerTunnel.Out />
+ )}
+ <PenModeButton
+ checked={appState.penMode}
+ onChange={() => onPenModeToggle(null)}
+ title={t("toolBar.penMode")}
+ isMobile
+ penDetected={appState.penDetected}
+ />
+ <LockButton
+ checked={appState.activeTool.locked}
+ onChange={onLockToggle}
+ title={t("toolBar.lock")}
+ isMobile
+ />
+ <HandButton
+ checked={isHandToolActive(appState)}
+ onChange={() => onHandToolToggle()}
+ title={t("toolBar.hand")}
+ isMobile
+ />
+ </div>
+ </Stack.Row>
+ </Stack.Col>
+ )}
+ </Section>
+ <HintViewer
+ appState={appState}
+ isMobile={true}
+ device={device}
+ app={app}
+ />
+ </FixedSideContainer>
+ );
+ };
+
+ const renderAppToolbar = () => {
+ if (
+ appState.viewModeEnabled ||
+ appState.openDialog?.name === "elementLinkSelector"
+ ) {
+ return (
+ <div className="App-toolbar-content">
+ <MainMenuTunnel.Out />
+ </div>
+ );
+ }
+
+ return (
+ <div className="App-toolbar-content">
+ <MainMenuTunnel.Out />
+ {actionManager.renderAction("toggleEditMenu")}
+ {actionManager.renderAction(
+ appState.multiElement ? "finalize" : "duplicateSelection",
+ )}
+ {actionManager.renderAction("deleteSelectedElements")}
+ <div>
+ {actionManager.renderAction("undo")}
+ {actionManager.renderAction("redo")}
+ </div>
+ </div>
+ );
+ };
+
+ return (
+ <>
+ {renderSidebars()}
+ {!appState.viewModeEnabled &&
+ appState.openDialog?.name !== "elementLinkSelector" &&
+ renderToolbar()}
+ <div
+ className="App-bottom-bar"
+ style={{
+ marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+ marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+ marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
+ }}
+ >
+ <Island padding={0}>
+ {appState.openMenu === "shape" &&
+ !appState.viewModeEnabled &&
+ appState.openDialog?.name !== "elementLinkSelector" &&
+ showSelectedShapeActions(appState, elements) ? (
+ <Section className="App-mobile-menu" heading="selectedShapeActions">
+ <SelectedShapeActions
+ appState={appState}
+ elementsMap={app.scene.getNonDeletedElementsMap()}
+ renderAction={actionManager.renderAction}
+ app={app}
+ />
+ </Section>
+ ) : null}
+ <footer className="App-toolbar">
+ {renderAppToolbar()}
+ {appState.scrolledOutside &&
+ !appState.openMenu &&
+ !appState.openSidebar && (
+ <button
+ type="button"
+ className="scroll-back-to-content"
+ onClick={() => {
+ setAppState((appState) => ({
+ ...calculateScrollCenter(elements, appState),
+ }));
+ }}
+ >
+ {t("buttons.scrollBackToContent")}
+ </button>
+ )}
+ </footer>
+ </Island>
+ </div>
+ </>
+ );
+};