LayerUI.tsx 22 KB

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