MobileMenu.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. import React from "react";
  2. import { AppState } 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 CollabButton from "./CollabButton";
  15. import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
  16. import { LockButton } from "./LockButton";
  17. import { UserList } from "./UserList";
  18. import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
  19. import { LibraryButton } from "./LibraryButton";
  20. type MobileMenuProps = {
  21. appState: AppState;
  22. actionManager: ActionManager;
  23. renderJSONExportDialog: () => React.ReactNode;
  24. renderImageExportDialog: () => React.ReactNode;
  25. setAppState: React.Component<any, AppState>["setState"];
  26. elements: readonly NonDeletedExcalidrawElement[];
  27. libraryMenu: JSX.Element | null;
  28. onCollabButtonClick?: () => void;
  29. onLockToggle: () => void;
  30. canvas: HTMLCanvasElement | null;
  31. isCollaborating: boolean;
  32. renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
  33. viewModeEnabled: boolean;
  34. showThemeBtn: boolean;
  35. renderTopRightUI?: (
  36. isMobile: boolean,
  37. appState: AppState,
  38. ) => JSX.Element | null;
  39. };
  40. export const MobileMenu = ({
  41. appState,
  42. elements,
  43. libraryMenu,
  44. actionManager,
  45. renderJSONExportDialog,
  46. renderImageExportDialog,
  47. setAppState,
  48. onCollabButtonClick,
  49. onLockToggle,
  50. canvas,
  51. isCollaborating,
  52. renderCustomFooter,
  53. viewModeEnabled,
  54. showThemeBtn,
  55. renderTopRightUI,
  56. }: MobileMenuProps) => {
  57. const renderToolbar = () => {
  58. return (
  59. <FixedSideContainer side="top" className="App-top-bar">
  60. <Section heading="shapes">
  61. {(heading) => (
  62. <Stack.Col gap={4} align="center">
  63. <Stack.Row gap={1}>
  64. <Island padding={1}>
  65. {heading}
  66. <Stack.Row gap={1}>
  67. <ShapesSwitcher
  68. canvas={canvas}
  69. elementType={appState.elementType}
  70. setAppState={setAppState}
  71. />
  72. </Stack.Row>
  73. </Island>
  74. {renderTopRightUI && renderTopRightUI(true, appState)}
  75. <LockButton
  76. checked={appState.elementLocked}
  77. onChange={onLockToggle}
  78. title={t("toolBar.lock")}
  79. />
  80. <LibraryButton appState={appState} setAppState={setAppState} />
  81. </Stack.Row>
  82. {libraryMenu}
  83. </Stack.Col>
  84. )}
  85. </Section>
  86. <HintViewer appState={appState} elements={elements} />
  87. </FixedSideContainer>
  88. );
  89. };
  90. const renderAppToolbar = () => {
  91. if (viewModeEnabled) {
  92. return (
  93. <div className="App-toolbar-content">
  94. {actionManager.renderAction("toggleCanvasMenu")}
  95. </div>
  96. );
  97. }
  98. return (
  99. <div className="App-toolbar-content">
  100. {actionManager.renderAction("toggleCanvasMenu")}
  101. {actionManager.renderAction("toggleEditMenu")}
  102. {actionManager.renderAction("undo")}
  103. {actionManager.renderAction("redo")}
  104. {actionManager.renderAction(
  105. appState.multiElement ? "finalize" : "duplicateSelection",
  106. )}
  107. {actionManager.renderAction("deleteSelectedElements")}
  108. </div>
  109. );
  110. };
  111. const renderCanvasActions = () => {
  112. if (viewModeEnabled) {
  113. return (
  114. <>
  115. {renderJSONExportDialog()}
  116. {renderImageExportDialog()}
  117. </>
  118. );
  119. }
  120. return (
  121. <>
  122. {actionManager.renderAction("clearCanvas")}
  123. {actionManager.renderAction("loadScene")}
  124. {renderJSONExportDialog()}
  125. {renderImageExportDialog()}
  126. {onCollabButtonClick && (
  127. <CollabButton
  128. isCollaborating={isCollaborating}
  129. collaboratorCount={appState.collaborators.size}
  130. onClick={onCollabButtonClick}
  131. />
  132. )}
  133. {
  134. <BackgroundPickerAndDarkModeToggle
  135. actionManager={actionManager}
  136. appState={appState}
  137. setAppState={setAppState}
  138. showThemeBtn={showThemeBtn}
  139. />
  140. }
  141. </>
  142. );
  143. };
  144. return (
  145. <>
  146. {!viewModeEnabled && renderToolbar()}
  147. <div
  148. className="App-bottom-bar"
  149. style={{
  150. marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
  151. marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
  152. marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
  153. }}
  154. >
  155. <Island padding={0}>
  156. {appState.openMenu === "canvas" ? (
  157. <Section className="App-mobile-menu" heading="canvasActions">
  158. <div className="panelColumn">
  159. <Stack.Col gap={4}>
  160. {renderCanvasActions()}
  161. {renderCustomFooter?.(true, appState)}
  162. {appState.collaborators.size > 0 && (
  163. <fieldset>
  164. <legend>{t("labels.collaborators")}</legend>
  165. <UserList mobile>
  166. {Array.from(appState.collaborators)
  167. // Collaborator is either not initialized or is actually the current user.
  168. .filter(
  169. ([_, client]) => Object.keys(client).length !== 0,
  170. )
  171. .map(([clientId, client]) => (
  172. <React.Fragment key={clientId}>
  173. {actionManager.renderAction("goToCollaborator", {
  174. id: clientId,
  175. })}
  176. </React.Fragment>
  177. ))}
  178. </UserList>
  179. </fieldset>
  180. )}
  181. </Stack.Col>
  182. </div>
  183. </Section>
  184. ) : appState.openMenu === "shape" &&
  185. !viewModeEnabled &&
  186. showSelectedShapeActions(appState, elements) ? (
  187. <Section className="App-mobile-menu" heading="selectedShapeActions">
  188. <SelectedShapeActions
  189. appState={appState}
  190. elements={elements}
  191. renderAction={actionManager.renderAction}
  192. elementType={appState.elementType}
  193. />
  194. </Section>
  195. ) : null}
  196. <footer className="App-toolbar">
  197. {renderAppToolbar()}
  198. {appState.scrolledOutside && !appState.openMenu && (
  199. <button
  200. className="scroll-back-to-content"
  201. onClick={() => {
  202. setAppState({
  203. ...calculateScrollCenter(elements, appState, canvas),
  204. });
  205. }}
  206. >
  207. {t("buttons.scrollBackToContent")}
  208. </button>
  209. )}
  210. </footer>
  211. </Island>
  212. </div>
  213. </>
  214. );
  215. };