mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: Support LaTeX and AsciiMath via MathJax on stem.excalidraw.com
This commit is contained in:
parent
c8370b394c
commit
86f5c2ebcf
84 changed files with 8331 additions and 289 deletions
|
@ -5,12 +5,23 @@ import { getSizeFromPoints } from "../points";
|
|||
import { randomInteger } from "../random";
|
||||
import { Point } from "../types";
|
||||
import { getUpdatedTimestamp } from "../utils";
|
||||
import { maybeGetSubtypeProps } from "./newElement";
|
||||
import { getSubtypeMethods } from "../subtypes";
|
||||
|
||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
"id" | "version" | "versionNonce"
|
||||
>;
|
||||
|
||||
const cleanUpdates = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
element: TElement,
|
||||
updates: ElementUpdate<TElement>,
|
||||
): ElementUpdate<TElement> => {
|
||||
const subtype = maybeGetSubtypeProps(element, element.type).subtype;
|
||||
const map = getSubtypeMethods(subtype);
|
||||
return map?.clean ? (map.clean(updates) as typeof updates) : updates;
|
||||
};
|
||||
|
||||
// This function tracks updates of text elements for the purposes for collaboration.
|
||||
// The version is used to compare updates when more than one user is working in
|
||||
// the same drawing. Note: this will trigger the component to update. Make sure you
|
||||
|
@ -21,6 +32,8 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
informMutation = true,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
let increment = false;
|
||||
const oldUpdates = cleanUpdates(element, updates);
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
|
@ -76,6 +89,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
|
||||
(element as any)[key] = value;
|
||||
didChange = true;
|
||||
key in oldUpdates && (increment = true);
|
||||
}
|
||||
}
|
||||
if (!didChange) {
|
||||
|
@ -91,9 +105,11 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||
invalidateShapeForElement(element);
|
||||
}
|
||||
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
if (increment) {
|
||||
element.version++;
|
||||
element.versionNonce = randomInteger();
|
||||
element.updated = getUpdatedTimestamp();
|
||||
}
|
||||
|
||||
if (informMutation) {
|
||||
Scene.getScene(element)?.informMutation();
|
||||
|
@ -107,6 +123,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||
updates: ElementUpdate<TElement>,
|
||||
): TElement => {
|
||||
let didChange = false;
|
||||
let increment = false;
|
||||
const oldUpdates = cleanUpdates(element, updates);
|
||||
for (const key in updates) {
|
||||
const value = (updates as any)[key];
|
||||
if (typeof value !== "undefined") {
|
||||
|
@ -118,6 +136,7 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||
continue;
|
||||
}
|
||||
didChange = true;
|
||||
key in oldUpdates && (increment = true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,6 +144,9 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
|||
return element;
|
||||
}
|
||||
|
||||
if (!increment) {
|
||||
return { ...element, ...updates };
|
||||
}
|
||||
return {
|
||||
...element,
|
||||
...updates,
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
FontFamilyValues,
|
||||
ExcalidrawTextContainer,
|
||||
} from "../element/types";
|
||||
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
import { getUpdatedTimestamp, isTestEnv } from "../utils";
|
||||
import { randomInteger, randomId } from "../random";
|
||||
import { mutateElement, newElementWith } from "./mutateElement";
|
||||
import { getNewGroupIdsForDuplication } from "../groups";
|
||||
|
@ -26,12 +26,36 @@ import {
|
|||
getBoundTextElementOffset,
|
||||
getContainerDims,
|
||||
getContainerElement,
|
||||
measureText,
|
||||
measureTextElement,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
wrapTextElement,
|
||||
} from "./textElement";
|
||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
||||
import { isArrowElement } from "./typeChecks";
|
||||
import { getSubtypeMethods, isValidSubtype } from "../subtypes";
|
||||
|
||||
export const maybeGetSubtypeProps = (
|
||||
obj: {
|
||||
subtype?: ExcalidrawElement["subtype"];
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
},
|
||||
type: ExcalidrawElement["type"],
|
||||
) => {
|
||||
const data: typeof obj = {};
|
||||
if ("subtype" in obj) {
|
||||
data.subtype = obj.subtype;
|
||||
}
|
||||
if ("customData" in obj) {
|
||||
data.customData = obj.customData;
|
||||
}
|
||||
if ("subtype" in data && !isValidSubtype(data.subtype, type)) {
|
||||
delete data.subtype;
|
||||
}
|
||||
if (!("subtype" in data) && "customData" in data) {
|
||||
delete data.customData;
|
||||
}
|
||||
return data as typeof obj;
|
||||
};
|
||||
|
||||
type ElementConstructorOpts = MarkOptional<
|
||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||
|
@ -44,6 +68,8 @@ type ElementConstructorOpts = MarkOptional<
|
|||
| "version"
|
||||
| "versionNonce"
|
||||
| "link"
|
||||
| "subtype"
|
||||
| "customData"
|
||||
>;
|
||||
|
||||
const _newElementBase = <T extends ExcalidrawElement>(
|
||||
|
@ -69,8 +95,10 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
|||
...rest
|
||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||
) => {
|
||||
const { subtype, customData } = rest;
|
||||
// assign type to guard against excess properties
|
||||
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
|
||||
...maybeGetSubtypeProps({ subtype, customData }, type),
|
||||
id: rest.id || randomId(),
|
||||
type,
|
||||
x,
|
||||
|
@ -103,8 +131,11 @@ export const newElement = (
|
|||
opts: {
|
||||
type: ExcalidrawGenericElement["type"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawGenericElement> =>
|
||||
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
): NonDeleted<ExcalidrawGenericElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
|
||||
};
|
||||
|
||||
/** computes element x/y offset based on textAlign/verticalAlign */
|
||||
const getTextElementPositionOffsets = (
|
||||
|
@ -138,8 +169,13 @@ export const newTextElement = (
|
|||
containerId?: ExcalidrawTextContainer["id"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawTextElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
const text = normalizeText(opts.text);
|
||||
const metrics = measureText(text, getFontString(opts));
|
||||
const metrics = measureTextElement(opts, {
|
||||
text,
|
||||
customData: opts.customData,
|
||||
});
|
||||
const offsets = getTextElementPositionOffsets(opts, metrics);
|
||||
const textElement = newElementWith(
|
||||
{
|
||||
|
@ -181,7 +217,7 @@ const getAdjustedDimensions = (
|
|||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseline,
|
||||
} = measureText(nextText, getFontString(element), maxWidth);
|
||||
} = measureTextElement(element, { text: nextText }, maxWidth);
|
||||
const { textAlign, verticalAlign } = element;
|
||||
let x: number;
|
||||
let y: number;
|
||||
|
@ -190,9 +226,9 @@ const getAdjustedDimensions = (
|
|||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||
!element.containerId
|
||||
) {
|
||||
const prevMetrics = measureText(
|
||||
element.text,
|
||||
getFontString(element),
|
||||
const prevMetrics = measureTextElement(
|
||||
element,
|
||||
{ fontSize: element.fontSize },
|
||||
maxWidth,
|
||||
);
|
||||
const offsets = getTextElementPositionOffsets(element, {
|
||||
|
@ -268,11 +304,9 @@ export const refreshTextDimensions = (
|
|||
) => {
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
text = wrapText(
|
||||
text = wrapTextElement(textElement, getMaxContainerWidth(container), {
|
||||
text,
|
||||
getFontString(textElement),
|
||||
getMaxContainerWidth(container),
|
||||
);
|
||||
});
|
||||
}
|
||||
const dimensions = getAdjustedDimensions(textElement, text);
|
||||
return { text, ...dimensions };
|
||||
|
@ -336,6 +370,8 @@ export const newFreeDrawElement = (
|
|||
simulatePressure: boolean;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawFreeDrawElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
|
@ -353,6 +389,8 @@ export const newLinearElement = (
|
|||
points?: ExcalidrawLinearElement["points"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawLinearElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
|
||||
points: opts.points || [],
|
||||
|
@ -372,6 +410,8 @@ export const newImageElement = (
|
|||
scale?: ExcalidrawImageElement["scale"];
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawImageElement> => {
|
||||
const map = getSubtypeMethods(opts?.subtype);
|
||||
map?.clean && map.clean(opts);
|
||||
return {
|
||||
..._newElementBase<ExcalidrawImageElement>("image", opts),
|
||||
// in the future we'll support changing stroke color for some SVG elements,
|
||||
|
|
|
@ -46,7 +46,7 @@ import {
|
|||
getBoundTextElementOffset,
|
||||
getContainerElement,
|
||||
handleBindTextResize,
|
||||
measureText,
|
||||
measureTextElement,
|
||||
} from "./textElement";
|
||||
import { getMaxContainerWidth } from "./newElement";
|
||||
|
||||
|
@ -211,9 +211,9 @@ const measureFontSizeFromWH = (
|
|||
if (nextFontSize < MIN_FONT_SIZE) {
|
||||
return null;
|
||||
}
|
||||
const metrics = measureText(
|
||||
element.text,
|
||||
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
||||
const metrics = measureTextElement(
|
||||
element,
|
||||
{ fontSize: nextFontSize },
|
||||
element.containerId ? width : null,
|
||||
);
|
||||
return {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { getSubtypeMethods, SubtypeMethods } from "../subtypes";
|
||||
import { getFontString, arrayToMap, isTestEnv } from "../utils";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
|
@ -29,6 +30,30 @@ import {
|
|||
updateOriginalContainerCache,
|
||||
} from "./textWysiwyg";
|
||||
|
||||
export const measureTextElement = function (element, next, maxWidth) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.measureText) {
|
||||
return map.measureText(element, next, maxWidth);
|
||||
}
|
||||
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||
const text = next?.text ?? element.text;
|
||||
return measureText(text, font, maxWidth);
|
||||
} as SubtypeMethods["measureText"];
|
||||
|
||||
export const wrapTextElement = function (element, containerWidth, next) {
|
||||
const map = getSubtypeMethods(element.subtype);
|
||||
if (map?.wrapText) {
|
||||
return map.wrapText(element, containerWidth, next);
|
||||
}
|
||||
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const font = getFontString({ fontSize, fontFamily: element.fontFamily });
|
||||
const text = next?.text ?? element.originalText;
|
||||
return wrapText(text, font, containerWidth);
|
||||
} as SubtypeMethods["wrapText"];
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
text
|
||||
|
@ -47,13 +72,15 @@ export const redrawTextBoundingBox = (
|
|||
let text = textElement.text;
|
||||
if (container) {
|
||||
maxWidth = getMaxContainerWidth(container);
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
);
|
||||
text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const metrics = measureText(text, getFontString(textElement), maxWidth);
|
||||
const width = measureTextElement(
|
||||
textElement,
|
||||
{ text: textElement.originalText },
|
||||
maxWidth,
|
||||
).width;
|
||||
const { height, baseline } = measureTextElement(textElement, { text });
|
||||
const metrics = { width, height, baseline };
|
||||
let coordY = textElement.y;
|
||||
let coordX = textElement.x;
|
||||
// Resize container and vertically center align the text
|
||||
|
@ -175,16 +202,12 @@ export const handleBindTextResize = (
|
|||
let nextBaseLine = textElement.baseline;
|
||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||
if (text) {
|
||||
text = wrapText(
|
||||
textElement.originalText,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
);
|
||||
text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const dimensions = measureText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
maxWidth,
|
||||
const dimensions = measureTextElement(
|
||||
textElement,
|
||||
{ text },
|
||||
container.width,
|
||||
);
|
||||
nextHeight = dimensions.height;
|
||||
nextWidth = dimensions.width;
|
||||
|
|
|
@ -10,8 +10,10 @@ import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
|||
import {
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
FontString,
|
||||
} from "./types";
|
||||
import * as textElementUtils from "./textElement";
|
||||
import { getFontString } from "../utils";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { resize } from "../tests/utils";
|
||||
|
@ -675,39 +677,53 @@ describe("textWysiwyg", () => {
|
|||
});
|
||||
|
||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation((text, font, maxWidth) => {
|
||||
let width = INITIAL_WIDTH;
|
||||
let height = APPROX_LINE_HEIGHT;
|
||||
let baseline = 10;
|
||||
if (!text) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
}
|
||||
baseline = 30;
|
||||
width = DUMMY_WIDTH;
|
||||
if (text === "Hello \nWorld!") {
|
||||
height = APPROX_LINE_HEIGHT * 2;
|
||||
}
|
||||
if (maxWidth) {
|
||||
width = maxWidth;
|
||||
// To capture cases where maxWidth passed is initial width
|
||||
// due to which the text is not wrapped correctly
|
||||
if (maxWidth === INITIAL_WIDTH) {
|
||||
height = DUMMY_HEIGHT;
|
||||
}
|
||||
}
|
||||
const mockMeasureText = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
maxWidth?: number | null,
|
||||
) => {
|
||||
let width = INITIAL_WIDTH;
|
||||
let height = APPROX_LINE_HEIGHT;
|
||||
let baseline = 10;
|
||||
if (!text) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
});
|
||||
}
|
||||
baseline = 30;
|
||||
width = DUMMY_WIDTH;
|
||||
if (text === "Hello \nWorld!") {
|
||||
height = APPROX_LINE_HEIGHT * 2;
|
||||
}
|
||||
if (maxWidth) {
|
||||
width = maxWidth;
|
||||
// To capture cases where maxWidth passed is initial width
|
||||
// due to which the text is not wrapped correctly
|
||||
if (maxWidth === INITIAL_WIDTH) {
|
||||
height = DUMMY_HEIGHT;
|
||||
}
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation(mockMeasureText);
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureTextElement")
|
||||
.mockImplementation((element, next, maxWidth) => {
|
||||
return mockMeasureText(
|
||||
next?.text ?? element.text,
|
||||
getFontString(element),
|
||||
maxWidth,
|
||||
);
|
||||
});
|
||||
expect(h.elements.length).toBe(1);
|
||||
|
||||
Keyboard.keyDown(KEYS.ENTER);
|
||||
|
@ -1057,28 +1073,42 @@ describe("textWysiwyg", () => {
|
|||
});
|
||||
|
||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation((text, font, maxWidth) => {
|
||||
let width = INITIAL_WIDTH;
|
||||
let height = APPROX_LINE_HEIGHT;
|
||||
let baseline = 10;
|
||||
if (!text) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
}
|
||||
baseline = 30;
|
||||
width = DUMMY_WIDTH;
|
||||
height = APPROX_LINE_HEIGHT * 5;
|
||||
|
||||
const mockMeasureText = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
maxWidth?: number | null,
|
||||
) => {
|
||||
let width = INITIAL_WIDTH;
|
||||
let height = APPROX_LINE_HEIGHT;
|
||||
let baseline = 10;
|
||||
if (!text) {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
}
|
||||
baseline = 30;
|
||||
width = DUMMY_WIDTH;
|
||||
height = APPROX_LINE_HEIGHT * 5;
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
};
|
||||
};
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureText")
|
||||
.mockImplementation(mockMeasureText);
|
||||
jest
|
||||
.spyOn(textElementUtils, "measureTextElement")
|
||||
.mockImplementation((element, next, maxWidth) => {
|
||||
return mockMeasureText(
|
||||
next?.text ?? element.text,
|
||||
getFontString(element),
|
||||
maxWidth,
|
||||
);
|
||||
});
|
||||
const originalRectHeight = rectangle.height;
|
||||
expect(rectangle.height).toBe(originalRectHeight);
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
getContainerDims,
|
||||
getContainerElement,
|
||||
getTextElementAngle,
|
||||
measureText,
|
||||
getTextWidth,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
|
@ -43,6 +44,7 @@ import { LinearElementEditor } from "./linearElementEditor";
|
|||
import { parseClipboard } from "../clipboard";
|
||||
|
||||
const getTransform = (
|
||||
offsetX: number,
|
||||
width: number,
|
||||
height: number,
|
||||
angle: number,
|
||||
|
@ -60,7 +62,7 @@ const getTransform = (
|
|||
if (height > maxHeight && zoom.value !== 1) {
|
||||
translateY = (maxHeight * (zoom.value - 1)) / 2;
|
||||
}
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg) translate(${offsetX}px, 0px)`;
|
||||
};
|
||||
|
||||
const originalContainerCache: {
|
||||
|
@ -153,11 +155,19 @@ export const textWysiwyg = ({
|
|||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||
let coordX = updatedTextElement.x;
|
||||
let coordY = updatedTextElement.y;
|
||||
let eCoordY = coordY;
|
||||
const container = getContainerElement(updatedTextElement);
|
||||
let maxWidth = updatedTextElement.width;
|
||||
|
||||
let maxHeight = updatedTextElement.height;
|
||||
const width = updatedTextElement.width;
|
||||
// Editing metrics
|
||||
const eMetrics = measureText(
|
||||
updatedTextElement.originalText,
|
||||
getFontString(updatedTextElement),
|
||||
container ? getContainerDims(container).width : null,
|
||||
);
|
||||
|
||||
let maxWidth = eMetrics.width;
|
||||
let maxHeight = eMetrics.height;
|
||||
const width = eMetrics.width;
|
||||
// Set to element height by default since that's
|
||||
// what is going to be used for unbounded text
|
||||
let textElementHeight = updatedTextElement.height;
|
||||
|
@ -170,6 +180,7 @@ export const textWysiwyg = ({
|
|||
);
|
||||
coordX = boundTextCoords.x;
|
||||
coordY = boundTextCoords.y;
|
||||
eCoordY = coordY;
|
||||
}
|
||||
const propertiesUpdated = textPropertiesUpdated(
|
||||
updatedTextElement,
|
||||
|
@ -183,7 +194,14 @@ export const textWysiwyg = ({
|
|||
}
|
||||
if (propertiesUpdated) {
|
||||
// update height of the editor after properties updated
|
||||
textElementHeight = updatedTextElement.height;
|
||||
const font = getFontString(updatedTextElement);
|
||||
textElementHeight =
|
||||
getApproxLineHeight(font) *
|
||||
updatedTextElement.text.split("\n").length;
|
||||
textElementHeight = Math.max(
|
||||
textElementHeight,
|
||||
updatedTextElement.height,
|
||||
);
|
||||
}
|
||||
|
||||
let originalContainerData;
|
||||
|
@ -235,6 +253,7 @@ export const textWysiwyg = ({
|
|||
if (!isArrowElement(container)) {
|
||||
coordY =
|
||||
container.y + containerDims.height / 2 - textElementHeight / 2;
|
||||
eCoordY = coordY + textElementHeight / 2 - eMetrics.height / 2;
|
||||
}
|
||||
}
|
||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||
|
@ -243,10 +262,11 @@ export const textWysiwyg = ({
|
|||
containerDims.height -
|
||||
textElementHeight -
|
||||
getBoundTextElementOffset(updatedTextElement);
|
||||
eCoordY = coordY + textElementHeight - eMetrics.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
||||
const [viewportX, viewportY] = getViewportCoords(coordX, eCoordY);
|
||||
const initialSelectionStart = editable.selectionStart;
|
||||
const initialSelectionEnd = editable.selectionEnd;
|
||||
const initialLength = editable.value.length;
|
||||
|
@ -268,10 +288,24 @@ export const textWysiwyg = ({
|
|||
const lines = updatedTextElement.originalText.split("\n");
|
||||
const lineHeight = updatedTextElement.containerId
|
||||
? approxLineHeight
|
||||
: updatedTextElement.height / lines.length;
|
||||
: eMetrics.height / lines.length;
|
||||
if (!container) {
|
||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||
}
|
||||
// Horizontal offset in case updatedTextElement has a non-WYSIWYG subtype
|
||||
const offWidth = container
|
||||
? Math.min(
|
||||
0,
|
||||
updatedTextElement.width - Math.min(maxWidth, eMetrics.width),
|
||||
)
|
||||
: Math.min(maxWidth, updatedTextElement.width) -
|
||||
Math.min(maxWidth, eMetrics.width);
|
||||
const offsetX =
|
||||
textAlign === "right"
|
||||
? offWidth
|
||||
: textAlign === "center"
|
||||
? offWidth / 2
|
||||
: 0;
|
||||
|
||||
// Make sure text editor height doesn't go beyond viewport
|
||||
const editorMaxHeight =
|
||||
|
@ -285,9 +319,12 @@ export const textWysiwyg = ({
|
|||
height: `${textElementHeight}px`,
|
||||
left: `${viewportX}px`,
|
||||
top: `${viewportY}px`,
|
||||
transformOrigin: `${updatedTextElement.width / 2}px
|
||||
${updatedTextElement.height / 2}px`,
|
||||
transform: getTransform(
|
||||
width,
|
||||
textElementHeight,
|
||||
offsetX,
|
||||
updatedTextElement.width,
|
||||
updatedTextElement.height,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
appState,
|
||||
maxWidth,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { Subtype } from "../subtypes";
|
||||
import { Point } from "../types";
|
||||
import {
|
||||
FONT_FAMILY,
|
||||
|
@ -63,6 +64,7 @@ type _ExcalidrawElementBase = Readonly<{
|
|||
updated: number;
|
||||
link: string | null;
|
||||
locked: boolean;
|
||||
subtype?: Subtype;
|
||||
customData?: Record<string, any>;
|
||||
}>;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue