LayerUI.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import React from "react";
  2. import { showSelectedShapeActions } from "../element";
  3. import { calculateScrollCenter } from "../scene";
  4. import { exportCanvas } from "../data";
  5. import { AppState } from "../types";
  6. import { ExcalidrawElement } from "../element/types";
  7. import { ActionManager } from "../actions/manager";
  8. import { Island } from "./Island";
  9. import Stack from "./Stack";
  10. import { FixedSideContainer } from "./FixedSideContainer";
  11. import { LockIcon } from "./LockIcon";
  12. import { ExportDialog, ExportCB } from "./ExportDialog";
  13. import { LanguageList } from "./LanguageList";
  14. import { t, languages, setLanguage } from "../i18n";
  15. import { HintViewer } from "./HintViewer";
  16. import useIsMobile from "../is-mobile";
  17. import { ExportType } from "../scene/types";
  18. import { MobileMenu } from "./MobileMenu";
  19. import { ZoomActions, SelectedShapeActions, ShapesSwitcher } from "./Actions";
  20. import { Section } from "./Section";
  21. import { RoomDialog } from "./RoomDialog";
  22. import { ErrorDialog } from "./ErrorDialog";
  23. import { LoadingMessage } from "./LoadingMessage";
  24. interface LayerUIProps {
  25. actionManager: ActionManager;
  26. appState: AppState;
  27. canvas: HTMLCanvasElement | null;
  28. setAppState: any;
  29. elements: readonly ExcalidrawElement[];
  30. setElements: (elements: readonly ExcalidrawElement[]) => void;
  31. onRoomCreate: () => void;
  32. onRoomDestroy: () => void;
  33. onLockToggle: () => void;
  34. }
  35. export const LayerUI = React.memo(
  36. ({
  37. actionManager,
  38. appState,
  39. setAppState,
  40. canvas,
  41. elements,
  42. setElements,
  43. onRoomCreate,
  44. onRoomDestroy,
  45. onLockToggle,
  46. }: LayerUIProps) => {
  47. const isMobile = useIsMobile();
  48. function renderExportDialog() {
  49. const createExporter = (type: ExportType): ExportCB => (
  50. exportedElements,
  51. scale,
  52. ) => {
  53. if (canvas) {
  54. exportCanvas(type, exportedElements, appState, canvas, {
  55. exportBackground: appState.exportBackground,
  56. name: appState.name,
  57. viewBackgroundColor: appState.viewBackgroundColor,
  58. scale,
  59. });
  60. }
  61. };
  62. return (
  63. <ExportDialog
  64. elements={elements}
  65. appState={appState}
  66. actionManager={actionManager}
  67. onExportToPng={createExporter("png")}
  68. onExportToSvg={createExporter("svg")}
  69. onExportToClipboard={createExporter("clipboard")}
  70. onExportToBackend={(exportedElements) => {
  71. if (canvas) {
  72. exportCanvas(
  73. "backend",
  74. exportedElements,
  75. {
  76. ...appState,
  77. selectedElementIds: {},
  78. },
  79. canvas,
  80. appState,
  81. );
  82. }
  83. }}
  84. />
  85. );
  86. }
  87. return isMobile ? (
  88. <MobileMenu
  89. appState={appState}
  90. elements={elements}
  91. setElements={setElements}
  92. actionManager={actionManager}
  93. exportButton={renderExportDialog()}
  94. setAppState={setAppState}
  95. onRoomCreate={onRoomCreate}
  96. onRoomDestroy={onRoomDestroy}
  97. onLockToggle={onLockToggle}
  98. />
  99. ) : (
  100. <>
  101. {appState.isLoading && <LoadingMessage />}
  102. {appState.errorMessage && (
  103. <ErrorDialog
  104. message={appState.errorMessage}
  105. onClose={() => setAppState({ errorMessage: null })}
  106. />
  107. )}
  108. <FixedSideContainer side="top">
  109. <HintViewer appState={appState} elements={elements} />
  110. <div className="App-menu App-menu_top">
  111. <Stack.Col gap={4}>
  112. <Section heading="canvasActions">
  113. <Island padding={4}>
  114. <Stack.Col gap={4}>
  115. <Stack.Row gap={1} justifyContent={"space-between"}>
  116. {actionManager.renderAction("loadScene")}
  117. {actionManager.renderAction("saveScene")}
  118. {renderExportDialog()}
  119. {actionManager.renderAction("clearCanvas")}
  120. <RoomDialog
  121. isCollaborating={appState.isCollaborating}
  122. collaboratorCount={appState.collaborators.size}
  123. onRoomCreate={onRoomCreate}
  124. onRoomDestroy={onRoomDestroy}
  125. />
  126. </Stack.Row>
  127. {actionManager.renderAction("changeViewBackgroundColor")}
  128. </Stack.Col>
  129. </Island>
  130. </Section>
  131. {showSelectedShapeActions(appState, elements) && (
  132. <Section heading="selectedShapeActions">
  133. <Island className="App-menu__left" padding={4}>
  134. <SelectedShapeActions
  135. appState={appState}
  136. elements={elements}
  137. renderAction={actionManager.renderAction}
  138. elementType={appState.elementType}
  139. />
  140. </Island>
  141. </Section>
  142. )}
  143. </Stack.Col>
  144. <Section heading="shapes">
  145. {(heading) => (
  146. <Stack.Col gap={4} align="start">
  147. <Stack.Row gap={1}>
  148. <Island padding={1}>
  149. {heading}
  150. <Stack.Row gap={1}>
  151. <ShapesSwitcher
  152. elementType={appState.elementType}
  153. setAppState={setAppState}
  154. setElements={setElements}
  155. elements={elements}
  156. />
  157. </Stack.Row>
  158. </Island>
  159. <LockIcon
  160. checked={appState.elementLocked}
  161. onChange={onLockToggle}
  162. title={t("toolBar.lock")}
  163. />
  164. </Stack.Row>
  165. </Stack.Col>
  166. )}
  167. </Section>
  168. <div />
  169. </div>
  170. <div className="App-menu App-menu_bottom">
  171. <Stack.Col gap={2}>
  172. <Section heading="canvasActions">
  173. <Island padding={1}>
  174. <ZoomActions
  175. renderAction={actionManager.renderAction}
  176. zoom={appState.zoom}
  177. />
  178. </Island>
  179. </Section>
  180. </Stack.Col>
  181. </div>
  182. </FixedSideContainer>
  183. <footer role="contentinfo">
  184. <LanguageList
  185. onChange={(lng) => {
  186. setLanguage(lng);
  187. setAppState({});
  188. }}
  189. languages={languages}
  190. floating
  191. />
  192. {appState.scrolledOutside && (
  193. <button
  194. className="scroll-back-to-content"
  195. onClick={() => {
  196. setAppState({ ...calculateScrollCenter(elements) });
  197. }}
  198. >
  199. {t("buttons.scrollBackToContent")}
  200. </button>
  201. )}
  202. </footer>
  203. </>
  204. );
  205. },
  206. (prev, next) => {
  207. const getNecessaryObj = (appState: AppState): Partial<AppState> => {
  208. const {
  209. draggingElement,
  210. resizingElement,
  211. multiElement,
  212. editingElement,
  213. isResizing,
  214. cursorX,
  215. cursorY,
  216. ...ret
  217. } = appState;
  218. return ret;
  219. };
  220. const prevAppState = getNecessaryObj(prev.appState);
  221. const nextAppState = getNecessaryObj(next.appState);
  222. const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
  223. return (
  224. prev.elements === next.elements &&
  225. keys.every((key) => prevAppState[key] === nextAppState[key])
  226. );
  227. },
  228. );