Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
commit
fb24221587
@ -42,9 +42,12 @@ export const actionUnbindText = register({
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const { width, height } = measureTextElement(boundTextElement, {
|
||||
text: boundTextElement.originalText,
|
||||
});
|
||||
const { width, height, baseline } = measureTextElement(
|
||||
boundTextElement,
|
||||
{
|
||||
text: boundTextElement.originalText,
|
||||
},
|
||||
);
|
||||
const originalContainerHeight = getOriginalContainerHeightFromCache(
|
||||
element.id,
|
||||
);
|
||||
@ -54,6 +57,7 @@ export const actionUnbindText = register({
|
||||
containerId: null,
|
||||
width,
|
||||
height,
|
||||
baseline,
|
||||
text: boundTextElement.originalText,
|
||||
});
|
||||
mutateElement(element, {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AppState } from "../../src/types";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import { ColorPicker } from "../components/ColorPicker";
|
||||
import { IconPicker } from "../components/IconPicker";
|
||||
@ -37,6 +38,7 @@ import {
|
||||
TextAlignLeftIcon,
|
||||
TextAlignCenterIcon,
|
||||
TextAlignRightIcon,
|
||||
FillZigZagIcon,
|
||||
} from "../components/icons";
|
||||
import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
@ -294,7 +296,12 @@ export const actionChangeBackgroundColor = register({
|
||||
export const actionChangeFillStyle = register({
|
||||
name: "changeFillStyle",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
trackEvent(
|
||||
"element",
|
||||
"changeFillStyle",
|
||||
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
||||
);
|
||||
return {
|
||||
elements: changeProperty(elements, appState, (el) =>
|
||||
newElementWith(el, {
|
||||
@ -305,40 +312,55 @@ export const actionChangeFillStyle = register({
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fill")}</legend>
|
||||
<ButtonIconSelect
|
||||
options={[
|
||||
{
|
||||
value: "hachure",
|
||||
text: t("labels.hachure"),
|
||||
icon: FillHachureIcon,
|
||||
},
|
||||
{
|
||||
value: "cross-hatch",
|
||||
text: t("labels.crossHatch"),
|
||||
icon: FillCrossHatchIcon,
|
||||
},
|
||||
{
|
||||
value: "solid",
|
||||
text: t("labels.solid"),
|
||||
icon: FillSolidIcon,
|
||||
},
|
||||
]}
|
||||
group="fill"
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.fillStyle,
|
||||
appState.currentItemFillStyle,
|
||||
)}
|
||||
onChange={(value) => {
|
||||
updateData(value);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
),
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const allElementsZigZag = selectedElements.every(
|
||||
(el) => el.fillStyle === "zigzag",
|
||||
);
|
||||
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fill")}</legend>
|
||||
<ButtonIconSelect
|
||||
type="button"
|
||||
options={[
|
||||
{
|
||||
value: "hachure",
|
||||
text: t("labels.hachure"),
|
||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||
active: allElementsZigZag ? true : undefined,
|
||||
},
|
||||
{
|
||||
value: "cross-hatch",
|
||||
text: t("labels.crossHatch"),
|
||||
icon: FillCrossHatchIcon,
|
||||
},
|
||||
{
|
||||
value: "solid",
|
||||
text: t("labels.solid"),
|
||||
icon: FillSolidIcon,
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.fillStyle,
|
||||
appState.currentItemFillStyle,
|
||||
)}
|
||||
onClick={(value, event) => {
|
||||
const nextValue =
|
||||
event.altKey &&
|
||||
value === "hachure" &&
|
||||
selectedElements.every((el) => el.fillStyle === "hachure")
|
||||
? "zigzag"
|
||||
: value;
|
||||
|
||||
updateData(nextValue);
|
||||
}}
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const actionChangeStrokeWidth = register({
|
||||
|
@ -1,33 +1,59 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||
export const ButtonIconSelect = <T extends Object>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
group,
|
||||
}: {
|
||||
options: { value: T; text: string; icon: JSX.Element; testId?: string }[];
|
||||
value: T | null;
|
||||
onChange: (value: T) => void;
|
||||
group: string;
|
||||
}) => (
|
||||
export const ButtonIconSelect = <T extends Object>(
|
||||
props: {
|
||||
options: {
|
||||
value: T;
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
testId?: string;
|
||||
/** if not supplied, defaults to value identity check */
|
||||
active?: boolean;
|
||||
}[];
|
||||
value: T | null;
|
||||
type?: "radio" | "button";
|
||||
} & (
|
||||
| { type?: "radio"; group: string; onChange: (value: T) => void }
|
||||
| {
|
||||
type: "button";
|
||||
onClick: (
|
||||
value: T,
|
||||
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
|
||||
) => void;
|
||||
}
|
||||
),
|
||||
) => (
|
||||
<div className="buttonList buttonListIcon">
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.text}
|
||||
className={clsx({ active: value === option.value })}
|
||||
title={option.text}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={group}
|
||||
onChange={() => onChange(option.value)}
|
||||
checked={value === option.value}
|
||||
{props.options.map((option) =>
|
||||
props.type === "button" ? (
|
||||
<button
|
||||
key={option.text}
|
||||
onClick={(event) => props.onClick(option.value, event)}
|
||||
className={clsx({
|
||||
active: option.active ?? props.value === option.value,
|
||||
})}
|
||||
data-testid={option.testId}
|
||||
/>
|
||||
{option.icon}
|
||||
</label>
|
||||
))}
|
||||
title={option.text}
|
||||
>
|
||||
{option.icon}
|
||||
</button>
|
||||
) : (
|
||||
<label
|
||||
key={option.text}
|
||||
className={clsx({ active: props.value === option.value })}
|
||||
title={option.text}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={props.group}
|
||||
onChange={() => props.onChange(option.value)}
|
||||
checked={props.value === option.value}
|
||||
data-testid={option.testId}
|
||||
/>
|
||||
{option.icon}
|
||||
</label>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -1008,6 +1008,13 @@ export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) =>
|
||||
),
|
||||
);
|
||||
|
||||
export const FillZigZagIcon = createIcon(
|
||||
<g strokeWidth={1.25}>
|
||||
<path d="M5.879 2.625h8.242a3.27 3.27 0 0 1 3.254 3.254v8.242a3.27 3.27 0 0 1-3.254 3.254H5.88a3.27 3.27 0 0 1-3.254-3.254V5.88A3.27 3.27 0 0 1 5.88 2.626l-.001-.001ZM4.518 16.118l7.608-12.83m.198 13.934 5.051-9.897M2.778 9.675l9.348-6.387m-7.608 12.83 12.857-8.793" />
|
||||
</g>,
|
||||
modifiedTablerIconProps,
|
||||
);
|
||||
|
||||
export const FillHachureIcon = createIcon(
|
||||
<>
|
||||
<path
|
||||
|
@ -155,6 +155,9 @@
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.welcome-screen-menu-item:focus-visible,
|
||||
.dropdown-menu-item:focus-visible,
|
||||
button:focus-visible,
|
||||
.buttonList label:focus-within,
|
||||
input:focus-visible {
|
||||
outline: transparent;
|
||||
|
@ -36,7 +36,11 @@ import { arrayToMap } from "../utils";
|
||||
import { isValidSubtype } from "../subtypes";
|
||||
import oc from "open-color";
|
||||
import { MarkOptional, Mutable } from "../utility-types";
|
||||
import { detectLineHeight, getDefaultLineHeight } from "../element/textElement";
|
||||
import {
|
||||
detectLineHeight,
|
||||
getDefaultLineHeight,
|
||||
measureTextElement,
|
||||
} from "../element/textElement";
|
||||
|
||||
type RestoredAppState = Omit<
|
||||
AppState,
|
||||
@ -176,6 +180,20 @@ const restoreElement = (
|
||||
}
|
||||
const text = element.text ?? "";
|
||||
|
||||
// line-height might not be specified either when creating elements
|
||||
// programmatically, or when importing old diagrams.
|
||||
// For the latter we want to detect the original line height which
|
||||
// will likely differ from our per-font fixed line height we now use,
|
||||
// to maintain backward compatibility.
|
||||
const lineHeight =
|
||||
element.lineHeight ||
|
||||
(element.height
|
||||
? // detect line-height from current element height and font-size
|
||||
detectLineHeight(element)
|
||||
: // no element height likely means programmatic use, so default
|
||||
// to a fixed line height
|
||||
getDefaultLineHeight(element.fontFamily));
|
||||
const baseline = measureTextElement(element, { text }).baseline;
|
||||
element = restoreElementWithProperties(element, {
|
||||
fontSize,
|
||||
fontFamily,
|
||||
@ -184,19 +202,9 @@ const restoreElement = (
|
||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||
containerId: element.containerId ?? null,
|
||||
originalText: element.originalText || text,
|
||||
// line-height might not be specified either when creating elements
|
||||
// programmatically, or when importing old diagrams.
|
||||
// For the latter we want to detect the original line height which
|
||||
// will likely differ from our per-font fixed line height we now use,
|
||||
// to maintain backward compatibility.
|
||||
lineHeight:
|
||||
element.lineHeight ||
|
||||
(element.height
|
||||
? // detect line-height from current element height and font-size
|
||||
detectLineHeight(element)
|
||||
: // no element height likely means programmatic use, so default
|
||||
// to a fixed line height
|
||||
getDefaultLineHeight(element.fontFamily)),
|
||||
|
||||
lineHeight,
|
||||
baseline,
|
||||
});
|
||||
|
||||
if (refreshDimensions) {
|
||||
|
@ -184,6 +184,7 @@ export const newTextElement = (
|
||||
},
|
||||
);
|
||||
const offsets = getTextElementPositionOffsets(opts, metrics);
|
||||
|
||||
const textElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawTextElement>("text", opts),
|
||||
@ -196,6 +197,7 @@ export const newTextElement = (
|
||||
y: opts.y - offsets.y,
|
||||
width: metrics.width,
|
||||
height: metrics.height,
|
||||
baseline: metrics.baseline,
|
||||
containerId: opts.containerId || null,
|
||||
originalText: text,
|
||||
lineHeight,
|
||||
@ -213,10 +215,15 @@ const getAdjustedDimensions = (
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
baseline: number;
|
||||
} => {
|
||||
const container = getContainerElement(element);
|
||||
|
||||
const { width: nextWidth, height: nextHeight } = measureTextElement(element, {
|
||||
const {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseline,
|
||||
} = measureTextElement(element, {
|
||||
text: nextText,
|
||||
});
|
||||
const { textAlign, verticalAlign } = element;
|
||||
@ -289,6 +296,7 @@ const getAdjustedDimensions = (
|
||||
return {
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseline,
|
||||
x: Number.isFinite(x) ? x : element.x,
|
||||
y: Number.isFinite(y) ? y : element.y,
|
||||
};
|
||||
@ -298,6 +306,9 @@ export const refreshTextDimensions = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
text = textElement.text,
|
||||
) => {
|
||||
if (textElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
text = wrapTextElement(textElement, getMaxContainerWidth(container), {
|
||||
|
@ -46,6 +46,8 @@ import {
|
||||
handleBindTextResize,
|
||||
getMaxContainerWidth,
|
||||
getApproxMinLineHeight,
|
||||
measureTextElement,
|
||||
getMaxContainerHeight,
|
||||
} from "./textElement";
|
||||
|
||||
export const normalizeAngle = (angle: number): number => {
|
||||
@ -193,7 +195,8 @@ const MIN_FONT_SIZE = 1;
|
||||
const measureFontSizeFromWidth = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
nextWidth: number,
|
||||
): number | null => {
|
||||
nextHeight: number,
|
||||
): { size: number; baseline: number } | null => {
|
||||
// We only use width to scale font on resize
|
||||
let width = element.width;
|
||||
|
||||
@ -208,8 +211,11 @@ const measureFontSizeFromWidth = (
|
||||
if (nextFontSize < MIN_FONT_SIZE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return nextFontSize;
|
||||
const metrics = measureTextElement(element, { fontSize: nextFontSize });
|
||||
return {
|
||||
size: nextFontSize,
|
||||
baseline: metrics.baseline + (nextHeight - metrics.height),
|
||||
};
|
||||
};
|
||||
|
||||
const getSidesForTransformHandle = (
|
||||
@ -280,8 +286,8 @@ const resizeSingleTextElement = (
|
||||
if (scale > 0) {
|
||||
const nextWidth = element.width * scale;
|
||||
const nextHeight = element.height * scale;
|
||||
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
|
||||
if (nextFontSize === null) {
|
||||
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
|
||||
if (metrics === null) {
|
||||
return;
|
||||
}
|
||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||
@ -305,9 +311,10 @@ const resizeSingleTextElement = (
|
||||
deltaY2,
|
||||
);
|
||||
mutateElement(element, {
|
||||
fontSize: nextFontSize,
|
||||
fontSize: metrics.size,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: metrics.baseline,
|
||||
x: nextElementX,
|
||||
y: nextElementY,
|
||||
});
|
||||
@ -360,7 +367,7 @@ export const resizeSingleElement = (
|
||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||
|
||||
let boundTextFontSize: number | null = null;
|
||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
@ -410,7 +417,10 @@ export const resizeSingleElement = (
|
||||
boundTextElement.id,
|
||||
) as typeof boundTextElement | undefined;
|
||||
if (stateOfBoundTextElementAtResize) {
|
||||
boundTextFontSize = stateOfBoundTextElementAtResize.fontSize;
|
||||
boundTextFont = {
|
||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||
baseline: stateOfBoundTextElementAtResize.baseline,
|
||||
};
|
||||
}
|
||||
if (shouldMaintainAspectRatio) {
|
||||
const updatedElement = {
|
||||
@ -419,14 +429,18 @@ export const resizeSingleElement = (
|
||||
height: eleNewHeight,
|
||||
};
|
||||
|
||||
const nextFontSize = measureFontSizeFromWidth(
|
||||
const nextFont = measureFontSizeFromWidth(
|
||||
boundTextElement,
|
||||
getMaxContainerWidth(updatedElement),
|
||||
getMaxContainerHeight(updatedElement),
|
||||
);
|
||||
if (nextFontSize === null) {
|
||||
if (nextFont === null) {
|
||||
return;
|
||||
}
|
||||
boundTextFontSize = nextFontSize;
|
||||
boundTextFont = {
|
||||
fontSize: nextFont.size,
|
||||
baseline: nextFont.baseline,
|
||||
};
|
||||
} else {
|
||||
const minWidth = getApproxMinLineWidth(
|
||||
getFontString(boundTextElement),
|
||||
@ -568,9 +582,10 @@ export const resizeSingleElement = (
|
||||
});
|
||||
|
||||
mutateElement(element, resizedElement);
|
||||
if (boundTextElement && boundTextFontSize != null) {
|
||||
if (boundTextElement && boundTextFont != null) {
|
||||
mutateElement(boundTextElement, {
|
||||
fontSize: boundTextFontSize,
|
||||
fontSize: boundTextFont.fontSize,
|
||||
baseline: boundTextFont.baseline,
|
||||
});
|
||||
}
|
||||
handleBindTextResize(element, transformHandleDirection);
|
||||
@ -677,6 +692,7 @@ const resizeMultipleElements = (
|
||||
y: number;
|
||||
points?: Point[];
|
||||
fontSize?: number;
|
||||
baseline?: number;
|
||||
} = {
|
||||
width,
|
||||
height,
|
||||
@ -685,7 +701,7 @@ const resizeMultipleElements = (
|
||||
...rescaledPoints,
|
||||
};
|
||||
|
||||
let boundTextUpdates: { fontSize: number } | null = null;
|
||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
||||
|
||||
const boundTextElement = getBoundTextElement(element.latest);
|
||||
|
||||
@ -695,24 +711,29 @@ const resizeMultipleElements = (
|
||||
width,
|
||||
height,
|
||||
};
|
||||
const fontSize = measureFontSizeFromWidth(
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
boundTextElement
|
||||
? getMaxContainerWidth(updatedElement)
|
||||
: updatedElement.width,
|
||||
boundTextElement
|
||||
? getMaxContainerHeight(updatedElement)
|
||||
: updatedElement.height,
|
||||
);
|
||||
|
||||
if (!fontSize) {
|
||||
if (!metrics) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTextElement(element.orig)) {
|
||||
update.fontSize = fontSize;
|
||||
update.fontSize = metrics.size;
|
||||
update.baseline = metrics.baseline;
|
||||
}
|
||||
|
||||
if (boundTextElement) {
|
||||
boundTextUpdates = {
|
||||
fontSize,
|
||||
fontSize: metrics.size,
|
||||
baseline: metrics.baseline,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
FONT_FAMILY,
|
||||
isSafari,
|
||||
TEXT_ALIGN,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
@ -83,6 +84,7 @@ export const redrawTextBoundingBox = (
|
||||
text: textElement.text,
|
||||
width: textElement.width,
|
||||
height: textElement.height,
|
||||
baseline: textElement.baseline,
|
||||
};
|
||||
|
||||
boundTextUpdates.text = textElement.text;
|
||||
@ -97,6 +99,7 @@ export const redrawTextBoundingBox = (
|
||||
|
||||
boundTextUpdates.width = metrics.width;
|
||||
boundTextUpdates.height = metrics.height;
|
||||
boundTextUpdates.baseline = metrics.baseline;
|
||||
|
||||
// Maintain coordX for non left-aligned text in case the width has changed
|
||||
if (!container) {
|
||||
@ -210,13 +213,15 @@ export const handleBindTextResize = (
|
||||
const maxWidth = getMaxContainerWidth(container);
|
||||
const maxHeight = getMaxContainerHeight(container);
|
||||
let containerHeight = containerDims.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||
if (text) {
|
||||
text = wrapTextElement(textElement, maxWidth);
|
||||
}
|
||||
const dimensions = measureTextElement(textElement, { text });
|
||||
nextHeight = dimensions.height;
|
||||
nextWidth = dimensions.width;
|
||||
const metrics = measureTextElement(textElement, { text });
|
||||
nextHeight = metrics.height;
|
||||
nextWidth = metrics.width;
|
||||
nextBaseLine = metrics.baseline;
|
||||
}
|
||||
// increase height in case text element height exceeds
|
||||
if (nextHeight > maxHeight) {
|
||||
@ -244,6 +249,7 @@ export const handleBindTextResize = (
|
||||
text,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
baseline: nextBaseLine,
|
||||
});
|
||||
|
||||
if (!isArrowElement(container)) {
|
||||
@ -304,8 +310,59 @@ export const measureText = (
|
||||
const fontSize = parseFloat(font);
|
||||
const height = getTextHeight(text, fontSize, lineHeight);
|
||||
const width = getTextWidth(text, font);
|
||||
const baseline = measureBaseline(text, font, lineHeight);
|
||||
return { width, height, baseline };
|
||||
};
|
||||
|
||||
return { width, height };
|
||||
export const measureBaseline = (
|
||||
text: string,
|
||||
font: FontString,
|
||||
lineHeight: ExcalidrawTextElement["lineHeight"],
|
||||
wrapInContainer?: boolean,
|
||||
) => {
|
||||
const container = document.createElement("div");
|
||||
container.style.position = "absolute";
|
||||
container.style.whiteSpace = "pre";
|
||||
container.style.font = font;
|
||||
container.style.minHeight = "1em";
|
||||
if (wrapInContainer) {
|
||||
container.style.overflow = "hidden";
|
||||
container.style.wordBreak = "break-word";
|
||||
container.style.whiteSpace = "pre-wrap";
|
||||
}
|
||||
|
||||
container.style.lineHeight = String(lineHeight);
|
||||
|
||||
container.innerText = text;
|
||||
|
||||
// Baseline is important for positioning text on canvas
|
||||
document.body.appendChild(container);
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.style.display = "inline-block";
|
||||
span.style.overflow = "hidden";
|
||||
span.style.width = "1px";
|
||||
span.style.height = "1px";
|
||||
container.appendChild(span);
|
||||
let baseline = span.offsetTop + span.offsetHeight;
|
||||
const height = container.offsetHeight;
|
||||
|
||||
if (isSafari) {
|
||||
const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight);
|
||||
const fontSize = parseFloat(font);
|
||||
// In Safari the font size gets rounded off when rendering hence calculating the safari height and shifting the baseline if it differs
|
||||
// from the actual canvas height
|
||||
const domHeight = getTextHeight(text, Math.round(fontSize), lineHeight);
|
||||
if (canvasHeight > height) {
|
||||
baseline += canvasHeight - domHeight;
|
||||
}
|
||||
|
||||
if (height > canvasHeight) {
|
||||
baseline -= domHeight - canvasHeight;
|
||||
}
|
||||
}
|
||||
document.body.removeChild(container);
|
||||
return baseline;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
isBoundToContainer,
|
||||
isTextElement,
|
||||
} from "./typeChecks";
|
||||
import { CLASSES, VERTICAL_ALIGN } from "../constants";
|
||||
import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
@ -35,6 +35,7 @@ import {
|
||||
getMaxContainerHeight,
|
||||
getMaxContainerWidth,
|
||||
computeContainerDimensionForBoundText,
|
||||
detectLineHeight,
|
||||
} from "./textElement";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
@ -328,13 +329,24 @@ export const textWysiwyg = ({
|
||||
? offWidth / 2
|
||||
: 0;
|
||||
const { width: w, height: h } = updatedTextElement;
|
||||
|
||||
let lineHeight = updatedTextElement.lineHeight;
|
||||
|
||||
// In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size
|
||||
if (isSafari) {
|
||||
lineHeight = detectLineHeight({
|
||||
...updatedTextElement,
|
||||
fontSize: Math.round(updatedTextElement.fontSize),
|
||||
});
|
||||
}
|
||||
|
||||
// Make sure text editor height doesn't go beyond viewport
|
||||
const editorMaxHeight =
|
||||
(appState.height - viewportY) / appState.zoom.value;
|
||||
Object.assign(editable.style, {
|
||||
font: getFontString(updatedTextElement),
|
||||
// must be defined *after* font ¯\_(ツ)_/¯
|
||||
lineHeight: element.lineHeight,
|
||||
lineHeight,
|
||||
width: `${Math.min(textElementWidth, maxWidth)}px`,
|
||||
height: `${textElementHeight}px`,
|
||||
left: `${viewportX}px`,
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||
|
||||
export type ChartType = "bar" | "line";
|
||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
|
||||
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
|
||||
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
|
||||
export type Theme = typeof THEME[keyof typeof THEME];
|
||||
@ -133,6 +133,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
||||
fontSize: number;
|
||||
fontFamily: FontFamilyValues;
|
||||
text: string;
|
||||
baseline: number;
|
||||
textAlign: TextAlign;
|
||||
verticalAlign: VerticalAlign;
|
||||
containerId: ExcalidrawGenericElement["id"] | null;
|
||||
|
@ -5,6 +5,7 @@ import { getFontString, getFontFamilyString, isRTL } from "../../../../utils";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
getDefaultLineHeight,
|
||||
getMaxContainerWidth,
|
||||
getTextWidth,
|
||||
measureText,
|
||||
@ -859,7 +860,7 @@ const ensureMathElement = (element: Partial<ExcalidrawElement>) => {
|
||||
const cleanMathElementUpdate = function (updates) {
|
||||
const oldUpdates = {};
|
||||
for (const key in updates) {
|
||||
if (key !== "fontFamily") {
|
||||
if (key !== "fontFamily" && key !== "lineHeight") {
|
||||
(oldUpdates as any)[key] = (updates as any)[key];
|
||||
}
|
||||
if (key === "customData") {
|
||||
@ -874,6 +875,7 @@ const cleanMathElementUpdate = function (updates) {
|
||||
}
|
||||
}
|
||||
(updates as any).fontFamily = FONT_FAMILY_MATH;
|
||||
(updates as any).lineHeight = getDefaultLineHeight(FONT_FAMILY_MATH);
|
||||
return oldUpdates;
|
||||
} as SubtypeMethods["clean"];
|
||||
|
||||
@ -888,8 +890,8 @@ const measureMathElement = function (element, next) {
|
||||
ensureMathElement(element);
|
||||
const isMathJaxLoaded = mathJaxLoaded;
|
||||
if (!isMathJaxLoaded && isMathElement(element as ExcalidrawElement)) {
|
||||
const { width, height } = element as ExcalidrawMathElement;
|
||||
return { width, height };
|
||||
const { width, height, baseline } = element as ExcalidrawMathElement;
|
||||
return { width, height, baseline };
|
||||
}
|
||||
const fontSize = next?.fontSize ?? element.fontSize;
|
||||
const lineHeight = element.lineHeight;
|
||||
@ -903,8 +905,7 @@ const measureMathElement = function (element, next) {
|
||||
mathProps,
|
||||
isMathJaxLoaded,
|
||||
);
|
||||
const { width, height } = metrics;
|
||||
return { width, height };
|
||||
return metrics;
|
||||
} as SubtypeMethods["measureText"];
|
||||
|
||||
const renderMathElement = function (element, context, renderCb) {
|
||||
|
@ -246,7 +246,6 @@ const drawImagePlaceholder = (
|
||||
size,
|
||||
);
|
||||
};
|
||||
|
||||
const drawElementOnCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
@ -338,18 +337,16 @@ const drawElementOnCanvas = (
|
||||
: element.textAlign === "right"
|
||||
? element.width
|
||||
: 0;
|
||||
context.textBaseline = "bottom";
|
||||
|
||||
const lineHeightPx = getLineHeightInPx(
|
||||
element.fontSize,
|
||||
element.lineHeight,
|
||||
);
|
||||
|
||||
const verticalOffset = element.height - element.baseline;
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
context.fillText(
|
||||
lines[index],
|
||||
horizontalOffset,
|
||||
(index + 1) * lineHeightPx,
|
||||
(index + 1) * lineHeightPx - verticalOffset,
|
||||
);
|
||||
}
|
||||
context.restore();
|
||||
|
@ -237,7 +237,7 @@ export type SubtypeMethods = {
|
||||
text?: string;
|
||||
customData?: ExcalidrawElement["customData"];
|
||||
},
|
||||
) => { width: number; height: number };
|
||||
) => { width: number; height: number; baseline: number };
|
||||
render: (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
context: CanvasRenderingContext2D,
|
||||
|
@ -282,6 +282,7 @@ exports[`restoreElements should restore text element correctly passing value for
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"baseline": 0,
|
||||
"boundElements": Array [],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
@ -321,6 +322,7 @@ exports[`restoreElements should restore text element correctly with unknown font
|
||||
Object {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"baseline": 0,
|
||||
"boundElements": Array [],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
|
Loading…
x
Reference in New Issue
Block a user