Compare commits

..
Author SHA1 Message Date
dwelle 94005b790a fix: icons not rotated properly in old safari 2024-05-15 10:20:04 +02:00
37 changed files with 489 additions and 1608 deletions
@@ -1,8 +1,8 @@
import {
BOUND_TEXT_PADDING,
ROUNDNESS,
TEXT_ALIGN,
VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants";
import { isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
@@ -142,7 +142,6 @@ export const actionBindText = register({
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
});
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
@@ -297,7 +296,6 @@ export const actionWrapTextInContainer = register({
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true,
},
false,
);
+2 -10
View File
@@ -65,10 +65,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => {
const { isUndoStackEmpty } = useEmitter<HistoryChangedEvent>(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
new HistoryChangedEvent(),
);
return (
@@ -79,7 +76,6 @@ export const createUndoAction: ActionCreator = (history, store) => ({
onClick={updateData}
size={data?.size || "medium"}
disabled={isUndoStackEmpty}
data-testid="button-undo"
/>
);
},
@@ -107,10 +103,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
PanelComponent: ({ updateData, data }) => {
const { isRedoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty,
),
new HistoryChangedEvent(),
);
return (
@@ -121,7 +114,6 @@ export const createRedoAction: ActionCreator = (history, store) => ({
onClick={updateData}
size={data?.size || "medium"}
disabled={isRedoStackEmpty}
data-testid="button-redo"
/>
);
},
@@ -167,7 +167,7 @@ const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement,
) => {
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
if (isBoundToContainer(nextElement)) {
return nextElement;
}
return mutateElement(
@@ -1,48 +0,0 @@
import { isTextElement } from "../element";
import { newElementWith } from "../element/mutateElement";
import { measureText } from "../element/textElement";
import { getSelectedElements } from "../scene";
import { StoreAction } from "../store";
import type { AppClassProperties } from "../types";
import { getFontString } from "../utils";
import { register } from "./register";
export const actionTextAutoResize = register({
name: "autoResize",
label: "labels.autoResize",
icon: null,
trackEvent: { category: "element" },
predicate: (elements, appState, _: unknown, app: AppClassProperties) => {
const selectedElements = getSelectedElements(elements, appState);
return (
selectedElements.length === 1 &&
isTextElement(selectedElements[0]) &&
!selectedElements[0].autoResize
);
},
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
return {
appState,
elements: elements.map((element) => {
if (element.id === selectedElements[0].id && isTextElement(element)) {
const metrics = measureText(
element.originalText,
getFontString(element),
element.lineHeight,
);
return newElementWith(element, {
autoResize: true,
width: metrics.width,
height: metrics.height,
text: element.originalText,
});
}
return element;
}),
storeAction: StoreAction.CAPTURE,
};
},
});
+1 -2
View File
@@ -134,8 +134,7 @@ export type ActionName =
| "setEmbeddableAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer"
| "commandPalette"
| "autoResize";
| "commandPalette";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
@@ -468,7 +468,6 @@ export const ExitZenModeAction = ({
showExitZenModeBtn: boolean;
}) => (
<button
type="button"
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
+26 -39
View File
@@ -114,7 +114,7 @@ import {
newTextElement,
newImageElement,
transformElements,
refreshTextDimensions,
updateTextElement,
redrawTextBoundingBox,
getElementAbsoluteCoords,
} from "../element";
@@ -429,7 +429,6 @@ import {
isPointHittingLinkIcon,
} from "./hyperlink/helpers";
import { getShortcutFromShortcutName } from "../actions/shortcuts";
import { actionTextAutoResize } from "../actions/actionTextAutoResize";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@@ -2594,9 +2593,6 @@ class App extends React.Component<AppProps, AppState> {
),
addEventListener(window, EVENT.FOCUS, () => {
this.maybeCleanupAfterMissingPointerUp(null);
// browsers (chrome?) tend to free up memory a lot, which results
// in canvas context being cleared. Thus re-render on focus.
this.triggerRender(true);
}),
);
@@ -3732,15 +3728,8 @@ class App extends React.Component<AppProps, AppState> {
},
);
private triggerRender = (
/** force always re-renders canvas even if no change */
force?: boolean,
) => {
if (force === true) {
this.scene.triggerUpdate();
} else {
this.setState({});
}
private triggerRender = () => {
this.setState({});
};
/**
@@ -4309,22 +4298,25 @@ class App extends React.Component<AppProps, AppState> {
) {
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = (nextOriginalText: string, isDeleted: boolean) => {
const updateElement = (
text: string,
originalText: string,
isDeleted: boolean,
) => {
this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) {
return newElementWith(_element, {
originalText: nextOriginalText,
isDeleted: isDeleted ?? _element.isDeleted,
// returns (wrapped) text and new dimensions
...refreshTextDimensions(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
nextOriginalText,
),
});
return updateTextElement(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
{
text,
isDeleted,
originalText,
},
);
}
return _element;
}),
@@ -4347,15 +4339,15 @@ class App extends React.Component<AppProps, AppState> {
viewportY - this.state.offsetTop,
];
},
onChange: withBatchedUpdates((nextOriginalText) => {
updateElement(nextOriginalText, false);
onChange: withBatchedUpdates((text) => {
updateElement(text, text, false);
if (isNonDeletedElement(element)) {
updateBoundElements(element, elementsMap);
}
}),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
const isDeleted = !nextOriginalText.trim();
updateElement(nextOriginalText, isDeleted);
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
const isDeleted = !text.trim();
updateElement(text, originalText, isDeleted);
// select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) {
@@ -4400,7 +4392,7 @@ class App extends React.Component<AppProps, AppState> {
// do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.originalText, false);
updateElement(element.text, element.originalText, false);
}
private deselectElements() {
@@ -5107,11 +5099,8 @@ class App extends React.Component<AppProps, AppState> {
this.translateCanvas({
zoom: zoomState.zoom,
// 2x multiplier is just a magic number that makes this work correctly
// on touchscreen devices (note: if we get report that panning is slower/faster
// than actual movement, consider swapping with devicePixelRatio)
scrollX: zoomState.scrollX + 2 * (deltaX / nextZoom),
scrollY: zoomState.scrollY + 2 * (deltaY / nextZoom),
scrollX: zoomState.scrollX + deltaX / nextZoom,
scrollY: zoomState.scrollY + deltaY / nextZoom,
shouldCacheIgnoreZoom: true,
});
});
@@ -9642,7 +9631,6 @@ class App extends React.Component<AppProps, AppState> {
}
return [
CONTEXT_MENU_SEPARATOR,
actionCut,
actionCopy,
actionPaste,
@@ -9655,7 +9643,6 @@ class App extends React.Component<AppProps, AppState> {
actionPasteStyles,
CONTEXT_MENU_SEPARATOR,
actionGroup,
actionTextAutoResize,
actionUnbindText,
actionBindText,
actionWrapTextInContainer,
@@ -28,7 +28,6 @@ export const ButtonIconSelect = <T extends Object>(
{props.options.map((option) =>
props.type === "button" ? (
<button
type="button"
key={option.text}
onClick={(event) => props.onClick(option.value, event)}
className={clsx({
@@ -22,12 +22,7 @@ export const CheckboxItem: React.FC<{
).focus();
}}
>
<button
type="button"
className="Checkbox-box"
role="checkbox"
aria-checked={checked}
>
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
{checkIcon}
</button>
<div className="Checkbox-label">{children}</div>
@@ -105,7 +105,6 @@ export const ContextMenu = React.memo(
}}
>
<button
type="button"
className={clsx("context-menu-item", {
dangerous: actionName === "deleteSelectedElements",
checkmark: item.checked?.(appState),
@@ -123,7 +123,6 @@ export const Dialog = (props: DialogProps) => {
onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")}
type="button"
>
{CloseIcon}
</button>
@@ -27,11 +27,7 @@ const FollowMode = ({
{userToFollow.username}
</span>
</div>
<button
type="button"
onClick={onDisconnect}
className="follow-mode__disconnect-btn"
>
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
{CloseIcon}
</button>
</div>
@@ -108,7 +108,6 @@ function Picker<T>({
<div className="picker-content" ref={rGallery}>
{options.map((option, i) => (
<button
type="button"
className={clsx("picker-option", {
active: value === option.value,
})}
@@ -172,7 +171,6 @@ export function IconPicker<T>({
<div>
<button
name={group}
type="button"
className={isActive ? "active" : ""}
aria-label={label}
onClick={() => setActive(!isActive)}
@@ -555,7 +555,6 @@ const LayerUI = ({
)}
{appState.scrolledOutside && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
@@ -194,7 +194,6 @@ export const MobileMenu = ({
!appState.openMenu &&
!appState.openSidebar && (
<button
type="button"
className="scroll-back-to-content"
onClick={() => {
setAppState((appState) => ({
@@ -65,7 +65,6 @@ const ChartPreviewBtn = (props: {
return (
<button
type="button"
className="ChartPreview"
onClick={() => {
if (chartElements) {
@@ -228,7 +228,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [
{
@@ -274,7 +273,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [
{
@@ -380,7 +378,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id48",
@@ -481,7 +478,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id37",
@@ -656,7 +652,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id41",
@@ -697,7 +692,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [
{
@@ -743,7 +737,6 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [
{
@@ -1201,7 +1194,6 @@ exports[`Test Transform > should transform regular shapes 6`] = `
exports[`Test Transform > should transform text element 1`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": null,
@@ -1242,7 +1234,6 @@ exports[`Test Transform > should transform text element 1`] = `
exports[`Test Transform > should transform text element 2`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": null,
@@ -1575,7 +1566,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
exports[`Test Transform > should transform the elements correctly when linear elements have single point 7`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "B",
@@ -1618,7 +1608,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
exports[`Test Transform > should transform the elements correctly when linear elements have single point 8`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "A",
@@ -1661,7 +1650,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
exports[`Test Transform > should transform the elements correctly when linear elements have single point 9`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "Alice",
@@ -1704,7 +1692,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
exports[`Test Transform > should transform the elements correctly when linear elements have single point 10`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "Bob",
@@ -1747,7 +1734,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
exports[`Test Transform > should transform the elements correctly when linear elements have single point 11`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "Bob_Alice",
@@ -1788,7 +1774,6 @@ exports[`Test Transform > should transform the elements correctly when linear el
exports[`Test Transform > should transform the elements correctly when linear elements have single point 12`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "Bob_B",
@@ -2037,7 +2022,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 5`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id25",
@@ -2078,7 +2062,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 6`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id26",
@@ -2119,7 +2102,6 @@ exports[`Test Transform > should transform to labelled arrows when label provide
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 7`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id27",
@@ -2161,7 +2143,6 @@ LABELLED ARROW",
exports[`Test Transform > should transform to labelled arrows when label provided for arrows 8`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id28",
@@ -2425,7 +2406,6 @@ exports[`Test Transform > should transform to text containers when label provide
exports[`Test Transform > should transform to text containers when label provided 7`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id13",
@@ -2466,7 +2446,6 @@ exports[`Test Transform > should transform to text containers when label provide
exports[`Test Transform > should transform to text containers when label provided 8`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id14",
@@ -2508,7 +2487,6 @@ CONTAINER",
exports[`Test Transform > should transform to text containers when label provided 9`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id15",
@@ -2552,7 +2530,6 @@ CONTAINER",
exports[`Test Transform > should transform to text containers when label provided 10`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id16",
@@ -2594,7 +2571,6 @@ TEXT CONTAINER",
exports[`Test Transform > should transform to text containers when label provided 11`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id17",
@@ -2637,7 +2613,6 @@ CONTAINER",
exports[`Test Transform > should transform to text containers when label provided 12`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": null,
"containerId": "id18",
+2 -2
View File
@@ -140,7 +140,7 @@ const restoreElementWithProperties = <
seed: element.seed ?? 1,
groupIds: element.groupIds ?? [],
frameId: element.frameId ?? null,
roundness: typeof element.roundness !== "undefined"
roundness: element.roundness
? element.roundness
: element.strokeSharpness === "round"
? {
@@ -208,7 +208,7 @@ const restoreElement = (
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
containerId: element.containerId ?? null,
originalText: element.originalText || text,
autoResize: element.autoResize ?? true,
lineHeight,
});
+1
View File
@@ -9,6 +9,7 @@ import { isLinearElementType } from "./typeChecks";
export {
newElement,
newTextElement,
updateTextElement,
refreshTextDimensions,
newLinearElement,
newImageElement,
+41 -33
View File
@@ -240,28 +240,24 @@ export const newTextElement = (
metrics,
);
const textElementProps: ExcalidrawTextElement = {
..._newElementBase<ExcalidrawTextElement>("text", opts),
text,
fontSize,
fontFamily,
textAlign,
verticalAlign,
x: opts.x - offsets.x,
y: opts.y - offsets.y,
width: metrics.width,
height: metrics.height,
containerId: opts.containerId || null,
originalText: text,
autoResize: true,
lineHeight,
};
const textElement: ExcalidrawTextElement = newElementWith(
textElementProps,
const textElement = newElementWith(
{
..._newElementBase<ExcalidrawTextElement>("text", opts),
text,
fontSize,
fontFamily,
textAlign,
verticalAlign,
x: opts.x - offsets.x,
y: opts.y - offsets.y,
width: metrics.width,
height: metrics.height,
containerId: opts.containerId || null,
originalText: text,
lineHeight,
},
{},
);
return textElement;
};
@@ -275,25 +271,18 @@ const getAdjustedDimensions = (
width: number;
height: number;
} => {
let { width: nextWidth, height: nextHeight } = measureText(
const { width: nextWidth, height: nextHeight } = measureText(
nextText,
getFontString(element),
element.lineHeight,
);
// wrapped text
if (!element.autoResize) {
nextWidth = element.width;
}
const { textAlign, verticalAlign } = element;
let x: number;
let y: number;
if (
textAlign === "center" &&
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId &&
element.autoResize
!element.containerId
) {
const prevMetrics = measureText(
element.text,
@@ -354,19 +343,38 @@ export const refreshTextDimensions = (
if (textElement.isDeleted) {
return;
}
if (container || !textElement.autoResize) {
if (container) {
text = wrapText(
text,
getFontString(textElement),
container
? getBoundTextMaxWidth(container, textElement)
: textElement.width,
getBoundTextMaxWidth(container, textElement),
);
}
const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
return { text, ...dimensions };
};
export const updateTextElement = (
textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
{
text,
isDeleted,
originalText,
}: {
text: string;
isDeleted?: boolean;
originalText: string;
},
): ExcalidrawTextElement => {
return newElementWith(textElement, {
originalText,
isDeleted: isDeleted ?? textElement.isDeleted,
...refreshTextDimensions(textElement, container, elementsMap, originalText),
});
};
export const newFreeDrawElement = (
opts: {
type: "freedraw";
+20 -121
View File
@@ -1,8 +1,4 @@
import {
BOUND_TEXT_PADDING,
MIN_FONT_SIZE,
SHIFT_LOCKING_ANGLE,
} from "../constants";
import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants";
import { rescalePoints } from "../points";
import { rotate, centerPoint, rotatePoint } from "../math";
@@ -49,9 +45,6 @@ import {
handleBindTextResize,
getBoundTextMaxWidth,
getApproxMinLineHeight,
wrapText,
measureText,
getMinCharWidth,
} from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { isInGroup } from "../groups";
@@ -91,9 +84,14 @@ export const transformElements = (
shouldRotateWithDiscreteAngle,
);
updateBoundElements(element, elementsMap);
} else if (isTextElement(element) && transformHandleType) {
} else if (
isTextElement(element) &&
(transformHandleType === "nw" ||
transformHandleType === "ne" ||
transformHandleType === "sw" ||
transformHandleType === "se")
) {
resizeSingleTextElement(
originalElements,
element,
elementsMap,
transformHandleType,
@@ -225,10 +223,9 @@ const measureFontSizeFromWidth = (
};
const resizeSingleTextElement = (
originalElements: PointerDownState["originalElements"],
element: NonDeleted<ExcalidrawTextElement>,
elementsMap: ElementsMap,
transformHandleType: TransformHandleDirection,
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
pointerY: number,
@@ -248,19 +245,17 @@ const resizeSingleTextElement = (
let scaleX = 0;
let scaleY = 0;
if (transformHandleType !== "e" && transformHandleType !== "w") {
if (transformHandleType.includes("e")) {
scaleX = (rotatedX - x1) / (x2 - x1);
}
if (transformHandleType.includes("w")) {
scaleX = (x2 - rotatedX) / (x2 - x1);
}
if (transformHandleType.includes("n")) {
scaleY = (y2 - rotatedY) / (y2 - y1);
}
if (transformHandleType.includes("s")) {
scaleY = (rotatedY - y1) / (y2 - y1);
}
if (transformHandleType.includes("e")) {
scaleX = (rotatedX - x1) / (x2 - x1);
}
if (transformHandleType.includes("w")) {
scaleX = (x2 - rotatedX) / (x2 - x1);
}
if (transformHandleType.includes("n")) {
scaleY = (y2 - rotatedY) / (y2 - y1);
}
if (transformHandleType.includes("s")) {
scaleY = (rotatedY - y1) / (y2 - y1);
}
const scale = Math.max(scaleX, scaleY);
@@ -323,102 +318,6 @@ const resizeSingleTextElement = (
y: nextY,
});
}
if (transformHandleType === "e" || transformHandleType === "w") {
const stateAtResizeStart = originalElements.get(element.id)!;
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
stateAtResizeStart,
stateAtResizeStart.width,
stateAtResizeStart.height,
true,
);
const startTopLeft: Point = [x1, y1];
const startBottomRight: Point = [x2, y2];
const startCenter: Point = centerPoint(startTopLeft, startBottomRight);
const rotatedPointer = rotatePoint(
[pointerX, pointerY],
startCenter,
-stateAtResizeStart.angle,
);
const [esx1, , esx2] = getResizedElementAbsoluteCoords(
element,
element.width,
element.height,
true,
);
const boundsCurrentWidth = esx2 - esx1;
const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0];
const minWidth =
getMinCharWidth(getFontString(element)) + BOUND_TEXT_PADDING * 2;
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
if (transformHandleType.includes("e")) {
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
}
if (transformHandleType.includes("w")) {
scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth;
}
const newWidth =
element.width * scaleX < minWidth ? minWidth : element.width * scaleX;
const text = wrapText(
element.originalText,
getFontString(element),
Math.abs(newWidth),
);
const metrics = measureText(
text,
getFontString(element),
element.lineHeight,
);
const eleNewHeight = metrics.height;
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
getResizedElementAbsoluteCoords(
stateAtResizeStart,
newWidth,
eleNewHeight,
true,
);
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
let newTopLeft = [...startTopLeft] as [number, number];
if (["n", "w", "nw"].includes(transformHandleType)) {
newTopLeft = [
startBottomRight[0] - Math.abs(newBoundsWidth),
startTopLeft[1],
];
}
// adjust topLeft to new rotation point
const angle = stateAtResizeStart.angle;
const rotatedTopLeft = rotatePoint(newTopLeft, startCenter, angle);
const newCenter: Point = [
newTopLeft[0] + Math.abs(newBoundsWidth) / 2,
newTopLeft[1] + Math.abs(newBoundsHeight) / 2,
];
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
const resizedElement: Partial<ExcalidrawTextElement> = {
width: Math.abs(newWidth),
height: Math.abs(metrics.height),
x: newTopLeft[0],
y: newTopLeft[1],
text,
autoResize: false,
};
mutateElement(element, resizedElement);
}
};
export const resizeSingleElement = (
+6 -2
View File
@@ -87,8 +87,12 @@ export const resizeTest = (
elementsMap,
);
// do not resize from the sides for linear elements with only two points
if (!(isLinearElement(element) && element.points.length <= 2)) {
// Note that for a text element, when "resized" from the side
// we should make it wrap/unwrap
if (
element.type !== "text" &&
!(isLinearElement(element) && element.points.length <= 2)
) {
const SPACING = SIDE_RESIZING_THRESHOLD / zoom.value;
const sides = getSelectionBorders(
[x1 - SPACING, y1 - SPACING],
+4 -10
View File
@@ -48,7 +48,7 @@ export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null,
elementsMap: ElementsMap,
informMutation = true,
informMutation: boolean = true,
) => {
let maxWidth = undefined;
const boundTextUpdates = {
@@ -62,27 +62,21 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text = textElement.text;
if (container || !textElement.autoResize) {
maxWidth = container
? getBoundTextMaxWidth(container, textElement)
: textElement.width;
if (container) {
maxWidth = getBoundTextMaxWidth(container, textElement);
boundTextUpdates.text = wrapText(
textElement.originalText,
getFontString(textElement),
maxWidth,
);
}
const metrics = measureText(
boundTextUpdates.text,
getFontString(textElement),
textElement.lineHeight,
);
// Note: only update width for unwrapped text and bound texts (which always have autoResize set to true)
if (textElement.autoResize) {
boundTextUpdates.width = metrics.width;
}
boundTextUpdates.width = metrics.width;
boundTextUpdates.height = metrics.height;
if (container) {
+12 -112
View File
@@ -236,117 +236,6 @@ describe("textWysiwyg", () => {
});
});
describe("Test text wrapping", () => {
const { h } = window;
const dimensions = { height: 400, width: 800 };
beforeAll(() => {
mockBoundingClientRect(dimensions);
});
beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
// @ts-ignore
h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
h.elements = [];
});
afterAll(() => {
restoreOriginalGetBoundingClientRect();
});
it("should keep width when editing a wrapped text", async () => {
const text = API.createElement({
type: "text",
text: "Excalidraw\nEditor",
});
h.elements = [text];
const prevWidth = text.width;
const prevHeight = text.height;
const prevText = text.text;
// text is wrapped
UI.resize(text, "e", [-20, 0]);
expect(text.width).not.toEqual(prevWidth);
expect(text.height).not.toEqual(prevHeight);
expect(text.text).not.toEqual(prevText);
expect(text.autoResize).toBe(false);
const wrappedWidth = text.width;
const wrappedHeight = text.height;
const wrappedText = text.text;
// edit text
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
const editor = await getTextEditor(textEditorSelector);
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1);
const nextText = `${wrappedText} is great!`;
updateTextEditor(editor, nextText);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
expect(h.elements[0].width).toEqual(wrappedWidth);
expect(h.elements[0].height).toBeGreaterThan(wrappedHeight);
// remove all texts and then add it back editing
updateTextEditor(editor, "");
await new Promise((cb) => setTimeout(cb, 0));
updateTextEditor(editor, nextText);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
expect(h.elements[0].width).toEqual(wrappedWidth);
});
it("should restore original text after unwrapping a wrapped text", async () => {
const originalText = "Excalidraw\neditor\nis great!";
const text = API.createElement({
type: "text",
text: originalText,
});
h.elements = [text];
// wrap
UI.resize(text, "e", [-40, 0]);
// enter text editing mode
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
const editor = await getTextEditor(textEditorSelector);
editor.blur();
// restore after unwrapping
UI.resize(text, "e", [40, 0]);
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
// wrap again and add a new line
UI.resize(text, "e", [-30, 0]);
const wrappedText = text.text;
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
updateTextEditor(editor, `${wrappedText}\nA new line!`);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
// remove the newly added line
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
updateTextEditor(editor, wrappedText);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
// unwrap
UI.resize(text, "e", [30, 0]);
// expect the text to be restored the same
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
});
});
describe("Test container-unbound text", () => {
const { h } = window;
const dimensions = { height: 400, width: 800 };
@@ -911,15 +800,26 @@ describe("textWysiwyg", () => {
mouse.down();
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
const editor = await getTextEditor(textEditorSelector, true);
let editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
UI.clickTool("text");
mouse.clickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
mouse.down();
editor = await getTextEditor(textEditorSelector, true);
editor.select();
fireEvent.click(screen.getByTitle(/code/i));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
+14 -11
View File
@@ -79,14 +79,12 @@ export const textWysiwyg = ({
app,
}: {
id: ExcalidrawElement["id"];
/**
* textWysiwyg only deals with `originalText`
*
* Note: `text`, which can be wrapped and therefore different from `originalText`,
* is derived from `originalText`
*/
onChange?: (nextOriginalText: string) => void;
onSubmit: (data: { viaKeyboard: boolean; nextOriginalText: string }) => void;
onChange?: (text: string) => void;
onSubmit: (data: {
text: string;
viaKeyboard: boolean;
originalText: string;
}) => void;
getViewportCoords: (x: number, y: number) => [number, number];
element: ExcalidrawTextElement;
canvas: HTMLCanvasElement;
@@ -131,8 +129,11 @@ export const textWysiwyg = ({
app.scene.getNonDeletedElementsMap(),
);
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
let textElementWidth = updatedTextElement.width;
// Set to element height by default since that's
// what is going to be used for unbounded text
const textElementHeight = updatedTextElement.height;
if (container && updatedTextElement.containerId) {
@@ -261,7 +262,6 @@ export const textWysiwyg = ({
if (isTestEnv()) {
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
}
mutateElement(updatedTextElement, { x: coordX, y: coordY });
}
};
@@ -278,7 +278,7 @@ export const textWysiwyg = ({
let whiteSpace = "pre";
let wordBreak = "normal";
if (isBoundToContainer(element) || !element.autoResize) {
if (isBoundToContainer(element)) {
whiteSpace = "pre-wrap";
wordBreak = "break-word";
}
@@ -501,12 +501,14 @@ export const textWysiwyg = ({
if (!updateElement) {
return;
}
let text = editable.value;
const container = getContainerElement(
updateElement,
app.scene.getNonDeletedElementsMap(),
);
if (container) {
text = updateElement.text;
if (editable.value.trim()) {
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) {
@@ -538,8 +540,9 @@ export const textWysiwyg = ({
}
onSubmit({
text,
viaKeyboard: submittedViaKeyboard,
nextOriginalText: editable.value,
originalText: editable.value,
});
};
@@ -9,6 +9,7 @@ import type { Bounds } from "./bounds";
import { getElementAbsoluteCoords } from "./bounds";
import { rotate } from "../math";
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
import { isTextElement } from ".";
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
import {
DEFAULT_TRANSFORM_HANDLE_SPACING,
@@ -64,6 +65,13 @@ export const OMIT_SIDES_FOR_FRAME = {
rotation: true,
};
const OMIT_SIDES_FOR_TEXT_ELEMENT = {
e: true,
s: true,
n: true,
w: true,
};
const OMIT_SIDES_FOR_LINE_SLASH = {
e: true,
s: true,
@@ -282,6 +290,8 @@ export const getTransformHandles = (
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
}
}
} else if (isTextElement(element)) {
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
} else if (isFrameLikeElement(element)) {
omitSides = {
...omitSides,
-7
View File
@@ -193,13 +193,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;
originalText: string;
/**
* If `true` the width will fit the text. If `false`, the text will
* wrap to fit the width.
*
* @default true
*/
autoResize: boolean;
/**
* Unitless line height (aligned to W3C). To get line height in px, multiply
* with font size (using `getLineHeightInPx` helper).
+1 -2
View File
@@ -149,8 +149,7 @@
"zoomToFitViewport": "Zoom to fit in viewport",
"zoomToFitSelection": "Zoom to fit selection",
"zoomToFit": "Zoom to fit all elements",
"installPWA": "Install Excalidraw locally (PWA)",
"autoResize": "Enable text auto-resizing"
"installPWA": "Install Excalidraw locally (PWA)"
},
"library": {
"noItems": "No items added yet...",
+1 -1
View File
@@ -58,7 +58,7 @@
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@excalidraw/laser-pointer": "1.3.1",
"@excalidraw/mermaid-to-excalidraw": "1.0.0",
"@excalidraw/mermaid-to-excalidraw": "0.3.0",
"@excalidraw/random-username": "1.1.0",
"@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2",
@@ -12,7 +12,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"collaborators": Map {},
"contextMenu": {
"items": [
"separator",
{
"icon": <svg
aria-hidden="true"
@@ -327,16 +326,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"category": "element",
},
},
{
"icon": null,
"label": "labels.autoResize",
"name": "autoResize",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
{
"label": "labels.unbindText",
"name": "unbindText",
@@ -398,11 +387,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -497,11 +482,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -4433,7 +4414,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"collaborators": Map {},
"contextMenu": {
"items": [
"separator",
{
"icon": <svg
aria-hidden="true"
@@ -4748,16 +4728,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"category": "element",
},
},
{
"icon": null,
"label": "labels.autoResize",
"name": "autoResize",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
{
"label": "labels.unbindText",
"name": "unbindText",
@@ -4819,11 +4789,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -4918,11 +4884,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -5552,7 +5514,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"collaborators": Map {},
"contextMenu": {
"items": [
"separator",
{
"icon": <svg
aria-hidden="true"
@@ -5867,16 +5828,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"category": "element",
},
},
{
"icon": null,
"label": "labels.autoResize",
"name": "autoResize",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
{
"label": "labels.unbindText",
"name": "unbindText",
@@ -5938,11 +5889,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -6037,11 +5984,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -7378,7 +7321,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"collaborators": Map {},
"contextMenu": {
"items": [
"separator",
{
"icon": <svg
aria-hidden="true"
@@ -7693,16 +7635,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"category": "element",
},
},
{
"icon": null,
"label": "labels.autoResize",
"name": "autoResize",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
{
"label": "labels.unbindText",
"name": "unbindText",
@@ -7764,11 +7696,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -7863,11 +7791,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -8264,7 +8188,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"collaborators": Map {},
"contextMenu": {
"items": [
"separator",
{
"icon": <svg
aria-hidden="true"
@@ -8579,16 +8502,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"category": "element",
},
},
{
"icon": null,
"label": "labels.autoResize",
"name": "autoResize",
"perform": [Function],
"predicate": [Function],
"trackEvent": {
"category": "element",
},
},
{
"label": "labels.unbindText",
"name": "unbindText",
@@ -8650,11 +8563,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
@@ -8749,11 +8658,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
style={
{
"transform": "rotate(180deg)",
}
}
transform="rotate(180)"
viewBox="0 0 24 24"
>
<g
File diff suppressed because it is too large Load Diff
@@ -10752,7 +10752,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"pendingImageElementId": null,
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollX": -6.2500000000000036,
"scrollX": -2.916666666666668,
"scrollY": 0,
"scrolledOutside": false,
"selectedElementIds": {},
@@ -13688,8 +13688,8 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"pendingImageElementId": null,
"previousSelectedElementIds": {},
"resizingElement": null,
"scrollX": 20,
"scrollY": -18.535533905932738,
"scrollX": 10,
"scrollY": -10,
"scrolledOutside": false,
"selectedElementIds": {},
"selectedElementsAreBeingDragged": false,
@@ -302,7 +302,6 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
exports[`restoreElements > should restore text element correctly passing value for each attribute 1`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [],
"containerId": null,
@@ -345,7 +344,6 @@ exports[`restoreElements > should restore text element correctly passing value f
exports[`restoreElements > should restore text element correctly with unknown font family, null text and undefined alignment 1`] = `
{
"angle": 0,
"autoResize": true,
"backgroundColor": "transparent",
"boundElements": [],
"containerId": null,
+2 -125
View File
@@ -10,9 +10,8 @@ import { Excalidraw } from "../index";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState";
import { fireEvent, queryByTestId, waitFor } from "@testing-library/react";
import { fireEvent, waitFor } from "@testing-library/react";
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import type { AppState, ExcalidrawImperativeAPI } from "../types";
import { arrayToMap, resolvablePromise } from "../utils";
@@ -50,6 +49,7 @@ const checkpoint = (name: string) => {
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
`[${name}] number of renders`,
);
// `scrolledOutside` does not appear to be stable between test runs
// `selectedLinearElemnt` includes `startBindingElement` containing seed and versionNonce
const {
@@ -1688,129 +1688,6 @@ describe("history", () => {
]);
});
});
it("should disable undo/redo buttons when stacks empty", async () => {
const { container } = await render(
<Excalidraw
initialData={{
elements: [API.createElement({ type: "rectangle", id: "A" })],
}}
/>,
);
const undoAction = createUndoAction(h.history, h.store);
const redoAction = createRedoAction(h.history, h.store);
await waitFor(() => {
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeTruthy();
});
const undoButton = queryByTestId(container, "button-undo");
const redoButton = queryByTestId(container, "button-redo");
expect(undoButton).toBeDisabled();
expect(redoButton).toBeDisabled();
const rectangle = UI.createElement("rectangle");
expect(h.elements).toEqual([
expect.objectContaining({ id: "A" }),
expect.objectContaining({ id: rectangle.id }),
]);
expect(h.history.isUndoStackEmpty).toBeFalsy();
expect(h.history.isRedoStackEmpty).toBeTruthy();
expect(undoButton).not.toBeDisabled();
expect(redoButton).toBeDisabled();
act(() => h.app.actionManager.executeAction(undoAction));
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeFalsy();
expect(undoButton).toBeDisabled();
expect(redoButton).not.toBeDisabled();
act(() => h.app.actionManager.executeAction(redoAction));
expect(h.history.isUndoStackEmpty).toBeFalsy();
expect(h.history.isRedoStackEmpty).toBeTruthy();
expect(undoButton).not.toBeDisabled();
expect(redoButton).toBeDisabled();
});
it("remounting undo/redo buttons should initialize undo/redo state correctly", async () => {
const { container } = await render(
<Excalidraw
initialData={{
elements: [API.createElement({ type: "rectangle", id: "A" })],
}}
/>,
);
const undoAction = createUndoAction(h.history, h.store);
await waitFor(() => {
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]);
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeTruthy();
});
expect(queryByTestId(container, "button-undo")).toBeDisabled();
expect(queryByTestId(container, "button-redo")).toBeDisabled();
// testing undo button
// -----------------------------------------------------------------------
const rectangle = UI.createElement("rectangle");
expect(h.elements).toEqual([
expect.objectContaining({ id: "A" }),
expect.objectContaining({ id: rectangle.id }),
]);
expect(h.history.isUndoStackEmpty).toBeFalsy();
expect(h.history.isRedoStackEmpty).toBeTruthy();
expect(queryByTestId(container, "button-undo")).not.toBeDisabled();
expect(queryByTestId(container, "button-redo")).toBeDisabled();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
expect(h.state.viewModeEnabled).toBe(true);
expect(queryByTestId(container, "button-undo")).toBeNull();
expect(queryByTestId(container, "button-redo")).toBeNull();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
expect(h.state.viewModeEnabled).toBe(false);
await waitFor(() => {
expect(queryByTestId(container, "button-undo")).not.toBeDisabled();
expect(queryByTestId(container, "button-redo")).toBeDisabled();
});
// testing redo button
// -----------------------------------------------------------------------
act(() => h.app.actionManager.executeAction(undoAction));
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeFalsy();
expect(queryByTestId(container, "button-undo")).toBeDisabled();
expect(queryByTestId(container, "button-redo")).not.toBeDisabled();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
expect(h.state.viewModeEnabled).toBe(true);
expect(queryByTestId(container, "button-undo")).toBeNull();
expect(queryByTestId(container, "button-redo")).toBeNull();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
expect(h.state.viewModeEnabled).toBe(false);
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeFalsy();
expect(queryByTestId(container, "button-undo")).toBeDisabled();
expect(queryByTestId(container, "button-redo")).not.toBeDisabled();
});
});
describe("multiplayer undo/redo", () => {
@@ -972,10 +972,10 @@ describe("Test Linear Elements", () => {
]);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
.toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
"Online whiteboard
collaboration made
easy"
`);
});
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
@@ -1006,10 +1006,10 @@ describe("Test Linear Elements", () => {
]);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
.toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
"Online whiteboard
collaboration made
easy"
`);
});
it("should not bind text to line when double clicked", async () => {
-106
View File
@@ -426,112 +426,6 @@ describe("text element", () => {
expect(text.fontSize).toBe(fontSize);
});
});
// text can be resized from sides
it("can be resized from e", async () => {
const text = UI.createElement("text");
await UI.editText(text, "Excalidraw\nEditor");
const width = text.width;
const height = text.height;
UI.resize(text, "e", [30, 0]);
expect(text.width).toBe(width + 30);
expect(text.height).toBe(height);
UI.resize(text, "e", [-30, 0]);
expect(text.width).toBe(width);
expect(text.height).toBe(height);
});
it("can be resized from w", async () => {
const text = UI.createElement("text");
await UI.editText(text, "Excalidraw\nEditor");
const width = text.width;
const height = text.height;
UI.resize(text, "w", [-50, 0]);
expect(text.width).toBe(width + 50);
expect(text.height).toBe(height);
UI.resize(text, "w", [50, 0]);
expect(text.width).toBe(width);
expect(text.height).toBe(height);
});
it("wraps when width is narrower than texts inside", async () => {
const text = UI.createElement("text");
await UI.editText(text, "Excalidraw\nEditor");
const prevWidth = text.width;
const prevHeight = text.height;
const prevText = text.text;
UI.resize(text, "w", [50, 0]);
expect(text.width).toBe(prevWidth - 50);
expect(text.height).toBeGreaterThan(prevHeight);
expect(text.text).not.toEqual(prevText);
expect(text.autoResize).toBe(false);
UI.resize(text, "w", [-50, 0]);
expect(text.width).toBe(prevWidth);
expect(text.height).toEqual(prevHeight);
expect(text.text).toEqual(prevText);
expect(text.autoResize).toBe(false);
UI.resize(text, "e", [-20, 0]);
expect(text.width).toBe(prevWidth - 20);
expect(text.height).toBeGreaterThan(prevHeight);
expect(text.text).not.toEqual(prevText);
expect(text.autoResize).toBe(false);
UI.resize(text, "e", [20, 0]);
expect(text.width).toBe(prevWidth);
expect(text.height).toEqual(prevHeight);
expect(text.text).toEqual(prevText);
expect(text.autoResize).toBe(false);
});
it("keeps properties when wrapped", async () => {
const text = UI.createElement("text");
await UI.editText(text, "Excalidraw\nEditor");
const alignment = text.textAlign;
const fontSize = text.fontSize;
const fontFamily = text.fontFamily;
UI.resize(text, "e", [-60, 0]);
expect(text.textAlign).toBe(alignment);
expect(text.fontSize).toBe(fontSize);
expect(text.fontFamily).toBe(fontFamily);
expect(text.autoResize).toBe(false);
UI.resize(text, "e", [60, 0]);
expect(text.textAlign).toBe(alignment);
expect(text.fontSize).toBe(fontSize);
expect(text.fontFamily).toBe(fontFamily);
expect(text.autoResize).toBe(false);
});
it("has a minimum width when wrapped", async () => {
const text = UI.createElement("text");
await UI.editText(text, "Excalidraw\nEditor");
const width = text.width;
UI.resize(text, "e", [-width, 0]);
expect(text.width).not.toEqual(0);
UI.resize(text, "e", [width - text.width, 0]);
expect(text.width).toEqual(width);
expect(text.autoResize).toBe(false);
UI.resize(text, "w", [width, 0]);
expect(text.width).not.toEqual(0);
UI.resize(text, "w", [text.width - width, 0]);
expect(text.width).toEqual(width);
expect(text.autoResize).toBe(false);
});
});
describe("image element", () => {
+4 -4
View File
@@ -1930,10 +1930,10 @@
resolved "https://registry.npmjs.org/@excalidraw/markdown-to-text/-/markdown-to-text-0.1.2.tgz#1703705e7da608cf478f17bfe96fb295f55a23eb"
integrity sha512-1nDXBNAojfi3oSFwJswKREkFm5wrSjqay81QlyRv2pkITG/XYB5v+oChENVBQLcxQwX4IUATWvXM5BcaNhPiIg==
"@excalidraw/mermaid-to-excalidraw@1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-1.0.0.tgz#8c058d2a43230425cba96d01e4a669a2d7c586a2"
integrity sha512-RGSoJBY2gFag6mQOIwa3OakTrvAZYx0bwvnr5ojuCZInih8Fxhje4X1WZfsaQx+GATEH8Ioq3O3b1FPDg4nKjQ==
"@excalidraw/mermaid-to-excalidraw@0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@excalidraw/mermaid-to-excalidraw/-/mermaid-to-excalidraw-0.3.0.tgz#94c438133fc66db6b920e237abda5152b62e6cb0"
integrity sha512-eyFN8y2ES3HFtETZWZZBakkSB5ROfnHJeCLeBlMgrIk1fxbXpPtxlu2VwGNpqPjDiCfV5FYnx7FaZ4CRiVRVMg==
dependencies:
"@excalidraw/markdown-to-text" "0.1.2"
mermaid "10.9.0"