fix: binding text to non-bindable containers and not always preferring selection (#4655)
This commit is contained in:
parent
8e26d5b500
commit
6d0716eb6b
@ -127,6 +127,7 @@ import {
|
|||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isLinearElementType,
|
isLinearElementType,
|
||||||
|
isTextBindableContainer,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
@ -140,6 +141,7 @@ import {
|
|||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getCenter, getDistance } from "../gesture";
|
import { getCenter, getDistance } from "../gesture";
|
||||||
import {
|
import {
|
||||||
@ -167,7 +169,7 @@ import { renderScene } from "../renderer";
|
|||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
getElementContainingPosition,
|
getTextBindableContainerAtPosition,
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
@ -238,7 +240,7 @@ import {
|
|||||||
bindTextToShapeAfterDuplication,
|
bindTextToShapeAfterDuplication,
|
||||||
getApproxMinLineHeight,
|
getApproxMinLineHeight,
|
||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElementId,
|
getBoundTextElement,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
import {
|
import {
|
||||||
@ -2157,28 +2159,40 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
window.devicePixelRatio,
|
window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
||||||
|
let container: ExcalidrawTextContainer | null = null;
|
||||||
|
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
this.scene.getElements(),
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedElements.length === 1) {
|
||||||
|
if (isTextElement(selectedElements[0])) {
|
||||||
|
existingTextElement = selectedElements[0];
|
||||||
|
} else if (isTextBindableContainer(selectedElements[0])) {
|
||||||
|
container = selectedElements[0];
|
||||||
|
existingTextElement = getBoundTextElement(container);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
existingTextElement =
|
||||||
|
existingTextElement ?? this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
|
|
||||||
// bind to container when shouldBind is true or
|
// bind to container when shouldBind is true or
|
||||||
// clicked on center of container
|
// clicked on center of container
|
||||||
const container =
|
if (
|
||||||
shouldBind || parentCenterPosition
|
!container &&
|
||||||
? getElementContainingPosition(
|
!existingTextElement &&
|
||||||
|
(shouldBind || parentCenterPosition)
|
||||||
|
) {
|
||||||
|
container = getTextBindableContainerAtPosition(
|
||||||
this.scene.getElements().filter((ele) => !isTextElement(ele)),
|
this.scene.getElements().filter((ele) => !isTextElement(ele)),
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
)
|
);
|
||||||
: null;
|
|
||||||
|
|
||||||
let existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
|
||||||
|
|
||||||
// consider bounded text element if container present
|
|
||||||
if (container) {
|
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
|
||||||
if (boundTextElementId) {
|
|
||||||
existingTextElement = this.scene.getElement(
|
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElement;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!existingTextElement && container) {
|
if (!existingTextElement && container) {
|
||||||
const fontString = {
|
const fontString = {
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize: this.state.currentItemFontSize,
|
||||||
@ -5432,7 +5446,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement | null,
|
||||||
scale: number,
|
scale: number,
|
||||||
) {
|
) {
|
||||||
const elementClickedInside = getElementContainingPosition(
|
const elementClickedInside = getTextBindableContainerAtPosition(
|
||||||
this.scene
|
this.scene
|
||||||
.getElementsIncludingDeleted()
|
.getElementsIncludingDeleted()
|
||||||
.filter((element) => !isTextElement(element)),
|
.filter((element) => !isTextElement(element)),
|
||||||
|
@ -3,10 +3,9 @@ import { updateBoundElements } from "./binding";
|
|||||||
import { getCommonBounds } from "./bounds";
|
import { getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
import { AppState, PointerDownState } from "../types";
|
import { AppState, PointerDownState } from "../types";
|
||||||
import { getBoundTextElementId } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
@ -39,16 +38,14 @@ export const dragSelectedElements = (
|
|||||||
// container is part of a group, but we're dragging the container directly
|
// container is part of a group, but we're dragging the container directly
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
||||||
) {
|
) {
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const textElement = getBoundTextElement(element);
|
||||||
if (boundTextElementId) {
|
if (textElement) {
|
||||||
const textElement =
|
|
||||||
Scene.getScene(element)!.getElement(boundTextElementId);
|
|
||||||
updateElementCoords(
|
updateElementCoords(
|
||||||
lockDirection,
|
lockDirection,
|
||||||
distanceX,
|
distanceX,
|
||||||
distanceY,
|
distanceY,
|
||||||
pointerDownState,
|
pointerDownState,
|
||||||
textElement!,
|
textElement,
|
||||||
offset,
|
offset,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,6 @@ import { getElementAbsoluteCoords } from ".";
|
|||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import { getContainerElement, measureText, wrapText } from "./textElement";
|
import { getContainerElement, measureText, wrapText } from "./textElement";
|
||||||
import { isBoundToContainer } from "./typeChecks";
|
|
||||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
@ -221,8 +220,7 @@ const getAdjustedDimensions = (
|
|||||||
|
|
||||||
// make sure container dimensions are set properly when
|
// make sure container dimensions are set properly when
|
||||||
// text editor overflows beyond viewport dimensions
|
// text editor overflows beyond viewport dimensions
|
||||||
if (isBoundToContainer(element)) {
|
if (container) {
|
||||||
const container = getContainerElement(element)!;
|
|
||||||
let height = container.height;
|
let height = container.height;
|
||||||
let width = container.width;
|
let width = container.width;
|
||||||
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
@ -12,6 +11,7 @@ import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
|||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { isTextElement } from ".";
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
element: ExcalidrawTextElement,
|
element: ExcalidrawTextElement,
|
||||||
@ -79,22 +79,24 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElementId = getBoundTextElementId(element);
|
||||||
|
|
||||||
if (boundTextElementId) {
|
if (boundTextElementId) {
|
||||||
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!;
|
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
|
||||||
mutateElement(
|
if (newTextElementId) {
|
||||||
sceneElementMap.get(newElementId) as ExcalidrawBindableElement,
|
const newContainer = sceneElementMap.get(newElementId);
|
||||||
{
|
if (newContainer) {
|
||||||
|
mutateElement(newContainer, {
|
||||||
boundElements: element.boundElements?.concat({
|
boundElements: element.boundElements?.concat({
|
||||||
type: "text",
|
type: "text",
|
||||||
id: newTextElementId,
|
id: newTextElementId,
|
||||||
}),
|
}),
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
mutateElement(
|
const newTextElement = sceneElementMap.get(newTextElementId);
|
||||||
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement,
|
if (newTextElement && isTextElement(newTextElement)) {
|
||||||
{
|
mutateElement(newTextElement, {
|
||||||
containerId: newElementId,
|
containerId: newContainer ? newElementId : null,
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -440,7 +442,10 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||||
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
|
return container?.boundElements?.length
|
||||||
|
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
|
||||||
|
null
|
||||||
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
||||||
|
@ -12,6 +12,8 @@ import {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import * as textElementUtils from "./textElement";
|
import * as textElementUtils from "./textElement";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { mutateElement } from "./mutateElement";
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@ -19,7 +21,201 @@ const tab = " ";
|
|||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("Test unbounded text", () => {
|
describe("start text editing", () => {
|
||||||
|
const { h } = window;
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
h.elements = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer editing selected text element (non-bindable container present)", async () => {
|
||||||
|
const line = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
width: 100,
|
||||||
|
height: 0,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 0],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: line.width / 2 - textSize / 2,
|
||||||
|
y: -textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
});
|
||||||
|
h.elements = [text, line];
|
||||||
|
|
||||||
|
API.setSelectedElements([text]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(
|
||||||
|
(h.state.editingElement as ExcalidrawTextElement).containerId,
|
||||||
|
).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer editing selected text element (bindable container present)", async () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 100,
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
|
||||||
|
const boundText = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundText2 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [container, boundText, boundText2];
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setSelectedElements([boundText2]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(boundText2.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create bound text on ENTER if text exists at container center", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 100,
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [container, text];
|
||||||
|
|
||||||
|
API.setSelectedElements([container]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
width: 100,
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
const textSize = 20;
|
||||||
|
|
||||||
|
const boundText = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const boundText2 = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: container.width / 2 - textSize / 2,
|
||||||
|
y: container.height / 2 - textSize / 2,
|
||||||
|
width: textSize,
|
||||||
|
height: textSize,
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [container, boundText, boundText2];
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setSelectedElements([container]);
|
||||||
|
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.state.editingElement?.id).toBe(boundText.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should edit text under cursor when clicked with text tool", () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: 60,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [text];
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
expect(editor).not.toBe(null);
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should edit text under cursor when double-clicked with selection tool", () => {
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "ola",
|
||||||
|
x: 60,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
h.elements = [text];
|
||||||
|
UI.clickTool("selection");
|
||||||
|
|
||||||
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
expect(editor).not.toBe(null);
|
||||||
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test container-unbound text", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
@ -235,7 +431,7 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test bounded text", () => {
|
describe("Test container-bound text", () => {
|
||||||
let rectangle: any;
|
let rectangle: any;
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
@ -315,6 +511,39 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shouldn't bind to non-text-bindable containers", async () => {
|
||||||
|
const line = API.createElement({
|
||||||
|
type: "line",
|
||||||
|
width: 100,
|
||||||
|
height: 0,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[100, 0],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
h.elements = [line];
|
||||||
|
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: {
|
||||||
|
value: "Hello World!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
|
expect(line.boundElements).toBe(null);
|
||||||
|
expect(h.elements[1].type).toBe("text");
|
||||||
|
expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
|
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);
|
expect(h.elements.length).toBe(1);
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isGenericElement = (
|
export const isGenericElement = (
|
||||||
@ -91,7 +92,9 @@ export const isBindableElement = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isTextBindableContainer = (element: ExcalidrawElement | null) => {
|
export const isTextBindableContainer = (
|
||||||
|
element: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawTextContainer => {
|
||||||
return (
|
return (
|
||||||
element != null &&
|
element != null &&
|
||||||
(element.type === "rectangle" ||
|
(element.type === "rectangle" ||
|
||||||
|
@ -135,8 +135,14 @@ export type ExcalidrawBindableElement =
|
|||||||
| ExcalidrawTextElement
|
| ExcalidrawTextElement
|
||||||
| ExcalidrawImageElement;
|
| ExcalidrawImageElement;
|
||||||
|
|
||||||
|
export type ExcalidrawTextContainer =
|
||||||
|
| ExcalidrawRectangleElement
|
||||||
|
| ExcalidrawDiamondElement
|
||||||
|
| ExcalidrawEllipseElement
|
||||||
|
| ExcalidrawImageElement;
|
||||||
|
|
||||||
export type ExcalidrawTextElementWithContainer = {
|
export type ExcalidrawTextElementWithContainer = {
|
||||||
containerId: ExcalidrawGenericElement["id"];
|
containerId: ExcalidrawTextContainer["id"];
|
||||||
} & ExcalidrawTextElement;
|
} & ExcalidrawTextElement;
|
||||||
|
|
||||||
export type PointBinding = {
|
export type PointBinding = {
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import {
|
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
|
||||||
GroupId,
|
|
||||||
ExcalidrawElement,
|
|
||||||
NonDeleted,
|
|
||||||
ExcalidrawTextElementWithContainer,
|
|
||||||
} from "./element/types";
|
|
||||||
import { AppState } from "./types";
|
import { AppState } from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElementId } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
import Scene from "./scene/Scene";
|
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
@ -182,13 +176,10 @@ export const getMaximumGroups = (
|
|||||||
|
|
||||||
const currentGroupMembers = groups.get(groupId) || [];
|
const currentGroupMembers = groups.get(groupId) || [];
|
||||||
|
|
||||||
// Include bounded text if present when grouping
|
// Include bound text if present when grouping
|
||||||
const boundTextElementId = getBoundTextElementId(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElementId) {
|
if (boundTextElement) {
|
||||||
const textElement = Scene.getScene(element)!.getElement(
|
currentGroupMembers.push(boundTextElement);
|
||||||
boundTextElementId,
|
|
||||||
) as ExcalidrawTextElementWithContainer;
|
|
||||||
currentGroupMembers.push(textElement);
|
|
||||||
}
|
}
|
||||||
groups.set(groupId, [...currentGroupMembers, element]);
|
groups.set(groupId, [...currentGroupMembers, element]);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
import { getElementAbsoluteCoords } from "../element";
|
||||||
|
import { isTextBindableContainer } from "../element/typeChecks";
|
||||||
|
|
||||||
export const hasBackground = (type: string) =>
|
export const hasBackground = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
@ -72,11 +74,11 @@ export const getElementsAtPosition = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElementContainingPosition = (
|
export const getTextBindableContainerAtPosition = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
) => {
|
): ExcalidrawTextContainer | null => {
|
||||||
let hitElement = null;
|
let hitElement = null;
|
||||||
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
// We need to to hit testing from front (end of the array) to back (beginning of the array)
|
||||||
for (let index = elements.length - 1; index >= 0; --index) {
|
for (let index = elements.length - 1; index >= 0; --index) {
|
||||||
@ -89,5 +91,5 @@ export const getElementContainingPosition = (
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hitElement;
|
return isTextBindableContainer(hitElement) ? hitElement : null;
|
||||||
};
|
};
|
||||||
|
@ -14,7 +14,7 @@ export {
|
|||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
getElementContainingPosition,
|
getTextBindableContainerAtPosition,
|
||||||
hasText,
|
hasText,
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
} from "./comparisons";
|
} from "./comparisons";
|
||||||
|
@ -152,6 +152,7 @@ describe("element binding", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
|
@ -9,7 +9,7 @@ Object {
|
|||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 0,
|
"height": 100,
|
||||||
"id": "id-arrow01",
|
"id": "id-arrow01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
@ -21,8 +21,8 @@ Object {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
Array [
|
Array [
|
||||||
0,
|
100,
|
||||||
0,
|
100,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -37,7 +37,7 @@ Object {
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": 0,
|
"versionNonce": 0,
|
||||||
"width": 0,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
@ -180,7 +180,7 @@ Object {
|
|||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 0,
|
"height": 100,
|
||||||
"id": "id-line01",
|
"id": "id-line01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
@ -192,8 +192,8 @@ Object {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
Array [
|
Array [
|
||||||
0,
|
100,
|
||||||
0,
|
100,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -208,7 +208,7 @@ Object {
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": 0,
|
"versionNonce": 0,
|
||||||
"width": 0,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
@ -223,7 +223,7 @@ Object {
|
|||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "hachure",
|
"fillStyle": "hachure",
|
||||||
"groupIds": Array [],
|
"groupIds": Array [],
|
||||||
"height": 0,
|
"height": 100,
|
||||||
"id": "id-draw01",
|
"id": "id-draw01",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
"lastCommittedPoint": null,
|
"lastCommittedPoint": null,
|
||||||
@ -235,8 +235,8 @@ Object {
|
|||||||
0,
|
0,
|
||||||
],
|
],
|
||||||
Array [
|
Array [
|
||||||
0,
|
100,
|
||||||
0,
|
100,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@ -251,7 +251,7 @@ Object {
|
|||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"versionNonce": 0,
|
"versionNonce": 0,
|
||||||
"width": 0,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import util from "util";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import { getMimeType } from "../../data/blob";
|
import { getMimeType } from "../../data/blob";
|
||||||
import { newFreeDrawElement } from "../../element/newElement";
|
import { newFreeDrawElement } from "../../element/newElement";
|
||||||
|
import { Point } from "../../types";
|
||||||
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
|
||||||
@ -98,6 +99,7 @@ export class API {
|
|||||||
containerId?: T extends "text"
|
containerId?: T extends "text"
|
||||||
? ExcalidrawTextElement["containerId"]
|
? ExcalidrawTextElement["containerId"]
|
||||||
: never;
|
: never;
|
||||||
|
points?: T extends "arrow" | "line" ? readonly Point[] : never;
|
||||||
}): T extends "arrow" | "line"
|
}): T extends "arrow" | "line"
|
||||||
? ExcalidrawLinearElement
|
? ExcalidrawLinearElement
|
||||||
: T extends "freedraw"
|
: T extends "freedraw"
|
||||||
@ -158,10 +160,13 @@ export class API {
|
|||||||
case "arrow":
|
case "arrow":
|
||||||
case "line":
|
case "line":
|
||||||
element = newLinearElement({
|
element = newLinearElement({
|
||||||
|
...base,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
type: type as "arrow" | "line",
|
type: type as "arrow" | "line",
|
||||||
startArrowhead: null,
|
startArrowhead: null,
|
||||||
endArrowhead: null,
|
endArrowhead: null,
|
||||||
...base,
|
points: rest.points ?? [],
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user