Compare commits
16
Commits
@@ -22,8 +22,3 @@ REACT_APP_DEV_ENABLE_SW=
|
|||||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
#Debug flags
|
|
||||||
|
|
||||||
# To enable bounding box for text containers
|
|
||||||
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
|
||||||
|
|||||||
+6
-6
@@ -1785,9 +1785,9 @@
|
|||||||
"@hapi/hoek" "^9.0.0"
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@sideway/formula@^3.0.0":
|
"@sideway/formula@^3.0.0":
|
||||||
version "3.0.1"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
||||||
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
||||||
|
|
||||||
"@sideway/pinpoint@^2.0.0":
|
"@sideway/pinpoint@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1:
|
|||||||
entities "^4.3.0"
|
entities "^4.3.0"
|
||||||
|
|
||||||
http-cache-semantics@^4.0.0:
|
http-cache-semantics@^4.0.0:
|
||||||
version "4.1.1"
|
version "4.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
||||||
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
||||||
|
|
||||||
http-deceiver@^1.2.7:
|
http-deceiver@^1.2.7:
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
|
|||||||
+12
-148
@@ -1,8 +1,7 @@
|
|||||||
import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
import { getNonDeletedElements, isTextElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -14,11 +13,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
isUsingAdaptiveRadius,
|
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawLinearElement,
|
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -133,151 +129,19 @@ export const actionBindText = register({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(textElement, container);
|
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 {
|
return {
|
||||||
elements: pushTextAboveContainer(elements, container, textElement),
|
elements: updatedElements,
|
||||||
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
||||||
commitToHistory: 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,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { isDarwin } from "../constants";
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { SubtypeOf } from "../utility-types";
|
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { MarkOptional } from "../utility-types";
|
|
||||||
|
|
||||||
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
||||||
|
|
||||||
@@ -114,8 +113,7 @@ export type ActionName =
|
|||||||
| "toggleLock"
|
| "toggleLock"
|
||||||
| "toggleLinearEditor"
|
| "toggleLinearEditor"
|
||||||
| "toggleEraserTool"
|
| "toggleEraserTool"
|
||||||
| "toggleHandTool"
|
| "toggleHandTool";
|
||||||
| "createContainerFromText";
|
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { actionClearCanvas } from "../actions";
|
import { actionClearCanvas } from "../actions";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { jotaiScope } from "../jotai";
|
|
||||||
import { useExcalidrawActionManager } from "./App";
|
import { useExcalidrawActionManager } from "./App";
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
|
||||||
@@ -10,7 +9,6 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
|||||||
export const ActiveConfirmDialog = () => {
|
export const ActiveConfirmDialog = () => {
|
||||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||||
activeConfirmDialogAtom,
|
activeConfirmDialogAtom,
|
||||||
jotaiScope,
|
|
||||||
);
|
);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
|||||||
+93
-4
@@ -175,7 +175,10 @@ import {
|
|||||||
} from "../keys";
|
} from "../keys";
|
||||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
import { renderScene } from "../renderer/renderScene";
|
import { renderScene } from "../renderer/renderScene";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import {
|
||||||
|
generateElementWithCanvas,
|
||||||
|
invalidateShapeForElement,
|
||||||
|
} from "../renderer/renderElement";
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
getElementsAtPosition,
|
getElementsAtPosition,
|
||||||
@@ -284,7 +287,6 @@ import { actionPaste } from "../actions/actionClipboard";
|
|||||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||||
import { jotaiStore } from "../jotai";
|
import { jotaiStore } from "../jotai";
|
||||||
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
isSmScreen: false,
|
isSmScreen: false,
|
||||||
@@ -2768,6 +2770,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
if (container) {
|
if (container) {
|
||||||
if (
|
if (
|
||||||
|
isArrowElement(container) ||
|
||||||
hasBoundTextElement(container) ||
|
hasBoundTextElement(container) ||
|
||||||
!isTransparent(container.backgroundColor) ||
|
!isTransparent(container.backgroundColor) ||
|
||||||
isHittingElementNotConsideringBoundingBox(container, this.state, [
|
isHittingElementNotConsideringBoundingBox(container, this.state, [
|
||||||
@@ -6238,7 +6241,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionGroup,
|
actionGroup,
|
||||||
actionUnbindText,
|
actionUnbindText,
|
||||||
actionBindText,
|
actionBindText,
|
||||||
actionCreateContainerFromText,
|
|
||||||
actionUngroup,
|
actionUngroup,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
@@ -6368,7 +6370,94 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
|
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
|
||||||
if (!this.unmounted) {
|
if (!this.unmounted) {
|
||||||
this.setState({ shouldCacheIgnoreZoom: false });
|
const cursorButton: {
|
||||||
|
[id: string]: string | undefined;
|
||||||
|
} = {};
|
||||||
|
const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] =
|
||||||
|
{};
|
||||||
|
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
|
||||||
|
{};
|
||||||
|
const pointerUsernames: { [id: string]: string } = {};
|
||||||
|
const pointerUserStates: { [id: string]: string } = {};
|
||||||
|
this.state.collaborators.forEach((user, socketId) => {
|
||||||
|
if (user.selectedElementIds) {
|
||||||
|
for (const id of Object.keys(user.selectedElementIds)) {
|
||||||
|
if (!(id in remoteSelectedElementIds)) {
|
||||||
|
remoteSelectedElementIds[id] = [];
|
||||||
|
}
|
||||||
|
remoteSelectedElementIds[id].push(socketId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!user.pointer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.username) {
|
||||||
|
pointerUsernames[socketId] = user.username;
|
||||||
|
}
|
||||||
|
if (user.userState) {
|
||||||
|
pointerUserStates[socketId] = user.userState;
|
||||||
|
}
|
||||||
|
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
|
||||||
|
{
|
||||||
|
sceneX: user.pointer.x,
|
||||||
|
sceneY: user.pointer.y,
|
||||||
|
},
|
||||||
|
this.state,
|
||||||
|
);
|
||||||
|
cursorButton[socketId] = user.button;
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderingElements = this.scene
|
||||||
|
.getNonDeletedElements()
|
||||||
|
.filter((element) => {
|
||||||
|
if (isImageElement(element)) {
|
||||||
|
if (
|
||||||
|
// not placed on canvas yet (but in elements array)
|
||||||
|
this.state.pendingImageElementId === element.id
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// don't render text element that's being currently edited (it's
|
||||||
|
// rendered on remote only)
|
||||||
|
return (
|
||||||
|
!this.state.editingElement ||
|
||||||
|
this.state.editingElement.type !== "text" ||
|
||||||
|
element.id !== this.state.editingElement.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectionColor = getComputedStyle(
|
||||||
|
document.querySelector(".excalidraw")!,
|
||||||
|
).getPropertyValue("--color-selection");
|
||||||
|
|
||||||
|
const renderConfig: RenderConfig = {
|
||||||
|
selectionColor,
|
||||||
|
scrollX: this.state.scrollX,
|
||||||
|
scrollY: this.state.scrollY,
|
||||||
|
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||||
|
zoom: this.state.zoom,
|
||||||
|
remotePointerViewportCoords: pointerViewportCoords,
|
||||||
|
remotePointerButton: cursorButton,
|
||||||
|
remoteSelectedElementIds,
|
||||||
|
remotePointerUsernames: pointerUsernames,
|
||||||
|
remotePointerUserStates: pointerUserStates,
|
||||||
|
shouldCacheIgnoreZoom: false,
|
||||||
|
theme: this.state.theme,
|
||||||
|
imageCache: this.imageCache,
|
||||||
|
isExporting: false,
|
||||||
|
renderScrollbars: !this.device.isMobile,
|
||||||
|
};
|
||||||
|
let i = 0;
|
||||||
|
for (; i < renderingElements.length; i += 200) {
|
||||||
|
const chunk = renderingElements.slice(i, i + 200);
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
chunk.forEach((el) => generateElementWithCanvas(el, renderConfig)),
|
||||||
|
i * 50,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setTimeout(() => this.setState({ shouldCacheIgnoreZoom: false }), i * 50);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import DialogActionButton from "./DialogActionButton";
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||||
import { useExcalidrawSetAppState } from "./App";
|
import { useExcalidrawSetAppState } from "./App";
|
||||||
import { jotaiScope } from "../jotai";
|
|
||||||
|
|
||||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
@@ -25,7 +24,7 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { AppState } from "../types";
|
|||||||
import { queryFocusableElements } from "../utils";
|
import { queryFocusableElements } from "../utils";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||||
import { jotaiScope } from "../jotai";
|
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -73,7 +72,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
}, [islandNode, props.autofocus]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { t } from "../i18n";
|
|
||||||
import { HelpIcon } from "./icons";
|
import { HelpIcon } from "./icons";
|
||||||
|
|
||||||
type HelpButtonProps = {
|
type HelpButtonProps = {
|
||||||
|
title?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
@@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => (
|
|||||||
className="help-icon"
|
className="help-icon"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
type="button"
|
type="button"
|
||||||
title={`${t("helpDialog.title")} — ?`}
|
title={`${props.title} — ?`}
|
||||||
aria-label={t("helpDialog.title")}
|
aria-label={props.title}
|
||||||
>
|
>
|
||||||
{HelpIcon}
|
{HelpIcon}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
||||||
isLibraryMenuOpenAtom,
|
isLibraryMenuOpenAtom,
|
||||||
jotaiScope,
|
|
||||||
);
|
);
|
||||||
const renderRemoveLibAlert = useCallback(() => {
|
const renderRemoveLibAlert = useCallback(() => {
|
||||||
const content = selectedItems.length
|
const content = selectedItems.length
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import "./DefaultItems.scss";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
import { jotaiScope } from "../../jotai";
|
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -114,10 +113,7 @@ Help.displayName = "Help";
|
|||||||
export const ClearCanvas = () => {
|
export const ClearCanvas = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const setActiveConfirmDialog = useSetAtom(
|
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
||||||
activeConfirmDialogAtom,
|
|
||||||
jotaiScope,
|
|
||||||
);
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { CanvasError } from "../errors";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState, DataURL, LibraryItem } from "../types";
|
import { AppState, DataURL, LibraryItem } from "../types";
|
||||||
import { ValueOf } from "../utility-types";
|
|
||||||
import { bytesToHexString } from "../utils";
|
import { bytesToHexString } from "../utils";
|
||||||
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
||||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import { bumpVersion } from "../element/mutateElement";
|
|||||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import { MarkOptional, Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
export type Bounds = readonly [number, number, number, number];
|
export type Bounds = readonly [number, number, number, number];
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ import { isTextElement } from ".";
|
|||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
import { shouldShowBoundingBox } from "./transformHandles";
|
import { shouldShowBoundingBox } from "./transformHandles";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
const isElementDraggableFromInside = (
|
const isElementDraggableFromInside = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { shouldRotateWithDiscreteAngle } from "../keys";
|
|||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
import { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { getSizeFromPoints } from "../points";
|
|||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
import { Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ import {
|
|||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
|
getContainerElement,
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
getContainerElement,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
@@ -414,11 +414,29 @@ export const resizeSingleElement = (
|
|||||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (shouldMaintainAspectRatio) {
|
||||||
|
const updatedElement = {
|
||||||
|
...element,
|
||||||
|
width: eleNewWidth,
|
||||||
|
height: eleNewHeight,
|
||||||
|
};
|
||||||
|
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
const nextFontSize = measureFontSizeFromWidth(
|
||||||
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
boundTextElement,
|
||||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
getMaxContainerWidth(updatedElement),
|
||||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
);
|
||||||
|
if (nextFontSize === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boundTextFont = {
|
||||||
|
fontSize: nextFontSize,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
||||||
|
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
||||||
|
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||||
|
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
||||||
@@ -551,11 +569,7 @@ export const resizeSingleElement = (
|
|||||||
if (boundTextElement && boundTextFont) {
|
if (boundTextElement && boundTextFont) {
|
||||||
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
||||||
}
|
}
|
||||||
handleBindTextResize(
|
handleBindTextResize(element, transformHandleDirection);
|
||||||
element,
|
|
||||||
transformHandleDirection,
|
|
||||||
shouldMaintainAspectRatio,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -679,9 +693,7 @@ const resizeMultipleElements = (
|
|||||||
};
|
};
|
||||||
const fontSize = measureFontSizeFromWidth(
|
const fontSize = measureFontSizeFromWidth(
|
||||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||||
boundTextElement
|
getMaxContainerWidth(updatedElement),
|
||||||
? getMaxContainerWidth(updatedElement)
|
|
||||||
: updatedElement.width,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!fontSize) {
|
if (!fontSize) {
|
||||||
|
|||||||
+105
-141
@@ -1,12 +1,11 @@
|
|||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { BOUND_TEXT_PADDING } from "../constants";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import {
|
import {
|
||||||
computeContainerDimensionForBoundText,
|
computeContainerHeightForBoundText,
|
||||||
computeBoundTextElementCoords,
|
getContainerCoords,
|
||||||
getMaxContainerWidth,
|
getMaxContainerWidth,
|
||||||
getMaxContainerHeight,
|
getMaxContainerHeight,
|
||||||
wrapText,
|
wrapText,
|
||||||
computeContainerCoords,
|
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
@@ -36,11 +35,10 @@ describe("Test wrapText", () => {
|
|||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
describe("When text doesn't contain new lines", () => {
|
||||||
const text = "Hello whats up";
|
const text = "Hello whats up";
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 80,
|
width: 90,
|
||||||
res: `Hello
|
res: `Hello
|
||||||
whats
|
whats
|
||||||
up`,
|
up`,
|
||||||
@@ -64,7 +62,7 @@ p`,
|
|||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
|
|
||||||
width: 140,
|
width: 150,
|
||||||
res: `Hello whats
|
res: `Hello whats
|
||||||
up`,
|
up`,
|
||||||
},
|
},
|
||||||
@@ -95,7 +93,7 @@ whats up`;
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 80,
|
width: 90,
|
||||||
res: `Hello
|
res: `Hello
|
||||||
whats
|
whats
|
||||||
up`,
|
up`,
|
||||||
@@ -178,150 +176,116 @@ break it now`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test computeBoundTextElementCoords", () => {
|
describe("Test measureText", () => {
|
||||||
const params = { width: 200, height: 100, x: 10, y: 20 };
|
describe("Test getContainerCoords", () => {
|
||||||
|
const params = { width: 200, height: 100, x: 10, y: 20 };
|
||||||
|
|
||||||
it("should compute coords correctly when ellipse", () => {
|
it("should compute coords correctly when ellipse", () => {
|
||||||
const element = API.createElement({
|
const element = API.createElement({
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
...params,
|
...params,
|
||||||
|
});
|
||||||
|
expect(getContainerCoords(element)).toEqual({
|
||||||
|
x: 44.2893218813452455,
|
||||||
|
y: 39.64466094067262,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
expect(computeBoundTextElementCoords(element)).toEqual({
|
|
||||||
x: 44.2893218813452455,
|
it("should compute coords correctly when rectangle", () => {
|
||||||
y: 39.64466094067262,
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(getContainerCoords(element)).toEqual({
|
||||||
|
x: 15,
|
||||||
|
y: 25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute coords correctly when diamond", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "diamond",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(getContainerCoords(element)).toEqual({
|
||||||
|
x: 65,
|
||||||
|
y: 50,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should compute coords correctly when rectangle", () => {
|
describe("Test computeContainerHeightForBoundText", () => {
|
||||||
const element = API.createElement({
|
const params = {
|
||||||
type: "rectangle",
|
width: 178,
|
||||||
...params,
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should compute container height correctly for rectangle", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerHeightForBoundText(element, 150)).toEqual(160);
|
||||||
});
|
});
|
||||||
expect(computeBoundTextElementCoords(element)).toEqual({
|
|
||||||
x: 15,
|
it("should compute container height correctly for ellipse", () => {
|
||||||
y: 25,
|
const element = API.createElement({
|
||||||
|
type: "ellipse",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerHeightForBoundText(element, 150)).toEqual(226);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute container height correctly for diamond", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "diamond",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerHeightForBoundText(element, 150)).toEqual(320);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should compute coords correctly when diamond", () => {
|
describe("Test getMaxContainerWidth", () => {
|
||||||
const element = API.createElement({
|
const params = {
|
||||||
type: "diamond",
|
width: 178,
|
||||||
...params,
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return max width when container is rectangle", () => {
|
||||||
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(168);
|
||||||
});
|
});
|
||||||
expect(computeBoundTextElementCoords(element)).toEqual({
|
|
||||||
x: 65,
|
it("should return max width when container is ellipse", () => {
|
||||||
y: 50,
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(116);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max width when container is diamond", () => {
|
||||||
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(79);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getMaxContainerHeight", () => {
|
||||||
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return max height when container is rectangle", () => {
|
||||||
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(184);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is ellipse", () => {
|
||||||
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(127);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is diamond", () => {
|
||||||
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(87);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test computeContainerCoords", () => {
|
|
||||||
const boundTextElement = API.createElement({
|
|
||||||
type: "text",
|
|
||||||
width: 200,
|
|
||||||
height: 100,
|
|
||||||
x: 10,
|
|
||||||
y: 20,
|
|
||||||
});
|
|
||||||
it("should compute coords correctly when ellipse", () => {
|
|
||||||
expect(computeContainerCoords(boundTextElement, "ellipse")).toEqual({
|
|
||||||
x: -24.289321881345245,
|
|
||||||
y: 0.3553390593273775,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should compute coords correctly when rectangle", () => {
|
|
||||||
expect(computeContainerCoords(boundTextElement, "rectangle")).toEqual({
|
|
||||||
x: 5,
|
|
||||||
y: 15,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should compute coords correctly when diamond", () => {
|
|
||||||
expect(computeContainerCoords(boundTextElement, "diamond")).toEqual({
|
|
||||||
x: -45,
|
|
||||||
y: -10,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test computeContainerDimensionForBoundText", () => {
|
|
||||||
const params = {
|
|
||||||
width: 178,
|
|
||||||
height: 194,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should compute container height correctly for rectangle", () => {
|
|
||||||
const element = API.createElement({
|
|
||||||
type: "rectangle",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
|
||||||
160,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should compute container height correctly for ellipse", () => {
|
|
||||||
const element = API.createElement({
|
|
||||||
type: "ellipse",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
|
||||||
226,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should compute container height correctly for diamond", () => {
|
|
||||||
const element = API.createElement({
|
|
||||||
type: "diamond",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
|
||||||
320,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test getMaxContainerWidth", () => {
|
|
||||||
const params = {
|
|
||||||
width: 178,
|
|
||||||
height: 194,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should return max width when container is rectangle", () => {
|
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
|
||||||
expect(getMaxContainerWidth(container)).toBe(168);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return max width when container is ellipse", () => {
|
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
|
||||||
expect(getMaxContainerWidth(container)).toBe(116);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return max width when container is diamond", () => {
|
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
|
||||||
expect(getMaxContainerWidth(container)).toBe(79);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Test getMaxContainerHeight", () => {
|
|
||||||
const params = {
|
|
||||||
width: 178,
|
|
||||||
height: 194,
|
|
||||||
};
|
|
||||||
|
|
||||||
it("should return max height when container is rectangle", () => {
|
|
||||||
const container = API.createElement({ type: "rectangle", ...params });
|
|
||||||
expect(getMaxContainerHeight(container)).toBe(184);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return max height when container is ellipse", () => {
|
|
||||||
const container = API.createElement({ type: "ellipse", ...params });
|
|
||||||
expect(getMaxContainerHeight(container)).toBe(127);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return max height when container is diamond", () => {
|
|
||||||
const container = API.createElement({ type: "diamond", ...params });
|
|
||||||
expect(getMaxContainerHeight(container)).toBe(87);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
+35
-104
@@ -12,7 +12,11 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
|||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
|
isImageElement,
|
||||||
|
isArrowElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isTextBindableContainer } from "./typeChecks";
|
import { isTextBindableContainer } from "./typeChecks";
|
||||||
@@ -23,7 +27,6 @@ import {
|
|||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
import { ExtractSetType } from "../utility-types";
|
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
@@ -81,9 +84,9 @@ export const redrawTextBoundingBox = (
|
|||||||
|
|
||||||
let nextHeight = containerDims.height;
|
let nextHeight = containerDims.height;
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (metrics.height > maxContainerHeight) {
|
||||||
nextHeight = computeContainerDimensionForBoundText(
|
nextHeight = computeContainerHeightForBoundText(
|
||||||
|
container,
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
|
||||||
);
|
);
|
||||||
mutateElement(container, { height: nextHeight });
|
mutateElement(container, { height: nextHeight });
|
||||||
maxContainerHeight = getMaxContainerHeight(container);
|
maxContainerHeight = getMaxContainerHeight(container);
|
||||||
@@ -147,7 +150,6 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
shouldMaintainAspectRatio = false,
|
|
||||||
) => {
|
) => {
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId) {
|
if (!boundTextElementId) {
|
||||||
@@ -185,10 +187,10 @@ export const handleBindTextResize = (
|
|||||||
nextWidth = dimensions.width;
|
nextWidth = dimensions.width;
|
||||||
}
|
}
|
||||||
// increase height in case text element height exceeds
|
// increase height in case text element height exceeds
|
||||||
if (!shouldMaintainAspectRatio && nextHeight > maxHeight) {
|
if (nextHeight > maxHeight) {
|
||||||
containerHeight = computeContainerDimensionForBoundText(
|
containerHeight = computeContainerHeightForBoundText(
|
||||||
|
container,
|
||||||
nextHeight,
|
nextHeight,
|
||||||
container.type,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const diff = containerHeight - containerDims.height;
|
const diff = containerHeight - containerDims.height;
|
||||||
@@ -206,53 +208,6 @@ export const handleBindTextResize = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
shouldMaintainAspectRatio &&
|
|
||||||
(nextHeight > maxHeight || nextWidth > maxWidth)
|
|
||||||
) {
|
|
||||||
let height = containerDims.height;
|
|
||||||
let width = containerDims.width;
|
|
||||||
let x = container.x;
|
|
||||||
let y = container.y;
|
|
||||||
|
|
||||||
if (nextHeight > maxHeight) {
|
|
||||||
height = computeContainerDimensionForBoundText(
|
|
||||||
nextHeight,
|
|
||||||
container.type,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (nextWidth > maxWidth) {
|
|
||||||
width = computeContainerDimensionForBoundText(
|
|
||||||
nextWidth,
|
|
||||||
container.type,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const diffX = width - containerDims.width;
|
|
||||||
const diffY = height - containerDims.height;
|
|
||||||
|
|
||||||
if (transformHandleType === "n") {
|
|
||||||
y = container.y - diffY;
|
|
||||||
} else if (
|
|
||||||
transformHandleType === "e" ||
|
|
||||||
transformHandleType === "w" ||
|
|
||||||
transformHandleType === "ne" ||
|
|
||||||
transformHandleType === "nw"
|
|
||||||
) {
|
|
||||||
y = container.y - diffY / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (transformHandleType === "s" || transformHandleType === "n") {
|
|
||||||
x = container.x - diffX / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(container, {
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mutateElement(textElement, {
|
mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
@@ -275,7 +230,7 @@ const computeBoundTextPosition = (
|
|||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
) => {
|
) => {
|
||||||
const containerCoords = computeBoundTextElementCoords(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
const maxContainerHeight = getMaxContainerHeight(container);
|
const maxContainerHeight = getMaxContainerHeight(container);
|
||||||
const maxContainerWidth = getMaxContainerWidth(container);
|
const maxContainerWidth = getMaxContainerWidth(container);
|
||||||
|
|
||||||
@@ -369,6 +324,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
const push = (str: string) => {
|
const push = (str: string) => {
|
||||||
if (str.trim()) {
|
if (str.trim()) {
|
||||||
lines.push(str);
|
lines.push(str);
|
||||||
@@ -442,7 +398,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
const word = words[index];
|
const word = words[index];
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
if (currentLineWidthTillNow > maxWidth) {
|
if (currentLineWidthTillNow >= maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
@@ -644,9 +600,7 @@ export const getContainerCenter = (
|
|||||||
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const computeBoundTextElementCoords = (
|
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
||||||
container: NonDeletedExcalidrawElement,
|
|
||||||
) => {
|
|
||||||
let offsetX = BOUND_TEXT_PADDING;
|
let offsetX = BOUND_TEXT_PADDING;
|
||||||
let offsetY = BOUND_TEXT_PADDING;
|
let offsetY = BOUND_TEXT_PADDING;
|
||||||
|
|
||||||
@@ -666,27 +620,6 @@ export const computeBoundTextElementCoords = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const computeContainerCoords = (
|
|
||||||
boundTextElement: ExcalidrawTextElement,
|
|
||||||
containerType: string,
|
|
||||||
) => {
|
|
||||||
let offsetX = BOUND_TEXT_PADDING;
|
|
||||||
let offsetY = BOUND_TEXT_PADDING;
|
|
||||||
|
|
||||||
if (containerType === "ellipse") {
|
|
||||||
offsetX += (boundTextElement.width / 2) * (1 - Math.sqrt(2) / 2);
|
|
||||||
offsetY += (boundTextElement.height / 2) * (1 - Math.sqrt(2) / 2);
|
|
||||||
}
|
|
||||||
if (containerType === "diamond") {
|
|
||||||
offsetX += boundTextElement.width / 4;
|
|
||||||
offsetY += boundTextElement.height / 4;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
x: boundTextElement.x - offsetX,
|
|
||||||
y: boundTextElement.y - offsetY,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||||
const container = getContainerElement(textElement);
|
const container = getContainerElement(textElement);
|
||||||
if (!container || isArrowElement(container)) {
|
if (!container || isArrowElement(container)) {
|
||||||
@@ -781,34 +714,32 @@ export const getTextBindableContainerAtPosition = (
|
|||||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VALID_CONTAINER_TYPES = new Set([
|
export const isValidTextContainer = (element: ExcalidrawElement) => {
|
||||||
"rectangle",
|
return (
|
||||||
"ellipse",
|
element.type === "rectangle" ||
|
||||||
"diamond",
|
element.type === "ellipse" ||
|
||||||
"image",
|
element.type === "diamond" ||
|
||||||
"arrow",
|
isImageElement(element) ||
|
||||||
]);
|
isArrowElement(element)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isValidTextContainer = (element: ExcalidrawElement) =>
|
export const computeContainerHeightForBoundText = (
|
||||||
VALID_CONTAINER_TYPES.has(element.type);
|
container: NonDeletedExcalidrawElement,
|
||||||
|
boundTextElementHeight: number,
|
||||||
export const computeContainerDimensionForBoundText = (
|
|
||||||
dimension: number,
|
|
||||||
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
|
||||||
) => {
|
) => {
|
||||||
dimension = Math.ceil(dimension);
|
if (container.type === "ellipse") {
|
||||||
const padding = BOUND_TEXT_PADDING * 2;
|
return Math.round(
|
||||||
|
((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2,
|
||||||
if (containerType === "ellipse") {
|
);
|
||||||
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
|
||||||
}
|
}
|
||||||
if (containerType === "arrow") {
|
if (isArrowElement(container)) {
|
||||||
return dimension + padding * 8;
|
return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2;
|
||||||
}
|
}
|
||||||
if (containerType === "diamond") {
|
if (container.type === "diamond") {
|
||||||
return 2 * (dimension + padding);
|
return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2);
|
||||||
}
|
}
|
||||||
return dimension + padding;
|
return boundTextElementHeight + BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||||
|
|||||||
@@ -3,14 +3,10 @@ import ExcalidrawApp from "../excalidraw-app";
|
|||||||
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
||||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import {
|
import { fireEvent } from "../tests/test-utils";
|
||||||
fireEvent,
|
|
||||||
mockBoundingClientRect,
|
|
||||||
restoreOriginalGetBoundingClientRect,
|
|
||||||
} from "../tests/test-utils";
|
|
||||||
import { queryByText } from "@testing-library/react";
|
import { queryByText } from "@testing-library/react";
|
||||||
|
|
||||||
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
import { FONT_FAMILY } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
@@ -19,7 +15,6 @@ import { API } from "../tests/helpers/api";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { resize } from "../tests/utils";
|
import { resize } from "../tests/utils";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -226,19 +221,11 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
describe("Test container-unbound text", () => {
|
describe("Test container-unbound text", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const dimensions = { height: 400, width: 800 };
|
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
mockBoundingClientRect(dimensions);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
//@ts-ignore
|
|
||||||
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
|
||||||
|
|
||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
@@ -248,10 +235,6 @@ describe("textWysiwyg", () => {
|
|||||||
)!;
|
)!;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
restoreOriginalGetBoundingClientRect();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should add a tab at the start of the first line", () => {
|
it("should add a tab at the start of the first line", () => {
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
textarea.value = "Line#1\nLine#2";
|
textarea.value = "Line#1\nLine#2";
|
||||||
@@ -450,27 +433,6 @@ describe("textWysiwyg", () => {
|
|||||||
);
|
);
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("text should never go beyond max width", async () => {
|
|
||||||
UI.clickTool("text");
|
|
||||||
mouse.clickAt(750, 300);
|
|
||||||
|
|
||||||
textarea = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
)!;
|
|
||||||
fireEvent.change(textarea, {
|
|
||||||
target: {
|
|
||||||
value:
|
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
textarea.dispatchEvent(new Event("input"));
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
|
||||||
textarea.blur();
|
|
||||||
expect(textarea.style.width).toBe("792px");
|
|
||||||
expect(h.elements[0].width).toBe(1000);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test container-bound text", () => {
|
describe("Test container-bound text", () => {
|
||||||
@@ -669,11 +631,11 @@ describe("textWysiwyg", () => {
|
|||||||
["freedraw", "line"].forEach((type: any) => {
|
["freedraw", "line"].forEach((type: any) => {
|
||||||
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
||||||
h.elements = [];
|
h.elements = [];
|
||||||
const element = UI.createElement(type, {
|
const elemnet = UI.createElement(type, {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 50,
|
height: 50,
|
||||||
});
|
});
|
||||||
API.setSelectedElements([element]);
|
API.setSelectedElements([elemnet]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -989,136 +951,26 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.only("when using shift resize", () => {
|
it("should scale font size correctly when resizing using shift", async () => {
|
||||||
it("should wrap text correctly when resizing using shift from 'ne' handle", async () => {
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, {
|
fireEvent.change(editor, { target: { value: "Hello" } });
|
||||||
target: { value: "Excalidraw is an opensource virtual whiteboard" },
|
editor.blur();
|
||||||
});
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
editor.blur();
|
expect(rectangle.width).toBe(90);
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
expect(rectangle.height).toBe(75);
|
||||||
expect(rectangle.width).toBe(90);
|
expect(textElement.fontSize).toBe(20);
|
||||||
expect(rectangle.height).toBe(202);
|
|
||||||
expect(textElement.fontSize).toBe(20);
|
|
||||||
expect(textElement.text).toBe(
|
|
||||||
`Excalid
|
|
||||||
raw is
|
|
||||||
an
|
|
||||||
opensou
|
|
||||||
rce
|
|
||||||
virtual
|
|
||||||
whitebo
|
|
||||||
ard`,
|
|
||||||
);
|
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
|
||||||
shift: true,
|
shift: true,
|
||||||
});
|
|
||||||
expect(rectangle.width).toBe(200);
|
|
||||||
expect(rectangle.height).toBe(449);
|
|
||||||
expect(textElement.fontSize).toBe(20);
|
|
||||||
expect(textElement.text).toBe(
|
|
||||||
`Excalidraw is an
|
|
||||||
opensource virtual
|
|
||||||
whiteboard`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should wrap text correctly when resizing using shift vertically using 'n' handle", async () => {
|
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
|
||||||
|
|
||||||
const editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: { value: "Excalidraw is an opensource virtual whiteboard" },
|
|
||||||
});
|
|
||||||
editor.blur();
|
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
|
||||||
expect(rectangle.width).toBe(90);
|
|
||||||
expect(rectangle.height).toBe(202);
|
|
||||||
expect(textElement.fontSize).toBe(20);
|
|
||||||
expect(textElement.text).toBe(
|
|
||||||
`Excalid
|
|
||||||
raw is
|
|
||||||
an
|
|
||||||
opensou
|
|
||||||
rce
|
|
||||||
virtual
|
|
||||||
whitebo
|
|
||||||
ard`,
|
|
||||||
);
|
|
||||||
|
|
||||||
resize(rectangle, "n", [rectangle.x + 30, rectangle.y - 50], {
|
|
||||||
shift: true,
|
|
||||||
});
|
|
||||||
expect(rectangle.width).toBe(104);
|
|
||||||
expect(rectangle.height).toBe(232);
|
|
||||||
expect(textElement.fontSize).toBe(20);
|
|
||||||
expect(textElement.text).toBe(
|
|
||||||
`Excalid
|
|
||||||
raw is
|
|
||||||
an
|
|
||||||
opensou
|
|
||||||
rce
|
|
||||||
virtual
|
|
||||||
whitebo
|
|
||||||
ard`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should wrap text correctly when resizing using shift horizontally and text overflows", async () => {
|
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
|
||||||
|
|
||||||
const editor = document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
fireEvent.change(editor, {
|
|
||||||
target: { value: "Excalidraw is an opensource virtual whiteboard" },
|
|
||||||
});
|
|
||||||
editor.blur();
|
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
|
||||||
expect(rectangle.width).toBe(90);
|
|
||||||
expect(rectangle.height).toBe(202);
|
|
||||||
expect(rectangle.y).toBe(20);
|
|
||||||
expect(textElement.fontSize).toBe(20);
|
|
||||||
expect(textElement.text).toBe(
|
|
||||||
`Excalid
|
|
||||||
raw is
|
|
||||||
an
|
|
||||||
opensou
|
|
||||||
rce
|
|
||||||
virtual
|
|
||||||
whitebo
|
|
||||||
ard`,
|
|
||||||
);
|
|
||||||
|
|
||||||
resize(rectangle, "e", [rectangle.x - 30, rectangle.y + 30], {
|
|
||||||
shift: true,
|
|
||||||
});
|
|
||||||
expect(rectangle.width).toBe(70);
|
|
||||||
expect(rectangle.height).toBe(226);
|
|
||||||
expect(rectangle.y).toBe(8);
|
|
||||||
expect(textElement.fontSize).toBe(20);
|
|
||||||
expect(textElement.text).toBe(
|
|
||||||
`Excal
|
|
||||||
idraw
|
|
||||||
is an
|
|
||||||
opens
|
|
||||||
ource
|
|
||||||
virtu
|
|
||||||
al
|
|
||||||
white
|
|
||||||
board`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
expect(rectangle.width).toBe(200);
|
||||||
|
expect(rectangle.height).toBe(166.66666666666669);
|
||||||
|
expect(textElement.fontSize).toBe(47.5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||||
@@ -1418,91 +1270,5 @@ board`,
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
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,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { mutateElement } from "./mutateElement";
|
|||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
getApproxLineHeight,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
computeBoundTextElementCoords,
|
getContainerCoords,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
@@ -233,7 +233,7 @@ export const textWysiwyg = ({
|
|||||||
// Start pushing text upward until a diff of 30px (padding)
|
// Start pushing text upward until a diff of 30px (padding)
|
||||||
// is reached
|
// is reached
|
||||||
else {
|
else {
|
||||||
const containerCoords = computeBoundTextElementCoords(container);
|
const containerCoords = getContainerCoords(container);
|
||||||
|
|
||||||
// vertically center align the text
|
// vertically center align the text
|
||||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||||
@@ -272,9 +272,9 @@ export const textWysiwyg = ({
|
|||||||
: updatedTextElement.height / lines.length;
|
: updatedTextElement.height / lines.length;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
textElementWidth = Math.min(textElementWidth, maxWidth);
|
}
|
||||||
} else if (isFirefox || isSafari) {
|
// As firefox, Safari needs little higher dimensions on DOM
|
||||||
// As firefox, Safari needs little higher dimensions on DOM
|
if (isFirefox || isSafari) {
|
||||||
textElementWidth += 0.5;
|
textElementWidth += 0.5;
|
||||||
}
|
}
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { MarkNonNullable } from "../utility-types";
|
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -140,7 +139,7 @@ export const hasBoundTextElement = (
|
|||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
|
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
|
||||||
return (
|
return (
|
||||||
isTextBindableContainer(element) &&
|
isBindableElement(element) &&
|
||||||
!!element.boundElements?.some(({ type }) => type === "text")
|
!!element.boundElements?.some(({ type }) => type === "text")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
THEME,
|
THEME,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { MarkNonNullable, ValueOf } from "../utility-types";
|
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { encryptData, decryptData } from "../../data/encryption";
|
|||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
import { reconcileElements } from "../collab/reconciliation";
|
import { reconcileElements } from "../collab/reconciliation";
|
||||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||||
import { ResolutionType } from "../../utility-types";
|
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ import { useAtomWithInitialValue } from "../jotai";
|
|||||||
import { appJotaiStore } from "./app-jotai";
|
import { appJotaiStore } from "./app-jotai";
|
||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { ResolutionType } from "../utility-types";
|
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
|
|||||||
Vendored
+47
@@ -50,6 +50,36 @@ interface Clipboard extends EventTarget {
|
|||||||
write(data: any[]): Promise<void>;
|
write(data: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Mutable<T> = {
|
||||||
|
-readonly [P in keyof T]: T[P];
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValueOf<T> = T[keyof T];
|
||||||
|
|
||||||
|
type Merge<M, N> = Omit<M, keyof N> & N;
|
||||||
|
|
||||||
|
/** utility type to assert that the second type is a subtype of the first type.
|
||||||
|
* Returns the subtype. */
|
||||||
|
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
|
||||||
|
|
||||||
|
type ResolutionType<T extends (...args: any) => any> = T extends (
|
||||||
|
...args: any
|
||||||
|
) => Promise<infer R>
|
||||||
|
? R
|
||||||
|
: any;
|
||||||
|
|
||||||
|
// https://github.com/krzkaczor/ts-essentials
|
||||||
|
type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||||
|
|
||||||
|
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
|
||||||
|
Required<Pick<T, RK>>;
|
||||||
|
|
||||||
|
type MarkNonNullable<T, K extends keyof T> = {
|
||||||
|
[P in K]-?: P extends K ? NonNullable<T[P]> : T[P];
|
||||||
|
} & { [P in keyof T]: T[P] };
|
||||||
|
|
||||||
|
type NonOptional<T> = Exclude<T, undefined>;
|
||||||
|
|
||||||
// PNG encoding/decoding
|
// PNG encoding/decoding
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
||||||
@@ -71,6 +101,23 @@ declare module "png-chunks-extract" {
|
|||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// type getter for interface's callable type
|
||||||
|
// src: https://stackoverflow.com/a/58658851/927631
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
type SignatureType<T> = T extends (...args: infer R) => any ? R : never;
|
||||||
|
type CallableType<T extends (...args: any[]) => any> = (
|
||||||
|
...args: SignatureType<T>
|
||||||
|
) => ReturnType<T>;
|
||||||
|
// --------------------------------------------------------------------------—
|
||||||
|
|
||||||
|
// Type for React.forwardRef --- supply only the first generic argument T
|
||||||
|
type ForwardRef<T, P = any> = Parameters<
|
||||||
|
CallableType<React.ForwardRefRenderFunction<T, P>>
|
||||||
|
>[1];
|
||||||
|
|
||||||
|
// --------------------------------------------------------------------------—
|
||||||
|
|
||||||
interface Blob {
|
interface Blob {
|
||||||
handle?: import("browser-fs-acces").FileSystemHandle;
|
handle?: import("browser-fs-acces").FileSystemHandle;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { AppState } from "./types";
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { isLinearElement } from "./element/typeChecks";
|
import { isLinearElement } from "./element/typeChecks";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
import { Mutable } from "./utility-types";
|
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
|
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
|
||||||
|
|||||||
@@ -110,7 +110,6 @@
|
|||||||
"increaseFontSize": "Increase font size",
|
"increaseFontSize": "Increase font size",
|
||||||
"unbindText": "Unbind text",
|
"unbindText": "Unbind text",
|
||||||
"bindText": "Bind text to the container",
|
"bindText": "Bind text to the container",
|
||||||
"createContainerFromText": "Wrap text in a container",
|
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edit link",
|
"edit": "Edit link",
|
||||||
"create": "Create link",
|
"create": "Create link",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { getShapeForElement } from "./renderer/renderElement";
|
import { getShapeForElement } from "./renderer/renderElement";
|
||||||
import { getCurvePathOps } from "./element/bounds";
|
import { getCurvePathOps } from "./element/bounds";
|
||||||
import { Mutable } from "./utility-types";
|
|
||||||
|
|
||||||
export const rotate = (
|
export const rotate = (
|
||||||
x1: number,
|
x1: number,
|
||||||
|
|||||||
@@ -2032,9 +2032,9 @@ dns-equal@^1.0.0:
|
|||||||
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
|
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
|
||||||
|
|
||||||
dns-packet@^5.2.2:
|
dns-packet@^5.2.2:
|
||||||
version "5.4.0"
|
version "5.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
|
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.3.1.tgz#eb94413789daec0f0ebe2fcc230bdc9d7c91b43d"
|
||||||
integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
|
integrity sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@leichtgewicht/ip-codec" "^2.0.1"
|
"@leichtgewicht/ip-codec" "^2.0.1"
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
hasBoundTextElement,
|
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getDiamondPoints,
|
getDiamondPoints,
|
||||||
@@ -42,10 +41,7 @@ import { getStroke, StrokeOptions } from "perfect-freehand";
|
|||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
getApproxLineHeight,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
computeBoundTextElementCoords,
|
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getMaxContainerHeight,
|
|
||||||
getMaxContainerWidth,
|
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
@@ -659,7 +655,7 @@ const generateElementShape = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateElementWithCanvas = (
|
export const generateElementWithCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: RenderConfig,
|
||||||
) => {
|
) => {
|
||||||
@@ -815,21 +811,6 @@ const drawElementFromCanvas = (
|
|||||||
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
|
||||||
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
|
|
||||||
hasBoundTextElement(element)
|
|
||||||
) {
|
|
||||||
const coords = computeBoundTextElementCoords(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();
|
context.restore();
|
||||||
|
|
||||||
|
|||||||
@@ -119,15 +119,6 @@ Object {
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"contextItemLabel": "labels.createContainerFromText",
|
|
||||||
"name": "createContainerFromText",
|
|
||||||
"perform": [Function],
|
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": Object {
|
|
||||||
"category": "element",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"contextItemLabel": "labels.ungroup",
|
"contextItemLabel": "labels.ungroup",
|
||||||
@@ -4516,15 +4507,6 @@ Object {
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"contextItemLabel": "labels.createContainerFromText",
|
|
||||||
"name": "createContainerFromText",
|
|
||||||
"perform": [Function],
|
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": Object {
|
|
||||||
"category": "element",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"contextItemLabel": "labels.ungroup",
|
"contextItemLabel": "labels.ungroup",
|
||||||
@@ -5066,15 +5048,6 @@ Object {
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"contextItemLabel": "labels.createContainerFromText",
|
|
||||||
"name": "createContainerFromText",
|
|
||||||
"perform": [Function],
|
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": Object {
|
|
||||||
"category": "element",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"contextItemLabel": "labels.ungroup",
|
"contextItemLabel": "labels.ungroup",
|
||||||
@@ -5915,15 +5888,6 @@ Object {
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"contextItemLabel": "labels.createContainerFromText",
|
|
||||||
"name": "createContainerFromText",
|
|
||||||
"perform": [Function],
|
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": Object {
|
|
||||||
"category": "element",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"contextItemLabel": "labels.ungroup",
|
"contextItemLabel": "labels.ungroup",
|
||||||
@@ -6261,15 +6225,6 @@ Object {
|
|||||||
"category": "element",
|
"category": "element",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"contextItemLabel": "labels.createContainerFromText",
|
|
||||||
"name": "createContainerFromText",
|
|
||||||
"perform": [Function],
|
|
||||||
"predicate": [Function],
|
|
||||||
"trackEvent": Object {
|
|
||||||
"category": "element",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"PanelComponent": [Function],
|
"PanelComponent": [Function],
|
||||||
"contextItemLabel": "labels.ungroup",
|
"contextItemLabel": "labels.ungroup",
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import { newFreeDrawElement, newImageElement } from "../../element/newElement";
|
|||||||
import { Point } from "../../types";
|
import { Point } from "../../types";
|
||||||
import { getSelectedElements } from "../../scene/selection";
|
import { getSelectedElements } from "../../scene/selection";
|
||||||
import { isLinearElementType } from "../../element/typeChecks";
|
import { isLinearElementType } from "../../element/typeChecks";
|
||||||
import { Mutable } from "../../utility-types";
|
|
||||||
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -31,7 +31,6 @@ import Library from "./data/library";
|
|||||||
import type { FileSystemHandle } from "./data/filesystem";
|
import type { FileSystemHandle } from "./data/filesystem";
|
||||||
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
|
||||||
import { ContextMenuItems } from "./components/ContextMenu";
|
import { ContextMenuItems } from "./components/ContextMenu";
|
||||||
import { Merge, ForwardRef } from "./utility-types";
|
|
||||||
|
|
||||||
export type Point = Readonly<RoughPoint>;
|
export type Point = Readonly<RoughPoint>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
export type Mutable<T> = {
|
|
||||||
-readonly [P in keyof T]: T[P];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ValueOf<T> = T[keyof T];
|
|
||||||
|
|
||||||
export type Merge<M, N> = Omit<M, keyof N> & N;
|
|
||||||
|
|
||||||
/** utility type to assert that the second type is a subtype of the first type.
|
|
||||||
* Returns the subtype. */
|
|
||||||
export type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
|
|
||||||
|
|
||||||
export type ResolutionType<T extends (...args: any) => any> = T extends (
|
|
||||||
...args: any
|
|
||||||
) => Promise<infer R>
|
|
||||||
? R
|
|
||||||
: any;
|
|
||||||
|
|
||||||
// https://github.com/krzkaczor/ts-essentials
|
|
||||||
export type MarkOptional<T, K extends keyof T> = Omit<T, K> &
|
|
||||||
Partial<Pick<T, K>>;
|
|
||||||
|
|
||||||
export type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
|
|
||||||
Required<Pick<T, RK>>;
|
|
||||||
|
|
||||||
export type MarkNonNullable<T, K extends keyof T> = {
|
|
||||||
[P in K]-?: P extends K ? NonNullable<T[P]> : T[P];
|
|
||||||
} & { [P in keyof T]: T[P] };
|
|
||||||
|
|
||||||
export type NonOptional<T> = Exclude<T, undefined>;
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// type getter for interface's callable type
|
|
||||||
// src: https://stackoverflow.com/a/58658851/927631
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
export type SignatureType<T> = T extends (...args: infer R) => any ? R : never;
|
|
||||||
export type CallableType<T extends (...args: any[]) => any> = (
|
|
||||||
...args: SignatureType<T>
|
|
||||||
) => ReturnType<T>;
|
|
||||||
// --------------------------------------------------------------------------—
|
|
||||||
|
|
||||||
// Type for React.forwardRef --- supply only the first generic argument T
|
|
||||||
export type ForwardRef<T, P = any> = Parameters<
|
|
||||||
CallableType<React.ForwardRefRenderFunction<T, P>>
|
|
||||||
>[1];
|
|
||||||
|
|
||||||
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
|
|
||||||
? U
|
|
||||||
: never;
|
|
||||||
@@ -16,7 +16,6 @@ import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
|
|||||||
import { unstable_batchedUpdates } from "react-dom";
|
import { unstable_batchedUpdates } from "react-dom";
|
||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { isEraserActive, isHandToolActive } from "./appState";
|
import { isEraserActive, isHandToolActive } from "./appState";
|
||||||
import { ResolutionType } from "./utility-types";
|
|
||||||
|
|
||||||
let mockDateTime: string | null = null;
|
let mockDateTime: string | null = null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user