2020-03-07 10:20:38 -05:00
|
|
|
import React from "react";
|
|
|
|
import { showSelectedShapeActions } from "../element";
|
|
|
|
import { calculateScrollCenter, getTargetElement } from "../scene";
|
|
|
|
import { exportCanvas } from "../data";
|
|
|
|
|
|
|
|
import { AppState } from "../types";
|
|
|
|
import { ExcalidrawElement } from "../element/types";
|
|
|
|
|
|
|
|
import { ActionManager } from "../actions/manager";
|
|
|
|
import { Island } from "./Island";
|
|
|
|
import Stack from "./Stack";
|
|
|
|
import { FixedSideContainer } from "./FixedSideContainer";
|
|
|
|
import { LockIcon } from "./LockIcon";
|
|
|
|
import { ExportDialog, ExportCB } from "./ExportDialog";
|
|
|
|
import { LanguageList } from "./LanguageList";
|
|
|
|
import { t, languages, setLanguage } from "../i18n";
|
|
|
|
import { HintViewer } from "./HintViewer";
|
|
|
|
import useIsMobile from "../is-mobile";
|
|
|
|
|
|
|
|
import { ExportType } from "../scene/types";
|
|
|
|
import { MobileMenu } from "./MobileMenu";
|
|
|
|
import { ZoomActions, SelectedShapeActions, ShapesSwitcher } from "./Actions";
|
|
|
|
import { Section } from "./Section";
|
2020-03-11 19:42:18 +01:00
|
|
|
import { RoomDialog } from "./RoomDialog";
|
2020-03-07 10:20:38 -05:00
|
|
|
|
|
|
|
interface LayerUIProps {
|
|
|
|
actionManager: ActionManager;
|
|
|
|
appState: AppState;
|
|
|
|
canvas: HTMLCanvasElement | null;
|
|
|
|
setAppState: any;
|
|
|
|
elements: readonly ExcalidrawElement[];
|
|
|
|
language: string;
|
|
|
|
setElements: (elements: readonly ExcalidrawElement[]) => void;
|
2020-03-11 19:42:18 +01:00
|
|
|
onRoomCreate: () => void;
|
|
|
|
onRoomDestroy: () => void;
|
2020-03-07 10:20:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
export const LayerUI = React.memo(
|
|
|
|
({
|
|
|
|
actionManager,
|
|
|
|
appState,
|
|
|
|
setAppState,
|
|
|
|
canvas,
|
|
|
|
elements,
|
|
|
|
language,
|
|
|
|
setElements,
|
2020-03-11 19:42:18 +01:00
|
|
|
onRoomCreate,
|
|
|
|
onRoomDestroy,
|
2020-03-07 10:20:38 -05:00
|
|
|
}: LayerUIProps) => {
|
|
|
|
const isMobile = useIsMobile();
|
|
|
|
|
|
|
|
function renderExportDialog() {
|
|
|
|
const createExporter = (type: ExportType): ExportCB => (
|
|
|
|
exportedElements,
|
|
|
|
scale,
|
|
|
|
) => {
|
|
|
|
if (canvas) {
|
2020-03-08 10:20:55 -07:00
|
|
|
exportCanvas(type, exportedElements, appState, canvas, {
|
2020-03-07 10:20:38 -05:00
|
|
|
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",
|
2020-03-08 10:20:55 -07:00
|
|
|
exportedElements,
|
|
|
|
{
|
|
|
|
...appState,
|
|
|
|
selectedElementIds: {},
|
|
|
|
},
|
2020-03-07 10:20:38 -05:00
|
|
|
canvas,
|
|
|
|
appState,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return isMobile ? (
|
|
|
|
<MobileMenu
|
|
|
|
appState={appState}
|
|
|
|
elements={elements}
|
|
|
|
setElements={setElements}
|
|
|
|
actionManager={actionManager}
|
|
|
|
exportButton={renderExportDialog()}
|
|
|
|
setAppState={setAppState}
|
2020-03-11 19:42:18 +01:00
|
|
|
onRoomCreate={onRoomCreate}
|
|
|
|
onRoomDestroy={onRoomDestroy}
|
2020-03-07 10:20:38 -05:00
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<FixedSideContainer side="top">
|
2020-03-08 10:20:55 -07:00
|
|
|
<HintViewer appState={appState} elements={elements} />
|
2020-03-07 10:20:38 -05:00
|
|
|
<div className="App-menu App-menu_top">
|
2020-03-11 19:42:18 +01:00
|
|
|
<Stack.Col gap={4}>
|
2020-03-07 10:20:38 -05:00
|
|
|
<Section className="App-right-menu" heading="canvasActions">
|
|
|
|
<Island padding={4}>
|
|
|
|
<Stack.Col gap={4}>
|
2020-03-11 22:48:27 +01:00
|
|
|
<Stack.Row gap={1} justifyContent={"space-between"}>
|
2020-03-07 10:20:38 -05:00
|
|
|
{actionManager.renderAction("loadScene")}
|
|
|
|
{actionManager.renderAction("saveScene")}
|
|
|
|
{renderExportDialog()}
|
|
|
|
{actionManager.renderAction("clearCanvas")}
|
2020-03-11 19:42:18 +01:00
|
|
|
<RoomDialog
|
|
|
|
isCollaborating={appState.isCollaborating}
|
2020-03-11 22:48:27 +01:00
|
|
|
collaboratorCount={appState.collaboratorCount}
|
2020-03-11 19:42:18 +01:00
|
|
|
onRoomCreate={onRoomCreate}
|
|
|
|
onRoomDestroy={onRoomDestroy}
|
|
|
|
/>
|
2020-03-07 10:20:38 -05:00
|
|
|
</Stack.Row>
|
|
|
|
{actionManager.renderAction("changeViewBackgroundColor")}
|
|
|
|
</Stack.Col>
|
|
|
|
</Island>
|
|
|
|
</Section>
|
|
|
|
{showSelectedShapeActions(appState, elements) && (
|
|
|
|
<Section
|
|
|
|
className="App-right-menu"
|
|
|
|
heading="selectedShapeActions"
|
|
|
|
>
|
|
|
|
<Island padding={4}>
|
|
|
|
<SelectedShapeActions
|
2020-03-08 10:20:55 -07:00
|
|
|
targetElements={getTargetElement(elements, appState)}
|
2020-03-07 10:20:38 -05:00
|
|
|
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}
|
|
|
|
setElements={setElements}
|
|
|
|
elements={elements}
|
|
|
|
/>
|
|
|
|
</Stack.Row>
|
|
|
|
</Island>
|
|
|
|
<LockIcon
|
|
|
|
checked={appState.elementLocked}
|
|
|
|
onChange={() => {
|
|
|
|
setAppState({
|
|
|
|
elementLocked: !appState.elementLocked,
|
|
|
|
elementType: appState.elementLocked
|
|
|
|
? "selection"
|
|
|
|
: appState.elementType,
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
title={t("toolBar.lock")}
|
|
|
|
isButton={isMobile}
|
|
|
|
/>
|
|
|
|
</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>
|
|
|
|
<footer role="contentinfo">
|
|
|
|
<LanguageList
|
|
|
|
onChange={lng => {
|
|
|
|
setLanguage(lng);
|
|
|
|
setAppState({});
|
|
|
|
}}
|
|
|
|
languages={languages}
|
|
|
|
currentLanguage={language}
|
|
|
|
floating
|
|
|
|
/>
|
|
|
|
{appState.scrolledOutside && (
|
|
|
|
<button
|
|
|
|
className="scroll-back-to-content"
|
|
|
|
onClick={() => {
|
|
|
|
setAppState({ ...calculateScrollCenter(elements) });
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{t("buttons.scrollBackToContent")}
|
|
|
|
</button>
|
|
|
|
)}
|
|
|
|
</footer>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
(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>)[];
|
|
|
|
|
|
|
|
return (
|
|
|
|
prev.language === next.language &&
|
|
|
|
prev.elements === next.elements &&
|
|
|
|
keys.every(key => prevAppState[key] === nextAppState[key])
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|