+
{children}
);
diff --git a/src/element/textElement.ts b/src/element/textElement.ts
index b86f936ca..cef793b35 100644
--- a/src/element/textElement.ts
+++ b/src/element/textElement.ts
@@ -264,10 +264,16 @@ export const handleBindTextResize = (
}
};
-const computeBoundTextPosition = (
+export const computeBoundTextPosition = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
+ if (isArrowElement(container)) {
+ return LinearElementEditor.getBoundTextElementPosition(
+ container,
+ boundTextElement,
+ );
+ }
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container);
diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx
index cb852cd1c..c55a9befa 100644
--- a/src/element/textWysiwyg.test.tsx
+++ b/src/element/textWysiwyg.test.tsx
@@ -740,6 +740,45 @@ describe("textWysiwyg", () => {
expect(rectangle.boundElements).toBe(null);
});
+ it("should bind text to container when triggered via context menu", async () => {
+ expect(h.elements.length).toBe(1);
+ expect(h.elements[0].id).toBe(rectangle.id);
+
+ UI.clickTool("text");
+ mouse.clickAt(20, 30);
+ const editor = document.querySelector(
+ ".excalidraw-textEditorContainer > textarea",
+ ) as HTMLTextAreaElement;
+
+ fireEvent.change(editor, {
+ target: {
+ value: "Excalidraw is an opensource virtual collaborative whiteboard",
+ },
+ });
+
+ editor.dispatchEvent(new Event("input"));
+ await new Promise((cb) => setTimeout(cb, 0));
+ expect(h.elements.length).toBe(2);
+ expect(h.elements[1].type).toBe("text");
+
+ API.setSelectedElements([h.elements[0], h.elements[1]]);
+ fireEvent.contextMenu(GlobalTestState.canvas, {
+ button: 2,
+ clientX: 20,
+ clientY: 30,
+ });
+ const contextMenu = document.querySelector(".context-menu");
+ fireEvent.click(
+ queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
+ );
+ const text = h.elements[1] as ExcalidrawTextElementWithContainer;
+ expect(rectangle.boundElements).toStrictEqual([
+ { id: h.elements[1].id, type: "text" },
+ ]);
+ expect(text.containerId).toBe(rectangle.id);
+ expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE);
+ });
+
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
expect(h.elements.length).toBe(1);
diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md
index be79fd8e4..26d2c7f9b 100644
--- a/src/packages/excalidraw/CHANGELOG.md
+++ b/src/packages/excalidraw/CHANGELOG.md
@@ -33,6 +33,10 @@ For more details refer to the [docs](https://docs.excalidraw.com)
- The optional parameter `refreshDimensions` in [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) has been removed and can be enabled via `opts`
+### Fixes
+
+- Exporting labelled arrows via export utils [#6443](https://github.com/excalidraw/excalidraw/pull/6443)
+
## 0.14.2 (2023-02-01)
### Features
diff --git a/src/packages/utils.ts b/src/packages/utils.ts
index d81995080..5161e0cab 100644
--- a/src/packages/utils.ts
+++ b/src/packages/utils.ts
@@ -5,7 +5,6 @@ import {
import { getDefaultAppState } from "../appState";
import { AppState, BinaryFiles } from "../types";
import { ExcalidrawElement, NonDeleted } from "../element/types";
-import { getNonDeletedElements } from "../element";
import { restore } from "../data/restore";
import { MIME_TYPES } from "../constants";
import { encodePngMetadata } from "../data/image";
@@ -15,6 +14,7 @@ import {
copyTextToSystemClipboard,
copyToClipboard,
} from "../clipboard";
+import Scene from "../scene/Scene";
export { MIME_TYPES };
@@ -44,9 +44,17 @@ export const exportToCanvas = ({
null,
null,
);
+ // The helper methods getContainerElement and getBoundTextElement are
+ // dependent on Scene which will not be available
+ // when these pure utils are called outside Excalidraw or even if called
+ // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted
+ // hence initailizing a new scene with the elements
+ // so its always available to helper methods
+ const scene = new Scene();
+ scene.replaceAllElements(restoredElements);
const { exportBackground, viewBackgroundColor } = restoredAppState;
return _exportToCanvas(
- getNonDeletedElements(restoredElements),
+ scene.getNonDeletedElements(),
{ ...restoredAppState, offsetTop: 0, offsetLeft: 0, width: 0, height: 0 },
files || {},
{ exportBackground, exportPadding, viewBackgroundColor },
@@ -114,8 +122,18 @@ export const exportToBlob = async (
};
}
- const canvas = await exportToCanvas(opts);
-
+ // The helper methods getContainerElement and getBoundTextElement are
+ // dependent on Scene which will not be available
+ // when these pure utils are called outside Excalidraw or even if called
+ // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted
+ // hence initailizing a new scene with the elements
+ // so its always available to helper methods
+ const scene = new Scene();
+ scene.replaceAllElements(opts.elements);
+ const canvas = await exportToCanvas({
+ ...opts,
+ elements: scene.getNonDeletedElements(),
+ });
quality = quality ? quality : /image\/jpe?g/.test(mimeType) ? 0.92 : 0.8;
return new Promise((resolve, reject) => {
@@ -132,7 +150,7 @@ export const exportToBlob = async (
blob = await encodePngMetadata({
blob,
metadata: serializeAsJSON(
- opts.elements,
+ scene.getNonDeletedElements(),
opts.appState,
opts.files || {},
"local",
@@ -160,8 +178,16 @@ export const exportToSvg = async ({
null,
null,
);
+ // The helper methods getContainerElement and getBoundTextElement are
+ // dependent on Scene which will not be available
+ // when these pure utils are called outside Excalidraw or even if called
+ // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted
+ // hence initailizing a new scene with the elements
+ // so its always available to helper methods
+ const scene = new Scene();
+ scene.replaceAllElements(restoredElements);
return _exportToSvg(
- getNonDeletedElements(restoredElements),
+ scene.getNonDeletedElements(),
{
...restoredAppState,
exportPadding,
@@ -177,6 +203,14 @@ export const exportToClipboard = async (
type: "png" | "svg" | "json";
},
) => {
+ // The helper methods getContainerElement and getBoundTextElement are
+ // dependent on Scene which will not be available
+ // when these pure utils are called outside Excalidraw or even if called
+ // from inside Excalidraw when Scene isn't available eg when using library items from store, as a result the element cannot be extracted
+ // hence initailizing a new scene with the elements
+ // so its always available to helper methods
+ const scene = new Scene();
+ scene.replaceAllElements(opts.elements);
if (opts.type === "svg") {
const svg = await exportToSvg(opts);
await copyTextToSystemClipboard(svg.outerHTML);
@@ -191,7 +225,7 @@ export const exportToClipboard = async (
...getDefaultAppState(),
...opts.appState,
};
- await copyToClipboard(opts.elements, appState, opts.files);
+ await copyToClipboard(scene.getNonDeletedElements(), appState, opts.files);
} else {
throw new Error("Invalid export type");
}
diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx
index ac4d801bc..15fd105ec 100644
--- a/src/tests/linearElementEditor.test.tsx
+++ b/src/tests/linearElementEditor.test.tsx
@@ -23,7 +23,7 @@ import {
getMaxContainerWidth,
} from "../element/textElement";
import * as textElementUtils from "../element/textElement";
-import { ROUNDNESS } from "../constants";
+import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
const renderScene = jest.spyOn(Renderer, "renderScene");
@@ -1191,5 +1191,62 @@ describe("Test Linear Elements", () => {
expect(queryByTestId(container, "align-horizontal-center")).toBeNull();
expect(queryByTestId(container, "align-right")).toBeNull();
});
+
+ it("should update label coords when a label binded via context menu is unbinded", async () => {
+ createTwoPointerLinearElement("arrow");
+ const text = API.createElement({
+ type: "text",
+ text: "Hello Excalidraw",
+ });
+ expect(text.x).toBe(0);
+ expect(text.y).toBe(0);
+
+ h.elements = [h.elements[0], text];
+
+ const container = h.elements[0];
+ API.setSelectedElements([container, text]);
+ fireEvent.contextMenu(GlobalTestState.canvas, {
+ button: 2,
+ clientX: 20,
+ clientY: 30,
+ });
+ let contextMenu = document.querySelector(".context-menu");
+
+ fireEvent.click(
+ queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
+ );
+ expect(container.boundElements).toStrictEqual([
+ { id: h.elements[1].id, type: "text" },
+ ]);
+ expect(text.containerId).toBe(container.id);
+ expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE);
+
+ mouse.reset();
+ mouse.clickAt(
+ container.x + container.width / 2,
+ container.y + container.height / 2,
+ );
+ mouse.down();
+ mouse.up();
+ API.setSelectedElements([h.elements[0], h.elements[1]]);
+
+ fireEvent.contextMenu(GlobalTestState.canvas, {
+ button: 2,
+ clientX: 20,
+ clientY: 30,
+ });
+ contextMenu = document.querySelector(".context-menu");
+ fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
+ expect(container.boundElements).toEqual([]);
+ expect(text).toEqual(
+ expect.objectContaining({
+ containerId: null,
+ width: 160,
+ height: 25,
+ x: -40,
+ y: 7.5,
+ }),
+ );
+ });
});
});