LayerUI.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import React from "react";
  2. import { showSelectedShapeActions } from "../element";
  3. import { calculateScrollCenter, getTargetElement } 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. interface LayerUIProps {
  23. actionManager: ActionManager;
  24. appState: AppState;
  25. canvas: HTMLCanvasElement | null;
  26. setAppState: any;
  27. elements: readonly ExcalidrawElement[];
  28. language: string;
  29. setElements: (elements: readonly ExcalidrawElement[]) => void;
  30. onRoomCreate: () => void;
  31. onRoomDestroy: () => void;
  32. onLockToggle: () => void;
  33. }
  34. export const LayerUI = React.memo(
  35. ({
  36. actionManager,
  37. appState,
  38. setAppState,
  39. canvas,
  40. elements,
  41. language,
  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. <FixedSideContainer side="top">
  102. <HintViewer appState={appState} elements={elements} />
  103. <div className="App-menu App-menu_top">
  104. <Stack.Col gap={4}>
  105. <Section className="App-right-menu" heading="canvasActions">
  106. <Island padding={4}>
  107. <Stack.Col gap={4}>
  108. <Stack.Row gap={1} justifyContent={"space-between"}>
  109. {actionManager.renderAction("loadScene")}
  110. {actionManager.renderAction("saveScene")}
  111. {renderExportDialog()}
  112. {actionManager.renderAction("clearCanvas")}
  113. <RoomDialog
  114. isCollaborating={appState.isCollaborating}
  115. collaboratorCount={appState.collaborators.size}
  116. onRoomCreate={onRoomCreate}
  117. onRoomDestroy={onRoomDestroy}
  118. />
  119. </Stack.Row>
  120. {actionManager.renderAction("changeViewBackgroundColor")}
  121. </Stack.Col>
  122. </Island>
  123. </Section>
  124. {showSelectedShapeActions(appState, elements) && (
  125. <Section
  126. className="App-right-menu"
  127. heading="selectedShapeActions"
  128. >
  129. <Island padding={4}>
  130. <SelectedShapeActions
  131. targetElements={getTargetElement(elements, appState)}
  132. renderAction={actionManager.renderAction}
  133. elementType={appState.elementType}
  134. />
  135. </Island>
  136. </Section>
  137. )}
  138. </Stack.Col>
  139. <Section heading="shapes">
  140. {(heading) => (
  141. <Stack.Col gap={4} align="start">
  142. <Stack.Row gap={1}>
  143. <Island padding={1}>
  144. {heading}
  145. <Stack.Row gap={1}>
  146. <ShapesSwitcher
  147. elementType={appState.elementType}
  148. setAppState={setAppState}
  149. setElements={setElements}
  150. elements={elements}
  151. />
  152. </Stack.Row>
  153. </Island>
  154. <LockIcon
  155. checked={appState.elementLocked}
  156. onChange={onLockToggle}
  157. title={t("toolBar.lock")}
  158. />
  159. </Stack.Row>
  160. </Stack.Col>
  161. )}
  162. </Section>
  163. <div />
  164. </div>
  165. <div className="App-menu App-menu_bottom">
  166. <Stack.Col gap={2}>
  167. <Section heading="canvasActions">
  168. <Island padding={1}>
  169. <ZoomActions
  170. renderAction={actionManager.renderAction}
  171. zoom={appState.zoom}
  172. />
  173. </Island>
  174. </Section>
  175. </Stack.Col>
  176. </div>
  177. </FixedSideContainer>
  178. <footer role="contentinfo">
  179. <LanguageList
  180. onChange={(lng) => {
  181. setLanguage(lng);
  182. setAppState({});
  183. }}
  184. languages={languages}
  185. currentLanguage={language}
  186. floating
  187. />
  188. {appState.scrolledOutside && (
  189. <button
  190. className="scroll-back-to-content"
  191. onClick={() => {
  192. setAppState({ ...calculateScrollCenter(elements) });
  193. }}
  194. >
  195. {t("buttons.scrollBackToContent")}
  196. </button>
  197. )}
  198. </footer>
  199. </>
  200. );
  201. },
  202. (prev, next) => {
  203. const getNecessaryObj = (appState: AppState): Partial<AppState> => {
  204. const {
  205. draggingElement,
  206. resizingElement,
  207. multiElement,
  208. editingElement,
  209. isResizing,
  210. cursorX,
  211. cursorY,
  212. ...ret
  213. } = appState;
  214. return ret;
  215. };
  216. const prevAppState = getNecessaryObj(prev.appState);
  217. const nextAppState = getNecessaryObj(next.appState);
  218. const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
  219. return (
  220. prev.language === next.language &&
  221. prev.elements === next.elements &&
  222. keys.every((key) => prevAppState[key] === nextAppState[key])
  223. );
  224. },
  225. );