diff --git a/src/actions/actionUnbindText.tsx b/src/actions/actionUnbindText.tsx new file mode 100644 index 00000000..1c116ebd --- /dev/null +++ b/src/actions/actionUnbindText.tsx @@ -0,0 +1,44 @@ +import { getNonDeletedElements } from "../element"; +import { mutateElement } from "../element/mutateElement"; +import { getBoundTextElement, measureText } from "../element/textElement"; +import { ExcalidrawTextElement } from "../element/types"; +import { getSelectedElements } from "../scene"; +import { getFontString } from "../utils"; +import { register } from "./register"; + +export const actionUnbindText = register({ + name: "unbindText", + contextItemLabel: "labels.unbindText", + perform: (elements, appState) => { + const selectedElements = getSelectedElements( + getNonDeletedElements(elements), + appState, + ); + selectedElements.forEach((element) => { + const boundTextElement = getBoundTextElement(element); + if (boundTextElement) { + const { width, height, baseline } = measureText( + boundTextElement.originalText, + getFontString(boundTextElement), + ); + mutateElement(boundTextElement as ExcalidrawTextElement, { + containerId: null, + width, + height, + baseline, + text: boundTextElement.originalText, + }); + mutateElement(element, { + boundElements: element.boundElements?.filter( + (ele) => ele.id !== boundTextElement.id, + ), + }); + } + }); + return { + elements, + appState, + commitToHistory: true, + }; + }, +}); diff --git a/src/actions/index.ts b/src/actions/index.ts index f37c7842..887ea2e2 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -80,3 +80,4 @@ export { actionToggleGridMode } from "./actionToggleGridMode"; export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleStats } from "./actionToggleStats"; +export { actionUnbindText } from "./actionUnbindText"; diff --git a/src/actions/types.ts b/src/actions/types.ts index bfca8a7f..e9569379 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -103,7 +103,8 @@ export type ActionName = | "exportWithDarkMode" | "toggleTheme" | "increaseFontSize" - | "decreaseFontSize"; + | "decreaseFontSize" + | "unbindText"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/components/App.tsx b/src/components/App.tsx index 48157d2f..64fdea9c 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -26,6 +26,7 @@ import { actionToggleGridMode, actionToggleStats, actionToggleZenMode, + actionUnbindText, actionUngroup, } from "../actions"; import { createRedoAction, createUndoAction } from "../actions/actionHistory"; @@ -5031,6 +5032,10 @@ class App extends React.Component { }); } } else if (type === "element") { + const elementsWithUnbindedText = getSelectedElements( + elements, + this.state, + ).some((element) => !hasBoundTextElement(element)); if (this.state.viewModeEnabled) { ContextMenu.push({ options: [navigator.clipboard && actionCopy, ...options], @@ -5064,6 +5069,7 @@ class App extends React.Component { actionPasteStyles, separator, maybeGroupAction && actionGroup, + !elementsWithUnbindedText && actionUnbindText, maybeUngroupAction && actionUngroup, (maybeGroupAction || maybeUngroupAction) && separator, actionAddToLibrary, diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 1de918ce..1c0c2b8b 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -175,7 +175,6 @@ export const measureText = ( container.style.whiteSpace = "pre"; container.style.font = font; container.style.minHeight = "1em"; - if (maxWidth) { const lineHeight = getApproxLineHeight(font); container.style.width = `${String(maxWidth)}px`; diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index ffe6ea17..0fe105ba 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -1,9 +1,11 @@ import ReactDOM from "react-dom"; import ExcalidrawApp from "../excalidraw-app"; -import { render, screen } from "../tests/test-utils"; +import { GlobalTestState, render, screen } from "../tests/test-utils"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { CODES, KEYS } from "../keys"; import { fireEvent } from "../tests/test-utils"; +import { queryByText } from "@testing-library/react"; + import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants"; import { ExcalidrawTextElement, @@ -472,5 +474,47 @@ describe("textWysiwyg", () => { expect(text.height).toBe(APPROX_LINE_HEIGHT); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); }); + + it("should unbind bound text when unbind action from context menu is triggred", async () => { + expect(h.elements.length).toBe(1); + expect(h.elements[0].id).toBe(rectangle.id); + + Keyboard.withModifierKeys({}, () => { + Keyboard.keyPress(KEYS.ENTER); + }); + + expect(h.elements.length).toBe(2); + + const text = h.elements[1] as ExcalidrawTextElementWithContainer; + expect(text.containerId).toBe(rectangle.id); + + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + + await new Promise((r) => setTimeout(r, 0)); + + fireEvent.change(editor, { target: { value: "Hello World!" } }); + editor.blur(); + expect(rectangle.boundElements).toStrictEqual([ + { id: text.id, type: "text" }, + ]); + mouse.reset(); + UI.clickTool("selection"); + mouse.clickAt(10, 20); + mouse.down(); + mouse.up(); + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: 20, + clientY: 30, + }); + const contextMenu = document.querySelector(".context-menu"); + fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!); + expect(h.elements[0].boundElements).toEqual([]); + expect((h.elements[1] as ExcalidrawTextElement).containerId).toEqual( + null, + ); + }); }); }); diff --git a/src/locales/en.json b/src/locales/en.json index 8a240907..12b89e2f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -104,7 +104,8 @@ "personalLib": "Personal Library", "excalidrawLib": "Excalidraw Library", "decreaseFontSize": "Decrease font size", - "increaseFontSize": "Increase font size" + "increaseFontSize": "Increase font size", + "unbindText": "Unbind text" }, "buttons": { "clearReset": "Reset the canvas",