MobileMenu.tsx 7.0 KB

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