From 64d330a332c0baf0a961d4fa9c9c5b5329df0485 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Fri, 6 May 2022 18:21:22 +0530 Subject: [PATCH] feat: support customType in activeTool (#5144) * feat: support customType in activeTool * fix * rewrite types and handling * tweaks * fix Co-authored-by: dwelle --- src/actions/actionCanvas.tsx | 29 +++++++---- src/actions/actionDeleteSelected.tsx | 3 +- src/actions/actionFinalize.tsx | 26 ++++++---- src/appState.ts | 1 + src/components/Actions.tsx | 11 +++- src/components/App.tsx | 49 ++++++++++------- src/data/restore.ts | 15 ++++-- .../__snapshots__/contextmenu.test.tsx.snap | 17 ++++++ .../regressionTests.test.tsx.snap | 52 +++++++++++++++++++ .../packages/__snapshots__/utils.test.ts.snap | 1 + src/types.ts | 28 ++++++++-- src/utils.ts | 29 ++++++++++- 12 files changed, 210 insertions(+), 51 deletions(-) diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index 8dc2c293..e8062355 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -11,7 +11,7 @@ import { getNormalizedZoom, getSelectedElements } from "../scene"; import { centerScrollOn } from "../scene/scroll"; import { getStateForZoom } from "../scene/zoom"; import { AppState, NormalizedZoomValue } from "../types"; -import { getShortcutKey } from "../utils"; +import { getShortcutKey, updateActiveTool } from "../utils"; import { register } from "./register"; import { Tooltip } from "../components/Tooltip"; import { newElementWith } from "../element/mutateElement"; @@ -304,21 +304,28 @@ export const actionErase = register({ name: "eraser", trackEvent: { category: "toolbar" }, perform: (elements, appState) => { + let activeTool: AppState["activeTool"]; + + if (isEraserActive(appState)) { + activeTool = updateActiveTool(appState, { + ...(appState.activeTool.lastActiveToolBeforeEraser || { + type: "selection", + }), + lastActiveToolBeforeEraser: null, + }); + } else { + activeTool = updateActiveTool(appState, { + type: "eraser", + lastActiveToolBeforeEraser: appState.activeTool, + }); + } + return { appState: { ...appState, selectedElementIds: {}, selectedGroupIds: {}, - activeTool: { - ...appState.activeTool, - type: isEraserActive(appState) - ? appState.activeTool.lastActiveToolBeforeEraser ?? "selection" - : "eraser", - lastActiveToolBeforeEraser: - appState.activeTool.type === "eraser" //node throws incorrect type error when using isEraserActive() - ? null - : appState.activeTool.type, - }, + activeTool, }, commitToHistory: true, }; diff --git a/src/actions/actionDeleteSelected.tsx b/src/actions/actionDeleteSelected.tsx index d7836314..0bbf6cb6 100644 --- a/src/actions/actionDeleteSelected.tsx +++ b/src/actions/actionDeleteSelected.tsx @@ -12,6 +12,7 @@ import { getElementsInGroup } from "../groups"; import { LinearElementEditor } from "../element/linearElementEditor"; import { fixBindingsAfterDeletion } from "../element/binding"; import { isBoundToContainer } from "../element/typeChecks"; +import { updateActiveTool } from "../utils"; const deleteSelectedElements = ( elements: readonly ExcalidrawElement[], @@ -134,7 +135,7 @@ export const actionDeleteSelected = register({ elements: nextElements, appState: { ...nextAppState, - activeTool: { ...appState.activeTool, type: "selection" }, + activeTool: updateActiveTool(appState, { type: "selection" }), multiElement: null, }, commitToHistory: isSomeElementSelected( diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 4a777220..74068793 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -1,6 +1,6 @@ import { KEYS } from "../keys"; import { isInvisiblySmallElement } from "../element"; -import { resetCursor } from "../utils"; +import { updateActiveTool, resetCursor } from "../utils"; import { ToolButton } from "../components/ToolButton"; import { done } from "../components/icons"; import { t } from "../i18n"; @@ -14,6 +14,7 @@ import { bindOrUnbindLinearElement, } from "../element/binding"; import { isBindingElement } from "../element/typeChecks"; +import { AppState } from "../types"; export const actionFinalize = register({ name: "finalize", @@ -137,6 +138,20 @@ export const actionFinalize = register({ resetCursor(canvas); } + let activeTool: AppState["activeTool"]; + if (appState.activeTool.type === "eraser") { + activeTool = updateActiveTool(appState, { + ...(appState.activeTool.lastActiveToolBeforeEraser || { + type: "selection", + }), + lastActiveToolBeforeEraser: null, + }); + } else { + activeTool = updateActiveTool(appState, { + type: "selection", + }); + } + return { elements: newElements, appState: { @@ -147,14 +162,7 @@ export const actionFinalize = register({ appState.activeTool.type === "freedraw") && multiPointElement ? appState.activeTool - : { - ...appState.activeTool, - type: - appState.activeTool.type === "eraser" && - appState.activeTool.lastActiveToolBeforeEraser - ? appState.activeTool.lastActiveToolBeforeEraser - : "selection", - }, + : activeTool, draggingElement: null, multiElement: null, editingElement: null, diff --git a/src/appState.ts b/src/appState.ts index f906f527..58f63a16 100644 --- a/src/appState.ts +++ b/src/appState.ts @@ -43,6 +43,7 @@ export const getDefaultAppState = (): Omit< editingLinearElement: null, activeTool: { type: "selection", + customType: null, locked: false, lastActiveToolBeforeEraser: null, }, diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index e18fb9c8..29eddbe0 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -15,7 +15,12 @@ import { } from "../scene"; import { SHAPES } from "../shapes"; import { AppState, Zoom } from "../types"; -import { capitalizeString, isTransparent, setCursorForShape } from "../utils"; +import { + capitalizeString, + isTransparent, + updateActiveTool, + setCursorForShape, +} from "../utils"; import Stack from "./Stack"; import { ToolButton } from "./ToolButton"; import { hasStrokeColor } from "../scene/comparisons"; @@ -229,7 +234,9 @@ export const ShapesSwitcher = ({ if (appState.activeTool.type !== value) { trackEvent("toolbar", value, "ui"); } - const nextActiveTool = { ...activeTool, type: value }; + const nextActiveTool = updateActiveTool(appState, { + type: value, + }); setAppState({ activeTool: nextActiveTool, multiElement: null, diff --git a/src/components/App.tsx b/src/components/App.tsx index 4bc0bc1c..868ea171 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -183,7 +183,7 @@ import { import Scene from "../scene/Scene"; import { RenderConfig, ScrollBars } from "../scene/types"; import { getStateForZoom } from "../scene/zoom"; -import { findShapeByKey } from "../shapes"; +import { findShapeByKey, SHAPES } from "../shapes"; import { AppClassProperties, AppProps, @@ -219,6 +219,7 @@ import { withBatchedUpdatesThrottled, updateObject, setEraserCursor, + updateActiveTool, } from "../utils"; import ContextMenu, { ContextMenuOption } from "./ContextMenu"; import LayerUI from "./LayerUI"; @@ -1074,7 +1075,7 @@ class App extends React.Component { isEraserActive(this.state) ) { this.setState({ - activeTool: { ...this.state.activeTool, type: "selection" }, + activeTool: updateActiveTool(this.state, { type: "selection" }), }); } if ( @@ -1444,7 +1445,7 @@ class App extends React.Component { } else if (data.text) { this.addTextFromPaste(data.text); } - this.setActiveTool({ ...this.state.activeTool, type: "selection" }); + this.setActiveTool({ type: "selection" }); event?.preventDefault(); }, ); @@ -1532,7 +1533,7 @@ class App extends React.Component { } }, ); - this.setActiveTool({ ...this.state.activeTool, type: "selection" }); + this.setActiveTool({ type: "selection" }); }; private addTextFromPaste(text: any) { @@ -1594,10 +1595,13 @@ class App extends React.Component { return { activeTool: { ...prevState.activeTool, + ...updateActiveTool( + this.state, + prevState.activeTool.locked + ? { type: "selection" } + : prevState.activeTool, + ), locked: !prevState.activeTool.locked, - type: prevState.activeTool.locked - ? "selection" - : prevState.activeTool.type, }, }; }); @@ -1895,7 +1899,7 @@ class App extends React.Component { `keyboard (${this.deviceType.isMobile ? "mobile" : "desktop"})`, ); } - this.setActiveTool({ ...this.state.activeTool, type: shape }); + this.setActiveTool({ type: shape }); event.stopPropagation(); } else if (event.key === KEYS.Q) { this.toggleLock("keyboard"); @@ -1971,28 +1975,33 @@ class App extends React.Component { } }); - private setActiveTool(tool: AppState["activeTool"]) { + private setActiveTool( + tool: + | { type: typeof SHAPES[number]["value"] | "eraser" } + | { type: "custom"; customType: string }, + ) { + const nextActiveTool = updateActiveTool(this.state, tool); if (!isHoldingSpace) { setCursorForShape(this.canvas, this.state); } if (isToolIcon(document.activeElement)) { this.focusContainer(); } - if (!isLinearElementType(tool.type)) { + if (!isLinearElementType(nextActiveTool.type)) { this.setState({ suggestedBindings: [] }); } - if (tool.type === "image") { + if (nextActiveTool.type === "image") { this.onImageAction(); } - if (tool.type !== "selection") { + if (nextActiveTool.type !== "selection") { this.setState({ - activeTool: tool, + activeTool: nextActiveTool, selectedElementIds: {}, selectedGroupIds: {}, editingGroupId: null, }); } else { - this.setState({ activeTool: tool }); + this.setState({ activeTool: nextActiveTool }); } } @@ -3057,6 +3066,8 @@ class App extends React.Component { this.state.activeTool.type, pointerDownState, ); + } else if (this.state.activeTool.type === "custom") { + setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR); } else if (this.state.activeTool.type !== "eraser") { this.createGenericElementOnPointerDown( this.state.activeTool.type, @@ -3638,7 +3649,7 @@ class App extends React.Component { resetCursor(this.canvas); if (!this.state.activeTool.locked) { this.setState({ - activeTool: { ...this.state.activeTool, type: "selection" }, + activeTool: updateActiveTool(this.state, { type: "selection" }), }); } }; @@ -4457,7 +4468,9 @@ class App extends React.Component { resetCursor(this.canvas); this.setState((prevState) => ({ draggingElement: null, - activeTool: { ...prevState.activeTool, type: "selection" }, + activeTool: updateActiveTool(this.state, { + type: "selection", + }), selectedElementIds: { ...prevState.selectedElementIds, [this.state.draggingElement!.id]: true, @@ -4679,7 +4692,7 @@ class App extends React.Component { this.setState({ draggingElement: null, suggestedBindings: [], - activeTool: { ...activeTool, type: "selection" }, + activeTool: updateActiveTool(this.state, { type: "selection" }), }); } else { this.setState({ @@ -4985,7 +4998,7 @@ class App extends React.Component { { pendingImageElement: null, editingElement: null, - activeTool: { ...this.state.activeTool, type: "selection" }, + activeTool: updateActiveTool(this.state, { type: "selection" }), }, () => { this.actionManager.executeAction(actionFinalize); diff --git a/src/data/restore.ts b/src/data/restore.ts index cc659607..8b5ba433 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -26,7 +26,7 @@ import { import { getDefaultAppState } from "../appState"; import { LinearElementEditor } from "../element/linearElementEditor"; import { bumpVersion } from "../element/mutateElement"; -import { getUpdatedTimestamp } from "../utils"; +import { getUpdatedTimestamp, updateActiveTool } from "../utils"; import { arrayToMap } from "../utils"; type RestoredAppState = Omit< @@ -48,6 +48,7 @@ export const AllowedExcalidrawActiveTools: Record< arrow: true, freedraw: true, eraser: false, + custom: true, }; export type RestoredDataState = { @@ -198,6 +199,7 @@ const restoreElement = ( y, }); } + // generic elements case "ellipse": return restoreElementWithProperties(element, {}); @@ -255,6 +257,7 @@ export const restoreAppState = ( ? localValue : defaultValue; } + return { ...nextAppState, cursorButton: localAppState?.cursorButton || "up", @@ -263,11 +266,15 @@ export const restoreAppState = ( localAppState?.penDetected ?? (appState.penMode ? appState.penDetected ?? false : false), activeTool: { + ...updateActiveTool( + defaultAppState, + nextAppState.activeTool.type && + AllowedExcalidrawActiveTools[nextAppState.activeTool.type] + ? nextAppState.activeTool + : { type: "selection" }, + ), lastActiveToolBeforeEraser: null, locked: nextAppState.activeTool.locked ?? false, - type: AllowedExcalidrawActiveTools[nextAppState.activeTool.type] - ? nextAppState.activeTool.type ?? "selection" - : "selection", }, // Migrates from previous version where appState.zoom was a number zoom: diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 9fadf47f..9ffbbad5 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -3,6 +3,7 @@ exports[`contextMenu element right-clicking on a group should select whole group: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -175,6 +176,7 @@ exports[`contextMenu element right-clicking on a group should select whole group exports[`contextMenu element selecting 'Add to library' in context menu adds element to library: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -351,6 +353,7 @@ exports[`contextMenu element selecting 'Add to library' in context menu adds ele exports[`contextMenu element selecting 'Bring forward' in context menu brings element forward: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -688,6 +691,7 @@ exports[`contextMenu element selecting 'Bring forward' in context menu brings el exports[`contextMenu element selecting 'Bring to front' in context menu brings element to front: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -1025,6 +1029,7 @@ exports[`contextMenu element selecting 'Bring to front' in context menu brings e exports[`contextMenu element selecting 'Copy styles' in context menu copies styles: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -1201,6 +1206,7 @@ exports[`contextMenu element selecting 'Copy styles' in context menu copies styl exports[`contextMenu element selecting 'Delete' in context menu deletes element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -1413,6 +1419,7 @@ exports[`contextMenu element selecting 'Delete' in context menu deletes element: exports[`contextMenu element selecting 'Duplicate' in context menu duplicates element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -1684,6 +1691,7 @@ exports[`contextMenu element selecting 'Duplicate' in context menu duplicates el exports[`contextMenu element selecting 'Group selection' in context menu groups selected elements: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -2039,6 +2047,7 @@ exports[`contextMenu element selecting 'Group selection' in context menu groups exports[`contextMenu element selecting 'Paste styles' in context menu pastes styles: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -2838,6 +2847,7 @@ exports[`contextMenu element selecting 'Paste styles' in context menu pastes sty exports[`contextMenu element selecting 'Send backward' in context menu sends element backward: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -3175,6 +3185,7 @@ exports[`contextMenu element selecting 'Send backward' in context menu sends ele exports[`contextMenu element selecting 'Send to back' in context menu sends element to back: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -3512,6 +3523,7 @@ exports[`contextMenu element selecting 'Send to back' in context menu sends elem exports[`contextMenu element selecting 'Ungroup selection' in context menu ungroups selected group: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -3929,6 +3941,7 @@ exports[`contextMenu element selecting 'Ungroup selection' in context menu ungro exports[`contextMenu element shows 'Group selection' in context menu for multiple selected elements: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4206,6 +4219,7 @@ exports[`contextMenu element shows 'Group selection' in context menu for multipl exports[`contextMenu element shows 'Ungroup selection' in context menu for group inside selected elements: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4564,6 +4578,7 @@ exports[`contextMenu element shows 'Ungroup selection' in context menu for group exports[`contextMenu element shows context menu for canvas: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4669,6 +4684,7 @@ exports[`contextMenu element shows context menu for canvas: [end of test] number exports[`contextMenu element shows context menu for element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4752,6 +4768,7 @@ Object { exports[`contextMenu element shows context menu for element: [end of test] appState 2`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index d8566f5c..3669e4e6 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -3,6 +3,7 @@ exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -511,6 +512,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -1025,6 +1027,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`regression tests Cmd/Ctrl-click exclusively select element under pointer: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -1884,6 +1887,7 @@ exports[`regression tests Cmd/Ctrl-click exclusively select element under pointe exports[`regression tests Drags selected element when hitting only bounding box and keeps element selected: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -2104,6 +2108,7 @@ exports[`regression tests Drags selected element when hitting only bounding box exports[`regression tests adjusts z order when grouping: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -2609,6 +2614,7 @@ exports[`regression tests adjusts z order when grouping: [end of test] number of exports[`regression tests alt-drag duplicates an element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -2884,6 +2890,7 @@ exports[`regression tests alt-drag duplicates an element: [end of test] number o exports[`regression tests arrow keys: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -3060,6 +3067,7 @@ exports[`regression tests arrow keys: [end of test] number of renders 1`] = `20` exports[`regression tests can drag element that covers another element, while another elem is selected: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -3548,6 +3556,7 @@ exports[`regression tests can drag element that covers another element, while an exports[`regression tests change the properties of a shape: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -3804,6 +3813,7 @@ exports[`regression tests change the properties of a shape: [end of test] number exports[`regression tests click on an element and drag it: [dragged] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4024,6 +4034,7 @@ exports[`regression tests click on an element and drag it: [dragged] number of r exports[`regression tests click on an element and drag it: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4288,6 +4299,7 @@ exports[`regression tests click on an element and drag it: [end of test] number exports[`regression tests click to select a shape: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4562,6 +4574,7 @@ exports[`regression tests click to select a shape: [end of test] number of rende exports[`regression tests click-drag to select a group: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -4958,6 +4971,7 @@ exports[`regression tests click-drag to select a group: [end of test] number of exports[`regression tests deselects group of selected elements on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -5281,6 +5295,7 @@ exports[`regression tests deselects group of selected elements on pointer down w exports[`regression tests deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -5579,6 +5594,7 @@ exports[`regression tests deselects group of selected elements on pointer up whe exports[`regression tests deselects selected element on pointer down when pointer doesn't hit any element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -5805,6 +5821,7 @@ exports[`regression tests deselects selected element on pointer down when pointe exports[`regression tests deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -6006,6 +6023,7 @@ exports[`regression tests deselects selected element, on pointer up, when click exports[`regression tests double click to edit a group: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -6506,6 +6524,7 @@ exports[`regression tests double click to edit a group: [end of test] number of exports[`regression tests drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -6853,6 +6872,7 @@ exports[`regression tests drags selected elements from point inside common bound exports[`regression tests draw every type of shape: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "freedraw", @@ -9091,6 +9111,7 @@ exports[`regression tests draw every type of shape: [end of test] number of rend exports[`regression tests given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -9488,6 +9509,7 @@ exports[`regression tests given a group of selected elements with an element tha exports[`regression tests given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -9763,6 +9785,7 @@ exports[`regression tests given a selected element A and a not selected element exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -9999,6 +10022,7 @@ exports[`regression tests given selected element A with lower z-index than unsel exports[`regression tests given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -10304,6 +10328,7 @@ exports[`regression tests given selected element A with lower z-index than unsel exports[`regression tests key 2 selects rectangle tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -10480,6 +10505,7 @@ exports[`regression tests key 2 selects rectangle tool: [end of test] number of exports[`regression tests key 3 selects diamond tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -10656,6 +10682,7 @@ exports[`regression tests key 3 selects diamond tool: [end of test] number of re exports[`regression tests key 4 selects ellipse tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -10832,6 +10859,7 @@ exports[`regression tests key 4 selects ellipse tool: [end of test] number of re exports[`regression tests key 5 selects arrow tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -11038,6 +11066,7 @@ exports[`regression tests key 5 selects arrow tool: [end of test] number of rend exports[`regression tests key 6 selects line tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -11244,6 +11273,7 @@ exports[`regression tests key 6 selects line tool: [end of test] number of rende exports[`regression tests key 7 selects freedraw tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "freedraw", @@ -11468,6 +11498,7 @@ exports[`regression tests key 7 selects freedraw tool: [end of test] number of r exports[`regression tests key a selects arrow tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -11674,6 +11705,7 @@ exports[`regression tests key a selects arrow tool: [end of test] number of rend exports[`regression tests key d selects diamond tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -11850,6 +11882,7 @@ exports[`regression tests key d selects diamond tool: [end of test] number of re exports[`regression tests key l selects line tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -12056,6 +12089,7 @@ exports[`regression tests key l selects line tool: [end of test] number of rende exports[`regression tests key o selects ellipse tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -12232,6 +12266,7 @@ exports[`regression tests key o selects ellipse tool: [end of test] number of re exports[`regression tests key r selects rectangle tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -12408,6 +12443,7 @@ exports[`regression tests key r selects rectangle tool: [end of test] number of exports[`regression tests key x selects freedraw tool: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "freedraw", @@ -12632,6 +12668,7 @@ exports[`regression tests key x selects freedraw tool: [end of test] number of r exports[`regression tests make a group and duplicate it: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -13425,6 +13462,7 @@ exports[`regression tests make a group and duplicate it: [end of test] number of exports[`regression tests noop interaction after undo shouldn't create history entry: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -13700,6 +13738,7 @@ exports[`regression tests noop interaction after undo shouldn't create history e exports[`regression tests pinch-to-zoom works: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -13807,6 +13846,7 @@ exports[`regression tests pinch-to-zoom works: [end of test] number of renders 1 exports[`regression tests rerenders UI on language change: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "rectangle", @@ -13912,6 +13952,7 @@ exports[`regression tests rerenders UI on language change: [end of test] number exports[`regression tests shift click on selected element should deselect it on pointer up: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -14091,6 +14132,7 @@ exports[`regression tests shift click on selected element should deselect it on exports[`regression tests shift-click to multiselect, then drag: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -14441,6 +14483,7 @@ exports[`regression tests shift-click to multiselect, then drag: [end of test] n exports[`regression tests should show fill icons when element has non transparent background: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -14657,6 +14700,7 @@ exports[`regression tests should show fill icons when element has non transparen exports[`regression tests single-clicking on a subgroup of a selected group should not alter selection: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -15568,6 +15612,7 @@ exports[`regression tests single-clicking on a subgroup of a selected group shou exports[`regression tests spacebar + drag scrolls the canvas: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -15673,6 +15718,7 @@ exports[`regression tests spacebar + drag scrolls the canvas: [end of test] numb exports[`regression tests supports nested groups: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -16486,6 +16532,7 @@ exports[`regression tests supports nested groups: [end of test] number of render exports[`regression tests switches from group of selected elements to another element on pointer down: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -16932,6 +16979,7 @@ exports[`regression tests switches from group of selected elements to another el exports[`regression tests switches selected element on pointer down: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -17255,6 +17303,7 @@ exports[`regression tests switches selected element on pointer down: [end of tes exports[`regression tests two-finger scroll works: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -17362,6 +17411,7 @@ exports[`regression tests two-finger scroll works: [end of test] number of rende exports[`regression tests undo/redo drawing an element: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", @@ -17905,6 +17955,7 @@ exports[`regression tests undo/redo drawing an element: [end of test] number of exports[`regression tests updates fontSize & fontFamily appState: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "text", @@ -18010,6 +18061,7 @@ exports[`regression tests updates fontSize & fontFamily appState: [end of test] exports[`regression tests zoom hotkeys: [end of test] appState 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", diff --git a/src/tests/packages/__snapshots__/utils.test.ts.snap b/src/tests/packages/__snapshots__/utils.test.ts.snap index 3c04b7b7..6d6154e6 100644 --- a/src/tests/packages/__snapshots__/utils.test.ts.snap +++ b/src/tests/packages/__snapshots__/utils.test.ts.snap @@ -3,6 +3,7 @@ exports[`exportToSvg with default arguments 1`] = ` Object { "activeTool": Object { + "customType": null, "lastActiveToolBeforeEraser": null, "locked": false, "type": "selection", diff --git a/src/types.ts b/src/types.ts index 19585c4a..80fd5abc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -66,6 +66,16 @@ export type BinaryFileMetadata = Omit; export type BinaryFiles = Record; +export type LastActiveToolBeforeEraser = + | { + type: typeof SHAPES[number]["value"] | "eraser"; + customType: null; + } + | { + type: "custom"; + customType: string; + } + | null; export type AppState = { isLoading: boolean; errorMessage: string | null; @@ -80,11 +90,19 @@ export type AppState = { // (e.g. text element when typing into the input) editingElement: NonDeletedExcalidrawElement | null; editingLinearElement: LinearElementEditor | null; - activeTool: { - type: typeof SHAPES[number]["value"] | "eraser"; - lastActiveToolBeforeEraser: typeof SHAPES[number]["value"] | null; - locked: boolean; - }; + activeTool: + | { + type: typeof SHAPES[number]["value"] | "eraser"; + lastActiveToolBeforeEraser: LastActiveToolBeforeEraser; + locked: boolean; + customType: null; + } + | { + type: "custom"; + customType: string; + lastActiveToolBeforeEraser: LastActiveToolBeforeEraser; + locked: boolean; + }; penMode: boolean; penDetected: boolean; exportBackground: boolean; diff --git a/src/utils.ts b/src/utils.ts index fd20f753..7cfe1309 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -11,9 +11,10 @@ import { WINDOWS_EMOJI_FALLBACK_FONT, } from "./constants"; import { FontFamilyValues, FontString } from "./element/types"; -import { AppState, DataURL, Zoom } from "./types"; +import { AppState, DataURL, LastActiveToolBeforeEraser, Zoom } from "./types"; import { unstable_batchedUpdates } from "react-dom"; import { isDarwin } from "./keys"; +import { SHAPES } from "./shapes"; let mockDateTime: string | null = null; @@ -207,6 +208,32 @@ export const removeSelection = () => { export const distance = (x: number, y: number) => Math.abs(x - y); +export const updateActiveTool = ( + appState: Pick, + data: ( + | { type: typeof SHAPES[number]["value"] | "eraser" } + | { type: "custom"; customType: string } + ) & { lastActiveToolBeforeEraser?: LastActiveToolBeforeEraser }, +): AppState["activeTool"] => { + if (data.type === "custom") { + return { + ...appState.activeTool, + type: "custom", + customType: data.customType, + }; + } + + return { + ...appState.activeTool, + lastActiveToolBeforeEraser: + data.lastActiveToolBeforeEraser === undefined + ? appState.activeTool.lastActiveToolBeforeEraser + : data.lastActiveToolBeforeEraser, + type: data.type, + customType: null, + }; +}; + export const resetCursor = (canvas: HTMLCanvasElement | null) => { if (canvas) { canvas.style.cursor = "";