fix: Always bind to container selected by user (#5880)
* fix: Always bind to container selected by user * Don't bind to container when using text tool * adjust z-index for bound text * fix * Add spec * Add test * Allow double click on transparent container and add spec * fix spec * adjust z-index only when binding * update index * fix * add index check * Update src/scene/Scene.ts Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
1f117995d9
commit
d2181847be
@ -133,7 +133,6 @@ import {
|
|||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isLinearElementType,
|
isLinearElementType,
|
||||||
isTextBindableContainer,
|
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
ExcalidrawBindableElement,
|
ExcalidrawBindableElement,
|
||||||
@ -175,7 +174,6 @@ import { renderScene } from "../renderer/renderScene";
|
|||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
getTextBindableContainerAtPosition,
|
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
getElementsWithinSelection,
|
getElementsWithinSelection,
|
||||||
getNormalizedZoom,
|
getNormalizedZoom,
|
||||||
@ -255,6 +253,7 @@ import {
|
|||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
|
getTextBindableContainerAtPosition,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||||
@ -1990,10 +1989,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isTextElement(selectedElement) ||
|
isTextElement(selectedElement) ||
|
||||||
isValidTextContainer(selectedElement)
|
isValidTextContainer(selectedElement)
|
||||||
) {
|
) {
|
||||||
|
let container;
|
||||||
|
if (!isTextElement(selectedElement)) {
|
||||||
|
container = selectedElement as ExcalidrawTextContainer;
|
||||||
|
}
|
||||||
this.startTextEditing({
|
this.startTextEditing({
|
||||||
sceneX: selectedElement.x + selectedElement.width / 2,
|
sceneX: selectedElement.x + selectedElement.width / 2,
|
||||||
sceneY: selectedElement.y + selectedElement.height / 2,
|
sceneY: selectedElement.y + selectedElement.height / 2,
|
||||||
shouldBind: true,
|
container,
|
||||||
});
|
});
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
return;
|
return;
|
||||||
@ -2317,7 +2320,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const element = this.getElementAtPosition(x, y, {
|
const element = this.getElementAtPosition(x, y, {
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (element && isTextElement(element) && !element.isDeleted) {
|
if (element && isTextElement(element) && !element.isDeleted) {
|
||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
@ -2394,29 +2396,31 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private startTextEditing = ({
|
private startTextEditing = ({
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
shouldBind,
|
|
||||||
insertAtParentCenter = true,
|
insertAtParentCenter = true,
|
||||||
|
container,
|
||||||
}: {
|
}: {
|
||||||
/** X position to insert text at */
|
/** X position to insert text at */
|
||||||
sceneX: number;
|
sceneX: number;
|
||||||
/** Y position to insert text at */
|
/** Y position to insert text at */
|
||||||
sceneY: number;
|
sceneY: number;
|
||||||
shouldBind: boolean;
|
|
||||||
/** whether to attempt to insert at element center if applicable */
|
/** whether to attempt to insert at element center if applicable */
|
||||||
insertAtParentCenter?: boolean;
|
insertAtParentCenter?: boolean;
|
||||||
|
container?: ExcalidrawTextContainer | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
let shouldBindToContainer = false;
|
||||||
|
|
||||||
let parentCenterPosition =
|
let parentCenterPosition =
|
||||||
insertAtParentCenter &&
|
insertAtParentCenter &&
|
||||||
this.getTextWysiwygSnappedToCenterPosition(
|
this.getTextWysiwygSnappedToCenterPosition(
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
container,
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
|
if (container && parentCenterPosition) {
|
||||||
|
shouldBindToContainer = true;
|
||||||
|
}
|
||||||
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
|
||||||
let container: ExcalidrawTextContainer | null = null;
|
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
const selectedElements = getSelectedElements(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
@ -2426,7 +2430,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
if (isTextElement(selectedElements[0])) {
|
if (isTextElement(selectedElements[0])) {
|
||||||
existingTextElement = selectedElements[0];
|
existingTextElement = selectedElements[0];
|
||||||
} else if (isTextBindableContainer(selectedElements[0], false)) {
|
} else if (container) {
|
||||||
existingTextElement = getBoundTextElement(selectedElements[0]);
|
existingTextElement = getBoundTextElement(selectedElements[0]);
|
||||||
} else {
|
} else {
|
||||||
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
@ -2435,26 +2439,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// bind to container when shouldBind is true or
|
if (!existingTextElement && shouldBindToContainer && container) {
|
||||||
// clicked on center of container
|
|
||||||
if (
|
|
||||||
!container &&
|
|
||||||
!existingTextElement &&
|
|
||||||
(shouldBind || parentCenterPosition)
|
|
||||||
) {
|
|
||||||
container = getTextBindableContainerAtPosition(
|
|
||||||
this.scene
|
|
||||||
.getNonDeletedElements()
|
|
||||||
.filter(
|
|
||||||
(ele) =>
|
|
||||||
isTextBindableContainer(ele, false) && !getBoundTextElement(ele),
|
|
||||||
),
|
|
||||||
sceneX,
|
|
||||||
sceneY,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!existingTextElement && container) {
|
|
||||||
const fontString = {
|
const fontString = {
|
||||||
fontSize: this.state.currentItemFontSize,
|
fontSize: this.state.currentItemFontSize,
|
||||||
fontFamily: this.state.currentItemFontFamily,
|
fontFamily: this.state.currentItemFontFamily,
|
||||||
@ -2472,12 +2457,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
this.state,
|
this.state,
|
||||||
this.canvas,
|
container,
|
||||||
window.devicePixelRatio,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = existingTextElement
|
const element = existingTextElement
|
||||||
? existingTextElement
|
? existingTextElement
|
||||||
: newTextElement({
|
: newTextElement({
|
||||||
@ -2504,7 +2487,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
verticalAlign: parentCenterPosition
|
verticalAlign: parentCenterPosition
|
||||||
? VERTICAL_ALIGN.MIDDLE
|
? VERTICAL_ALIGN.MIDDLE
|
||||||
: DEFAULT_VERTICAL_ALIGN,
|
: DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: container?.id ?? undefined,
|
containerId: shouldBindToContainer ? container?.id : undefined,
|
||||||
groupIds: container?.groupIds ?? [],
|
groupIds: container?.groupIds ?? [],
|
||||||
locked: false,
|
locked: false,
|
||||||
});
|
});
|
||||||
@ -2512,10 +2495,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ editingElement: element });
|
this.setState({ editingElement: element });
|
||||||
|
|
||||||
if (!existingTextElement) {
|
if (!existingTextElement) {
|
||||||
this.scene.replaceAllElements([
|
if (container && shouldBindToContainer) {
|
||||||
...this.scene.getElementsIncludingDeleted(),
|
const containerIndex = this.scene.getElementIndex(container.id);
|
||||||
element,
|
this.scene.insertElementAtIndex(element, containerIndex + 1);
|
||||||
]);
|
} else {
|
||||||
|
this.scene.replaceAllElements([
|
||||||
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
|
element,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// case: creating new text not centered to parent element → offset Y
|
// case: creating new text not centered to parent element → offset Y
|
||||||
// so that the text is centered to cursor position
|
// so that the text is centered to cursor position
|
||||||
@ -2603,22 +2591,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.canvas);
|
||||||
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
|
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
|
||||||
const selectedElements = getSelectedElements(
|
const container = getTextBindableContainerAtPosition(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
this.state,
|
this.state,
|
||||||
|
sceneX,
|
||||||
|
sceneY,
|
||||||
);
|
);
|
||||||
if (selectedElements.length === 1) {
|
if (container) {
|
||||||
const selectedElement = selectedElements[0];
|
if (hasBoundTextElement(container)) {
|
||||||
if (hasBoundTextElement(selectedElement)) {
|
sceneX = container.x + container.width / 2;
|
||||||
sceneX = selectedElement.x + selectedElement.width / 2;
|
sceneY = container.y + container.height / 2;
|
||||||
sceneY = selectedElement.y + selectedElement.height / 2;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.startTextEditing({
|
this.startTextEditing({
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
shouldBind: false,
|
|
||||||
insertAtParentCenter: !event.altKey,
|
insertAtParentCenter: !event.altKey,
|
||||||
|
container,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -3911,15 +3900,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let container = getTextBindableContainerAtPosition(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
this.state,
|
||||||
|
sceneX,
|
||||||
|
sceneY,
|
||||||
|
);
|
||||||
|
|
||||||
if (hasBoundTextElement(element)) {
|
if (hasBoundTextElement(element)) {
|
||||||
|
container = element as ExcalidrawTextContainer;
|
||||||
sceneX = element.x + element.width / 2;
|
sceneX = element.x + element.width / 2;
|
||||||
sceneY = element.y + element.height / 2;
|
sceneY = element.y + element.height / 2;
|
||||||
}
|
}
|
||||||
this.startTextEditing({
|
this.startTextEditing({
|
||||||
sceneX,
|
sceneX,
|
||||||
sceneY,
|
sceneY,
|
||||||
shouldBind: false,
|
|
||||||
insertAtParentCenter: !event.altKey,
|
insertAtParentCenter: !event.altKey,
|
||||||
|
container,
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.canvas);
|
||||||
@ -6171,21 +6168,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement | null,
|
container?: ExcalidrawTextContainer | null,
|
||||||
scale: number,
|
|
||||||
) {
|
) {
|
||||||
const elementClickedInside = getTextBindableContainerAtPosition(
|
if (container) {
|
||||||
this.scene
|
const elementCenterX = container.x + container.width / 2;
|
||||||
.getElementsIncludingDeleted()
|
const elementCenterY = container.y + container.height / 2;
|
||||||
.filter((element) => !isTextElement(element)),
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
);
|
|
||||||
if (elementClickedInside) {
|
|
||||||
const elementCenterX =
|
|
||||||
elementClickedInside.x + elementClickedInside.width / 2;
|
|
||||||
const elementCenterY =
|
|
||||||
elementClickedInside.y + elementClickedInside.height / 2;
|
|
||||||
const distanceToCenter = Math.hypot(
|
const distanceToCenter = Math.hypot(
|
||||||
x - elementCenterX,
|
x - elementCenterX,
|
||||||
y - elementCenterY,
|
y - elementCenterY,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
FontString,
|
FontString,
|
||||||
@ -12,6 +13,10 @@ import { MaybeTransformHandleType } from "./transformHandles";
|
|||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
||||||
|
import { isTextBindableContainer } from "./typeChecks";
|
||||||
|
import { getElementAbsoluteCoords } from "../element";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { getSelectedElements } from "../scene";
|
||||||
import { isImageElement } from "./typeChecks";
|
import { isImageElement } from "./typeChecks";
|
||||||
|
|
||||||
export const redrawTextBoundingBox = (
|
export const redrawTextBoundingBox = (
|
||||||
@ -492,6 +497,32 @@ export const getContainerDims = (element: ExcalidrawElement) => {
|
|||||||
return { width: element.width, height: element.height };
|
return { width: element.width, height: element.height };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTextBindableContainerAtPosition = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): ExcalidrawTextContainer | null => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
if (selectedElements.length === 1) {
|
||||||
|
return selectedElements[0] as ExcalidrawTextContainer;
|
||||||
|
}
|
||||||
|
let hitElement = null;
|
||||||
|
// 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) {
|
||||||
|
if (elements[index].isDeleted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
||||||
|
if (x1 < x && x < x2 && y1 < y && y < y2) {
|
||||||
|
hitElement = elements[index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||||
|
};
|
||||||
|
|
||||||
export const isValidTextContainer = (element: ExcalidrawElement) => {
|
export const isValidTextContainer = (element: ExcalidrawElement) => {
|
||||||
return (
|
return (
|
||||||
element.type === "rectangle" ||
|
element.type === "rectangle" ||
|
||||||
|
@ -500,7 +500,7 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bind text to container when double clicked on center", async () => {
|
it("should bind text to container when double clicked on center of filled container", async () => {
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
|
|
||||||
@ -527,6 +527,40 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should bind text to container when double clicked on center of transparent container", async () => {
|
||||||
|
const rectangle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
});
|
||||||
|
h.elements = [rectangle];
|
||||||
|
|
||||||
|
mouse.doubleClickAt(
|
||||||
|
rectangle.x + rectangle.width / 2,
|
||||||
|
rectangle.y + rectangle.height / 2,
|
||||||
|
);
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
mouse.down();
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
editor.blur();
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should bind text to container when clicked on container and enter pressed", async () => {
|
it("should bind text to container when clicked on container and enter pressed", async () => {
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
@ -825,9 +859,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
// Bind first text
|
// Bind first text
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
let editor = document.querySelector(
|
const editor = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@ -837,25 +871,14 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Attempt to bind another text
|
mouse.select(rectangle);
|
||||||
UI.clickTool("text");
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
mouse.clickAt(
|
expect(h.elements.length).toBe(2);
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
);
|
|
||||||
mouse.down();
|
|
||||||
expect(h.elements.length).toBe(3);
|
|
||||||
text = h.elements[2] as ExcalidrawTextElementWithContainer;
|
|
||||||
editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, { target: { value: "Whats up?" } });
|
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: h.elements[1].id, type: "text" },
|
{ id: h.elements[1].id, type: "text" },
|
||||||
]);
|
]);
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should respect text alignment when resizing", async () => {
|
it("should respect text alignment when resizing", async () => {
|
||||||
@ -1045,5 +1068,36 @@ describe("textWysiwyg", () => {
|
|||||||
`"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.Hello this text should get merged with the existing one"`,
|
`"Wikipedia is hosted by the Wikimedia Foundation, a non-profit organization that also hosts a range of other projects.Hello this text should get merged with the existing one"`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should always bind to selected container and insert it in correct position", async () => {
|
||||||
|
const rectangle2 = UI.createElement("rectangle", {
|
||||||
|
x: 5,
|
||||||
|
y: 10,
|
||||||
|
width: 120,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setSelectedElements([rectangle]);
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
expect(h.elements.length).toBe(3);
|
||||||
|
expect(h.elements[1].type).toBe("text");
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
mouse.down();
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
editor.blur();
|
||||||
|
expect(rectangle2.boundElements).toBeNull();
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -150,6 +150,24 @@ class Scene {
|
|||||||
// (I guess?)
|
// (I guess?)
|
||||||
this.callbacks.clear();
|
this.callbacks.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
insertElementAtIndex(element: ExcalidrawElement, index: number) {
|
||||||
|
if (!Number.isFinite(index) || index < 0) {
|
||||||
|
throw new Error(
|
||||||
|
"insertElementAtIndex can only be called with index >= 0",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const nextElements = [
|
||||||
|
...this.elements.slice(0, index),
|
||||||
|
element,
|
||||||
|
...this.elements.slice(index),
|
||||||
|
];
|
||||||
|
this.replaceAllElements(nextElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
getElementIndex(elementId: string) {
|
||||||
|
return this.elements.findIndex((element) => element.id === elementId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Scene;
|
export default Scene;
|
||||||
|
@ -1,11 +1,4 @@
|
|||||||
import {
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawTextContainer,
|
|
||||||
NonDeletedExcalidrawElement,
|
|
||||||
} from "../element/types";
|
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "../element";
|
|
||||||
import { isTextBindableContainer } from "../element/typeChecks";
|
|
||||||
|
|
||||||
export const hasBackground = (type: string) =>
|
export const hasBackground = (type: string) =>
|
||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
@ -73,23 +66,3 @@ export const getElementsAtPosition = (
|
|||||||
(element) => !element.isDeleted && isAtPositionFn(element),
|
(element) => !element.isDeleted && isAtPositionFn(element),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextBindableContainerAtPosition = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
): ExcalidrawTextContainer | null => {
|
|
||||||
let hitElement = null;
|
|
||||||
// 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) {
|
|
||||||
if (elements[index].isDeleted) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
|
|
||||||
if (x1 < x && x < x2 && y1 < y && y < y2) {
|
|
||||||
hitElement = elements[index];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
|
||||||
};
|
|
||||||
|
@ -14,7 +14,6 @@ export {
|
|||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
canChangeSharpness,
|
canChangeSharpness,
|
||||||
getElementAtPosition,
|
getElementAtPosition,
|
||||||
getTextBindableContainerAtPosition,
|
|
||||||
hasText,
|
hasText,
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
} from "./comparisons";
|
} from "./comparisons";
|
||||||
|
@ -232,7 +232,7 @@ describe("element locking", () => {
|
|||||||
API.setSelectedElements([container]);
|
API.setSelectedElements([container]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(h.state.editingElement?.id).not.toBe(text.id);
|
expect(h.state.editingElement?.id).not.toBe(text.id);
|
||||||
expect(h.state.editingElement?.id).toBe(h.elements[2].id);
|
expect(h.state.editingElement?.id).toBe(h.elements[1].id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should ignore locked text under cursor when clicked with text tool", () => {
|
it("should ignore locked text under cursor when clicked with text tool", () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user