Merge remote-tracking branch 'origin/release' into danieljgeiger-mathjax
This commit is contained in:
commit
28261c4b29
@ -22,3 +22,8 @@ REACT_APP_DEV_ENABLE_SW=
|
||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||
|
||||
FAST_REFRESH=false
|
||||
|
||||
#Debug flags
|
||||
|
||||
# To enable bounding box for text containers
|
||||
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
||||
|
@ -1785,9 +1785,9 @@
|
||||
"@hapi/hoek" "^9.0.0"
|
||||
|
||||
"@sideway/formula@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||
|
||||
"@sideway/pinpoint@^2.0.0":
|
||||
version "2.0.0"
|
||||
@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1:
|
||||
entities "^4.3.0"
|
||||
|
||||
http-cache-semantics@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||
|
||||
http-deceiver@^1.2.7:
|
||||
version "1.2.7"
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { VERTICAL_ALIGN } from "../constants";
|
||||
import { getNonDeletedElements, isTextElement } from "../element";
|
||||
import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import {
|
||||
computeContainerDimensionForBoundText,
|
||||
getBoundTextElement,
|
||||
measureTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@ -13,8 +14,11 @@ import {
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isTextBindableContainer,
|
||||
isUsingAdaptiveRadius,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextContainer,
|
||||
ExcalidrawTextElement,
|
||||
} from "../element/types";
|
||||
@ -127,19 +131,151 @@ export const actionBindText = register({
|
||||
}),
|
||||
});
|
||||
redrawTextBoundingBox(textElement, container);
|
||||
const updatedElements = elements.slice();
|
||||
const textElementIndex = updatedElements.findIndex(
|
||||
(ele) => ele.id === textElement.id,
|
||||
);
|
||||
updatedElements.splice(textElementIndex, 1);
|
||||
const containerIndex = updatedElements.findIndex(
|
||||
(ele) => ele.id === container.id,
|
||||
);
|
||||
updatedElements.splice(containerIndex + 1, 0, textElement);
|
||||
|
||||
return {
|
||||
elements: updatedElements,
|
||||
elements: pushTextAboveContainer(elements, container, textElement),
|
||||
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const pushTextAboveContainer = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
container: ExcalidrawElement,
|
||||
textElement: ExcalidrawTextElement,
|
||||
) => {
|
||||
const updatedElements = elements.slice();
|
||||
const textElementIndex = updatedElements.findIndex(
|
||||
(ele) => ele.id === textElement.id,
|
||||
);
|
||||
updatedElements.splice(textElementIndex, 1);
|
||||
|
||||
const containerIndex = updatedElements.findIndex(
|
||||
(ele) => ele.id === container.id,
|
||||
);
|
||||
updatedElements.splice(containerIndex + 1, 0, textElement);
|
||||
return updatedElements;
|
||||
};
|
||||
|
||||
const pushContainerBelowText = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
container: ExcalidrawElement,
|
||||
textElement: ExcalidrawTextElement,
|
||||
) => {
|
||||
const updatedElements = elements.slice();
|
||||
const containerIndex = updatedElements.findIndex(
|
||||
(ele) => ele.id === container.id,
|
||||
);
|
||||
updatedElements.splice(containerIndex, 1);
|
||||
|
||||
const textElementIndex = updatedElements.findIndex(
|
||||
(ele) => ele.id === textElement.id,
|
||||
);
|
||||
updatedElements.splice(textElementIndex, 0, container);
|
||||
return updatedElements;
|
||||
};
|
||||
|
||||
export const actionCreateContainerFromText = register({
|
||||
name: "createContainerFromText",
|
||||
contextItemLabel: "labels.createContainerFromText",
|
||||
trackEvent: { category: "element" },
|
||||
predicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
|
||||
},
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
const updatedElements = elements.slice();
|
||||
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||
const textElement = selectedElements[0];
|
||||
const container = newElement({
|
||||
type: "rectangle",
|
||||
backgroundColor: appState.currentItemBackgroundColor,
|
||||
boundElements: [
|
||||
...(textElement.boundElements || []),
|
||||
{ id: textElement.id, type: "text" },
|
||||
],
|
||||
angle: textElement.angle,
|
||||
fillStyle: appState.currentItemFillStyle,
|
||||
strokeColor: appState.currentItemStrokeColor,
|
||||
roughness: appState.currentItemRoughness,
|
||||
strokeWidth: appState.currentItemStrokeWidth,
|
||||
strokeStyle: appState.currentItemStrokeStyle,
|
||||
roundness:
|
||||
appState.currentItemRoundness === "round"
|
||||
? {
|
||||
type: isUsingAdaptiveRadius("rectangle")
|
||||
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||
}
|
||||
: null,
|
||||
opacity: 100,
|
||||
locked: false,
|
||||
x: textElement.x - BOUND_TEXT_PADDING,
|
||||
y: textElement.y - BOUND_TEXT_PADDING,
|
||||
width: computeContainerDimensionForBoundText(
|
||||
textElement.width,
|
||||
"rectangle",
|
||||
),
|
||||
height: computeContainerDimensionForBoundText(
|
||||
textElement.height,
|
||||
"rectangle",
|
||||
),
|
||||
groupIds: textElement.groupIds,
|
||||
});
|
||||
|
||||
// update bindings
|
||||
if (textElement.boundElements?.length) {
|
||||
const linearElementIds = textElement.boundElements
|
||||
.filter((ele) => ele.type === "arrow")
|
||||
.map((el) => el.id);
|
||||
const linearElements = updatedElements.filter((ele) =>
|
||||
linearElementIds.includes(ele.id),
|
||||
) as ExcalidrawLinearElement[];
|
||||
linearElements.forEach((ele) => {
|
||||
let startBinding = null;
|
||||
let endBinding = null;
|
||||
if (ele.startBinding) {
|
||||
startBinding = { ...ele.startBinding, elementId: container.id };
|
||||
}
|
||||
if (ele.endBinding) {
|
||||
endBinding = { ...ele.endBinding, elementId: container.id };
|
||||
}
|
||||
mutateElement(ele, { startBinding, endBinding });
|
||||
});
|
||||
}
|
||||
|
||||
mutateElement(textElement, {
|
||||
containerId: container.id,
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
boundElements: null,
|
||||
});
|
||||
redrawTextBoundingBox(textElement, container);
|
||||
|
||||
return {
|
||||
elements: pushContainerBelowText(
|
||||
[...elements, container],
|
||||
container,
|
||||
textElement,
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {
|
||||
[container.id]: true,
|
||||
[textElement.id]: false,
|
||||
},
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
elements: updatedElements,
|
||||
appState,
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -122,7 +122,8 @@ export type ActionName =
|
||||
| "toggleLock"
|
||||
| "toggleLinearEditor"
|
||||
| "toggleEraserTool"
|
||||
| "toggleHandTool";
|
||||
| "toggleHandTool"
|
||||
| "createContainerFromText";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { actionClearCanvas } from "../actions";
|
||||
import { t } from "../i18n";
|
||||
import { jotaiScope } from "../jotai";
|
||||
import { useExcalidrawActionManager } from "./App";
|
||||
import ConfirmDialog from "./ConfirmDialog";
|
||||
|
||||
@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
||||
export const ActiveConfirmDialog = () => {
|
||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||
activeConfirmDialogAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
|
@ -293,6 +293,7 @@ import { actionPaste } from "../actions/actionClipboard";
|
||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||
import { jotaiStore } from "../jotai";
|
||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
||||
|
||||
const deviceContextInitialValue = {
|
||||
isSmScreen: false,
|
||||
@ -2830,7 +2831,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
if (container) {
|
||||
if (
|
||||
isArrowElement(container) ||
|
||||
hasBoundTextElement(container) ||
|
||||
!isTransparent(container.backgroundColor) ||
|
||||
isHittingElementNotConsideringBoundingBox(container, this.state, [
|
||||
@ -6327,6 +6327,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
actionGroup,
|
||||
actionUnbindText,
|
||||
actionBindText,
|
||||
actionCreateContainerFromText,
|
||||
actionUngroup,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionAddToLibrary,
|
||||
|
@ -6,6 +6,7 @@ import DialogActionButton from "./DialogActionButton";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||
import { useExcalidrawSetAppState } from "./App";
|
||||
import { jotaiScope } from "../jotai";
|
||||
|
||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||
onConfirm: () => void;
|
||||
@ -24,7 +25,7 @@ const ConfirmDialog = (props: Props) => {
|
||||
...rest
|
||||
} = props;
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
|
@ -16,6 +16,7 @@ import { AppState } from "../types";
|
||||
import { queryFocusableElements } from "../utils";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||
import { jotaiScope } from "../jotai";
|
||||
|
||||
export interface DialogProps {
|
||||
children: React.ReactNode;
|
||||
@ -72,7 +73,7 @@ export const Dialog = (props: DialogProps) => {
|
||||
}, [islandNode, props.autofocus]);
|
||||
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||
|
||||
const onClose = () => {
|
||||
setAppState({ openMenu: null });
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { t } from "../i18n";
|
||||
import { HelpIcon } from "./icons";
|
||||
|
||||
type HelpButtonProps = {
|
||||
title?: string;
|
||||
name?: string;
|
||||
id?: string;
|
||||
onClick?(): void;
|
||||
@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => (
|
||||
className="help-icon"
|
||||
onClick={props.onClick}
|
||||
type="button"
|
||||
title={`${props.title} — ?`}
|
||||
aria-label={props.title}
|
||||
title={`${t("helpDialog.title")} — ?`}
|
||||
aria-label={t("helpDialog.title")}
|
||||
>
|
||||
{HelpIcon}
|
||||
</button>
|
||||
|
@ -48,6 +48,7 @@ export const LibraryMenuHeader: React.FC<{
|
||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
||||
isLibraryMenuOpenAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
const renderRemoveLibAlert = useCallback(() => {
|
||||
const content = selectedItems.length
|
||||
|
@ -31,6 +31,7 @@ import "./DefaultItems.scss";
|
||||
import clsx from "clsx";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||
import { jotaiScope } from "../../jotai";
|
||||
|
||||
export const LoadScene = () => {
|
||||
const { t } = useI18n();
|
||||
@ -113,7 +114,10 @@ Help.displayName = "Help";
|
||||
export const ClearCanvas = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||
const setActiveConfirmDialog = useSetAtom(
|
||||
activeConfirmDialogAtom,
|
||||
jotaiScope,
|
||||
);
|
||||
const actionManager = useExcalidrawActionManager();
|
||||
|
||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||
|
@ -693,7 +693,9 @@ const resizeMultipleElements = (
|
||||
};
|
||||
const fontSize = measureFontSizeFromWidth(
|
||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||
getMaxContainerWidth(updatedElement),
|
||||
boundTextElement
|
||||
? getMaxContainerWidth(updatedElement)
|
||||
: updatedElement.width,
|
||||
);
|
||||
|
||||
if (!fontSize) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BOUND_TEXT_PADDING } from "../constants";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import {
|
||||
computeContainerHeightForBoundText,
|
||||
computeContainerDimensionForBoundText,
|
||||
getContainerCoords,
|
||||
getMaxContainerWidth,
|
||||
getMaxContainerHeight,
|
||||
@ -35,10 +35,11 @@ describe("Test wrapText", () => {
|
||||
|
||||
describe("When text doesn't contain new lines", () => {
|
||||
const text = "Hello whats up";
|
||||
|
||||
[
|
||||
{
|
||||
desc: "break all words when width of each word is less than container width",
|
||||
width: 90,
|
||||
width: 80,
|
||||
res: `Hello
|
||||
whats
|
||||
up`,
|
||||
@ -62,7 +63,7 @@ p`,
|
||||
{
|
||||
desc: "break words as per the width",
|
||||
|
||||
width: 150,
|
||||
width: 140,
|
||||
res: `Hello whats
|
||||
up`,
|
||||
},
|
||||
@ -93,7 +94,7 @@ whats up`;
|
||||
[
|
||||
{
|
||||
desc: "break all words when width of each word is less than container width",
|
||||
width: 90,
|
||||
width: 80,
|
||||
res: `Hello
|
||||
whats
|
||||
up`,
|
||||
@ -214,7 +215,7 @@ describe("Test measureText", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test computeContainerHeightForBoundText", () => {
|
||||
describe("Test computeContainerDimensionForBoundText", () => {
|
||||
const params = {
|
||||
width: 178,
|
||||
height: 194,
|
||||
@ -225,7 +226,9 @@ describe("Test measureText", () => {
|
||||
type: "rectangle",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerHeightForBoundText(element, 150)).toEqual(160);
|
||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||
160,
|
||||
);
|
||||
});
|
||||
|
||||
it("should compute container height correctly for ellipse", () => {
|
||||
@ -233,7 +236,9 @@ describe("Test measureText", () => {
|
||||
type: "ellipse",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerHeightForBoundText(element, 150)).toEqual(226);
|
||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||
226,
|
||||
);
|
||||
});
|
||||
|
||||
it("should compute container height correctly for diamond", () => {
|
||||
@ -241,7 +246,9 @@ describe("Test measureText", () => {
|
||||
type: "diamond",
|
||||
...params,
|
||||
});
|
||||
expect(computeContainerHeightForBoundText(element, 150)).toEqual(320);
|
||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||
320,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -13,11 +13,7 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||
import { MaybeTransformHandleType } from "./transformHandles";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isTextElement } from ".";
|
||||
import {
|
||||
isBoundToContainer,
|
||||
isImageElement,
|
||||
isArrowElement,
|
||||
} from "./typeChecks";
|
||||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { AppState } from "../types";
|
||||
import { isTextBindableContainer } from "./typeChecks";
|
||||
@ -112,9 +108,9 @@ export const redrawTextBoundingBox = (
|
||||
|
||||
let nextHeight = containerDims.height;
|
||||
if (metrics.height > maxContainerHeight) {
|
||||
nextHeight = computeContainerHeightForBoundText(
|
||||
container,
|
||||
nextHeight = computeContainerDimensionForBoundText(
|
||||
metrics.height,
|
||||
container.type,
|
||||
);
|
||||
mutateElement(container, { height: nextHeight });
|
||||
maxContainerHeight = getMaxContainerHeight(container);
|
||||
@ -212,9 +208,9 @@ export const handleBindTextResize = (
|
||||
}
|
||||
// increase height in case text element height exceeds
|
||||
if (nextHeight > maxHeight) {
|
||||
containerHeight = computeContainerHeightForBoundText(
|
||||
container,
|
||||
containerHeight = computeContainerDimensionForBoundText(
|
||||
nextHeight,
|
||||
container.type,
|
||||
);
|
||||
|
||||
const diff = containerHeight - containerDims.height;
|
||||
@ -348,7 +344,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||
const lines: Array<string> = [];
|
||||
const originalLines = text.split("\n");
|
||||
const spaceWidth = getLineWidth(" ", font);
|
||||
|
||||
const push = (str: string) => {
|
||||
if (str.trim()) {
|
||||
lines.push(str);
|
||||
@ -422,7 +417,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||
const word = words[index];
|
||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||
|
||||
if (currentLineWidthTillNow >= maxWidth) {
|
||||
if (currentLineWidthTillNow > maxWidth) {
|
||||
push(currentLine);
|
||||
currentLineWidthTillNow = 0;
|
||||
currentLine = "";
|
||||
@ -738,32 +733,34 @@ export const getTextBindableContainerAtPosition = (
|
||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||
};
|
||||
|
||||
export const isValidTextContainer = (element: ExcalidrawElement) => {
|
||||
return (
|
||||
element.type === "rectangle" ||
|
||||
element.type === "ellipse" ||
|
||||
element.type === "diamond" ||
|
||||
isImageElement(element) ||
|
||||
isArrowElement(element)
|
||||
);
|
||||
};
|
||||
const VALID_CONTAINER_TYPES = new Set([
|
||||
"rectangle",
|
||||
"ellipse",
|
||||
"diamond",
|
||||
"image",
|
||||
"arrow",
|
||||
]);
|
||||
|
||||
export const computeContainerHeightForBoundText = (
|
||||
container: NonDeletedExcalidrawElement,
|
||||
boundTextElementHeight: number,
|
||||
export const isValidTextContainer = (element: ExcalidrawElement) =>
|
||||
VALID_CONTAINER_TYPES.has(element.type);
|
||||
|
||||
export const computeContainerDimensionForBoundText = (
|
||||
dimension: number,
|
||||
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
||||
) => {
|
||||
if (container.type === "ellipse") {
|
||||
return Math.round(
|
||||
((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2,
|
||||
);
|
||||
dimension = Math.ceil(dimension);
|
||||
const padding = BOUND_TEXT_PADDING * 2;
|
||||
|
||||
if (containerType === "ellipse") {
|
||||
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
||||
}
|
||||
if (isArrowElement(container)) {
|
||||
return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2;
|
||||
if (containerType === "arrow") {
|
||||
return dimension + padding * 8;
|
||||
}
|
||||
if (container.type === "diamond") {
|
||||
return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2);
|
||||
if (containerType === "diamond") {
|
||||
return 2 * (dimension + padding);
|
||||
}
|
||||
return boundTextElementHeight + BOUND_TEXT_PADDING * 2;
|
||||
return dimension + padding;
|
||||
};
|
||||
|
||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
} from "../tests/test-utils";
|
||||
import { queryByText } from "@testing-library/react";
|
||||
|
||||
import { FONT_FAMILY } from "../constants";
|
||||
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||
import {
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
@ -19,6 +19,7 @@ import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { resize } from "../tests/utils";
|
||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
@ -1307,5 +1308,91 @@ describe("textWysiwyg", () => {
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(20, 30);
|
||||
const editor = document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(editor, {
|
||||
target: {
|
||||
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
||||
},
|
||||
});
|
||||
|
||||
editor.dispatchEvent(new Event("input"));
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
|
||||
editor.select();
|
||||
fireEvent.click(screen.getByTitle("Left"));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
editor.blur();
|
||||
|
||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||
expect(textElement.width).toBe(600);
|
||||
expect(textElement.height).toBe(24);
|
||||
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
||||
expect((textElement as ExcalidrawTextElement).text).toBe(
|
||||
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||
);
|
||||
|
||||
API.setSelectedElements([textElement]);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
});
|
||||
|
||||
const contextMenu = document.querySelector(".context-menu");
|
||||
fireEvent.click(
|
||||
queryByText(contextMenu as HTMLElement, "Wrap text in a container")!,
|
||||
);
|
||||
expect(h.elements.length).toBe(3);
|
||||
|
||||
expect(h.elements[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
angle: 0,
|
||||
backgroundColor: "transparent",
|
||||
boundElements: [
|
||||
{
|
||||
id: h.elements[2].id,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
fillStyle: "hachure",
|
||||
groupIds: [],
|
||||
height: 34,
|
||||
isDeleted: false,
|
||||
link: null,
|
||||
locked: false,
|
||||
opacity: 100,
|
||||
roughness: 1,
|
||||
roundness: {
|
||||
type: 3,
|
||||
},
|
||||
strokeColor: "#000000",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
type: "rectangle",
|
||||
updated: 1,
|
||||
version: 1,
|
||||
width: 610,
|
||||
x: 15,
|
||||
y: 25,
|
||||
}),
|
||||
);
|
||||
expect(h.elements[2] as ExcalidrawTextElement).toEqual(
|
||||
expect.objectContaining({
|
||||
text: "Excalidraw is an opensource virtual collaborative whiteboard",
|
||||
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||
textAlign: TEXT_ALIGN.LEFT,
|
||||
boundElements: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
66
src/element/typeChecks.test.ts
Normal file
66
src/element/typeChecks.test.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { hasBoundTextElement } from "./typeChecks";
|
||||
|
||||
describe("Test TypeChecks", () => {
|
||||
describe("Test hasBoundTextElement", () => {
|
||||
it("should return true for text bindable containers with bound text", () => {
|
||||
expect(
|
||||
hasBoundTextElement(
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
boundElements: [{ type: "text", id: "text-id" }],
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
hasBoundTextElement(
|
||||
API.createElement({
|
||||
type: "ellipse",
|
||||
boundElements: [{ type: "text", id: "text-id" }],
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
hasBoundTextElement(
|
||||
API.createElement({
|
||||
type: "arrow",
|
||||
boundElements: [{ type: "text", id: "text-id" }],
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(
|
||||
hasBoundTextElement(
|
||||
API.createElement({
|
||||
type: "image",
|
||||
boundElements: [{ type: "text", id: "text-id" }],
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should return false for text bindable containers without bound text", () => {
|
||||
expect(
|
||||
hasBoundTextElement(
|
||||
API.createElement({
|
||||
type: "freedraw",
|
||||
boundElements: [{ type: "arrow", id: "arrow-id" }],
|
||||
}),
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return false for non text bindable containers", () => {
|
||||
expect(
|
||||
hasBoundTextElement(
|
||||
API.createElement({
|
||||
type: "freedraw",
|
||||
boundElements: [{ type: "text", id: "text-id" }],
|
||||
}),
|
||||
),
|
||||
).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
@ -139,7 +139,7 @@ export const hasBoundTextElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
|
||||
return (
|
||||
isBindableElement(element) &&
|
||||
isTextBindableContainer(element) &&
|
||||
!!element.boundElements?.some(({ type }) => type === "text")
|
||||
);
|
||||
};
|
||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -166,3 +166,5 @@ declare module "image-blob-reduce" {
|
||||
const reduce: ImageBlobReduce.ImageBlobReduceStatic;
|
||||
export = reduce;
|
||||
}
|
||||
|
||||
type ExtractSetType<T extends Set<any>> = T extends Set<infer U> ? U : never;
|
||||
|
@ -111,6 +111,7 @@
|
||||
"increaseFontSize": "Increase font size",
|
||||
"unbindText": "Unbind text",
|
||||
"bindText": "Bind text to the container",
|
||||
"createContainerFromText": "Wrap text in a container",
|
||||
"link": {
|
||||
"edit": "Edit link",
|
||||
"create": "Create link",
|
||||
|
@ -2032,9 +2032,9 @@ dns-equal@^1.0.0:
|
||||
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
|
||||
|
||||
dns-packet@^5.2.2:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.3.1.tgz#eb94413789daec0f0ebe2fcc230bdc9d7c91b43d"
|
||||
integrity sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
|
||||
integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
|
||||
dependencies:
|
||||
"@leichtgewicht/ip-codec" "^2.0.1"
|
||||
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
isFreeDrawElement,
|
||||
isInitializedImageElement,
|
||||
isArrowElement,
|
||||
hasBoundTextElement,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
getDiamondPoints,
|
||||
@ -42,7 +43,10 @@ import { getStroke, StrokeOptions } from "perfect-freehand";
|
||||
import {
|
||||
getApproxLineHeight,
|
||||
getBoundTextElement,
|
||||
getContainerCoords,
|
||||
getContainerElement,
|
||||
getMaxContainerHeight,
|
||||
getMaxContainerWidth,
|
||||
} from "../element/textElement";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
|
||||
@ -818,6 +822,21 @@ const drawElementFromCanvas = (
|
||||
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
||||
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
||||
);
|
||||
|
||||
if (
|
||||
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
|
||||
hasBoundTextElement(element)
|
||||
) {
|
||||
const coords = getContainerCoords(element);
|
||||
context.strokeStyle = "#c92a2a";
|
||||
context.lineWidth = 3;
|
||||
context.strokeRect(
|
||||
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
||||
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
||||
getMaxContainerWidth(element) * window.devicePixelRatio,
|
||||
getMaxContainerHeight(element) * window.devicePixelRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
context.restore();
|
||||
|
||||
|
@ -119,6 +119,15 @@ Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"contextItemLabel": "labels.createContainerFromText",
|
||||
"name": "createContainerFromText",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"PanelComponent": [Function],
|
||||
"contextItemLabel": "labels.ungroup",
|
||||
@ -4507,6 +4516,15 @@ Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"contextItemLabel": "labels.createContainerFromText",
|
||||
"name": "createContainerFromText",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"PanelComponent": [Function],
|
||||
"contextItemLabel": "labels.ungroup",
|
||||
@ -5048,6 +5066,15 @@ Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"contextItemLabel": "labels.createContainerFromText",
|
||||
"name": "createContainerFromText",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"PanelComponent": [Function],
|
||||
"contextItemLabel": "labels.ungroup",
|
||||
@ -5888,6 +5915,15 @@ Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"contextItemLabel": "labels.createContainerFromText",
|
||||
"name": "createContainerFromText",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"PanelComponent": [Function],
|
||||
"contextItemLabel": "labels.ungroup",
|
||||
@ -6225,6 +6261,15 @@ Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"contextItemLabel": "labels.createContainerFromText",
|
||||
"name": "createContainerFromText",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": Object {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"PanelComponent": [Function],
|
||||
"contextItemLabel": "labels.ungroup",
|
||||
|
30
src/tests/shortcuts.test.tsx
Normal file
30
src/tests/shortcuts.test.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { KEYS } from "../keys";
|
||||
import { Excalidraw } from "../packages/excalidraw/entry";
|
||||
import { API } from "./helpers/api";
|
||||
import { Keyboard } from "./helpers/ui";
|
||||
import { fireEvent, render, waitFor } from "./test-utils";
|
||||
|
||||
describe("shortcuts", () => {
|
||||
it("Clear canvas shortcut should display confirm dialog", async () => {
|
||||
await render(
|
||||
<Excalidraw
|
||||
initialData={{ elements: [API.createElement({ type: "rectangle" })] }}
|
||||
handleKeyboardGlobally
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(window.h.elements.length).toBe(1);
|
||||
|
||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||
Keyboard.keyDown(KEYS.DELETE);
|
||||
});
|
||||
const confirmDialog = document.querySelector(".confirm-dialog")!;
|
||||
expect(confirmDialog).not.toBe(null);
|
||||
|
||||
fireEvent.click(confirmDialog.querySelector('[aria-label="Confirm"]')!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.h.elements[0].isDeleted).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user