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:
Jed Fox 2020-03-01 14:39:03 -05:00 committed by GitHub
parent 0ee33fe341
commit 8e0206cc1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 271 additions and 127 deletions

View file

@ -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);