|
@@ -23,7 +23,6 @@ import {
|
|
|
deleteSelectedElements,
|
|
|
getElementsWithinSelection,
|
|
|
isOverScrollBars,
|
|
|
- restoreFromLocalStorage,
|
|
|
saveToLocalStorage,
|
|
|
getElementAtPosition,
|
|
|
createScene,
|
|
@@ -32,9 +31,8 @@ import {
|
|
|
hasStroke,
|
|
|
hasText,
|
|
|
exportCanvas,
|
|
|
- importFromBackend,
|
|
|
- addToLoadedScenes,
|
|
|
loadedScenes,
|
|
|
+ loadScene,
|
|
|
calculateScrollCenter,
|
|
|
loadFromBlob,
|
|
|
} from "./scene";
|
|
@@ -163,6 +161,7 @@ interface LayerUIProps {
|
|
|
canvas: HTMLCanvasElement | null;
|
|
|
setAppState: any;
|
|
|
elements: readonly ExcalidrawElement[];
|
|
|
+ language: string;
|
|
|
setElements: (elements: readonly ExcalidrawElement[]) => void;
|
|
|
}
|
|
|
|
|
@@ -173,6 +172,7 @@ const LayerUI = React.memo(
|
|
|
setAppState,
|
|
|
canvas,
|
|
|
elements,
|
|
|
+ language,
|
|
|
setElements,
|
|
|
}: LayerUIProps) => {
|
|
|
function renderCanvasActions() {
|
|
@@ -318,56 +318,101 @@ const LayerUI = React.memo(
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+ function renderIdsDropdown() {
|
|
|
+ const scenes = loadedScenes();
|
|
|
+ if (scenes.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ <StoredScenesList
|
|
|
+ scenes={scenes}
|
|
|
+ currentId={appState.selectedId}
|
|
|
+ onChange={async (id, k) =>
|
|
|
+ actionManager.updater(await loadScene(id, k))
|
|
|
+ }
|
|
|
+ />
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
return (
|
|
|
- <FixedSideContainer side="top">
|
|
|
- <div className="App-menu App-menu_top">
|
|
|
- <Stack.Col gap={4} align="end">
|
|
|
- <section
|
|
|
- className="App-right-menu"
|
|
|
- aria-labelledby="canvas-actions-title"
|
|
|
- >
|
|
|
- <h2 className="visually-hidden" id="canvas-actions-title">
|
|
|
- {t("headings.canvasActions")}
|
|
|
- </h2>
|
|
|
- <Island padding={4}>{renderCanvasActions()}</Island>
|
|
|
+ <>
|
|
|
+ <FixedSideContainer side="top">
|
|
|
+ <div className="App-menu App-menu_top">
|
|
|
+ <Stack.Col gap={4} align="end">
|
|
|
+ <section
|
|
|
+ className="App-right-menu"
|
|
|
+ aria-labelledby="canvas-actions-title"
|
|
|
+ >
|
|
|
+ <h2 className="visually-hidden" id="canvas-actions-title">
|
|
|
+ {t("headings.canvasActions")}
|
|
|
+ </h2>
|
|
|
+ <Island padding={4}>{renderCanvasActions()}</Island>
|
|
|
+ </section>
|
|
|
+ <section
|
|
|
+ className="App-right-menu"
|
|
|
+ aria-labelledby="selected-shape-title"
|
|
|
+ >
|
|
|
+ <h2 className="visually-hidden" id="selected-shape-title">
|
|
|
+ {t("headings.selectedShapeActions")}
|
|
|
+ </h2>
|
|
|
+ {renderSelectedShapeActions(elements)}
|
|
|
+ </section>
|
|
|
+ </Stack.Col>
|
|
|
+ <section aria-labelledby="shapes-title">
|
|
|
+ <Stack.Col gap={4} align="start">
|
|
|
+ <Stack.Row gap={1}>
|
|
|
+ <Island padding={1}>
|
|
|
+ <h2 className="visually-hidden" id="shapes-title">
|
|
|
+ {t("headings.shapes")}
|
|
|
+ </h2>
|
|
|
+ <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
|
|
+ </Island>
|
|
|
+ <LockIcon
|
|
|
+ checked={appState.elementLocked}
|
|
|
+ onChange={() => {
|
|
|
+ setAppState({
|
|
|
+ elementLocked: !appState.elementLocked,
|
|
|
+ elementType: appState.elementLocked
|
|
|
+ ? "selection"
|
|
|
+ : appState.elementType,
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ title={t("toolBar.lock")}
|
|
|
+ />
|
|
|
+ </Stack.Row>
|
|
|
+ </Stack.Col>
|
|
|
</section>
|
|
|
- <section
|
|
|
- className="App-right-menu"
|
|
|
- aria-labelledby="selected-shape-title"
|
|
|
+ <div />
|
|
|
+ </div>
|
|
|
+ </FixedSideContainer>
|
|
|
+ <footer role="contentinfo">
|
|
|
+ <HintViewer
|
|
|
+ elementType={appState.elementType}
|
|
|
+ multiMode={appState.multiElement !== null}
|
|
|
+ isResizing={appState.isResizing}
|
|
|
+ elements={elements}
|
|
|
+ />
|
|
|
+ <LanguageList
|
|
|
+ onChange={lng => {
|
|
|
+ setLanguage(lng);
|
|
|
+ setAppState({});
|
|
|
+ }}
|
|
|
+ languages={languages}
|
|
|
+ currentLanguage={language}
|
|
|
+ />
|
|
|
+ {renderIdsDropdown()}
|
|
|
+ {appState.scrolledOutside && (
|
|
|
+ <button
|
|
|
+ className="scroll-back-to-content"
|
|
|
+ onClick={() => {
|
|
|
+ setAppState({ ...calculateScrollCenter(elements) });
|
|
|
+ }}
|
|
|
>
|
|
|
- <h2 className="visually-hidden" id="selected-shape-title">
|
|
|
- {t("headings.selectedShapeActions")}
|
|
|
- </h2>
|
|
|
- {renderSelectedShapeActions(elements)}
|
|
|
- </section>
|
|
|
- </Stack.Col>
|
|
|
- <section aria-labelledby="shapes-title">
|
|
|
- <Stack.Col gap={4} align="start">
|
|
|
- <Stack.Row gap={1}>
|
|
|
- <Island padding={1}>
|
|
|
- <h2 className="visually-hidden" id="shapes-title">
|
|
|
- {t("headings.shapes")}
|
|
|
- </h2>
|
|
|
- <Stack.Row gap={1}>{renderShapesSwitcher()}</Stack.Row>
|
|
|
- </Island>
|
|
|
- <LockIcon
|
|
|
- checked={appState.elementLocked}
|
|
|
- onChange={() => {
|
|
|
- setAppState({
|
|
|
- elementLocked: !appState.elementLocked,
|
|
|
- elementType: appState.elementLocked
|
|
|
- ? "selection"
|
|
|
- : appState.elementType,
|
|
|
- });
|
|
|
- }}
|
|
|
- title={t("toolBar.lock")}
|
|
|
- />
|
|
|
- </Stack.Row>
|
|
|
- </Stack.Col>
|
|
|
- </section>
|
|
|
- <div />
|
|
|
- </div>
|
|
|
- </FixedSideContainer>
|
|
|
+ {t("buttons.scrollBackToContent")}
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </footer>
|
|
|
+ </>
|
|
|
);
|
|
|
},
|
|
|
(prev, next) => {
|
|
@@ -390,6 +435,7 @@ const LayerUI = React.memo(
|
|
|
const keys = Object.keys(prevAppState) as (keyof Partial<AppState>)[];
|
|
|
|
|
|
return (
|
|
|
+ prev.language === next.language &&
|
|
|
prev.elements === next.elements &&
|
|
|
keys.every(k => prevAppState[k] === nextAppState[k])
|
|
|
);
|
|
@@ -406,9 +452,6 @@ export class App extends React.Component<any, AppState> {
|
|
|
super(props);
|
|
|
this.actionManager = new ActionManager(
|
|
|
this.syncActionResult,
|
|
|
- () => {
|
|
|
- history.resumeRecording();
|
|
|
- },
|
|
|
() => this.state,
|
|
|
() => elements,
|
|
|
);
|
|
@@ -443,13 +486,22 @@ export class App extends React.Component<any, AppState> {
|
|
|
this.canvasOnlyActions = [actionSelectAll];
|
|
|
}
|
|
|
|
|
|
- private syncActionResult = (res: ActionResult) => {
|
|
|
- if (res.elements !== undefined) {
|
|
|
+ private syncActionResult = (
|
|
|
+ res: ActionResult,
|
|
|
+ commitToHistory: boolean = true,
|
|
|
+ ) => {
|
|
|
+ if (res.elements) {
|
|
|
elements = res.elements;
|
|
|
+ if (commitToHistory) {
|
|
|
+ history.resumeRecording();
|
|
|
+ }
|
|
|
this.setState({});
|
|
|
}
|
|
|
|
|
|
- if (res.appState !== undefined) {
|
|
|
+ if (res.appState) {
|
|
|
+ if (commitToHistory) {
|
|
|
+ history.resumeRecording();
|
|
|
+ }
|
|
|
this.setState({ ...res.appState });
|
|
|
}
|
|
|
};
|
|
@@ -478,32 +530,6 @@ export class App extends React.Component<any, AppState> {
|
|
|
this.saveDebounced.flush();
|
|
|
};
|
|
|
|
|
|
- private async loadScene(id: string | null, k: string | undefined) {
|
|
|
- let data;
|
|
|
- let selectedId;
|
|
|
- if (id != null) {
|
|
|
- // k is the private key used to decrypt the content from the server, take
|
|
|
- // extra care not to leak it
|
|
|
- data = await importFromBackend(id, k);
|
|
|
- addToLoadedScenes(id, k);
|
|
|
- selectedId = id;
|
|
|
- window.history.replaceState({}, "Excalidraw", window.location.origin);
|
|
|
- } else {
|
|
|
- data = restoreFromLocalStorage();
|
|
|
- }
|
|
|
-
|
|
|
- if (data.elements) {
|
|
|
- elements = data.elements;
|
|
|
- }
|
|
|
-
|
|
|
- if (data.appState) {
|
|
|
- history.resumeRecording();
|
|
|
- this.setState({ ...data.appState, selectedId });
|
|
|
- } else {
|
|
|
- this.setState({});
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
public async componentDidMount() {
|
|
|
document.addEventListener("copy", this.onCopy);
|
|
|
document.addEventListener("paste", this.pasteFromClipboard);
|
|
@@ -523,15 +549,15 @@ export class App extends React.Component<any, AppState> {
|
|
|
|
|
|
if (id) {
|
|
|
// Backwards compatibility with legacy url format
|
|
|
- this.loadScene(id, undefined);
|
|
|
+ this.syncActionResult(await loadScene(id));
|
|
|
} else {
|
|
|
const match = window.location.hash.match(
|
|
|
/^#json=([0-9]+),([a-zA-Z0-9_-]+)$/,
|
|
|
);
|
|
|
if (match) {
|
|
|
- this.loadScene(match[1], match[2]);
|
|
|
+ this.syncActionResult(await loadScene(match[1], match[2]));
|
|
|
} else {
|
|
|
- this.loadScene(null, undefined);
|
|
|
+ this.syncActionResult(await loadScene(null));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -572,13 +598,8 @@ export class App extends React.Component<any, AppState> {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- const actionResult = this.actionManager.handleKeyDown(event);
|
|
|
-
|
|
|
- if (actionResult) {
|
|
|
- this.syncActionResult(actionResult);
|
|
|
- if (actionResult) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ if (this.actionManager.handleKeyDown(event)) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
const shape = findShapeByKey(event.key);
|
|
@@ -750,6 +771,7 @@ export class App extends React.Component<any, AppState> {
|
|
|
actionManager={this.actionManager}
|
|
|
elements={elements}
|
|
|
setElements={this.setElements}
|
|
|
+ language={getLanguage()}
|
|
|
/>
|
|
|
<main>
|
|
|
<canvas
|
|
@@ -1797,10 +1819,7 @@ export class App extends React.Component<any, AppState> {
|
|
|
if (file?.type === "application/json") {
|
|
|
loadFromBlob(file)
|
|
|
.then(({ elements, appState }) =>
|
|
|
- this.syncActionResult({
|
|
|
- elements,
|
|
|
- appState,
|
|
|
- } as ActionResult),
|
|
|
+ this.syncActionResult({ elements, appState }),
|
|
|
)
|
|
|
.catch(err => console.error(err));
|
|
|
}
|
|
@@ -1809,52 +1828,10 @@ export class App extends React.Component<any, AppState> {
|
|
|
{t("labels.drawingCanvas")}
|
|
|
</canvas>
|
|
|
</main>
|
|
|
- <footer role="contentinfo">
|
|
|
- <HintViewer
|
|
|
- elementType={this.state.elementType}
|
|
|
- multiMode={this.state.multiElement !== null}
|
|
|
- isResizing={this.state.isResizing}
|
|
|
- elements={elements}
|
|
|
- />
|
|
|
-
|
|
|
- <LanguageList
|
|
|
- onChange={lng => {
|
|
|
- setLanguage(lng);
|
|
|
- this.setState({ lng });
|
|
|
- }}
|
|
|
- languages={languages}
|
|
|
- currentLanguage={getLanguage()}
|
|
|
- />
|
|
|
- {this.renderIdsDropdown()}
|
|
|
- {this.state.scrolledOutside && (
|
|
|
- <button
|
|
|
- className="scroll-back-to-content"
|
|
|
- onClick={() => {
|
|
|
- this.setState({ ...calculateScrollCenter(elements) });
|
|
|
- }}
|
|
|
- >
|
|
|
- {t("buttons.scrollBackToContent")}
|
|
|
- </button>
|
|
|
- )}
|
|
|
- </footer>
|
|
|
</div>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- private renderIdsDropdown() {
|
|
|
- const scenes = loadedScenes();
|
|
|
- if (scenes.length === 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
- return (
|
|
|
- <StoredScenesList
|
|
|
- scenes={scenes}
|
|
|
- currentId={this.state.selectedId}
|
|
|
- onChange={(id, k) => this.loadScene(id, k)}
|
|
|
- />
|
|
|
- );
|
|
|
- }
|
|
|
-
|
|
|
private handleWheel = (e: WheelEvent) => {
|
|
|
e.preventDefault();
|
|
|
const { deltaX, deltaY } = e;
|