Files
upstream-excalidraw/packages/excalidraw/actions/actionTextAutoResize.ts
2026-09-16 05:31:04 +05:00

89 lines
2.7 KiB
TypeScript

import { getFontString } from "@excalidraw/common";
import {
getTextAnchorRatios,
isExcalidrawElement,
isNonDeletedElement,
newElementWith,
updateBoundElements,
} from "@excalidraw/element";
import { measureText } from "@excalidraw/element";
import { isTextElement } from "@excalidraw/element";
import { CaptureUpdateAction } from "@excalidraw/element";
import type { ExcalidrawElement } from "@excalidraw/element/types";
import { getSelectedElements } from "../scene";
import { register } from "./register";
export const actionTextAutoResize = register({
name: "autoResize",
label: "labels.autoResize",
icon: null,
trackEvent: { category: "element" },
predicate: (elements, appState, _: unknown) => {
const selectedElements = getSelectedElements(elements, appState);
return (
selectedElements.length === 1 &&
isTextElement(selectedElements[0]) &&
!selectedElements[0].autoResize
);
},
perform: (elements, appState, targetElement, app) => {
const selectedElements = getSelectedElements(elements, appState);
const targetTextElement =
isExcalidrawElement(targetElement) && isTextElement(targetElement)
? targetElement
: (selectedElements[0] as ExcalidrawElement | undefined);
const target = elements.find(
(element) => element.id === targetTextElement?.id,
);
if (!target || !isTextElement(target)) {
return false;
}
const metrics = measureText(
target.originalText,
getFontString(target),
target.lineHeight,
);
// unwrapping resizes the box, so keep the point the text's alignment pins
// — otherwise a right-aligned or centred text slides sideways, and one
// bound to an arrow endpoint drags the arrow with it
const anchor = getTextAnchorRatios(target);
const resized = newElementWith(target, {
autoResize: true,
width: metrics.width,
height: metrics.height,
x: target.x + (target.width - metrics.width) * anchor.x,
y: target.y + (target.height - metrics.height) * anchor.y,
text: target.originalText,
});
// the anchor only pins one point of the box, so any other arrow bound to
// this text still has to be re-routed to its new geometry. The scene holds
// the pre-resize text, hence handing the new one over explicitly.
if (isNonDeletedElement(resized)) {
updateBoundElements(resized, app.scene, {
changedElements: new Map([[resized.id, resized]]),
});
}
return {
appState,
elements: elements.map((element) =>
element.id === resized.id ? resized : element,
),
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
};
},
});