|
@@ -40,212 +40,233 @@ interface LayerUIProps {
|
|
|
onLockToggle: () => void;
|
|
|
}
|
|
|
|
|
|
-export const LayerUI = React.memo(
|
|
|
- ({
|
|
|
- actionManager,
|
|
|
- appState,
|
|
|
- setAppState,
|
|
|
- canvas,
|
|
|
- elements,
|
|
|
- onRoomCreate,
|
|
|
- onUsernameChange,
|
|
|
- onRoomDestroy,
|
|
|
- onLockToggle,
|
|
|
- }: LayerUIProps) => {
|
|
|
- const isMobile = useIsMobile();
|
|
|
+const LayerUI = ({
|
|
|
+ actionManager,
|
|
|
+ appState,
|
|
|
+ setAppState,
|
|
|
+ canvas,
|
|
|
+ elements,
|
|
|
+ onRoomCreate,
|
|
|
+ onUsernameChange,
|
|
|
+ onRoomDestroy,
|
|
|
+ onLockToggle,
|
|
|
+}: LayerUIProps) => {
|
|
|
+ const isMobile = useIsMobile();
|
|
|
|
|
|
- function renderExportDialog() {
|
|
|
- const createExporter = (type: ExportType): ExportCB => (
|
|
|
- exportedElements,
|
|
|
- scale,
|
|
|
- ) => {
|
|
|
- if (canvas) {
|
|
|
- exportCanvas(type, exportedElements, appState, canvas, {
|
|
|
- exportBackground: appState.exportBackground,
|
|
|
- name: appState.name,
|
|
|
- viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
- scale,
|
|
|
- });
|
|
|
- }
|
|
|
- };
|
|
|
- return (
|
|
|
- <ExportDialog
|
|
|
- elements={elements}
|
|
|
- appState={appState}
|
|
|
- actionManager={actionManager}
|
|
|
- onExportToPng={createExporter("png")}
|
|
|
- onExportToSvg={createExporter("svg")}
|
|
|
- onExportToClipboard={createExporter("clipboard")}
|
|
|
- onExportToBackend={(exportedElements) => {
|
|
|
- if (canvas) {
|
|
|
- exportCanvas(
|
|
|
- "backend",
|
|
|
- exportedElements,
|
|
|
- {
|
|
|
- ...appState,
|
|
|
- selectedElementIds: {},
|
|
|
- },
|
|
|
- canvas,
|
|
|
- appState,
|
|
|
- );
|
|
|
- }
|
|
|
- }}
|
|
|
- />
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
- return isMobile ? (
|
|
|
- <MobileMenu
|
|
|
- appState={appState}
|
|
|
+ const renderExportDialog = () => {
|
|
|
+ const createExporter = (type: ExportType): ExportCB => (
|
|
|
+ exportedElements,
|
|
|
+ scale,
|
|
|
+ ) => {
|
|
|
+ if (canvas) {
|
|
|
+ exportCanvas(type, exportedElements, appState, canvas, {
|
|
|
+ exportBackground: appState.exportBackground,
|
|
|
+ name: appState.name,
|
|
|
+ viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
+ scale,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ return (
|
|
|
+ <ExportDialog
|
|
|
elements={elements}
|
|
|
+ appState={appState}
|
|
|
actionManager={actionManager}
|
|
|
- exportButton={renderExportDialog()}
|
|
|
- setAppState={setAppState}
|
|
|
- onUsernameChange={onUsernameChange}
|
|
|
- onRoomCreate={onRoomCreate}
|
|
|
- onRoomDestroy={onRoomDestroy}
|
|
|
- onLockToggle={onLockToggle}
|
|
|
+ onExportToPng={createExporter("png")}
|
|
|
+ onExportToSvg={createExporter("svg")}
|
|
|
+ onExportToClipboard={createExporter("clipboard")}
|
|
|
+ onExportToBackend={(exportedElements) => {
|
|
|
+ if (canvas) {
|
|
|
+ exportCanvas(
|
|
|
+ "backend",
|
|
|
+ exportedElements,
|
|
|
+ {
|
|
|
+ ...appState,
|
|
|
+ selectedElementIds: {},
|
|
|
+ },
|
|
|
+ canvas,
|
|
|
+ appState,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }}
|
|
|
/>
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- {appState.isLoading && <LoadingMessage />}
|
|
|
- {appState.errorMessage && (
|
|
|
- <ErrorDialog
|
|
|
- message={appState.errorMessage}
|
|
|
- onClose={() => setAppState({ errorMessage: null })}
|
|
|
- />
|
|
|
- )}
|
|
|
- {appState.showShortcutsDialog && (
|
|
|
- <ShortcutsDialog
|
|
|
- onClose={() => setAppState({ showShortcutsDialog: null })}
|
|
|
- />
|
|
|
- )}
|
|
|
- <FixedSideContainer side="top">
|
|
|
- <HintViewer appState={appState} elements={elements} />
|
|
|
- <div className="App-menu App-menu_top">
|
|
|
- <Stack.Col gap={4}>
|
|
|
- <Section heading="canvasActions">
|
|
|
- {/* the zIndex ensures this menu has higher stacking order,
|
|
|
- see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
|
|
- <Island padding={4} style={{ zIndex: 1 }}>
|
|
|
- <Stack.Col gap={4}>
|
|
|
- <Stack.Row gap={1} justifyContent={"space-between"}>
|
|
|
- {actionManager.renderAction("loadScene")}
|
|
|
- {actionManager.renderAction("saveScene")}
|
|
|
- {renderExportDialog()}
|
|
|
- {actionManager.renderAction("clearCanvas")}
|
|
|
- <RoomDialog
|
|
|
- isCollaborating={appState.isCollaborating}
|
|
|
- collaboratorCount={appState.collaborators.size}
|
|
|
- username={appState.username}
|
|
|
- onUsernameChange={onUsernameChange}
|
|
|
- onRoomCreate={onRoomCreate}
|
|
|
- onRoomDestroy={onRoomDestroy}
|
|
|
+ );
|
|
|
+ };
|
|
|
+
|
|
|
+ const renderCanvasActions = () => (
|
|
|
+ <Section heading="canvasActions">
|
|
|
+ {/* the zIndex ensures this menu has higher stacking order,
|
|
|
+ see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
|
|
+ <Island padding={4} style={{ zIndex: 1 }}>
|
|
|
+ <Stack.Col gap={4}>
|
|
|
+ <Stack.Row gap={1} justifyContent="space-between">
|
|
|
+ {actionManager.renderAction("loadScene")}
|
|
|
+ {actionManager.renderAction("saveScene")}
|
|
|
+ {renderExportDialog()}
|
|
|
+ {actionManager.renderAction("clearCanvas")}
|
|
|
+ <RoomDialog
|
|
|
+ isCollaborating={appState.isCollaborating}
|
|
|
+ collaboratorCount={appState.collaborators.size}
|
|
|
+ username={appState.username}
|
|
|
+ onUsernameChange={onUsernameChange}
|
|
|
+ onRoomCreate={onRoomCreate}
|
|
|
+ onRoomDestroy={onRoomDestroy}
|
|
|
+ />
|
|
|
+ </Stack.Row>
|
|
|
+ {actionManager.renderAction("changeViewBackgroundColor")}
|
|
|
+ </Stack.Col>
|
|
|
+ </Island>
|
|
|
+ </Section>
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderSelectedShapeActions = () => (
|
|
|
+ <Section heading="selectedShapeActions">
|
|
|
+ <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={4}>
|
|
|
+ <SelectedShapeActions
|
|
|
+ appState={appState}
|
|
|
+ elements={elements}
|
|
|
+ renderAction={actionManager.renderAction}
|
|
|
+ elementType={appState.elementType}
|
|
|
+ />
|
|
|
+ </Island>
|
|
|
+ </Section>
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderFixedSideContainer = () => {
|
|
|
+ const shouldRenderSelectedShapeActions = showSelectedShapeActions(
|
|
|
+ appState,
|
|
|
+ elements,
|
|
|
+ );
|
|
|
+ return (
|
|
|
+ <FixedSideContainer side="top">
|
|
|
+ <HintViewer appState={appState} elements={elements} />
|
|
|
+ <div className="App-menu App-menu_top">
|
|
|
+ <Stack.Col gap={4}>
|
|
|
+ {renderCanvasActions()}
|
|
|
+ {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
|
|
+ </Stack.Col>
|
|
|
+ <Section heading="shapes">
|
|
|
+ {(heading) => (
|
|
|
+ <Stack.Col gap={4} align="start">
|
|
|
+ <Stack.Row gap={1}>
|
|
|
+ <Island padding={1}>
|
|
|
+ {heading}
|
|
|
+ <Stack.Row gap={1}>
|
|
|
+ <ShapesSwitcher
|
|
|
+ elementType={appState.elementType}
|
|
|
+ setAppState={setAppState}
|
|
|
/>
|
|
|
</Stack.Row>
|
|
|
- {actionManager.renderAction("changeViewBackgroundColor")}
|
|
|
- </Stack.Col>
|
|
|
- </Island>
|
|
|
- </Section>
|
|
|
- {showSelectedShapeActions(appState, elements) && (
|
|
|
- <Section heading="selectedShapeActions">
|
|
|
- <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={4}>
|
|
|
- <SelectedShapeActions
|
|
|
- appState={appState}
|
|
|
- elements={elements}
|
|
|
- renderAction={actionManager.renderAction}
|
|
|
- elementType={appState.elementType}
|
|
|
- />
|
|
|
</Island>
|
|
|
- </Section>
|
|
|
- )}
|
|
|
- </Stack.Col>
|
|
|
- <Section heading="shapes">
|
|
|
- {(heading) => (
|
|
|
- <Stack.Col gap={4} align="start">
|
|
|
- <Stack.Row gap={1}>
|
|
|
- <Island padding={1}>
|
|
|
- {heading}
|
|
|
- <Stack.Row gap={1}>
|
|
|
- <ShapesSwitcher
|
|
|
- elementType={appState.elementType}
|
|
|
- setAppState={setAppState}
|
|
|
- />
|
|
|
- </Stack.Row>
|
|
|
- </Island>
|
|
|
- <LockIcon
|
|
|
- checked={appState.elementLocked}
|
|
|
- onChange={onLockToggle}
|
|
|
- title={t("toolBar.lock")}
|
|
|
- />
|
|
|
- </Stack.Row>
|
|
|
- </Stack.Col>
|
|
|
- )}
|
|
|
- </Section>
|
|
|
- <div />
|
|
|
- </div>
|
|
|
- <div className="App-menu App-menu_bottom">
|
|
|
- <Stack.Col gap={2}>
|
|
|
- <Section heading="canvasActions">
|
|
|
- <Island padding={1}>
|
|
|
- <ZoomActions
|
|
|
- renderAction={actionManager.renderAction}
|
|
|
- zoom={appState.zoom}
|
|
|
+ <LockIcon
|
|
|
+ checked={appState.elementLocked}
|
|
|
+ onChange={onLockToggle}
|
|
|
+ title={t("toolBar.lock")}
|
|
|
/>
|
|
|
- </Island>
|
|
|
- </Section>
|
|
|
- </Stack.Col>
|
|
|
- </div>
|
|
|
- </FixedSideContainer>
|
|
|
- <aside>
|
|
|
- <GitHubCorner />
|
|
|
- </aside>
|
|
|
- <footer role="contentinfo">
|
|
|
- <LanguageList
|
|
|
- onChange={(lng) => {
|
|
|
- setLanguage(lng);
|
|
|
- setAppState({});
|
|
|
- }}
|
|
|
- languages={languages}
|
|
|
- floating
|
|
|
- />
|
|
|
- {actionManager.renderAction("toggleShortcuts")}
|
|
|
- {appState.scrolledOutside && (
|
|
|
- <button
|
|
|
- className="scroll-back-to-content"
|
|
|
- onClick={() => {
|
|
|
- setAppState({ ...calculateScrollCenter(elements) });
|
|
|
- }}
|
|
|
- >
|
|
|
- {t("buttons.scrollBackToContent")}
|
|
|
- </button>
|
|
|
- )}
|
|
|
- </footer>
|
|
|
- </>
|
|
|
+ </Stack.Row>
|
|
|
+ </Stack.Col>
|
|
|
+ )}
|
|
|
+ </Section>
|
|
|
+ <div />
|
|
|
+ </div>
|
|
|
+ <div className="App-menu App-menu_bottom">
|
|
|
+ <Stack.Col gap={2}>
|
|
|
+ <Section heading="canvasActions">
|
|
|
+ <Island padding={1}>
|
|
|
+ <ZoomActions
|
|
|
+ renderAction={actionManager.renderAction}
|
|
|
+ zoom={appState.zoom}
|
|
|
+ />
|
|
|
+ </Island>
|
|
|
+ </Section>
|
|
|
+ </Stack.Col>
|
|
|
+ </div>
|
|
|
+ </FixedSideContainer>
|
|
|
);
|
|
|
- },
|
|
|
- (prev, next) => {
|
|
|
- const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
|
|
- const {
|
|
|
- draggingElement,
|
|
|
- resizingElement,
|
|
|
- multiElement,
|
|
|
- editingElement,
|
|
|
- isResizing,
|
|
|
- cursorX,
|
|
|
- cursorY,
|
|
|
- ...ret
|
|
|
- } = appState;
|
|
|
- return ret;
|
|
|
- };
|
|
|
- const prevAppState = getNecessaryObj(prev.appState);
|
|
|
- const nextAppState = getNecessaryObj(next.appState);
|
|
|
+ };
|
|
|
|
|
|
- const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
|
+ const renderFooter = () => (
|
|
|
+ <footer role="contentinfo">
|
|
|
+ <LanguageList
|
|
|
+ onChange={(lng) => {
|
|
|
+ setLanguage(lng);
|
|
|
+ setAppState({});
|
|
|
+ }}
|
|
|
+ languages={languages}
|
|
|
+ floating
|
|
|
+ />
|
|
|
+ {actionManager.renderAction("toggleShortcuts")}
|
|
|
+ {appState.scrolledOutside && (
|
|
|
+ <button
|
|
|
+ className="scroll-back-to-content"
|
|
|
+ onClick={() => {
|
|
|
+ setAppState({ ...calculateScrollCenter(elements) });
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {t("buttons.scrollBackToContent")}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </footer>
|
|
|
+ );
|
|
|
|
|
|
- return (
|
|
|
- prev.elements === next.elements &&
|
|
|
- keys.every((key) => prevAppState[key] === nextAppState[key])
|
|
|
- );
|
|
|
- },
|
|
|
-);
|
|
|
+ return isMobile ? (
|
|
|
+ <MobileMenu
|
|
|
+ appState={appState}
|
|
|
+ elements={elements}
|
|
|
+ actionManager={actionManager}
|
|
|
+ exportButton={renderExportDialog()}
|
|
|
+ setAppState={setAppState}
|
|
|
+ onUsernameChange={onUsernameChange}
|
|
|
+ onRoomCreate={onRoomCreate}
|
|
|
+ onRoomDestroy={onRoomDestroy}
|
|
|
+ onLockToggle={onLockToggle}
|
|
|
+ />
|
|
|
+ ) : (
|
|
|
+ <>
|
|
|
+ {appState.isLoading && <LoadingMessage />}
|
|
|
+ {appState.errorMessage && (
|
|
|
+ <ErrorDialog
|
|
|
+ message={appState.errorMessage}
|
|
|
+ onClose={() => setAppState({ errorMessage: null })}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {appState.showShortcutsDialog && (
|
|
|
+ <ShortcutsDialog
|
|
|
+ onClose={() => setAppState({ showShortcutsDialog: null })}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ {renderFixedSideContainer()}
|
|
|
+ <aside>
|
|
|
+ <GitHubCorner />
|
|
|
+ </aside>
|
|
|
+ {renderFooter()}
|
|
|
+ </>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
|
|
|
+ const getNecessaryObj = (appState: AppState): Partial<AppState> => {
|
|
|
+ const {
|
|
|
+ draggingElement,
|
|
|
+ resizingElement,
|
|
|
+ multiElement,
|
|
|
+ editingElement,
|
|
|
+ isResizing,
|
|
|
+ cursorX,
|
|
|
+ cursorY,
|
|
|
+ ...ret
|
|
|
+ } = appState;
|
|
|
+ return ret;
|
|
|
+ };
|
|
|
+ const prevAppState = getNecessaryObj(prev.appState);
|
|
|
+ const nextAppState = getNecessaryObj(next.appState);
|
|
|
+
|
|
|
+ const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
|
+
|
|
|
+ return (
|
|
|
+ prev.elements === next.elements &&
|
|
|
+ keys.every((key) => prevAppState[key] === nextAppState[key])
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default React.memo(LayerUI, areEqual);
|