Compare commits

...

22 Commits

Author SHA1 Message Date
zsviczian 17477480f3 check for strict null 2024-08-09 23:21:43 +02:00
Clarence Chan f7b3befd0a fix: text content with tab characters act different in view/edit (#8336)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-08-09 20:20:36 +00:00
DDDDD12138 7b2bee9746 chore: remove unused parameter (#8355) 2024-08-09 21:39:40 +02:00
David Luzar 88014ace4a fix: drawing from 0-dimension canvas (#8356) 2024-08-09 21:36:04 +02:00
David Luzar 87a9430809 fix: disable flowchart keybindings inside inputs (#8353) 2024-08-09 18:44:17 +02:00
Márk Tolmács 99b91c46f7 fix: Yet more patching of intersect code (#8352)
* Yet more patching of intersect code
2024-08-09 17:33:12 +02:00
David Luzar 1ea5b26f25 fix: missing act() in flowchart tests (#8354) 2024-08-09 17:27:02 +02:00
Clarence Chan d5f4ee7b3f fix: z-index change by one causes app to freeze (#8314) 2024-08-09 15:26:08 +02:00
Márk Tolmács 261304c1a4 fix: Patch over intersection calculation issue (#8350)
* Patch over intersection calculation issue
2024-08-09 14:40:57 +02:00
Márk Tolmács 84398a7e5c fix: Point duplication in LEE on ALT+click (#8347) 2024-08-09 12:01:32 +02:00
Ryan Di 54491d13d4 feat: create flowcharts from a generic element using elbow arrows (#8329)
Co-authored-by: Mark Tolmacs <mark@lazycat.hu>
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-08-08 21:43:15 +02:00
Márk Tolmács dd1370381d chore: Refactor and remove scene from elbow arrow generation (#8342)
* Refactor and remove scene from elbow arrow generation
2024-08-08 14:06:26 +02:00
Márk Tolmács 72d6ee48fc fix: Do not allow resizing unbound elbow arrows either (#8333)
* Fix resizing of unbound elbow arrows
2024-08-06 17:33:34 +02:00
David Luzar 232242d2e9 test: skip test.yml in PRs (#8330) 2024-08-06 16:04:04 +02:00
David Luzar f19ce30dfe chore: bump @testing-library/react 12.1.5 -> 16.0.0 (#8322) 2024-08-06 15:17:42 +02:00
Ryan Di 3cf14c73a3 refactor: rename draggingElement -> newElement (#8294)
* add newElement to appState

* freedraw should not be an editing element

* do not set editing element for freedraw and generic

* remove ununsed `appState.draggingElement`

* remove setting dragged for new linear element

* decouple selection element from new element

* fix hint for text bindables

* update snapshot

* fixes

* fix frame regressions

* add comments to types

* document `editingElement`

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-08-06 19:26:06 +08:00
Márk Tolmács 8d530cf102 fix: Docker build in CI (#8312)
* Fix Docker build CI
* Bump nginx-alpine version to 1.27
2024-08-06 13:21:20 +02:00
David Luzar b87925d253 build: add example apps public and vite dev-dist to eslintignore (#8326) 2024-08-05 23:43:48 +02:00
b1sar a6684b09f2 docs: fix typos in props.mdx (#8325) 2024-08-05 21:41:19 +00:00
David Luzar b427617f66 build: add rm:build, rm:node_modules & clean-install scripts (#8323) 2024-08-05 23:37:33 +02:00
dependabot[bot] 2907fbe34b build(deps): bump @excalidraw/excalidraw from 0.17.0 to 0.17.6 in /dev-docs (#7908)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-05 09:49:57 +02:00
zsviczian c67815f7b0 fix: Duplicating arrow without bound elements throws error (#8316)
Co-authored-by: David Luzar <5153846+dwelle@users.noreply.github.com>
2024-08-03 16:37:36 +00:00
109 changed files with 3282 additions and 1895 deletions
+1
View File
@@ -8,6 +8,7 @@
!package.json
!public/
!packages/
!scripts/
!tsconfig.json
!yarn.lock
+2
View File
@@ -6,3 +6,5 @@ firebase/
dist/
public/workbox
packages/excalidraw/types
examples/**/public
dev-dist
-1
View File
@@ -1,7 +1,6 @@
name: Tests
on:
pull_request:
push:
branches: master
+1 -1
View File
@@ -12,7 +12,7 @@ ARG NODE_ENV=production
RUN yarn build:app:docker
FROM nginx:1.24-alpine
FROM nginx:1.27-alpine
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
@@ -9,9 +9,9 @@ All `props` are _optional_.
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down events |
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when something is pasted into the scene |
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
@@ -26,7 +26,7 @@ All `props` are _optional_.
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](/docs/@excalidraw/excalidraw/api/props/ui-options#canvasactions) |
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
| [`autoFocus`](#autofocus) | `boolean` | `false` | Indicates whether to focus the Excalidraw component on page load |
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
| [`validateEmbeddable`](#validateEmbeddable) | string[] | `boolean | RegExp | RegExp[] | ((link: string) => boolean | undefined)` | \_ | use for custom src url validation |
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
+1 -1
View File
@@ -18,7 +18,7 @@
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.17.0",
"@excalidraw/excalidraw": "0.17.6",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3",
+4 -4
View File
@@ -1718,10 +1718,10 @@
url-loader "^4.1.1"
webpack "^5.73.0"
"@excalidraw/excalidraw@0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.0.tgz#3c64aa8e36406ac171b008cfecbdce5bb0755725"
integrity sha512-NzP22v5xMqxYW27ZtTHhiGFe7kE8NeBk45aoeM/mDSkXiOXPDH+PcvwzHRN/Ei+Vj/0sTPHxejn8bZyRWKGjXg==
"@excalidraw/excalidraw@0.17.6":
version "0.17.6"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.6.tgz#5fd208ce69d33ca712d1804b50d7d06d5c46ac4d"
integrity sha512-fyCl+zG/Z5yhHDh5Fq2ZGmphcrALmuOdtITm8gN4d8w4ntnaopTXcTfnAAaU3VleDC6LhTkoLOTG6P5kgREiIg==
"@hapi/hoek@^9.0.0":
version "9.3.0"
+5 -6
View File
@@ -2,7 +2,6 @@ import { vi } from "vitest";
import {
act,
render,
updateSceneData,
waitFor,
} from "../../packages/excalidraw/tests/test-utils";
import ExcalidrawApp from "../App";
@@ -88,12 +87,12 @@ describe("collaboration", () => {
const rect1 = API.createElement({ ...rect1Props });
const rect2 = API.createElement({ ...rect2Props });
updateSceneData({
API.updateScene({
elements: syncInvalidIndices([rect1, rect2]),
storeAction: StoreAction.CAPTURE,
});
updateSceneData({
API.updateScene({
elements: syncInvalidIndices([
rect1,
newElementWith(h.elements[1], { isDeleted: true }),
@@ -143,7 +142,7 @@ describe("collaboration", () => {
});
// simulate force deleting the element remotely
updateSceneData({
API.updateScene({
elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
});
@@ -178,7 +177,7 @@ describe("collaboration", () => {
act(() => h.app.actionManager.executeAction(undoAction));
// simulate local update
updateSceneData({
API.updateScene({
elements: syncInvalidIndices([
h.elements[0],
newElementWith(h.elements[1], { x: 100 }),
@@ -216,7 +215,7 @@ describe("collaboration", () => {
});
// simulate force deleting the element remotely
updateSceneData({
API.updateScene({
elements: syncInvalidIndices([rect1]),
storeAction: StoreAction.UPDATE,
});
+3 -3
View File
@@ -26,10 +26,10 @@ export default defineConfig({
assetFileNames(chunkInfo) {
if (chunkInfo?.name?.endsWith(".woff2")) {
// put on root so we are flexible about the CDN path
return '[name]-[hash][extname]';
return "[name]-[hash][extname]";
}
return 'assets/[name]-[hash][extname]';
return "assets/[name]-[hash][extname]";
},
// Creating separate chunk for locales except for en and percentages.json so they
// can be cached at runtime and not merged with
@@ -44,7 +44,7 @@ export default defineConfig({
// Taking the substring after "locales/"
return `locales/${id.substring(index + 8)}`;
}
}
},
},
},
sourcemap: true,
+4 -1
View File
@@ -76,7 +76,10 @@
"autorelease": "node scripts/autorelease.js",
"prerelease:excalidraw": "node scripts/prerelease.js",
"build:preview": "yarn build && vite preview --port 5000",
"release:excalidraw": "node scripts/release.js"
"release:excalidraw": "node scripts/release.js",
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/*/{build,dist}",
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
"clean-install": "yarn rm:node_modules && yarn install"
},
"resolutions": {
"@types/react": "18.2.0"
@@ -25,6 +25,7 @@ const deleteSelectedElements = (
appState: AppState,
app: AppClassProperties,
) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
const framesToBeDeleted = new Set(
getSelectedElements(
elements.filter((el) => isFrameLikeElement(el)),
@@ -51,7 +52,7 @@ const deleteSelectedElements = (
? null
: bound.endBinding,
});
mutateElbowArrow(bound, app.scene, bound.points);
mutateElbowArrow(bound, elementsMap, bound.points);
}
});
}
@@ -159,7 +160,7 @@ export const actionDeleteSelected = register({
LinearElementEditor.deletePoints(
element,
selectedPointsIndices,
app.scene,
elementsMap,
);
return {
@@ -44,7 +44,7 @@ export const actionDuplicateSelection = register({
if (appState.editingLinearElement) {
const ret = LinearElementEditor.duplicateSelectedPoints(
appState,
app.scene,
app.scene.getNonDeletedElementsMap(),
);
if (!ret) {
@@ -1,3 +1,4 @@
import React from "react";
import { Excalidraw } from "../index";
import { queryByTestId, fireEvent } from "@testing-library/react";
import { render } from "../tests/test-utils";
@@ -73,8 +73,8 @@ export const actionFinalize = register({
const multiPointElement = appState.multiElement
? appState.multiElement
: appState.editingElement?.type === "freedraw"
? appState.editingElement
: appState.newElement?.type === "freedraw"
? appState.newElement
: null;
if (multiPointElement) {
@@ -176,7 +176,8 @@ export const actionFinalize = register({
? appState.activeTool
: activeTool,
activeEmbeddable: null,
draggingElement: null,
newElement: null,
selectionElement: null,
multiElement: null,
editingElement: null,
startBoundElement: null,
@@ -204,7 +205,7 @@ export const actionFinalize = register({
keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) ||
(!appState.newElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null),
PanelComponent: ({ appState, updateData, data }) => (
@@ -120,7 +120,6 @@ const flipElements = (
true,
flipDirection === "horizontal" ? maxX : minX,
flipDirection === "horizontal" ? minY : maxY,
app.scene,
);
bindOrUnbindLinearElements(
@@ -4,7 +4,7 @@ import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import type { History } from "../history";
import { HistoryChangedEvent } from "../history";
import type { AppState } from "../types";
import type { AppClassProperties, AppState } from "../types";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { isWindows } from "../constants";
@@ -13,7 +13,8 @@ import type { Store } from "../store";
import { StoreAction } from "../store";
import { useEmitter } from "../hooks/useEmitter";
const writeData = (
const executeHistoryAction = (
app: AppClassProperties,
appState: Readonly<AppState>,
updater: () => [SceneElementsMap, AppState] | void,
): ActionResult => {
@@ -21,7 +22,10 @@ const writeData = (
!appState.multiElement &&
!appState.resizingElement &&
!appState.editingElement &&
!appState.draggingElement
!appState.newElement &&
!appState.selectedElementsAreBeingDragged &&
!appState.selectionElement &&
!app.flowChartCreator.isCreatingChart
) {
const result = updater();
@@ -51,12 +55,11 @@ export const createUndoAction: ActionCreator = (history, store) => ({
trackEvent: { category: "history" },
viewMode: false,
perform: (elements, appState, value, app) =>
writeData(appState, () =>
executeHistoryAction(app, appState, () =>
history.undo(
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
appState,
store.snapshot,
app.scene,
),
),
keyTest: (event) =>
@@ -93,12 +96,11 @@ export const createRedoAction: ActionCreator = (history, store) => ({
trackEvent: { category: "history" },
viewMode: false,
perform: (elements, appState, _, app) =>
writeData(appState, () =>
executeHistoryAction(app, appState, () =>
history.redo(
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
appState,
store.snapshot,
app.scene,
),
),
keyTest: (event) =>
@@ -1,3 +1,4 @@
import React from "react";
import { Excalidraw } from "../index";
import { queryByTestId } from "@testing-library/react";
import { render } from "../tests/test-utils";
@@ -6,8 +7,6 @@ import { API } from "../tests/helpers/api";
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
const { h } = window;
describe("element locking", () => {
beforeEach(async () => {
await render(<Excalidraw />);
@@ -22,7 +21,7 @@ describe("element locking", () => {
// just in case we change it in the future
expect(color).not.toBe(COLOR_PALETTE.transparent);
h.setState({
API.setAppState({
currentItemBackgroundColor: color,
});
const activeColor = queryByTestId(
@@ -40,14 +39,14 @@ describe("element locking", () => {
// just in case we change it in the future
expect(color).not.toBe(COLOR_PALETTE.transparent);
h.setState({
API.setAppState({
currentItemBackgroundColor: color,
currentItemFillStyle: "hachure",
});
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
expect(hachureFillButton).toHaveClass("active");
h.setState({
API.setAppState({
currentItemFillStyle: "solid",
});
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
@@ -57,7 +56,7 @@ describe("element locking", () => {
it("should not show fill style when background transparent", () => {
UI.clickTool("rectangle");
h.setState({
API.setAppState({
currentItemBackgroundColor: COLOR_PALETTE.transparent,
currentItemFillStyle: "hachure",
});
@@ -69,7 +68,7 @@ describe("element locking", () => {
it("should show horizontal text align for text tool", () => {
UI.clickTool("text");
h.setState({
API.setAppState({
currentItemTextAlign: "right",
});
@@ -85,7 +84,7 @@ describe("element locking", () => {
backgroundColor: "red",
fillStyle: "cross-hatch",
});
h.elements = [rect];
API.setElements([rect]);
API.setSelectedElements([rect]);
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
@@ -98,7 +97,7 @@ describe("element locking", () => {
backgroundColor: COLOR_PALETTE.transparent,
fillStyle: "cross-hatch",
});
h.elements = [rect];
API.setElements([rect]);
API.setSelectedElements([rect]);
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
@@ -114,7 +113,7 @@ describe("element locking", () => {
type: "rectangle",
strokeWidth: STROKE_WIDTH.thin,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
API.setSelectedElements([rect1, rect2]);
const thinStrokeWidthButton = queryByTestId(
@@ -133,7 +132,7 @@ describe("element locking", () => {
type: "rectangle",
strokeWidth: STROKE_WIDTH.bold,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
API.setSelectedElements([rect1, rect2]);
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
@@ -157,7 +156,7 @@ describe("element locking", () => {
type: "text",
fontFamily: FONT_FAMILY["Comic Shanns"],
});
h.elements = [rect, text];
API.setElements([rect, text]);
API.setSelectedElements([rect, text]);
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
@@ -1648,7 +1648,7 @@ export const actionChangeArrowType = register({
mutateElbowArrow(
newElement,
app.scene,
elementsMap,
[finalStartPoint, finalEndPoint].map(
(point) =>
[point[0] - newElement.x, point[1] - newElement.y] as Point,
+2 -2
View File
@@ -41,7 +41,7 @@ export const getDefaultAppState = (): Omit<
currentHoveredFontFamily: null,
cursorButton: "up",
activeEmbeddable: null,
draggingElement: null,
newElement: null,
editingElement: null,
editingGroupId: null,
editingLinearElement: null,
@@ -160,7 +160,7 @@ const APP_STATE_STORAGE_CONF = (<
currentHoveredFontFamily: { browser: false, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
activeEmbeddable: { browser: false, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
newElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
+4 -5
View File
@@ -29,7 +29,6 @@ import type {
} from "./element/types";
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { getNonDeletedGroupIds } from "./groups";
import type Scene from "./scene/Scene";
import { getObservedAppState } from "./store";
import type {
AppState,
@@ -1054,7 +1053,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
public applyTo(
elements: SceneElementsMap,
snapshot: Map<string, OrderedExcalidrawElement>,
scene: Scene,
): [SceneElementsMap, boolean] {
let nextElements = toBrandedType<SceneElementsMap>(new Map(elements));
let changedElements: Map<string, OrderedExcalidrawElement>;
@@ -1102,7 +1100,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
try {
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
ElementsChange.redrawBoundArrows(nextElements, changedElements, scene);
// the following reorder performs also mutations, but only on new instances of changed elements
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
@@ -1111,6 +1108,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
changedElements,
flags,
);
// Need ordered nextElements to avoid z-index binding issues
ElementsChange.redrawBoundArrows(nextElements, changedElements);
} catch (e) {
console.error(
`Couldn't mutate elements after applying elements change`,
@@ -1459,11 +1459,10 @@ export class ElementsChange implements Change<SceneElementsMap> {
private static redrawBoundArrows(
elements: SceneElementsMap,
changed: Map<string, OrderedExcalidrawElement>,
scene: Scene,
) {
for (const element of changed.values()) {
if (!element.isDeleted && isBindableElement(element)) {
updateBoundElements(element, elements, scene, {
updateBoundElements(element, elements, {
changedElements: changed,
});
}
File diff suppressed because it is too large Load Diff
@@ -9,7 +9,7 @@ import {
import {
assertExcalidrawWithSidebar,
assertSidebarDockButton,
} from "./Sidebar/Sidebar.test";
} from "./Sidebar/siderbar.test.helpers";
const { h } = window;
@@ -304,6 +304,16 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
className="HelpDialog__island--editor"
caption={t("helpDialog.editor")}
>
<Shortcut
label={t("helpDialog.createFlowchart")}
shortcuts={[getShortcutKey(`CtrlOrCmd+Arrow Key`)]}
isOr={true}
/>
<Shortcut
label={t("helpDialog.navigateFlowchart")}
shortcuts={[getShortcutKey(`Alt+Arrow Key`)]}
isOr={true}
/>
<Shortcut
label={t("labels.moveCanvas")}
shortcuts={[
@@ -9,6 +9,7 @@ $wide-viewport-width: 1000px;
box-sizing: border-box;
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
left: 0;
top: 100%;
+35 -7
View File
@@ -1,6 +1,7 @@
import { t } from "../i18n";
import type { AppClassProperties, Device, UIAppState } from "../types";
import {
isFlowchartNodeElement,
isImageElement,
isLinearElement,
isTextBindableContainer,
@@ -10,6 +11,7 @@ import { getShortcutKey } from "../utils";
import { isEraserActive } from "../appState";
import "./HintViewer.scss";
import { isNodeInFlowchart } from "../element/flowchart";
interface HintViewerProps {
appState: UIAppState;
@@ -18,7 +20,12 @@ interface HintViewerProps {
app: AppClassProperties;
}
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
const getHints = ({
appState,
isMobile,
device,
app,
}: HintViewerProps): null | string | string[] => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
@@ -85,7 +92,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
if (activeTool.type === "selection") {
if (
appState.draggingElement?.type === "selection" &&
appState.selectionElement &&
!selectedElements.length &&
!appState.editingElement &&
!appState.editingLinearElement
@@ -93,7 +100,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
return t("hints.deepBoxSelect");
}
if (appState.gridSize && appState.draggingElement) {
if (appState.gridSize && appState.selectedElementsAreBeingDragged) {
return t("hints.disableSnapping");
}
@@ -111,9 +118,23 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
return t("hints.lineEditor_info");
}
if (
!appState.draggingElement &&
!appState.newElement &&
!appState.selectedElementsAreBeingDragged &&
isTextBindableContainer(selectedElements[0])
) {
if (isFlowchartNodeElement(selectedElements[0])) {
if (
isNodeInFlowchart(
selectedElements[0],
app.scene.getNonDeletedElementsMap(),
)
) {
return [t("hints.bindTextToElement"), t("hints.createFlowchart")];
}
return [t("hints.bindTextToElement"), t("hints.createFlowchart")];
}
return t("hints.bindTextToElement");
}
}
@@ -128,17 +149,24 @@ export const HintViewer = ({
device,
app,
}: HintViewerProps) => {
let hint = getHints({
const hints = getHints({
appState,
isMobile,
device,
app,
});
if (!hint) {
if (!hints) {
return null;
}
hint = getShortcutKey(hint);
const hint = Array.isArray(hints)
? hints
.map((hint) => {
return getShortcutKey(hint).replace(/\. ?$/, "");
})
.join(". ")
: getShortcutKey(hints);
return (
<div className="HintViewer">
@@ -2,8 +2,8 @@ import React from "react";
import { DEFAULT_SIDEBAR } from "../../constants";
import { Excalidraw, Sidebar } from "../../index";
import {
act,
fireEvent,
GlobalTestState,
queryAllByTestId,
queryByTestId,
render,
@@ -11,39 +11,17 @@ import {
withExcalidrawDimensions,
} from "../../tests/test-utils";
import { vi } from "vitest";
import {
assertExcalidrawWithSidebar,
assertSidebarDockButton,
} from "./siderbar.test.helpers";
export const assertSidebarDockButton = async <T extends boolean>(
hasDockButton: T,
): Promise<
T extends false
? { dockButton: null; sidebar: HTMLElement }
: { dockButton: HTMLElement; sidebar: HTMLElement }
> => {
const sidebar =
GlobalTestState.renderResult.container.querySelector<HTMLElement>(
".sidebar",
);
expect(sidebar).not.toBe(null);
const dockButton = queryByTestId(sidebar!, "sidebar-dock");
if (hasDockButton) {
expect(dockButton).not.toBe(null);
return { dockButton: dockButton!, sidebar: sidebar! } as any;
}
expect(dockButton).toBe(null);
return { dockButton: null, sidebar: sidebar! } as any;
};
export const assertExcalidrawWithSidebar = async (
sidebar: React.ReactNode,
name: string,
test: () => void,
) => {
await render(
<Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
{sidebar}
</Excalidraw>,
);
await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
const toggleSidebar = (
...args: Parameters<typeof window.h.app.toggleSidebar>
): Promise<boolean> => {
return act(() => {
return window.h.app.toggleSidebar(...args);
});
};
describe("Sidebar", () => {
@@ -103,7 +81,7 @@ describe("Sidebar", () => {
// toggle sidebar on
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
expect(await toggleSidebar({ name: "customSidebar" })).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
@@ -112,7 +90,7 @@ describe("Sidebar", () => {
// toggle sidebar off
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(false);
expect(await toggleSidebar({ name: "customSidebar" })).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
@@ -121,9 +99,9 @@ describe("Sidebar", () => {
// force-toggle sidebar off (=> still hidden)
// -------------------------------------------------------------------------
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: false }),
).toBe(false);
expect(await toggleSidebar({ name: "customSidebar", force: false })).toBe(
false,
);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
@@ -132,12 +110,12 @@ describe("Sidebar", () => {
// force-toggle sidebar on
// -------------------------------------------------------------------------
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
).toBe(true);
expect(
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
).toBe(true);
expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe(
true,
);
expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe(
true,
);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
@@ -146,9 +124,7 @@ describe("Sidebar", () => {
// toggle library (= hide custom sidebar)
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(
true,
);
expect(await toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
@@ -161,13 +137,13 @@ describe("Sidebar", () => {
// closing sidebar using `{ name: null }`
// -------------------------------------------------------------------------
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
expect(await toggleSidebar({ name: "customSidebar" })).toBe(true);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).not.toBe(null);
});
expect(window.h.app.toggleSidebar({ name: null })).toBe(false);
expect(await toggleSidebar({ name: null })).toBe(false);
await waitFor(() => {
const node = container.querySelector("#test-sidebar-content");
expect(node).toBe(null);
@@ -321,6 +297,9 @@ describe("Sidebar", () => {
});
it("shouldn't be user-dockable when only `onDock` supplied w/o `docked`", async () => {
// we expect warnings in this test and don't want to pollute stdout
const mock = jest.spyOn(console, "warn").mockImplementation(() => {});
await render(
<Excalidraw
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
@@ -341,6 +320,8 @@ describe("Sidebar", () => {
await assertSidebarDockButton(false);
},
);
mock.mockRestore();
});
});
@@ -367,9 +348,9 @@ describe("Sidebar", () => {
).toBeNull();
// open library sidebar
expect(
window.h.app.toggleSidebar({ name: "custom", tab: "library" }),
).toBe(true);
expect(await toggleSidebar({ name: "custom", tab: "library" })).toBe(
true,
);
expect(
container.querySelector<HTMLElement>(
"[role=tabpanel][data-testid=library]",
@@ -377,9 +358,9 @@ describe("Sidebar", () => {
).not.toBeNull();
// switch to comments tab
expect(
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
).toBe(true);
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
true,
);
expect(
container.querySelector<HTMLElement>(
"[role=tabpanel][data-testid=comments]",
@@ -387,9 +368,9 @@ describe("Sidebar", () => {
).not.toBeNull();
// toggle sidebar closed
expect(
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
).toBe(false);
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
false,
);
expect(
container.querySelector<HTMLElement>(
"[role=tabpanel][data-testid=comments]",
@@ -397,9 +378,9 @@ describe("Sidebar", () => {
).toBeNull();
// toggle sidebar open
expect(
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
).toBe(true);
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
true,
);
expect(
container.querySelector<HTMLElement>(
"[role=tabpanel][data-testid=comments]",
@@ -0,0 +1,42 @@
import React from "react";
import { Excalidraw } from "../..";
import {
GlobalTestState,
queryByTestId,
render,
withExcalidrawDimensions,
} from "../../tests/test-utils";
export const assertSidebarDockButton = async <T extends boolean>(
hasDockButton: T,
): Promise<
T extends false
? { dockButton: null; sidebar: HTMLElement }
: { dockButton: HTMLElement; sidebar: HTMLElement }
> => {
const sidebar =
GlobalTestState.renderResult.container.querySelector<HTMLElement>(
".sidebar",
);
expect(sidebar).not.toBe(null);
const dockButton = queryByTestId(sidebar!, "sidebar-dock");
if (hasDockButton) {
expect(dockButton).not.toBe(null);
return { dockButton: dockButton!, sidebar: sidebar! } as any;
}
expect(dockButton).toBe(null);
return { dockButton: null, sidebar: sidebar! } as any;
};
export const assertExcalidrawWithSidebar = async (
sidebar: React.ReactNode,
name: string,
test: () => void,
) => {
await render(
<Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
{sidebar}
</Excalidraw>,
);
await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
};
@@ -23,7 +23,6 @@ const handleDimensionChange: DragInputCallbackType<
> = ({
accumulatedChange,
originalElements,
originalElementsMap,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
@@ -66,7 +66,6 @@ const resizeElementInGroup = (
origElement: ExcalidrawElement,
elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap,
scene: Scene,
) => {
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
const { width: oldWidth, height: oldHeight } = latestElement;
@@ -78,7 +77,7 @@ const resizeElementInGroup = (
);
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale;
updateBoundElements(latestElement, elementsMap, scene, {
updateBoundElements(latestElement, elementsMap, {
oldSize: { width: oldWidth, height: oldHeight },
});
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
@@ -111,7 +110,6 @@ const resizeGroup = (
originalElements: ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
originalElementsMap: ElementsMap,
scene: Scene,
) => {
// keep aspect ratio for groups
if (property === "width") {
@@ -135,7 +133,6 @@ const resizeGroup = (
origElement,
elementsMap,
originalElementsMap,
scene,
);
}
};
@@ -190,7 +187,6 @@ const handleDimensionChange: DragInputCallbackType<
originalElements,
elementsMap,
originalElementsMap,
scene,
);
} else {
const [el] = elementsInUnit;
@@ -296,7 +292,6 @@ const handleDimensionChange: DragInputCallbackType<
originalElements,
elementsMap,
originalElementsMap,
scene,
);
} else {
const [el] = elementsInUnit;
@@ -1,4 +1,5 @@
import { fireEvent, queryByTestId } from "@testing-library/react";
import React from "react";
import { act, fireEvent, queryByTestId } from "@testing-library/react";
import { Keyboard, Pointer, UI } from "../../tests/helpers/ui";
import { getStepSizedValue } from "./utils";
import {
@@ -24,7 +25,6 @@ import { getCommonBounds, isTextElement } from "../../element";
import { API } from "../../tests/helpers/api";
import { actionGroup } from "../../actions";
import { isInGroup } from "../../groups";
import React from "react";
const { h } = window;
const mouse = new Pointer("mouse");
@@ -32,12 +32,6 @@ const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
let stats: HTMLElement | null = null;
let elementStats: HTMLElement | null | undefined = null;
const editInput = (input: HTMLInputElement, value: string) => {
input.focus();
fireEvent.change(input, { target: { value } });
input.blur();
};
const getStatsProperty = (label: string) => {
const elementStats = UI.queryStats()?.querySelector("#elementStats");
@@ -65,7 +59,7 @@ const testInputProperty = (
) as HTMLInputElement;
expect(input).toBeDefined();
expect(input.value).toBe(initialValue.toString());
editInput(input, String(nextValue));
UI.updateInput(input, String(nextValue));
if (property === "angle") {
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
} else if (property === "fontSize" && isTextElement(element)) {
@@ -110,7 +104,7 @@ describe("binding with linear elements", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
button: 2,
@@ -148,7 +142,7 @@ describe("binding with linear elements", () => {
expect(linear.startBinding).not.toBe(null);
expect(inputX).not.toBeNull();
editInput(inputX, String("204"));
UI.updateInput(inputX, String("204"));
expect(linear.startBinding).not.toBe(null);
});
@@ -159,7 +153,7 @@ describe("binding with linear elements", () => {
) as HTMLInputElement;
expect(linear.startBinding).not.toBe(null);
editInput(inputAngle, String("1"));
UI.updateInput(inputAngle, String("1"));
expect(linear.startBinding).not.toBe(null);
});
@@ -171,7 +165,7 @@ describe("binding with linear elements", () => {
expect(linear.startBinding).not.toBe(null);
expect(inputX).not.toBeNull();
editInput(inputX, String("254"));
UI.updateInput(inputX, String("254"));
expect(linear.startBinding).toBe(null);
});
@@ -182,7 +176,7 @@ describe("binding with linear elements", () => {
) as HTMLInputElement;
expect(linear.startBinding).not.toBe(null);
editInput(inputAngle, String("45"));
UI.updateInput(inputAngle, String("45"));
expect(linear.startBinding).toBe(null);
});
});
@@ -197,7 +191,7 @@ describe("stats for a generic element", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
button: 2,
@@ -268,13 +262,13 @@ describe("stats for a generic element", () => {
) as HTMLInputElement;
expect(input).toBeDefined();
expect(input.value).toBe(rectangle.width.toString());
editInput(input, "123.123");
UI.updateInput(input, "123.123");
expect(h.elements.length).toBe(1);
expect(rectangle.id).toBe(rectangleId);
expect(input.value).toBe("123.12");
expect(rectangle.width).toBe(123.12);
editInput(input, "88.98766");
UI.updateInput(input, "88.98766");
expect(input.value).toBe("88.99");
expect(rectangle.width).toBe(88.99);
});
@@ -387,7 +381,7 @@ describe("stats for a non-generic element", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
button: 2,
@@ -412,9 +406,10 @@ describe("stats for a non-generic element", () => {
mouse.clickAt(20, 30);
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello!");
editor.blur();
act(() => {
editor.blur();
});
const text = h.elements[0] as ExcalidrawTextElement;
mouse.clickOn(text);
@@ -427,7 +422,7 @@ describe("stats for a non-generic element", () => {
) as HTMLInputElement;
expect(input).toBeDefined();
expect(input.value).toBe(text.fontSize.toString());
editInput(input, "36");
UI.updateInput(input, "36");
expect(text.fontSize).toBe(36);
// cannot change width or height
@@ -437,7 +432,7 @@ describe("stats for a non-generic element", () => {
expect(height).toBeUndefined();
// min font size is 4
editInput(input, "0");
UI.updateInput(input, "0");
expect(text.fontSize).not.toBe(0);
expect(text.fontSize).toBe(4);
});
@@ -449,8 +444,8 @@ describe("stats for a non-generic element", () => {
x: 150,
width: 150,
});
h.elements = [frame];
h.setState({
API.setElements([frame]);
API.setAppState({
selectedElementIds: {
[frame.id]: true,
},
@@ -471,9 +466,9 @@ describe("stats for a non-generic element", () => {
it("image element", () => {
const image = API.createElement({ type: "image", width: 200, height: 100 });
h.elements = [image];
API.setElements([image]);
mouse.clickOn(image);
h.setState({
API.setAppState({
selectedElementIds: {
[image.id]: true,
},
@@ -508,7 +503,7 @@ describe("stats for a non-generic element", () => {
mutateElement(container, {
boundElements: [{ type: "text", id: text.id }],
});
h.elements = [container, text];
API.setElements([container, text]);
API.setSelectedElements([container]);
const fontSize = getStatsProperty("F")?.querySelector(
@@ -516,7 +511,7 @@ describe("stats for a non-generic element", () => {
) as HTMLInputElement;
expect(fontSize).toBeDefined();
editInput(fontSize, "40");
UI.updateInput(fontSize, "40");
expect(text.fontSize).toBe(40);
});
@@ -533,7 +528,7 @@ describe("stats for multiple elements", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
button: 2,
@@ -566,7 +561,7 @@ describe("stats for multiple elements", () => {
mouse.down(-100, -100);
mouse.up(125, 145);
h.setState({
API.setAppState({
selectedElementIds: h.elements.reduce((acc, el) => {
acc[el.id] = true;
return acc;
@@ -588,12 +583,12 @@ describe("stats for multiple elements", () => {
) as HTMLInputElement;
expect(angle.value).toBe("0");
editInput(width, "250");
UI.updateInput(width, "250");
h.elements.forEach((el) => {
expect(el.width).toBe(250);
});
editInput(height, "450");
UI.updateInput(height, "450");
h.elements.forEach((el) => {
expect(el.height).toBe(450);
});
@@ -605,9 +600,10 @@ describe("stats for multiple elements", () => {
mouse.clickAt(20, 30);
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello!");
editor.blur();
act(() => {
editor.blur();
});
UI.clickTool("rectangle");
mouse.down();
@@ -619,12 +615,12 @@ describe("stats for multiple elements", () => {
width: 150,
});
h.elements = [...h.elements, frame];
API.setElements([...h.elements, frame]);
const text = h.elements.find((el) => el.type === "text");
const rectangle = h.elements.find((el) => el.type === "rectangle");
h.setState({
API.setAppState({
selectedElementIds: h.elements.reduce((acc, el) => {
acc[el.id] = true;
return acc;
@@ -657,13 +653,13 @@ describe("stats for multiple elements", () => {
expect(fontSize).toBeDefined();
// changing width does not affect text
editInput(width, "200");
UI.updateInput(width, "200");
expect(rectangle?.width).toBe(200);
expect(frame.width).toBe(200);
expect(text?.width).not.toBe(200);
editInput(angle, "40");
UI.updateInput(angle, "40");
const angleInRadian = degreeToRadian(40);
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
@@ -686,7 +682,7 @@ describe("stats for multiple elements", () => {
mouse.click();
});
h.app.actionManager.executeAction(actionGroup);
API.executeAction(actionGroup);
};
createAndSelectGroup();
@@ -703,7 +699,7 @@ describe("stats for multiple elements", () => {
expect(x).toBeDefined();
expect(Number(x.value)).toBe(x1);
editInput(x, "300");
UI.updateInput(x, "300");
expect(h.elements[0].x).toBe(300);
expect(h.elements[1].x).toBe(400);
@@ -716,7 +712,7 @@ describe("stats for multiple elements", () => {
expect(y).toBeDefined();
expect(Number(y.value)).toBe(y1);
editInput(y, "200");
UI.updateInput(y, "200");
expect(h.elements[0].y).toBe(200);
expect(h.elements[1].y).toBe(300);
@@ -734,20 +730,20 @@ describe("stats for multiple elements", () => {
expect(height).toBeDefined();
expect(Number(height.value)).toBe(200);
editInput(width, "400");
UI.updateInput(width, "400");
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
let newGroupWidth = x2 - x1;
expect(newGroupWidth).toBeCloseTo(400, 4);
editInput(width, "300");
UI.updateInput(width, "300");
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
newGroupWidth = x2 - x1;
expect(newGroupWidth).toBeCloseTo(300, 4);
editInput(height, "500");
UI.updateInput(height, "500");
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
const newGroupHeight = y2 - y1;
@@ -198,7 +198,7 @@ export const resizeElement = (
}
}
updateBoundElements(latestElement, elementsMap, scene, {
updateBoundElements(latestElement, elementsMap, {
oldSize: { width: oldWidth, height: oldHeight },
});
@@ -316,6 +316,6 @@ export const updateBindings = (
[],
);
} else {
updateBoundElements(latestElement, elementsMap, scene, options);
updateBoundElements(latestElement, elementsMap, options);
}
};
@@ -1,7 +1,13 @@
import React from "react";
import { Excalidraw } from "../../index";
import { KEYS } from "../../keys";
import { Keyboard } from "../../tests/helpers/ui";
import { render, waitFor, getByTestId } from "../../tests/test-utils";
import {
render,
waitFor,
getByTestId,
fireEvent,
} from "../../tests/test-utils";
describe("Test <DropdownMenu/>", () => {
it("should", async () => {
@@ -9,7 +15,7 @@ describe("Test <DropdownMenu/>", () => {
expect(window.h.state.openMenu).toBe(null);
getByTestId(container, "main-menu-trigger").click();
fireEvent.click(getByTestId(container, "main-menu-trigger"));
expect(window.h.state.openMenu).toBe("canvas");
await waitFor(() => {
@@ -1,3 +1,4 @@
import React from "react";
import { render, queryAllByTestId } from "../../tests/test-utils";
import { Excalidraw, MainMenu } from "../../index";
@@ -211,7 +211,7 @@ export const Hyperlink = ({
const { x, y } = getCoordsForPopover(element, appState, elementsMap);
if (
appState.contextMenu ||
appState.draggingElement ||
appState.selectedElementsAreBeingDragged ||
appState.resizingElement ||
appState.isRotating ||
appState.openMenu ||
+1 -1
View File
@@ -26,7 +26,7 @@ const shouldDiscardRemoteElement = (
// local element is being edited
(local.id === localAppState.editingElement?.id ||
local.id === localAppState.resizingElement?.id ||
local.id === localAppState.draggingElement?.id || // TODO: Is this still valid? As draggingElement is selection element, which is never part of the elements array
local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
// local element is newer
local.version > remote.version ||
// resolve conflicting edits deterministically by taking the one with
+2 -1
View File
@@ -51,6 +51,7 @@ import { normalizeLink } from "./url";
import { syncInvalidIndices } from "../fractionalIndex";
import { getSizeFromPoints } from "../points";
import { getLineHeight } from "../fonts";
import { normalizeFixedPoint } from "../element/binding";
type RestoredAppState = Omit<
AppState,
@@ -106,7 +107,7 @@ const repairBinding = (
...binding,
focus: binding.focus || 0,
fixedPoint: isElbowArrow(element)
? binding.fixedPoint ?? ([0, 0] as [number, number])
? normalizeFixedPoint(binding.fixedPoint ?? [0, 0])
: null,
};
};
@@ -36,7 +36,7 @@ export const ElementCanvasButtons = ({
if (
appState.contextMenu ||
appState.draggingElement ||
appState.newElement ||
appState.resizingElement ||
appState.isRotating ||
appState.openMenu ||
+47 -22
View File
@@ -25,6 +25,7 @@ import type {
OrderedExcalidrawElement,
ExcalidrawElbowArrowElement,
FixedPoint,
SceneElementsMap,
} from "./types";
import type { Bounds } from "./bounds";
@@ -40,6 +41,7 @@ import {
isElbowArrow,
isFrameLikeElement,
isLinearElement,
isRectangularElement,
isTextElement,
} from "./typeChecks";
import type { ElementUpdate } from "./mutateElement";
@@ -124,7 +126,6 @@ export const bindOrUnbindLinearElement = (
boundToElementIds,
unboundFromElementIds,
elementsMap,
scene,
);
bindOrUnbindLinearElementEdge(
linearElement,
@@ -134,7 +135,6 @@ export const bindOrUnbindLinearElement = (
boundToElementIds,
unboundFromElementIds,
elementsMap,
scene,
);
const onlyUnbound = Array.from(unboundFromElementIds).filter(
@@ -161,7 +161,6 @@ const bindOrUnbindLinearElementEdge = (
// Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap,
scene: Scene,
): void => {
// "keep" is for method chaining convenience, a "no-op", so just bail out
if (bindableElement === "keep") {
@@ -571,8 +570,7 @@ const calculateFocusAndGap = (
// in explicitly.
export const updateBoundElements = (
changedElement: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[];
oldSize?: { width: number; height: number };
@@ -658,7 +656,7 @@ export const updateBoundElements = (
LinearElementEditor.movePoints(
element,
updates,
scene,
elementsMap,
{
...(changedElement.id === element.startBinding?.elementId
? { startBinding: bindings.startBinding }
@@ -754,7 +752,7 @@ export const bindPointToSnapToElementOutline = (
const aabb = bindableElement && aabbForElement(bindableElement);
if (bindableElement && aabb) {
// TODO: Dirty hack until tangents are properly calculated
// TODO: Dirty hacks until tangents are properly calculated
const intersections = [
...intersectElementWithLine(
bindableElement,
@@ -762,20 +760,33 @@ export const bindPointToSnapToElementOutline = (
[point[0], point[1] + 2 * bindableElement.height],
FIXED_BINDING_DISTANCE,
elementsMap,
),
).map((i) => {
if (!isRectangularElement(bindableElement)) {
return i;
}
const d = distanceToBindableElement(bindableElement, i, elementsMap);
return d >= bindableElement.height / 2 || d < FIXED_BINDING_DISTANCE
? ([point[0], -1 * i[1]] as Point)
: ([point[0], i[1]] as Point);
}),
...intersectElementWithLine(
bindableElement,
[point[0] - 2 * bindableElement.width, point[1]],
[point[0] + 2 * bindableElement.width, point[1]],
FIXED_BINDING_DISTANCE,
elementsMap,
),
].map((i) =>
distanceToBindableElement(bindableElement, i, elementsMap) >
Math.min(bindableElement.width, bindableElement.height) / 2
? ([-1 * i[0], -1 * i[1]] as Point)
: i,
);
).map((i) => {
if (!isRectangularElement(bindableElement)) {
return i;
}
const d = distanceToBindableElement(bindableElement, i, elementsMap);
return d >= bindableElement.width / 2 || d < FIXED_BINDING_DISTANCE
? ([-1 * i[0], point[1]] as Point)
: ([i[0], point[1]] as Point);
}),
];
const heading = headingForPointFromElement(bindableElement, aabb, point);
const isVertical =
@@ -1003,7 +1014,7 @@ const updateBoundPoint = (
if (isElbowArrow(linearElement)) {
const fixedPoint =
binding.fixedPoint ??
normalizeFixedPoint(binding.fixedPoint) ??
calculateFixedPointForElbowArrowBinding(
linearElement,
bindableElement,
@@ -1116,12 +1127,12 @@ export const calculateFixedPointForElbowArrowBinding = (
) as Point;
return {
fixedPoint: [
fixedPoint: normalizeFixedPoint([
(nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) /
hoveredElement.width,
(nonRotatedSnappedGlobalPoint[1] - hoveredElement.y) /
hoveredElement.height,
] as [number, number],
]),
};
};
@@ -1246,11 +1257,11 @@ export const fixBindingsAfterDuplication = (
.filter(({ id }) => allBindableElementIds.has(id))
.forEach((bindableElement) => {
const oldElementId = duplicateIdToOldId.get(bindableElement.id);
const { boundElements } = sceneElements.find(
const boundElements = sceneElements.find(
({ id }) => id === oldElementId,
)!;
)?.boundElements;
if (boundElements != null && boundElements.length > 0) {
if (boundElements && boundElements.length > 0) {
mutateElement(bindableElement, {
boundElements: boundElements.map((boundElement) =>
oldIdToDuplicatedId.has(boundElement.id)
@@ -2174,7 +2185,8 @@ export const getGlobalFixedPointForBindableElement = (
fixedPointRatio: [number, number],
element: ExcalidrawBindableElement,
) => {
const [fixedX, fixedY] = fixedPointRatio;
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
return rotatePoint(
[element.x + element.width * fixedX, element.y + element.height * fixedY],
getCenterForElement(element),
@@ -2228,3 +2240,16 @@ export const getArrowLocalFixedPoints = (
LinearElementEditor.pointFromAbsoluteCoords(arrow, endPoint, elementsMap),
];
};
export const normalizeFixedPoint = <T extends FixedPoint | null>(
fixedPoint: T,
): T extends null ? null : FixedPoint => {
// Do not allow a precise 0.5 for fixed point ratio
// to avoid jumping arrow heading due to floating point imprecision
if (fixedPoint && (fixedPoint[0] === 0.5 || fixedPoint[1] === 0.5)) {
return fixedPoint.map((ratio) =>
ratio === 0.5 ? 0.5001 : ratio,
) as T extends null ? null : FixedPoint;
}
return fixedPoint as any as T extends null ? null : FixedPoint;
};
+11 -19
View File
@@ -91,14 +91,9 @@ export const dragSelectedElements = (
updateElementCoords(pointerDownState, textElement, adjustedOffset);
}
}
updateBoundElements(
element,
scene.getElementsMapIncludingDeleted(),
scene,
{
simultaneouslyUpdated: Array.from(elementsToUpdate),
},
);
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
simultaneouslyUpdated: Array.from(elementsToUpdate),
});
});
};
@@ -160,7 +155,7 @@ export const getDragOffsetXY = (
};
export const dragNewElement = (
draggingElement: NonDeletedExcalidrawElement,
newElement: NonDeletedExcalidrawElement,
elementType: AppState["activeTool"]["type"],
originX: number,
originY: number,
@@ -179,7 +174,7 @@ export const dragNewElement = (
y: number;
} | null = null,
) => {
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
if (shouldMaintainAspectRatio && newElement.type !== "selection") {
if (widthAspectRatio) {
height = width / widthAspectRatio;
} else {
@@ -218,17 +213,14 @@ export const dragNewElement = (
let textAutoResize = null;
// NOTE this should apply only to creating text elements, not existing
// (once we rewrite appState.draggingElement to actually mean dragging
// elements)
if (isTextElement(draggingElement)) {
height = draggingElement.height;
if (isTextElement(newElement)) {
height = newElement.height;
const minWidth = getMinTextElementWidth(
getFontString({
fontSize: draggingElement.fontSize,
fontFamily: draggingElement.fontFamily,
fontSize: newElement.fontSize,
fontFamily: newElement.fontFamily,
}),
draggingElement.lineHeight,
newElement.lineHeight,
);
width = Math.max(width, minWidth);
@@ -245,7 +237,7 @@ export const dragNewElement = (
}
if (width !== 0 && height !== 0) {
mutateElement(draggingElement, {
mutateElement(newElement, {
x: newX + (originOffset?.x ?? 0),
y: newY + (originOffset?.y ?? 0),
width,
@@ -0,0 +1,404 @@
import ReactDOM from "react-dom";
import { render } from "../tests/test-utils";
import { reseed } from "../random";
import { UI, Keyboard, Pointer } from "../tests/helpers/ui";
import { Excalidraw } from "../index";
import { API } from "../tests/helpers/api";
import { KEYS } from "../keys";
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const { h } = window;
const mouse = new Pointer("mouse");
beforeEach(async () => {
localStorage.clear();
reseed(7);
mouse.reset();
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.state.width = 1000;
h.state.height = 1000;
// The bounds of hand-drawn linear elements may change after flipping, so
// removing this style for testing
UI.clickTool("arrow");
UI.clickByTitle("Architect");
UI.clickTool("selection");
});
describe("flow chart creation", () => {
beforeEach(() => {
API.clearSelection();
const rectangle = API.createElement({
type: "rectangle",
width: 200,
height: 100,
});
API.setElements([rectangle]);
API.setSelectedElements([rectangle]);
});
// multiple at once
it("create multiple successor nodes at once", () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.length).toBe(5);
expect(h.elements.filter((el) => el.type === "rectangle").length).toBe(3);
expect(h.elements.filter((el) => el.type === "arrow").length).toBe(2);
});
it("when directions are changed, only the last same directions will apply", () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_UP);
Keyboard.keyPress(KEYS.ARROW_UP);
Keyboard.keyPress(KEYS.ARROW_UP);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.length).toBe(7);
expect(h.elements.filter((el) => el.type === "rectangle").length).toBe(4);
expect(h.elements.filter((el) => el.type === "arrow").length).toBe(3);
});
it("when escaped, no nodes will be created", () => {
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_UP);
Keyboard.keyPress(KEYS.ARROW_DOWN);
});
Keyboard.keyPress(KEYS.ESCAPE);
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.length).toBe(1);
});
it("create nodes one at a time", () => {
const initialNode = h.elements[0];
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.length).toBe(3);
expect(h.elements.filter((el) => el.type === "rectangle").length).toBe(2);
expect(h.elements.filter((el) => el.type === "arrow").length).toBe(1);
const firstChildNode = h.elements.filter(
(el) => el.type === "rectangle" && el.id !== initialNode.id,
)[0];
expect(firstChildNode).not.toBe(null);
expect(firstChildNode.id).toBe(Object.keys(h.state.selectedElementIds)[0]);
API.setSelectedElements([initialNode]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.length).toBe(5);
expect(h.elements.filter((el) => el.type === "rectangle").length).toBe(3);
expect(h.elements.filter((el) => el.type === "arrow").length).toBe(2);
const secondChildNode = h.elements.filter(
(el) =>
el.type === "rectangle" &&
el.id !== initialNode.id &&
el.id !== firstChildNode.id,
)[0];
expect(secondChildNode).not.toBe(null);
expect(secondChildNode.id).toBe(Object.keys(h.state.selectedElementIds)[0]);
API.setSelectedElements([initialNode]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.length).toBe(7);
expect(h.elements.filter((el) => el.type === "rectangle").length).toBe(4);
expect(h.elements.filter((el) => el.type === "arrow").length).toBe(3);
const thirdChildNode = h.elements.filter(
(el) =>
el.type === "rectangle" &&
el.id !== initialNode.id &&
el.id !== firstChildNode.id &&
el.id !== secondChildNode.id,
)[0];
expect(thirdChildNode).not.toBe(null);
expect(thirdChildNode.id).toBe(Object.keys(h.state.selectedElementIds)[0]);
expect(firstChildNode.x).toBe(secondChildNode.x);
expect(secondChildNode.x).toBe(thirdChildNode.x);
});
});
describe("flow chart navigation", () => {
it("single node at each level", () => {
/**
* -> -> -> ->
*/
API.clearSelection();
const rectangle = API.createElement({
type: "rectangle",
width: 200,
height: 100,
});
API.setElements([rectangle]);
API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
expect(h.elements.filter((el) => el.type === "rectangle").length).toBe(5);
expect(h.elements.filter((el) => el.type === "arrow").length).toBe(4);
// all the way to the left, gets us to the first node
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rectangle.id]).toBe(true);
// all the way to the right, gets us to the last node
const rightMostNode = h.elements[h.elements.length - 2];
expect(rightMostNode);
expect(rightMostNode.type).toBe("rectangle");
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rightMostNode.id]).toBe(true);
});
it("multiple nodes at each level", () => {
/**
* from the perspective of the first node, there're four layers, and
* there are four nodes at the second layer
*
* ->
* -> -> -> ->
* ->
* ->
*/
API.clearSelection();
const rectangle = API.createElement({
type: "rectangle",
width: 200,
height: 100,
});
API.setElements([rectangle]);
API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
const secondNode = h.elements[1];
const rightMostNode = h.elements[h.elements.length - 2];
API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
API.setSelectedElements([rectangle]);
// because of same level cycling,
// going right five times should take us back to the second node again
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[secondNode.id]).toBe(true);
// from the second node, going right three times should take us to the rightmost node
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rightMostNode.id]).toBe(true);
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rectangle.id]).toBe(true);
});
it("take the most obvious link when possible", () => {
/**
*
*
*
*/
API.clearSelection();
const rectangle = API.createElement({
type: "rectangle",
width: 200,
height: 100,
});
API.setElements([rectangle]);
API.setSelectedElements([rectangle]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_DOWN);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_UP);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.CTRL_OR_CMD);
// last node should be the one that's selected
const rightMostNode = h.elements[h.elements.length - 2];
expect(rightMostNode.type).toBe("rectangle");
expect(h.state.selectedElementIds[rightMostNode.id]).toBe(true);
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
Keyboard.keyPress(KEYS.ARROW_LEFT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rectangle.id]).toBe(true);
// going any direction takes us to the predecessor as well
const predecessorToRightMostNode = h.elements[h.elements.length - 4];
expect(predecessorToRightMostNode.type).toBe("rectangle");
API.setSelectedElements([rightMostNode]);
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_RIGHT);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rightMostNode.id]).not.toBe(true);
expect(h.state.selectedElementIds[predecessorToRightMostNode.id]).toBe(
true,
);
API.setSelectedElements([rightMostNode]);
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_UP);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rightMostNode.id]).not.toBe(true);
expect(h.state.selectedElementIds[predecessorToRightMostNode.id]).toBe(
true,
);
API.setSelectedElements([rightMostNode]);
Keyboard.withModifierKeys({ alt: true }, () => {
Keyboard.keyPress(KEYS.ARROW_DOWN);
});
Keyboard.keyUp(KEYS.ALT);
expect(h.state.selectedElementIds[rightMostNode.id]).not.toBe(true);
expect(h.state.selectedElementIds[predecessorToRightMostNode.id]).toBe(
true,
);
});
});
+698
View File
@@ -0,0 +1,698 @@
import {
HEADING_DOWN,
HEADING_LEFT,
HEADING_RIGHT,
HEADING_UP,
compareHeading,
headingForPointFromElement,
type Heading,
} from "./heading";
import { bindLinearElement } from "./binding";
import { LinearElementEditor } from "./linearElementEditor";
import { newArrowElement, newElement } from "./newElement";
import { aabbForElement } from "../math";
import type {
ElementsMap,
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawFlowchartNodeElement,
NonDeletedSceneElementsMap,
OrderedExcalidrawElement,
} from "./types";
import { KEYS } from "../keys";
import type { AppState, PendingExcalidrawElements, Point } from "../types";
import { mutateElement } from "./mutateElement";
import { elementOverlapsWithFrame, elementsAreInFrameBounds } from "../frame";
import {
isBindableElement,
isElbowArrow,
isFrameElement,
isFlowchartNodeElement,
} from "./typeChecks";
import { invariant } from "../utils";
type LinkDirection = "up" | "right" | "down" | "left";
const VERTICAL_OFFSET = 100;
const HORIZONTAL_OFFSET = 100;
export const getLinkDirectionFromKey = (key: string): LinkDirection => {
switch (key) {
case KEYS.ARROW_UP:
return "up";
case KEYS.ARROW_DOWN:
return "down";
case KEYS.ARROW_RIGHT:
return "right";
case KEYS.ARROW_LEFT:
return "left";
default:
return "right";
}
};
const getNodeRelatives = (
type: "predecessors" | "successors",
node: ExcalidrawBindableElement,
elementsMap: ElementsMap,
direction: LinkDirection,
) => {
const items = [...elementsMap.values()].reduce(
(acc: { relative: ExcalidrawBindableElement; heading: Heading }[], el) => {
let oppositeBinding;
if (
isElbowArrow(el) &&
// we want check existence of the opposite binding, in the direction
// we're interested in
(oppositeBinding =
el[type === "predecessors" ? "startBinding" : "endBinding"]) &&
// similarly, we need to filter only arrows bound to target node
el[type === "predecessors" ? "endBinding" : "startBinding"]
?.elementId === node.id
) {
const relative = elementsMap.get(oppositeBinding.elementId);
if (!relative) {
return acc;
}
invariant(
isBindableElement(relative),
"not an ExcalidrawBindableElement",
);
const edgePoint: Point =
type === "predecessors" ? el.points[el.points.length - 1] : [0, 0];
const heading = headingForPointFromElement(node, aabbForElement(node), [
edgePoint[0] + el.x,
edgePoint[1] + el.y,
]);
acc.push({
relative,
heading,
});
}
return acc;
},
[],
);
switch (direction) {
case "up":
return items
.filter((item) => compareHeading(item.heading, HEADING_UP))
.map((item) => item.relative);
case "down":
return items
.filter((item) => compareHeading(item.heading, HEADING_DOWN))
.map((item) => item.relative);
case "right":
return items
.filter((item) => compareHeading(item.heading, HEADING_RIGHT))
.map((item) => item.relative);
case "left":
return items
.filter((item) => compareHeading(item.heading, HEADING_LEFT))
.map((item) => item.relative);
}
};
const getSuccessors = (
node: ExcalidrawBindableElement,
elementsMap: ElementsMap,
direction: LinkDirection,
) => {
return getNodeRelatives("successors", node, elementsMap, direction);
};
export const getPredecessors = (
node: ExcalidrawBindableElement,
elementsMap: ElementsMap,
direction: LinkDirection,
) => {
return getNodeRelatives("predecessors", node, elementsMap, direction);
};
const getOffsets = (
element: ExcalidrawFlowchartNodeElement,
linkedNodes: ExcalidrawElement[],
direction: LinkDirection,
) => {
const _HORIZONTAL_OFFSET = HORIZONTAL_OFFSET + element.width;
// check if vertical space or horizontal space is available first
if (direction === "up" || direction === "down") {
const _VERTICAL_OFFSET = VERTICAL_OFFSET + element.height;
// check vertical space
const minX = element.x;
const maxX = element.x + element.width;
// vertical space is available
if (
linkedNodes.every(
(linkedNode) =>
linkedNode.x + linkedNode.width < minX || linkedNode.x > maxX,
)
) {
return {
x: 0,
y: _VERTICAL_OFFSET * (direction === "up" ? -1 : 1),
};
}
} else if (direction === "right" || direction === "left") {
const minY = element.y;
const maxY = element.y + element.height;
if (
linkedNodes.every(
(linkedNode) =>
linkedNode.y + linkedNode.height < minY || linkedNode.y > maxY,
)
) {
return {
x:
(HORIZONTAL_OFFSET + element.width) * (direction === "left" ? -1 : 1),
y: 0,
};
}
}
if (direction === "up" || direction === "down") {
const _VERTICAL_OFFSET = VERTICAL_OFFSET + element.height;
const y = linkedNodes.length === 0 ? _VERTICAL_OFFSET : _VERTICAL_OFFSET;
const x =
linkedNodes.length === 0
? 0
: (linkedNodes.length + 1) % 2 === 0
? ((linkedNodes.length + 1) / 2) * _HORIZONTAL_OFFSET
: (linkedNodes.length / 2) * _HORIZONTAL_OFFSET * -1;
if (direction === "up") {
return {
x,
y: y * -1,
};
}
return {
x,
y,
};
}
const _VERTICAL_OFFSET = VERTICAL_OFFSET + element.height;
const x =
(linkedNodes.length === 0 ? HORIZONTAL_OFFSET : HORIZONTAL_OFFSET) +
element.width;
const y =
linkedNodes.length === 0
? 0
: (linkedNodes.length + 1) % 2 === 0
? ((linkedNodes.length + 1) / 2) * _VERTICAL_OFFSET
: (linkedNodes.length / 2) * _VERTICAL_OFFSET * -1;
if (direction === "left") {
return {
x: x * -1,
y,
};
}
return {
x,
y,
};
};
const addNewNode = (
element: ExcalidrawFlowchartNodeElement,
elementsMap: ElementsMap,
appState: AppState,
direction: LinkDirection,
) => {
const successors = getSuccessors(element, elementsMap, direction);
const predeccessors = getPredecessors(element, elementsMap, direction);
const offsets = getOffsets(
element,
[...successors, ...predeccessors],
direction,
);
const nextNode = newElement({
type: element.type,
x: element.x + offsets.x,
y: element.y + offsets.y,
// TODO: extract this to a util
width: element.width,
height: element.height,
roundness: element.roundness,
roughness: element.roughness,
backgroundColor: element.backgroundColor,
strokeColor: element.strokeColor,
strokeWidth: element.strokeWidth,
});
invariant(
isFlowchartNodeElement(nextNode),
"not an ExcalidrawFlowchartNodeElement",
);
const bindingArrow = createBindingArrow(
element,
nextNode,
elementsMap,
direction,
appState,
);
return {
nextNode,
bindingArrow,
};
};
export const addNewNodes = (
startNode: ExcalidrawFlowchartNodeElement,
elementsMap: ElementsMap,
appState: AppState,
direction: LinkDirection,
numberOfNodes: number,
) => {
// always start from 0 and distribute evenly
const newNodes: ExcalidrawElement[] = [];
for (let i = 0; i < numberOfNodes; i++) {
let nextX: number;
let nextY: number;
if (direction === "left" || direction === "right") {
const totalHeight =
VERTICAL_OFFSET * (numberOfNodes - 1) +
numberOfNodes * startNode.height;
const startY = startNode.y + startNode.height / 2 - totalHeight / 2;
let offsetX = HORIZONTAL_OFFSET + startNode.width;
if (direction === "left") {
offsetX *= -1;
}
nextX = startNode.x + offsetX;
const offsetY = (VERTICAL_OFFSET + startNode.height) * i;
nextY = startY + offsetY;
} else {
const totalWidth =
HORIZONTAL_OFFSET * (numberOfNodes - 1) +
numberOfNodes * startNode.width;
const startX = startNode.x + startNode.width / 2 - totalWidth / 2;
let offsetY = VERTICAL_OFFSET + startNode.height;
if (direction === "up") {
offsetY *= -1;
}
nextY = startNode.y + offsetY;
const offsetX = (HORIZONTAL_OFFSET + startNode.width) * i;
nextX = startX + offsetX;
}
const nextNode = newElement({
type: startNode.type,
x: nextX,
y: nextY,
// TODO: extract this to a util
width: startNode.width,
height: startNode.height,
roundness: startNode.roundness,
roughness: startNode.roughness,
backgroundColor: startNode.backgroundColor,
strokeColor: startNode.strokeColor,
strokeWidth: startNode.strokeWidth,
});
invariant(
isFlowchartNodeElement(nextNode),
"not an ExcalidrawFlowchartNodeElement",
);
const bindingArrow = createBindingArrow(
startNode,
nextNode,
elementsMap,
direction,
appState,
);
newNodes.push(nextNode);
newNodes.push(bindingArrow);
}
return newNodes;
};
const createBindingArrow = (
startBindingElement: ExcalidrawFlowchartNodeElement,
endBindingElement: ExcalidrawFlowchartNodeElement,
elementsMap: ElementsMap,
direction: LinkDirection,
appState: AppState,
) => {
let startX: number;
let startY: number;
const PADDING = 6;
switch (direction) {
case "up": {
startX = startBindingElement.x + startBindingElement.width / 2;
startY = startBindingElement.y - PADDING;
break;
}
case "down": {
startX = startBindingElement.x + startBindingElement.width / 2;
startY = startBindingElement.y + startBindingElement.height + PADDING;
break;
}
case "right": {
startX = startBindingElement.x + startBindingElement.width + PADDING;
startY = startBindingElement.y + startBindingElement.height / 2;
break;
}
case "left": {
startX = startBindingElement.x - PADDING;
startY = startBindingElement.y + startBindingElement.height / 2;
break;
}
}
let endX: number;
let endY: number;
switch (direction) {
case "up": {
endX = endBindingElement.x + endBindingElement.width / 2 - startX;
endY = endBindingElement.y + endBindingElement.height - startY + PADDING;
break;
}
case "down": {
endX = endBindingElement.x + endBindingElement.width / 2 - startX;
endY = endBindingElement.y - startY - PADDING;
break;
}
case "right": {
endX = endBindingElement.x - startX - PADDING;
endY = endBindingElement.y - startY + endBindingElement.height / 2;
break;
}
case "left": {
endX = endBindingElement.x + endBindingElement.width - startX + PADDING;
endY = endBindingElement.y - startY + endBindingElement.height / 2;
break;
}
}
const bindingArrow = newArrowElement({
type: "arrow",
x: startX,
y: startY,
startArrowhead: appState.currentItemStartArrowhead,
endArrowhead: appState.currentItemEndArrowhead,
strokeColor: appState.currentItemStrokeColor,
strokeStyle: appState.currentItemStrokeStyle,
strokeWidth: appState.currentItemStrokeWidth,
points: [
[0, 0],
[endX, endY],
],
elbowed: true,
});
bindLinearElement(
bindingArrow,
startBindingElement,
"start",
elementsMap as NonDeletedSceneElementsMap,
);
bindLinearElement(
bindingArrow,
endBindingElement,
"end",
elementsMap as NonDeletedSceneElementsMap,
);
const changedElements = new Map<string, OrderedExcalidrawElement>();
changedElements.set(
startBindingElement.id,
startBindingElement as OrderedExcalidrawElement,
);
changedElements.set(
endBindingElement.id,
endBindingElement as OrderedExcalidrawElement,
);
changedElements.set(
bindingArrow.id,
bindingArrow as OrderedExcalidrawElement,
);
LinearElementEditor.movePoints(
bindingArrow,
[
{
index: 1,
point: bindingArrow.points[1],
},
],
elementsMap as NonDeletedSceneElementsMap,
undefined,
{
changedElements,
},
);
return bindingArrow;
};
export class FlowChartNavigator {
isExploring: boolean = false;
// nodes that are ONE link away (successor and predecessor both included)
private sameLevelNodes: ExcalidrawElement[] = [];
private sameLevelIndex: number = 0;
// set it to the opposite of the defalut creation direction
private direction: LinkDirection | null = null;
// for speedier navigation
private visitedNodes: Set<ExcalidrawElement["id"]> = new Set();
clear() {
this.isExploring = false;
this.sameLevelNodes = [];
this.sameLevelIndex = 0;
this.direction = null;
this.visitedNodes.clear();
}
exploreByDirection(
element: ExcalidrawElement,
elementsMap: ElementsMap,
direction: LinkDirection,
): ExcalidrawElement["id"] | null {
if (!isBindableElement(element)) {
return null;
}
// clear if going at a different direction
if (direction !== this.direction) {
this.clear();
}
// add the current node to the visited
if (!this.visitedNodes.has(element.id)) {
this.visitedNodes.add(element.id);
}
/**
* CASE:
* - already started exploring, AND
* - there are multiple nodes at the same level, AND
* - still going at the same direction, AND
*
* RESULT:
* - loop through nodes at the same level
*
* WHY:
* - provides user the capability to loop through nodes at the same level
*/
if (
this.isExploring &&
direction === this.direction &&
this.sameLevelNodes.length > 1
) {
this.sameLevelIndex =
(this.sameLevelIndex + 1) % this.sameLevelNodes.length;
return this.sameLevelNodes[this.sameLevelIndex].id;
}
const nodes = [
...getSuccessors(element, elementsMap, direction),
...getPredecessors(element, elementsMap, direction),
];
/**
* CASE:
* - just started exploring at the given direction
*
* RESULT:
* - go to the first node in the given direction
*/
if (nodes.length > 0) {
this.sameLevelIndex = 0;
this.isExploring = true;
this.sameLevelNodes = nodes;
this.direction = direction;
this.visitedNodes.add(nodes[0].id);
return nodes[0].id;
}
/**
* CASE:
* - (just started exploring or still going at the same direction) OR
* - there're no nodes at the given direction
*
* RESULT:
* - go to some other unvisited linked node
*
* WHY:
* - provide a speedier navigation from a given node to some predecessor
* without the user having to change arrow key
*/
if (direction === this.direction || !this.isExploring) {
if (!this.isExploring) {
// just started and no other nodes at the given direction
// so the current node is technically the first visited node
// (this is needed so that we don't get stuck between looping through )
this.visitedNodes.add(element.id);
}
const otherDirections: LinkDirection[] = [
"up",
"right",
"down",
"left",
].filter((dir): dir is LinkDirection => dir !== direction);
const otherLinkedNodes = otherDirections
.map((dir) => [
...getSuccessors(element, elementsMap, dir),
...getPredecessors(element, elementsMap, dir),
])
.flat()
.filter((linkedNode) => !this.visitedNodes.has(linkedNode.id));
for (const linkedNode of otherLinkedNodes) {
if (!this.visitedNodes.has(linkedNode.id)) {
this.visitedNodes.add(linkedNode.id);
this.isExploring = true;
this.direction = direction;
return linkedNode.id;
}
}
}
return null;
}
}
export class FlowChartCreator {
isCreatingChart: boolean = false;
private numberOfNodes: number = 0;
private direction: LinkDirection | null = "right";
pendingNodes: PendingExcalidrawElements | null = null;
createNodes(
startNode: ExcalidrawFlowchartNodeElement,
elementsMap: ElementsMap,
appState: AppState,
direction: LinkDirection,
) {
if (direction !== this.direction) {
const { nextNode, bindingArrow } = addNewNode(
startNode,
elementsMap,
appState,
direction,
);
this.numberOfNodes = 1;
this.isCreatingChart = true;
this.direction = direction;
this.pendingNodes = [nextNode, bindingArrow];
} else {
this.numberOfNodes += 1;
const newNodes = addNewNodes(
startNode,
elementsMap,
appState,
direction,
this.numberOfNodes,
);
this.isCreatingChart = true;
this.direction = direction;
this.pendingNodes = newNodes;
}
// add pending nodes to the same frame as the start node
// if every pending node is at least intersecting with the frame
if (startNode.frameId) {
const frame = elementsMap.get(startNode.frameId);
invariant(
frame && isFrameElement(frame),
"not an ExcalidrawFrameElement",
);
if (
frame &&
this.pendingNodes.every(
(node) =>
elementsAreInFrameBounds([node], frame, elementsMap) ||
elementOverlapsWithFrame(node, frame, elementsMap),
)
) {
this.pendingNodes = this.pendingNodes.map((node) =>
mutateElement(
node,
{
frameId: startNode.frameId,
},
false,
),
);
}
}
}
clear() {
this.isCreatingChart = false;
this.pendingNodes = null;
this.direction = null;
this.numberOfNodes = 0;
}
}
export const isNodeInFlowchart = (
element: ExcalidrawFlowchartNodeElement,
elementsMap: ElementsMap,
) => {
for (const [, el] of elementsMap) {
if (
el.type === "arrow" &&
(el.startBinding?.elementId === element.id ||
el.endBinding?.elementId === element.id)
) {
return true;
}
}
return false;
};
@@ -9,6 +9,7 @@ import type {
NonDeletedSceneElementsMap,
OrderedExcalidrawElement,
FixedPointBinding,
SceneElementsMap,
} from "./types";
import {
distance2d,
@@ -43,7 +44,7 @@ import {
getHoveredElementForBinding,
isBindingEnabled,
} from "./binding";
import { tupleToCoors } from "../utils";
import { toBrandedType, tupleToCoors } from "../utils";
import {
isBindingElement,
isElbowArrow,
@@ -151,10 +152,7 @@ export class LinearElementEditor {
setState: React.Component<any, AppState>["setState"],
elementsMap: NonDeletedSceneElementsMap,
) {
if (
!appState.editingLinearElement ||
appState.draggingElement?.type !== "selection"
) {
if (!appState.editingLinearElement || !appState.selectionElement) {
return false;
}
const { editingLinearElement } = appState;
@@ -166,7 +164,7 @@ export class LinearElementEditor {
}
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(appState.draggingElement, elementsMap);
getElementAbsoluteCoords(appState.selectionElement, elementsMap);
const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
element,
@@ -293,7 +291,7 @@ export class LinearElementEditor {
isDragging: selectedIndex === lastClickedPoint,
},
],
scene,
elementsMap,
);
} else {
const newDraggingPointPosition = LinearElementEditor.createPointAt(
@@ -329,7 +327,7 @@ export class LinearElementEditor {
isDragging: pointIndex === lastClickedPoint,
};
}),
scene,
elementsMap,
);
}
@@ -423,7 +421,7 @@ export class LinearElementEditor {
: element.points[0],
},
],
scene,
elementsMap,
);
}
@@ -743,7 +741,7 @@ export class LinearElementEditor {
}
if (event.altKey && appState.editingLinearElement) {
if (
linearElementEditor.lastUncommittedPoint == null ||
linearElementEditor.lastUncommittedPoint === null &&
!isElbowArrow(element)
) {
mutateElement(element, {
@@ -879,13 +877,12 @@ export class LinearElementEditor {
scenePointerX: number,
scenePointerY: number,
appState: AppState,
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
): LinearElementEditor | null {
if (!appState.editingLinearElement) {
return null;
}
const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return appState.editingLinearElement;
@@ -896,7 +893,11 @@ export class LinearElementEditor {
if (!event.altKey) {
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, [points.length - 1], scene);
LinearElementEditor.deletePoints(
element,
[points.length - 1],
elementsMap,
);
}
return {
...appState.editingLinearElement,
@@ -942,14 +943,13 @@ export class LinearElementEditor {
point: newPoint,
},
],
scene,
elementsMap,
);
} else {
LinearElementEditor.addPoints(
element,
appState,
[{ point: newPoint }],
scene,
elementsMap,
);
}
return {
@@ -1094,7 +1094,7 @@ export class LinearElementEditor {
const offsetY = points[0][1];
return {
points: points.map((point, _idx) => {
points: points.map((point) => {
return [point[0] - offsetX, point[1] - offsetY] as const;
}),
x: element.x + offsetX,
@@ -1109,13 +1109,15 @@ export class LinearElementEditor {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
}
static duplicateSelectedPoints(appState: AppState, scene: Scene) {
static duplicateSelectedPoints(
appState: AppState,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) {
if (!appState.editingLinearElement) {
return false;
}
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const elementsMap = scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element || selectedPointsIndices === null) {
@@ -1166,7 +1168,7 @@ export class LinearElementEditor {
point: [lastPoint[0] + 30, lastPoint[1] + 30],
},
],
scene,
elementsMap,
);
}
@@ -1184,7 +1186,7 @@ export class LinearElementEditor {
static deletePoints(
element: NonDeleted<ExcalidrawLinearElement>,
pointIndices: readonly number[],
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) {
let offsetX = 0;
let offsetY = 0;
@@ -1217,15 +1219,14 @@ export class LinearElementEditor {
nextPoints,
offsetX,
offsetY,
scene,
elementsMap,
);
}
static addPoints(
element: NonDeleted<ExcalidrawLinearElement>,
appState: AppState,
targetPoints: { point: Point }[],
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) {
const offsetX = 0;
const offsetY = 0;
@@ -1236,14 +1237,14 @@ export class LinearElementEditor {
nextPoints,
offsetX,
offsetY,
scene,
elementsMap,
);
}
static movePoints(
element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
otherUpdates?: {
startBinding?: PointBinding | null;
endBinding?: PointBinding | null;
@@ -1299,7 +1300,7 @@ export class LinearElementEditor {
nextPoints,
offsetX,
offsetY,
scene,
elementsMap,
otherUpdates,
{
isDragging: targetPoints.reduce(
@@ -1416,7 +1417,7 @@ export class LinearElementEditor {
nextPoints: readonly Point[],
offsetX: number,
offsetY: number,
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
otherUpdates?: {
startBinding?: PointBinding | null;
endBinding?: PointBinding | null;
@@ -1446,9 +1447,15 @@ export class LinearElementEditor {
: null;
}
const mergedElementsMap = options?.changedElements
? toBrandedType<SceneElementsMap>(
new Map([...elementsMap, ...options.changedElements]),
)
: elementsMap;
mutateElbowArrow(
element,
scene,
mergedElementsMap,
nextPoints,
[offsetX, offsetY],
bindings,
+22 -29
View File
@@ -11,6 +11,8 @@ import type {
ExcalidrawTextElementWithContainer,
ExcalidrawImageElement,
ElementsMap,
NonDeletedSceneElementsMap,
SceneElementsMap,
} from "./types";
import type { Mutable } from "../utility-types";
import {
@@ -69,7 +71,7 @@ export const transformElements = (
originalElements: PointerDownState["originalElements"],
transformHandleType: MaybeTransformHandleType,
selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
elementsMap: SceneElementsMap,
shouldRotateWithDiscreteAngle: boolean,
shouldResizeFromCenter: boolean,
shouldMaintainAspectRatio: boolean,
@@ -77,7 +79,6 @@ export const transformElements = (
pointerY: number,
centerX: number,
centerY: number,
scene: Scene,
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
@@ -90,7 +91,7 @@ export const transformElements = (
pointerY,
shouldRotateWithDiscreteAngle,
);
updateBoundElements(element, elementsMap, scene);
updateBoundElements(element, elementsMap);
}
} else if (isTextElement(element) && transformHandleType) {
resizeSingleTextElement(
@@ -102,7 +103,7 @@ export const transformElements = (
pointerX,
pointerY,
);
updateBoundElements(element, elementsMap, scene);
updateBoundElements(element, elementsMap);
} else if (transformHandleType) {
resizeSingleElement(
originalElements,
@@ -113,7 +114,6 @@ export const transformElements = (
shouldResizeFromCenter,
pointerX,
pointerY,
scene,
);
}
@@ -129,7 +129,6 @@ export const transformElements = (
shouldRotateWithDiscreteAngle,
centerX,
centerY,
scene,
);
return true;
} else if (transformHandleType) {
@@ -142,7 +141,6 @@ export const transformElements = (
shouldMaintainAspectRatio,
pointerX,
pointerY,
scene,
);
return true;
}
@@ -434,22 +432,12 @@ export const resizeSingleElement = (
originalElements: PointerDownState["originalElements"],
shouldMaintainAspectRatio: boolean,
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
elementsMap: SceneElementsMap,
transformHandleDirection: TransformHandleDirection,
shouldResizeFromCenter: boolean,
pointerX: number,
pointerY: number,
scene: Scene,
) => {
// Elbow arrows cannot be resized when bound on either end
if (
isArrowElement(element) &&
isElbowArrow(element) &&
(element.startBinding || element.endBinding)
) {
return;
}
const stateAtResizeStart = originalElements.get(element.id)!;
// Gets bounds corners
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
@@ -719,7 +707,7 @@ export const resizeSingleElement = (
) {
mutateElement(element, resizedElement);
updateBoundElements(element, elementsMap, scene, {
updateBoundElements(element, elementsMap, {
oldSize: {
width: stateAtResizeStart.width,
height: stateAtResizeStart.height,
@@ -743,13 +731,12 @@ export const resizeSingleElement = (
export const resizeMultipleElements = (
originalElements: PointerDownState["originalElements"],
selectedElements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
transformHandleType: TransformHandleDirection,
shouldResizeFromCenter: boolean,
shouldMaintainAspectRatio: boolean,
pointerX: number,
pointerY: number,
scene: Scene,
) => {
// map selected elements to the original elements. While it never should
// happen that pointerDownState.originalElements won't contain the selected
@@ -983,12 +970,19 @@ export const resizeMultipleElements = (
mutateElement(element, update, false);
if (isArrowElement(element) && isElbowArrow(element)) {
mutateElbowArrow(element, scene, element.points, undefined, undefined, {
informMutation: false,
});
mutateElbowArrow(
element,
elementsMap,
element.points,
undefined,
undefined,
{
informMutation: false,
},
);
}
updateBoundElements(element, elementsMap, scene, {
updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elementsToUpdate,
oldSize: { width: oldWidth, height: oldHeight },
});
@@ -1013,13 +1007,12 @@ export const resizeMultipleElements = (
const rotateMultipleElements = (
originalElements: PointerDownState["originalElements"],
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
elementsMap: SceneElementsMap,
pointerX: number,
pointerY: number,
shouldRotateWithDiscreteAngle: boolean,
centerX: number,
centerY: number,
scene: Scene,
) => {
let centerAngle =
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
@@ -1046,7 +1039,7 @@ const rotateMultipleElements = (
if (isArrowElement(element) && isElbowArrow(element)) {
const points = getArrowLocalFixedPoints(element, elementsMap);
mutateElbowArrow(element, scene, points);
mutateElbowArrow(element, elementsMap, points);
} else {
mutateElement(
element,
@@ -1059,7 +1052,7 @@ const rotateMultipleElements = (
);
}
updateBoundElements(element, elementsMap, scene, {
updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elements,
});
+3 -9
View File
@@ -22,12 +22,6 @@ const { h } = window;
const mouse = new Pointer("mouse");
const editInput = (input: HTMLInputElement, value: string) => {
input.focus();
fireEvent.change(input, { target: { value } });
input.blur();
};
const getStatsProperty = (label: string) => {
const elementStats = UI.queryStats()?.querySelector("#elementStats");
@@ -51,7 +45,7 @@ describe("elbow arrow routing", () => {
elbowed: true,
}) as ExcalidrawElbowArrowElement;
scene.insertElement(arrow);
mutateElbowArrow(arrow, scene, [
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
[-45 - arrow.x, -100.1 - arrow.y],
[45 - arrow.x, 99.9 - arrow.y],
]);
@@ -104,7 +98,7 @@ describe("elbow arrow routing", () => {
expect(arrow.startBinding).not.toBe(null);
expect(arrow.endBinding).not.toBe(null);
mutateElbowArrow(arrow, scene, [
mutateElbowArrow(arrow, elementsMap, [
[0, 0],
[90, 200],
]);
@@ -202,7 +196,7 @@ describe("elbow arrow ui", () => {
const inputAngle = getStatsProperty("A")?.querySelector(
".drag-input",
) as HTMLInputElement;
editInput(inputAngle, String("40"));
UI.updateInput(inputAngle, String("40"));
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
[0, 0],
+36 -52
View File
@@ -10,7 +10,6 @@ import {
translatePoint,
} from "../math";
import { getSizeFromPoints } from "../points";
import type Scene from "../scene/Scene";
import type { Point } from "../types";
import { isAnyTrue, toBrandedType, tupleToCoors } from "../utils";
import {
@@ -37,14 +36,10 @@ import { isBindableElement, isRectanguloidElement } from "./typeChecks";
import type {
ExcalidrawElbowArrowElement,
FixedPointBinding,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
SceneElementsMap,
} from "./types";
import type {
ElementsMap,
ExcalidrawBindableElement,
OrderedExcalidrawElement,
} from "./types";
import type { ElementsMap, ExcalidrawBindableElement } from "./types";
type Node = {
f: number;
@@ -67,7 +62,7 @@ const BASE_PADDING = 40;
export const mutateElbowArrow = (
arrow: ExcalidrawElbowArrowElement,
scene: Scene,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
nextPoints: readonly Point[],
offset?: Point,
otherUpdates?: {
@@ -75,15 +70,11 @@ export const mutateElbowArrow = (
endBinding?: FixedPointBinding | null;
},
options?: {
changedElements?: Map<string, OrderedExcalidrawElement>;
isDragging?: boolean;
disableBinding?: boolean;
informMutation?: boolean;
},
) => {
const elements = getAllElements(scene, options?.changedElements);
const elementsMap = getAllElementsMap(scene, options?.changedElements);
const origStartGlobalPoint = translatePoint(nextPoints[0], [
arrow.x + (offset ? offset[0] : 0),
arrow.y + (offset ? offset[1] : 0),
@@ -99,22 +90,9 @@ export const mutateElbowArrow = (
const endElement =
arrow.endBinding &&
getBindableElementForId(arrow.endBinding.elementId, elementsMap);
const hoveredStartElement = options?.isDragging
? getHoveredElementForBinding(
tupleToCoors(origStartGlobalPoint),
elements,
elementsMap,
true,
)
: startElement;
const hoveredEndElement = options?.isDragging
? getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint),
elements,
elementsMap,
true,
)
: endElement;
const [hoveredStartElement, hoveredEndElement] = options?.isDragging
? getHoveredElements(origStartGlobalPoint, origEndGlobalPoint, elementsMap)
: [startElement, endElement];
const startGlobalPoint = getGlobalPoint(
arrow.startBinding?.fixedPoint,
origStartGlobalPoint,
@@ -895,7 +873,7 @@ const normalizedArrowElementUpdate = (
const offsetY = global[0][1];
const points = global.map(
(point, _idx) => [point[0] - offsetX, point[1] - offsetY] as const,
(point) => [point[0] - offsetX, point[1] - offsetY] as const,
);
return {
@@ -935,32 +913,11 @@ const neighborIndexToHeading = (idx: number): Heading => {
return HEADING_LEFT;
};
const getAllElementsMap = (
scene: Scene,
changedElements?: Map<string, OrderedExcalidrawElement>,
): NonDeletedSceneElementsMap =>
changedElements
? toBrandedType<NonDeletedSceneElementsMap>(
new Map([...scene.getNonDeletedElementsMap(), ...changedElements]),
)
: scene.getNonDeletedElementsMap();
const getAllElements = (
scene: Scene,
changedElements?: Map<string, OrderedExcalidrawElement>,
): readonly NonDeletedExcalidrawElement[] =>
changedElements
? ([
...scene.getNonDeletedElements(),
...[...changedElements].map(([_, value]) => value),
] as NonDeletedExcalidrawElement[])
: scene.getNonDeletedElements();
const getGlobalPoint = (
fixedPointRatio: [number, number] | undefined | null,
initialPoint: Point,
otherPoint: Point,
elementsMap: NonDeletedSceneElementsMap,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
boundElement?: ExcalidrawBindableElement | null,
hoveredElement?: ExcalidrawBindableElement | null,
isDragging?: boolean,
@@ -1016,7 +973,7 @@ const getSnapPoint = (
const getBindPointHeading = (
point: Point,
otherPoint: Point,
elementsMap: NonDeletedSceneElementsMap,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
hoveredElement: ExcalidrawBindableElement | null | undefined,
origPoint: Point,
) =>
@@ -1034,3 +991,30 @@ const getBindPointHeading = (
elementsMap,
origPoint,
);
const getHoveredElements = (
origStartGlobalPoint: Point,
origEndGlobalPoint: Point,
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
) => {
// TODO: Might be a performance bottleneck and the Map type
// remembers the insertion order anyway...
const nonDeletedSceneElementsMap = toBrandedType<NonDeletedSceneElementsMap>(
new Map([...elementsMap].filter((el) => !el[1].isDeleted)),
);
const elements = Array.from(elementsMap.values());
return [
getHoveredElementForBinding(
tupleToCoors(origStartGlobalPoint),
elements,
nonDeletedSceneElementsMap,
true,
),
getHoveredElementForBinding(
tupleToCoors(origEndGlobalPoint),
elements,
nonDeletedSceneElementsMap,
true,
),
];
};
@@ -55,6 +55,43 @@ export const isElementInViewport = (
);
};
export const isElementCompletelyInViewport = (
element: ExcalidrawElement,
width: number,
height: number,
viewTransformations: {
zoom: Zoom;
offsetLeft: number;
offsetTop: number;
scrollX: number;
scrollY: number;
},
elementsMap: ElementsMap,
) => {
const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft,
clientY: viewTransformations.offsetTop,
},
viewTransformations,
);
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
{
clientX: viewTransformations.offsetLeft + width,
clientY: viewTransformations.offsetTop + height,
},
viewTransformations,
);
return (
x1 >= topLeftSceneCoords.x &&
y1 >= topLeftSceneCoords.y &&
x2 <= bottomRightSceneCoords.x &&
y2 <= bottomRightSceneCoords.y
);
};
/**
* Makes a perfect shape or diagonal/horizontal/vertical line
*/
+121 -179
View File
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import { Excalidraw } from "../index";
import { GlobalTestState, render, screen } from "../tests/test-utils";
@@ -16,7 +17,6 @@ import type {
ExcalidrawTextElementWithContainer,
} from "./types";
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
import { getOriginalContainerHeightFromCache } from "./containerCache";
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
@@ -33,7 +33,7 @@ describe("textWysiwyg", () => {
const { h } = window;
beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
});
it("should prefer editing selected text element (non-bindable container present)", async () => {
@@ -55,7 +55,7 @@ describe("textWysiwyg", () => {
width: textSize,
height: textSize,
});
h.elements = [text, line];
API.setElements([text, line]);
API.setSelectedElements([text]);
@@ -95,9 +95,9 @@ describe("textWysiwyg", () => {
containerId: container.id,
});
h.elements = [container, boundText, boundText2];
API.setElements([container, boundText, boundText2]);
mutateElement(container, {
API.updateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
@@ -123,11 +123,11 @@ describe("textWysiwyg", () => {
height: textSize,
containerId: container.id,
});
mutateElement(container, {
API.updateElement(container, {
boundElements: [{ type: "text", id: text.id }],
});
h.elements = [container, text];
API.setElements([container, text]);
API.setSelectedElements([container]);
@@ -164,9 +164,9 @@ describe("textWysiwyg", () => {
containerId: container.id,
});
h.elements = [container, boundText, boundText2];
API.setElements([container, boundText, boundText2]);
mutateElement(container, {
API.updateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
@@ -187,7 +187,7 @@ describe("textWysiwyg", () => {
height: 100,
});
h.elements = [text];
API.setElements([text]);
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
@@ -209,7 +209,7 @@ describe("textWysiwyg", () => {
height: 100,
});
h.elements = [text];
API.setElements([text]);
UI.clickTool("selection");
mouse.doubleClickAt(text.x + 50, text.y + 50);
@@ -251,7 +251,7 @@ describe("textWysiwyg", () => {
// @ts-ignore
h.app.refreshEditorBreakpoints();
h.elements = [];
API.setElements([]);
});
afterAll(() => {
@@ -264,7 +264,7 @@ describe("textWysiwyg", () => {
text: "Excalidraw\nEditor",
});
h.elements = [text];
API.setElements([text]);
const prevWidth = text.width;
const prevHeight = text.height;
@@ -291,18 +291,15 @@ describe("textWysiwyg", () => {
const nextText = `${wrappedText} is great!`;
updateTextEditor(editor, nextText);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(h.elements[0].width).toEqual(wrappedWidth);
expect(h.elements[0].height).toBeGreaterThan(wrappedHeight);
// remove all texts and then add it back editing
updateTextEditor(editor, "");
await new Promise((cb) => setTimeout(cb, 0));
updateTextEditor(editor, nextText);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(h.elements[0].width).toEqual(wrappedWidth);
});
@@ -313,7 +310,7 @@ describe("textWysiwyg", () => {
type: "text",
text: originalText,
});
h.elements = [text];
API.setElements([text]);
// wrap
UI.resize(text, "e", [-40, 0]);
@@ -321,7 +318,7 @@ describe("textWysiwyg", () => {
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
const editor = await getTextEditor(textEditorSelector);
editor.blur();
Keyboard.exitTextEditor(editor);
// restore after unwrapping
UI.resize(text, "e", [40, 0]);
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
@@ -332,14 +329,12 @@ describe("textWysiwyg", () => {
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
updateTextEditor(editor, `${wrappedText}\nA new line!`);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
// remove the newly added line
UI.clickTool("selection");
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
updateTextEditor(editor, wrappedText);
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
// unwrap
UI.resize(text, "e", [30, 0]);
// expect the text to be restored the same
@@ -376,12 +371,11 @@ describe("textWysiwyg", () => {
});
it("should add a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
// cursor: "|Line#1\nLine#2"
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, { key: KEYS.TAB });
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
// cursor: " |Line#1\nLine#2"
@@ -390,13 +384,12 @@ describe("textWysiwyg", () => {
});
it("should add a tab at the start of the second line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
// cursor: "Line#1\nLin|e#2"
textarea.selectionStart = 10;
textarea.selectionEnd = 10;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, { key: KEYS.TAB });
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
@@ -406,13 +399,12 @@ describe("textWysiwyg", () => {
});
it("should add a tab at the start of the first and second line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2\nLine#3";
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
textarea.selectionStart = 2;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, { key: KEYS.TAB });
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
@@ -422,16 +414,15 @@ describe("textWysiwyg", () => {
});
it("should remove a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
textarea.value = `${tab}Line#1\nLine#2`;
// cursor: "| Line#1\nLine#2"
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, {
key: KEYS.TAB,
shiftKey: true,
});
expect(textarea.value).toEqual(`Line#1\nLine#2`);
@@ -441,16 +432,15 @@ describe("textWysiwyg", () => {
});
it("should remove a tab at the start of the second line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n Lin|e#2"
textarea.value = `Line#1\n${tab}Line#2`;
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, {
key: KEYS.TAB,
shiftKey: true,
});
expect(textarea.value).toEqual(`Line#1\nLine#2`);
// cursor: "Line#1\nLin|e#2"
@@ -459,16 +449,15 @@ describe("textWysiwyg", () => {
});
it("should remove a tab at the start of the first and second line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
textarea.selectionStart = 6;
textarea.selectionEnd = 17;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, {
key: KEYS.TAB,
shiftKey: true,
});
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
@@ -477,45 +466,41 @@ describe("textWysiwyg", () => {
});
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n | Line#2"
textarea.value = `Line#1\n${tab}Line#2`;
textarea.selectionStart = 9;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
// cursor: "Line#1\n|Line#2"
expect(textarea.selectionStart).toEqual(7);
// expect(textarea.selectionEnd).toEqual(7);
});
it("should remove partial tabs", () => {
const event = new KeyboardEvent("keydown", {
fireEvent.keyDown(textarea, {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n|Line#2"
expect(textarea.selectionStart).toEqual(7);
});
it("should remove partial tabs", () => {
// cursor: "Line#1\n Line#|2"
textarea.value = `Line#1\n Line#2`;
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, {
key: KEYS.TAB,
shiftKey: true,
});
expect(textarea.value).toEqual(`Line#1\nLine#2`);
});
it("should remove nothing", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n Li|ne#2"
textarea.value = `Line#1\nLine#2`;
textarea.selectionStart = 9;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
fireEvent.keyDown(textarea, {
key: KEYS.TAB,
shiftKey: true,
});
expect(textarea.value).toEqual(`Line#1\nLine#2`);
});
@@ -523,54 +508,42 @@ describe("textWysiwyg", () => {
it("should resize text via shortcuts while in wysiwyg", () => {
textarea.value = "abc def";
const origFontSize = textElement.fontSize;
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
key: KEYS.CHEVRON_RIGHT,
ctrlKey: true,
shiftKey: true,
}),
);
fireEvent.keyDown(textarea, {
key: KEYS.CHEVRON_RIGHT,
ctrlKey: true,
shiftKey: true,
});
expect(textElement.fontSize).toBe(origFontSize * 1.1);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
key: KEYS.CHEVRON_LEFT,
ctrlKey: true,
shiftKey: true,
}),
);
fireEvent.keyDown(textarea, {
key: KEYS.CHEVRON_LEFT,
ctrlKey: true,
shiftKey: true,
});
expect(textElement.fontSize).toBe(origFontSize);
});
it("zooming via keyboard should zoom canvas", () => {
expect(h.state.zoom.value).toBe(1);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.MINUS,
ctrlKey: true,
}),
);
fireEvent.keyDown(textarea, {
code: CODES.MINUS,
ctrlKey: true,
});
expect(h.state.zoom.value).toBe(0.9);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.NUM_SUBTRACT,
ctrlKey: true,
}),
);
fireEvent.keyDown(textarea, {
code: CODES.NUM_SUBTRACT,
ctrlKey: true,
});
expect(h.state.zoom.value).toBe(0.8);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.NUM_ADD,
ctrlKey: true,
}),
);
fireEvent.keyDown(textarea, {
code: CODES.NUM_ADD,
ctrlKey: true,
});
expect(h.state.zoom.value).toBe(0.9);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.EQUAL,
ctrlKey: true,
}),
);
fireEvent.keyDown(textarea, {
code: CODES.EQUAL,
ctrlKey: true,
});
expect(h.state.zoom.value).toBe(1);
});
@@ -583,8 +556,8 @@ describe("textWysiwyg", () => {
textarea,
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
);
await new Promise((cb) => setTimeout(cb, 0));
textarea.blur();
Keyboard.exitTextEditor(textarea);
expect(textarea.style.width).toBe("792px");
expect(h.elements[0].width).toBe(1000);
});
@@ -596,7 +569,7 @@ describe("textWysiwyg", () => {
beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
rectangle = UI.createElement("rectangle", {
x: 10,
@@ -615,7 +588,7 @@ describe("textWysiwyg", () => {
height: 75,
backgroundColor: "red",
});
h.elements = [rectangle];
API.setElements([rectangle]);
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
@@ -634,8 +607,7 @@ describe("textWysiwyg", () => {
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -648,7 +620,7 @@ describe("textWysiwyg", () => {
height: 75,
angle: 45,
});
h.elements = [rectangle];
API.setElements([rectangle]);
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
@@ -662,8 +634,7 @@ describe("textWysiwyg", () => {
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -677,7 +648,7 @@ describe("textWysiwyg", () => {
width: 90,
height: 75,
});
h.elements = [diamond];
API.setElements([diamond]);
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(diamond.id);
@@ -687,7 +658,6 @@ describe("textWysiwyg", () => {
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
const value = new Array(1000).fill("1").join("\n");
// Pasting large text to simulate height increase
@@ -712,7 +682,7 @@ describe("textWysiwyg", () => {
height: 75,
backgroundColor: "transparent",
});
h.elements = [rectangle];
API.setElements([rectangle]);
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
expect(h.elements.length).toBe(2);
@@ -721,8 +691,7 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(null);
mouse.down();
let editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
@@ -738,8 +707,7 @@ describe("textWysiwyg", () => {
editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
@@ -759,10 +727,8 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(rectangle.id);
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -777,7 +743,7 @@ describe("textWysiwyg", () => {
height: 75,
strokeWidth: 4,
});
h.elements = [rectangle];
API.setElements([rectangle]);
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
@@ -795,8 +761,7 @@ describe("textWysiwyg", () => {
const editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -808,7 +773,7 @@ describe("textWysiwyg", () => {
width: 100,
height: 0,
});
h.elements = [freedraw];
API.setElements([freedraw]);
UI.clickTool("text");
@@ -819,7 +784,7 @@ describe("textWysiwyg", () => {
const editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
Keyboard.exitTextEditor(editor);
expect(freedraw.boundElements).toBe(null);
expect(h.elements[1].type).toBe("text");
@@ -828,7 +793,7 @@ describe("textWysiwyg", () => {
["freedraw", "line"].forEach((type: any) => {
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
h.elements = [];
API.setElements([]);
const element = UI.createElement(type, {
width: 100,
height: 50,
@@ -855,8 +820,7 @@ describe("textWysiwyg", () => {
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toBe(null);
});
@@ -872,7 +836,6 @@ describe("textWysiwyg", () => {
editor,
"Excalidraw is an opensource virtual collaborative whiteboard",
);
await new Promise((cb) => setTimeout(cb, 0));
expect(h.elements.length).toBe(2);
expect(h.elements[1].type).toBe("text");
@@ -908,14 +871,18 @@ describe("textWysiwyg", () => {
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
mouse.down();
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(await getTextEditor(textEditorSelector, false)).toBe(null);
expect(h.state.editingElement).toBe(null);
expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
fireEvent.click(screen.getByTitle(/code/i));
@@ -950,8 +917,7 @@ describe("textWysiwyg", () => {
updateTextEditor(editor, "Hello World!");
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.text).toBe("Hello \nWorld!");
expect(text.originalText).toBe("Hello World!");
@@ -970,9 +936,7 @@ describe("textWysiwyg", () => {
editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.text).toBe("Hello");
@@ -998,10 +962,8 @@ describe("textWysiwyg", () => {
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -1034,9 +996,8 @@ describe("textWysiwyg", () => {
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -1055,9 +1016,8 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
let editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
Keyboard.exitTextEditor(editor);
// should center align horizontally and vertically by default
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
@@ -1076,12 +1036,8 @@ describe("textWysiwyg", () => {
editor.select();
fireEvent.click(screen.getByTitle("Left"));
await new Promise((r) => setTimeout(r, 0));
fireEvent.click(screen.getByTitle("Align bottom"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
// should left align horizontally and bottom vertically after resize
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
@@ -1101,9 +1057,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Right"));
fireEvent.click(screen.getByTitle("Align top"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
// should right align horizontally and top vertically after resize
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
@@ -1136,8 +1090,7 @@ describe("textWysiwyg", () => {
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle2.boundElements).toBeNull();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
@@ -1148,9 +1101,8 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
Keyboard.exitTextEditor(editor);
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(rectangle.width).toBe(90);
expect(rectangle.height).toBe(75);
@@ -1168,9 +1120,8 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(h.elements.length).toBe(2);
mouse.select(rectangle);
@@ -1200,9 +1151,8 @@ describe("textWysiwyg", () => {
it("undo should work", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: h.elements[1].id, type: "text" },
]);
@@ -1237,10 +1187,9 @@ describe("textWysiwyg", () => {
it("should not allow bound text with only whitespaces", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, " ");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([]);
expect(h.elements[1].isDeleted).toBe(true);
});
@@ -1259,7 +1208,7 @@ describe("textWysiwyg", () => {
text: "Online whiteboard collaboration made easy",
});
h.elements = [container, text];
API.setElements([container, text]);
API.setSelectedElements([container, text]);
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
button: 2,
@@ -1292,9 +1241,8 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
let editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
Keyboard.exitTextEditor(editor);
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect(rectangle.height).toBeCloseTo(155, 8);
@@ -1305,8 +1253,7 @@ describe("textWysiwyg", () => {
editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.height).toBeCloseTo(155, 8);
// cache updated again
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
@@ -1321,7 +1268,7 @@ describe("textWysiwyg", () => {
const editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
editor.blur();
Keyboard.exitTextEditor(editor);
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
@@ -1346,7 +1293,7 @@ describe("textWysiwyg", () => {
const editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello World!");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.25);
@@ -1378,7 +1325,7 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true);
updateTextEditor(editor, "Hello");
editor.blur();
Keyboard.exitTextEditor(editor);
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true);
@@ -1498,13 +1445,11 @@ describe("textWysiwyg", () => {
editor,
"Excalidraw is an opensource virtual collaborative whiteboard",
);
await new Promise((cb) => setTimeout(cb, 0));
editor.select();
fireEvent.click(screen.getByTitle("Left"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(textElement.width).toBe(600);
@@ -1581,16 +1526,14 @@ describe("textWysiwyg", () => {
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
let editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello!");
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
).toBe(VERTICAL_ALIGN.MIDDLE);
fireEvent.click(screen.getByTitle("Align bottom"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
@@ -1606,9 +1549,8 @@ describe("textWysiwyg", () => {
rectangle.y + rectangle.height / 2,
);
editor = await getTextEditor(textEditorSelector, true);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Excalidraw");
editor.blur();
Keyboard.exitTextEditor(editor);
expect(h.elements.length).toBe(3);
expect(rectangle.boundElements).toStrictEqual([
+10 -1
View File
@@ -357,7 +357,16 @@ export const textWysiwyg = ({
};
editable.oninput = () => {
onChange(normalizeText(editable.value));
const normalized = normalizeText(editable.value);
if (editable.value !== normalized) {
const selectionStart = editable.selectionStart;
editable.value = normalized;
// put the cursor at some position close to where it was before
// normalization (otherwise it'll end up at the end of the text)
editable.selectionStart = selectionStart;
editable.selectionEnd = selectionStart;
}
onChange(editable.value);
};
}
@@ -321,6 +321,7 @@ export const shouldShowBoundingBox = (
}
const element = elements[0];
if (isElbowArrow(element)) {
// Elbow arrows cannot be resized as single selected elements
return false;
}
if (!isLinearElement(element)) {
+28
View File
@@ -24,6 +24,7 @@ import type {
ExcalidrawElbowArrowElement,
PointBinding,
FixedPointBinding,
ExcalidrawFlowchartNodeElement,
} from "./types";
export const isInitializedImageElement = (
@@ -175,6 +176,23 @@ export const isRectanguloidElement = (
);
};
// TODO: Remove this when proper distance calculation is introduced
// @see binding.ts:distanceToBindableElement()
export const isRectangularElement = (
element?: ExcalidrawElement | null,
): element is ExcalidrawBindableElement => {
return (
element != null &&
(element.type === "rectangle" ||
element.type === "image" ||
element.type === "text" ||
element.type === "iframe" ||
element.type === "embeddable" ||
element.type === "frame" ||
element.type === "magicframe")
);
};
export const isTextBindableContainer = (
element: ExcalidrawElement | null,
includeLocked = true,
@@ -219,6 +237,16 @@ export const isExcalidrawElement = (
}
};
export const isFlowchartNodeElement = (
element: ExcalidrawElement,
): element is ExcalidrawFlowchartNodeElement => {
return (
element.type === "rectangle" ||
element.type === "ellipse" ||
element.type === "diamond"
);
};
export const hasBoundTextElement = (
element: ExcalidrawElement | null,
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
+10
View File
@@ -160,6 +160,11 @@ export type ExcalidrawGenericElement =
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement;
export type ExcalidrawFlowchartNodeElement =
| ExcalidrawRectangleElement
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement;
/**
* ExcalidrawElement should be JSON serializable and (eventually) contain
* no computed data. The list of all ExcalidrawElements should be shareable
@@ -176,6 +181,11 @@ export type ExcalidrawElement =
| ExcalidrawIframeElement
| ExcalidrawEmbeddableElement;
export type ExcalidrawNonSelectionElement = Exclude<
ExcalidrawElement,
ExcalidrawSelectionElement
>;
export type Ordered<TElement extends ExcalidrawElement> = TElement & {
index: FractionalIndex;
};
+39 -36
View File
@@ -1,3 +1,4 @@
import React from "react";
import type { ExcalidrawElement } from "./element/types";
import { convertToExcalidrawElements, Excalidraw } from "./index";
import { API } from "./tests/helpers/api";
@@ -122,7 +123,7 @@ describe("adding elements to frames", () => {
) => {
describe.skip("when frame is in a layer below", async () => {
it("should add an element", async () => {
h.elements = [frame, rect2];
API.setElements([frame, rect2]);
func(frame, rect2);
@@ -131,7 +132,7 @@ describe("adding elements to frames", () => {
});
it("should add elements", async () => {
h.elements = [frame, rect2, rect3];
API.setElements([frame, rect2, rect3]);
func(frame, rect2);
func(frame, rect3);
@@ -142,7 +143,7 @@ describe("adding elements to frames", () => {
});
it("should add elements when there are other other elements in between", async () => {
h.elements = [frame, rect1, rect2, rect4, rect3];
API.setElements([frame, rect1, rect2, rect4, rect3]);
func(frame, rect2);
func(frame, rect3);
@@ -153,7 +154,7 @@ describe("adding elements to frames", () => {
});
it("should add elements when there are other elements in between and the order is reversed", async () => {
h.elements = [frame, rect3, rect4, rect2, rect1];
API.setElements([frame, rect3, rect4, rect2, rect1]);
func(frame, rect2);
func(frame, rect3);
@@ -166,7 +167,7 @@ describe("adding elements to frames", () => {
describe.skip("when frame is in a layer above", async () => {
it("should add an element", async () => {
h.elements = [rect2, frame];
API.setElements([rect2, frame]);
func(frame, rect2);
@@ -175,7 +176,7 @@ describe("adding elements to frames", () => {
});
it("should add elements", async () => {
h.elements = [rect2, rect3, frame];
API.setElements([rect2, rect3, frame]);
func(frame, rect2);
func(frame, rect3);
@@ -186,7 +187,7 @@ describe("adding elements to frames", () => {
});
it("should add elements when there are other other elements in between", async () => {
h.elements = [rect1, rect2, rect4, rect3, frame];
API.setElements([rect1, rect2, rect4, rect3, frame]);
func(frame, rect2);
func(frame, rect3);
@@ -197,7 +198,7 @@ describe("adding elements to frames", () => {
});
it("should add elements when there are other elements in between and the order is reversed", async () => {
h.elements = [rect3, rect4, rect2, rect1, frame];
API.setElements([rect3, rect4, rect2, rect1, frame]);
func(frame, rect2);
func(frame, rect3);
@@ -210,7 +211,7 @@ describe("adding elements to frames", () => {
describe("when frame is in an inner layer", async () => {
it.skip("should add elements", async () => {
h.elements = [rect2, frame, rect3];
API.setElements([rect2, frame, rect3]);
func(frame, rect2);
func(frame, rect3);
@@ -221,7 +222,7 @@ describe("adding elements to frames", () => {
});
it.skip("should add elements when there are other other elements in between", async () => {
h.elements = [rect2, rect1, frame, rect4, rect3];
API.setElements([rect2, rect1, frame, rect4, rect3]);
func(frame, rect2);
func(frame, rect3);
@@ -232,7 +233,7 @@ describe("adding elements to frames", () => {
});
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
h.elements = [rect3, rect4, frame, rect2, rect1];
API.setElements([rect3, rect4, frame, rect2, rect1]);
func(frame, rect2);
func(frame, rect3);
@@ -253,20 +254,22 @@ describe("adding elements to frames", () => {
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
h.elements = reorderElements(
[
frame,
...convertToExcalidrawElements([
{
type: containerType,
x: 100,
y: 100,
height: 10,
label: { text: "xx" },
},
]),
],
initialOrder,
API.setElements(
reorderElements(
[
frame,
...convertToExcalidrawElements([
{
type: containerType,
x: 100,
y: 100,
height: 10,
label: { text: "xx" },
},
]),
],
initialOrder,
),
);
assertOrder(h.elements, initialOrder);
@@ -337,7 +340,7 @@ describe("adding elements to frames", () => {
});
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
h.elements = [frame, arrow, text];
API.setElements([frame, arrow, text]);
resizeFrameOverElement(frame, arrow);
@@ -347,7 +350,7 @@ describe("adding elements to frames", () => {
});
it("should add arrow bound with text when frame is in a layer above", async () => {
h.elements = [arrow, text, frame];
API.setElements([arrow, text, frame]);
resizeFrameOverElement(frame, arrow);
@@ -357,7 +360,7 @@ describe("adding elements to frames", () => {
});
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
h.elements = [arrow, frame, text];
API.setElements([arrow, frame, text]);
resizeFrameOverElement(frame, arrow);
@@ -369,7 +372,7 @@ describe("adding elements to frames", () => {
describe("resizing frame over elements but downwards", async () => {
it.skip("should add elements when frame is in a layer below", async () => {
h.elements = [frame, rect1, rect2, rect3, rect4];
API.setElements([frame, rect1, rect2, rect3, rect4]);
resizeFrameOverElement(frame, rect4);
resizeFrameOverElement(frame, rect3);
@@ -380,7 +383,7 @@ describe("adding elements to frames", () => {
});
it.skip("should add elements when frame is in a layer above", async () => {
h.elements = [rect1, rect2, rect3, rect4, frame];
API.setElements([rect1, rect2, rect3, rect4, frame]);
resizeFrameOverElement(frame, rect4);
resizeFrameOverElement(frame, rect3);
@@ -391,7 +394,7 @@ describe("adding elements to frames", () => {
});
it.skip("should add elements when frame is in an inner layer", async () => {
h.elements = [rect1, rect2, frame, rect3, rect4];
API.setElements([rect1, rect2, frame, rect3, rect4]);
resizeFrameOverElement(frame, rect4);
resizeFrameOverElement(frame, rect3);
@@ -406,7 +409,7 @@ describe("adding elements to frames", () => {
await commonTestCases(dragElementIntoFrame);
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
h.elements = [frame, rect2];
API.setElements([frame, rect2]);
dragElementIntoFrame(frame, rect2);
@@ -420,7 +423,7 @@ describe("adding elements to frames", () => {
});
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
h.elements = [frame, rect2];
API.setElements([frame, rect2]);
dragElementIntoFrame(frame, rect2);
@@ -490,7 +493,7 @@ describe("adding elements to frames", () => {
frameId: frame3.id,
});
h.elements = [
API.setElements([
frame1,
rectangle4,
rectangle1,
@@ -498,7 +501,7 @@ describe("adding elements to frames", () => {
frame3,
rectangle2,
frame2,
];
]);
API.setSelectedElements([rectangle2]);
@@ -541,7 +544,7 @@ describe("adding elements to frames", () => {
frameId: frame2.id,
});
h.elements = [rectangle1, rectangle2, frame1, frame2];
API.setElements([rectangle1, rectangle2, frame1, frame2]);
API.setSelectedElements([rectangle2]);
+2 -9
View File
@@ -1,7 +1,6 @@
import type { AppStateChange, ElementsChange } from "./change";
import type { SceneElementsMap } from "./element/types";
import { Emitter } from "./emitter";
import type Scene from "./scene/Scene";
import type { Snapshot } from "./store";
import type { AppState } from "./types";
@@ -65,7 +64,6 @@ export class History {
elements: SceneElementsMap,
appState: AppState,
snapshot: Readonly<Snapshot>,
scene: Scene,
) {
return this.perform(
elements,
@@ -73,7 +71,6 @@ export class History {
snapshot,
() => History.pop(this.undoStack),
(entry: HistoryEntry) => History.push(this.redoStack, entry, elements),
scene,
);
}
@@ -81,7 +78,6 @@ export class History {
elements: SceneElementsMap,
appState: AppState,
snapshot: Readonly<Snapshot>,
scene: Scene,
) {
return this.perform(
elements,
@@ -89,7 +85,6 @@ export class History {
snapshot,
() => History.pop(this.redoStack),
(entry: HistoryEntry) => History.push(this.undoStack, entry, elements),
scene,
);
}
@@ -99,7 +94,6 @@ export class History {
snapshot: Readonly<Snapshot>,
pop: () => HistoryEntry | null,
push: (entry: HistoryEntry) => void,
scene: Scene,
): [SceneElementsMap, AppState] | void {
try {
let historyEntry = pop();
@@ -116,7 +110,7 @@ export class History {
while (historyEntry) {
try {
[nextElements, nextAppState, containsVisibleChange] =
historyEntry.applyTo(nextElements, nextAppState, snapshot, scene);
historyEntry.applyTo(nextElements, nextAppState, snapshot);
} finally {
// make sure to always push / pop, even if the increment is corrupted
push(historyEntry);
@@ -187,10 +181,9 @@ export class HistoryEntry {
elements: SceneElementsMap,
appState: AppState,
snapshot: Readonly<Snapshot>,
scene: Scene,
): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] =
this.elementsChange.applyTo(elements, snapshot.elements, scene);
this.elementsChange.applyTo(elements, snapshot.elements);
const [nextAppState, appStateContainsVisibleChange] =
this.appStateChange.applyTo(appState, nextElements);
+3
View File
@@ -316,6 +316,7 @@
"placeImage": "Click to place the image, or click and drag to set its size manually",
"publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text",
"createFlowchart": "Hold CtrlOrCmd and Arrow key to create a flowchart",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion",
"firefox_clipboard_write": "This feature can likely be enabled by setting the \"dom.events.asyncClipboard.clipboardItem\" flag to \"true\". To change the browser flags in Firefox, visit the \"about:config\" page.",
@@ -366,6 +367,8 @@
"click": "click",
"deepSelect": "Deep select",
"deepBoxSelect": "Deep select within box, and prevent dragging",
"createFlowchart": "Create a flowchart from a generic element",
"navigateFlowchart": "Navigate a flowchart",
"curvedArrow": "Curved arrow",
"curvedLine": "Curved line",
"documentation": "Documentation",
+2 -1
View File
@@ -96,8 +96,9 @@
"@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1",
"@size-limit/preset-big-lib": "9.0.0",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5",
"@testing-library/react": "16.0.0",
"@types/pako": "1.0.3",
"@types/pica": "5.1.3",
"@types/resize-observer-browser": "0.1.7",
+21 -1
View File
@@ -35,6 +35,7 @@ import type {
Zoom,
InteractiveCanvasAppState,
ElementsPendingErasure,
PendingExcalidrawElements,
} from "../types";
import { getDefaultAppState } from "../appState";
import {
@@ -104,6 +105,7 @@ export const getRenderOpacity = (
element: ExcalidrawElement,
containingFrame: ExcalidrawFrameLikeElement | null,
elementsPendingErasure: ElementsPendingErasure,
pendingNodes: Readonly<PendingExcalidrawElements> | null,
) => {
// multiplying frame opacity with element opacity to combine them
// (e.g. frame 50% and element 50% opacity should result in 25% opacity)
@@ -113,6 +115,7 @@ export const getRenderOpacity = (
// (so that erasing always results in lower opacity than original)
if (
elementsPendingErasure.has(element.id) ||
(pendingNodes && pendingNodes.some((node) => node.id === element.id)) ||
(containingFrame && elementsPendingErasure.has(containingFrame.id))
) {
opacity *= ELEMENT_READY_TO_ERASE_OPACITY / 100;
@@ -196,7 +199,7 @@ const generateElementCanvas = (
zoom: Zoom,
renderConfig: StaticCanvasRenderConfig,
appState: StaticCanvasAppState,
): ExcalidrawElementWithCanvas => {
): ExcalidrawElementWithCanvas | null => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!;
const padding = getCanvasPadding(element);
@@ -207,6 +210,10 @@ const generateElementCanvas = (
zoom,
);
if (!width || !height) {
return null;
}
canvas.width = width;
canvas.height = height;
@@ -537,6 +544,10 @@ const generateElementWithCanvas = (
appState,
);
if (!elementWithCanvas) {
return null;
}
elementWithCanvasCache.set(element, elementWithCanvas);
return elementWithCanvas;
@@ -672,6 +683,7 @@ export const renderElement = (
element,
getContainingFrame(element, elementsMap),
renderConfig.elementsPendingErasure,
renderConfig.pendingFlowchartNodes,
);
switch (element.type) {
@@ -738,6 +750,10 @@ export const renderElement = (
renderConfig,
appState,
);
if (!elementWithCanvas) {
return;
}
drawElementFromCanvas(
elementWithCanvas,
context,
@@ -877,6 +893,10 @@ export const renderElement = (
appState,
);
if (!elementWithCanvas) {
return;
}
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
if (
@@ -370,6 +370,23 @@ const _renderStaticScene = ({
console.error(error);
}
});
// render pending nodes for flowcharts
renderConfig.pendingFlowchartNodes?.forEach((element) => {
try {
renderElement(
element,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState,
);
} catch (error) {
console.error(error);
}
});
};
/** throttled to animation framerate */
+9 -1
View File
@@ -377,6 +377,10 @@ class Scene {
}
insertElementsAtIndex(elements: ExcalidrawElement[], index: number) {
if (!elements.length) {
return;
}
if (!Number.isFinite(index) || index < 0) {
throw new Error(
"insertElementAtIndex can only be called with index >= 0",
@@ -403,7 +407,11 @@ class Scene {
};
insertElements = (elements: ExcalidrawElement[]) => {
const index = elements[0].frameId
if (!elements.length) {
return;
}
const index = elements[0]?.frameId
? this.getElementIndex(elements[0].frameId)
: this.elements.length;
+1
View File
@@ -242,6 +242,7 @@ export const exportToCanvas = async (
// empty disables embeddable rendering
embedsValidationStatus: new Map(),
elementsPendingErasure: new Set(),
pendingFlowchartNodes: null,
},
});
+2
View File
@@ -16,6 +16,7 @@ import type {
SocketId,
UserIdleState,
Device,
PendingExcalidrawElements,
} from "../types";
import type { MakeBrand } from "../utility-types";
@@ -33,6 +34,7 @@ export type StaticCanvasRenderConfig = {
isExporting: boolean;
embedsValidationStatus: EmbedsValidationStatus;
elementsPendingErasure: ElementsPendingErasure;
pendingFlowchartNodes: PendingExcalidrawElements | null;
};
export type SVGRenderConfig = {
+5 -7
View File
@@ -1209,16 +1209,14 @@ export const snapResizingElements = (
};
export const snapNewElement = (
draggingElement: ExcalidrawElement,
newElement: ExcalidrawElement,
appState: AppState,
event: KeyboardModifiersObject,
origin: Vector2D,
dragOffset: Vector2D,
elementsMap: ElementsMap,
) => {
if (
!isSnappingEnabled({ event, selectedElements: [draggingElement], appState })
) {
if (!isSnappingEnabled({ event, selectedElements: [newElement], appState })) {
return {
snapOffset: { x: 0, y: 0 },
snapLines: [],
@@ -1240,7 +1238,7 @@ export const snapNewElement = (
const nearestSnapsY: Snaps = [];
getPointSnaps(
[draggingElement],
[newElement],
selectionSnapPoints,
appState,
event,
@@ -1259,13 +1257,13 @@ export const snapNewElement = (
nearestSnapsX.length = 0;
nearestSnapsY.length = 0;
const corners = getElementsCorners([draggingElement], elementsMap, {
const corners = getElementsCorners([newElement], elementsMap, {
boundingBoxCorners: true,
omitCenter: true,
});
getPointSnaps(
[draggingElement],
[newElement],
corners,
appState,
event,
+1
View File
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import * as StaticScene from "../renderer/staticScene";
import { reseed } from "../random";
@@ -1,4 +1,5 @@
import { act, render, waitFor } from "./test-utils";
import React from "react";
import { render, waitFor } from "./test-utils";
import { Excalidraw } from "../index";
import { expect } from "vitest";
import { getTextEditor, updateTextEditor } from "./queries/dom";
@@ -89,7 +90,7 @@ describe("Test <MermaidToExcalidraw/>", () => {
it("should open mermaid popup when active tool is mermaid", async () => {
const dialog = document.querySelector(".ttd-dialog")!;
await waitFor(() => dialog.querySelector("canvas"));
await waitFor(() => expect(dialog.querySelector("canvas")).not.toBeNull());
expect(dialog.outerHTML).toMatchSnapshot();
});
@@ -103,19 +104,9 @@ describe("Test <MermaidToExcalidraw/>", () => {
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
expect(editor.textContent).toMatchInlineSnapshot(`
"flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[Car]"
`);
expect(editor.textContent).toMatchSnapshot();
await act(async () => {
updateTextEditor(editor, "flowchart TD1");
await new Promise((cb) => setTimeout(cb, 0));
});
updateTextEditor(editor, "flowchart TD1");
editor = await getTextEditor(selector, false);
expect(editor.textContent).toBe("flowchart TD1");
@@ -1,10 +1,19 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style="animation-duration: 0s;"><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style=""><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
A[Christmas] --&gt;|Get money| B(Go shopping)
B --&gt; C{Let me think}
C --&gt;|One| D[Laptop]
C --&gt;|Two| E[iPhone]
C --&gt;|Three| F[Car]</textarea><div class="ttd-dialog-panel-button-container invisible" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class=""></div></button></div></div><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Preview</label></div><div class="ttd-dialog-output-wrapper"><div style="opacity: 1;" class="ttd-dialog-output-canvas-container"><canvas width="89" height="158" dir="ltr"></canvas></div></div><div class="ttd-dialog-panel-button-container" style="display: flex; align-items: center;"><button type="button" class="excalidraw-button ttd-dialog-panel-button"><div class="">Insert<span><svg aria-hidden="true" focusable="false" role="img" viewBox="0 0 20 20" class="" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><g stroke-width="1.25"><path d="M4.16602 10H15.8327"></path><path d="M12.5 13.3333L15.8333 10"></path><path d="M12.5 6.66666L15.8333 9.99999"></path></g></svg></span></div></button><div class="ttd-dialog-submit-shortcut"><div class="ttd-dialog-submit-shortcut__key">Ctrl</div><div class="ttd-dialog-submit-shortcut__key">Enter</div></div></div></div></div></div></div></div></div></div></div>"
`;
exports[`Test <MermaidToExcalidraw/> > should show error in preview when mermaid library throws error 1`] = `
"flowchart TD
A[Christmas] -->|Get money| B(Go shopping)
B --> C{Let me think}
C -->|One| D[Laptop]
C -->|Two| E[iPhone]
C -->|Three| F[Car]"
`;
@@ -812,7 +812,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -841,6 +840,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -1015,7 +1015,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1044,6 +1043,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -1228,7 +1228,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1257,6 +1256,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -1556,7 +1556,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1585,6 +1584,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -1884,7 +1884,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1913,6 +1912,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -2097,7 +2097,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2126,6 +2125,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -2334,7 +2334,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2363,6 +2362,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -2632,7 +2632,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2661,6 +2660,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -2998,7 +2998,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3027,6 +3026,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -3470,7 +3470,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3499,6 +3498,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -3790,7 +3790,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3819,6 +3818,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -4110,7 +4110,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4139,6 +4138,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -5293,7 +5293,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5322,6 +5321,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -6417,7 +6417,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -6446,6 +6445,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -7349,7 +7349,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7378,6 +7377,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -8258,7 +8258,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8287,6 +8286,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -9149,7 +9149,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9178,6 +9177,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 20,
"offsetTop": 10,
@@ -29,7 +29,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -57,6 +56,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -628,7 +628,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -656,6 +655,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1131,7 +1131,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1159,6 +1158,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1496,7 +1496,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1524,6 +1523,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1862,7 +1862,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1890,6 +1889,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2126,7 +2126,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2154,6 +2153,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2563,7 +2563,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2591,6 +2590,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2859,7 +2859,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2887,6 +2886,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3140,7 +3140,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3168,6 +3167,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3431,7 +3431,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3459,6 +3458,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3714,7 +3714,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3742,6 +3741,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3946,7 +3946,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3974,6 +3973,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4202,7 +4202,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4230,6 +4229,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4472,7 +4472,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4500,6 +4499,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4700,7 +4700,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4728,6 +4727,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4928,7 +4928,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4956,6 +4955,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5154,7 +5154,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5182,6 +5181,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5380,7 +5380,6 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5408,6 +5407,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5636,7 +5636,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5664,6 +5663,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5964,7 +5964,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5992,6 +5991,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -6386,7 +6386,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -6414,6 +6413,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -6761,7 +6761,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -6789,6 +6788,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7077,7 +7077,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7105,6 +7104,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7372,7 +7372,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7400,6 +7399,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7598,7 +7598,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7626,6 +7625,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7950,7 +7950,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7978,6 +7977,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8302,7 +8302,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8330,6 +8329,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8703,7 +8703,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8731,6 +8730,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8987,7 +8987,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9015,6 +9014,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9249,7 +9249,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9277,6 +9276,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9510,7 +9510,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9538,6 +9537,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9738,7 +9738,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9766,6 +9765,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -10036,7 +10036,6 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -10064,6 +10063,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -10373,7 +10373,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -10401,6 +10400,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -10605,7 +10605,6 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -10633,6 +10632,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11055,7 +11055,6 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11083,6 +11082,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11306,7 +11306,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11334,6 +11333,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11542,7 +11542,6 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11570,6 +11569,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11780,7 +11780,6 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11808,6 +11807,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -12178,7 +12178,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -12206,6 +12205,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -12422,7 +12422,6 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -12450,6 +12449,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -12660,7 +12660,6 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -12688,6 +12687,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -12898,7 +12898,6 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -12926,6 +12925,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13142,7 +13142,6 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13170,6 +13169,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13471,7 +13471,6 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13499,6 +13498,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13640,7 +13640,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13668,6 +13667,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13925,7 +13925,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13953,6 +13952,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -14189,7 +14189,6 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -14217,6 +14216,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -14461,7 +14461,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -14489,6 +14488,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -14619,7 +14619,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -14647,6 +14646,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -15312,7 +15312,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -15340,6 +15339,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -15929,7 +15929,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -15957,6 +15956,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -16546,7 +16546,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -16574,6 +16573,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -17255,7 +17255,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -17283,6 +17282,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -18002,7 +18002,6 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -18030,6 +18029,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -18473,7 +18473,6 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -18501,6 +18500,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -18992,7 +18992,6 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -19020,6 +19019,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -19445,7 +19445,6 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -19473,6 +19472,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
"isRotating": false,
"lastPointerDownWith": "mouse",
"multiElement": null,
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1,5 +1,17 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Test Linear Elements > Test bound text element > should bind text to arrow when clicked on arrow and enter pressed 1`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should bind text to arrow when double clicked 1`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `
<textarea
class="excalidraw-wysiwyg"
@@ -10,3 +22,36 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
wrap="off"
/>
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 2`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text and bounding box correctly when 3 pointer arrow element resized 6`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 2`] = `
"Online whiteboard
collaboration made
easy"
`;
exports[`Test Linear Elements > Test bound text element > should resize and position the bound text correctly when 2 pointer linear element resized 5`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 1`] = `
"Online whiteboard
collaboration made easy"
`;
exports[`Test Linear Elements > Test bound text element > should wrap the bound text when arrow bound container moves 2`] = `
"Online whiteboard
collaboration made
easy"
`;
@@ -29,7 +29,6 @@ exports[`given element A and group of elements B and given both are selected whe
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -58,6 +57,7 @@ exports[`given element A and group of elements B and given both are selected whe
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -438,7 +438,6 @@ exports[`given element A and group of elements B and given both are selected whe
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -467,6 +466,7 @@ exports[`given element A and group of elements B and given both are selected whe
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -838,7 +838,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": "id10",
@@ -867,6 +866,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1377,7 +1377,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1406,6 +1405,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1575,7 +1575,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1604,6 +1603,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1944,7 +1944,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -1973,6 +1972,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2178,7 +2178,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2207,6 +2206,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2352,7 +2352,6 @@ exports[`regression tests > can drag element that covers another element, while
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2381,6 +2380,7 @@ exports[`regression tests > can drag element that covers another element, while
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2666,7 +2666,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2695,6 +2694,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -2906,7 +2906,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -2935,6 +2934,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3143,7 +3143,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3172,6 +3171,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3367,7 +3367,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3396,6 +3395,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3617,7 +3617,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3646,6 +3645,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -3922,7 +3922,6 @@ exports[`regression tests > deleting last but one element in editing group shoul
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -3951,6 +3950,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4330,37 +4330,6 @@ exports[`regression tests > deselects group of selected elements on pointer down
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id3",
"index": null,
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1505387817,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 500,
"y": 500,
},
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4389,6 +4358,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4637,37 +4607,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id3",
"index": null,
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1505387817,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 50,
"y": 50,
},
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4696,6 +4635,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -4914,37 +4854,6 @@ exports[`regression tests > deselects selected element on pointer down when poin
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id1",
"index": null,
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1150084233,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 110,
"y": 110,
},
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -4973,6 +4882,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5148,7 +5058,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5177,6 +5086,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5341,7 +5251,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": "id3",
@@ -5370,6 +5279,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -5717,7 +5627,6 @@ exports[`regression tests > drags selected elements from point inside common bou
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -5746,6 +5655,7 @@ exports[`regression tests > drags selected elements from point inside common bou
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -6001,7 +5911,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -6030,6 +5939,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -6803,7 +6713,6 @@ exports[`regression tests > given a group of selected elements with an element t
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -6832,6 +6741,7 @@ exports[`regression tests > given a group of selected elements with an element t
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7127,7 +7037,6 @@ exports[`regression tests > given a selected element A and a not selected elemen
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7156,6 +7065,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7397,7 +7307,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7426,6 +7335,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7625,7 +7535,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7654,6 +7563,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -7856,7 +7766,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -7885,6 +7794,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8030,7 +7940,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8059,6 +7968,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8204,7 +8114,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8233,6 +8142,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8378,7 +8288,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8407,6 +8316,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8594,7 +8504,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8623,6 +8532,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8809,7 +8719,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -8838,6 +8747,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -8997,7 +8907,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9026,6 +8935,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9213,7 +9123,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9242,6 +9151,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9387,7 +9297,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9416,6 +9325,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9602,7 +9512,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9631,6 +9540,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9776,7 +9686,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9805,6 +9714,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -9964,7 +9874,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -9993,6 +9902,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -10138,7 +10048,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -10167,6 +10076,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -10646,7 +10556,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -10675,6 +10584,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -10917,7 +10827,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -10946,6 +10855,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
"lastPointerDownWith": "touch",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11037,7 +10947,6 @@ exports[`regression tests > shift click on selected element should deselect it o
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11066,6 +10975,7 @@ exports[`regression tests > shift click on selected element should deselect it o
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11230,7 +11140,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11259,6 +11168,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11535,7 +11445,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11564,6 +11473,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -11941,7 +11851,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -11970,6 +11879,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -12548,7 +12458,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -12577,6 +12486,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -12671,7 +12581,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": "id3",
@@ -12700,6 +12609,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13249,37 +13159,6 @@ exports[`regression tests > switches from group of selected elements to another
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id4",
"index": null,
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1723083209,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 0,
"y": 0,
},
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13308,6 +13187,7 @@ exports[`regression tests > switches from group of selected elements to another
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13611,37 +13491,6 @@ exports[`regression tests > switches selected element on pointer down > [end of
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": {
"angle": 0,
"backgroundColor": "transparent",
"boundElements": null,
"customData": undefined,
"fillStyle": "solid",
"frameId": null,
"groupIds": [],
"height": 0,
"id": "id2",
"index": null,
"isDeleted": false,
"link": null,
"locked": false,
"opacity": 100,
"roughness": 1,
"roundness": {
"type": 2,
},
"seed": 1604849351,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 2,
"type": "selection",
"updated": 1,
"version": 1,
"versionNonce": 0,
"width": 0,
"x": 0,
"y": 0,
},
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13670,6 +13519,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -13900,7 +13750,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"currentItemTextAlign": "left",
"cursorButton": "down",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -13929,6 +13778,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
"lastPointerDownWith": "touch",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -14020,7 +13870,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -14049,6 +13898,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -14393,7 +14243,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -14422,6 +14271,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -14513,7 +14363,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"currentItemTextAlign": "left",
"cursorButton": "up",
"defaultSidebarDockedPreference": false,
"draggingElement": null,
"editingElement": null,
"editingFrame": null,
"editingGroupId": null,
@@ -14542,6 +14391,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
"lastPointerDownWith": "mouse",
"multiElement": null,
"name": "Untitled-201933152653",
"newElement": null,
"objectsSnapModeEnabled": false,
"offsetLeft": 0,
"offsetTop": 0,
@@ -1,3 +1,4 @@
import React from "react";
import { Excalidraw } from "../index";
import { CODES } from "../keys";
import { API } from "../tests/helpers/api";
+30 -29
View File
@@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
import { render } from "./test-utils";
import { act, render } from "./test-utils";
import { Excalidraw } from "../index";
import { defaultLang, setLanguage } from "../i18n";
import { UI, Pointer, Keyboard } from "./helpers/ui";
@@ -15,8 +16,6 @@ import {
actionAlignRight,
} from "../actions";
const { h } = window;
const mouse = new Pointer("mouse");
const createAndSelectTwoRectangles = () => {
@@ -59,7 +58,9 @@ describe("aligning", () => {
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
mouse.reset();
await setLanguage(defaultLang);
await act(() => {
return setLanguage(defaultLang);
});
await render(<Excalidraw handleKeyboardGlobally={true} />);
});
@@ -156,7 +157,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
API.executeAction(actionAlignVerticallyCentered);
// Check if x position did not change
expect(API.getSelectedElements()[0].x).toEqual(0);
@@ -175,7 +176,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(110);
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(60);
expect(API.getSelectedElements()[1].x).toEqual(55);
@@ -201,7 +202,7 @@ describe("aligning", () => {
mouse.click();
});
h.app.actionManager.executeAction(actionGroup);
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
@@ -222,7 +223,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
h.app.actionManager.executeAction(actionAlignTop);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
@@ -236,7 +237,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
h.app.actionManager.executeAction(actionAlignBottom);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
@@ -250,7 +251,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
h.app.actionManager.executeAction(actionAlignLeft);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
@@ -264,7 +265,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
h.app.actionManager.executeAction(actionAlignRight);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
@@ -278,7 +279,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[1].y).toEqual(100);
expect(API.getSelectedElements()[2].y).toEqual(200);
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
@@ -292,7 +293,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[1].x).toEqual(100);
expect(API.getSelectedElements()[2].x).toEqual(200);
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
@@ -315,7 +316,7 @@ describe("aligning", () => {
mouse.click();
});
h.app.actionManager.executeAction(actionGroup);
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
@@ -331,7 +332,7 @@ describe("aligning", () => {
mouse.click();
});
h.app.actionManager.executeAction(actionGroup);
API.executeAction(actionGroup);
// Select the first group.
// The second group is already selected because it was the last group created
@@ -349,7 +350,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
h.app.actionManager.executeAction(actionAlignTop);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
@@ -365,7 +366,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
h.app.actionManager.executeAction(actionAlignBottom);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(200);
expect(API.getSelectedElements()[1].y).toEqual(300);
@@ -381,7 +382,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
h.app.actionManager.executeAction(actionAlignLeft);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
@@ -397,7 +398,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
h.app.actionManager.executeAction(actionAlignRight);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(200);
expect(API.getSelectedElements()[1].x).toEqual(300);
@@ -413,7 +414,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
@@ -429,7 +430,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
@@ -454,7 +455,7 @@ describe("aligning", () => {
});
// Create first group of rectangles
h.app.actionManager.executeAction(actionGroup);
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
@@ -468,7 +469,7 @@ describe("aligning", () => {
});
// Create the nested group
h.app.actionManager.executeAction(actionGroup);
API.executeAction(actionGroup);
mouse.reset();
UI.clickTool("rectangle");
@@ -490,7 +491,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
h.app.actionManager.executeAction(actionAlignTop);
API.executeAction(actionAlignTop);
expect(API.getSelectedElements()[0].y).toEqual(0);
expect(API.getSelectedElements()[1].y).toEqual(100);
@@ -506,7 +507,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
h.app.actionManager.executeAction(actionAlignBottom);
API.executeAction(actionAlignBottom);
expect(API.getSelectedElements()[0].y).toEqual(100);
expect(API.getSelectedElements()[1].y).toEqual(200);
@@ -522,7 +523,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
h.app.actionManager.executeAction(actionAlignLeft);
API.executeAction(actionAlignLeft);
expect(API.getSelectedElements()[0].x).toEqual(0);
expect(API.getSelectedElements()[1].x).toEqual(100);
@@ -538,7 +539,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
h.app.actionManager.executeAction(actionAlignRight);
API.executeAction(actionAlignRight);
expect(API.getSelectedElements()[0].x).toEqual(100);
expect(API.getSelectedElements()[1].x).toEqual(200);
@@ -554,7 +555,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].y).toEqual(200);
expect(API.getSelectedElements()[3].y).toEqual(300);
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
API.executeAction(actionAlignVerticallyCentered);
expect(API.getSelectedElements()[0].y).toEqual(50);
expect(API.getSelectedElements()[1].y).toEqual(150);
@@ -570,7 +571,7 @@ describe("aligning", () => {
expect(API.getSelectedElements()[2].x).toEqual(200);
expect(API.getSelectedElements()[3].x).toEqual(300);
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
API.executeAction(actionAlignHorizontallyCentered);
expect(API.getSelectedElements()[0].x).toEqual(50);
expect(API.getSelectedElements()[1].x).toEqual(150);
+4 -4
View File
@@ -1,5 +1,5 @@
import { queryByTestId, render, waitFor } from "./test-utils";
import React from "react";
import { fireEvent, queryByTestId, render, waitFor } from "./test-utils";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
import { getDefaultAppState } from "../appState";
@@ -31,7 +31,7 @@ describe("appState", () => {
expect(h.state.viewBackgroundColor).toBe("#F00");
});
API.drop(
await API.drop(
new Blob(
[
JSON.stringify({
@@ -69,7 +69,7 @@ describe("appState", () => {
UI.clickTool("text");
expect(h.state.currentItemFontSize).toBe(30);
queryByTestId(container, "fontSize-small")!.click();
fireEvent.click(queryByTestId(container, "fontSize-small")!);
expect(h.state.currentItemFontSize).toBe(16);
const mouse = new Pointer("mouse");
+6 -5
View File
@@ -1,3 +1,4 @@
import React from "react";
import { fireEvent, render } from "./test-utils";
import { Excalidraw, isLinearElement } from "../index";
import { UI, Pointer, Keyboard } from "./helpers/ui";
@@ -37,7 +38,7 @@ describe("element binding", () => {
[100, 0],
],
});
h.elements = [rect, arrow];
API.setElements([rect, arrow]);
expect(arrow.startBinding).toBe(null);
// select arrow
@@ -225,7 +226,7 @@ describe("element binding", () => {
height: 100,
});
h.elements = [text];
API.setElements([text]);
const arrow = UI.createElement("arrow", {
x: 0,
@@ -267,7 +268,7 @@ describe("element binding", () => {
height: 100,
});
h.elements = [text];
API.setElements([text]);
const arrow = UI.createElement("arrow", {
x: 0,
@@ -362,13 +363,13 @@ describe("element binding", () => {
],
});
h.elements = [rectangle1, arrow1, arrow2, text1];
API.setElements([rectangle1, arrow1, arrow2, text1]);
API.setSelectedElements([text1]);
expect(h.state.selectedElementIds[text1.id]).toBe(true);
h.app.actionManager.executeAction(actionWrapTextInContainer);
API.executeAction(actionWrapTextInContainer);
// new text container will be placed before the text element
const container = h.elements.at(-2)!;
+5 -4
View File
@@ -1,3 +1,4 @@
import React from "react";
import { vi } from "vitest";
import ReactDOM from "react-dom";
import { render, waitFor, GlobalTestState } from "./test-utils";
@@ -279,7 +280,7 @@ describe("pasting & frames", () => {
});
const rect = API.createElement({ type: "rectangle" });
h.elements = [frame];
API.setElements([frame]);
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect],
@@ -318,7 +319,7 @@ describe("pasting & frames", () => {
y: 100,
});
h.elements = [frame];
API.setElements([frame]);
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2],
@@ -361,7 +362,7 @@ describe("pasting & frames", () => {
groupIds: ["g1"],
});
h.elements = [frame];
API.setElements([frame]);
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2],
@@ -412,7 +413,7 @@ describe("pasting & frames", () => {
frameId: frame2.id,
});
h.elements = [frame];
API.setElements([frame]);
const clipboardJSON = await serializeAsClipboardJSON({
elements: [rect, rect2, frame2],
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import {
render,
@@ -159,7 +160,7 @@ describe("contextMenu element", () => {
width: 200,
backgroundColor: "red",
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
API.setSelectedElements([rect1]);
// lower z-index
@@ -607,7 +608,7 @@ describe("contextMenu element", () => {
fillStyle: "solid",
groupIds: ["g1"],
});
h.elements = [rectangle1, rectangle2];
API.setElements([rectangle1, rectangle2]);
mouse.rightClickAt(50, 50);
expect(API.getSelectedElements().length).toBe(2);
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import { Excalidraw } from "../index";
import * as StaticScene from "../renderer/staticScene";
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import { Excalidraw } from "../index";
import { render } from "../tests/test-utils";
@@ -16,7 +17,7 @@ const h = window.h;
describe("element locking", () => {
beforeEach(async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.elements = [];
API.setElements([]);
});
it("click-selecting a locked element is disabled", () => {
@@ -28,7 +29,7 @@ describe("element locking", () => {
locked: true,
});
h.elements = [lockedRectangle];
API.setElements([lockedRectangle]);
mouse.clickAt(50, 50);
expect(API.getSelectedElements().length).toBe(0);
@@ -45,7 +46,7 @@ describe("element locking", () => {
y: 100,
});
h.elements = [lockedRectangle];
API.setElements([lockedRectangle]);
mouse.downAt(50, 50);
mouse.moveTo(250, 250);
@@ -62,7 +63,7 @@ describe("element locking", () => {
locked: true,
});
h.elements = [lockedRectangle];
API.setElements([lockedRectangle]);
mouse.downAt(50, 50);
mouse.moveTo(100, 100);
@@ -85,7 +86,7 @@ describe("element locking", () => {
locked: true,
});
h.elements = [rectangle, lockedRectangle];
API.setElements([rectangle, lockedRectangle]);
mouse.downAt(50, 50);
mouse.moveTo(100, 100);
@@ -97,11 +98,11 @@ describe("element locking", () => {
});
it("selectAll shouldn't select locked elements", () => {
h.elements = [
API.setElements([
API.createElement({ type: "rectangle" }),
API.createElement({ type: "rectangle", locked: true }),
];
h.app.actionManager.executeAction(actionSelectAll);
]);
API.executeAction(actionSelectAll);
expect(API.getSelectedElements().length).toBe(1);
});
@@ -120,7 +121,7 @@ describe("element locking", () => {
locked: true,
});
h.elements = [rectangle, lockedRectangle];
API.setElements([rectangle, lockedRectangle]);
expect(API.getSelectedElements().length).toBe(0);
mouse.clickAt(50, 50);
expect(API.getSelectedElements().length).toBe(1);
@@ -142,7 +143,7 @@ describe("element locking", () => {
locked: true,
});
h.elements = [rectangle, lockedRectangle];
API.setElements([rectangle, lockedRectangle]);
expect(API.getSelectedElements().length).toBe(0);
mouse.rightClickAt(50, 50);
expect(API.getSelectedElements().length).toBe(1);
@@ -172,7 +173,7 @@ describe("element locking", () => {
locked: true,
});
h.elements = [rectangle, lockedRectangle];
API.setElements([rectangle, lockedRectangle]);
API.setSelectedElements([rectangle]);
expect(API.getSelectedElements().length).toBe(1);
expect(API.getSelectedElement().id).toBe(rectangle.id);
@@ -203,7 +204,7 @@ describe("element locking", () => {
y: 200,
});
h.elements = [rectangle, lockedRectangle];
API.setElements([rectangle, lockedRectangle]);
mouse.clickAt(250, 250);
expect(API.getSelectedElements().length).toBe(0);
@@ -228,7 +229,7 @@ describe("element locking", () => {
containerId: container.id,
locked: true,
});
h.elements = [container, text];
API.setElements([container, text]);
API.setSelectedElements([container]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).not.toBe(text.id);
@@ -245,7 +246,7 @@ describe("element locking", () => {
height: 100,
locked: true,
});
h.elements = [text];
API.setElements([text]);
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
@@ -267,7 +268,7 @@ describe("element locking", () => {
height: 100,
locked: true,
});
h.elements = [text];
API.setElements([text]);
UI.clickTool("selection");
mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
@@ -298,7 +299,7 @@ describe("element locking", () => {
boundElements: [{ id: text.id, type: "text" }],
});
h.elements = [container, text];
API.setElements([container, text]);
UI.clickTool("selection");
mouse.clickAt(container.x + 10, container.y + 10);
@@ -338,7 +339,7 @@ describe("element locking", () => {
mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }],
});
h.elements = [container, text];
API.setElements([container, text]);
UI.clickTool("selection");
mouse.doubleClickAt(container.width / 2, container.height / 2);
@@ -372,7 +373,7 @@ describe("element locking", () => {
mutateElement(container, {
boundElements: [{ id: text.id, type: "text" }],
});
h.elements = [container, text];
API.setElements([container, text]);
UI.clickTool("text");
mouse.clickAt(container.width / 2, container.height / 2);
@@ -1,3 +1,4 @@
import React from "react";
import { fireEvent, GlobalTestState, toggleMenu, render } from "./test-utils";
import { Excalidraw, Footer, MainMenu } from "../index";
import { queryByText, queryByTestId } from "@testing-library/react";
+6 -5
View File
@@ -1,3 +1,4 @@
import React from "react";
import { render, waitFor } from "./test-utils";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
@@ -51,7 +52,7 @@ describe("export", () => {
blob: pngBlob,
metadata: serializeAsJSON(testElements, h.state, {}, "local"),
});
API.drop(pngBlobEmbedded);
await API.drop(pngBlobEmbedded);
await waitFor(() => {
expect(h.elements).toEqual([
@@ -71,7 +72,7 @@ describe("export", () => {
});
it("import embedded png (legacy v1)", async () => {
API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
await waitFor(() => {
expect(h.elements).toEqual([
expect.objectContaining({ type: "text", text: "test" }),
@@ -80,7 +81,7 @@ describe("export", () => {
});
it("import embedded png (v2)", async () => {
API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
await waitFor(() => {
expect(h.elements).toEqual([
expect.objectContaining({ type: "text", text: "😀" }),
@@ -89,7 +90,7 @@ describe("export", () => {
});
it("import embedded svg (legacy v1)", async () => {
API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
await waitFor(() => {
expect(h.elements).toEqual([
expect.objectContaining({ type: "text", text: "test" }),
@@ -98,7 +99,7 @@ describe("export", () => {
});
it("import embedded svg (v2)", async () => {
API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
await waitFor(() => {
expect(h.elements).toEqual([
expect.objectContaining({ type: "text", text: "😀" }),
+29 -15
View File
@@ -1,4 +1,5 @@
import { render } from "./test-utils";
import React from "react";
import { act, render } from "./test-utils";
import { API } from "./helpers/api";
import { Excalidraw } from "../index";
@@ -6,6 +7,17 @@ import { vi } from "vitest";
const { h } = window;
const waitForNextAnimationFrame = () => {
return act(
() =>
new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
}),
);
};
describe("fitToContent", () => {
it("should zoom to fit the selected element", async () => {
await render(<Excalidraw />);
@@ -22,7 +34,9 @@ describe("fitToContent", () => {
expect(h.state.zoom.value).toBe(1);
h.app.scrollToContent(rectElement, { fitToContent: true });
act(() => {
h.app.scrollToContent(rectElement, { fitToContent: true });
});
// element is 10x taller than the viewport size,
// zoom should be at least 1/10
@@ -51,8 +65,10 @@ describe("fitToContent", () => {
expect(h.state.zoom.value).toBe(1);
h.app.scrollToContent([topLeft, bottomRight], {
fitToContent: true,
act(() => {
h.app.scrollToContent([topLeft, bottomRight], {
fitToContent: true,
});
});
// elements take 100x100, which is 10x bigger than the viewport size,
@@ -77,7 +93,9 @@ describe("fitToContent", () => {
expect(h.state.scrollX).toBe(0);
expect(h.state.scrollY).toBe(0);
h.app.scrollToContent(rectElement);
act(() => {
h.app.scrollToContent(rectElement);
});
// zoom level should stay the same
expect(h.state.zoom.value).toBe(1);
@@ -88,14 +106,6 @@ describe("fitToContent", () => {
});
});
const waitForNextAnimationFrame = () => {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
});
};
describe("fitToContent animated", () => {
beforeEach(() => {
vi.spyOn(window, "requestAnimationFrame");
@@ -118,7 +128,9 @@ describe("fitToContent animated", () => {
y: -100,
});
h.app.scrollToContent(rectElement, { animate: true });
act(() => {
h.app.scrollToContent(rectElement, { animate: true });
});
expect(window.requestAnimationFrame).toHaveBeenCalled();
@@ -157,7 +169,9 @@ describe("fitToContent animated", () => {
expect(h.state.scrollX).toBe(0);
expect(h.state.scrollY).toBe(0);
h.app.scrollToContent(rectElement, { animate: true, fitToContent: true });
act(() => {
h.app.scrollToContent(rectElement, { animate: true, fitToContent: true });
});
expect(window.requestAnimationFrame).toHaveBeenCalled();
+100 -87
View File
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import {
fireEvent,
@@ -19,7 +20,6 @@ import type {
} from "../element/types";
import { newLinearElement } from "../element";
import { Excalidraw } from "../index";
import { mutateElement } from "../element/mutateElement";
import type { NormalizedZoomValue } from "../types";
import { ROUNDNESS } from "../constants";
import { vi } from "vitest";
@@ -54,7 +54,7 @@ beforeEach(async () => {
elementFromPoint: () => GlobalTestState.canvas,
});
await render(<Excalidraw autoFocus={true} handleKeyboardGlobally={true} />);
h.setState({
API.setAppState({
zoom: {
value: 1 as NormalizedZoomValue,
},
@@ -204,14 +204,14 @@ const checkElementsBoundingBox = async (
const checkHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
const originalElement = cloneJSON(h.elements[0]);
h.app.actionManager.executeAction(actionFlipHorizontal);
API.executeAction(actionFlipHorizontal);
const newElement = h.elements[0];
await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
};
const checkTwoPointsLineHorizontalFlip = async () => {
const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
h.app.actionManager.executeAction(actionFlipHorizontal);
API.executeAction(actionFlipHorizontal);
const newElement = h.elements[0] as ExcalidrawLinearElement;
await waitFor(() => {
expect(originalElement.points[0][0]).toBeCloseTo(
@@ -235,7 +235,7 @@ const checkTwoPointsLineHorizontalFlip = async () => {
const checkTwoPointsLineVerticalFlip = async () => {
const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
h.app.actionManager.executeAction(actionFlipVertical);
API.executeAction(actionFlipVertical);
const newElement = h.elements[0] as ExcalidrawLinearElement;
await waitFor(() => {
expect(originalElement.points[0][0]).toBeCloseTo(
@@ -262,7 +262,7 @@ const checkRotatedHorizontalFlip = async (
toleranceInPx: number = 0.00001,
) => {
const originalElement = cloneJSON(h.elements[0]);
h.app.actionManager.executeAction(actionFlipHorizontal);
API.executeAction(actionFlipHorizontal);
const newElement = h.elements[0];
await waitFor(() => {
expect(newElement.angle).toBeCloseTo(expectedAngle);
@@ -275,7 +275,7 @@ const checkRotatedVerticalFlip = async (
toleranceInPx: number = 0.00001,
) => {
const originalElement = cloneJSON(h.elements[0]);
h.app.actionManager.executeAction(actionFlipVertical);
API.executeAction(actionFlipVertical);
const newElement = h.elements[0];
await waitFor(() => {
expect(newElement.angle).toBeCloseTo(expectedAngle);
@@ -286,7 +286,7 @@ const checkRotatedVerticalFlip = async (
const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => {
const originalElement = cloneJSON(h.elements[0]);
h.app.actionManager.executeAction(actionFlipVertical);
API.executeAction(actionFlipVertical);
const newElement = h.elements[0];
await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
@@ -295,8 +295,8 @@ const checkVerticalFlip = async (toleranceInPx: number = 0.00001) => {
const checkVerticalHorizontalFlip = async (toleranceInPx: number = 0.00001) => {
const originalElement = cloneJSON(h.elements[0]);
h.app.actionManager.executeAction(actionFlipHorizontal);
h.app.actionManager.executeAction(actionFlipVertical);
API.executeAction(actionFlipHorizontal);
API.executeAction(actionFlipVertical);
const newElement = h.elements[0];
await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
@@ -309,7 +309,6 @@ const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20;
describe("rectangle", () => {
it("flips an unrotated rectangle horizontally correctly", async () => {
createAndSelectOneRectangle();
await checkHorizontalFlip();
});
@@ -408,8 +407,8 @@ describe("ellipse", () => {
describe("arrow", () => {
it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => {
const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.elements = [arrow];
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
API.setElements([arrow]);
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
await checkHorizontalFlip(
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
);
@@ -417,8 +416,8 @@ describe("arrow", () => {
it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => {
const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.elements = [arrow];
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
API.setElements([arrow]);
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
await checkVerticalFlip(50);
});
@@ -427,12 +426,14 @@ describe("arrow", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.elements = [line];
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
mutateElement(line, {
API.setElements([line]);
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[line.id]: true,
},
});
API.updateElement(line, {
angle: originalAngle,
});
@@ -446,12 +447,14 @@ describe("arrow", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.elements = [line];
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
mutateElement(line, {
API.setElements([line]);
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[line.id]: true,
},
});
API.updateElement(line, {
angle: originalAngle,
});
@@ -464,8 +467,8 @@ describe("arrow", () => {
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
it.skip("flips an unrotated arrow horizontally with line outside min/max points bounds", async () => {
const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
h.elements = [arrow];
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
API.setElements([arrow]);
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
await checkHorizontalFlip(
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
@@ -477,9 +480,9 @@ describe("arrow", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
mutateElement(line, { angle: originalAngle });
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.updateElement(line, { angle: originalAngle });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkRotatedVerticalFlip(
expectedAngle,
@@ -490,8 +493,8 @@ describe("arrow", () => {
//TODO: elements with curve outside minMax points have a wrong bounding box!!!
it.skip("flips an unrotated arrow vertically with line outside min/max points bounds", async () => {
const arrow = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
h.elements = [arrow];
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
API.setElements([arrow]);
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
});
@@ -501,9 +504,9 @@ describe("arrow", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
mutateElement(line, { angle: originalAngle });
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.updateElement(line, { angle: originalAngle });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkRotatedVerticalFlip(
expectedAngle,
@@ -538,8 +541,8 @@ describe("arrow", () => {
describe("line", () => {
it("flips an unrotated line horizontally with line inside min/max points bounds", async () => {
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkHorizontalFlip(
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
@@ -548,8 +551,8 @@ describe("line", () => {
it("flips an unrotated line vertically with line inside min/max points bounds", async () => {
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
});
@@ -563,8 +566,8 @@ describe("line", () => {
//TODO: elements with curve outside minMax points have a wrong bounding box
it.skip("flips an unrotated line horizontally with line outside min/max points bounds", async () => {
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkHorizontalFlip(
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
@@ -574,8 +577,8 @@ describe("line", () => {
//TODO: elements with curve outside minMax points have a wrong bounding box
it.skip("flips an unrotated line vertically with line outside min/max points bounds", async () => {
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
});
@@ -585,9 +588,9 @@ describe("line", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
mutateElement(line, { angle: originalAngle });
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.updateElement(line, { angle: originalAngle });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkRotatedHorizontalFlip(
expectedAngle,
@@ -600,9 +603,9 @@ describe("line", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
mutateElement(line, { angle: originalAngle });
h.elements = [line];
h.app.setState({ selectedElementIds: { [line.id]: true } });
API.updateElement(line, { angle: originalAngle });
API.setElements([line]);
API.setAppState({ selectedElementIds: { [line.id]: true } });
await checkRotatedVerticalFlip(
expectedAngle,
@@ -619,12 +622,14 @@ describe("line", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.elements = [line];
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
mutateElement(line, {
API.setElements([line]);
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[line.id]: true,
},
});
API.updateElement(line, {
angle: originalAngle,
});
@@ -638,12 +643,14 @@ describe("line", () => {
const originalAngle = Math.PI / 4;
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.elements = [line];
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
mutateElement(line, {
API.setElements([line]);
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[line.id]: true,
},
});
API.updateElement(line, {
angle: originalAngle,
});
@@ -669,20 +676,24 @@ describe("freedraw", () => {
it("flips an unrotated drawing horizontally correctly", async () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[draw.id]: true,
},
});
await checkHorizontalFlip();
});
it("flips an unrotated drawing vertically correctly", async () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[draw.id]: true,
},
});
await checkVerticalFlip();
});
@@ -692,10 +703,12 @@ describe("freedraw", () => {
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[draw.id]: true,
},
});
await checkRotatedHorizontalFlip(expectedAngle);
});
@@ -706,10 +719,12 @@ describe("freedraw", () => {
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
API.setAppState({
selectedElementIds: {
...h.state.selectedElementIds,
[draw.id]: true,
},
});
await checkRotatedVerticalFlip(expectedAngle);
});
@@ -767,7 +782,7 @@ describe("image", () => {
expect(API.getSelectedElements()[0].type).toEqual("image");
expect(h.app.files.fileId).toBeDefined();
});
mutateElement(h.elements[0], {
API.updateElement(h.elements[0], {
angle: originalAngle,
});
await checkRotatedHorizontalFlip(expectedAngle);
@@ -786,7 +801,7 @@ describe("image", () => {
expect(API.getSelectedElements()[0].type).toEqual("image");
expect(h.app.files.fileId).toBeDefined();
});
mutateElement(h.elements[0], {
API.updateElement(h.elements[0], {
angle: originalAngle,
});
@@ -827,8 +842,7 @@ describe("mutliple elements", () => {
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.input(editor, { target: { value: "arrow" } });
await new Promise((resolve) => setTimeout(resolve, 0));
Keyboard.keyPress(KEYS.ESCAPE);
Keyboard.exitTextEditor(editor);
const rectangle = UI.createElement("rectangle", {
x: 0,
@@ -842,12 +856,11 @@ describe("mutliple elements", () => {
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.input(editor, { target: { value: "rect\ntext" } });
await new Promise((resolve) => setTimeout(resolve, 0));
Keyboard.keyPress(KEYS.ESCAPE);
Keyboard.exitTextEditor(editor);
mouse.select([arrow, rectangle]);
h.app.actionManager.executeAction(actionFlipHorizontal);
h.app.actionManager.executeAction(actionFlipVertical);
API.executeAction(actionFlipHorizontal);
API.executeAction(actionFlipVertical);
const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
const arrowTextPos = getBoundTextElementPosition(
+51 -10
View File
@@ -13,7 +13,7 @@ import type {
import { newElement, newTextElement, newLinearElement } from "../../element";
import { DEFAULT_VERTICAL_ALIGN, ROUNDNESS } from "../../constants";
import { getDefaultAppState } from "../../appState";
import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
import { GlobalTestState, createEvent, fireEvent, act } from "../test-utils";
import fs from "fs";
import util from "util";
import path from "path";
@@ -27,12 +27,15 @@ import {
newImageElement,
newMagicFrameElement,
} from "../../element/newElement";
import type { Point } from "../../types";
import type { AppState, Point } from "../../types";
import { getSelectedElements } from "../../scene/selection";
import { isLinearElementType } from "../../element/typeChecks";
import type { Mutable } from "../../utility-types";
import { assertNever } from "../../utils";
import type App from "../../components/App";
import { createTestHook } from "../../components/App";
import type { Action } from "../../actions/types";
import { mutateElement } from "../../element/mutateElement";
const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well.
@@ -41,12 +44,42 @@ createTestHook();
const { h } = window;
export class API {
static updateScene: InstanceType<typeof App>["updateScene"] = (...args) => {
act(() => {
h.app.updateScene(...args);
});
};
static setAppState: React.Component<any, AppState>["setState"] = (
state,
cb,
) => {
act(() => {
h.setState(state, cb);
});
};
static setElements = (elements: readonly ExcalidrawElement[]) => {
act(() => {
h.elements = elements;
});
};
static setSelectedElements = (elements: ExcalidrawElement[]) => {
h.setState({
selectedElementIds: elements.reduce((acc, element) => {
acc[element.id] = true;
return acc;
}, {} as Record<ExcalidrawElement["id"], true>),
act(() => {
h.setState({
selectedElementIds: elements.reduce((acc, element) => {
acc[element.id] = true;
return acc;
}, {} as Record<ExcalidrawElement["id"], true>),
});
});
};
static updateElement = (
...[element, updates]: Parameters<typeof mutateElement>
) => {
act(() => {
mutateElement(element, updates);
});
};
@@ -85,8 +118,10 @@ export class API {
};
static clearSelection = () => {
// @ts-ignore
h.app.clearSelection(null);
act(() => {
// @ts-ignore
h.app.clearSelection(null);
});
expect(API.getSelectedElements().length).toBe(0);
};
@@ -361,6 +396,12 @@ export class API {
},
},
});
fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
await fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
};
static executeAction = (action: Action) => {
act(() => {
h.app.actionManager.executeAction(action);
});
};
}
@@ -0,0 +1,2 @@
export const yellow = (str: string) => `\u001b[33m${str}\u001b[0m`;
export const red = (str: string) => `\u001b[31m${str}\u001b[0m`;
+29 -12
View File
@@ -20,7 +20,7 @@ import {
type TransformHandleDirection,
} from "../../element/transformHandles";
import { KEYS } from "../../keys";
import { fireEvent, GlobalTestState, screen } from "../test-utils";
import { act, fireEvent, GlobalTestState, screen } from "../test-utils";
import { mutateElement } from "../../element/mutateElement";
import { API } from "./api";
import {
@@ -125,6 +125,10 @@ export class Keyboard {
Keyboard.keyPress("z");
});
};
static exitTextEditor = (textarea: HTMLTextAreaElement) => {
fireEvent.keyDown(textarea, { key: KEYS.ESCAPE });
};
}
const getElementPointForSelection = (element: ExcalidrawElement): Point => {
@@ -299,14 +303,16 @@ const transform = (
keyboardModifiers: KeyboardModifiers = {},
) => {
const elements = Array.isArray(element) ? element : [element];
h.setState({
selectedElementIds: elements.reduce(
(acc, e) => ({
...acc,
[e.id]: true,
}),
{},
),
act(() => {
h.setState({
selectedElementIds: elements.reduce(
(acc, e) => ({
...acc,
[e.id]: true,
}),
{},
),
});
});
let handleCoords: TransformHandle | undefined;
if (elements.length === 1) {
@@ -487,7 +493,9 @@ export class UI {
const origElement = h.elements[h.elements.length - 1] as any;
if (angle !== 0) {
mutateElement(origElement, { angle });
act(() => {
mutateElement(origElement, { angle });
});
}
return proxy(origElement);
@@ -511,8 +519,9 @@ export class UI {
}
fireEvent.input(editor, { target: { value: text } });
await new Promise((resolve) => setTimeout(resolve, 0));
editor.blur();
act(() => {
editor.blur();
});
return isTextElement(element)
? element
@@ -523,6 +532,14 @@ export class UI {
);
}
static updateInput = (input: HTMLInputElement, value: string | number) => {
act(() => {
input.focus();
fireEvent.change(input, { target: { value: String(value) } });
input.blur();
});
};
static resize(
element: ExcalidrawElement | ExcalidrawElement[],
handle: TransformHandleDirection,
+111 -140
View File
@@ -1,5 +1,5 @@
import "../global.d.ts";
import React from "react";
import "../global.d.ts";
import * as StaticScene from "../renderer/staticScene";
import {
GlobalTestState,
@@ -16,8 +16,8 @@ import { fireEvent, queryByTestId, waitFor } from "@testing-library/react";
import { createUndoAction, createRedoAction } from "../actions/actionHistory";
import { actionToggleViewMode } from "../actions/actionToggleViewMode";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import type { AppState, ExcalidrawImperativeAPI } from "../types";
import { arrayToMap, resolvablePromise } from "../utils";
import type { AppState } from "../types";
import { arrayToMap } from "../utils";
import {
COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
@@ -44,7 +44,6 @@ import { queryByText } from "@testing-library/react";
import { HistoryEntry } from "../history";
import { AppStateChange, ElementsChange } from "../change";
import { Snapshot, StoreAction } from "../store";
import type Scene from "../scene/Scene";
const { h } = window;
@@ -95,7 +94,7 @@ describe("history", () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
const rect = API.createElement({ type: "rectangle" });
h.elements = [rect];
API.setElements([rect]);
const corrupedEntry = HistoryEntry.create(
AppStateChange.empty(),
@@ -118,7 +117,6 @@ describe("history", () => {
arrayToMap(h.elements) as SceneElementsMap,
appState,
Snapshot.empty(),
{} as Scene,
) as any,
);
} catch (e) {
@@ -140,7 +138,6 @@ describe("history", () => {
arrayToMap(h.elements) as SceneElementsMap,
appState,
Snapshot.empty(),
{} as Scene,
) as any,
);
} catch (e) {
@@ -158,7 +155,7 @@ describe("history", () => {
const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] });
const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] });
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
mouse.select(rect1);
assertSelectedElements([rect1, rect2]);
expect(h.state.selectedGroupIds).toEqual({ A: true });
@@ -173,19 +170,12 @@ describe("history", () => {
});
it("should not end up with history entry when there are no elements changes", async () => {
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
await render(
<Excalidraw
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
handleKeyboardGlobally={true}
/>,
);
const excalidrawAPI = await excalidrawAPIPromise;
await render(<Excalidraw handleKeyboardGlobally={true} />);
const rect1 = API.createElement({ type: "rectangle" });
const rect2 = API.createElement({ type: "rectangle" });
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect1, rect2],
storeAction: StoreAction.CAPTURE,
});
@@ -197,7 +187,7 @@ describe("history", () => {
expect.objectContaining({ id: rect2.id, isDeleted: false }),
]);
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect1, rect2],
storeAction: StoreAction.CAPTURE, // even though the flag is on, same elements are passed, nothing to commit
});
@@ -447,7 +437,7 @@ describe("history", () => {
const undoAction = createUndoAction(h.history, h.store);
const redoAction = createRedoAction(h.history, h.store);
// noop
act(() => h.app.actionManager.executeAction(undoAction));
API.executeAction(undoAction);
expect(h.elements).toEqual([
expect.objectContaining({ id: "A", isDeleted: false }),
]);
@@ -456,21 +446,21 @@ describe("history", () => {
expect.objectContaining({ id: "A" }),
expect.objectContaining({ id: rectangle.id }),
]);
act(() => h.app.actionManager.executeAction(undoAction));
API.executeAction(undoAction);
expect(h.elements).toEqual([
expect.objectContaining({ id: "A", isDeleted: false }),
expect.objectContaining({ id: rectangle.id, isDeleted: true }),
]);
// noop
act(() => h.app.actionManager.executeAction(undoAction));
API.executeAction(undoAction);
expect(h.elements).toEqual([
expect.objectContaining({ id: "A", isDeleted: false }),
expect.objectContaining({ id: rectangle.id, isDeleted: true }),
]);
expect(API.getUndoStack().length).toBe(0);
act(() => h.app.actionManager.executeAction(redoAction));
API.executeAction(redoAction);
expect(h.elements).toEqual([
expect.objectContaining({ id: "A", isDeleted: false }),
expect.objectContaining({ id: rectangle.id, isDeleted: false }),
@@ -495,7 +485,7 @@ describe("history", () => {
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
);
API.drop(
await API.drop(
new Blob(
[
JSON.stringify({
@@ -523,7 +513,7 @@ describe("history", () => {
const undoAction = createUndoAction(h.history, h.store);
const redoAction = createRedoAction(h.history, h.store);
act(() => h.app.actionManager.executeAction(undoAction));
API.executeAction(undoAction);
expect(API.getSnapshot()).toEqual([
expect.objectContaining({ id: "A", isDeleted: false }),
@@ -535,7 +525,7 @@ describe("history", () => {
]);
expect(h.state.viewBackgroundColor).toBe("#FFF");
act(() => h.app.actionManager.executeAction(redoAction));
API.executeAction(redoAction);
expect(h.state.viewBackgroundColor).toBe("#000");
expect(API.getSnapshot()).toEqual([
expect.objectContaining({ id: "A", isDeleted: true }),
@@ -548,10 +538,8 @@ describe("history", () => {
});
it("should support appstate name or viewBackgroundColor change", async () => {
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
await render(
<Excalidraw
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
handleKeyboardGlobally={true}
initialData={{
appState: {
@@ -561,9 +549,11 @@ describe("history", () => {
}}
/>,
);
const excalidrawAPI = await excalidrawAPIPromise;
excalidrawAPI.updateScene({
expect(h.state.isLoading).toBe(false);
expect(h.state.name).toBe("Old name");
API.updateScene({
appState: {
name: "New name",
},
@@ -574,7 +564,7 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(0);
expect(h.state.name).toBe("New name");
excalidrawAPI.updateScene({
API.updateScene({
appState: {
viewBackgroundColor: "#000",
},
@@ -586,7 +576,7 @@ describe("history", () => {
expect(h.state.viewBackgroundColor).toBe("#000");
// just to double check that same change is not recorded
excalidrawAPI.updateScene({
API.updateScene({
appState: {
name: "New name",
viewBackgroundColor: "#000",
@@ -1060,7 +1050,7 @@ describe("history", () => {
x: 100,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
mouse.select(rect1);
assertSelectedElements([rect1, rect2]);
expect(API.getUndoStack().length).toBe(1);
@@ -1203,7 +1193,7 @@ describe("history", () => {
const rect2 = UI.createElement("rectangle", { x: 20, y: 20 });
const rect3 = UI.createElement("rectangle", { x: 40, y: 40 });
act(() => h.app.actionManager.executeAction(actionSendBackward));
API.executeAction(actionSendBackward);
expect(API.getUndoStack().length).toBe(4);
expect(API.getRedoStack().length).toBe(0);
@@ -1234,7 +1224,7 @@ describe("history", () => {
expect(API.getRedoStack().length).toBe(0);
assertSelectedElements([rect1, rect3]);
act(() => h.app.actionManager.executeAction(actionBringForward));
API.executeAction(actionBringForward);
expect(API.getUndoStack().length).toBe(7);
expect(API.getRedoStack().length).toBe(0);
@@ -1262,8 +1252,6 @@ describe("history", () => {
});
describe("should support bidirectional bindings", async () => {
let excalidrawAPI: ExcalidrawImperativeAPI;
let rect1: ExcalidrawGenericElement;
let rect2: ExcalidrawGenericElement;
let text: ExcalidrawTextElement;
@@ -1292,22 +1280,13 @@ describe("history", () => {
} as const;
beforeEach(async () => {
const excalidrawAPIPromise =
resolvablePromise<ExcalidrawImperativeAPI>();
await render(
<Excalidraw
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
handleKeyboardGlobally={true}
/>,
);
excalidrawAPI = await excalidrawAPIPromise;
await render(<Excalidraw handleKeyboardGlobally={true} />);
rect1 = API.createElement({ ...rect1Props });
text = API.createElement({ ...textProps });
rect2 = API.createElement({ ...rect2Props });
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect1, text, rect2],
storeAction: StoreAction.CAPTURE,
});
@@ -1758,14 +1737,14 @@ describe("history", () => {
expect(undoButton).not.toBeDisabled();
expect(redoButton).toBeDisabled();
act(() => h.app.actionManager.executeAction(undoAction));
API.executeAction(undoAction);
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeFalsy();
expect(undoButton).toBeDisabled();
expect(redoButton).not.toBeDisabled();
act(() => h.app.actionManager.executeAction(redoAction));
API.executeAction(redoAction);
expect(h.history.isUndoStackEmpty).toBeFalsy();
expect(h.history.isRedoStackEmpty).toBeTruthy();
@@ -1807,13 +1786,13 @@ describe("history", () => {
expect(queryByTestId(container, "button-undo")).not.toBeDisabled();
expect(queryByTestId(container, "button-redo")).toBeDisabled();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
API.executeAction(actionToggleViewMode);
expect(h.state.viewModeEnabled).toBe(true);
expect(queryByTestId(container, "button-undo")).toBeNull();
expect(queryByTestId(container, "button-redo")).toBeNull();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
API.executeAction(actionToggleViewMode);
expect(h.state.viewModeEnabled).toBe(false);
await waitFor(() => {
@@ -1824,20 +1803,20 @@ describe("history", () => {
// testing redo button
// -----------------------------------------------------------------------
act(() => h.app.actionManager.executeAction(undoAction));
API.executeAction(undoAction);
expect(h.history.isUndoStackEmpty).toBeTruthy();
expect(h.history.isRedoStackEmpty).toBeFalsy();
expect(queryByTestId(container, "button-undo")).toBeDisabled();
expect(queryByTestId(container, "button-redo")).not.toBeDisabled();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
API.executeAction(actionToggleViewMode);
expect(h.state.viewModeEnabled).toBe(true);
expect(queryByTestId(container, "button-undo")).toBeNull();
expect(queryByTestId(container, "button-redo")).toBeNull();
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
API.executeAction(actionToggleViewMode);
expect(h.state.viewModeEnabled).toBe(false);
expect(h.history.isUndoStackEmpty).toBeTruthy();
@@ -1848,8 +1827,6 @@ describe("history", () => {
});
describe("multiplayer undo/redo", () => {
let excalidrawAPI: ExcalidrawImperativeAPI;
// Util to check that we end up in the same state after series of undo / redo
function runTwice(callback: () => void) {
for (let i = 0; i < 2; i++) {
@@ -1858,15 +1835,9 @@ describe("history", () => {
}
beforeEach(async () => {
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
await render(
<Excalidraw
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
handleKeyboardGlobally={true}
isCollaborating={true}
/>,
<Excalidraw handleKeyboardGlobally={true} isCollaborating={true} />,
);
excalidrawAPI = await excalidrawAPIPromise;
});
it("should not override remote changes on different elements", async () => {
@@ -1881,7 +1852,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
...h.elements,
API.createElement({
@@ -1921,7 +1892,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(2);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
strokeColor: yellow,
@@ -1969,7 +1940,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
backgroundColor: yellow,
@@ -1985,7 +1956,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
backgroundColor: violet,
@@ -2034,13 +2005,13 @@ describe("history", () => {
elbowed: true,
});
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect, diamond],
storeAction: StoreAction.CAPTURE,
});
// Connect the arrow
excalidrawAPI.updateScene({
API.updateScene({
elements: [
{
...rect,
@@ -2090,7 +2061,7 @@ describe("history", () => {
Keyboard.undo();
excalidrawAPI.updateScene({
API.updateScene({
elements: h.elements.map((el) =>
el.id === "KPrBI4g_v9qUB1XxYLgSz"
? {
@@ -2122,13 +2093,13 @@ describe("history", () => {
const rect2 = API.createElement({ type: "rectangle" });
// Initialize scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect1, rect2],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], { groupIds: ["A"] }),
newElementWith(h.elements[1], { groupIds: ["A"] }),
@@ -2140,7 +2111,7 @@ describe("history", () => {
const rect4 = API.createElement({ type: "rectangle", groupIds: ["B"] });
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], { groupIds: ["A", "B"] }),
newElementWith(h.elements[1], { groupIds: ["A", "B"] }),
@@ -2181,7 +2152,7 @@ describe("history", () => {
Keyboard.keyPress(KEYS.ENTER);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
points: [
@@ -2281,7 +2252,7 @@ describe("history", () => {
]);
// Simulate remote update & restore
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
backgroundColor: yellow,
@@ -2358,7 +2329,7 @@ describe("history", () => {
]);
// Simulate remote update & deletion
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
backgroundColor: yellow,
@@ -2417,7 +2388,7 @@ describe("history", () => {
expect(API.getUndoStack().length).toBe(5);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(h.elements[1], {
@@ -2482,7 +2453,7 @@ describe("history", () => {
const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 });
const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 });
h.elements = [rect1, rect2, rect3];
API.setElements([rect1, rect2, rect3]);
mouse.select(rect1);
mouse.select([rect2, rect3]);
@@ -2493,7 +2464,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(h.elements[1], {
@@ -2532,7 +2503,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(h.elements[1], {
@@ -2586,7 +2557,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect1, rect2],
storeAction: StoreAction.UPDATE,
});
@@ -2596,7 +2567,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [h.elements[0], h.elements[1], rect3, rect4],
storeAction: StoreAction.UPDATE,
});
@@ -2610,7 +2581,7 @@ describe("history", () => {
expect(h.state.selectedGroupIds).toEqual({ A: true, B: true });
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
isDeleted: true,
@@ -2635,7 +2606,7 @@ describe("history", () => {
Keyboard.undo();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
isDeleted: false,
@@ -2653,7 +2624,7 @@ describe("history", () => {
expect(h.state.selectedGroupIds).toEqual({ A: true });
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [h.elements[0], h.elements[1], rect3, rect4],
storeAction: StoreAction.UPDATE,
});
@@ -2676,7 +2647,7 @@ describe("history", () => {
x: 100,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
mouse.select(rect1);
// inside the editing group
@@ -2692,7 +2663,7 @@ describe("history", () => {
expect(h.state.editingGroupId).toBeNull();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
isDeleted: true,
@@ -2715,7 +2686,7 @@ describe("history", () => {
expect(h.state.editingGroupId).toBeNull();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
isDeleted: false,
@@ -2759,7 +2730,7 @@ describe("history", () => {
expect(h.state.selectedLinearElement).toBeNull();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
isDeleted: true,
@@ -2786,11 +2757,11 @@ describe("history", () => {
const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 }); // b "a1"
const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 }); // c "a2"
h.elements = [rect1, rect2, rect3];
API.setElements([rect1, rect2, rect3]);
mouse.select(rect2);
act(() => h.app.actionManager.executeAction(actionSendToBack));
API.executeAction(actionSendToBack);
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(0);
@@ -2802,7 +2773,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[2], { index: "Zy" as FractionalIndex }),
h.elements[0],
@@ -2841,7 +2812,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[2], { index: "Zx" as FractionalIndex }),
h.elements[0],
@@ -2876,11 +2847,11 @@ describe("history", () => {
const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 });
const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 });
h.elements = [rect1, rect2, rect3];
API.setElements([rect1, rect2, rect3]);
mouse.select(rect2);
act(() => h.app.actionManager.executeAction(actionSendToBack));
API.executeAction(actionSendToBack);
expect(API.getUndoStack().length).toBe(2);
expect(API.getRedoStack().length).toBe(0);
@@ -2892,7 +2863,7 @@ describe("history", () => {
]);
// Simulate remote update (fixes all invalid z-indices)
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[2], // rect3
h.elements[0], // rect2
@@ -2922,7 +2893,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[1], // rect2
h.elements[0], // rect3
@@ -2956,7 +2927,7 @@ describe("history", () => {
const rect = API.createElement({ ...rectProps });
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [...h.elements, rect],
storeAction: StoreAction.UPDATE,
});
@@ -3008,7 +2979,7 @@ describe("history", () => {
const rect3 = API.createElement({ ...rect3Props });
// // Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [...h.elements, rect3],
storeAction: StoreAction.UPDATE,
});
@@ -3098,7 +3069,7 @@ describe("history", () => {
const rect3 = API.createElement({ ...rect3Props });
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [...h.elements, rect3],
storeAction: StoreAction.UPDATE,
});
@@ -3275,13 +3246,13 @@ describe("history", () => {
it("should rebind bindings when both are updated through the history and there no conflicting updates in the meantime", async () => {
// Initialize the scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [container, text],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -3310,7 +3281,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
// no conflicting updates
@@ -3362,13 +3333,13 @@ describe("history", () => {
// TODO: #7348 we do rebind now, when we have bi-directional binding in history, to eliminate potential data-integrity issues, but we should consider not rebinding in the future
it("should rebind bindings when both are updated through the history and the container got bound to a different text in the meantime", async () => {
// Initialize the scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [container, text],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -3403,7 +3374,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: remoteText.id, type: "text" }],
@@ -3465,13 +3436,13 @@ describe("history", () => {
// TODO: #7348 we do rebind now, when we have bi-directional binding in history, to eliminate potential data-integrity issues, but we should consider not rebinding in the future
it("should rebind bindings when both are updated through the history and the text got bound to a different container in the meantime", async () => {
// Initialize the scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [container, text],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -3507,7 +3478,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(remoteContainer, {
@@ -3573,13 +3544,13 @@ describe("history", () => {
it("should rebind remotely added bound text when it's container is added through the history", async () => {
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [container],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -3634,13 +3605,13 @@ describe("history", () => {
it("should rebind remotely added container when it's bound text is added through the history", async () => {
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [text],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(container, {
boundElements: [{ id: text.id, type: "text" }],
@@ -3694,13 +3665,13 @@ describe("history", () => {
it("should preserve latest remotely added binding and unbind previous one when the container is added through the history", async () => {
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [container],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -3736,7 +3707,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: remoteText.id, type: "text" }],
@@ -3801,13 +3772,13 @@ describe("history", () => {
it("should preserve latest remotely added binding and unbind previous one when the text is added through history", async () => {
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [text],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(container, {
boundElements: [{ id: text.id, type: "text" }],
@@ -3843,7 +3814,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: remoteText.id, type: "text" }],
@@ -3907,13 +3878,13 @@ describe("history", () => {
it("should unbind remotely deleted bound text from container when the container is added through the history", async () => {
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [container],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -3964,13 +3935,13 @@ describe("history", () => {
it("should unbind remotely deleted container from bound text when the text is added through the history", async () => {
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [text],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(container, {
boundElements: [{ id: text.id, type: "text" }],
@@ -4021,13 +3992,13 @@ describe("history", () => {
it("should redraw remotely added bound text when it's container is updated through the history", async () => {
// Initialize the scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [container],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
x: 200,
@@ -4041,7 +4012,7 @@ describe("history", () => {
Keyboard.undo();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
boundElements: [{ id: text.id, type: "text" }],
@@ -4139,13 +4110,13 @@ describe("history", () => {
// TODO: #7348 this leads to empty undo/redo and could be confusing - instead we might consider redrawing container based on the text dimensions
it("should redraw bound text to match container dimensions when the bound text is updated through the history", async () => {
// Initialize the scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [text],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
x: 205,
@@ -4159,7 +4130,7 @@ describe("history", () => {
Keyboard.undo();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(container, {
boundElements: [{ id: text.id, type: "text" }],
@@ -4257,7 +4228,7 @@ describe("history", () => {
rect2 = API.createElement({ ...rect2Props });
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect1, rect2],
storeAction: StoreAction.CAPTURE,
});
@@ -4333,7 +4304,7 @@ describe("history", () => {
]);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
// no conflicting updates
@@ -4478,7 +4449,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(h.elements[1], { boundElements: [] }),
@@ -4589,7 +4560,7 @@ describe("history", () => {
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
arrow,
newElementWith(h.elements[0], {
@@ -4674,13 +4645,13 @@ describe("history", () => {
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [arrow],
storeAction: StoreAction.CAPTURE,
});
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
startBinding: {
@@ -4833,7 +4804,7 @@ describe("history", () => {
);
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(h.elements[1], { x: 500, y: -500 }),
@@ -4909,19 +4880,19 @@ describe("history", () => {
it("should not rebind frame child with frame when frame was remotely deleted and frame child is added back through the history ", async () => {
// Initialize the scene
excalidrawAPI.updateScene({
API.updateScene({
elements: [frame],
storeAction: StoreAction.UPDATE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [rect, h.elements[0]],
storeAction: StoreAction.CAPTURE,
});
// Simulate local update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
newElementWith(h.elements[0], {
frameId: frame.id,
@@ -4965,7 +4936,7 @@ describe("history", () => {
Keyboard.undo();
// Simulate remote update
excalidrawAPI.updateScene({
API.updateScene({
elements: [
h.elements[0],
newElementWith(h.elements[1], {
+6 -3
View File
@@ -1,6 +1,7 @@
import React from "react";
import { vi } from "vitest";
import { fireEvent, render, waitFor } from "./test-utils";
import { queryByTestId } from "@testing-library/react";
import { act, queryByTestId } from "@testing-library/react";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
@@ -43,7 +44,9 @@ vi.mock("../data/filesystem.ts", async (importOriginal) => {
describe("library", () => {
beforeEach(async () => {
await render(<Excalidraw />);
h.app.library.resetLibrary();
await act(() => {
return h.app.library.resetLibrary();
});
});
it("import library via drag&drop", async () => {
@@ -208,7 +211,7 @@ describe("library menu", () => {
"dropdown-menu-button",
)!,
);
queryByTestId(container, "lib-dropdown--load")!.click();
fireEvent.click(queryByTestId(container, "lib-dropdown--load")!);
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
@@ -1,9 +1,11 @@
import React from "react";
import ReactDOM from "react-dom";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElementWithContainer,
FontString,
SceneElementsMap,
} from "../element/types";
import { Excalidraw, mutateElement } from "../index";
import { centerPoint } from "../math";
@@ -17,7 +19,7 @@ import { API } from "../tests/helpers/api";
import type { Point } from "../types";
import { KEYS } from "../keys";
import { LinearElementEditor } from "../element/linearElementEditor";
import { queryByTestId, queryByText } from "@testing-library/react";
import { act, queryByTestId, queryByText } from "@testing-library/react";
import {
getBoundTextElementPosition,
wrapText,
@@ -27,7 +29,6 @@ import * as textElementUtils from "../element/textElement";
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { vi } from "vitest";
import { arrayToMap } from "../utils";
import React from "react";
const renderInteractiveScene = vi.spyOn(
InteractiveCanvas,
@@ -80,7 +81,7 @@ describe("Test Linear Elements", () => {
],
roundness,
});
h.elements = [line];
API.setElements([line]);
mouse.clickAt(p1[0], p1[1]);
return line;
@@ -108,7 +109,7 @@ describe("Test Linear Elements", () => {
roundness,
});
mutateElement(line, { points: line.points });
h.elements = [line];
API.setElements([line]);
mouse.clickAt(p1[0], p1[1]);
return line;
};
@@ -786,7 +787,7 @@ describe("Test Linear Elements", () => {
it("in-editor dragging a line point covered by another element", () => {
createTwoPointerLinearElement("line");
const line = h.elements[0] as ExcalidrawLinearElement;
h.elements = [
API.setElements([
line,
API.createElement({
type: "rectangle",
@@ -797,7 +798,7 @@ describe("Test Linear Elements", () => {
backgroundColor: "red",
fillStyle: "solid",
}),
];
]);
const dragEndPositionOffset = [100, 100] as const;
API.setSelectedElements([line]);
enterLineEditingMode(line, true);
@@ -854,7 +855,7 @@ describe("Test Linear Elements", () => {
}
});
const updatedTextElement = { ...textElement, originalText: text };
h.elements = [...elements, updatedTextElement];
API.setElements([...elements, updatedTextElement]);
return { textElement: updatedTextElement, container };
};
@@ -968,17 +969,13 @@ describe("Test Linear Elements", () => {
target: { value: DEFAULT_TEXT },
});
await new Promise((r) => setTimeout(r, 0));
editor.blur();
Keyboard.exitTextEditor(editor);
expect(arrow.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
.toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).text,
).toMatchSnapshot();
});
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
@@ -998,21 +995,16 @@ describe("Test Linear Elements", () => {
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, {
target: { value: DEFAULT_TEXT },
});
editor.blur();
Keyboard.exitTextEditor(editor);
expect(arrow.boundElements).toStrictEqual([
{ id: textElement.id, type: "text" },
]);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
.toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).text,
).toMatchSnapshot();
});
it("should not bind text to line when double clicked", async () => {
@@ -1059,11 +1051,7 @@ describe("Test Linear Elements", () => {
"y": 60,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
expect(textElement.text).toMatchSnapshot();
expect(
LinearElementEditor.getElementAbsoluteCoords(
container,
@@ -1103,11 +1091,9 @@ describe("Test Linear Elements", () => {
"y": 45,
}
`);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
.toMatchInlineSnapshot(`
"Online whiteboard
collaboration made easy"
`);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).text,
).toMatchSnapshot();
expect(
LinearElementEditor.getElementAbsoluteCoords(
container,
@@ -1143,11 +1129,7 @@ describe("Test Linear Elements", () => {
"y": 10,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
expect(textElement.text).toMatchSnapshot();
const points = LinearElementEditor.getPointsGlobalCoordinates(
container,
elementsMap,
@@ -1171,10 +1153,7 @@ describe("Test Linear Elements", () => {
"y": -5,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made easy"
`);
expect(textElement.text).toMatchSnapshot();
});
it("should not render vertical align tool when element selected", () => {
@@ -1207,9 +1186,8 @@ describe("Test Linear Elements", () => {
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: DEFAULT_TEXT } });
editor.blur();
Keyboard.exitTextEditor(editor);
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
@@ -1223,10 +1201,7 @@ describe("Test Linear Elements", () => {
font,
getBoundTextMaxWidth(arrow, null),
),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made easy"
`);
).toMatchSnapshot();
const handleBindTextResizeSpy = vi.spyOn(
textElementUtils,
"handleBindTextResize",
@@ -1252,11 +1227,7 @@ describe("Test Linear Elements", () => {
font,
getBoundTextMaxWidth(arrow, null),
),
).toMatchInlineSnapshot(`
"Online whiteboard
collaboration made
easy"
`);
).toMatchSnapshot();
});
it("should not render horizontal align tool when element selected", () => {
@@ -1280,7 +1251,7 @@ describe("Test Linear Elements", () => {
expect(text.x).toBe(0);
expect(text.y).toBe(0);
h.elements = [h.elements[0], text];
API.setElements([h.elements[0], text]);
const container = h.elements[0];
API.setSelectedElements([container, text]);
@@ -1358,20 +1329,25 @@ describe("Test Linear Elements", () => {
const line = createThreePointerLinearElement("arrow");
const [origStartX, origStartY] = [line.x, line.y];
LinearElementEditor.movePoints(
line,
[
{ index: 0, point: [line.points[0][0] + 10, line.points[0][1] + 10] },
{
index: line.points.length - 1,
point: [
line.points[line.points.length - 1][0] - 10,
line.points[line.points.length - 1][1] - 10,
],
},
],
h.scene,
);
act(() => {
LinearElementEditor.movePoints(
line,
[
{
index: 0,
point: [line.points[0][0] + 10, line.points[0][1] + 10],
},
{
index: line.points.length - 1,
point: [
line.points[line.points.length - 1][0] - 10,
line.points[line.points.length - 1][1] - 10,
],
},
],
new Map() as SceneElementsMap,
);
});
expect(line.x).toBe(origStartX + 10);
expect(line.y).toBe(origStartY + 10);
+14 -11
View File
@@ -1,5 +1,6 @@
import React from "react";
import ReactDOM from "react-dom";
import { render, fireEvent } from "./test-utils";
import { render, fireEvent, act } from "./test-utils";
import { Excalidraw } from "../index";
import * as StaticScene from "../renderer/staticScene";
import * as InteractiveCanvas from "../renderer/interactiveScene";
@@ -80,22 +81,24 @@ describe("move element", () => {
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const arrow = UI.createElement("arrow", { x: 110, y: 50, size: 80 });
const elementsMap = h.app.scene.getNonDeletedElementsMap();
// bind line to two rectangles
bindOrUnbindLinearElement(
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement,
elementsMap,
{} as Scene,
);
act(() => {
// bind line to two rectangles
bindOrUnbindLinearElement(
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement,
elementsMap,
{} as Scene,
);
});
// select the second rectangle
new Pointer("mouse").clickOn(rectB);
expect(renderInteractiveScene.mock.calls.length).toMatchInlineSnapshot(
`20`,
`19`,
);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`17`);
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`16`);
expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(3);
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import {
render,
@@ -1,9 +1,11 @@
import React from "react";
import { vi } from "vitest";
import { Excalidraw, StoreAction } from "../../index";
import type { ExcalidrawImperativeAPI } from "../../types";
import { resolvablePromise } from "../../utils";
import { render } from "../test-utils";
import { Pointer } from "../helpers/ui";
import { API } from "../helpers/api";
describe("event callbacks", () => {
const h = window.h;
@@ -27,7 +29,7 @@ describe("event callbacks", () => {
const origBackgroundColor = h.state.viewBackgroundColor;
excalidrawAPI.onChange(onChange);
excalidrawAPI.updateScene({
API.updateScene({
appState: { viewBackgroundColor: "red" },
storeAction: StoreAction.CAPTURE,
});
+1 -1
View File
@@ -15,5 +15,5 @@ export const updateTextEditor = (
value: string,
) => {
fireEvent.change(editor, { target: { value } });
editor.dispatchEvent(new Event("input"));
fireEvent.input(editor);
};
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import type { ExcalidrawElement } from "../element/types";
import { CODES, KEYS } from "../keys";
@@ -55,7 +56,7 @@ beforeEach(async () => {
finger2.reset();
await render(<Excalidraw handleKeyboardGlobally={true} />);
h.setState({ height: 768, width: 1024 });
API.setAppState({ height: 768, width: 1024 });
});
afterEach(() => {
@@ -757,7 +758,7 @@ describe("regression tests", () => {
width: 500,
height: 500,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
mouse.select(rect1);
@@ -793,7 +794,7 @@ describe("regression tests", () => {
width: 500,
height: 500,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
mouse.select(rect1);
+8 -7
View File
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import { render } from "./test-utils";
import { reseed } from "../random";
@@ -537,7 +538,7 @@ describe("text element", () => {
describe("image element", () => {
it("resizes", async () => {
const image = API.createElement({ type: "image", width: 100, height: 100 });
h.elements = [image];
API.setElements([image]);
UI.resize(image, "ne", [-20, -30]);
expect(image.x).toBeCloseTo(0);
@@ -550,7 +551,7 @@ describe("image element", () => {
it("flips while resizing", async () => {
const image = API.createElement({ type: "image", width: 100, height: 100 });
h.elements = [image];
API.setElements([image]);
UI.resize(image, "sw", [150, -150]);
expect(image.x).toBeCloseTo(100);
@@ -563,7 +564,7 @@ describe("image element", () => {
it("resizes with locked/unlocked aspect ratio", async () => {
const image = API.createElement({ type: "image", width: 100, height: 100 });
h.elements = [image];
API.setElements([image]);
UI.resize(image, "ne", [30, -20]);
expect(image.x).toBeCloseTo(0);
@@ -581,7 +582,7 @@ describe("image element", () => {
it("resizes from center", async () => {
const image = API.createElement({ type: "image", width: 100, height: 100 });
h.elements = [image];
API.setElements([image]);
UI.resize(image, "nw", [25, 15], { alt: true });
expect(image.x).toBeCloseTo(15);
@@ -598,7 +599,7 @@ describe("image element", () => {
width: 100,
height: 100,
});
h.elements = [image];
API.setElements([image]);
const arrow = UI.createElement("arrow", {
x: -30,
y: 50,
@@ -971,7 +972,7 @@ describe("multiple selection", () => {
width: 120,
height: 80,
});
h.elements = [topImage, bottomImage];
API.setElements([topImage, bottomImage]);
const selectionWidth = 200;
const selectionHeight = 230;
@@ -1043,7 +1044,7 @@ describe("multiple selection", () => {
height: 100,
angle: (Math.PI * 7) / 6,
});
h.elements = [image];
API.setElements([image]);
const line = UI.createElement("line", {
x: 60,
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import { render } from "./test-utils";
import { reseed } from "../random";
@@ -88,7 +88,7 @@ describe("exportToSvg", () => {
);
expect(svgElement.getAttribute("filter")).toMatchInlineSnapshot(
'"_themeFilter_1883f3"',
`"_themeFilter_1883f3"`,
);
});
+4 -3
View File
@@ -1,3 +1,4 @@
import React from "react";
import {
mockBoundingClientRect,
render,
@@ -98,13 +99,13 @@ describe("appState", () => {
const zoom = h.state.zoom.value;
// Assert we scroll properly when zoomed in
h.setState({ zoom: { value: (zoom * 1.1) as typeof zoom } });
API.setAppState({ zoom: { value: (zoom * 1.1) as typeof zoom } });
scrollTest();
// Assert we scroll properly when zoomed out
h.setState({ zoom: { value: (zoom * 0.9) as typeof zoom } });
API.setAppState({ zoom: { value: (zoom * 0.9) as typeof zoom } });
scrollTest();
// Assert we scroll properly with normal zoom
h.setState({ zoom: { value: zoom } });
API.setAppState({ zoom: { value: zoom } });
scrollTest();
restoreOriginalGetBoundingClientRect();
});
+7 -6
View File
@@ -1,3 +1,4 @@
import React from "react";
import ReactDOM from "react-dom";
import {
render,
@@ -59,7 +60,7 @@ describe("box-selection", () => {
height: 50,
});
h.elements = [rect1, rect2];
API.setElements([rect1, rect2]);
mouse.downAt(175, -20);
mouse.moveTo(85, 70);
@@ -87,7 +88,7 @@ describe("box-selection", () => {
fillStyle: "solid",
});
h.elements = [rect1];
API.setElements([rect1]);
mouse.downAt(75, -20);
mouse.moveTo(-15, 70);
@@ -132,7 +133,7 @@ describe("inner box-selection", () => {
width: 50,
height: 50,
});
h.elements = [rect1, rect2, rect3];
API.setElements([rect1, rect2, rect3]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.downAt(40, 40);
mouse.moveTo(290, 290);
@@ -168,7 +169,7 @@ describe("inner box-selection", () => {
height: 50,
groupIds: ["A"],
});
h.elements = [rect1, rect2, rect3];
API.setElements([rect1, rect2, rect3]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.downAt(40, 40);
@@ -206,7 +207,7 @@ describe("inner box-selection", () => {
height: 50,
groupIds: ["A"],
});
h.elements = [rect1, rect2, rect3];
API.setElements([rect1, rect2, rect3]);
Keyboard.withModifierKeys({ ctrl: true }, () => {
mouse.downAt(rect2.x - 20, rect2.y - 20);
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
@@ -506,7 +507,7 @@ describe("selectedElementIds stability", () => {
height: 10,
});
h.elements = [rectangle];
API.setElements([rectangle]);
const selectedElementIds_1 = h.state.selectedElementIds;
@@ -1,3 +1,4 @@
import React from "react";
import { KEYS } from "../keys";
import { Excalidraw } from "../index";
import { API } from "./helpers/api";
+21 -16
View File
@@ -1,13 +1,12 @@
import "pepjs";
import type { RenderResult, RenderOptions } from "@testing-library/react";
import { act } from "@testing-library/react";
import { render, queries, waitFor, fireEvent } from "@testing-library/react";
import * as toolQueries from "./queries/toolQueries";
import type { ImportedDataState } from "../data/types";
import { STORAGE_KEYS } from "../../../excalidraw-app/app_constants";
import type { SceneData } from "../types";
import { getSelectedElements } from "../scene/selection";
import type { ExcalidrawElement } from "../element/types";
import { UI } from "./helpers/ui";
@@ -67,6 +66,12 @@ const renderApp: TestRenderFn = async (ui, options) => {
if (!interactiveCanvas) {
throw new Error("not initialized yet");
}
// hack-awaiting app.initialScene() which solves some test race conditions
// (later we may switch this with proper event listener)
if (window.h.state.isLoading) {
throw new Error("still loading");
}
});
return renderResult;
@@ -118,10 +123,6 @@ const initLocalStorage = (data: ImportedDataState) => {
}
};
export const updateSceneData = (data: SceneData) => {
(window.collab as any).excalidrawAPI.updateScene(data);
};
const originalGetBoundingClientRect =
global.window.HTMLDivElement.prototype.getBoundingClientRect;
@@ -166,20 +167,24 @@ export const withExcalidrawDimensions = async (
cb: () => void,
) => {
mockBoundingClientRect(dimensions);
// @ts-ignore
h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh();
act(() => {
// @ts-ignore
h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh();
});
await cb();
restoreOriginalGetBoundingClientRect();
// @ts-ignore
h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh();
act(() => {
// @ts-ignore
h.app.refreshViewportBreakpoints();
// @ts-ignore
h.app.refreshEditorBreakpoints();
window.h.app.refresh();
});
};
export const restoreOriginalGetBoundingClientRect = () => {
+11 -4
View File
@@ -1,7 +1,8 @@
import React from "react";
import { Excalidraw } from "../index";
import type { ExcalidrawImperativeAPI } from "../types";
import { resolvablePromise } from "../utils";
import { render } from "./test-utils";
import { act, render } from "./test-utils";
import { Pointer } from "./helpers/ui";
describe("setActiveTool()", () => {
@@ -28,7 +29,9 @@ describe("setActiveTool()", () => {
it("should set the active tool type", async () => {
expect(h.state.activeTool.type).toBe("selection");
excalidrawAPI.setActiveTool({ type: "rectangle" });
act(() => {
excalidrawAPI.setActiveTool({ type: "rectangle" });
});
expect(h.state.activeTool.type).toBe("rectangle");
mouse.down(10, 10);
@@ -39,7 +42,9 @@ describe("setActiveTool()", () => {
it("should support tool locking", async () => {
expect(h.state.activeTool.type).toBe("selection");
excalidrawAPI.setActiveTool({ type: "rectangle", locked: true });
act(() => {
excalidrawAPI.setActiveTool({ type: "rectangle", locked: true });
});
expect(h.state.activeTool.type).toBe("rectangle");
mouse.down(10, 10);
@@ -50,7 +55,9 @@ describe("setActiveTool()", () => {
it("should set custom tool", async () => {
expect(h.state.activeTool.type).toBe("selection");
excalidrawAPI.setActiveTool({ type: "custom", customType: "comment" });
act(() => {
excalidrawAPI.setActiveTool({ type: "custom", customType: "comment" });
});
expect(h.state.activeTool.type).toBe("custom");
expect(h.state.activeTool.customType).toBe("comment");
});
+5 -4
View File
@@ -1,10 +1,11 @@
import React from "react";
import { render, GlobalTestState } from "./test-utils";
import { Excalidraw } from "../index";
import { KEYS } from "../keys";
import { Keyboard, Pointer, UI } from "./helpers/ui";
import { CURSOR_TYPE } from "../constants";
import { API } from "./helpers/api";
const { h } = window;
const mouse = new Pointer("mouse");
const touch = new Pointer("touch");
const pen = new Pointer("pen");
@@ -16,14 +17,14 @@ describe("view mode", () => {
});
it("after switching to view mode cursor type should be pointer", async () => {
h.setState({ viewModeEnabled: true });
API.setAppState({ viewModeEnabled: true });
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
CURSOR_TYPE.GRAB,
);
});
it("after switching to view mode, moving, clicking, and pressing space key cursor type should be pointer", async () => {
h.setState({ viewModeEnabled: true });
API.setAppState({ viewModeEnabled: true });
pointerTypes.forEach((pointerType) => {
const pointer = pointerType;
@@ -58,7 +59,7 @@ describe("view mode", () => {
);
}
h.setState({ viewModeEnabled: true });
API.setAppState({ viewModeEnabled: true });
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
CURSOR_TYPE.GRAB,
);

Some files were not shown because too many files have changed in this diff Show More