LayerUI.tsx 23 KB

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