diff --git a/src/actions/actionLinearEditor.ts b/src/actions/actionLinearEditor.ts new file mode 100644 index 00000000..ff1435f8 --- /dev/null +++ b/src/actions/actionLinearEditor.ts @@ -0,0 +1,49 @@ +import { getNonDeletedElements } from "../element"; +import { LinearElementEditor } from "../element/linearElementEditor"; +import { isLinearElement } from "../element/typeChecks"; +import { ExcalidrawLinearElement } from "../element/types"; +import { getSelectedElements } from "../scene"; +import { register } from "./register"; + +export const actionToggleLinearEditor = register({ + name: "toggleLinearEditor", + trackEvent: { + category: "element", + }, + contextItemPredicate: (elements, appState) => { + const selectedElements = getSelectedElements(elements, appState); + if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { + return true; + } + return false; + }, + perform(elements, appState, _, app) { + const selectedElement = getSelectedElements( + getNonDeletedElements(elements), + appState, + true, + )[0] as ExcalidrawLinearElement; + + const editingLinearElement = + appState.editingLinearElement?.elementId === selectedElement.id + ? null + : new LinearElementEditor(selectedElement, app.scene); + return { + appState: { + ...appState, + editingLinearElement, + }, + commitToHistory: false, + }; + }, + contextItemLabel: (elements, appState) => { + const selectedElement = getSelectedElements( + getNonDeletedElements(elements), + appState, + true, + )[0] as ExcalidrawLinearElement; + return appState.editingLinearElement?.elementId === selectedElement.id + ? "labels.lineEditor.exit" + : "labels.lineEditor.edit"; + }, +}); diff --git a/src/actions/index.ts b/src/actions/index.ts index d8ec6763..eea4faf7 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -85,3 +85,4 @@ export { actionToggleStats } from "./actionToggleStats"; export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionLink } from "../element/Hyperlink"; export { actionToggleLock } from "./actionToggleLock"; +export { actionToggleLinearEditor } from "./actionLinearEditor"; diff --git a/src/actions/types.ts b/src/actions/types.ts index 098c69ad..ecde6540 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -111,7 +111,8 @@ export type ActionName = | "hyperlink" | "eraser" | "bindText" - | "toggleLock"; + | "toggleLock" + | "toggleLinearEditor"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index d3149f60..739d0ae5 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -34,6 +34,7 @@ import { actionUngroup, actionLink, actionToggleLock, + actionToggleLinearEditor, } from "../actions"; import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { ActionManager } from "../actions/manager"; @@ -5876,6 +5877,12 @@ class App extends React.Component { this.actionManager.getAppState(), ); + const mayBeAllowToggleLineEditing = + actionToggleLinearEditor.contextItemPredicate( + this.actionManager.getElementsIncludingDeleted(), + this.actionManager.getAppState(), + ); + const separator = "separator"; const elements = this.scene.getNonDeletedElements(); @@ -6017,6 +6024,7 @@ class App extends React.Component { maybeFlipHorizontal && actionFlipHorizontal, maybeFlipVertical && actionFlipVertical, (maybeFlipHorizontal || maybeFlipVertical) && separator, + mayBeAllowToggleLineEditing && actionToggleLinearEditor, actionLink.contextItemPredicate(elements, this.state) && actionLink, actionDuplicateSelection, actionToggleLock, diff --git a/src/locales/en.json b/src/locales/en.json index 36dd36fc..c11d0bc7 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -114,6 +114,11 @@ "create": "Create link", "label": "Link" }, + "lineEditor": { + "edit": "Edit line", + "exit": "Exit line editor" + }, + "elementLock": { "lock": "Lock", "unlock": "Unlock", diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index dccd6f87..4b3f795c 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -5,11 +5,12 @@ import { centerPoint } from "../math"; import { reseed } from "../random"; import * as Renderer from "../renderer/renderScene"; import { Keyboard, Pointer } from "./helpers/ui"; -import { screen, render, fireEvent } from "./test-utils"; +import { screen, render, fireEvent, GlobalTestState } from "./test-utils"; import { API } from "../tests/helpers/api"; import { Point } from "../types"; import { KEYS } from "../keys"; import { LinearElementEditor } from "../element/linearElementEditor"; +import { queryByText } from "@testing-library/react"; const renderScene = jest.spyOn(Renderer, "renderScene"); @@ -150,6 +151,42 @@ describe(" Test Linear Elements", () => { `); }); + it("should allow entering and exiting line editor via context menu", () => { + createTwoPointerLinearElement("line"); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: midpoint[0], + clientY: midpoint[1], + }); + // Enter line editor + let contextMenu = document.querySelector(".context-menu"); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: midpoint[0], + clientY: midpoint[1], + }); + fireEvent.click(queryByText(contextMenu as HTMLElement, "Edit line")!); + + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + + // Exiting line editor + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: midpoint[0], + clientY: midpoint[1], + }); + contextMenu = document.querySelector(".context-menu"); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: midpoint[0], + clientY: midpoint[1], + }); + fireEvent.click( + queryByText(contextMenu as HTMLElement, "Exit line editor")!, + ); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + }); + describe("Inside editor", () => { it("should allow dragging line from midpoint in 2 pointer lines", async () => { createTwoPointerLinearElement("line");