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) &&
|
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
||||||
appState.multiElement !== null),
|
appState.multiElement !== null),
|
||||||
PanelComponent: ({ appState, updateData }) => (
|
PanelComponent: ({ appState, updateData }) => (
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
visibility: appState.multiElement != null ? "visible" : "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
icon={done}
|
icon={done}
|
||||||
title={t("buttons.done")}
|
title={t("buttons.done")}
|
||||||
aria-label={t("buttons.done")}
|
aria-label={t("buttons.done")}
|
||||||
onClick={() => updateData(null)}
|
onClick={updateData}
|
||||||
|
visible={appState.multiElement != null}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
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";
|
} from "./actionExport";
|
||||||
|
|
||||||
export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
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]) {
|
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
|
||||||
const action = this.actions[name];
|
const action = this.actions[name];
|
||||||
const PanelComponent = action.PanelComponent!;
|
const PanelComponent = action.PanelComponent!;
|
||||||
const updateData = (formState: any) => {
|
const updateData = (formState?: any) => {
|
||||||
const commitToHistory =
|
const commitToHistory =
|
||||||
action.commitToHistory &&
|
action.commitToHistory &&
|
||||||
action.commitToHistory(this.getAppState(), this.getElements());
|
action.commitToHistory(this.getAppState(), this.getElements());
|
||||||
|
@ -21,7 +21,7 @@ export interface Action {
|
|||||||
PanelComponent?: React.FC<{
|
PanelComponent?: React.FC<{
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
appState: AppState;
|
appState: AppState;
|
||||||
updateData: (formData: any) => void;
|
updateData: (formData?: any) => void;
|
||||||
}>;
|
}>;
|
||||||
perform: ActionFn;
|
perform: ActionFn;
|
||||||
keyPriority?: number;
|
keyPriority?: number;
|
||||||
|
@ -30,7 +30,7 @@ export function getDefaultAppState(): AppState {
|
|||||||
isResizing: false,
|
isResizing: false,
|
||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
openedMenu: null,
|
openMenu: null,
|
||||||
lastPointerDownWith: "mouse",
|
lastPointerDownWith: "mouse",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.HintViewer {
|
.HintViewer {
|
||||||
background-color: rgba(255, 255, 255, 0.88);
|
|
||||||
color: #868e96; /* OC: GRAY 6*/
|
color: #868e96; /* OC: GRAY 6*/
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -9,9 +8,16 @@
|
|||||||
transform: translateX(calc(-50% - 16px)); /* 16px is half of lock icon */
|
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) {
|
@media (max-width: 600px), (max-height: 500px) and (max-width: 1000px) {
|
||||||
.HintViewer {
|
.HintViewer {
|
||||||
position: static;
|
position: static;
|
||||||
|
transform: none;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -52,5 +52,9 @@ export const HintViewer = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="HintViewer">{hint}</div>;
|
return (
|
||||||
|
<div className="HintViewer">
|
||||||
|
<span>{hint}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,7 @@ type ToolButtonBaseProps = {
|
|||||||
keyBindingLabel?: string;
|
keyBindingLabel?: string;
|
||||||
showAriaLabel?: boolean;
|
showAriaLabel?: boolean;
|
||||||
visible?: boolean;
|
visible?: boolean;
|
||||||
|
selected?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ToolButtonProps =
|
type ToolButtonProps =
|
||||||
@ -40,7 +41,9 @@ export const ToolButton = React.forwardRef(function(
|
|||||||
if (props.type === "button") {
|
if (props.type === "button") {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`ToolIcon_type_button ToolIcon ${sizeCn}`}
|
className={`ToolIcon_type_button ToolIcon ${sizeCn}${
|
||||||
|
props.selected ? " ToolIcon--selected" : ""
|
||||||
|
}`}
|
||||||
title={props.title}
|
title={props.title}
|
||||||
aria-label={props["aria-label"]}
|
aria-label={props["aria-label"]}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
:root {
|
||||||
|
--button-gray-1: #e9ecef;
|
||||||
|
--button-gray-2: #ced4da;
|
||||||
|
--button-gray-3: #adb5bd;
|
||||||
|
--button-blue: #a5d8ff;
|
||||||
|
}
|
||||||
|
|
||||||
.ToolIcon {
|
.ToolIcon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: Cascadia;
|
font-family: Cascadia;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #e9ecef;
|
background-color: var(--button-gray-1);
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,13 +48,19 @@
|
|||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #e9ecef;
|
background-color: var(--button-gray-1);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
background-color: #ced4da;
|
background-color: var(--button-gray-2);
|
||||||
}
|
}
|
||||||
&:focus {
|
&: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;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
&:hover + .ToolIcon__icon {
|
|
||||||
background-color: #e9ecef;
|
|
||||||
}
|
|
||||||
&:checked + .ToolIcon__icon {
|
&:checked + .ToolIcon__icon {
|
||||||
background-color: #ced4da;
|
background-color: var(--button-gray-2);
|
||||||
}
|
}
|
||||||
&:focus + .ToolIcon__icon {
|
&:focus + .ToolIcon__icon {
|
||||||
box-shadow: 0 0 0 2px #a5d8ff;
|
box-shadow: 0 0 0 2px var(--button-blue);
|
||||||
}
|
}
|
||||||
&:active + .ToolIcon__icon {
|
&:active + .ToolIcon__icon {
|
||||||
background-color: #adb5bd;
|
background-color: var(--button-gray-3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +115,7 @@
|
|||||||
bottom: 2px;
|
bottom: 2px;
|
||||||
right: 3px;
|
right: 3px;
|
||||||
font-size: 0.5em;
|
font-size: 0.5em;
|
||||||
color: #adb5bd; // OC GRAY 5
|
color: var(--button-gray-3); // OC GRAY 5
|
||||||
font-family: var(--ui-font);
|
font-family: var(--ui-font);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
@ -23,3 +23,4 @@ export {
|
|||||||
resizePerfectLineForNWHandler,
|
resizePerfectLineForNWHandler,
|
||||||
normalizeDimensions,
|
normalizeDimensions,
|
||||||
} from "./sizeHelpers";
|
} 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 { ExcalidrawElement } from "./element/types";
|
||||||
import { clearAppStatePropertiesForHistory } from "./appState";
|
import { clearAppStatePropertiesForHistory } from "./appState";
|
||||||
|
|
||||||
class SceneHistory {
|
type Result = {
|
||||||
|
appState: AppState;
|
||||||
|
elements: ExcalidrawElement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SceneHistory {
|
||||||
private recording: boolean = true;
|
private recording: boolean = true;
|
||||||
private stateHistory: string[] = [];
|
private stateHistory: string[] = [];
|
||||||
private redoStack: string[] = [];
|
private redoStack: string[] = [];
|
||||||
@ -53,7 +58,7 @@ class SceneHistory {
|
|||||||
this.redoStack.splice(0, this.redoStack.length);
|
this.redoStack.splice(0, this.redoStack.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
redoOnce() {
|
redoOnce(): Result | null {
|
||||||
if (this.redoStack.length === 0) {
|
if (this.redoStack.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -68,7 +73,7 @@ class SceneHistory {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
undoOnce() {
|
undoOnce(): Result | null {
|
||||||
if (this.stateHistory.length === 0) {
|
if (this.stateHistory.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
143
src/index.tsx
143
src/index.tsx
@ -17,6 +17,7 @@ import {
|
|||||||
getCursorForResizingElement,
|
getCursorForResizingElement,
|
||||||
getPerfectElementSize,
|
getPerfectElementSize,
|
||||||
normalizeDimensions,
|
normalizeDimensions,
|
||||||
|
showSelectedShapeActions,
|
||||||
} from "./element";
|
} from "./element";
|
||||||
import {
|
import {
|
||||||
clearSelection,
|
clearSelection,
|
||||||
@ -41,7 +42,7 @@ import {
|
|||||||
} from "./scene";
|
} from "./scene";
|
||||||
|
|
||||||
import { renderScene } from "./renderer";
|
import { renderScene } from "./renderer";
|
||||||
import { AppState, FlooredNumber, Gesture } from "./types";
|
import { AppState, FlooredNumber, Gesture, GestureEvent } from "./types";
|
||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -91,6 +92,8 @@ import {
|
|||||||
actionCopyStyles,
|
actionCopyStyles,
|
||||||
actionPasteStyles,
|
actionPasteStyles,
|
||||||
actionFinalize,
|
actionFinalize,
|
||||||
|
actionToggleCanvasMenu,
|
||||||
|
actionToggleEditMenu,
|
||||||
} from "./actions";
|
} from "./actions";
|
||||||
import { Action, ActionResult } from "./actions/types";
|
import { Action, ActionResult } from "./actions/types";
|
||||||
import { getDefaultAppState } from "./appState";
|
import { getDefaultAppState } from "./appState";
|
||||||
@ -109,7 +112,7 @@ import useIsMobile, { IsMobileProvider } from "./is-mobile";
|
|||||||
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
import { copyToAppClipboard, getClipboardContent } from "./clipboard";
|
||||||
import { normalizeScroll } from "./scene/data";
|
import { normalizeScroll } from "./scene/data";
|
||||||
import { getCenter, getDistance } from "./gesture";
|
import { getCenter, getDistance } from "./gesture";
|
||||||
import { menu, palette } from "./components/icons";
|
import { createUndoAction, createRedoAction } from "./actions/actionHistory";
|
||||||
|
|
||||||
let { elements } = createScene();
|
let { elements } = createScene();
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
@ -287,12 +290,6 @@ const LayerUI = React.memo(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSelectedShapeActions = Boolean(
|
|
||||||
appState.editingElement ||
|
|
||||||
getSelectedElements(elements).length ||
|
|
||||||
appState.elementType !== "selection",
|
|
||||||
);
|
|
||||||
|
|
||||||
function renderSelectedShapeActions() {
|
function renderSelectedShapeActions() {
|
||||||
const { elementType, editingElement } = appState;
|
const { elementType, editingElement } = appState;
|
||||||
const targetElements = editingElement
|
const targetElements = editingElement
|
||||||
@ -392,7 +389,7 @@ const LayerUI = React.memo(
|
|||||||
|
|
||||||
return isMobile ? (
|
return isMobile ? (
|
||||||
<>
|
<>
|
||||||
{appState.openedMenu === "canvas" ? (
|
{appState.openMenu === "canvas" ? (
|
||||||
<section
|
<section
|
||||||
className="App-mobile-menu"
|
className="App-mobile-menu"
|
||||||
aria-labelledby="canvas-actions-title"
|
aria-labelledby="canvas-actions-title"
|
||||||
@ -421,7 +418,8 @@ const LayerUI = React.memo(
|
|||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
) : appState.openedMenu === "shape" && showSelectedShapeActions ? (
|
) : appState.openMenu === "shape" &&
|
||||||
|
showSelectedShapeActions(appState, elements) ? (
|
||||||
<section
|
<section
|
||||||
className="App-mobile-menu"
|
className="App-mobile-menu"
|
||||||
aria-labelledby="selected-shape-title"
|
aria-labelledby="selected-shape-title"
|
||||||
@ -456,46 +454,13 @@ const LayerUI = React.memo(
|
|||||||
</FixedSideContainer>
|
</FixedSideContainer>
|
||||||
<footer className="App-toolbar">
|
<footer className="App-toolbar">
|
||||||
<div className="App-toolbar-content">
|
<div className="App-toolbar-content">
|
||||||
{appState.multiElement ? (
|
{actionManager.renderAction("toggleCanvasMenu")}
|
||||||
<>
|
{actionManager.renderAction("toggleEditMenu")}
|
||||||
{actionManager.renderAction("deleteSelectedElements")}
|
{actionManager.renderAction("undo")}
|
||||||
<ToolButton
|
{actionManager.renderAction("redo")}
|
||||||
visible={showSelectedShapeActions}
|
|
||||||
type="button"
|
|
||||||
icon={palette}
|
|
||||||
aria-label={t("buttons.edit")}
|
|
||||||
onClick={() =>
|
|
||||||
setAppState(({ openedMenu }: any) => ({
|
|
||||||
openedMenu: openedMenu === "shape" ? null : "shape",
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{actionManager.renderAction("finalize")}
|
{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")}
|
{actionManager.renderAction("deleteSelectedElements")}
|
||||||
|
</div>
|
||||||
{appState.scrolledOutside && (
|
{appState.scrolledOutside && (
|
||||||
<button
|
<button
|
||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
@ -506,9 +471,6 @@ const LayerUI = React.memo(
|
|||||||
{t("buttons.scrollBackToContent")}
|
{t("buttons.scrollBackToContent")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@ -541,7 +503,7 @@ const LayerUI = React.memo(
|
|||||||
</Stack.Col>
|
</Stack.Col>
|
||||||
</Island>
|
</Island>
|
||||||
</section>
|
</section>
|
||||||
{showSelectedShapeActions && (
|
{showSelectedShapeActions(appState, elements) && (
|
||||||
<section
|
<section
|
||||||
className="App-right-menu"
|
className="App-right-menu"
|
||||||
aria-labelledby="selected-shape-title"
|
aria-labelledby="selected-shape-title"
|
||||||
@ -686,6 +648,12 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.actionManager.registerAction(actionCopyStyles);
|
this.actionManager.registerAction(actionCopyStyles);
|
||||||
this.actionManager.registerAction(actionPasteStyles);
|
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];
|
this.canvasOnlyActions = [actionSelectAll];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -755,6 +723,19 @@ export class App extends React.Component<any, AppState> {
|
|||||||
window.addEventListener("dragover", this.disableEvent, false);
|
window.addEventListener("dragover", this.disableEvent, false);
|
||||||
window.addEventListener("drop", 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 searchParams = new URLSearchParams(window.location.search);
|
||||||
const id = searchParams.get("id");
|
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("blur", this.onUnload, false);
|
||||||
window.removeEventListener("dragover", this.disableEvent, false);
|
window.removeEventListener("dragover", this.disableEvent, false);
|
||||||
window.removeEventListener("drop", 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();
|
public state: AppState = getDefaultAppState();
|
||||||
@ -853,34 +846,6 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.state.draggingElement === null
|
this.state.draggingElement === null
|
||||||
) {
|
) {
|
||||||
this.selectShapeTool(shape);
|
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) {
|
} else if (event.key === KEYS.SPACE && gesture.pointers.length === 0) {
|
||||||
isHoldingSpace = true;
|
isHoldingSpace = true;
|
||||||
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
document.documentElement.style.cursor = CURSOR_TYPE.GRABBING;
|
||||||
@ -967,6 +932,22 @@ export class App extends React.Component<any, AppState> {
|
|||||||
this.setState({ elementType });
|
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) => {
|
setAppState = (obj: any) => {
|
||||||
this.setState(obj);
|
this.setState(obj);
|
||||||
};
|
};
|
||||||
@ -2214,7 +2195,7 @@ export class App extends React.Component<any, AppState> {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const { deltaX, deltaY } = event;
|
const { deltaX, deltaY } = event;
|
||||||
|
|
||||||
if (event[KEYS.META]) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
const sign = Math.sign(deltaY);
|
const sign = Math.sign(deltaY);
|
||||||
const MAX_STEP = 10;
|
const MAX_STEP = 10;
|
||||||
let delta = Math.abs(deltaY);
|
let delta = Math.abs(deltaY);
|
||||||
|
@ -60,7 +60,9 @@
|
|||||||
"resetZoom": "Reset zoom",
|
"resetZoom": "Reset zoom",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
"done": "Done",
|
"done": "Done",
|
||||||
"edit": "Edit"
|
"edit": "Edit",
|
||||||
|
"undo": "Undo",
|
||||||
|
"redo": "Redo"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "This will clear the whole canvas. Are you sure?",
|
"clearReset": "This will clear the whole canvas. Are you sure?",
|
||||||
|
@ -31,7 +31,7 @@ export type AppState = {
|
|||||||
selectedId?: string;
|
selectedId?: string;
|
||||||
isResizing: boolean;
|
isResizing: boolean;
|
||||||
zoom: number;
|
zoom: number;
|
||||||
openedMenu: "canvas" | "shape" | null;
|
openMenu: "canvas" | "shape" | null;
|
||||||
lastPointerDownWith: PointerType;
|
lastPointerDownWith: PointerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -47,3 +47,8 @@ export type Gesture = {
|
|||||||
initialDistance: number | null;
|
initialDistance: number | null;
|
||||||
initialScale: 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