MobileMenu.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import React from "react";
  2. import { AppState, Device, ExcalidrawProps } from "../types";
  3. import { ActionManager } from "../actions/manager";
  4. import { t } from "../i18n";
  5. import Stack from "./Stack";
  6. import { showSelectedShapeActions } from "../element";
  7. import { NonDeletedExcalidrawElement } from "../element/types";
  8. import { FixedSideContainer } from "./FixedSideContainer";
  9. import { Island } from "./Island";
  10. import { HintViewer } from "./HintViewer";
  11. import { calculateScrollCenter } from "../scene";
  12. import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
  13. import { Section } from "./Section";
  14. import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
  15. import { LockButton } from "./LockButton";
  16. import { LibraryButton } from "./LibraryButton";
  17. import { PenModeButton } from "./PenModeButton";
  18. import { Stats } from "./Stats";
  19. import { actionToggleStats } from "../actions";
  20. import WelcomeScreen from "./WelcomeScreen";
  21. type MobileMenuProps = {
  22. appState: AppState;
  23. actionManager: ActionManager;
  24. renderJSONExportDialog: () => React.ReactNode;
  25. renderImageExportDialog: () => React.ReactNode;
  26. setAppState: React.Component<any, AppState>["setState"];
  27. elements: readonly NonDeletedExcalidrawElement[];
  28. onCollabButtonClick?: () => void;
  29. onLockToggle: () => void;
  30. onPenModeToggle: () => void;
  31. canvas: HTMLCanvasElement | null;
  32. isCollaborating: boolean;
  33. onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
  34. renderTopRightUI?: (
  35. isMobile: boolean,
  36. appState: AppState,
  37. ) => JSX.Element | null;
  38. renderCustomStats?: ExcalidrawProps["renderCustomStats"];
  39. renderSidebars: () => JSX.Element | null;
  40. device: Device;
  41. renderWelcomeScreen?: boolean;
  42. renderMenu: () => React.ReactNode;
  43. };
  44. export const MobileMenu = ({
  45. appState,
  46. elements,
  47. actionManager,
  48. setAppState,
  49. onLockToggle,
  50. onPenModeToggle,
  51. canvas,
  52. isCollaborating,
  53. onImageAction,
  54. renderTopRightUI,
  55. renderCustomStats,
  56. renderSidebars,
  57. device,
  58. renderWelcomeScreen,
  59. renderMenu,
  60. }: MobileMenuProps) => {
  61. const renderToolbar = () => {
  62. return (
  63. <FixedSideContainer side="top" className="App-top-bar">
  64. {renderWelcomeScreen && !appState.isLoading && (
  65. <WelcomeScreen appState={appState} actionManager={actionManager} />
  66. )}
  67. <Section heading="shapes">
  68. {(heading: React.ReactNode) => (
  69. <Stack.Col gap={4} align="center">
  70. <Stack.Row gap={1} className="App-toolbar-container">
  71. <Island padding={1} className="App-toolbar App-toolbar--mobile">
  72. {heading}
  73. <Stack.Row gap={1}>
  74. {/* <PenModeButton
  75. checked={appState.penMode}
  76. onChange={onPenModeToggle}
  77. title={t("toolBar.penMode")}
  78. isMobile
  79. penDetected={appState.penDetected}
  80. />
  81. <LockButton
  82. checked={appState.activeTool.locked}
  83. onChange={onLockToggle}
  84. title={t("toolBar.lock")}
  85. isMobile
  86. />
  87. <div className="App-toolbar__divider"></div> */}
  88. <ShapesSwitcher
  89. appState={appState}
  90. canvas={canvas}
  91. activeTool={appState.activeTool}
  92. setAppState={setAppState}
  93. onImageAction={({ pointerType }) => {
  94. onImageAction({
  95. insertOnCanvasDirectly: pointerType !== "mouse",
  96. });
  97. }}
  98. />
  99. </Stack.Row>
  100. </Island>
  101. {renderTopRightUI && renderTopRightUI(true, appState)}
  102. <div className="mobile-misc-tools-container">
  103. <PenModeButton
  104. checked={appState.penMode}
  105. onChange={onPenModeToggle}
  106. title={t("toolBar.penMode")}
  107. isMobile
  108. penDetected={appState.penDetected}
  109. // penDetected={true}
  110. />
  111. <LockButton
  112. checked={appState.activeTool.locked}
  113. onChange={onLockToggle}
  114. title={t("toolBar.lock")}
  115. isMobile
  116. />
  117. {!appState.viewModeEnabled && (
  118. <LibraryButton
  119. appState={appState}
  120. setAppState={setAppState}
  121. isMobile
  122. />
  123. )}
  124. </div>
  125. </Stack.Row>
  126. </Stack.Col>
  127. )}
  128. </Section>
  129. <HintViewer
  130. appState={appState}
  131. elements={elements}
  132. isMobile={true}
  133. device={device}
  134. />
  135. </FixedSideContainer>
  136. );
  137. };
  138. const renderAppToolbar = () => {
  139. if (appState.viewModeEnabled) {
  140. return <div className="App-toolbar-content">{renderMenu()}</div>;
  141. }
  142. return (
  143. <div className="App-toolbar-content">
  144. {renderMenu()}
  145. {actionManager.renderAction("toggleEditMenu")}
  146. {actionManager.renderAction("undo")}
  147. {actionManager.renderAction("redo")}
  148. {actionManager.renderAction(
  149. appState.multiElement ? "finalize" : "duplicateSelection",
  150. )}
  151. {actionManager.renderAction("deleteSelectedElements")}
  152. </div>
  153. );
  154. };
  155. return (
  156. <>
  157. {renderSidebars()}
  158. {!appState.viewModeEnabled && renderToolbar()}
  159. {!appState.openMenu && appState.showStats && (
  160. <Stats
  161. appState={appState}
  162. setAppState={setAppState}
  163. elements={elements}
  164. onClose={() => {
  165. actionManager.executeAction(actionToggleStats);
  166. }}
  167. renderCustomStats={renderCustomStats}
  168. />
  169. )}
  170. <div
  171. className="App-bottom-bar"
  172. style={{
  173. marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
  174. marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
  175. marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
  176. }}
  177. >
  178. <Island padding={0}>
  179. {appState.openMenu === "shape" &&
  180. !appState.viewModeEnabled &&
  181. showSelectedShapeActions(appState, elements) ? (
  182. <Section className="App-mobile-menu" heading="selectedShapeActions">
  183. <SelectedShapeActions
  184. appState={appState}
  185. elements={elements}
  186. renderAction={actionManager.renderAction}
  187. />
  188. </Section>
  189. ) : null}
  190. <footer className="App-toolbar">
  191. {renderAppToolbar()}
  192. {appState.scrolledOutside &&
  193. !appState.openMenu &&
  194. appState.openSidebar !== "library" && (
  195. <button
  196. className="scroll-back-to-content"
  197. onClick={() => {
  198. setAppState({
  199. ...calculateScrollCenter(elements, appState, canvas),
  200. });
  201. }}
  202. >
  203. {t("buttons.scrollBackToContent")}
  204. </button>
  205. )}
  206. </footer>
  207. </Island>
  208. </div>
  209. </>
  210. );
  211. };