diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 2baa25081875..8092b1918b7f 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -54,18 +54,13 @@ export const actionFinalize: Action = { ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && appState.multiElement !== null), PanelComponent: ({ appState, updateData }) => ( -
- updateData(null)} - /> -
+ ), }; diff --git a/src/actions/actionHistory.tsx b/src/actions/actionHistory.tsx new file mode 100644 index 000000000000..9c05e79c222b --- /dev/null +++ b/src/actions/actionHistory.tsx @@ -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 }) => ( + + ), + 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 }) => ( + + ), + commitToHistory: () => false, +}); diff --git a/src/actions/actionMenu.tsx b/src/actions/actionMenu.tsx new file mode 100644 index 000000000000..153a6c965b0d --- /dev/null +++ b/src/actions/actionMenu.tsx @@ -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 }) => ( + + ), +}; + +export const actionToggleEditMenu: Action = { + name: "toggleEditMenu", + perform: (_elements, appState) => ({ + appState: { + ...appState, + openMenu: appState.openMenu === "shape" ? null : "shape", + }, + }), + PanelComponent: ({ elements, appState, updateData }) => ( + + ), +}; diff --git a/src/actions/index.ts b/src/actions/index.ts index 659c9d4b0930..e366adebe0dd 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -36,3 +36,4 @@ export { } from "./actionExport"; export { actionCopyStyles, actionPasteStyles } from "./actionStyles"; +export { actionToggleCanvasMenu, actionToggleEditMenu } from "./actionMenu"; diff --git a/src/actions/manager.tsx b/src/actions/manager.tsx index 9f4481580905..e9cb310e8454 100644 --- a/src/actions/manager.tsx +++ b/src/actions/manager.tsx @@ -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()); diff --git a/src/actions/types.ts b/src/actions/types.ts index 5e57d7ab695b..c1fe7251f416 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -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; diff --git a/src/appState.ts b/src/appState.ts index e282d4fd8f6e..3ed68f559ae2 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -30,7 +30,7 @@ export function getDefaultAppState(): AppState { isResizing: false, selectionElement: null, zoom: 1, - openedMenu: null, + openMenu: null, lastPointerDownWith: "mouse", }; } diff --git a/src/components/HintViewer.css b/src/components/HintViewer.css index f5049b3655e7..fdd44793ed6d 100644 --- a/src/components/HintViewer.css +++ b/src/components/HintViewer.css @@ -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; } diff --git a/src/components/HintViewer.tsx b/src/components/HintViewer.tsx index 85b8454da9f8..0111be10ceaa 100644 --- a/src/components/HintViewer.tsx +++ b/src/components/HintViewer.tsx @@ -52,5 +52,9 @@ export const HintViewer = ({ return null; } - return
{hint}
; + return ( +
+ {hint} +
+ ); }; diff --git a/src/components/ToolButton.tsx b/src/components/ToolButton.tsx index 161af0824a15..e41184958853 100644 --- a/src/components/ToolButton.tsx +++ b/src/components/ToolButton.tsx @@ -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 ( - )} - - )} + {actionManager.renderAction("toggleCanvasMenu")} + {actionManager.renderAction("toggleEditMenu")} + {actionManager.renderAction("undo")} + {actionManager.renderAction("redo")} + {actionManager.renderAction("finalize")} + {actionManager.renderAction("deleteSelectedElements")} + {appState.scrolledOutside && ( + + )} ) : ( @@ -541,7 +503,7 @@ const LayerUI = React.memo( - {showSelectedShapeActions && ( + {showSelectedShapeActions(appState, elements) && (
{ 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 { 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 { 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 { 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 { 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 { 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); diff --git a/src/locales/en.json b/src/locales/en.json index 203c0643eff1..f7a47887e9c7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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?", diff --git a/src/types.ts b/src/types.ts index 9498661b5adf..6225c441fc65 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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; +}