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
@ -54,18 +54,13 @@ export const actionFinalize: Action = {
|
||||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
||||
appState.multiElement !== null),
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<div
|
||||
style={{
|
||||
visibility: appState.multiElement != null ? "visible" : "hidden",
|
||||
}}
|
||||
>
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={done}
|
||||
title={t("buttons.done")}
|
||||
aria-label={t("buttons.done")}
|
||||
onClick={() => updateData(null)}
|
||||
/>
|
||||
</div>
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={done}
|
||||
title={t("buttons.done")}
|
||||
aria-label={t("buttons.done")}
|
||||
onClick={updateData}
|
||||
visible={appState.multiElement != null}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
73
src/actions/actionHistory.tsx
Normal file
73
src/actions/actionHistory.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import { Action } from "./types";
|
||||
import React from "react";
|
||||
import { undo, redo } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { SceneHistory } from "../history";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppState } from "../types";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
const writeData = (
|
||||
appState: AppState,
|
||||
data: { elements: ExcalidrawElement[]; appState: AppState } | null,
|
||||
) => {
|
||||
if (data !== null) {
|
||||
return {
|
||||
elements: data.elements,
|
||||
appState: { ...appState, ...data.appState },
|
||||
};
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const testUndo = (shift: boolean) => (
|
||||
event: KeyboardEvent,
|
||||
appState: AppState,
|
||||
) => event[KEYS.META] && /z/i.test(event.key) && event.shiftKey === shift;
|
||||
|
||||
export const createUndoAction: (h: SceneHistory) => Action = history => ({
|
||||
name: "undo",
|
||||
perform: (_, appState) =>
|
||||
[
|
||||
appState.multiElement,
|
||||
appState.resizingElement,
|
||||
appState.editingElement,
|
||||
appState.draggingElement,
|
||||
].every(x => x === null)
|
||||
? writeData(appState, history.undoOnce())
|
||||
: {},
|
||||
keyTest: testUndo(false),
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={undo}
|
||||
aria-label={t("buttons.undo")}
|
||||
onClick={updateData}
|
||||
/>
|
||||
),
|
||||
commitToHistory: () => false,
|
||||
});
|
||||
|
||||
export const createRedoAction: (h: SceneHistory) => Action = history => ({
|
||||
name: "redo",
|
||||
perform: (_, appState) =>
|
||||
[
|
||||
appState.multiElement,
|
||||
appState.resizingElement,
|
||||
appState.editingElement,
|
||||
appState.draggingElement,
|
||||
].every(x => x === null)
|
||||
? writeData(appState, history.redoOnce())
|
||||
: {},
|
||||
keyTest: testUndo(true),
|
||||
PanelComponent: ({ updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={redo}
|
||||
aria-label={t("buttons.redo")}
|
||||
onClick={updateData}
|
||||
/>
|
||||
),
|
||||
commitToHistory: () => false,
|
||||
});
|
45
src/actions/actionMenu.tsx
Normal file
45
src/actions/actionMenu.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Action } from "./types";
|
||||
import React from "react";
|
||||
import { menu, palette } from "../components/icons";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { t } from "../i18n";
|
||||
import { showSelectedShapeActions } from "../element";
|
||||
|
||||
export const actionToggleCanvasMenu: Action = {
|
||||
name: "toggleCanvasMenu",
|
||||
perform: (_, appState) => ({
|
||||
appState: {
|
||||
...appState,
|
||||
openMenu: appState.openMenu === "canvas" ? null : "canvas",
|
||||
},
|
||||
}),
|
||||
PanelComponent: ({ appState, updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={menu}
|
||||
aria-label={t("buttons.menu")}
|
||||
onClick={updateData}
|
||||
selected={appState.openMenu === "canvas"}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const actionToggleEditMenu: Action = {
|
||||
name: "toggleEditMenu",
|
||||
perform: (_elements, appState) => ({
|
||||
appState: {
|
||||
...appState,
|
||||
openMenu: appState.openMenu === "shape" ? null : "shape",
|
||||
},
|
||||
}),
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
visible={showSelectedShapeActions(appState, elements)}
|
||||
type="button"
|
||||
icon={palette}
|
||||
aria-label={t("buttons.edit")}
|
||||
onClick={updateData}
|
||||
selected={appState.openMenu === "shape"}
|
||||
/>
|
||||
),
|
||||
};
|
@ -36,3 +36,4 @@ export {
|
||||
} from "./actionExport";
|
||||
|
||||
export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
||||
export { actionToggleCanvasMenu, actionToggleEditMenu } from "./actionMenu";
|
||||
|
@ -83,7 +83,7 @@ export class ActionManager implements ActionsManagerInterface {
|
||||
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
||||
const action = this.actions[name];
|
||||
const PanelComponent = action.PanelComponent!;
|
||||
const updateData = (formState: any) => {
|
||||
const updateData = (formState?: any) => {
|
||||
const commitToHistory =
|
||||
action.commitToHistory &&
|
||||
action.commitToHistory(this.getAppState(), this.getElements());
|
||||
|
@ -21,7 +21,7 @@ export interface Action {
|
||||
PanelComponent?: React.FC<{
|
||||
elements: readonly ExcalidrawElement[];
|
||||
appState: AppState;
|
||||
updateData: (formData: any) => void;
|
||||
updateData: (formData?: any) => void;
|
||||
}>;
|
||||
perform: ActionFn;
|
||||
keyPriority?: number;
|
||||
|
@ -30,7 +30,7 @@ export function getDefaultAppState(): AppState {
|
||||
isResizing: false,
|
||||
selectionElement: null,
|
||||
zoom: 1,
|
||||
openedMenu: null,
|
||||
openMenu: null,
|
||||
lastPointerDownWith: "mouse",
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
.HintViewer {
|
||||
background-color: rgba(255, 255, 255, 0.88);
|
||||
color: #868e96; /* OC: GRAY 6*/
|
||||
font-size: 0.8rem;
|
||||
left: 50%;
|
||||
@ -9,9 +8,16 @@
|
||||
transform: translateX(calc(-50% - 16px)); /* 16px is half of lock icon */
|
||||
}
|
||||
|
||||
.HintViewer > span {
|
||||
background-color: rgba(255, 255, 255, 0.88);
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px), (max-height: 500px) and (max-width: 1000px) {
|
||||
.HintViewer {
|
||||
position: static;
|
||||
transform: none;
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -52,5 +52,9 @@ export const HintViewer = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="HintViewer">{hint}</div>;
|
||||
return (
|
||||
<div className="HintViewer">
|
||||
<span>{hint}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,6 +16,7 @@ type ToolButtonBaseProps = {
|
||||
keyBindingLabel?: string;
|
||||
showAriaLabel?: boolean;
|
||||
visible?: boolean;
|
||||
selected?: boolean;
|
||||
};
|
||||
|
||||
type ToolButtonProps =
|
||||
@ -40,7 +41,9 @@ export const ToolButton = React.forwardRef(function(
|
||||
if (props.type === "button") {
|
||||
return (
|
||||
<button
|
||||
className={`ToolIcon_type_button ToolIcon ${sizeCn}`}
|
||||
className={`ToolIcon_type_button ToolIcon ${sizeCn}${
|
||||
props.selected ? " ToolIcon--selected" : ""
|
||||
}`}
|
||||
title={props.title}
|
||||
aria-label={props["aria-label"]}
|
||||
type="button"
|
||||
|
@ -1,10 +1,17 @@
|
||||
:root {
|
||||
--button-gray-1: #e9ecef;
|
||||
--button-gray-2: #ced4da;
|
||||
--button-gray-3: #adb5bd;
|
||||
--button-blue: #a5d8ff;
|
||||
}
|
||||
|
||||
.ToolIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
font-family: Cascadia;
|
||||
cursor: pointer;
|
||||
background-color: #e9ecef;
|
||||
background-color: var(--button-gray-1);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
@ -41,13 +48,19 @@
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
background-color: #e9ecef;
|
||||
background-color: var(--button-gray-1);
|
||||
}
|
||||
&:active {
|
||||
background-color: #ced4da;
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px #a5d8ff;
|
||||
box-shadow: 0 0 0 2px var(--button-blue);
|
||||
}
|
||||
&.ToolIcon--selected {
|
||||
background-color: var(--button-gray-2);
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,17 +70,14 @@
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:hover + .ToolIcon__icon {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
&:checked + .ToolIcon__icon {
|
||||
background-color: #ced4da;
|
||||
background-color: var(--button-gray-2);
|
||||
}
|
||||
&:focus + .ToolIcon__icon {
|
||||
box-shadow: 0 0 0 2px #a5d8ff;
|
||||
box-shadow: 0 0 0 2px var(--button-blue);
|
||||
}
|
||||
&:active + .ToolIcon__icon {
|
||||
background-color: #adb5bd;
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +115,7 @@
|
||||
bottom: 2px;
|
||||
right: 3px;
|
||||
font-size: 0.5em;
|
||||
color: #adb5bd; // OC GRAY 5
|
||||
color: var(--button-gray-3); // OC GRAY 5
|
||||
font-family: var(--ui-font);
|
||||
user-select: none;
|
||||
}
|
||||
|
@ -23,3 +23,4 @@ export {
|
||||
resizePerfectLineForNWHandler,
|
||||
normalizeDimensions,
|
||||
} from "./sizeHelpers";
|
||||
export { showSelectedShapeActions } from "./showSelectedShapeActions";
|
||||
|
13
src/element/showSelectedShapeActions.ts
Normal file
13
src/element/showSelectedShapeActions.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { AppState } from "../types";
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { getSelectedElements } from "../scene";
|
||||
|
||||
export const showSelectedShapeActions = (
|
||||
appState: AppState,
|
||||
elements: readonly ExcalidrawElement[],
|
||||
) =>
|
||||
Boolean(
|
||||
appState.editingElement ||
|
||||
getSelectedElements(elements).length ||
|
||||
appState.elementType !== "selection",
|
||||
);
|
@ -2,7 +2,12 @@ import { AppState } from "./types";
|
||||
import { ExcalidrawElement } from "./element/types";
|
||||
import { clearAppStatePropertiesForHistory } from "./appState";
|
||||
|
||||
class SceneHistory {
|
||||
type Result = {
|
||||
appState: AppState;
|
||||
elements: ExcalidrawElement[];
|
||||
};
|
||||
|
||||
export class SceneHistory {
|
||||
private recording: boolean = true;
|
||||
private stateHistory: string[] = [];
|
||||
private redoStack: string[] = [];
|
||||
@ -53,7 +58,7 @@ class SceneHistory {
|
||||
this.redoStack.splice(0, this.redoStack.length);
|
||||
}
|
||||
|
||||
redoOnce() {
|
||||
redoOnce(): Result | null {
|
||||
if (this.redoStack.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@ -68,7 +73,7 @@ class SceneHistory {
|
||||
return null;
|
||||
}
|
||||
|
||||
undoOnce() {
|
||||
undoOnce(): Result | null {
|
||||
if (this.stateHistory.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
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);
|
||||
|
@ -60,7 +60,9 @@
|
||||
"resetZoom": "Reset zoom",
|
||||
"menu": "Menu",
|
||||
"done": "Done",
|
||||
"edit": "Edit"
|
||||
"edit": "Edit",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||
|
@ -31,7 +31,7 @@ export type AppState = {
|
||||
selectedId?: string;
|
||||
isResizing: boolean;
|
||||
zoom: number;
|
||||
openedMenu: "canvas" | "shape" | null;
|
||||
openMenu: "canvas" | "shape" | null;
|
||||
lastPointerDownWith: PointerType;
|
||||
};
|
||||
|
||||
@ -47,3 +47,8 @@ export type Gesture = {
|
||||
initialDistance: number | null;
|
||||
initialScale: number | null;
|
||||
};
|
||||
|
||||
export declare class GestureEvent extends UIEvent {
|
||||
readonly rotation: number;
|
||||
readonly scale: number;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user