mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
move footer into layerUI & refactor ActionManager (#729)
This commit is contained in:
parent
88eacc9da7
commit
d79293de06
7 changed files with 164 additions and 179 deletions
255
src/index.tsx
255
src/index.tsx
|
@ -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>
|
||||
</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>
|
||||
<>
|
||||
<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>
|
||||
<div />
|
||||
</div>
|
||||
</FixedSideContainer>
|
||||
<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>
|
||||
<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) });
|
||||
}}
|
||||
>
|
||||
{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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue