LayerUI.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767
  1. import clsx from "clsx";
  2. import React, {
  3. RefObject,
  4. useCallback,
  5. useEffect,
  6. useRef,
  7. useState,
  8. } from "react";
  9. import { ActionManager } from "../actions/manager";
  10. import { CLASSES } from "../constants";
  11. import { exportCanvas } from "../data";
  12. import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
  13. import { Library } from "../data/library";
  14. import { isTextElement, showSelectedShapeActions } from "../element";
  15. import { NonDeletedExcalidrawElement } from "../element/types";
  16. import { Language, t } from "../i18n";
  17. import { useIsMobile } from "../components/App";
  18. import { calculateScrollCenter, getSelectedElements } from "../scene";
  19. import { ExportType } from "../scene/types";
  20. import {
  21. AppProps,
  22. AppState,
  23. ExcalidrawProps,
  24. LibraryItem,
  25. LibraryItems,
  26. } from "../types";
  27. import { muteFSAbortError } from "../utils";
  28. import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
  29. import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
  30. import CollabButton from "./CollabButton";
  31. import { ErrorDialog } from "./ErrorDialog";
  32. import { ExportCB, ExportDialog } from "./ExportDialog";
  33. import { FixedSideContainer } from "./FixedSideContainer";
  34. import { GitHubCorner } from "./GitHubCorner";
  35. import { HintViewer } from "./HintViewer";
  36. import { exportFile, load, shield, trash } from "./icons";
  37. import { Island } from "./Island";
  38. import "./LayerUI.scss";
  39. import { LibraryUnit } from "./LibraryUnit";
  40. import { LoadingMessage } from "./LoadingMessage";
  41. import { LockIcon } from "./LockIcon";
  42. import { MobileMenu } from "./MobileMenu";
  43. import { PasteChartDialog } from "./PasteChartDialog";
  44. import { Section } from "./Section";
  45. import { HelpDialog } from "./HelpDialog";
  46. import Stack from "./Stack";
  47. import { ToolButton } from "./ToolButton";
  48. import { Tooltip } from "./Tooltip";
  49. import { UserList } from "./UserList";
  50. interface LayerUIProps {
  51. actionManager: ActionManager;
  52. appState: AppState;
  53. canvas: HTMLCanvasElement | null;
  54. setAppState: React.Component<any, AppState>["setState"];
  55. elements: readonly NonDeletedExcalidrawElement[];
  56. onCollabButtonClick?: () => void;
  57. onLockToggle: () => void;
  58. onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
  59. zenModeEnabled: boolean;
  60. showExitZenModeBtn: boolean;
  61. showThemeBtn: boolean;
  62. toggleZenMode: () => void;
  63. langCode: Language["code"];
  64. isCollaborating: boolean;
  65. onExportToBackend?: (
  66. exportedElements: readonly NonDeletedExcalidrawElement[],
  67. appState: AppState,
  68. canvas: HTMLCanvasElement | null,
  69. ) => void;
  70. renderCustomFooter?: (isMobile: boolean) => JSX.Element;
  71. viewModeEnabled: boolean;
  72. libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
  73. UIOptions: AppProps["UIOptions"];
  74. focusContainer: () => void;
  75. }
  76. const useOnClickOutside = (
  77. ref: RefObject<HTMLElement>,
  78. cb: (event: MouseEvent) => void,
  79. ) => {
  80. useEffect(() => {
  81. const listener = (event: MouseEvent) => {
  82. if (!ref.current) {
  83. return;
  84. }
  85. if (
  86. event.target instanceof Element &&
  87. (ref.current.contains(event.target) ||
  88. !document.body.contains(event.target))
  89. ) {
  90. return;
  91. }
  92. cb(event);
  93. };
  94. document.addEventListener("pointerdown", listener, false);
  95. return () => {
  96. document.removeEventListener("pointerdown", listener);
  97. };
  98. }, [ref, cb]);
  99. };
  100. const LibraryMenuItems = ({
  101. library,
  102. onRemoveFromLibrary,
  103. onAddToLibrary,
  104. onInsertShape,
  105. pendingElements,
  106. setAppState,
  107. setLibraryItems,
  108. libraryReturnUrl,
  109. focusContainer,
  110. }: {
  111. library: LibraryItems;
  112. pendingElements: LibraryItem;
  113. onRemoveFromLibrary: (index: number) => void;
  114. onInsertShape: (elements: LibraryItem) => void;
  115. onAddToLibrary: (elements: LibraryItem) => void;
  116. setAppState: React.Component<any, AppState>["setState"];
  117. setLibraryItems: (library: LibraryItems) => void;
  118. libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
  119. focusContainer: () => void;
  120. }) => {
  121. const isMobile = useIsMobile();
  122. const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
  123. const CELLS_PER_ROW = isMobile ? 4 : 6;
  124. const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
  125. const rows = [];
  126. let addedPendingElements = false;
  127. const referrer =
  128. libraryReturnUrl || window.location.origin + window.location.pathname;
  129. rows.push(
  130. <div className="layer-ui__library-header" key="library-header">
  131. <ToolButton
  132. key="import"
  133. type="button"
  134. title={t("buttons.load")}
  135. aria-label={t("buttons.load")}
  136. icon={load}
  137. onClick={() => {
  138. importLibraryFromJSON()
  139. .then(() => {
  140. // Close and then open to get the libraries updated
  141. setAppState({ isLibraryOpen: false });
  142. setAppState({ isLibraryOpen: true });
  143. })
  144. .catch(muteFSAbortError)
  145. .catch((error) => {
  146. setAppState({ errorMessage: error.message });
  147. });
  148. }}
  149. />
  150. {!!library.length && (
  151. <>
  152. <ToolButton
  153. key="export"
  154. type="button"
  155. title={t("buttons.export")}
  156. aria-label={t("buttons.export")}
  157. icon={exportFile}
  158. onClick={() => {
  159. saveLibraryAsJSON()
  160. .catch(muteFSAbortError)
  161. .catch((error) => {
  162. setAppState({ errorMessage: error.message });
  163. });
  164. }}
  165. />
  166. <ToolButton
  167. key="reset"
  168. type="button"
  169. title={t("buttons.resetLibrary")}
  170. aria-label={t("buttons.resetLibrary")}
  171. icon={trash}
  172. onClick={() => {
  173. if (window.confirm(t("alerts.resetLibrary"))) {
  174. Library.resetLibrary();
  175. setLibraryItems([]);
  176. focusContainer();
  177. }
  178. }}
  179. />
  180. </>
  181. )}
  182. <a
  183. href={`https://libraries.excalidraw.com?target=${
  184. window.name || "_blank"
  185. }&referrer=${referrer}&useHash=true&token=${Library.csrfToken}`}
  186. target="_excalidraw_libraries"
  187. >
  188. {t("labels.libraries")}
  189. </a>
  190. </div>,
  191. );
  192. for (let row = 0; row < numRows; row++) {
  193. const y = CELLS_PER_ROW * row;
  194. const children = [];
  195. for (let x = 0; x < CELLS_PER_ROW; x++) {
  196. const shouldAddPendingElements: boolean =
  197. pendingElements.length > 0 &&
  198. !addedPendingElements &&
  199. y + x >= library.length;
  200. addedPendingElements = addedPendingElements || shouldAddPendingElements;
  201. children.push(
  202. <Stack.Col key={x}>
  203. <LibraryUnit
  204. elements={library[y + x]}
  205. pendingElements={
  206. shouldAddPendingElements ? pendingElements : undefined
  207. }
  208. onRemoveFromLibrary={onRemoveFromLibrary.bind(null, y + x)}
  209. onClick={
  210. shouldAddPendingElements
  211. ? onAddToLibrary.bind(null, pendingElements)
  212. : onInsertShape.bind(null, library[y + x])
  213. }
  214. />
  215. </Stack.Col>,
  216. );
  217. }
  218. rows.push(
  219. <Stack.Row align="center" gap={1} key={row}>
  220. {children}
  221. </Stack.Row>,
  222. );
  223. }
  224. return (
  225. <Stack.Col align="start" gap={1} className="layer-ui__library-items">
  226. {rows}
  227. </Stack.Col>
  228. );
  229. };
  230. const LibraryMenu = ({
  231. onClickOutside,
  232. onInsertShape,
  233. pendingElements,
  234. onAddToLibrary,
  235. setAppState,
  236. libraryReturnUrl,
  237. focusContainer,
  238. }: {
  239. pendingElements: LibraryItem;
  240. onClickOutside: (event: MouseEvent) => void;
  241. onInsertShape: (elements: LibraryItem) => void;
  242. onAddToLibrary: () => void;
  243. setAppState: React.Component<any, AppState>["setState"];
  244. libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
  245. focusContainer: () => void;
  246. }) => {
  247. const ref = useRef<HTMLDivElement | null>(null);
  248. useOnClickOutside(ref, (event) => {
  249. // If click on the library icon, do nothing.
  250. if ((event.target as Element).closest(".ToolIcon_type_button__library")) {
  251. return;
  252. }
  253. onClickOutside(event);
  254. });
  255. const [libraryItems, setLibraryItems] = useState<LibraryItems>([]);
  256. const [loadingState, setIsLoading] = useState<
  257. "preloading" | "loading" | "ready"
  258. >("preloading");
  259. const loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
  260. useEffect(() => {
  261. Promise.race([
  262. new Promise((resolve) => {
  263. loadingTimerRef.current = setTimeout(() => {
  264. resolve("loading");
  265. }, 100);
  266. }),
  267. Library.loadLibrary().then((items) => {
  268. setLibraryItems(items);
  269. setIsLoading("ready");
  270. }),
  271. ]).then((data) => {
  272. if (data === "loading") {
  273. setIsLoading("loading");
  274. }
  275. });
  276. return () => {
  277. clearTimeout(loadingTimerRef.current!);
  278. };
  279. }, []);
  280. const removeFromLibrary = useCallback(async (indexToRemove) => {
  281. const items = await Library.loadLibrary();
  282. const nextItems = items.filter((_, index) => index !== indexToRemove);
  283. Library.saveLibrary(nextItems);
  284. setLibraryItems(nextItems);
  285. }, []);
  286. const addToLibrary = useCallback(
  287. async (elements: LibraryItem) => {
  288. const items = await Library.loadLibrary();
  289. const nextItems = [...items, elements];
  290. onAddToLibrary();
  291. Library.saveLibrary(nextItems);
  292. setLibraryItems(nextItems);
  293. },
  294. [onAddToLibrary],
  295. );
  296. return loadingState === "preloading" ? null : (
  297. <Island padding={1} ref={ref} className="layer-ui__library">
  298. {loadingState === "loading" ? (
  299. <div className="layer-ui__library-message">
  300. {t("labels.libraryLoadingMessage")}
  301. </div>
  302. ) : (
  303. <LibraryMenuItems
  304. library={libraryItems}
  305. onRemoveFromLibrary={removeFromLibrary}
  306. onAddToLibrary={addToLibrary}
  307. onInsertShape={onInsertShape}
  308. pendingElements={pendingElements}
  309. setAppState={setAppState}
  310. setLibraryItems={setLibraryItems}
  311. libraryReturnUrl={libraryReturnUrl}
  312. focusContainer={focusContainer}
  313. />
  314. )}
  315. </Island>
  316. );
  317. };
  318. const LayerUI = ({
  319. actionManager,
  320. appState,
  321. setAppState,
  322. canvas,
  323. elements,
  324. onCollabButtonClick,
  325. onLockToggle,
  326. onInsertElements,
  327. zenModeEnabled,
  328. showExitZenModeBtn,
  329. showThemeBtn,
  330. toggleZenMode,
  331. isCollaborating,
  332. onExportToBackend,
  333. renderCustomFooter,
  334. viewModeEnabled,
  335. libraryReturnUrl,
  336. UIOptions,
  337. focusContainer,
  338. }: LayerUIProps) => {
  339. const isMobile = useIsMobile();
  340. const renderEncryptedIcon = () => (
  341. <a
  342. className={clsx("encrypted-icon tooltip zen-mode-visibility", {
  343. "zen-mode-visibility--hidden": zenModeEnabled,
  344. })}
  345. href="https://blog.excalidraw.com/end-to-end-encryption/"
  346. target="_blank"
  347. rel="noopener noreferrer"
  348. aria-label={t("encrypted.link")}
  349. >
  350. <Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
  351. {shield}
  352. </Tooltip>
  353. </a>
  354. );
  355. const renderExportDialog = () => {
  356. if (!UIOptions.canvasActions.export) {
  357. return null;
  358. }
  359. const createExporter = (type: ExportType): ExportCB => async (
  360. exportedElements,
  361. scale,
  362. ) => {
  363. if (canvas) {
  364. await exportCanvas(type, exportedElements, appState, canvas, {
  365. exportBackground: appState.exportBackground,
  366. name: appState.name,
  367. viewBackgroundColor: appState.viewBackgroundColor,
  368. scale,
  369. shouldAddWatermark: appState.shouldAddWatermark,
  370. })
  371. .catch(muteFSAbortError)
  372. .catch((error) => {
  373. console.error(error);
  374. setAppState({ errorMessage: error.message });
  375. });
  376. }
  377. };
  378. return (
  379. <ExportDialog
  380. elements={elements}
  381. appState={appState}
  382. actionManager={actionManager}
  383. onExportToPng={createExporter("png")}
  384. onExportToSvg={createExporter("svg")}
  385. onExportToClipboard={createExporter("clipboard")}
  386. onExportToBackend={
  387. onExportToBackend
  388. ? (elements) => {
  389. onExportToBackend &&
  390. onExportToBackend(elements, appState, canvas);
  391. }
  392. : undefined
  393. }
  394. />
  395. );
  396. };
  397. const renderViewModeCanvasActions = () => {
  398. return (
  399. <Section
  400. heading="canvasActions"
  401. className={clsx("zen-mode-transition", {
  402. "transition-left": zenModeEnabled,
  403. })}
  404. >
  405. {/* the zIndex ensures this menu has higher stacking order,
  406. see https://github.com/excalidraw/excalidraw/pull/1445 */}
  407. <Island padding={2} style={{ zIndex: 1 }}>
  408. <Stack.Col gap={4}>
  409. <Stack.Row gap={1} justifyContent="space-between">
  410. {actionManager.renderAction("saveScene")}
  411. {actionManager.renderAction("saveAsScene")}
  412. {renderExportDialog()}
  413. </Stack.Row>
  414. </Stack.Col>
  415. </Island>
  416. </Section>
  417. );
  418. };
  419. const renderCanvasActions = () => (
  420. <Section
  421. heading="canvasActions"
  422. className={clsx("zen-mode-transition", {
  423. "transition-left": zenModeEnabled,
  424. })}
  425. >
  426. {/* the zIndex ensures this menu has higher stacking order,
  427. see https://github.com/excalidraw/excalidraw/pull/1445 */}
  428. <Island padding={2} style={{ zIndex: 1 }}>
  429. <Stack.Col gap={4}>
  430. <Stack.Row gap={1} justifyContent="space-between">
  431. {actionManager.renderAction("loadScene")}
  432. {actionManager.renderAction("saveScene")}
  433. {actionManager.renderAction("saveAsScene")}
  434. {renderExportDialog()}
  435. {actionManager.renderAction("clearCanvas")}
  436. {onCollabButtonClick && (
  437. <CollabButton
  438. isCollaborating={isCollaborating}
  439. collaboratorCount={appState.collaborators.size}
  440. onClick={onCollabButtonClick}
  441. />
  442. )}
  443. </Stack.Row>
  444. <BackgroundPickerAndDarkModeToggle
  445. actionManager={actionManager}
  446. appState={appState}
  447. setAppState={setAppState}
  448. showThemeBtn={showThemeBtn}
  449. />
  450. </Stack.Col>
  451. </Island>
  452. </Section>
  453. );
  454. const renderSelectedShapeActions = () => (
  455. <Section
  456. heading="selectedShapeActions"
  457. className={clsx("zen-mode-transition", {
  458. "transition-left": zenModeEnabled,
  459. })}
  460. >
  461. <Island
  462. className={CLASSES.SHAPE_ACTIONS_MENU}
  463. padding={2}
  464. style={{
  465. // we want to make sure this doesn't overflow so substracting 200
  466. // which is approximately height of zoom footer and top left menu items with some buffer
  467. maxHeight: `${appState.height - 200}px`,
  468. }}
  469. >
  470. <SelectedShapeActions
  471. appState={appState}
  472. elements={elements}
  473. renderAction={actionManager.renderAction}
  474. elementType={appState.elementType}
  475. />
  476. </Island>
  477. </Section>
  478. );
  479. const closeLibrary = useCallback(
  480. (event) => {
  481. setAppState({ isLibraryOpen: false });
  482. },
  483. [setAppState],
  484. );
  485. const deselectItems = useCallback(() => {
  486. setAppState({
  487. selectedElementIds: {},
  488. selectedGroupIds: {},
  489. });
  490. }, [setAppState]);
  491. const libraryMenu = appState.isLibraryOpen ? (
  492. <LibraryMenu
  493. pendingElements={getSelectedElements(elements, appState)}
  494. onClickOutside={closeLibrary}
  495. onInsertShape={onInsertElements}
  496. onAddToLibrary={deselectItems}
  497. setAppState={setAppState}
  498. libraryReturnUrl={libraryReturnUrl}
  499. focusContainer={focusContainer}
  500. />
  501. ) : null;
  502. const renderFixedSideContainer = () => {
  503. const shouldRenderSelectedShapeActions = showSelectedShapeActions(
  504. appState,
  505. elements,
  506. );
  507. return (
  508. <FixedSideContainer side="top">
  509. <div className="App-menu App-menu_top">
  510. <Stack.Col
  511. gap={4}
  512. className={clsx({ "disable-pointerEvents": zenModeEnabled })}
  513. >
  514. {viewModeEnabled
  515. ? renderViewModeCanvasActions()
  516. : renderCanvasActions()}
  517. {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
  518. </Stack.Col>
  519. {!viewModeEnabled && (
  520. <Section heading="shapes">
  521. {(heading) => (
  522. <Stack.Col gap={4} align="start">
  523. <Stack.Row gap={1}>
  524. <Island
  525. padding={1}
  526. className={clsx({ "zen-mode": zenModeEnabled })}
  527. >
  528. <HintViewer appState={appState} elements={elements} />
  529. {heading}
  530. <Stack.Row gap={1}>
  531. <ShapesSwitcher
  532. canvas={canvas}
  533. elementType={appState.elementType}
  534. setAppState={setAppState}
  535. isLibraryOpen={appState.isLibraryOpen}
  536. />
  537. </Stack.Row>
  538. </Island>
  539. <LockIcon
  540. zenModeEnabled={zenModeEnabled}
  541. checked={appState.elementLocked}
  542. onChange={onLockToggle}
  543. title={t("toolBar.lock")}
  544. />
  545. </Stack.Row>
  546. {libraryMenu}
  547. </Stack.Col>
  548. )}
  549. </Section>
  550. )}
  551. <UserList
  552. className={clsx("zen-mode-transition", {
  553. "transition-right": zenModeEnabled,
  554. })}
  555. >
  556. {appState.collaborators.size > 0 &&
  557. Array.from(appState.collaborators)
  558. // Collaborator is either not initialized or is actually the current user.
  559. .filter(([_, client]) => Object.keys(client).length !== 0)
  560. .map(([clientId, client]) => (
  561. <Tooltip
  562. label={client.username || "Unknown user"}
  563. key={clientId}
  564. >
  565. {actionManager.renderAction("goToCollaborator", clientId)}
  566. </Tooltip>
  567. ))}
  568. </UserList>
  569. </div>
  570. </FixedSideContainer>
  571. );
  572. };
  573. const renderBottomAppMenu = () => {
  574. return (
  575. <div
  576. className={clsx("App-menu App-menu_bottom zen-mode-transition", {
  577. "App-menu_bottom--transition-left": zenModeEnabled,
  578. })}
  579. >
  580. <Stack.Col gap={2}>
  581. <Section heading="canvasActions">
  582. <Island padding={1}>
  583. <ZoomActions
  584. renderAction={actionManager.renderAction}
  585. zoom={appState.zoom}
  586. />
  587. </Island>
  588. {renderEncryptedIcon()}
  589. </Section>
  590. </Stack.Col>
  591. </div>
  592. );
  593. };
  594. const renderGitHubCorner = () => {
  595. return (
  596. <aside
  597. className={clsx(
  598. "layer-ui__wrapper__github-corner zen-mode-transition",
  599. {
  600. "transition-right": zenModeEnabled,
  601. },
  602. )}
  603. >
  604. <GitHubCorner theme={appState.theme} />
  605. </aside>
  606. );
  607. };
  608. const renderFooter = () => (
  609. <footer role="contentinfo" className="layer-ui__wrapper__footer">
  610. <div
  611. className={clsx("zen-mode-transition", {
  612. "transition-right disable-pointerEvents": zenModeEnabled,
  613. })}
  614. >
  615. {renderCustomFooter?.(false)}
  616. {actionManager.renderAction("toggleShortcuts")}
  617. </div>
  618. <button
  619. className={clsx("disable-zen-mode", {
  620. "disable-zen-mode--visible": showExitZenModeBtn,
  621. })}
  622. onClick={toggleZenMode}
  623. >
  624. {t("buttons.exitZenMode")}
  625. </button>
  626. </footer>
  627. );
  628. const dialogs = (
  629. <>
  630. {appState.isLoading && <LoadingMessage />}
  631. {appState.errorMessage && (
  632. <ErrorDialog
  633. message={appState.errorMessage}
  634. onClose={() => setAppState({ errorMessage: null })}
  635. />
  636. )}
  637. {appState.showHelpDialog && (
  638. <HelpDialog
  639. onClose={() => {
  640. const helpIcon = document.querySelector(
  641. ".help-icon",
  642. )! as HTMLElement;
  643. helpIcon.focus();
  644. setAppState({ showHelpDialog: false });
  645. }}
  646. />
  647. )}
  648. {appState.pasteDialog.shown && (
  649. <PasteChartDialog
  650. setAppState={setAppState}
  651. appState={appState}
  652. onInsertChart={onInsertElements}
  653. onClose={() =>
  654. setAppState({
  655. pasteDialog: { shown: false, data: null },
  656. })
  657. }
  658. />
  659. )}
  660. </>
  661. );
  662. return isMobile ? (
  663. <>
  664. {dialogs}
  665. <MobileMenu
  666. appState={appState}
  667. elements={elements}
  668. actionManager={actionManager}
  669. libraryMenu={libraryMenu}
  670. exportButton={renderExportDialog()}
  671. setAppState={setAppState}
  672. onCollabButtonClick={onCollabButtonClick}
  673. onLockToggle={onLockToggle}
  674. canvas={canvas}
  675. isCollaborating={isCollaborating}
  676. renderCustomFooter={renderCustomFooter}
  677. viewModeEnabled={viewModeEnabled}
  678. showThemeBtn={showThemeBtn}
  679. />
  680. </>
  681. ) : (
  682. <div
  683. className={clsx("layer-ui__wrapper", {
  684. "disable-pointerEvents":
  685. appState.draggingElement ||
  686. appState.resizingElement ||
  687. (appState.editingElement && !isTextElement(appState.editingElement)),
  688. })}
  689. >
  690. {dialogs}
  691. {renderFixedSideContainer()}
  692. {renderBottomAppMenu()}
  693. {renderGitHubCorner()}
  694. {renderFooter()}
  695. {appState.scrolledOutside && (
  696. <button
  697. className="scroll-back-to-content"
  698. onClick={() => {
  699. setAppState({
  700. ...calculateScrollCenter(elements, appState, canvas),
  701. });
  702. }}
  703. >
  704. {t("buttons.scrollBackToContent")}
  705. </button>
  706. )}
  707. </div>
  708. );
  709. };
  710. const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
  711. const getNecessaryObj = (appState: AppState): Partial<AppState> => {
  712. const {
  713. suggestedBindings,
  714. startBoundElement: boundElement,
  715. ...ret
  716. } = appState;
  717. return ret;
  718. };
  719. const prevAppState = getNecessaryObj(prev.appState);
  720. const nextAppState = getNecessaryObj(next.appState);
  721. const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
  722. return (
  723. prev.renderCustomFooter === next.renderCustomFooter &&
  724. prev.langCode === next.langCode &&
  725. prev.elements === next.elements &&
  726. keys.every((key) => prevAppState[key] === nextAppState[key])
  727. );
  728. };
  729. export default React.memo(LayerUI, areEqual);