89 lines
2.7 KiB
TypeScript
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,
|
|
};
|
|
},
|
|
});
|