mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Undo/Redo buttons, refactor menu toggles (#793)
* Make Undo & Redo and the menu buttons into actions; add undo/redo buttons * Create variables for the ToolIcon colors * Darken the menu buttons when they’re active * Put the more intensive test in `perform` * Fix & restyle hint viewer * Add pinch zoom for macOS Safari * Chrome/Firefox trackpad pinch zoom * openedMenu → openMenu * needsShapeEditor.ts → showSelectedShapeActions.ts * Call showSelectedShapeActions
This commit is contained in:
parent
0ee33fe341
commit
8e0206cc1e
17 changed files with 271 additions and 127 deletions
165
src/index.tsx
165
src/index.tsx
|
@ -17,6 +17,7 @@ import {
|
|||
getCursorForResizingElement,
|
||||
getPerfectElementSize,
|
||||
normalizeDimensions,
|
||||
showSelectedShapeActions,
|
||||
} from "./element";
|
||||
import {
|
||||
clearSelection,
|
||||
|
@ -41,7 +42,7 @@ import {
|
|||
} from "./scene";
|
||||
|
||||
import { renderScene } from "./renderer";
|
||||
import { AppState, FlooredNumber, Gesture } from "./types";
|
||||
import { AppState, FlooredNumber, Gesture, GestureEvent } from "./types";
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
|
||||
import {
|
||||
|
@ -91,6 +92,8 @@ import {
|
|||
actionCopyStyles,
|
||||
actionPasteStyles,
|
||||
actionFinalize,
|
||||
actionToggleCanvasMenu,
|
||||
actionToggleEditMenu,
|
||||
} from "./actions";
|
||||
import { Action, ActionResult } from "./actions/types";
|
||||
import { getDefaultAppState } from "./appState";
|
||||
|
@ -109,7 +112,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
|
|||
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
||||
import { normalizeScroll } from "./scene/data";
|
||||
import { getCenter, getDistance } from "./gesture";
|
||||
import { menu, palette } from "./components/icons";
|
||||
import { createUndoAction, createRedoAction } from "./actions/actionHistory";
|
||||
|
||||
let { elements } = createScene();
|
||||
const { history } = createHistory();
|
||||
|
@ -287,12 +290,6 @@ const LayerUI = React.memo(
|
|||
);
|
||||
}
|
||||
|
||||
const showSelectedShapeActions = Boolean(
|
||||
appState.editingElement ||
|
||||
getSelectedElements(elements).length ||
|
||||
appState.elementType !== "selection",
|
||||
);
|
||||
|
||||
function renderSelectedShapeActions() {
|
||||
const { elementType, editingElement } = appState;
|
||||
const targetElements = editingElement
|
||||
|
@ -392,7 +389,7 @@ const LayerUI = React.memo(
|
|||
|
||||
return isMobile ? (
|
||||
<>
|
||||
{appState.openedMenu === "canvas" ? (
|
||||
{appState.openMenu === "canvas" ? (
|
||||
<section
|
||||
className="App-mobile-menu"
|
||||
aria-labelledby="canvas-actions-title"
|
||||
|
@ -421,7 +418,8 @@ const LayerUI = React.memo(
|
|||
</Stack.Col>
|
||||
</div>
|
||||
</section>
|
||||
) : appState.openedMenu === "shape" && showSelectedShapeActions ? (
|
||||
) : appState.openMenu === "shape" &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
<section
|
||||
className="App-mobile-menu"
|
||||
aria-labelledby="selected-shape-title"
|
||||
|
@ -456,59 +454,23 @@ const LayerUI = React.memo(
|
|||
</FixedSideContainer>
|
||||
<footer className="App-toolbar">
|
||||
<div className="App-toolbar-content">
|
||||
{appState.multiElement ? (
|
||||
<>
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
<ToolButton
|
||||
visible={showSelectedShapeActions}
|
||||
type="button"
|
||||
icon={palette}
|
||||
aria-label={t("buttons.edit")}
|
||||
onClick={() =>
|
||||
setAppState(({ openedMenu }: any) => ({
|
||||
openedMenu: openedMenu === "shape" ? null : "shape",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{actionManager.renderAction("finalize")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={menu}
|
||||
aria-label={t("buttons.menu")}
|
||||
onClick={() =>
|
||||
setAppState(({ openedMenu }: any) => ({
|
||||
openedMenu: openedMenu === "canvas" ? null : "canvas",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<ToolButton
|
||||
visible={showSelectedShapeActions}
|
||||
type="button"
|
||||
icon={palette}
|
||||
aria-label={t("buttons.edit")}
|
||||
onClick={() =>
|
||||
setAppState(({ openedMenu }: any) => ({
|
||||
openedMenu: openedMenu === "shape" ? null : "shape",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({ ...calculateScrollCenter(elements) });
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{actionManager.renderAction("toggleCanvasMenu")}
|
||||
{actionManager.renderAction("toggleEditMenu")}
|
||||
{actionManager.renderAction("undo")}
|
||||
{actionManager.renderAction("redo")}
|
||||
{actionManager.renderAction("finalize")}
|
||||
{actionManager.renderAction("deleteSelectedElements")}
|
||||
</div>
|
||||
{appState.scrolledOutside && (
|
||||
<button
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState({ ...calculateScrollCenter(elements) });
|
||||
}}
|
||||
>
|
||||
{t("buttons.scrollBackToContent")}
|
||||
</button>
|
||||
)}
|
||||
</footer>
|
||||
</>
|
||||
) : (
|
||||
|
@ -541,7 +503,7 @@ const LayerUI = React.memo(
|
|||
</Stack.Col>
|
||||
</Island>
|
||||
</section>
|
||||
{showSelectedShapeActions && (
|
||||
{showSelectedShapeActions(appState, elements) && (
|
||||
<section
|
||||
className="App-right-menu"
|
||||
aria-labelledby="selected-shape-title"
|
||||
|
@ -686,6 +648,12 @@ export class App extends React.Component<any, AppState> {
|
|||
this.actionManager.registerAction(actionCopyStyles);
|
||||
this.actionManager.registerAction(actionPasteStyles);
|
||||
|
||||
this.actionManager.registerAction(actionToggleCanvasMenu);
|
||||
this.actionManager.registerAction(actionToggleEditMenu);
|
||||
|
||||
this.actionManager.registerAction(createUndoAction(history));
|
||||
this.actionManager.registerAction(createRedoAction(history));
|
||||
|
||||
this.canvasOnlyActions = [actionSelectAll];
|
||||
}
|
||||
|
||||
|
@ -755,6 +723,19 @@ export class App extends React.Component<any, AppState> {
|
|||
window.addEventListener("dragover", this.disableEvent, false);
|
||||
window.addEventListener("drop", this.disableEvent, false);
|
||||
|
||||
// Safari-only desktop pinch zoom
|
||||
document.addEventListener(
|
||||
"gesturestart",
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener(
|
||||
"gesturechange",
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.addEventListener("gestureend", this.onGestureEnd as any, false);
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const id = searchParams.get("id");
|
||||
|
||||
|
@ -794,6 +775,18 @@ export class App extends React.Component<any, AppState> {
|
|||
window.removeEventListener("blur", this.onUnload, false);
|
||||
window.removeEventListener("dragover", this.disableEvent, false);
|
||||
window.removeEventListener("drop", this.disableEvent, false);
|
||||
|
||||
document.removeEventListener(
|
||||
"gesturestart",
|
||||
this.onGestureStart as any,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener(
|
||||
"gesturechange",
|
||||
this.onGestureChange as any,
|
||||
false,
|
||||
);
|
||||
document.removeEventListener("gestureend", this.onGestureEnd as any, false);
|
||||
}
|
||||
|
||||
public state: AppState = getDefaultAppState();
|
||||
|
@ -853,34 +846,6 @@ export class App extends React.Component<any, AppState> {
|
|||
this.state.draggingElement === null
|
||||
) {
|
||||
this.selectShapeTool(shape);
|
||||
// Undo action
|
||||
} else if (event[KEYS.META] && /z/i.test(event.key)) {
|
||||
event.preventDefault();
|
||||
|
||||
if (
|
||||
this.state.multiElement ||
|
||||
this.state.resizingElement ||
|
||||
this.state.editingElement ||
|
||||
this.state.draggingElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
// Redo action
|
||||
const data = history.redoOnce();
|
||||
if (data !== null) {
|
||||
elements = data.elements;
|
||||
this.setState({ ...data.appState });
|
||||
}
|
||||
} else {
|
||||
// undo action
|
||||
const data = history.undoOnce();
|
||||
if (data !== null) {
|
||||
elements = data.elements;
|
||||
this.setState({ ...data.appState });
|
||||
}
|
||||
}
|
||||
} else if (event.key === KEYS.SPACE && gesture.pointers.length === 0) {
|
||||
isHoldingSpace = true;
|
||||
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
||||
|
@ -967,6 +932,22 @@ export class App extends React.Component<any, AppState> {
|
|||
this.setState({ elementType });
|
||||
}
|
||||
|
||||
private onGestureStart = (event: GestureEvent) => {
|
||||
event.preventDefault();
|
||||
gesture.initialScale = this.state.zoom;
|
||||
};
|
||||
private onGestureChange = (event: GestureEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
this.setState({
|
||||
zoom: getNormalizedZoom(gesture.initialScale! * event.scale),
|
||||
});
|
||||
};
|
||||
private onGestureEnd = (event: GestureEvent) => {
|
||||
event.preventDefault();
|
||||
gesture.initialScale = null;
|
||||
};
|
||||
|
||||
setAppState = (obj: any) => {
|
||||
this.setState(obj);
|
||||
};
|
||||
|
@ -2214,7 +2195,7 @@ export class App extends React.Component<any, AppState> {
|
|||
event.preventDefault();
|
||||
const { deltaX, deltaY } = event;
|
||||
|
||||
if (event[KEYS.META]) {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
const sign = Math.sign(deltaY);
|
||||
const MAX_STEP = 10;
|
||||
let delta = Math.abs(deltaY);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue