MobileMenu.tsx 6.9 KB

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