Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa1f6890c4 |
@@ -8,7 +8,6 @@
|
||||
!package.json
|
||||
!public/
|
||||
!packages/
|
||||
!scripts/
|
||||
!tsconfig.json
|
||||
!yarn.lock
|
||||
|
||||
|
||||
@@ -6,5 +6,3 @@ firebase/
|
||||
dist/
|
||||
public/workbox
|
||||
packages/excalidraw/types
|
||||
examples/**/public
|
||||
dev-dist
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: master
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ ARG NODE_ENV=production
|
||||
|
||||
RUN yarn build:app:docker
|
||||
|
||||
FROM nginx:1.27-alpine
|
||||
FROM nginx:1.24-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 events |
|
||||
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
|
||||
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when something is pasted into the scene |
|
||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to 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>` |
|
||||
|
||||
@@ -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.6",
|
||||
"@excalidraw/excalidraw": "0.17.0",
|
||||
"@mdx-js/react": "^1.6.22",
|
||||
"clsx": "^1.2.1",
|
||||
"docusaurus-plugin-sass": "0.2.3",
|
||||
|
||||
+32
-27
@@ -1547,7 +1547,7 @@
|
||||
"@docusaurus/theme-search-algolia" "2.2.0"
|
||||
"@docusaurus/types" "2.2.0"
|
||||
|
||||
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
||||
"@docusaurus/react-loadable@5.5.2":
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||
@@ -1718,10 +1718,10 @@
|
||||
url-loader "^4.1.1"
|
||||
webpack "^5.73.0"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
|
||||
"@hapi/hoek@^9.0.0":
|
||||
version "9.3.0"
|
||||
@@ -6190,14 +6190,13 @@ react-dev-utils@^12.0.1:
|
||||
strip-ansi "^6.0.1"
|
||||
text-table "^0.2.0"
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-error-overlay@^6.0.11:
|
||||
version "6.0.11"
|
||||
@@ -6260,6 +6259,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.3"
|
||||
|
||||
"react-loadable@npm:@docusaurus/react-loadable@5.5.2":
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@docusaurus/react-loadable/-/react-loadable-5.5.2.tgz#81aae0db81ecafbdaee3651f12804580868fa6ce"
|
||||
integrity sha512-A3dYjdBGuy0IGT+wyLIGIKLRE+sAk1iNk0f1HjNDysO7u8lhL4N3VEm+FAubmJbAztn94F7MxBTPmnixbiyFdQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-router-config@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988"
|
||||
@@ -6310,13 +6317,12 @@ react-textarea-autosize@^8.3.2:
|
||||
use-composed-ref "^1.3.0"
|
||||
use-latest "^1.2.1"
|
||||
|
||||
react@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
|
||||
react@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
readable-stream@^2.0.1:
|
||||
version "2.3.7"
|
||||
@@ -6664,13 +6670,12 @@ sax@^1.2.4:
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
scheduler@^0.20.2:
|
||||
version "0.20.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
|
||||
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
schema-utils@2.7.0:
|
||||
version "2.7.0"
|
||||
@@ -7801,14 +7806,14 @@ write-file-atomic@^3.0.0:
|
||||
typedarray-to-buffer "^3.1.5"
|
||||
|
||||
ws@^7.3.1:
|
||||
version "7.5.9"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
|
||||
integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==
|
||||
version "7.5.10"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9"
|
||||
integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==
|
||||
|
||||
ws@^8.4.2:
|
||||
version "8.8.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0"
|
||||
integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc"
|
||||
integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==
|
||||
|
||||
xdg-basedir@^4.0.0:
|
||||
version "4.0.0"
|
||||
|
||||
@@ -128,6 +128,29 @@
|
||||
<script>
|
||||
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||
</script>
|
||||
|
||||
<!-- in DEV we need to preload from the local server and without the hash -->
|
||||
<link
|
||||
rel="preload"
|
||||
href="../packages/excalidraw/fonts/assets/Excalifont-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="../packages/excalidraw/fonts/assets/Virgil-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="../packages/excalidraw/fonts/assets/ComicShanns-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<% } %>
|
||||
|
||||
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||
|
||||
@@ -2,6 +2,7 @@ import { vi } from "vitest";
|
||||
import {
|
||||
act,
|
||||
render,
|
||||
updateSceneData,
|
||||
waitFor,
|
||||
} from "../../packages/excalidraw/tests/test-utils";
|
||||
import ExcalidrawApp from "../App";
|
||||
@@ -87,12 +88,12 @@ describe("collaboration", () => {
|
||||
const rect1 = API.createElement({ ...rect1Props });
|
||||
const rect2 = API.createElement({ ...rect2Props });
|
||||
|
||||
API.updateScene({
|
||||
updateSceneData({
|
||||
elements: syncInvalidIndices([rect1, rect2]),
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
API.updateScene({
|
||||
updateSceneData({
|
||||
elements: syncInvalidIndices([
|
||||
rect1,
|
||||
newElementWith(h.elements[1], { isDeleted: true }),
|
||||
@@ -142,7 +143,7 @@ describe("collaboration", () => {
|
||||
});
|
||||
|
||||
// simulate force deleting the element remotely
|
||||
API.updateScene({
|
||||
updateSceneData({
|
||||
elements: syncInvalidIndices([rect1]),
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -177,7 +178,7 @@ describe("collaboration", () => {
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
|
||||
// simulate local update
|
||||
API.updateScene({
|
||||
updateSceneData({
|
||||
elements: syncInvalidIndices([
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], { x: 100 }),
|
||||
@@ -215,7 +216,7 @@ describe("collaboration", () => {
|
||||
});
|
||||
|
||||
// simulate force deleting the element remotely
|
||||
API.updateScene({
|
||||
updateSceneData({
|
||||
elements: syncInvalidIndices([rect1]),
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
@@ -73,8 +73,8 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
workbox: {
|
||||
// Don't push fonts, locales and wasm to app precache
|
||||
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js", "**/*.wasm-*.js"],
|
||||
// Don't push fonts and locales to app precache
|
||||
globIgnores: ["fonts.css", "**/locales/**", "service-worker.js"],
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: new RegExp("/.+.(ttf|woff2|otf)"),
|
||||
@@ -108,17 +108,6 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: new RegExp(".wasm-.+.js"),
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "wasm",
|
||||
expiration: {
|
||||
maxEntries: 50,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 90, // <== 90 days
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
|
||||
+1
-4
@@ -76,10 +76,7 @@
|
||||
"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",
|
||||
"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"
|
||||
"release:excalidraw": "node scripts/release.js"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.2.0"
|
||||
|
||||
@@ -25,7 +25,6 @@ const deleteSelectedElements = (
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
const framesToBeDeleted = new Set(
|
||||
getSelectedElements(
|
||||
elements.filter((el) => isFrameLikeElement(el)),
|
||||
@@ -52,7 +51,7 @@ const deleteSelectedElements = (
|
||||
? null
|
||||
: bound.endBinding,
|
||||
});
|
||||
mutateElbowArrow(bound, elementsMap, bound.points);
|
||||
mutateElbowArrow(bound, app.scene, bound.points);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -160,7 +159,7 @@ export const actionDeleteSelected = register({
|
||||
LinearElementEditor.deletePoints(
|
||||
element,
|
||||
selectedPointsIndices,
|
||||
elementsMap,
|
||||
app.scene,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -44,7 +44,7 @@ export const actionDuplicateSelection = register({
|
||||
if (appState.editingLinearElement) {
|
||||
const ret = LinearElementEditor.duplicateSelectedPoints(
|
||||
appState,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
app.scene,
|
||||
);
|
||||
|
||||
if (!ret) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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.newElement?.type === "freedraw"
|
||||
? appState.newElement
|
||||
: appState.editingElement?.type === "freedraw"
|
||||
? appState.editingElement
|
||||
: null;
|
||||
|
||||
if (multiPointElement) {
|
||||
@@ -176,8 +176,7 @@ export const actionFinalize = register({
|
||||
? appState.activeTool
|
||||
: activeTool,
|
||||
activeEmbeddable: null,
|
||||
newElement: null,
|
||||
selectionElement: null,
|
||||
draggingElement: null,
|
||||
multiElement: null,
|
||||
editingElement: null,
|
||||
startBoundElement: null,
|
||||
@@ -205,7 +204,7 @@ export const actionFinalize = register({
|
||||
keyTest: (event, appState) =>
|
||||
(event.key === KEYS.ESCAPE &&
|
||||
(appState.editingLinearElement !== null ||
|
||||
(!appState.newElement && appState.multiElement === null))) ||
|
||||
(!appState.draggingElement && appState.multiElement === null))) ||
|
||||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
||||
appState.multiElement !== null),
|
||||
PanelComponent: ({ appState, updateData, data }) => (
|
||||
|
||||
@@ -120,6 +120,7 @@ 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 { AppClassProperties, AppState } from "../types";
|
||||
import type { AppState } from "../types";
|
||||
import { KEYS } from "../keys";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { isWindows } from "../constants";
|
||||
@@ -13,8 +13,7 @@ import type { Store } from "../store";
|
||||
import { StoreAction } from "../store";
|
||||
import { useEmitter } from "../hooks/useEmitter";
|
||||
|
||||
const executeHistoryAction = (
|
||||
app: AppClassProperties,
|
||||
const writeData = (
|
||||
appState: Readonly<AppState>,
|
||||
updater: () => [SceneElementsMap, AppState] | void,
|
||||
): ActionResult => {
|
||||
@@ -22,10 +21,7 @@ const executeHistoryAction = (
|
||||
!appState.multiElement &&
|
||||
!appState.resizingElement &&
|
||||
!appState.editingElement &&
|
||||
!appState.newElement &&
|
||||
!appState.selectedElementsAreBeingDragged &&
|
||||
!appState.selectionElement &&
|
||||
!app.flowChartCreator.isCreatingChart
|
||||
!appState.draggingElement
|
||||
) {
|
||||
const result = updater();
|
||||
|
||||
@@ -55,11 +51,12 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
||||
trackEvent: { category: "history" },
|
||||
viewMode: false,
|
||||
perform: (elements, appState, value, app) =>
|
||||
executeHistoryAction(app, appState, () =>
|
||||
writeData(appState, () =>
|
||||
history.undo(
|
||||
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
||||
appState,
|
||||
store.snapshot,
|
||||
app.scene,
|
||||
),
|
||||
),
|
||||
keyTest: (event) =>
|
||||
@@ -96,11 +93,12 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
||||
trackEvent: { category: "history" },
|
||||
viewMode: false,
|
||||
perform: (elements, appState, _, app) =>
|
||||
executeHistoryAction(app, appState, () =>
|
||||
writeData(appState, () =>
|
||||
history.redo(
|
||||
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
||||
appState,
|
||||
store.snapshot,
|
||||
app.scene,
|
||||
),
|
||||
),
|
||||
keyTest: (event) =>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Excalidraw } from "../index";
|
||||
import { queryByTestId } from "@testing-library/react";
|
||||
import { render } from "../tests/test-utils";
|
||||
@@ -7,6 +6,8 @@ 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 />);
|
||||
@@ -21,7 +22,7 @@ describe("element locking", () => {
|
||||
// just in case we change it in the future
|
||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
currentItemBackgroundColor: color,
|
||||
});
|
||||
const activeColor = queryByTestId(
|
||||
@@ -39,14 +40,14 @@ describe("element locking", () => {
|
||||
// just in case we change it in the future
|
||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
currentItemBackgroundColor: color,
|
||||
currentItemFillStyle: "hachure",
|
||||
});
|
||||
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||
|
||||
expect(hachureFillButton).toHaveClass("active");
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
currentItemFillStyle: "solid",
|
||||
});
|
||||
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
||||
@@ -56,7 +57,7 @@ describe("element locking", () => {
|
||||
it("should not show fill style when background transparent", () => {
|
||||
UI.clickTool("rectangle");
|
||||
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
||||
currentItemFillStyle: "hachure",
|
||||
});
|
||||
@@ -68,7 +69,7 @@ describe("element locking", () => {
|
||||
it("should show horizontal text align for text tool", () => {
|
||||
UI.clickTool("text");
|
||||
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
currentItemTextAlign: "right",
|
||||
});
|
||||
|
||||
@@ -84,7 +85,7 @@ describe("element locking", () => {
|
||||
backgroundColor: "red",
|
||||
fillStyle: "cross-hatch",
|
||||
});
|
||||
API.setElements([rect]);
|
||||
h.elements = [rect];
|
||||
API.setSelectedElements([rect]);
|
||||
|
||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||
@@ -97,7 +98,7 @@ describe("element locking", () => {
|
||||
backgroundColor: COLOR_PALETTE.transparent,
|
||||
fillStyle: "cross-hatch",
|
||||
});
|
||||
API.setElements([rect]);
|
||||
h.elements = [rect];
|
||||
API.setSelectedElements([rect]);
|
||||
|
||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||
@@ -113,7 +114,7 @@ describe("element locking", () => {
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.thin,
|
||||
});
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
API.setSelectedElements([rect1, rect2]);
|
||||
|
||||
const thinStrokeWidthButton = queryByTestId(
|
||||
@@ -132,7 +133,7 @@ describe("element locking", () => {
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.bold,
|
||||
});
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
API.setSelectedElements([rect1, rect2]);
|
||||
|
||||
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
||||
@@ -156,7 +157,7 @@ describe("element locking", () => {
|
||||
type: "text",
|
||||
fontFamily: FONT_FAMILY["Comic Shanns"],
|
||||
});
|
||||
API.setElements([rect, text]);
|
||||
h.elements = [rect, text];
|
||||
API.setSelectedElements([rect, text]);
|
||||
|
||||
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||
|
||||
@@ -850,7 +850,7 @@ export const actionChangeFontFamily = register({
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawElement | null
|
||||
>();
|
||||
let uniqueChars = new Set<string>();
|
||||
let uniqueGlyphs = new Set<string>();
|
||||
let skipFontFaceCheck = false;
|
||||
|
||||
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
|
||||
@@ -898,8 +898,8 @@ export const actionChangeFontFamily = register({
|
||||
}
|
||||
|
||||
if (!skipFontFaceCheck) {
|
||||
uniqueChars = new Set([
|
||||
...uniqueChars,
|
||||
uniqueGlyphs = new Set([
|
||||
...uniqueGlyphs,
|
||||
...Array.from(newElement.originalText),
|
||||
]);
|
||||
}
|
||||
@@ -919,9 +919,12 @@ export const actionChangeFontFamily = register({
|
||||
const fontString = `10px ${getFontFamilyString({
|
||||
fontFamily: nextFontFamily,
|
||||
})}`;
|
||||
const chars = Array.from(uniqueChars.values()).join();
|
||||
const glyphs = Array.from(uniqueGlyphs.values()).join();
|
||||
|
||||
if (skipFontFaceCheck || window.document.fonts.check(fontString, chars)) {
|
||||
if (
|
||||
skipFontFaceCheck ||
|
||||
window.document.fonts.check(fontString, glyphs)
|
||||
) {
|
||||
// we either skip the check (have at least one font face loaded) or do the check and find out all the font faces have loaded
|
||||
for (const [element, container] of elementContainerMapping) {
|
||||
// trigger synchronous redraw
|
||||
@@ -933,8 +936,8 @@ export const actionChangeFontFamily = register({
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
||||
window.document.fonts.load(fontString, chars).then((fontFaces) => {
|
||||
// otherwise try to load all font faces for the given glyphs and redraw elements once our font faces loaded
|
||||
window.document.fonts.load(fontString, glyphs).then((fontFaces) => {
|
||||
for (const [element, container] of elementContainerMapping) {
|
||||
// use latest element state to ensure we don't have closure over an old instance in order to avoid possible race conditions (i.e. font faces load out-of-order while rapidly switching fonts)
|
||||
const latestElement = app.scene.getElement(element.id);
|
||||
@@ -1645,7 +1648,7 @@ export const actionChangeArrowType = register({
|
||||
|
||||
mutateElbowArrow(
|
||||
newElement,
|
||||
elementsMap,
|
||||
app.scene,
|
||||
[finalStartPoint, finalEndPoint].map(
|
||||
(point) =>
|
||||
[point[0] - newElement.x, point[1] - newElement.y] as Point,
|
||||
|
||||
@@ -41,7 +41,7 @@ export const getDefaultAppState = (): Omit<
|
||||
currentHoveredFontFamily: null,
|
||||
cursorButton: "up",
|
||||
activeEmbeddable: null,
|
||||
newElement: null,
|
||||
draggingElement: 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 },
|
||||
newElement: { browser: false, export: false, server: false },
|
||||
draggingElement: { 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 },
|
||||
|
||||
@@ -29,6 +29,7 @@ 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,
|
||||
@@ -1053,6 +1054,7 @@ 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>;
|
||||
@@ -1100,6 +1102,7 @@ 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)
|
||||
@@ -1108,9 +1111,6 @@ 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,10 +1459,11 @@ 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, {
|
||||
updateBoundElements(element, elements, scene, {
|
||||
changedElements: changed,
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import {
|
||||
import {
|
||||
assertExcalidrawWithSidebar,
|
||||
assertSidebarDockButton,
|
||||
} from "./Sidebar/siderbar.test.helpers";
|
||||
} from "./Sidebar/Sidebar.test";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
|
||||
@@ -304,16 +304,6 @@ 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,7 +9,6 @@ $wide-viewport-width: 1000px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from "../i18n";
|
||||
import type { AppClassProperties, Device, UIAppState } from "../types";
|
||||
import {
|
||||
isFlowchartNodeElement,
|
||||
isImageElement,
|
||||
isLinearElement,
|
||||
isTextBindableContainer,
|
||||
@@ -11,7 +10,6 @@ import { getShortcutKey } from "../utils";
|
||||
import { isEraserActive } from "../appState";
|
||||
|
||||
import "./HintViewer.scss";
|
||||
import { isNodeInFlowchart } from "../element/flowchart";
|
||||
|
||||
interface HintViewerProps {
|
||||
appState: UIAppState;
|
||||
@@ -20,12 +18,7 @@ interface HintViewerProps {
|
||||
app: AppClassProperties;
|
||||
}
|
||||
|
||||
const getHints = ({
|
||||
appState,
|
||||
isMobile,
|
||||
device,
|
||||
app,
|
||||
}: HintViewerProps): null | string | string[] => {
|
||||
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||
const multiMode = appState.multiElement !== null;
|
||||
|
||||
@@ -92,7 +85,7 @@ const getHints = ({
|
||||
|
||||
if (activeTool.type === "selection") {
|
||||
if (
|
||||
appState.selectionElement &&
|
||||
appState.draggingElement?.type === "selection" &&
|
||||
!selectedElements.length &&
|
||||
!appState.editingElement &&
|
||||
!appState.editingLinearElement
|
||||
@@ -100,7 +93,7 @@ const getHints = ({
|
||||
return t("hints.deepBoxSelect");
|
||||
}
|
||||
|
||||
if (appState.gridSize && appState.selectedElementsAreBeingDragged) {
|
||||
if (appState.gridSize && appState.draggingElement) {
|
||||
return t("hints.disableSnapping");
|
||||
}
|
||||
|
||||
@@ -118,23 +111,9 @@ const getHints = ({
|
||||
return t("hints.lineEditor_info");
|
||||
}
|
||||
if (
|
||||
!appState.newElement &&
|
||||
!appState.selectedElementsAreBeingDragged &&
|
||||
!appState.draggingElement &&
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -149,24 +128,17 @@ export const HintViewer = ({
|
||||
device,
|
||||
app,
|
||||
}: HintViewerProps) => {
|
||||
const hints = getHints({
|
||||
let hint = getHints({
|
||||
appState,
|
||||
isMobile,
|
||||
device,
|
||||
app,
|
||||
});
|
||||
|
||||
if (!hints) {
|
||||
if (!hint) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hint = Array.isArray(hints)
|
||||
? hints
|
||||
.map((hint) => {
|
||||
return getShortcutKey(hint).replace(/\. ?$/, "");
|
||||
})
|
||||
.join(". ")
|
||||
: getShortcutKey(hints);
|
||||
hint = getShortcutKey(hint);
|
||||
|
||||
return (
|
||||
<div className="HintViewer">
|
||||
|
||||
@@ -133,7 +133,6 @@ const SingleLibraryItem = ({
|
||||
exportBackground: true,
|
||||
},
|
||||
files: null,
|
||||
skipInliningFonts: true,
|
||||
});
|
||||
node.innerHTML = svg.outerHTML;
|
||||
})();
|
||||
|
||||
@@ -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,17 +11,39 @@ import {
|
||||
withExcalidrawDimensions,
|
||||
} from "../../tests/test-utils";
|
||||
import { vi } from "vitest";
|
||||
import {
|
||||
assertExcalidrawWithSidebar,
|
||||
assertSidebarDockButton,
|
||||
} from "./siderbar.test.helpers";
|
||||
|
||||
const toggleSidebar = (
|
||||
...args: Parameters<typeof window.h.app.toggleSidebar>
|
||||
): Promise<boolean> => {
|
||||
return act(() => {
|
||||
return window.h.app.toggleSidebar(...args);
|
||||
});
|
||||
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);
|
||||
};
|
||||
|
||||
describe("Sidebar", () => {
|
||||
@@ -81,7 +103,7 @@ describe("Sidebar", () => {
|
||||
|
||||
// toggle sidebar on
|
||||
// -------------------------------------------------------------------------
|
||||
expect(await toggleSidebar({ name: "customSidebar" })).toBe(true);
|
||||
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
@@ -90,7 +112,7 @@ describe("Sidebar", () => {
|
||||
|
||||
// toggle sidebar off
|
||||
// -------------------------------------------------------------------------
|
||||
expect(await toggleSidebar({ name: "customSidebar" })).toBe(false);
|
||||
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(false);
|
||||
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
@@ -99,9 +121,9 @@ describe("Sidebar", () => {
|
||||
|
||||
// force-toggle sidebar off (=> still hidden)
|
||||
// -------------------------------------------------------------------------
|
||||
expect(await toggleSidebar({ name: "customSidebar", force: false })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "customSidebar", force: false }),
|
||||
).toBe(false);
|
||||
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
@@ -110,12 +132,12 @@ describe("Sidebar", () => {
|
||||
|
||||
// force-toggle sidebar on
|
||||
// -------------------------------------------------------------------------
|
||||
expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
|
||||
).toBe(true);
|
||||
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
@@ -124,7 +146,9 @@ describe("Sidebar", () => {
|
||||
|
||||
// toggle library (= hide custom sidebar)
|
||||
// -------------------------------------------------------------------------
|
||||
expect(await toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(true);
|
||||
expect(window.h.app.toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
@@ -137,13 +161,13 @@ describe("Sidebar", () => {
|
||||
|
||||
// closing sidebar using `{ name: null }`
|
||||
// -------------------------------------------------------------------------
|
||||
expect(await toggleSidebar({ name: "customSidebar" })).toBe(true);
|
||||
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
expect(node).not.toBe(null);
|
||||
});
|
||||
|
||||
expect(await toggleSidebar({ name: null })).toBe(false);
|
||||
expect(window.h.app.toggleSidebar({ name: null })).toBe(false);
|
||||
await waitFor(() => {
|
||||
const node = container.querySelector("#test-sidebar-content");
|
||||
expect(node).toBe(null);
|
||||
@@ -297,9 +321,6 @@ 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" } } }}
|
||||
@@ -320,8 +341,6 @@ describe("Sidebar", () => {
|
||||
await assertSidebarDockButton(false);
|
||||
},
|
||||
);
|
||||
|
||||
mock.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -348,9 +367,9 @@ describe("Sidebar", () => {
|
||||
).toBeNull();
|
||||
|
||||
// open library sidebar
|
||||
expect(await toggleSidebar({ name: "custom", tab: "library" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "custom", tab: "library" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
container.querySelector<HTMLElement>(
|
||||
"[role=tabpanel][data-testid=library]",
|
||||
@@ -358,9 +377,9 @@ describe("Sidebar", () => {
|
||||
).not.toBeNull();
|
||||
|
||||
// switch to comments tab
|
||||
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
container.querySelector<HTMLElement>(
|
||||
"[role=tabpanel][data-testid=comments]",
|
||||
@@ -368,9 +387,9 @@ describe("Sidebar", () => {
|
||||
).not.toBeNull();
|
||||
|
||||
// toggle sidebar closed
|
||||
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
container.querySelector<HTMLElement>(
|
||||
"[role=tabpanel][data-testid=comments]",
|
||||
@@ -378,9 +397,9 @@ describe("Sidebar", () => {
|
||||
).toBeNull();
|
||||
|
||||
// toggle sidebar open
|
||||
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
|
||||
).toBe(true);
|
||||
expect(
|
||||
container.querySelector<HTMLElement>(
|
||||
"[role=tabpanel][data-testid=comments]",
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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);
|
||||
};
|
||||
@@ -66,6 +66,7 @@ const resizeElementInGroup = (
|
||||
origElement: ExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||
@@ -77,7 +78,7 @@ const resizeElementInGroup = (
|
||||
);
|
||||
if (boundTextElement) {
|
||||
const newFontSize = boundTextElement.fontSize * scale;
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
updateBoundElements(latestElement, elementsMap, scene, {
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||
@@ -110,6 +111,7 @@ const resizeGroup = (
|
||||
originalElements: ExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
originalElementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
) => {
|
||||
// keep aspect ratio for groups
|
||||
if (property === "width") {
|
||||
@@ -133,6 +135,7 @@ const resizeGroup = (
|
||||
origElement,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -187,6 +190,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
@@ -292,6 +296,7 @@ const handleDimensionChange: DragInputCallbackType<
|
||||
originalElements,
|
||||
elementsMap,
|
||||
originalElementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const [el] = elementsInUnit;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { act, fireEvent, queryByTestId } from "@testing-library/react";
|
||||
import { fireEvent, queryByTestId } from "@testing-library/react";
|
||||
import { Keyboard, Pointer, UI } from "../../tests/helpers/ui";
|
||||
import { getStepSizedValue } from "./utils";
|
||||
import {
|
||||
@@ -25,6 +24,7 @@ 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,6 +32,12 @@ 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");
|
||||
|
||||
@@ -59,7 +65,7 @@ const testInputProperty = (
|
||||
) as HTMLInputElement;
|
||||
expect(input).toBeDefined();
|
||||
expect(input.value).toBe(initialValue.toString());
|
||||
UI.updateInput(input, String(nextValue));
|
||||
editInput(input, String(nextValue));
|
||||
if (property === "angle") {
|
||||
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
||||
} else if (property === "fontSize" && isTextElement(element)) {
|
||||
@@ -104,7 +110,7 @@ describe("binding with linear elements", () => {
|
||||
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -142,7 +148,7 @@ describe("binding with linear elements", () => {
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("204"));
|
||||
editInput(inputX, String("204"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
@@ -153,7 +159,7 @@ describe("binding with linear elements", () => {
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
UI.updateInput(inputAngle, String("1"));
|
||||
editInput(inputAngle, String("1"));
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
});
|
||||
|
||||
@@ -165,7 +171,7 @@ describe("binding with linear elements", () => {
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
expect(inputX).not.toBeNull();
|
||||
UI.updateInput(inputX, String("254"));
|
||||
editInput(inputX, String("254"));
|
||||
expect(linear.startBinding).toBe(null);
|
||||
});
|
||||
|
||||
@@ -176,7 +182,7 @@ describe("binding with linear elements", () => {
|
||||
) as HTMLInputElement;
|
||||
|
||||
expect(linear.startBinding).not.toBe(null);
|
||||
UI.updateInput(inputAngle, String("45"));
|
||||
editInput(inputAngle, String("45"));
|
||||
expect(linear.startBinding).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -191,7 +197,7 @@ describe("stats for a generic element", () => {
|
||||
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -262,13 +268,13 @@ describe("stats for a generic element", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(input).toBeDefined();
|
||||
expect(input.value).toBe(rectangle.width.toString());
|
||||
UI.updateInput(input, "123.123");
|
||||
editInput(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);
|
||||
|
||||
UI.updateInput(input, "88.98766");
|
||||
editInput(input, "88.98766");
|
||||
expect(input.value).toBe("88.99");
|
||||
expect(rectangle.width).toBe(88.99);
|
||||
});
|
||||
@@ -381,7 +387,7 @@ describe("stats for a non-generic element", () => {
|
||||
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -406,10 +412,9 @@ 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!");
|
||||
act(() => {
|
||||
editor.blur();
|
||||
});
|
||||
editor.blur();
|
||||
|
||||
const text = h.elements[0] as ExcalidrawTextElement;
|
||||
mouse.clickOn(text);
|
||||
@@ -422,7 +427,7 @@ describe("stats for a non-generic element", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(input).toBeDefined();
|
||||
expect(input.value).toBe(text.fontSize.toString());
|
||||
UI.updateInput(input, "36");
|
||||
editInput(input, "36");
|
||||
expect(text.fontSize).toBe(36);
|
||||
|
||||
// cannot change width or height
|
||||
@@ -432,7 +437,7 @@ describe("stats for a non-generic element", () => {
|
||||
expect(height).toBeUndefined();
|
||||
|
||||
// min font size is 4
|
||||
UI.updateInput(input, "0");
|
||||
editInput(input, "0");
|
||||
expect(text.fontSize).not.toBe(0);
|
||||
expect(text.fontSize).toBe(4);
|
||||
});
|
||||
@@ -444,8 +449,8 @@ describe("stats for a non-generic element", () => {
|
||||
x: 150,
|
||||
width: 150,
|
||||
});
|
||||
API.setElements([frame]);
|
||||
API.setAppState({
|
||||
h.elements = [frame];
|
||||
h.setState({
|
||||
selectedElementIds: {
|
||||
[frame.id]: true,
|
||||
},
|
||||
@@ -466,9 +471,9 @@ describe("stats for a non-generic element", () => {
|
||||
|
||||
it("image element", () => {
|
||||
const image = API.createElement({ type: "image", width: 200, height: 100 });
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
mouse.clickOn(image);
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
selectedElementIds: {
|
||||
[image.id]: true,
|
||||
},
|
||||
@@ -503,7 +508,7 @@ describe("stats for a non-generic element", () => {
|
||||
mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
|
||||
API.setSelectedElements([container]);
|
||||
const fontSize = getStatsProperty("F")?.querySelector(
|
||||
@@ -511,7 +516,7 @@ describe("stats for a non-generic element", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(fontSize).toBeDefined();
|
||||
|
||||
UI.updateInput(fontSize, "40");
|
||||
editInput(fontSize, "40");
|
||||
|
||||
expect(text.fontSize).toBe(40);
|
||||
});
|
||||
@@ -528,7 +533,7 @@ describe("stats for multiple elements", () => {
|
||||
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -561,7 +566,7 @@ describe("stats for multiple elements", () => {
|
||||
mouse.down(-100, -100);
|
||||
mouse.up(125, 145);
|
||||
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
selectedElementIds: h.elements.reduce((acc, el) => {
|
||||
acc[el.id] = true;
|
||||
return acc;
|
||||
@@ -583,12 +588,12 @@ describe("stats for multiple elements", () => {
|
||||
) as HTMLInputElement;
|
||||
expect(angle.value).toBe("0");
|
||||
|
||||
UI.updateInput(width, "250");
|
||||
editInput(width, "250");
|
||||
h.elements.forEach((el) => {
|
||||
expect(el.width).toBe(250);
|
||||
});
|
||||
|
||||
UI.updateInput(height, "450");
|
||||
editInput(height, "450");
|
||||
h.elements.forEach((el) => {
|
||||
expect(el.height).toBe(450);
|
||||
});
|
||||
@@ -600,10 +605,9 @@ 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!");
|
||||
act(() => {
|
||||
editor.blur();
|
||||
});
|
||||
editor.blur();
|
||||
|
||||
UI.clickTool("rectangle");
|
||||
mouse.down();
|
||||
@@ -615,12 +619,12 @@ describe("stats for multiple elements", () => {
|
||||
width: 150,
|
||||
});
|
||||
|
||||
API.setElements([...h.elements, frame]);
|
||||
h.elements = [...h.elements, frame];
|
||||
|
||||
const text = h.elements.find((el) => el.type === "text");
|
||||
const rectangle = h.elements.find((el) => el.type === "rectangle");
|
||||
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
selectedElementIds: h.elements.reduce((acc, el) => {
|
||||
acc[el.id] = true;
|
||||
return acc;
|
||||
@@ -653,13 +657,13 @@ describe("stats for multiple elements", () => {
|
||||
expect(fontSize).toBeDefined();
|
||||
|
||||
// changing width does not affect text
|
||||
UI.updateInput(width, "200");
|
||||
editInput(width, "200");
|
||||
|
||||
expect(rectangle?.width).toBe(200);
|
||||
expect(frame.width).toBe(200);
|
||||
expect(text?.width).not.toBe(200);
|
||||
|
||||
UI.updateInput(angle, "40");
|
||||
editInput(angle, "40");
|
||||
|
||||
const angleInRadian = degreeToRadian(40);
|
||||
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
||||
@@ -682,7 +686,7 @@ describe("stats for multiple elements", () => {
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
h.app.actionManager.executeAction(actionGroup);
|
||||
};
|
||||
|
||||
createAndSelectGroup();
|
||||
@@ -699,7 +703,7 @@ describe("stats for multiple elements", () => {
|
||||
expect(x).toBeDefined();
|
||||
expect(Number(x.value)).toBe(x1);
|
||||
|
||||
UI.updateInput(x, "300");
|
||||
editInput(x, "300");
|
||||
|
||||
expect(h.elements[0].x).toBe(300);
|
||||
expect(h.elements[1].x).toBe(400);
|
||||
@@ -712,7 +716,7 @@ describe("stats for multiple elements", () => {
|
||||
expect(y).toBeDefined();
|
||||
expect(Number(y.value)).toBe(y1);
|
||||
|
||||
UI.updateInput(y, "200");
|
||||
editInput(y, "200");
|
||||
|
||||
expect(h.elements[0].y).toBe(200);
|
||||
expect(h.elements[1].y).toBe(300);
|
||||
@@ -730,20 +734,20 @@ describe("stats for multiple elements", () => {
|
||||
expect(height).toBeDefined();
|
||||
expect(Number(height.value)).toBe(200);
|
||||
|
||||
UI.updateInput(width, "400");
|
||||
editInput(width, "400");
|
||||
|
||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||
let newGroupWidth = x2 - x1;
|
||||
|
||||
expect(newGroupWidth).toBeCloseTo(400, 4);
|
||||
|
||||
UI.updateInput(width, "300");
|
||||
editInput(width, "300");
|
||||
|
||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||
newGroupWidth = x2 - x1;
|
||||
expect(newGroupWidth).toBeCloseTo(300, 4);
|
||||
|
||||
UI.updateInput(height, "500");
|
||||
editInput(height, "500");
|
||||
|
||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||
const newGroupHeight = y2 - y1;
|
||||
|
||||
@@ -198,7 +198,7 @@ export const resizeElement = (
|
||||
}
|
||||
}
|
||||
|
||||
updateBoundElements(latestElement, elementsMap, {
|
||||
updateBoundElements(latestElement, elementsMap, scene, {
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
|
||||
@@ -316,6 +316,6 @@ export const updateBindings = (
|
||||
[],
|
||||
);
|
||||
} else {
|
||||
updateBoundElements(latestElement, elementsMap, options);
|
||||
updateBoundElements(latestElement, elementsMap, scene, options);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import React from "react";
|
||||
import { Excalidraw } from "../../index";
|
||||
import { KEYS } from "../../keys";
|
||||
import { Keyboard } from "../../tests/helpers/ui";
|
||||
import {
|
||||
render,
|
||||
waitFor,
|
||||
getByTestId,
|
||||
fireEvent,
|
||||
} from "../../tests/test-utils";
|
||||
import { render, waitFor, getByTestId } from "../../tests/test-utils";
|
||||
|
||||
describe("Test <DropdownMenu/>", () => {
|
||||
it("should", async () => {
|
||||
@@ -15,7 +9,7 @@ describe("Test <DropdownMenu/>", () => {
|
||||
|
||||
expect(window.h.state.openMenu).toBe(null);
|
||||
|
||||
fireEvent.click(getByTestId(container, "main-menu-trigger"));
|
||||
getByTestId(container, "main-menu-trigger").click();
|
||||
expect(window.h.state.openMenu).toBe("canvas");
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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.selectedElementsAreBeingDragged ||
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
appState.isRotating ||
|
||||
appState.openMenu ||
|
||||
|
||||
@@ -26,7 +26,7 @@ const shouldDiscardRemoteElement = (
|
||||
// local element is being edited
|
||||
(local.id === localAppState.editingElement?.id ||
|
||||
local.id === localAppState.resizingElement?.id ||
|
||||
local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
|
||||
local.id === localAppState.draggingElement?.id || // TODO: Is this still valid? As draggingElement 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
|
||||
|
||||
@@ -51,7 +51,6 @@ 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,
|
||||
@@ -107,7 +106,7 @@ const repairBinding = (
|
||||
...binding,
|
||||
focus: binding.focus || 0,
|
||||
fixedPoint: isElbowArrow(element)
|
||||
? normalizeFixedPoint(binding.fixedPoint ?? [0, 0])
|
||||
? binding.fixedPoint ?? ([0, 0] as [number, number])
|
||||
: null,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export const ElementCanvasButtons = ({
|
||||
|
||||
if (
|
||||
appState.contextMenu ||
|
||||
appState.newElement ||
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
appState.isRotating ||
|
||||
appState.openMenu ||
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
OrderedExcalidrawElement,
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPoint,
|
||||
SceneElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import type { Bounds } from "./bounds";
|
||||
@@ -125,6 +124,7 @@ export const bindOrUnbindLinearElement = (
|
||||
boundToElementIds,
|
||||
unboundFromElementIds,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
bindOrUnbindLinearElementEdge(
|
||||
linearElement,
|
||||
@@ -134,6 +134,7 @@ export const bindOrUnbindLinearElement = (
|
||||
boundToElementIds,
|
||||
unboundFromElementIds,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
|
||||
const onlyUnbound = Array.from(unboundFromElementIds).filter(
|
||||
@@ -160,6 +161,7 @@ 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") {
|
||||
@@ -569,7 +571,8 @@ const calculateFocusAndGap = (
|
||||
// in explicitly.
|
||||
export const updateBoundElements = (
|
||||
changedElement: NonDeletedExcalidrawElement,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
elementsMap: ElementsMap,
|
||||
scene: Scene,
|
||||
options?: {
|
||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||
oldSize?: { width: number; height: number };
|
||||
@@ -655,7 +658,7 @@ export const updateBoundElements = (
|
||||
LinearElementEditor.movePoints(
|
||||
element,
|
||||
updates,
|
||||
elementsMap,
|
||||
scene,
|
||||
{
|
||||
...(changedElement.id === element.startBinding?.elementId
|
||||
? { startBinding: bindings.startBinding }
|
||||
@@ -759,11 +762,6 @@ export const bindPointToSnapToElementOutline = (
|
||||
[point[0], point[1] + 2 * bindableElement.height],
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
).map((i) =>
|
||||
distanceToBindableElement(bindableElement, i, elementsMap) >=
|
||||
bindableElement.height / 2
|
||||
? ([point[0], -1 * i[1]] as Point)
|
||||
: ([point[0], i[1]] as Point),
|
||||
),
|
||||
...intersectElementWithLine(
|
||||
bindableElement,
|
||||
@@ -771,13 +769,13 @@ export const bindPointToSnapToElementOutline = (
|
||||
[point[0] + 2 * bindableElement.width, point[1]],
|
||||
FIXED_BINDING_DISTANCE,
|
||||
elementsMap,
|
||||
).map((i) =>
|
||||
distanceToBindableElement(bindableElement, i, elementsMap) >=
|
||||
bindableElement.width / 2
|
||||
? ([-1 * i[0], point[1]] as Point)
|
||||
: ([i[0], point[1]] as Point),
|
||||
),
|
||||
];
|
||||
].map((i) =>
|
||||
distanceToBindableElement(bindableElement, i, elementsMap) >
|
||||
Math.min(bindableElement.width, bindableElement.height) / 2
|
||||
? ([-1 * i[0], -1 * i[1]] as Point)
|
||||
: i,
|
||||
);
|
||||
|
||||
const heading = headingForPointFromElement(bindableElement, aabb, point);
|
||||
const isVertical =
|
||||
@@ -1005,7 +1003,7 @@ const updateBoundPoint = (
|
||||
|
||||
if (isElbowArrow(linearElement)) {
|
||||
const fixedPoint =
|
||||
normalizeFixedPoint(binding.fixedPoint) ??
|
||||
binding.fixedPoint ??
|
||||
calculateFixedPointForElbowArrowBinding(
|
||||
linearElement,
|
||||
bindableElement,
|
||||
@@ -1118,12 +1116,12 @@ export const calculateFixedPointForElbowArrowBinding = (
|
||||
) as Point;
|
||||
|
||||
return {
|
||||
fixedPoint: normalizeFixedPoint([
|
||||
fixedPoint: [
|
||||
(nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) /
|
||||
hoveredElement.width,
|
||||
(nonRotatedSnappedGlobalPoint[1] - hoveredElement.y) /
|
||||
hoveredElement.height,
|
||||
]),
|
||||
] as [number, number],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1248,11 +1246,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 && boundElements.length > 0) {
|
||||
if (boundElements != null && boundElements.length > 0) {
|
||||
mutateElement(bindableElement, {
|
||||
boundElements: boundElements.map((boundElement) =>
|
||||
oldIdToDuplicatedId.has(boundElement.id)
|
||||
@@ -2176,8 +2174,7 @@ export const getGlobalFixedPointForBindableElement = (
|
||||
fixedPointRatio: [number, number],
|
||||
element: ExcalidrawBindableElement,
|
||||
) => {
|
||||
const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio);
|
||||
|
||||
const [fixedX, fixedY] = fixedPointRatio;
|
||||
return rotatePoint(
|
||||
[element.x + element.width * fixedX, element.y + element.height * fixedY],
|
||||
getCenterForElement(element),
|
||||
@@ -2231,16 +2228,3 @@ 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;
|
||||
};
|
||||
|
||||
@@ -91,9 +91,14 @@ export const dragSelectedElements = (
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
}
|
||||
}
|
||||
updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
});
|
||||
updateBoundElements(
|
||||
element,
|
||||
scene.getElementsMapIncludingDeleted(),
|
||||
scene,
|
||||
{
|
||||
simultaneouslyUpdated: Array.from(elementsToUpdate),
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -155,7 +160,7 @@ export const getDragOffsetXY = (
|
||||
};
|
||||
|
||||
export const dragNewElement = (
|
||||
newElement: NonDeletedExcalidrawElement,
|
||||
draggingElement: NonDeletedExcalidrawElement,
|
||||
elementType: AppState["activeTool"]["type"],
|
||||
originX: number,
|
||||
originY: number,
|
||||
@@ -174,7 +179,7 @@ export const dragNewElement = (
|
||||
y: number;
|
||||
} | null = null,
|
||||
) => {
|
||||
if (shouldMaintainAspectRatio && newElement.type !== "selection") {
|
||||
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
||||
if (widthAspectRatio) {
|
||||
height = width / widthAspectRatio;
|
||||
} else {
|
||||
@@ -213,14 +218,17 @@ export const dragNewElement = (
|
||||
|
||||
let textAutoResize = null;
|
||||
|
||||
if (isTextElement(newElement)) {
|
||||
height = newElement.height;
|
||||
// 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;
|
||||
const minWidth = getMinTextElementWidth(
|
||||
getFontString({
|
||||
fontSize: newElement.fontSize,
|
||||
fontFamily: newElement.fontFamily,
|
||||
fontSize: draggingElement.fontSize,
|
||||
fontFamily: draggingElement.fontFamily,
|
||||
}),
|
||||
newElement.lineHeight,
|
||||
draggingElement.lineHeight,
|
||||
);
|
||||
width = Math.max(width, minWidth);
|
||||
|
||||
@@ -237,7 +245,7 @@ export const dragNewElement = (
|
||||
}
|
||||
|
||||
if (width !== 0 && height !== 0) {
|
||||
mutateElement(newElement, {
|
||||
mutateElement(draggingElement, {
|
||||
x: newX + (originOffset?.x ?? 0),
|
||||
y: newY + (originOffset?.y ?? 0),
|
||||
width,
|
||||
|
||||
@@ -1,404 +0,0 @@
|
||||
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,
|
||||
});
|
||||
|
||||
h.elements = [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,
|
||||
});
|
||||
|
||||
h.elements = [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,
|
||||
});
|
||||
|
||||
h.elements = [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,
|
||||
});
|
||||
|
||||
h.elements = [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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,698 +0,0 @@
|
||||
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,7 +9,6 @@ import type {
|
||||
NonDeletedSceneElementsMap,
|
||||
OrderedExcalidrawElement,
|
||||
FixedPointBinding,
|
||||
SceneElementsMap,
|
||||
} from "./types";
|
||||
import {
|
||||
distance2d,
|
||||
@@ -44,7 +43,7 @@ import {
|
||||
getHoveredElementForBinding,
|
||||
isBindingEnabled,
|
||||
} from "./binding";
|
||||
import { toBrandedType, tupleToCoors } from "../utils";
|
||||
import { tupleToCoors } from "../utils";
|
||||
import {
|
||||
isBindingElement,
|
||||
isElbowArrow,
|
||||
@@ -152,7 +151,10 @@ export class LinearElementEditor {
|
||||
setState: React.Component<any, AppState>["setState"],
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
) {
|
||||
if (!appState.editingLinearElement || !appState.selectionElement) {
|
||||
if (
|
||||
!appState.editingLinearElement ||
|
||||
appState.draggingElement?.type !== "selection"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const { editingLinearElement } = appState;
|
||||
@@ -164,7 +166,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
|
||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||
getElementAbsoluteCoords(appState.selectionElement, elementsMap);
|
||||
getElementAbsoluteCoords(appState.draggingElement, elementsMap);
|
||||
|
||||
const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
element,
|
||||
@@ -291,7 +293,7 @@ export class LinearElementEditor {
|
||||
isDragging: selectedIndex === lastClickedPoint,
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||
@@ -327,7 +329,7 @@ export class LinearElementEditor {
|
||||
isDragging: pointIndex === lastClickedPoint,
|
||||
};
|
||||
}),
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -421,7 +423,7 @@ export class LinearElementEditor {
|
||||
: element.points[0],
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -741,7 +743,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
if (
|
||||
linearElementEditor.lastUncommittedPoint == null &&
|
||||
linearElementEditor.lastUncommittedPoint == null ||
|
||||
!isElbowArrow(element)
|
||||
) {
|
||||
mutateElement(element, {
|
||||
@@ -877,12 +879,13 @@ export class LinearElementEditor {
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
appState: AppState,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
): 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;
|
||||
@@ -893,11 +896,7 @@ export class LinearElementEditor {
|
||||
|
||||
if (!event.altKey) {
|
||||
if (lastPoint === lastUncommittedPoint) {
|
||||
LinearElementEditor.deletePoints(
|
||||
element,
|
||||
[points.length - 1],
|
||||
elementsMap,
|
||||
);
|
||||
LinearElementEditor.deletePoints(element, [points.length - 1], scene);
|
||||
}
|
||||
return {
|
||||
...appState.editingLinearElement,
|
||||
@@ -943,13 +942,14 @@ export class LinearElementEditor {
|
||||
point: newPoint,
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
} else {
|
||||
LinearElementEditor.addPoints(
|
||||
element,
|
||||
appState,
|
||||
[{ point: newPoint }],
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
return {
|
||||
@@ -1094,7 +1094,7 @@ export class LinearElementEditor {
|
||||
const offsetY = points[0][1];
|
||||
|
||||
return {
|
||||
points: points.map((point) => {
|
||||
points: points.map((point, _idx) => {
|
||||
return [point[0] - offsetX, point[1] - offsetY] as const;
|
||||
}),
|
||||
x: element.x + offsetX,
|
||||
@@ -1109,15 +1109,13 @@ export class LinearElementEditor {
|
||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
||||
}
|
||||
|
||||
static duplicateSelectedPoints(
|
||||
appState: AppState,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
) {
|
||||
static duplicateSelectedPoints(appState: AppState, scene: Scene) {
|
||||
if (!appState.editingLinearElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||
const elementsMap = scene.getNonDeletedElementsMap();
|
||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||
|
||||
if (!element || selectedPointsIndices === null) {
|
||||
@@ -1168,7 +1166,7 @@ export class LinearElementEditor {
|
||||
point: [lastPoint[0] + 30, lastPoint[1] + 30],
|
||||
},
|
||||
],
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1186,7 +1184,7 @@ export class LinearElementEditor {
|
||||
static deletePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
pointIndices: readonly number[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
) {
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
@@ -1219,14 +1217,15 @@ export class LinearElementEditor {
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
static addPoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
targetPoints: { point: Point }[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
) {
|
||||
const offsetX = 0;
|
||||
const offsetY = 0;
|
||||
@@ -1237,14 +1236,14 @@ export class LinearElementEditor {
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
elementsMap,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
static movePoints(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding | null;
|
||||
endBinding?: PointBinding | null;
|
||||
@@ -1300,7 +1299,7 @@ export class LinearElementEditor {
|
||||
nextPoints,
|
||||
offsetX,
|
||||
offsetY,
|
||||
elementsMap,
|
||||
scene,
|
||||
otherUpdates,
|
||||
{
|
||||
isDragging: targetPoints.reduce(
|
||||
@@ -1417,7 +1416,7 @@ export class LinearElementEditor {
|
||||
nextPoints: readonly Point[],
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding | null;
|
||||
endBinding?: PointBinding | null;
|
||||
@@ -1447,15 +1446,9 @@ export class LinearElementEditor {
|
||||
: null;
|
||||
}
|
||||
|
||||
const mergedElementsMap = options?.changedElements
|
||||
? toBrandedType<SceneElementsMap>(
|
||||
new Map([...elementsMap, ...options.changedElements]),
|
||||
)
|
||||
: elementsMap;
|
||||
|
||||
mutateElbowArrow(
|
||||
element,
|
||||
mergedElementsMap,
|
||||
scene,
|
||||
nextPoints,
|
||||
[offsetX, offsetY],
|
||||
bindings,
|
||||
|
||||
@@ -11,8 +11,6 @@ import type {
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
SceneElementsMap,
|
||||
} from "./types";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import {
|
||||
@@ -71,7 +69,7 @@ export const transformElements = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: SceneElementsMap,
|
||||
elementsMap: ElementsMap,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
@@ -79,6 +77,7 @@ export const transformElements = (
|
||||
pointerY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
scene: Scene,
|
||||
) => {
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
@@ -91,7 +90,7 @@ export const transformElements = (
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
);
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, elementsMap, scene);
|
||||
}
|
||||
} else if (isTextElement(element) && transformHandleType) {
|
||||
resizeSingleTextElement(
|
||||
@@ -103,7 +102,7 @@ export const transformElements = (
|
||||
pointerX,
|
||||
pointerY,
|
||||
);
|
||||
updateBoundElements(element, elementsMap);
|
||||
updateBoundElements(element, elementsMap, scene);
|
||||
} else if (transformHandleType) {
|
||||
resizeSingleElement(
|
||||
originalElements,
|
||||
@@ -114,6 +113,7 @@ export const transformElements = (
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
pointerY,
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ export const transformElements = (
|
||||
shouldRotateWithDiscreteAngle,
|
||||
centerX,
|
||||
centerY,
|
||||
scene,
|
||||
);
|
||||
return true;
|
||||
} else if (transformHandleType) {
|
||||
@@ -141,6 +142,7 @@ export const transformElements = (
|
||||
shouldMaintainAspectRatio,
|
||||
pointerX,
|
||||
pointerY,
|
||||
scene,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
@@ -432,12 +434,22 @@ export const resizeSingleElement = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: SceneElementsMap,
|
||||
elementsMap: ElementsMap,
|
||||
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(
|
||||
@@ -707,7 +719,7 @@ export const resizeSingleElement = (
|
||||
) {
|
||||
mutateElement(element, resizedElement);
|
||||
|
||||
updateBoundElements(element, elementsMap, {
|
||||
updateBoundElements(element, elementsMap, scene, {
|
||||
oldSize: {
|
||||
width: stateAtResizeStart.width,
|
||||
height: stateAtResizeStart.height,
|
||||
@@ -731,12 +743,13 @@ export const resizeSingleElement = (
|
||||
export const resizeMultipleElements = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
elementsMap: ElementsMap,
|
||||
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
|
||||
@@ -970,19 +983,12 @@ export const resizeMultipleElements = (
|
||||
mutateElement(element, update, false);
|
||||
|
||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||
mutateElbowArrow(
|
||||
element,
|
||||
elementsMap,
|
||||
element.points,
|
||||
undefined,
|
||||
undefined,
|
||||
{
|
||||
informMutation: false,
|
||||
},
|
||||
);
|
||||
mutateElbowArrow(element, scene, element.points, undefined, undefined, {
|
||||
informMutation: false,
|
||||
});
|
||||
}
|
||||
|
||||
updateBoundElements(element, elementsMap, {
|
||||
updateBoundElements(element, elementsMap, scene, {
|
||||
simultaneouslyUpdated: elementsToUpdate,
|
||||
oldSize: { width: oldWidth, height: oldHeight },
|
||||
});
|
||||
@@ -1007,12 +1013,13 @@ export const resizeMultipleElements = (
|
||||
const rotateMultipleElements = (
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: SceneElementsMap,
|
||||
elementsMap: ElementsMap,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
scene: Scene,
|
||||
) => {
|
||||
let centerAngle =
|
||||
(5 * Math.PI) / 2 + Math.atan2(pointerY - centerY, pointerX - centerX);
|
||||
@@ -1039,7 +1046,7 @@ const rotateMultipleElements = (
|
||||
|
||||
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||
const points = getArrowLocalFixedPoints(element, elementsMap);
|
||||
mutateElbowArrow(element, elementsMap, points);
|
||||
mutateElbowArrow(element, scene, points);
|
||||
} else {
|
||||
mutateElement(
|
||||
element,
|
||||
@@ -1052,7 +1059,7 @@ const rotateMultipleElements = (
|
||||
);
|
||||
}
|
||||
|
||||
updateBoundElements(element, elementsMap, {
|
||||
updateBoundElements(element, elementsMap, scene, {
|
||||
simultaneouslyUpdated: elements,
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ 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");
|
||||
|
||||
@@ -45,7 +51,7 @@ describe("elbow arrow routing", () => {
|
||||
elbowed: true,
|
||||
}) as ExcalidrawElbowArrowElement;
|
||||
scene.insertElement(arrow);
|
||||
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||
mutateElbowArrow(arrow, scene, [
|
||||
[-45 - arrow.x, -100.1 - arrow.y],
|
||||
[45 - arrow.x, 99.9 - arrow.y],
|
||||
]);
|
||||
@@ -98,7 +104,7 @@ describe("elbow arrow routing", () => {
|
||||
expect(arrow.startBinding).not.toBe(null);
|
||||
expect(arrow.endBinding).not.toBe(null);
|
||||
|
||||
mutateElbowArrow(arrow, elementsMap, [
|
||||
mutateElbowArrow(arrow, scene, [
|
||||
[0, 0],
|
||||
[90, 200],
|
||||
]);
|
||||
@@ -196,7 +202,7 @@ describe("elbow arrow ui", () => {
|
||||
const inputAngle = getStatsProperty("A")?.querySelector(
|
||||
".drag-input",
|
||||
) as HTMLInputElement;
|
||||
UI.updateInput(inputAngle, String("40"));
|
||||
editInput(inputAngle, String("40"));
|
||||
|
||||
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||
[0, 0],
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 {
|
||||
@@ -36,10 +37,14 @@ import { isBindableElement, isRectanguloidElement } from "./typeChecks";
|
||||
import type {
|
||||
ExcalidrawElbowArrowElement,
|
||||
FixedPointBinding,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
SceneElementsMap,
|
||||
} from "./types";
|
||||
import type { ElementsMap, ExcalidrawBindableElement } from "./types";
|
||||
import type {
|
||||
ElementsMap,
|
||||
ExcalidrawBindableElement,
|
||||
OrderedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
type Node = {
|
||||
f: number;
|
||||
@@ -62,7 +67,7 @@ const BASE_PADDING = 40;
|
||||
|
||||
export const mutateElbowArrow = (
|
||||
arrow: ExcalidrawElbowArrowElement,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
scene: Scene,
|
||||
nextPoints: readonly Point[],
|
||||
offset?: Point,
|
||||
otherUpdates?: {
|
||||
@@ -70,11 +75,15 @@ 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),
|
||||
@@ -90,9 +99,22 @@ export const mutateElbowArrow = (
|
||||
const endElement =
|
||||
arrow.endBinding &&
|
||||
getBindableElementForId(arrow.endBinding.elementId, elementsMap);
|
||||
const [hoveredStartElement, hoveredEndElement] = options?.isDragging
|
||||
? getHoveredElements(origStartGlobalPoint, origEndGlobalPoint, elementsMap)
|
||||
: [startElement, endElement];
|
||||
const hoveredStartElement = options?.isDragging
|
||||
? getHoveredElementForBinding(
|
||||
tupleToCoors(origStartGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
true,
|
||||
)
|
||||
: startElement;
|
||||
const hoveredEndElement = options?.isDragging
|
||||
? getHoveredElementForBinding(
|
||||
tupleToCoors(origEndGlobalPoint),
|
||||
elements,
|
||||
elementsMap,
|
||||
true,
|
||||
)
|
||||
: endElement;
|
||||
const startGlobalPoint = getGlobalPoint(
|
||||
arrow.startBinding?.fixedPoint,
|
||||
origStartGlobalPoint,
|
||||
@@ -873,7 +895,7 @@ const normalizedArrowElementUpdate = (
|
||||
const offsetY = global[0][1];
|
||||
|
||||
const points = global.map(
|
||||
(point) => [point[0] - offsetX, point[1] - offsetY] as const,
|
||||
(point, _idx) => [point[0] - offsetX, point[1] - offsetY] as const,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -913,11 +935,32 @@ 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 | SceneElementsMap,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
boundElement?: ExcalidrawBindableElement | null,
|
||||
hoveredElement?: ExcalidrawBindableElement | null,
|
||||
isDragging?: boolean,
|
||||
@@ -973,7 +1016,7 @@ const getSnapPoint = (
|
||||
const getBindPointHeading = (
|
||||
point: Point,
|
||||
otherPoint: Point,
|
||||
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||
elementsMap: NonDeletedSceneElementsMap,
|
||||
hoveredElement: ExcalidrawBindableElement | null | undefined,
|
||||
origPoint: Point,
|
||||
) =>
|
||||
@@ -991,30 +1034,3 @@ 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,43 +55,6 @@ 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
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Excalidraw } from "../index";
|
||||
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
||||
@@ -17,6 +16,7 @@ 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} />);
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
});
|
||||
|
||||
it("should prefer editing selected text element (non-bindable container present)", async () => {
|
||||
@@ -55,7 +55,7 @@ describe("textWysiwyg", () => {
|
||||
width: textSize,
|
||||
height: textSize,
|
||||
});
|
||||
API.setElements([text, line]);
|
||||
h.elements = [text, line];
|
||||
|
||||
API.setSelectedElements([text]);
|
||||
|
||||
@@ -95,9 +95,9 @@ describe("textWysiwyg", () => {
|
||||
containerId: container.id,
|
||||
});
|
||||
|
||||
API.setElements([container, boundText, boundText2]);
|
||||
h.elements = [container, boundText, boundText2];
|
||||
|
||||
API.updateElement(container, {
|
||||
mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: boundText.id }],
|
||||
});
|
||||
|
||||
@@ -123,11 +123,11 @@ describe("textWysiwyg", () => {
|
||||
height: textSize,
|
||||
containerId: container.id,
|
||||
});
|
||||
API.updateElement(container, {
|
||||
mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: text.id }],
|
||||
});
|
||||
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
|
||||
API.setSelectedElements([container]);
|
||||
|
||||
@@ -164,9 +164,9 @@ describe("textWysiwyg", () => {
|
||||
containerId: container.id,
|
||||
});
|
||||
|
||||
API.setElements([container, boundText, boundText2]);
|
||||
h.elements = [container, boundText, boundText2];
|
||||
|
||||
API.updateElement(container, {
|
||||
mutateElement(container, {
|
||||
boundElements: [{ type: "text", id: boundText.id }],
|
||||
});
|
||||
|
||||
@@ -187,7 +187,7 @@ describe("textWysiwyg", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
UI.clickTool("text");
|
||||
|
||||
mouse.clickAt(text.x + 50, text.y + 50);
|
||||
@@ -209,7 +209,7 @@ describe("textWysiwyg", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
UI.clickTool("selection");
|
||||
|
||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||
@@ -251,7 +251,7 @@ describe("textWysiwyg", () => {
|
||||
// @ts-ignore
|
||||
h.app.refreshEditorBreakpoints();
|
||||
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -264,7 +264,7 @@ describe("textWysiwyg", () => {
|
||||
text: "Excalidraw\nEditor",
|
||||
});
|
||||
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
|
||||
const prevWidth = text.width;
|
||||
const prevHeight = text.height;
|
||||
@@ -291,15 +291,18 @@ describe("textWysiwyg", () => {
|
||||
|
||||
const nextText = `${wrappedText} is great!`;
|
||||
updateTextEditor(editor, nextText);
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
|
||||
expect(h.elements[0].width).toEqual(wrappedWidth);
|
||||
expect(h.elements[0].height).toBeGreaterThan(wrappedHeight);
|
||||
|
||||
// remove all texts and then add it back editing
|
||||
updateTextEditor(editor, "");
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
updateTextEditor(editor, nextText);
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
|
||||
expect(h.elements[0].width).toEqual(wrappedWidth);
|
||||
});
|
||||
@@ -310,7 +313,7 @@ describe("textWysiwyg", () => {
|
||||
type: "text",
|
||||
text: originalText,
|
||||
});
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
|
||||
// wrap
|
||||
UI.resize(text, "e", [-40, 0]);
|
||||
@@ -318,7 +321,7 @@ describe("textWysiwyg", () => {
|
||||
UI.clickTool("selection");
|
||||
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||
const editor = await getTextEditor(textEditorSelector);
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
// restore after unwrapping
|
||||
UI.resize(text, "e", [40, 0]);
|
||||
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
|
||||
@@ -329,12 +332,14 @@ describe("textWysiwyg", () => {
|
||||
UI.clickTool("selection");
|
||||
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||
updateTextEditor(editor, `${wrappedText}\nA new line!`);
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
// remove the newly added line
|
||||
UI.clickTool("selection");
|
||||
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||
updateTextEditor(editor, wrappedText);
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
// unwrap
|
||||
UI.resize(text, "e", [30, 0]);
|
||||
// expect the text to be restored the same
|
||||
@@ -371,11 +376,12 @@ 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;
|
||||
fireEvent.keyDown(textarea, { key: KEYS.TAB });
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
||||
// cursor: " |Line#1\nLine#2"
|
||||
@@ -384,12 +390,13 @@ 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;
|
||||
|
||||
fireEvent.keyDown(textarea, { key: KEYS.TAB });
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
||||
|
||||
@@ -399,12 +406,13 @@ 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;
|
||||
|
||||
fireEvent.keyDown(textarea, { key: KEYS.TAB });
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
||||
|
||||
@@ -414,15 +422,16 @@ 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;
|
||||
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||
|
||||
@@ -432,15 +441,16 @@ 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;
|
||||
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||
// cursor: "Line#1\nLin|e#2"
|
||||
@@ -449,15 +459,16 @@ 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;
|
||||
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||
@@ -466,41 +477,45 @@ 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;
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
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", {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
// cursor: "Line#1\n Line#|2"
|
||||
textarea.value = `Line#1\n Line#2`;
|
||||
textarea.selectionStart = 15;
|
||||
textarea.selectionEnd = 15;
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
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;
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.TAB,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(event);
|
||||
|
||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||
});
|
||||
@@ -508,42 +523,54 @@ describe("textWysiwyg", () => {
|
||||
it("should resize text via shortcuts while in wysiwyg", () => {
|
||||
textarea.value = "abc def";
|
||||
const origFontSize = textElement.fontSize;
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.CHEVRON_RIGHT,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: KEYS.CHEVRON_RIGHT,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
}),
|
||||
);
|
||||
expect(textElement.fontSize).toBe(origFontSize * 1.1);
|
||||
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: KEYS.CHEVRON_LEFT,
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
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);
|
||||
fireEvent.keyDown(textarea, {
|
||||
code: CODES.MINUS,
|
||||
ctrlKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
code: CODES.MINUS,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
);
|
||||
expect(h.state.zoom.value).toBe(0.9);
|
||||
fireEvent.keyDown(textarea, {
|
||||
code: CODES.NUM_SUBTRACT,
|
||||
ctrlKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
code: CODES.NUM_SUBTRACT,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
);
|
||||
expect(h.state.zoom.value).toBe(0.8);
|
||||
fireEvent.keyDown(textarea, {
|
||||
code: CODES.NUM_ADD,
|
||||
ctrlKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
code: CODES.NUM_ADD,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
);
|
||||
expect(h.state.zoom.value).toBe(0.9);
|
||||
fireEvent.keyDown(textarea, {
|
||||
code: CODES.EQUAL,
|
||||
ctrlKey: true,
|
||||
});
|
||||
textarea.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
code: CODES.EQUAL,
|
||||
ctrlKey: true,
|
||||
}),
|
||||
);
|
||||
expect(h.state.zoom.value).toBe(1);
|
||||
});
|
||||
|
||||
@@ -556,8 +583,8 @@ describe("textWysiwyg", () => {
|
||||
textarea,
|
||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||
);
|
||||
Keyboard.exitTextEditor(textarea);
|
||||
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
textarea.blur();
|
||||
expect(textarea.style.width).toBe("792px");
|
||||
expect(h.elements[0].width).toBe(1000);
|
||||
});
|
||||
@@ -569,7 +596,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
|
||||
rectangle = UI.createElement("rectangle", {
|
||||
x: 10,
|
||||
@@ -588,7 +615,7 @@ describe("textWysiwyg", () => {
|
||||
height: 75,
|
||||
backgroundColor: "red",
|
||||
});
|
||||
API.setElements([rectangle]);
|
||||
h.elements = [rectangle];
|
||||
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(rectangle.id);
|
||||
@@ -607,7 +634,8 @@ describe("textWysiwyg", () => {
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
@@ -620,7 +648,7 @@ describe("textWysiwyg", () => {
|
||||
height: 75,
|
||||
angle: 45,
|
||||
});
|
||||
API.setElements([rectangle]);
|
||||
h.elements = [rectangle];
|
||||
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
@@ -634,7 +662,8 @@ describe("textWysiwyg", () => {
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
@@ -648,7 +677,7 @@ describe("textWysiwyg", () => {
|
||||
width: 90,
|
||||
height: 75,
|
||||
});
|
||||
API.setElements([diamond]);
|
||||
h.elements = [diamond];
|
||||
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(diamond.id);
|
||||
@@ -658,6 +687,7 @@ 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
|
||||
@@ -682,7 +712,7 @@ describe("textWysiwyg", () => {
|
||||
height: 75,
|
||||
backgroundColor: "transparent",
|
||||
});
|
||||
API.setElements([rectangle]);
|
||||
h.elements = [rectangle];
|
||||
|
||||
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||
expect(h.elements.length).toBe(2);
|
||||
@@ -691,7 +721,8 @@ describe("textWysiwyg", () => {
|
||||
expect(text.containerId).toBe(null);
|
||||
mouse.down();
|
||||
let editor = await getTextEditor(textEditorSelector, true);
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
|
||||
mouse.doubleClickAt(
|
||||
rectangle.x + rectangle.width / 2,
|
||||
@@ -707,7 +738,8 @@ describe("textWysiwyg", () => {
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
@@ -727,8 +759,10 @@ 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!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
@@ -743,7 +777,7 @@ describe("textWysiwyg", () => {
|
||||
height: 75,
|
||||
strokeWidth: 4,
|
||||
});
|
||||
API.setElements([rectangle]);
|
||||
h.elements = [rectangle];
|
||||
|
||||
expect(h.elements.length).toBe(1);
|
||||
expect(h.elements[0].id).toBe(rectangle.id);
|
||||
@@ -761,7 +795,8 @@ describe("textWysiwyg", () => {
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
@@ -773,7 +808,7 @@ describe("textWysiwyg", () => {
|
||||
width: 100,
|
||||
height: 0,
|
||||
});
|
||||
API.setElements([freedraw]);
|
||||
h.elements = [freedraw];
|
||||
|
||||
UI.clickTool("text");
|
||||
|
||||
@@ -784,7 +819,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||
|
||||
expect(freedraw.boundElements).toBe(null);
|
||||
expect(h.elements[1].type).toBe("text");
|
||||
@@ -793,7 +828,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
["freedraw", "line"].forEach((type: any) => {
|
||||
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
const element = UI.createElement(type, {
|
||||
width: 100,
|
||||
height: 50,
|
||||
@@ -820,7 +855,8 @@ describe("textWysiwyg", () => {
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toBe(null);
|
||||
});
|
||||
|
||||
@@ -836,6 +872,7 @@ 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");
|
||||
|
||||
@@ -871,18 +908,14 @@ 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!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
|
||||
expect(await getTextEditor(textEditorSelector, false)).toBe(null);
|
||||
|
||||
expect(h.state.editingElement).toBe(null);
|
||||
|
||||
editor.blur();
|
||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
|
||||
|
||||
fireEvent.click(screen.getByTitle(/code/i));
|
||||
@@ -917,7 +950,8 @@ describe("textWysiwyg", () => {
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
editor.blur();
|
||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.text).toBe("Hello \nWorld!");
|
||||
expect(text.originalText).toBe("Hello World!");
|
||||
@@ -936,7 +970,9 @@ describe("textWysiwyg", () => {
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
editor.blur();
|
||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
|
||||
expect(text.text).toBe("Hello");
|
||||
@@ -962,8 +998,10 @@ describe("textWysiwyg", () => {
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
@@ -996,8 +1034,9 @@ 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!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
@@ -1016,8 +1055,9 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
let editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
// should center align horizontally and vertically by default
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
@@ -1036,8 +1076,12 @@ describe("textWysiwyg", () => {
|
||||
editor.select();
|
||||
|
||||
fireEvent.click(screen.getByTitle("Left"));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
editor.blur();
|
||||
|
||||
// should left align horizontally and bottom vertically after resize
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
@@ -1057,7 +1101,9 @@ describe("textWysiwyg", () => {
|
||||
fireEvent.click(screen.getByTitle("Right"));
|
||||
fireEvent.click(screen.getByTitle("Align top"));
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
editor.blur();
|
||||
|
||||
// should right align horizontally and top vertically after resize
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
@@ -1090,7 +1136,8 @@ describe("textWysiwyg", () => {
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle2.boundElements).toBeNull();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
@@ -1101,8 +1148,9 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||
expect(rectangle.width).toBe(90);
|
||||
expect(rectangle.height).toBe(75);
|
||||
@@ -1120,8 +1168,9 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(h.elements.length).toBe(2);
|
||||
|
||||
mouse.select(rectangle);
|
||||
@@ -1151,8 +1200,9 @@ 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");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: h.elements[1].id, type: "text" },
|
||||
]);
|
||||
@@ -1187,9 +1237,10 @@ 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, " ");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(rectangle.boundElements).toStrictEqual([]);
|
||||
expect(h.elements[1].isDeleted).toBe(true);
|
||||
});
|
||||
@@ -1208,7 +1259,7 @@ describe("textWysiwyg", () => {
|
||||
text: "Online whiteboard collaboration made easy",
|
||||
});
|
||||
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
API.setSelectedElements([container, text]);
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
@@ -1241,8 +1292,9 @@ 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");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
@@ -1253,7 +1305,8 @@ describe("textWysiwyg", () => {
|
||||
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
// cache updated again
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
|
||||
@@ -1268,7 +1321,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
@@ -1293,7 +1346,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||
).toEqual(1.25);
|
||||
@@ -1325,7 +1378,7 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
@@ -1445,11 +1498,13 @@ 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));
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||
expect(textElement.width).toBe(600);
|
||||
@@ -1526,14 +1581,16 @@ 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));
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
@@ -1549,8 +1606,9 @@ describe("textWysiwyg", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Excalidraw");
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
expect(h.elements.length).toBe(3);
|
||||
expect(rectangle.boundElements).toStrictEqual([
|
||||
|
||||
@@ -321,7 +321,6 @@ export const shouldShowBoundingBox = (
|
||||
}
|
||||
const element = elements[0];
|
||||
if (isElbowArrow(element)) {
|
||||
// Elbow arrows cannot be resized as single selected elements
|
||||
return false;
|
||||
}
|
||||
if (!isLinearElement(element)) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
ExcalidrawElbowArrowElement,
|
||||
PointBinding,
|
||||
FixedPointBinding,
|
||||
ExcalidrawFlowchartNodeElement,
|
||||
} from "./types";
|
||||
|
||||
export const isInitializedImageElement = (
|
||||
@@ -220,16 +219,6 @@ 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"> => {
|
||||
|
||||
@@ -160,11 +160,6 @@ 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
|
||||
@@ -181,11 +176,6 @@ export type ExcalidrawElement =
|
||||
| ExcalidrawIframeElement
|
||||
| ExcalidrawEmbeddableElement;
|
||||
|
||||
export type ExcalidrawNonSelectionElement = Exclude<
|
||||
ExcalidrawElement,
|
||||
ExcalidrawSelectionElement
|
||||
>;
|
||||
|
||||
export type Ordered<TElement extends ExcalidrawElement> = TElement & {
|
||||
index: FractionalIndex;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { stringToBase64, toByteString } from "../data/encode";
|
||||
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
||||
import loadWoff2 from "./wasm/woff2.loader";
|
||||
import loadHbSubset from "./wasm/hb-subset.loader";
|
||||
|
||||
export interface Font {
|
||||
urls: URL[];
|
||||
fontFace: FontFace;
|
||||
getContent(codePoints: ReadonlySet<number>): Promise<string>;
|
||||
getContent(): Promise<string>;
|
||||
}
|
||||
export const UNPKG_FALLBACK_URL = `https://unpkg.com/${
|
||||
export const UNPKG_PROD_URL = `https://unpkg.com/${
|
||||
import.meta.env.VITE_PKG_NAME
|
||||
? `${import.meta.env.VITE_PKG_NAME}@${import.meta.env.PKG_VERSION}` // should be provided by vite during package build
|
||||
: "@excalidraw/excalidraw" // fallback to latest package version (i.e. for app)
|
||||
@@ -34,32 +32,21 @@ export class ExcalidrawFont implements Font {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to fetch woff2 content, based on the registered urls (from first to last, treated as fallbacks).
|
||||
* Tries to fetch woff2 content, based on the registered urls.
|
||||
* Returns last defined url in case of errors.
|
||||
*
|
||||
* NOTE: assumes usage of `dataurl` outside the browser environment
|
||||
*
|
||||
* @returns base64 with subsetted glyphs based on the passed codepoint, last defined url otherwise
|
||||
* Note: uses browser APIs for base64 encoding - use dataurl outside the browser environment.
|
||||
*/
|
||||
public async getContent(codePoints: ReadonlySet<number>): Promise<string> {
|
||||
public async getContent(): Promise<string> {
|
||||
let i = 0;
|
||||
const errorMessages = [];
|
||||
|
||||
while (i < this.urls.length) {
|
||||
const url = this.urls[i];
|
||||
|
||||
// it's dataurl (server), the font is inlined as base64, no need to fetch
|
||||
if (url.protocol === "data:") {
|
||||
const arrayBuffer = Buffer.from(
|
||||
url.toString().split(",")[1],
|
||||
"base64",
|
||||
).buffer;
|
||||
|
||||
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||
arrayBuffer,
|
||||
codePoints,
|
||||
);
|
||||
|
||||
return base64;
|
||||
// it's dataurl, the font is inlined as base64, no need to fetch
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -70,13 +57,13 @@ export class ExcalidrawFont implements Font {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||
arrayBuffer,
|
||||
codePoints,
|
||||
);
|
||||
const mimeType = await response.headers.get("Content-Type");
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return base64;
|
||||
return `data:${mimeType};base64,${await stringToBase64(
|
||||
await toByteString(buffer),
|
||||
true,
|
||||
)}`;
|
||||
}
|
||||
|
||||
// response not ok, try to continue
|
||||
@@ -102,48 +89,6 @@ export class ExcalidrawFont implements Font {
|
||||
return this.urls.length ? this.urls[this.urls.length - 1].toString() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to subset glyphs in a font based on the used codepoints, returning the font as daturl.
|
||||
*
|
||||
* @param arrayBuffer font data buffer, preferrably in the woff2 format, though others should work as well
|
||||
* @param codePoints codepoints used to subset the glyphs
|
||||
*
|
||||
* @returns font with subsetted glyphs (all glyphs in case of errors) converted into a dataurl
|
||||
*/
|
||||
private static async subsetGlyphsByCodePoints(
|
||||
arrayBuffer: ArrayBuffer,
|
||||
codePoints: ReadonlySet<number>,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// lazy loaded wasm modules to avoid multiple initializations in case of concurrent triggers
|
||||
const { compress, decompress } = await loadWoff2();
|
||||
const { subset } = await loadHbSubset();
|
||||
|
||||
const decompressedBinary = decompress(arrayBuffer).buffer;
|
||||
const subsetSnft = subset(decompressedBinary, codePoints);
|
||||
const compressedBinary = compress(subsetSnft.buffer);
|
||||
|
||||
return ExcalidrawFont.toBase64(compressedBinary.buffer);
|
||||
} catch (e) {
|
||||
console.error("Skipped glyph subsetting", e);
|
||||
// Fallback to encoding whole font in case of errors
|
||||
return ExcalidrawFont.toBase64(arrayBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private static async toBase64(arrayBuffer: ArrayBuffer) {
|
||||
let base64: string;
|
||||
|
||||
if (typeof Buffer !== "undefined") {
|
||||
// node + server-side
|
||||
base64 = Buffer.from(arrayBuffer).toString("base64");
|
||||
} else {
|
||||
base64 = await stringToBase64(await toByteString(arrayBuffer), true);
|
||||
}
|
||||
|
||||
return `data:font/woff2;base64,${base64}`;
|
||||
}
|
||||
|
||||
private static createUrls(uri: string): URL[] {
|
||||
if (uri.startsWith(LOCAL_FONT_PROTOCOL)) {
|
||||
// no url for local fonts
|
||||
@@ -173,14 +118,15 @@ export class ExcalidrawFont implements Font {
|
||||
}
|
||||
|
||||
// fallback url for bundled fonts
|
||||
urls.push(new URL(assetUrl, UNPKG_FALLBACK_URL));
|
||||
urls.push(new URL(assetUrl, UNPKG_PROD_URL));
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
private static getFormat(url: URL) {
|
||||
try {
|
||||
const parts = new URL(url).pathname.split(".");
|
||||
const pathname = new URL(url).pathname;
|
||||
const parts = pathname.split(".");
|
||||
|
||||
if (parts.length === 1) {
|
||||
return "";
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
/**
|
||||
* Modified version of hb-subset bindings from "subset-font" package https://github.com/papandreou/subset-font/blob/3f711c8aa29a426c7f22655861abfb976950f527/index.js
|
||||
*
|
||||
* CHANGELOG:
|
||||
* - removed dependency on node APIs to work inside the browser
|
||||
* - removed dependency on font fontverter for brotli compression
|
||||
* - removed dependencies on lodash and p-limit
|
||||
* - removed options for preserveNameIds, variationAxes, noLayoutClosure (not needed for now)
|
||||
* - replaced text input with codepoints
|
||||
* - rewritten in typescript and with esm modules
|
||||
|
||||
Copyright (c) 2012, Andreas Lind Petersen
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of the author nor the names of contributors may
|
||||
be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
||||
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
||||
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
// function HB_TAG(str) {
|
||||
// return str.split("").reduce((a, ch) => {
|
||||
// return (a << 8) + ch.charCodeAt(0);
|
||||
// }, 0);
|
||||
// }
|
||||
|
||||
function subset(
|
||||
hbSubsetWasm: any,
|
||||
heapu8: Uint8Array,
|
||||
font: ArrayBuffer,
|
||||
codePoints: ReadonlySet<number>,
|
||||
) {
|
||||
const input = hbSubsetWasm.hb_subset_input_create_or_fail();
|
||||
if (input === 0) {
|
||||
throw new Error(
|
||||
"hb_subset_input_create_or_fail (harfbuzz) returned zero, indicating failure",
|
||||
);
|
||||
}
|
||||
|
||||
const fontBuffer = hbSubsetWasm.malloc(font.byteLength);
|
||||
heapu8.set(new Uint8Array(font), fontBuffer);
|
||||
|
||||
// Create the face
|
||||
const blob = hbSubsetWasm.hb_blob_create(
|
||||
fontBuffer,
|
||||
font.byteLength,
|
||||
2, // HB_MEMORY_MODE_WRITABLE
|
||||
0,
|
||||
0,
|
||||
);
|
||||
const face = hbSubsetWasm.hb_face_create(blob, 0);
|
||||
hbSubsetWasm.hb_blob_destroy(blob);
|
||||
|
||||
// Do the equivalent of --font-features=*
|
||||
const layoutFeatures = hbSubsetWasm.hb_subset_input_set(
|
||||
input,
|
||||
6, // HB_SUBSET_SETS_LAYOUT_FEATURE_TAG
|
||||
);
|
||||
hbSubsetWasm.hb_set_clear(layoutFeatures);
|
||||
hbSubsetWasm.hb_set_invert(layoutFeatures);
|
||||
|
||||
// if (preserveNameIds) {
|
||||
// const inputNameIds = harfbuzzJsWasm.hb_subset_input_set(
|
||||
// input,
|
||||
// 4, // HB_SUBSET_SETS_NAME_ID
|
||||
// );
|
||||
// for (const nameId of preserveNameIds) {
|
||||
// harfbuzzJsWasm.hb_set_add(inputNameIds, nameId);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (noLayoutClosure) {
|
||||
// harfbuzzJsWasm.hb_subset_input_set_flags(
|
||||
// input,
|
||||
// harfbuzzJsWasm.hb_subset_input_get_flags(input) | 0x00000200, // HB_SUBSET_FLAGS_NO_LAYOUT_CLOSURE
|
||||
// );
|
||||
// }
|
||||
|
||||
// Add unicodes indices
|
||||
const inputUnicodes = hbSubsetWasm.hb_subset_input_unicode_set(input);
|
||||
for (const c of codePoints) {
|
||||
hbSubsetWasm.hb_set_add(inputUnicodes, c);
|
||||
}
|
||||
|
||||
// if (variationAxes) {
|
||||
// for (const [axisName, value] of Object.entries(variationAxes)) {
|
||||
// if (typeof value === "number") {
|
||||
// // Simple case: Pin/instance the variation axis to a single value
|
||||
// if (
|
||||
// !harfbuzzJsWasm.hb_subset_input_pin_axis_location(
|
||||
// input,
|
||||
// face,
|
||||
// HB_TAG(axisName),
|
||||
// value,
|
||||
// )
|
||||
// ) {
|
||||
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||
// harfbuzzJsWasm.free(fontBuffer);
|
||||
// throw new Error(
|
||||
// `hb_subset_input_pin_axis_location (harfbuzz) returned zero when pinning ${axisName} to ${value}, indicating failure. Maybe the axis does not exist in the font?`,
|
||||
// );
|
||||
// }
|
||||
// } else if (value && typeof value === "object") {
|
||||
// // Complex case: Reduce the variation space of the axis
|
||||
// if (
|
||||
// typeof value.min === "undefined" ||
|
||||
// typeof value.max === "undefined"
|
||||
// ) {
|
||||
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||
// harfbuzzJsWasm.free(fontBuffer);
|
||||
// throw new Error(
|
||||
// `${axisName}: You must provide both a min and a max value when setting the axis range`,
|
||||
// );
|
||||
// }
|
||||
// if (
|
||||
// !harfbuzzJsWasm.hb_subset_input_set_axis_range(
|
||||
// input,
|
||||
// face,
|
||||
// HB_TAG(axisName),
|
||||
// value.min,
|
||||
// value.max,
|
||||
// // An explicit NaN makes harfbuzz use the existing default value, clamping to the new range if necessary
|
||||
// value.default ?? NaN,
|
||||
// )
|
||||
// ) {
|
||||
// harfbuzzJsWasm.hb_face_destroy(face);
|
||||
// harfbuzzJsWasm.free(fontBuffer);
|
||||
// throw new Error(
|
||||
// `hb_subset_input_set_axis_range (harfbuzz) returned zero when setting the range of ${axisName} to [${value.min}; ${value.max}] and a default value of ${value.default}, indicating failure. Maybe the axis does not exist in the font?`,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
let subset;
|
||||
try {
|
||||
subset = hbSubsetWasm.hb_subset_or_fail(face, input);
|
||||
if (subset === 0) {
|
||||
hbSubsetWasm.hb_face_destroy(face);
|
||||
hbSubsetWasm.free(fontBuffer);
|
||||
throw new Error(
|
||||
"hb_subset_or_fail (harfbuzz) returned zero, indicating failure. Maybe the input file is corrupted?",
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
// Clean up
|
||||
hbSubsetWasm.hb_subset_input_destroy(input);
|
||||
}
|
||||
|
||||
// Get result blob
|
||||
const result = hbSubsetWasm.hb_face_reference_blob(subset);
|
||||
|
||||
const offset = hbSubsetWasm.hb_blob_get_data(result, 0);
|
||||
const subsetByteLength = hbSubsetWasm.hb_blob_get_length(result);
|
||||
if (subsetByteLength === 0) {
|
||||
hbSubsetWasm.hb_blob_destroy(result);
|
||||
hbSubsetWasm.hb_face_destroy(subset);
|
||||
hbSubsetWasm.hb_face_destroy(face);
|
||||
hbSubsetWasm.free(fontBuffer);
|
||||
throw new Error(
|
||||
"Failed to create subset font, maybe the input file is corrupted?",
|
||||
);
|
||||
}
|
||||
|
||||
const subsetFont = new Uint8Array(
|
||||
heapu8.subarray(offset, offset + subsetByteLength),
|
||||
);
|
||||
|
||||
// Clean up
|
||||
hbSubsetWasm.hb_blob_destroy(result);
|
||||
hbSubsetWasm.hb_face_destroy(subset);
|
||||
hbSubsetWasm.hb_face_destroy(face);
|
||||
hbSubsetWasm.free(fontBuffer);
|
||||
|
||||
return subsetFont;
|
||||
}
|
||||
|
||||
export default {
|
||||
subset,
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
/**
|
||||
* Lazy loads wasm and respective bindings for font subsetting based on the harfbuzzjs.
|
||||
*/
|
||||
let loadedWasm: ReturnType<typeof load> | null = null;
|
||||
|
||||
// TODO: add support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||
const load = (): Promise<{
|
||||
subset: (
|
||||
fontBuffer: ArrayBuffer,
|
||||
codePoints: ReadonlySet<number>,
|
||||
) => Uint8Array;
|
||||
}> => {
|
||||
return new Promise(async (resolve) => {
|
||||
const [binary, bindings] = await Promise.all([
|
||||
import("./hb-subset.wasm"),
|
||||
import("./hb-subset.bindings"),
|
||||
]);
|
||||
|
||||
WebAssembly.instantiate(binary.default).then((module) => {
|
||||
const harfbuzzJsWasm = module.instance.exports;
|
||||
// @ts-expect-error since `.buffer` is custom prop
|
||||
const heapu8 = new Uint8Array(harfbuzzJsWasm.memory.buffer);
|
||||
|
||||
const hbSubset = {
|
||||
subset: (fontBuffer: ArrayBuffer, codePoints: ReadonlySet<number>) => {
|
||||
return bindings.default.subset(
|
||||
harfbuzzJsWasm,
|
||||
heapu8,
|
||||
fontBuffer,
|
||||
codePoints,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
resolve(hbSubset);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// lazy load the default export
|
||||
export default (): ReturnType<typeof load> => {
|
||||
if (!loadedWasm) {
|
||||
loadedWasm = load();
|
||||
}
|
||||
|
||||
return loadedWasm;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,62 +0,0 @@
|
||||
/**
|
||||
* Lazy loads wasm and respective bindings for woff2 compression and decompression.
|
||||
*/
|
||||
type Vector = any;
|
||||
|
||||
let loadedWasm: ReturnType<typeof load> | null = null;
|
||||
|
||||
// TODO: add support for fetching the wasm from an URL (external CDN, data URL, etc.)
|
||||
const load = (): Promise<{
|
||||
compress: (buffer: ArrayBuffer) => Uint8Array;
|
||||
decompress: (buffer: ArrayBuffer) => Uint8Array;
|
||||
}> => {
|
||||
return new Promise(async (resolve) => {
|
||||
const [binary, bindings] = await Promise.all([
|
||||
import("./woff2.wasm"),
|
||||
import("./woff2.bindings"),
|
||||
]);
|
||||
|
||||
// initializing the module manually, so that we could pass in the wasm binary
|
||||
bindings
|
||||
.default({ wasmBinary: binary.default })
|
||||
.then(
|
||||
(module: {
|
||||
woff2Enc: (buffer: ArrayBuffer, byteLength: number) => Vector;
|
||||
woff2Dec: (buffer: ArrayBuffer, byteLength: number) => Vector;
|
||||
}) => {
|
||||
// re-map from internal vector into byte array
|
||||
function convertFromVecToUint8Array(vector: Vector): Uint8Array {
|
||||
const arr = [];
|
||||
for (let i = 0, l = vector.size(); i < l; i++) {
|
||||
arr.push(vector.get(i));
|
||||
}
|
||||
|
||||
return new Uint8Array(arr);
|
||||
}
|
||||
|
||||
// re-exporting only compress and decompress functions (also avoids infinite loop inside emscripten bindings)
|
||||
const woff2 = {
|
||||
compress: (buffer: ArrayBuffer) =>
|
||||
convertFromVecToUint8Array(
|
||||
module.woff2Enc(buffer, buffer.byteLength),
|
||||
),
|
||||
decompress: (buffer: ArrayBuffer) =>
|
||||
convertFromVecToUint8Array(
|
||||
module.woff2Dec(buffer, buffer.byteLength),
|
||||
),
|
||||
};
|
||||
|
||||
resolve(woff2);
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// lazy loaded default export
|
||||
export default (): ReturnType<typeof load> => {
|
||||
if (!loadedWasm) {
|
||||
loadedWasm = load();
|
||||
}
|
||||
|
||||
return loadedWasm;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import type { ExcalidrawElement } from "./element/types";
|
||||
import { convertToExcalidrawElements, Excalidraw } from "./index";
|
||||
import { API } from "./tests/helpers/api";
|
||||
@@ -123,7 +122,7 @@ describe("adding elements to frames", () => {
|
||||
) => {
|
||||
describe.skip("when frame is in a layer below", async () => {
|
||||
it("should add an element", async () => {
|
||||
API.setElements([frame, rect2]);
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
func(frame, rect2);
|
||||
|
||||
@@ -132,7 +131,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
API.setElements([frame, rect2, rect3]);
|
||||
h.elements = [frame, rect2, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -143,7 +142,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
API.setElements([frame, rect1, rect2, rect4, rect3]);
|
||||
h.elements = [frame, rect1, rect2, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -154,7 +153,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
API.setElements([frame, rect3, rect4, rect2, rect1]);
|
||||
h.elements = [frame, rect3, rect4, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -167,7 +166,7 @@ describe("adding elements to frames", () => {
|
||||
|
||||
describe.skip("when frame is in a layer above", async () => {
|
||||
it("should add an element", async () => {
|
||||
API.setElements([rect2, frame]);
|
||||
h.elements = [rect2, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
|
||||
@@ -176,7 +175,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add elements", async () => {
|
||||
API.setElements([rect2, rect3, frame]);
|
||||
h.elements = [rect2, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -187,7 +186,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
API.setElements([rect1, rect2, rect4, rect3, frame]);
|
||||
h.elements = [rect1, rect2, rect4, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -198,7 +197,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
API.setElements([rect3, rect4, rect2, rect1, frame]);
|
||||
h.elements = [rect3, rect4, rect2, rect1, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -211,7 +210,7 @@ describe("adding elements to frames", () => {
|
||||
|
||||
describe("when frame is in an inner layer", async () => {
|
||||
it.skip("should add elements", async () => {
|
||||
API.setElements([rect2, frame, rect3]);
|
||||
h.elements = [rect2, frame, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -222,7 +221,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should add elements when there are other other elements in between", async () => {
|
||||
API.setElements([rect2, rect1, frame, rect4, rect3]);
|
||||
h.elements = [rect2, rect1, frame, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -233,7 +232,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
API.setElements([rect3, rect4, frame, rect2, rect1]);
|
||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||
|
||||
func(frame, rect2);
|
||||
func(frame, rect3);
|
||||
@@ -254,22 +253,20 @@ describe("adding elements to frames", () => {
|
||||
|
||||
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
||||
|
||||
API.setElements(
|
||||
reorderElements(
|
||||
[
|
||||
frame,
|
||||
...convertToExcalidrawElements([
|
||||
{
|
||||
type: containerType,
|
||||
x: 100,
|
||||
y: 100,
|
||||
height: 10,
|
||||
label: { text: "xx" },
|
||||
},
|
||||
]),
|
||||
],
|
||||
initialOrder,
|
||||
),
|
||||
h.elements = reorderElements(
|
||||
[
|
||||
frame,
|
||||
...convertToExcalidrawElements([
|
||||
{
|
||||
type: containerType,
|
||||
x: 100,
|
||||
y: 100,
|
||||
height: 10,
|
||||
label: { text: "xx" },
|
||||
},
|
||||
]),
|
||||
],
|
||||
initialOrder,
|
||||
);
|
||||
|
||||
assertOrder(h.elements, initialOrder);
|
||||
@@ -340,7 +337,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
|
||||
API.setElements([frame, arrow, text]);
|
||||
h.elements = [frame, arrow, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
@@ -350,7 +347,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in a layer above", async () => {
|
||||
API.setElements([arrow, text, frame]);
|
||||
h.elements = [arrow, text, frame];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
@@ -360,7 +357,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||
API.setElements([arrow, frame, text]);
|
||||
h.elements = [arrow, frame, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
|
||||
@@ -372,7 +369,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 () => {
|
||||
API.setElements([frame, rect1, rect2, rect3, rect4]);
|
||||
h.elements = [frame, rect1, rect2, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
@@ -383,7 +380,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should add elements when frame is in a layer above", async () => {
|
||||
API.setElements([rect1, rect2, rect3, rect4, frame]);
|
||||
h.elements = [rect1, rect2, rect3, rect4, frame];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
@@ -394,7 +391,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should add elements when frame is in an inner layer", async () => {
|
||||
API.setElements([rect1, rect2, frame, rect3, rect4]);
|
||||
h.elements = [rect1, rect2, frame, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
resizeFrameOverElement(frame, rect3);
|
||||
@@ -409,7 +406,7 @@ describe("adding elements to frames", () => {
|
||||
await commonTestCases(dragElementIntoFrame);
|
||||
|
||||
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
||||
API.setElements([frame, rect2]);
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
@@ -423,7 +420,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
|
||||
API.setElements([frame, rect2]);
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
|
||||
@@ -493,7 +490,7 @@ describe("adding elements to frames", () => {
|
||||
frameId: frame3.id,
|
||||
});
|
||||
|
||||
API.setElements([
|
||||
h.elements = [
|
||||
frame1,
|
||||
rectangle4,
|
||||
rectangle1,
|
||||
@@ -501,7 +498,7 @@ describe("adding elements to frames", () => {
|
||||
frame3,
|
||||
rectangle2,
|
||||
frame2,
|
||||
]);
|
||||
];
|
||||
|
||||
API.setSelectedElements([rectangle2]);
|
||||
|
||||
@@ -544,7 +541,7 @@ describe("adding elements to frames", () => {
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
API.setElements([rectangle1, rectangle2, frame1, frame2]);
|
||||
h.elements = [rectangle1, rectangle2, frame1, frame2];
|
||||
|
||||
API.setSelectedElements([rectangle2]);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
|
||||
@@ -64,6 +65,7 @@ export class History {
|
||||
elements: SceneElementsMap,
|
||||
appState: AppState,
|
||||
snapshot: Readonly<Snapshot>,
|
||||
scene: Scene,
|
||||
) {
|
||||
return this.perform(
|
||||
elements,
|
||||
@@ -71,6 +73,7 @@ export class History {
|
||||
snapshot,
|
||||
() => History.pop(this.undoStack),
|
||||
(entry: HistoryEntry) => History.push(this.redoStack, entry, elements),
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -78,6 +81,7 @@ export class History {
|
||||
elements: SceneElementsMap,
|
||||
appState: AppState,
|
||||
snapshot: Readonly<Snapshot>,
|
||||
scene: Scene,
|
||||
) {
|
||||
return this.perform(
|
||||
elements,
|
||||
@@ -85,6 +89,7 @@ export class History {
|
||||
snapshot,
|
||||
() => History.pop(this.redoStack),
|
||||
(entry: HistoryEntry) => History.push(this.undoStack, entry, elements),
|
||||
scene,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,6 +99,7 @@ export class History {
|
||||
snapshot: Readonly<Snapshot>,
|
||||
pop: () => HistoryEntry | null,
|
||||
push: (entry: HistoryEntry) => void,
|
||||
scene: Scene,
|
||||
): [SceneElementsMap, AppState] | void {
|
||||
try {
|
||||
let historyEntry = pop();
|
||||
@@ -110,7 +116,7 @@ export class History {
|
||||
while (historyEntry) {
|
||||
try {
|
||||
[nextElements, nextAppState, containsVisibleChange] =
|
||||
historyEntry.applyTo(nextElements, nextAppState, snapshot);
|
||||
historyEntry.applyTo(nextElements, nextAppState, snapshot, scene);
|
||||
} finally {
|
||||
// make sure to always push / pop, even if the increment is corrupted
|
||||
push(historyEntry);
|
||||
@@ -181,9 +187,10 @@ export class HistoryEntry {
|
||||
elements: SceneElementsMap,
|
||||
appState: AppState,
|
||||
snapshot: Readonly<Snapshot>,
|
||||
scene: Scene,
|
||||
): [SceneElementsMap, AppState, boolean] {
|
||||
const [nextElements, elementsContainVisibleChange] =
|
||||
this.elementsChange.applyTo(elements, snapshot.elements);
|
||||
this.elementsChange.applyTo(elements, snapshot.elements, scene);
|
||||
|
||||
const [nextAppState, appStateContainsVisibleChange] =
|
||||
this.appStateChange.applyTo(appState, nextElements);
|
||||
|
||||
@@ -18,7 +18,6 @@ const exportLibraryItemToSvg = async (elements: LibraryItem["elements"]) => {
|
||||
},
|
||||
files: null,
|
||||
renderEmbeddables: false,
|
||||
skipInliningFonts: true,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -41,7 +40,6 @@ export const useLibraryItemSvg = (
|
||||
// When there is no svg in cache export it and save to cache
|
||||
(async () => {
|
||||
const exportedSvg = await exportLibraryItemToSvg(elements);
|
||||
// TODO: should likely be removed for custom fonts
|
||||
exportedSvg.querySelector(".style-fonts")?.remove();
|
||||
|
||||
if (exportedSvg) {
|
||||
|
||||
@@ -316,7 +316,6 @@
|
||||
"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.",
|
||||
@@ -367,8 +366,6 @@
|
||||
"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",
|
||||
|
||||
@@ -96,9 +96,8 @@
|
||||
"@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": "16.0.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@types/pako": "1.0.3",
|
||||
"@types/pica": "5.1.3",
|
||||
"@types/resize-observer-browser": "0.1.7",
|
||||
@@ -113,8 +112,6 @@
|
||||
"esbuild-sass-plugin": "2.16.0",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"fake-indexeddb": "3.1.7",
|
||||
"fonteditor-core": "2.4.1",
|
||||
"harfbuzzjs": "0.3.6",
|
||||
"import-meta-loader": "1.1.0",
|
||||
"mini-css-extract-plugin": "2.6.1",
|
||||
"postcss-loader": "7.0.1",
|
||||
|
||||
@@ -35,7 +35,6 @@ import type {
|
||||
Zoom,
|
||||
InteractiveCanvasAppState,
|
||||
ElementsPendingErasure,
|
||||
PendingExcalidrawElements,
|
||||
} from "../types";
|
||||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
@@ -105,7 +104,6 @@ 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)
|
||||
@@ -115,7 +113,6 @@ 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;
|
||||
@@ -675,7 +672,6 @@ export const renderElement = (
|
||||
element,
|
||||
getContainingFrame(element, elementsMap),
|
||||
renderConfig.elementsPendingErasure,
|
||||
renderConfig.pendingFlowchartNodes,
|
||||
);
|
||||
|
||||
switch (element.type) {
|
||||
|
||||
@@ -370,23 +370,6 @@ 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 */
|
||||
|
||||
@@ -377,10 +377,6 @@ 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",
|
||||
@@ -407,11 +403,7 @@ class Scene {
|
||||
};
|
||||
|
||||
insertElements = (elements: ExcalidrawElement[]) => {
|
||||
if (!elements.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = elements[0]?.frameId
|
||||
const index = elements[0].frameId
|
||||
? this.getElementIndex(elements[0].frameId)
|
||||
: this.elements.length;
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ import type { RenderableElementsMap } from "./types";
|
||||
import { syncInvalidIndices } from "../fractionalIndex";
|
||||
import { renderStaticScene } from "../renderer/staticScene";
|
||||
import { Fonts } from "../fonts";
|
||||
import type { Font } from "../fonts/ExcalidrawFont";
|
||||
|
||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||
|
||||
@@ -243,7 +242,6 @@ export const exportToCanvas = async (
|
||||
// empty disables embeddable rendering
|
||||
embedsValidationStatus: new Map(),
|
||||
elementsPendingErasure: new Set(),
|
||||
pendingFlowchartNodes: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -356,14 +354,50 @@ export const exportToSvg = async (
|
||||
</clipPath>`;
|
||||
}
|
||||
|
||||
const fontFaces = opts?.skipInliningFonts ? [] : await getFontFaces(elements);
|
||||
const fontFamilies = elements.reduce((acc, element) => {
|
||||
if (isTextElement(element)) {
|
||||
acc.add(element.fontFamily);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, new Set<number>());
|
||||
|
||||
const fontFaces = opts?.skipInliningFonts
|
||||
? []
|
||||
: await Promise.all(
|
||||
Array.from(fontFamilies).map(async (x) => {
|
||||
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
|
||||
|
||||
if (!Array.isArray(fonts)) {
|
||||
console.error(
|
||||
`Couldn't find registered fonts for font-family "${x}"`,
|
||||
Fonts.registered,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata?.local) {
|
||||
// don't inline local fonts
|
||||
return;
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
fonts.map(
|
||||
async (font) => `@font-face {
|
||||
font-family: ${font.fontFace.family};
|
||||
src: url(${await font.getContent()});
|
||||
}`,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
svgRoot.innerHTML = `
|
||||
${SVG_EXPORT_TAG}
|
||||
${metadata}
|
||||
<defs>
|
||||
<style class="style-fonts">
|
||||
${fontFaces.join("\n")}
|
||||
${fontFaces.flat().filter(Boolean).join("\n")}
|
||||
</style>
|
||||
${exportingFrameClipPath}
|
||||
</defs>
|
||||
@@ -434,67 +468,3 @@ export const getExportSize = (
|
||||
|
||||
return [width, height];
|
||||
};
|
||||
|
||||
const getFontFaces = async (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): Promise<string[]> => {
|
||||
const fontFamilies = new Set<number>();
|
||||
const codePoints = new Set<number>();
|
||||
|
||||
for (const element of elements) {
|
||||
if (!isTextElement(element)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fontFamilies.add(element.fontFamily);
|
||||
|
||||
// gather unique codepoints only when inlining fonts
|
||||
for (const codePoint of Array.from(element.originalText, (u) =>
|
||||
u.codePointAt(0),
|
||||
)) {
|
||||
if (codePoint) {
|
||||
codePoints.add(codePoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getSource = (font: Font) => {
|
||||
try {
|
||||
// retrieve font source as dataurl based on the used codepoints
|
||||
return font.getContent(codePoints);
|
||||
} catch {
|
||||
// fallback to font source as a url
|
||||
return font.urls[0].toString();
|
||||
}
|
||||
};
|
||||
|
||||
const fontFaces = await Promise.all(
|
||||
Array.from(fontFamilies).map(async (x) => {
|
||||
const { fonts, metadata } = Fonts.registered.get(x) ?? {};
|
||||
|
||||
if (!Array.isArray(fonts)) {
|
||||
console.error(
|
||||
`Couldn't find registered fonts for font-family "${x}"`,
|
||||
Fonts.registered,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (metadata?.local) {
|
||||
// don't inline local fonts
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
fonts.map(
|
||||
async (font) => `@font-face {
|
||||
font-family: ${font.fontFace.family};
|
||||
src: url(${await getSource(font)});
|
||||
}`,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return fontFaces.flat();
|
||||
};
|
||||
|
||||
@@ -16,7 +16,6 @@ import type {
|
||||
SocketId,
|
||||
UserIdleState,
|
||||
Device,
|
||||
PendingExcalidrawElements,
|
||||
} from "../types";
|
||||
import type { MakeBrand } from "../utility-types";
|
||||
|
||||
@@ -34,7 +33,6 @@ export type StaticCanvasRenderConfig = {
|
||||
isExporting: boolean;
|
||||
embedsValidationStatus: EmbedsValidationStatus;
|
||||
elementsPendingErasure: ElementsPendingErasure;
|
||||
pendingFlowchartNodes: PendingExcalidrawElements | null;
|
||||
};
|
||||
|
||||
export type SVGRenderConfig = {
|
||||
|
||||
@@ -1209,14 +1209,16 @@ export const snapResizingElements = (
|
||||
};
|
||||
|
||||
export const snapNewElement = (
|
||||
newElement: ExcalidrawElement,
|
||||
draggingElement: ExcalidrawElement,
|
||||
appState: AppState,
|
||||
event: KeyboardModifiersObject,
|
||||
origin: Vector2D,
|
||||
dragOffset: Vector2D,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
if (!isSnappingEnabled({ event, selectedElements: [newElement], appState })) {
|
||||
if (
|
||||
!isSnappingEnabled({ event, selectedElements: [draggingElement], appState })
|
||||
) {
|
||||
return {
|
||||
snapOffset: { x: 0, y: 0 },
|
||||
snapLines: [],
|
||||
@@ -1238,7 +1240,7 @@ export const snapNewElement = (
|
||||
const nearestSnapsY: Snaps = [];
|
||||
|
||||
getPointSnaps(
|
||||
[newElement],
|
||||
[draggingElement],
|
||||
selectionSnapPoints,
|
||||
appState,
|
||||
event,
|
||||
@@ -1257,13 +1259,13 @@ export const snapNewElement = (
|
||||
nearestSnapsX.length = 0;
|
||||
nearestSnapsY.length = 0;
|
||||
|
||||
const corners = getElementsCorners([newElement], elementsMap, {
|
||||
const corners = getElementsCorners([draggingElement], elementsMap, {
|
||||
boundingBoxCorners: true,
|
||||
omitCenter: true,
|
||||
});
|
||||
|
||||
getPointSnaps(
|
||||
[newElement],
|
||||
[draggingElement],
|
||||
corners,
|
||||
appState,
|
||||
event,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
import { reseed } from "../random";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { render, waitFor } from "./test-utils";
|
||||
import { act, render, waitFor } from "./test-utils";
|
||||
import { Excalidraw } from "../index";
|
||||
import { expect } from "vitest";
|
||||
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
||||
@@ -90,7 +89,7 @@ describe("Test <MermaidToExcalidraw/>", () => {
|
||||
|
||||
it("should open mermaid popup when active tool is mermaid", async () => {
|
||||
const dialog = document.querySelector(".ttd-dialog")!;
|
||||
await waitFor(() => expect(dialog.querySelector("canvas")).not.toBeNull());
|
||||
await waitFor(() => dialog.querySelector("canvas"));
|
||||
expect(dialog.outerHTML).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -104,9 +103,19 @@ describe("Test <MermaidToExcalidraw/>", () => {
|
||||
|
||||
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
|
||||
|
||||
expect(editor.textContent).toMatchSnapshot();
|
||||
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]"
|
||||
`);
|
||||
|
||||
updateTextEditor(editor, "flowchart TD1");
|
||||
await act(async () => {
|
||||
updateTextEditor(editor, "flowchart TD1");
|
||||
await new Promise((cb) => setTimeout(cb, 0));
|
||||
});
|
||||
editor = await getTextEditor(selector, false);
|
||||
|
||||
expect(editor.textContent).toBe("flowchart TD1");
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
// 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=""><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="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
|
||||
A[Christmas] -->|Get money| B(Go shopping)
|
||||
B --> C{Let me think}
|
||||
C -->|One| D[Laptop]
|
||||
C -->|Two| E[iPhone]
|
||||
C -->|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,6 +812,7 @@ 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,
|
||||
@@ -840,7 +841,6 @@ 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,6 +1015,7 @@ 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,
|
||||
@@ -1043,7 +1044,6 @@ 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,6 +1228,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -1256,7 +1257,6 @@ 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,6 +1556,7 @@ 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,
|
||||
@@ -1584,7 +1585,6 @@ 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,6 +1884,7 @@ 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,
|
||||
@@ -1912,7 +1913,6 @@ 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,6 +2097,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -2125,7 +2126,6 @@ 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,6 +2334,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -2362,7 +2363,6 @@ 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,6 +2632,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -2660,7 +2661,6 @@ 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,6 +2998,7 @@ 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,
|
||||
@@ -3026,7 +3027,6 @@ 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,6 +3470,7 @@ 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,
|
||||
@@ -3498,7 +3499,6 @@ 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,6 +3790,7 @@ 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,
|
||||
@@ -3818,7 +3819,6 @@ 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,6 +4110,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -4138,7 +4139,6 @@ 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,6 +5293,7 @@ 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,
|
||||
@@ -5321,7 +5322,6 @@ 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,6 +6417,7 @@ 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,
|
||||
@@ -6445,7 +6446,6 @@ 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,6 +7349,7 @@ 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,
|
||||
@@ -7377,7 +7378,6 @@ 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,6 +8258,7 @@ 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,
|
||||
@@ -8286,7 +8287,6 @@ 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,6 +9149,7 @@ 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,
|
||||
@@ -9177,7 +9178,6 @@ 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,6 +29,7 @@ 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,
|
||||
@@ -56,7 +57,6 @@ 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,6 +628,7 @@ 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,
|
||||
@@ -655,7 +656,6 @@ 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,6 +1131,7 @@ 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,
|
||||
@@ -1158,7 +1159,6 @@ 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,6 +1496,7 @@ 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,
|
||||
@@ -1523,7 +1524,6 @@ 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,6 +1862,7 @@ 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,
|
||||
@@ -1889,7 +1890,6 @@ 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,6 +2126,7 @@ 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,
|
||||
@@ -2153,7 +2154,6 @@ 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,6 +2563,7 @@ 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,
|
||||
@@ -2590,7 +2591,6 @@ 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,6 +2859,7 @@ 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,
|
||||
@@ -2886,7 +2887,6 @@ 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,6 +3140,7 @@ 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,
|
||||
@@ -3167,7 +3168,6 @@ 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,6 +3431,7 @@ 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,
|
||||
@@ -3458,7 +3459,6 @@ 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,6 +3714,7 @@ 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,
|
||||
@@ -3741,7 +3742,6 @@ 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,6 +3946,7 @@ 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,
|
||||
@@ -3973,7 +3974,6 @@ 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,6 +4202,7 @@ 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,
|
||||
@@ -4229,7 +4230,6 @@ 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,6 +4472,7 @@ 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,
|
||||
@@ -4499,7 +4500,6 @@ 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,6 +4700,7 @@ 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,
|
||||
@@ -4727,7 +4728,6 @@ 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,6 +4928,7 @@ 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,
|
||||
@@ -4955,7 +4956,6 @@ 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,6 +5154,7 @@ 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,
|
||||
@@ -5181,7 +5182,6 @@ 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,6 +5380,7 @@ 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,
|
||||
@@ -5407,7 +5408,6 @@ 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,6 +5636,7 @@ 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,
|
||||
@@ -5663,7 +5664,6 @@ 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,6 +5964,7 @@ 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,
|
||||
@@ -5991,7 +5992,6 @@ 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,6 +6386,7 @@ 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,
|
||||
@@ -6413,7 +6414,6 @@ 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,6 +6761,7 @@ 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,
|
||||
@@ -6788,7 +6789,6 @@ 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,6 +7077,7 @@ 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,
|
||||
@@ -7104,7 +7105,6 @@ 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,6 +7372,7 @@ 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,
|
||||
@@ -7399,7 +7400,6 @@ 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,6 +7598,7 @@ 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,
|
||||
@@ -7625,7 +7626,6 @@ 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,6 +7950,7 @@ 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,
|
||||
@@ -7977,7 +7978,6 @@ 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,6 +8302,7 @@ 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,
|
||||
@@ -8329,7 +8330,6 @@ 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,6 +8703,7 @@ 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,
|
||||
@@ -8730,7 +8731,6 @@ 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,6 +8987,7 @@ 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,
|
||||
@@ -9014,7 +9015,6 @@ 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,6 +9249,7 @@ 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,
|
||||
@@ -9276,7 +9277,6 @@ 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,6 +9510,7 @@ 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,
|
||||
@@ -9537,7 +9538,6 @@ 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,6 +9738,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -9765,7 +9766,6 @@ 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,6 +10036,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -10063,7 +10064,6 @@ 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,6 +10373,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -10400,7 +10401,6 @@ 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,6 +10605,7 @@ 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,
|
||||
@@ -10632,7 +10633,6 @@ 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,6 +11055,7 @@ 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,
|
||||
@@ -11082,7 +11083,6 @@ 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,6 +11306,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -11333,7 +11334,6 @@ 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,6 +11542,7 @@ 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,
|
||||
@@ -11569,7 +11570,6 @@ 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,6 +11780,7 @@ 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,
|
||||
@@ -11807,7 +11808,6 @@ 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,6 +12178,7 @@ 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,
|
||||
@@ -12205,7 +12206,6 @@ 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,6 +12422,7 @@ 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,
|
||||
@@ -12449,7 +12450,6 @@ 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,6 +12660,7 @@ 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,
|
||||
@@ -12687,7 +12688,6 @@ 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,6 +12898,7 @@ 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,
|
||||
@@ -12925,7 +12926,6 @@ 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,6 +13142,7 @@ 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,
|
||||
@@ -13169,7 +13170,6 @@ 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,6 +13471,7 @@ 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,
|
||||
@@ -13498,7 +13499,6 @@ 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,6 +13640,7 @@ 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,
|
||||
@@ -13667,7 +13668,6 @@ 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,6 +13925,7 @@ 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,
|
||||
@@ -13952,7 +13953,6 @@ 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,6 +14189,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -14216,7 +14217,6 @@ 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,6 +14461,7 @@ 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,
|
||||
@@ -14488,7 +14489,6 @@ 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,6 +14619,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -14646,7 +14647,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"isRotating": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"newElement": null,
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 0,
|
||||
"offsetTop": 0,
|
||||
@@ -15312,6 +15312,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -15339,7 +15340,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"isRotating": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"newElement": null,
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 0,
|
||||
"offsetTop": 0,
|
||||
@@ -15929,6 +15929,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -15956,7 +15957,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"isRotating": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"newElement": null,
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 0,
|
||||
"offsetTop": 0,
|
||||
@@ -16546,6 +16546,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -16573,7 +16574,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"isRotating": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"newElement": null,
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 0,
|
||||
"offsetTop": 0,
|
||||
@@ -17255,6 +17255,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -17282,7 +17283,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
||||
"isRotating": false,
|
||||
"lastPointerDownWith": "mouse",
|
||||
"multiElement": null,
|
||||
"newElement": null,
|
||||
"objectsSnapModeEnabled": false,
|
||||
"offsetLeft": 0,
|
||||
"offsetTop": 0,
|
||||
@@ -18002,6 +18002,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -18029,7 +18030,6 @@ 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,6 +18473,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -18500,7 +18501,6 @@ 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,6 +18992,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -19019,7 +19020,6 @@ 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,6 +19445,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -19472,7 +19473,6 @@ 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,17 +1,5 @@
|
||||
// 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"
|
||||
@@ -22,36 +10,3 @@ 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,6 +29,7 @@ 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,
|
||||
@@ -57,7 +58,6 @@ 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,6 +438,7 @@ 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,
|
||||
@@ -466,7 +467,6 @@ 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,6 +838,7 @@ 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",
|
||||
@@ -866,7 +867,6 @@ 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,6 +1377,7 @@ 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,
|
||||
@@ -1405,7 +1406,6 @@ 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,6 +1575,7 @@ 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,
|
||||
@@ -1603,7 +1604,6 @@ 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,6 +1944,7 @@ 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,
|
||||
@@ -1972,7 +1973,6 @@ 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,6 +2178,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -2206,7 +2207,6 @@ 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,6 +2352,7 @@ 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,
|
||||
@@ -2380,7 +2381,6 @@ 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,6 +2666,7 @@ 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,
|
||||
@@ -2694,7 +2695,6 @@ 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,6 +2906,7 @@ 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,
|
||||
@@ -2934,7 +2935,6 @@ 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,6 +3143,7 @@ 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,
|
||||
@@ -3171,7 +3172,6 @@ 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,6 +3367,7 @@ 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,
|
||||
@@ -3395,7 +3396,6 @@ 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,6 +3617,7 @@ 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,
|
||||
@@ -3645,7 +3646,6 @@ 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,6 +3922,7 @@ 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,
|
||||
@@ -3950,7 +3951,6 @@ 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,6 +4330,37 @@ 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,
|
||||
@@ -4358,7 +4389,6 @@ 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,
|
||||
@@ -4607,6 +4637,37 @@ 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,
|
||||
@@ -4635,7 +4696,6 @@ 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,
|
||||
@@ -4854,6 +4914,37 @@ 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,
|
||||
@@ -4882,7 +4973,6 @@ 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,
|
||||
@@ -5058,6 +5148,7 @@ 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,
|
||||
@@ -5086,7 +5177,6 @@ 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,
|
||||
@@ -5251,6 +5341,7 @@ 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",
|
||||
@@ -5279,7 +5370,6 @@ 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,
|
||||
@@ -5627,6 +5717,7 @@ 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,
|
||||
@@ -5655,7 +5746,6 @@ 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,
|
||||
@@ -5911,6 +6001,7 @@ 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,
|
||||
@@ -5939,7 +6030,6 @@ 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,
|
||||
@@ -6713,6 +6803,7 @@ 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,
|
||||
@@ -6741,7 +6832,6 @@ 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,
|
||||
@@ -7037,6 +7127,7 @@ 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,
|
||||
@@ -7065,7 +7156,6 @@ 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,
|
||||
@@ -7307,6 +7397,7 @@ 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,
|
||||
@@ -7335,7 +7426,6 @@ 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,
|
||||
@@ -7535,6 +7625,7 @@ 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,
|
||||
@@ -7563,7 +7654,6 @@ 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,
|
||||
@@ -7766,6 +7856,7 @@ 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,
|
||||
@@ -7794,7 +7885,6 @@ 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,
|
||||
@@ -7940,6 +8030,7 @@ 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,
|
||||
@@ -7968,7 +8059,6 @@ 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,
|
||||
@@ -8114,6 +8204,7 @@ 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,
|
||||
@@ -8142,7 +8233,6 @@ 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,
|
||||
@@ -8288,6 +8378,7 @@ 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,
|
||||
@@ -8316,7 +8407,6 @@ 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,
|
||||
@@ -8504,6 +8594,7 @@ 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,
|
||||
@@ -8532,7 +8623,6 @@ 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,
|
||||
@@ -8719,6 +8809,7 @@ 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,
|
||||
@@ -8747,7 +8838,6 @@ 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,
|
||||
@@ -8907,6 +8997,7 @@ 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,
|
||||
@@ -8935,7 +9026,6 @@ 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,
|
||||
@@ -9123,6 +9213,7 @@ 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,
|
||||
@@ -9151,7 +9242,6 @@ 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,
|
||||
@@ -9297,6 +9387,7 @@ 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,
|
||||
@@ -9325,7 +9416,6 @@ 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,
|
||||
@@ -9512,6 +9602,7 @@ 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,
|
||||
@@ -9540,7 +9631,6 @@ 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,
|
||||
@@ -9686,6 +9776,7 @@ 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,
|
||||
@@ -9714,7 +9805,6 @@ 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,
|
||||
@@ -9874,6 +9964,7 @@ 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,
|
||||
@@ -9902,7 +9993,6 @@ 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,
|
||||
@@ -10048,6 +10138,7 @@ 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,
|
||||
@@ -10076,7 +10167,6 @@ 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,
|
||||
@@ -10556,6 +10646,7 @@ 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,
|
||||
@@ -10584,7 +10675,6 @@ 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,
|
||||
@@ -10827,6 +10917,7 @@ 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,
|
||||
@@ -10855,7 +10946,6 @@ 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,
|
||||
@@ -10947,6 +11037,7 @@ 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,
|
||||
@@ -10975,7 +11066,6 @@ 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,
|
||||
@@ -11140,6 +11230,7 @@ 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,
|
||||
@@ -11168,7 +11259,6 @@ 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,
|
||||
@@ -11445,6 +11535,7 @@ 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,
|
||||
@@ -11473,7 +11564,6 @@ 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,
|
||||
@@ -11851,6 +11941,7 @@ 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,
|
||||
@@ -11879,7 +11970,6 @@ 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,
|
||||
@@ -12458,6 +12548,7 @@ 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,
|
||||
@@ -12486,7 +12577,6 @@ 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,
|
||||
@@ -12581,6 +12671,7 @@ 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",
|
||||
@@ -12609,7 +12700,6 @@ 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,
|
||||
@@ -13159,6 +13249,37 @@ 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,
|
||||
@@ -13187,7 +13308,6 @@ 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,
|
||||
@@ -13491,6 +13611,37 @@ 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,
|
||||
@@ -13519,7 +13670,6 @@ 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,
|
||||
@@ -13750,6 +13900,7 @@ 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,
|
||||
@@ -13778,7 +13929,6 @@ 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,
|
||||
@@ -13870,6 +14020,7 @@ 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,
|
||||
@@ -13898,7 +14049,6 @@ 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,
|
||||
@@ -14243,6 +14393,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -14271,7 +14422,6 @@ 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,
|
||||
@@ -14363,6 +14513,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
||||
"currentItemTextAlign": "left",
|
||||
"cursorButton": "up",
|
||||
"defaultSidebarDockedPreference": false,
|
||||
"draggingElement": null,
|
||||
"editingElement": null,
|
||||
"editingFrame": null,
|
||||
"editingGroupId": null,
|
||||
@@ -14391,7 +14542,6 @@ 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,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Excalidraw } from "../index";
|
||||
import { CODES } from "../keys";
|
||||
import { API } from "../tests/helpers/api";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { act, render } from "./test-utils";
|
||||
import { render } from "./test-utils";
|
||||
import { Excalidraw } from "../index";
|
||||
import { defaultLang, setLanguage } from "../i18n";
|
||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||
@@ -16,6 +15,8 @@ import {
|
||||
actionAlignRight,
|
||||
} from "../actions";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
const createAndSelectTwoRectangles = () => {
|
||||
@@ -58,9 +59,7 @@ describe("aligning", () => {
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
mouse.reset();
|
||||
|
||||
await act(() => {
|
||||
return setLanguage(defaultLang);
|
||||
});
|
||||
await setLanguage(defaultLang);
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
});
|
||||
|
||||
@@ -157,7 +156,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(110);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
// Check if x position did not change
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
@@ -176,7 +175,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(110);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(60);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(55);
|
||||
@@ -202,7 +201,7 @@ describe("aligning", () => {
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
h.app.actionManager.executeAction(actionGroup);
|
||||
|
||||
mouse.reset();
|
||||
UI.clickTool("rectangle");
|
||||
@@ -223,7 +222,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
h.app.actionManager.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
@@ -237,7 +236,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
h.app.actionManager.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(200);
|
||||
@@ -251,7 +250,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
h.app.actionManager.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
@@ -265,7 +264,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
h.app.actionManager.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(200);
|
||||
@@ -279,7 +278,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(150);
|
||||
@@ -293,7 +292,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(150);
|
||||
@@ -316,7 +315,7 @@ describe("aligning", () => {
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
h.app.actionManager.executeAction(actionGroup);
|
||||
|
||||
mouse.reset();
|
||||
UI.clickTool("rectangle");
|
||||
@@ -332,7 +331,7 @@ describe("aligning", () => {
|
||||
mouse.click();
|
||||
});
|
||||
|
||||
API.executeAction(actionGroup);
|
||||
h.app.actionManager.executeAction(actionGroup);
|
||||
|
||||
// Select the first group.
|
||||
// The second group is already selected because it was the last group created
|
||||
@@ -350,7 +349,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].y).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
h.app.actionManager.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
@@ -366,7 +365,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].y).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
h.app.actionManager.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(300);
|
||||
@@ -382,7 +381,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
h.app.actionManager.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
@@ -398,7 +397,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
h.app.actionManager.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(300);
|
||||
@@ -414,7 +413,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].y).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(200);
|
||||
@@ -430,7 +429,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(200);
|
||||
@@ -455,7 +454,7 @@ describe("aligning", () => {
|
||||
});
|
||||
|
||||
// Create first group of rectangles
|
||||
API.executeAction(actionGroup);
|
||||
h.app.actionManager.executeAction(actionGroup);
|
||||
|
||||
mouse.reset();
|
||||
UI.clickTool("rectangle");
|
||||
@@ -469,7 +468,7 @@ describe("aligning", () => {
|
||||
});
|
||||
|
||||
// Create the nested group
|
||||
API.executeAction(actionGroup);
|
||||
h.app.actionManager.executeAction(actionGroup);
|
||||
|
||||
mouse.reset();
|
||||
UI.clickTool("rectangle");
|
||||
@@ -491,7 +490,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].y).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignTop);
|
||||
h.app.actionManager.executeAction(actionAlignTop);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(100);
|
||||
@@ -507,7 +506,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].y).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignBottom);
|
||||
h.app.actionManager.executeAction(actionAlignBottom);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(200);
|
||||
@@ -523,7 +522,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignLeft);
|
||||
h.app.actionManager.executeAction(actionAlignLeft);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(0);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(100);
|
||||
@@ -539,7 +538,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignRight);
|
||||
h.app.actionManager.executeAction(actionAlignRight);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(100);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(200);
|
||||
@@ -555,7 +554,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].y).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].y).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignVerticallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignVerticallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].y).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].y).toEqual(150);
|
||||
@@ -571,7 +570,7 @@ describe("aligning", () => {
|
||||
expect(API.getSelectedElements()[2].x).toEqual(200);
|
||||
expect(API.getSelectedElements()[3].x).toEqual(300);
|
||||
|
||||
API.executeAction(actionAlignHorizontallyCentered);
|
||||
h.app.actionManager.executeAction(actionAlignHorizontallyCentered);
|
||||
|
||||
expect(API.getSelectedElements()[0].x).toEqual(50);
|
||||
expect(API.getSelectedElements()[1].x).toEqual(150);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { fireEvent, queryByTestId, render, waitFor } from "./test-utils";
|
||||
import { 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");
|
||||
});
|
||||
|
||||
await API.drop(
|
||||
API.drop(
|
||||
new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
@@ -69,7 +69,7 @@ describe("appState", () => {
|
||||
UI.clickTool("text");
|
||||
|
||||
expect(h.state.currentItemFontSize).toBe(30);
|
||||
fireEvent.click(queryByTestId(container, "fontSize-small")!);
|
||||
queryByTestId(container, "fontSize-small")!.click();
|
||||
expect(h.state.currentItemFontSize).toBe(16);
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { fireEvent, render } from "./test-utils";
|
||||
import { Excalidraw, isLinearElement } from "../index";
|
||||
import { UI, Pointer, Keyboard } from "./helpers/ui";
|
||||
@@ -38,7 +37,7 @@ describe("element binding", () => {
|
||||
[100, 0],
|
||||
],
|
||||
});
|
||||
API.setElements([rect, arrow]);
|
||||
h.elements = [rect, arrow];
|
||||
expect(arrow.startBinding).toBe(null);
|
||||
|
||||
// select arrow
|
||||
@@ -226,7 +225,7 @@ describe("element binding", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: 0,
|
||||
@@ -268,7 +267,7 @@ describe("element binding", () => {
|
||||
height: 100,
|
||||
});
|
||||
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: 0,
|
||||
@@ -363,13 +362,13 @@ describe("element binding", () => {
|
||||
],
|
||||
});
|
||||
|
||||
API.setElements([rectangle1, arrow1, arrow2, text1]);
|
||||
h.elements = [rectangle1, arrow1, arrow2, text1];
|
||||
|
||||
API.setSelectedElements([text1]);
|
||||
|
||||
expect(h.state.selectedElementIds[text1.id]).toBe(true);
|
||||
|
||||
API.executeAction(actionWrapTextInContainer);
|
||||
h.app.actionManager.executeAction(actionWrapTextInContainer);
|
||||
|
||||
// new text container will be placed before the text element
|
||||
const container = h.elements.at(-2)!;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
import ReactDOM from "react-dom";
|
||||
import { render, waitFor, GlobalTestState } from "./test-utils";
|
||||
@@ -280,7 +279,7 @@ describe("pasting & frames", () => {
|
||||
});
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
|
||||
API.setElements([frame]);
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect],
|
||||
@@ -319,7 +318,7 @@ describe("pasting & frames", () => {
|
||||
y: 100,
|
||||
});
|
||||
|
||||
API.setElements([frame]);
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect, rect2],
|
||||
@@ -362,7 +361,7 @@ describe("pasting & frames", () => {
|
||||
groupIds: ["g1"],
|
||||
});
|
||||
|
||||
API.setElements([frame]);
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect, rect2],
|
||||
@@ -413,7 +412,7 @@ describe("pasting & frames", () => {
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
API.setElements([frame]);
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect, rect2, frame2],
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
render,
|
||||
@@ -160,7 +159,7 @@ describe("contextMenu element", () => {
|
||||
width: 200,
|
||||
backgroundColor: "red",
|
||||
});
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
API.setSelectedElements([rect1]);
|
||||
|
||||
// lower z-index
|
||||
@@ -608,7 +607,7 @@ describe("contextMenu element", () => {
|
||||
fillStyle: "solid",
|
||||
groupIds: ["g1"],
|
||||
});
|
||||
API.setElements([rectangle1, rectangle2]);
|
||||
h.elements = [rectangle1, rectangle2];
|
||||
|
||||
mouse.rightClickAt(50, 50);
|
||||
expect(API.getSelectedElements().length).toBe(2);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Excalidraw } from "../index";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Excalidraw } from "../index";
|
||||
import { render } from "../tests/test-utils";
|
||||
@@ -17,7 +16,7 @@ const h = window.h;
|
||||
describe("element locking", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
API.setElements([]);
|
||||
h.elements = [];
|
||||
});
|
||||
|
||||
it("click-selecting a locked element is disabled", () => {
|
||||
@@ -29,7 +28,7 @@ describe("element locking", () => {
|
||||
locked: true,
|
||||
});
|
||||
|
||||
API.setElements([lockedRectangle]);
|
||||
h.elements = [lockedRectangle];
|
||||
|
||||
mouse.clickAt(50, 50);
|
||||
expect(API.getSelectedElements().length).toBe(0);
|
||||
@@ -46,7 +45,7 @@ describe("element locking", () => {
|
||||
y: 100,
|
||||
});
|
||||
|
||||
API.setElements([lockedRectangle]);
|
||||
h.elements = [lockedRectangle];
|
||||
|
||||
mouse.downAt(50, 50);
|
||||
mouse.moveTo(250, 250);
|
||||
@@ -63,7 +62,7 @@ describe("element locking", () => {
|
||||
locked: true,
|
||||
});
|
||||
|
||||
API.setElements([lockedRectangle]);
|
||||
h.elements = [lockedRectangle];
|
||||
|
||||
mouse.downAt(50, 50);
|
||||
mouse.moveTo(100, 100);
|
||||
@@ -86,7 +85,7 @@ describe("element locking", () => {
|
||||
locked: true,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, lockedRectangle]);
|
||||
h.elements = [rectangle, lockedRectangle];
|
||||
|
||||
mouse.downAt(50, 50);
|
||||
mouse.moveTo(100, 100);
|
||||
@@ -98,11 +97,11 @@ describe("element locking", () => {
|
||||
});
|
||||
|
||||
it("selectAll shouldn't select locked elements", () => {
|
||||
API.setElements([
|
||||
h.elements = [
|
||||
API.createElement({ type: "rectangle" }),
|
||||
API.createElement({ type: "rectangle", locked: true }),
|
||||
]);
|
||||
API.executeAction(actionSelectAll);
|
||||
];
|
||||
h.app.actionManager.executeAction(actionSelectAll);
|
||||
expect(API.getSelectedElements().length).toBe(1);
|
||||
});
|
||||
|
||||
@@ -121,7 +120,7 @@ describe("element locking", () => {
|
||||
locked: true,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, lockedRectangle]);
|
||||
h.elements = [rectangle, lockedRectangle];
|
||||
expect(API.getSelectedElements().length).toBe(0);
|
||||
mouse.clickAt(50, 50);
|
||||
expect(API.getSelectedElements().length).toBe(1);
|
||||
@@ -143,7 +142,7 @@ describe("element locking", () => {
|
||||
locked: true,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, lockedRectangle]);
|
||||
h.elements = [rectangle, lockedRectangle];
|
||||
expect(API.getSelectedElements().length).toBe(0);
|
||||
mouse.rightClickAt(50, 50);
|
||||
expect(API.getSelectedElements().length).toBe(1);
|
||||
@@ -173,7 +172,7 @@ describe("element locking", () => {
|
||||
locked: true,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, lockedRectangle]);
|
||||
h.elements = [rectangle, lockedRectangle];
|
||||
API.setSelectedElements([rectangle]);
|
||||
expect(API.getSelectedElements().length).toBe(1);
|
||||
expect(API.getSelectedElement().id).toBe(rectangle.id);
|
||||
@@ -204,7 +203,7 @@ describe("element locking", () => {
|
||||
y: 200,
|
||||
});
|
||||
|
||||
API.setElements([rectangle, lockedRectangle]);
|
||||
h.elements = [rectangle, lockedRectangle];
|
||||
|
||||
mouse.clickAt(250, 250);
|
||||
expect(API.getSelectedElements().length).toBe(0);
|
||||
@@ -229,7 +228,7 @@ describe("element locking", () => {
|
||||
containerId: container.id,
|
||||
locked: true,
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
API.setSelectedElements([container]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(h.state.editingElement?.id).not.toBe(text.id);
|
||||
@@ -246,7 +245,7 @@ describe("element locking", () => {
|
||||
height: 100,
|
||||
locked: true,
|
||||
});
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(text.x + 50, text.y + 50);
|
||||
const editor = document.querySelector(
|
||||
@@ -268,7 +267,7 @@ describe("element locking", () => {
|
||||
height: 100,
|
||||
locked: true,
|
||||
});
|
||||
API.setElements([text]);
|
||||
h.elements = [text];
|
||||
UI.clickTool("selection");
|
||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||
const editor = document.querySelector(
|
||||
@@ -299,7 +298,7 @@ describe("element locking", () => {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
|
||||
UI.clickTool("selection");
|
||||
mouse.clickAt(container.x + 10, container.y + 10);
|
||||
@@ -339,7 +338,7 @@ describe("element locking", () => {
|
||||
mutateElement(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
|
||||
UI.clickTool("selection");
|
||||
mouse.doubleClickAt(container.width / 2, container.height / 2);
|
||||
@@ -373,7 +372,7 @@ describe("element locking", () => {
|
||||
mutateElement(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
});
|
||||
API.setElements([container, text]);
|
||||
h.elements = [container, text];
|
||||
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(container.width / 2, container.height / 2);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { render, waitFor } from "./test-utils";
|
||||
import { Excalidraw } from "../index";
|
||||
import { API } from "./helpers/api";
|
||||
@@ -52,7 +51,7 @@ describe("export", () => {
|
||||
blob: pngBlob,
|
||||
metadata: serializeAsJSON(testElements, h.state, {}, "local"),
|
||||
});
|
||||
await API.drop(pngBlobEmbedded);
|
||||
API.drop(pngBlobEmbedded);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
@@ -72,7 +71,7 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded png (legacy v1)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
|
||||
API.drop(await API.loadFile("./fixtures/test_embedded_v1.png"));
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "test" }),
|
||||
@@ -81,7 +80,7 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded png (v2)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
|
||||
API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.png"));
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "😀" }),
|
||||
@@ -90,7 +89,7 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded svg (legacy v1)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
|
||||
API.drop(await API.loadFile("./fixtures/test_embedded_v1.svg"));
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "test" }),
|
||||
@@ -99,7 +98,7 @@ describe("export", () => {
|
||||
});
|
||||
|
||||
it("import embedded svg (v2)", async () => {
|
||||
await API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
|
||||
API.drop(await API.loadFile("./fixtures/smiley_embedded_v2.svg"));
|
||||
await waitFor(() => {
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ type: "text", text: "😀" }),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from "react";
|
||||
import { act, render } from "./test-utils";
|
||||
import { render } from "./test-utils";
|
||||
import { API } from "./helpers/api";
|
||||
|
||||
import { Excalidraw } from "../index";
|
||||
@@ -7,17 +6,6 @@ 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 />);
|
||||
@@ -34,9 +22,7 @@ describe("fitToContent", () => {
|
||||
|
||||
expect(h.state.zoom.value).toBe(1);
|
||||
|
||||
act(() => {
|
||||
h.app.scrollToContent(rectElement, { fitToContent: true });
|
||||
});
|
||||
h.app.scrollToContent(rectElement, { fitToContent: true });
|
||||
|
||||
// element is 10x taller than the viewport size,
|
||||
// zoom should be at least 1/10
|
||||
@@ -65,10 +51,8 @@ describe("fitToContent", () => {
|
||||
|
||||
expect(h.state.zoom.value).toBe(1);
|
||||
|
||||
act(() => {
|
||||
h.app.scrollToContent([topLeft, bottomRight], {
|
||||
fitToContent: true,
|
||||
});
|
||||
h.app.scrollToContent([topLeft, bottomRight], {
|
||||
fitToContent: true,
|
||||
});
|
||||
|
||||
// elements take 100x100, which is 10x bigger than the viewport size,
|
||||
@@ -93,9 +77,7 @@ describe("fitToContent", () => {
|
||||
expect(h.state.scrollX).toBe(0);
|
||||
expect(h.state.scrollY).toBe(0);
|
||||
|
||||
act(() => {
|
||||
h.app.scrollToContent(rectElement);
|
||||
});
|
||||
h.app.scrollToContent(rectElement);
|
||||
|
||||
// zoom level should stay the same
|
||||
expect(h.state.zoom.value).toBe(1);
|
||||
@@ -106,6 +88,14 @@ describe("fitToContent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
const waitForNextAnimationFrame = () => {
|
||||
return new Promise((resolve) => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(resolve);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
describe("fitToContent animated", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(window, "requestAnimationFrame");
|
||||
@@ -128,9 +118,7 @@ describe("fitToContent animated", () => {
|
||||
y: -100,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
h.app.scrollToContent(rectElement, { animate: true });
|
||||
});
|
||||
h.app.scrollToContent(rectElement, { animate: true });
|
||||
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalled();
|
||||
|
||||
@@ -169,9 +157,7 @@ describe("fitToContent animated", () => {
|
||||
expect(h.state.scrollX).toBe(0);
|
||||
expect(h.state.scrollY).toBe(0);
|
||||
|
||||
act(() => {
|
||||
h.app.scrollToContent(rectElement, { animate: true, fitToContent: true });
|
||||
});
|
||||
h.app.scrollToContent(rectElement, { animate: true, fitToContent: true });
|
||||
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalled();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
fireEvent,
|
||||
@@ -20,6 +19,7 @@ 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} />);
|
||||
API.setAppState({
|
||||
h.setState({
|
||||
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]);
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
h.app.actionManager.executeAction(actionFlipHorizontal);
|
||||
const newElement = h.elements[0];
|
||||
await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
|
||||
};
|
||||
|
||||
const checkTwoPointsLineHorizontalFlip = async () => {
|
||||
const originalElement = cloneJSON(h.elements[0]) as ExcalidrawLinearElement;
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
h.app.actionManager.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;
|
||||
API.executeAction(actionFlipVertical);
|
||||
h.app.actionManager.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]);
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
h.app.actionManager.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]);
|
||||
API.executeAction(actionFlipVertical);
|
||||
h.app.actionManager.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]);
|
||||
|
||||
API.executeAction(actionFlipVertical);
|
||||
h.app.actionManager.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]);
|
||||
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
API.executeAction(actionFlipVertical);
|
||||
h.app.actionManager.executeAction(actionFlipHorizontal);
|
||||
h.app.actionManager.executeAction(actionFlipVertical);
|
||||
|
||||
const newElement = h.elements[0];
|
||||
await checkElementsBoundingBox(originalElement, newElement, toleranceInPx);
|
||||
@@ -309,6 +309,7 @@ const MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS = 20;
|
||||
describe("rectangle", () => {
|
||||
it("flips an unrotated rectangle horizontally correctly", async () => {
|
||||
createAndSelectOneRectangle();
|
||||
|
||||
await checkHorizontalFlip();
|
||||
});
|
||||
|
||||
@@ -407,8 +408,8 @@ describe("ellipse", () => {
|
||||
describe("arrow", () => {
|
||||
it("flips an unrotated arrow horizontally with line inside min/max points bounds", async () => {
|
||||
const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||
API.setElements([arrow]);
|
||||
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
|
||||
h.elements = [arrow];
|
||||
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
|
||||
await checkHorizontalFlip(
|
||||
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
|
||||
);
|
||||
@@ -416,8 +417,8 @@ describe("arrow", () => {
|
||||
|
||||
it("flips an unrotated arrow vertically with line inside min/max points bounds", async () => {
|
||||
const arrow = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||
API.setElements([arrow]);
|
||||
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
|
||||
h.elements = [arrow];
|
||||
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
|
||||
|
||||
await checkVerticalFlip(50);
|
||||
});
|
||||
@@ -426,14 +427,12 @@ describe("arrow", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||
API.setElements([line]);
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
},
|
||||
});
|
||||
API.updateElement(line, {
|
||||
h.elements = [line];
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
};
|
||||
mutateElement(line, {
|
||||
angle: originalAngle,
|
||||
});
|
||||
|
||||
@@ -447,14 +446,12 @@ describe("arrow", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
|
||||
API.setElements([line]);
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
},
|
||||
});
|
||||
API.updateElement(line, {
|
||||
h.elements = [line];
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
};
|
||||
mutateElement(line, {
|
||||
angle: originalAngle,
|
||||
});
|
||||
|
||||
@@ -467,8 +464,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");
|
||||
API.setElements([arrow]);
|
||||
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
|
||||
h.elements = [arrow];
|
||||
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
|
||||
|
||||
await checkHorizontalFlip(
|
||||
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
|
||||
@@ -480,9 +477,9 @@ describe("arrow", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
||||
API.updateElement(line, { angle: originalAngle });
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
mutateElement(line, { angle: originalAngle });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkRotatedVerticalFlip(
|
||||
expectedAngle,
|
||||
@@ -493,8 +490,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");
|
||||
API.setElements([arrow]);
|
||||
API.setAppState({ selectedElementIds: { [arrow.id]: true } });
|
||||
h.elements = [arrow];
|
||||
h.app.setState({ selectedElementIds: { [arrow.id]: true } });
|
||||
|
||||
await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
|
||||
});
|
||||
@@ -504,9 +501,9 @@ describe("arrow", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("arrow");
|
||||
API.updateElement(line, { angle: originalAngle });
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
mutateElement(line, { angle: originalAngle });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkRotatedVerticalFlip(
|
||||
expectedAngle,
|
||||
@@ -541,8 +538,8 @@ describe("arrow", () => {
|
||||
describe("line", () => {
|
||||
it("flips an unrotated line horizontally with line inside min/max points bounds", async () => {
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkHorizontalFlip(
|
||||
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
|
||||
@@ -551,8 +548,8 @@ describe("line", () => {
|
||||
|
||||
it("flips an unrotated line vertically with line inside min/max points bounds", async () => {
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
|
||||
});
|
||||
@@ -566,8 +563,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");
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkHorizontalFlip(
|
||||
MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS,
|
||||
@@ -577,8 +574,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");
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkVerticalFlip(MULTIPOINT_LINEAR_ELEMENT_FLIP_TOLERANCE_IN_PIXELS);
|
||||
});
|
||||
@@ -588,9 +585,9 @@ describe("line", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
||||
API.updateElement(line, { angle: originalAngle });
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
mutateElement(line, { angle: originalAngle });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkRotatedHorizontalFlip(
|
||||
expectedAngle,
|
||||
@@ -603,9 +600,9 @@ describe("line", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementsWithCurveOutsideMinMaxPoints("line");
|
||||
API.updateElement(line, { angle: originalAngle });
|
||||
API.setElements([line]);
|
||||
API.setAppState({ selectedElementIds: { [line.id]: true } });
|
||||
mutateElement(line, { angle: originalAngle });
|
||||
h.elements = [line];
|
||||
h.app.setState({ selectedElementIds: { [line.id]: true } });
|
||||
|
||||
await checkRotatedVerticalFlip(
|
||||
expectedAngle,
|
||||
@@ -622,14 +619,12 @@ describe("line", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||
API.setElements([line]);
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
},
|
||||
});
|
||||
API.updateElement(line, {
|
||||
h.elements = [line];
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
};
|
||||
mutateElement(line, {
|
||||
angle: originalAngle,
|
||||
});
|
||||
|
||||
@@ -643,14 +638,12 @@ describe("line", () => {
|
||||
const originalAngle = Math.PI / 4;
|
||||
const expectedAngle = (7 * Math.PI) / 4;
|
||||
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
|
||||
API.setElements([line]);
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
},
|
||||
});
|
||||
API.updateElement(line, {
|
||||
h.elements = [line];
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[line.id]: true,
|
||||
};
|
||||
mutateElement(line, {
|
||||
angle: originalAngle,
|
||||
});
|
||||
|
||||
@@ -676,24 +669,20 @@ describe("freedraw", () => {
|
||||
it("flips an unrotated drawing horizontally correctly", async () => {
|
||||
const draw = createAndReturnOneDraw();
|
||||
// select draw, since not done automatically
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
},
|
||||
});
|
||||
h.state.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
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
},
|
||||
});
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
};
|
||||
await checkVerticalFlip();
|
||||
});
|
||||
|
||||
@@ -703,12 +692,10 @@ describe("freedraw", () => {
|
||||
|
||||
const draw = createAndReturnOneDraw(originalAngle);
|
||||
// select draw, since not done automatically
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
},
|
||||
});
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
};
|
||||
|
||||
await checkRotatedHorizontalFlip(expectedAngle);
|
||||
});
|
||||
@@ -719,12 +706,10 @@ describe("freedraw", () => {
|
||||
|
||||
const draw = createAndReturnOneDraw(originalAngle);
|
||||
// select draw, since not done automatically
|
||||
API.setAppState({
|
||||
selectedElementIds: {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
},
|
||||
});
|
||||
h.state.selectedElementIds = {
|
||||
...h.state.selectedElementIds,
|
||||
[draw.id]: true,
|
||||
};
|
||||
|
||||
await checkRotatedVerticalFlip(expectedAngle);
|
||||
});
|
||||
@@ -782,7 +767,7 @@ describe("image", () => {
|
||||
expect(API.getSelectedElements()[0].type).toEqual("image");
|
||||
expect(h.app.files.fileId).toBeDefined();
|
||||
});
|
||||
API.updateElement(h.elements[0], {
|
||||
mutateElement(h.elements[0], {
|
||||
angle: originalAngle,
|
||||
});
|
||||
await checkRotatedHorizontalFlip(expectedAngle);
|
||||
@@ -801,7 +786,7 @@ describe("image", () => {
|
||||
expect(API.getSelectedElements()[0].type).toEqual("image");
|
||||
expect(h.app.files.fileId).toBeDefined();
|
||||
});
|
||||
API.updateElement(h.elements[0], {
|
||||
mutateElement(h.elements[0], {
|
||||
angle: originalAngle,
|
||||
});
|
||||
|
||||
@@ -842,7 +827,8 @@ describe("mutliple elements", () => {
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
)!;
|
||||
fireEvent.input(editor, { target: { value: "arrow" } });
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
Keyboard.keyPress(KEYS.ESCAPE);
|
||||
|
||||
const rectangle = UI.createElement("rectangle", {
|
||||
x: 0,
|
||||
@@ -856,11 +842,12 @@ describe("mutliple elements", () => {
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
)!;
|
||||
fireEvent.input(editor, { target: { value: "rect\ntext" } });
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
Keyboard.keyPress(KEYS.ESCAPE);
|
||||
|
||||
mouse.select([arrow, rectangle]);
|
||||
API.executeAction(actionFlipHorizontal);
|
||||
API.executeAction(actionFlipVertical);
|
||||
h.app.actionManager.executeAction(actionFlipHorizontal);
|
||||
h.app.actionManager.executeAction(actionFlipVertical);
|
||||
|
||||
const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
const arrowTextPos = getBoundTextElementPosition(
|
||||
|
||||
@@ -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, act } from "../test-utils";
|
||||
import { GlobalTestState, createEvent, fireEvent } from "../test-utils";
|
||||
import fs from "fs";
|
||||
import util from "util";
|
||||
import path from "path";
|
||||
@@ -27,15 +27,12 @@ import {
|
||||
newImageElement,
|
||||
newMagicFrameElement,
|
||||
} from "../../element/newElement";
|
||||
import type { AppState, Point } from "../../types";
|
||||
import type { 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.
|
||||
@@ -44,42 +41,12 @@ 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[]) => {
|
||||
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);
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce((acc, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
}, {} as Record<ExcalidrawElement["id"], true>),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -118,10 +85,8 @@ export class API {
|
||||
};
|
||||
|
||||
static clearSelection = () => {
|
||||
act(() => {
|
||||
// @ts-ignore
|
||||
h.app.clearSelection(null);
|
||||
});
|
||||
// @ts-ignore
|
||||
h.app.clearSelection(null);
|
||||
expect(API.getSelectedElements().length).toBe(0);
|
||||
};
|
||||
|
||||
@@ -396,12 +361,6 @@ export class API {
|
||||
},
|
||||
},
|
||||
});
|
||||
await fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
|
||||
};
|
||||
|
||||
static executeAction = (action: Action) => {
|
||||
act(() => {
|
||||
h.app.actionManager.executeAction(action);
|
||||
});
|
||||
fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
type TransformHandleDirection,
|
||||
} from "../../element/transformHandles";
|
||||
import { KEYS } from "../../keys";
|
||||
import { act, fireEvent, GlobalTestState, screen } from "../test-utils";
|
||||
import { fireEvent, GlobalTestState, screen } from "../test-utils";
|
||||
import { mutateElement } from "../../element/mutateElement";
|
||||
import { API } from "./api";
|
||||
import {
|
||||
@@ -125,10 +125,6 @@ export class Keyboard {
|
||||
Keyboard.keyPress("z");
|
||||
});
|
||||
};
|
||||
|
||||
static exitTextEditor = (textarea: HTMLTextAreaElement) => {
|
||||
fireEvent.keyDown(textarea, { key: KEYS.ESCAPE });
|
||||
};
|
||||
}
|
||||
|
||||
const getElementPointForSelection = (element: ExcalidrawElement): Point => {
|
||||
@@ -303,16 +299,14 @@ const transform = (
|
||||
keyboardModifiers: KeyboardModifiers = {},
|
||||
) => {
|
||||
const elements = Array.isArray(element) ? element : [element];
|
||||
act(() => {
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce(
|
||||
(acc, e) => ({
|
||||
...acc,
|
||||
[e.id]: true,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
h.setState({
|
||||
selectedElementIds: elements.reduce(
|
||||
(acc, e) => ({
|
||||
...acc,
|
||||
[e.id]: true,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
});
|
||||
let handleCoords: TransformHandle | undefined;
|
||||
if (elements.length === 1) {
|
||||
@@ -493,9 +487,7 @@ export class UI {
|
||||
const origElement = h.elements[h.elements.length - 1] as any;
|
||||
|
||||
if (angle !== 0) {
|
||||
act(() => {
|
||||
mutateElement(origElement, { angle });
|
||||
});
|
||||
mutateElement(origElement, { angle });
|
||||
}
|
||||
|
||||
return proxy(origElement);
|
||||
@@ -519,9 +511,8 @@ export class UI {
|
||||
}
|
||||
|
||||
fireEvent.input(editor, { target: { value: text } });
|
||||
act(() => {
|
||||
editor.blur();
|
||||
});
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
editor.blur();
|
||||
|
||||
return isTextElement(element)
|
||||
? element
|
||||
@@ -532,14 +523,6 @@ 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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import "../global.d.ts";
|
||||
import React from "react";
|
||||
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 } from "../types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import type { AppState, ExcalidrawImperativeAPI } from "../types";
|
||||
import { arrayToMap, resolvablePromise } from "../utils";
|
||||
import {
|
||||
COLOR_PALETTE,
|
||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
|
||||
@@ -44,6 +44,7 @@ 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;
|
||||
|
||||
@@ -94,7 +95,7 @@ describe("history", () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
|
||||
API.setElements([rect]);
|
||||
h.elements = [rect];
|
||||
|
||||
const corrupedEntry = HistoryEntry.create(
|
||||
AppStateChange.empty(),
|
||||
@@ -117,6 +118,7 @@ describe("history", () => {
|
||||
arrayToMap(h.elements) as SceneElementsMap,
|
||||
appState,
|
||||
Snapshot.empty(),
|
||||
{} as Scene,
|
||||
) as any,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -138,6 +140,7 @@ describe("history", () => {
|
||||
arrayToMap(h.elements) as SceneElementsMap,
|
||||
appState,
|
||||
Snapshot.empty(),
|
||||
{} as Scene,
|
||||
) as any,
|
||||
);
|
||||
} catch (e) {
|
||||
@@ -155,7 +158,7 @@ describe("history", () => {
|
||||
const rect1 = API.createElement({ type: "rectangle", groupIds: ["A"] });
|
||||
const rect2 = API.createElement({ type: "rectangle", groupIds: ["A"] });
|
||||
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
mouse.select(rect1);
|
||||
assertSelectedElements([rect1, rect2]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
@@ -170,12 +173,19 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
it("should not end up with history entry when there are no elements changes", async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
||||
await render(
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
||||
handleKeyboardGlobally={true}
|
||||
/>,
|
||||
);
|
||||
const excalidrawAPI = await excalidrawAPIPromise;
|
||||
|
||||
const rect1 = API.createElement({ type: "rectangle" });
|
||||
const rect2 = API.createElement({ type: "rectangle" });
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect1, rect2],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
@@ -187,7 +197,7 @@ describe("history", () => {
|
||||
expect.objectContaining({ id: rect2.id, isDeleted: false }),
|
||||
]);
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect1, rect2],
|
||||
storeAction: StoreAction.CAPTURE, // even though the flag is on, same elements are passed, nothing to commit
|
||||
});
|
||||
@@ -437,7 +447,7 @@ describe("history", () => {
|
||||
const undoAction = createUndoAction(h.history, h.store);
|
||||
const redoAction = createRedoAction(h.history, h.store);
|
||||
// noop
|
||||
API.executeAction(undoAction);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
||||
]);
|
||||
@@ -446,21 +456,21 @@ describe("history", () => {
|
||||
expect.objectContaining({ id: "A" }),
|
||||
expect.objectContaining({ id: rectangle.id }),
|
||||
]);
|
||||
API.executeAction(undoAction);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
||||
expect.objectContaining({ id: rectangle.id, isDeleted: true }),
|
||||
]);
|
||||
|
||||
// noop
|
||||
API.executeAction(undoAction);
|
||||
act(() => h.app.actionManager.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);
|
||||
|
||||
API.executeAction(redoAction);
|
||||
act(() => h.app.actionManager.executeAction(redoAction));
|
||||
expect(h.elements).toEqual([
|
||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
||||
expect.objectContaining({ id: rectangle.id, isDeleted: false }),
|
||||
@@ -485,7 +495,7 @@ describe("history", () => {
|
||||
expect(h.elements).toEqual([expect.objectContaining({ id: "A" })]),
|
||||
);
|
||||
|
||||
await API.drop(
|
||||
API.drop(
|
||||
new Blob(
|
||||
[
|
||||
JSON.stringify({
|
||||
@@ -513,7 +523,7 @@ describe("history", () => {
|
||||
|
||||
const undoAction = createUndoAction(h.history, h.store);
|
||||
const redoAction = createRedoAction(h.history, h.store);
|
||||
API.executeAction(undoAction);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining({ id: "A", isDeleted: false }),
|
||||
@@ -525,7 +535,7 @@ describe("history", () => {
|
||||
]);
|
||||
expect(h.state.viewBackgroundColor).toBe("#FFF");
|
||||
|
||||
API.executeAction(redoAction);
|
||||
act(() => h.app.actionManager.executeAction(redoAction));
|
||||
expect(h.state.viewBackgroundColor).toBe("#000");
|
||||
expect(API.getSnapshot()).toEqual([
|
||||
expect.objectContaining({ id: "A", isDeleted: true }),
|
||||
@@ -538,8 +548,10 @@ 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: {
|
||||
@@ -549,11 +561,9 @@ describe("history", () => {
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const excalidrawAPI = await excalidrawAPIPromise;
|
||||
|
||||
expect(h.state.isLoading).toBe(false);
|
||||
expect(h.state.name).toBe("Old name");
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
name: "New name",
|
||||
},
|
||||
@@ -564,7 +574,7 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
expect(h.state.name).toBe("New name");
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
viewBackgroundColor: "#000",
|
||||
},
|
||||
@@ -576,7 +586,7 @@ describe("history", () => {
|
||||
expect(h.state.viewBackgroundColor).toBe("#000");
|
||||
|
||||
// just to double check that same change is not recorded
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
name: "New name",
|
||||
viewBackgroundColor: "#000",
|
||||
@@ -1050,7 +1060,7 @@ describe("history", () => {
|
||||
x: 100,
|
||||
});
|
||||
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
mouse.select(rect1);
|
||||
assertSelectedElements([rect1, rect2]);
|
||||
expect(API.getUndoStack().length).toBe(1);
|
||||
@@ -1193,7 +1203,7 @@ describe("history", () => {
|
||||
const rect2 = UI.createElement("rectangle", { x: 20, y: 20 });
|
||||
const rect3 = UI.createElement("rectangle", { x: 40, y: 40 });
|
||||
|
||||
API.executeAction(actionSendBackward);
|
||||
act(() => h.app.actionManager.executeAction(actionSendBackward));
|
||||
|
||||
expect(API.getUndoStack().length).toBe(4);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
@@ -1224,7 +1234,7 @@ describe("history", () => {
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
assertSelectedElements([rect1, rect3]);
|
||||
|
||||
API.executeAction(actionBringForward);
|
||||
act(() => h.app.actionManager.executeAction(actionBringForward));
|
||||
|
||||
expect(API.getUndoStack().length).toBe(7);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
@@ -1252,6 +1262,8 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
describe("should support bidirectional bindings", async () => {
|
||||
let excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
|
||||
let rect1: ExcalidrawGenericElement;
|
||||
let rect2: ExcalidrawGenericElement;
|
||||
let text: ExcalidrawTextElement;
|
||||
@@ -1280,13 +1292,22 @@ describe("history", () => {
|
||||
} as const;
|
||||
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
const excalidrawAPIPromise =
|
||||
resolvablePromise<ExcalidrawImperativeAPI>();
|
||||
|
||||
await render(
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
||||
handleKeyboardGlobally={true}
|
||||
/>,
|
||||
);
|
||||
excalidrawAPI = await excalidrawAPIPromise;
|
||||
|
||||
rect1 = API.createElement({ ...rect1Props });
|
||||
text = API.createElement({ ...textProps });
|
||||
rect2 = API.createElement({ ...rect2Props });
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect1, text, rect2],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
@@ -1737,14 +1758,14 @@ describe("history", () => {
|
||||
expect(undoButton).not.toBeDisabled();
|
||||
expect(redoButton).toBeDisabled();
|
||||
|
||||
API.executeAction(undoAction);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
|
||||
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||
expect(h.history.isRedoStackEmpty).toBeFalsy();
|
||||
expect(undoButton).toBeDisabled();
|
||||
expect(redoButton).not.toBeDisabled();
|
||||
|
||||
API.executeAction(redoAction);
|
||||
act(() => h.app.actionManager.executeAction(redoAction));
|
||||
|
||||
expect(h.history.isUndoStackEmpty).toBeFalsy();
|
||||
expect(h.history.isRedoStackEmpty).toBeTruthy();
|
||||
@@ -1786,13 +1807,13 @@ describe("history", () => {
|
||||
expect(queryByTestId(container, "button-undo")).not.toBeDisabled();
|
||||
expect(queryByTestId(container, "button-redo")).toBeDisabled();
|
||||
|
||||
API.executeAction(actionToggleViewMode);
|
||||
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||
expect(h.state.viewModeEnabled).toBe(true);
|
||||
|
||||
expect(queryByTestId(container, "button-undo")).toBeNull();
|
||||
expect(queryByTestId(container, "button-redo")).toBeNull();
|
||||
|
||||
API.executeAction(actionToggleViewMode);
|
||||
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||
expect(h.state.viewModeEnabled).toBe(false);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -1803,20 +1824,20 @@ describe("history", () => {
|
||||
// testing redo button
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
API.executeAction(undoAction);
|
||||
act(() => h.app.actionManager.executeAction(undoAction));
|
||||
|
||||
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||
expect(h.history.isRedoStackEmpty).toBeFalsy();
|
||||
expect(queryByTestId(container, "button-undo")).toBeDisabled();
|
||||
expect(queryByTestId(container, "button-redo")).not.toBeDisabled();
|
||||
|
||||
API.executeAction(actionToggleViewMode);
|
||||
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||
expect(h.state.viewModeEnabled).toBe(true);
|
||||
|
||||
expect(queryByTestId(container, "button-undo")).toBeNull();
|
||||
expect(queryByTestId(container, "button-redo")).toBeNull();
|
||||
|
||||
API.executeAction(actionToggleViewMode);
|
||||
act(() => h.app.actionManager.executeAction(actionToggleViewMode));
|
||||
expect(h.state.viewModeEnabled).toBe(false);
|
||||
|
||||
expect(h.history.isUndoStackEmpty).toBeTruthy();
|
||||
@@ -1827,6 +1848,8 @@ 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++) {
|
||||
@@ -1835,9 +1858,15 @@ describe("history", () => {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
||||
await render(
|
||||
<Excalidraw handleKeyboardGlobally={true} isCollaborating={true} />,
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api) => excalidrawAPIPromise.resolve(api as any)}
|
||||
handleKeyboardGlobally={true}
|
||||
isCollaborating={true}
|
||||
/>,
|
||||
);
|
||||
excalidrawAPI = await excalidrawAPIPromise;
|
||||
});
|
||||
|
||||
it("should not override remote changes on different elements", async () => {
|
||||
@@ -1852,7 +1881,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
...h.elements,
|
||||
API.createElement({
|
||||
@@ -1892,7 +1921,7 @@ describe("history", () => {
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
strokeColor: yellow,
|
||||
@@ -1940,7 +1969,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
backgroundColor: yellow,
|
||||
@@ -1956,7 +1985,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
backgroundColor: violet,
|
||||
@@ -2005,13 +2034,13 @@ describe("history", () => {
|
||||
elbowed: true,
|
||||
});
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect, diamond],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Connect the arrow
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
{
|
||||
...rect,
|
||||
@@ -2061,7 +2090,7 @@ describe("history", () => {
|
||||
|
||||
Keyboard.undo();
|
||||
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: h.elements.map((el) =>
|
||||
el.id === "KPrBI4g_v9qUB1XxYLgSz"
|
||||
? {
|
||||
@@ -2093,13 +2122,13 @@ describe("history", () => {
|
||||
const rect2 = API.createElement({ type: "rectangle" });
|
||||
|
||||
// Initialize scene
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect1, rect2],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], { groupIds: ["A"] }),
|
||||
newElementWith(h.elements[1], { groupIds: ["A"] }),
|
||||
@@ -2111,7 +2140,7 @@ describe("history", () => {
|
||||
const rect4 = API.createElement({ type: "rectangle", groupIds: ["B"] });
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], { groupIds: ["A", "B"] }),
|
||||
newElementWith(h.elements[1], { groupIds: ["A", "B"] }),
|
||||
@@ -2152,7 +2181,7 @@ describe("history", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
||||
points: [
|
||||
@@ -2252,7 +2281,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update & restore
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
backgroundColor: yellow,
|
||||
@@ -2329,7 +2358,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update & deletion
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
backgroundColor: yellow,
|
||||
@@ -2388,7 +2417,7 @@ describe("history", () => {
|
||||
expect(API.getUndoStack().length).toBe(5);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], {
|
||||
@@ -2453,7 +2482,7 @@ describe("history", () => {
|
||||
const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 });
|
||||
const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 });
|
||||
|
||||
API.setElements([rect1, rect2, rect3]);
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
mouse.select(rect1);
|
||||
mouse.select([rect2, rect3]);
|
||||
|
||||
@@ -2464,7 +2493,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], {
|
||||
@@ -2503,7 +2532,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], {
|
||||
@@ -2557,7 +2586,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect1, rect2],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -2567,7 +2596,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [h.elements[0], h.elements[1], rect3, rect4],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -2581,7 +2610,7 @@ describe("history", () => {
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true, B: true });
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
isDeleted: true,
|
||||
@@ -2606,7 +2635,7 @@ describe("history", () => {
|
||||
Keyboard.undo();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
isDeleted: false,
|
||||
@@ -2624,7 +2653,7 @@ describe("history", () => {
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [h.elements[0], h.elements[1], rect3, rect4],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -2647,7 +2676,7 @@ describe("history", () => {
|
||||
x: 100,
|
||||
});
|
||||
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
mouse.select(rect1);
|
||||
|
||||
// inside the editing group
|
||||
@@ -2663,7 +2692,7 @@ describe("history", () => {
|
||||
expect(h.state.editingGroupId).toBeNull();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
isDeleted: true,
|
||||
@@ -2686,7 +2715,7 @@ describe("history", () => {
|
||||
expect(h.state.editingGroupId).toBeNull();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
isDeleted: false,
|
||||
@@ -2730,7 +2759,7 @@ describe("history", () => {
|
||||
expect(h.state.selectedLinearElement).toBeNull();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
isDeleted: true,
|
||||
@@ -2757,11 +2786,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"
|
||||
|
||||
API.setElements([rect1, rect2, rect3]);
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
|
||||
mouse.select(rect2);
|
||||
|
||||
API.executeAction(actionSendToBack);
|
||||
act(() => h.app.actionManager.executeAction(actionSendToBack));
|
||||
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
@@ -2773,7 +2802,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[2], { index: "Zy" as FractionalIndex }),
|
||||
h.elements[0],
|
||||
@@ -2812,7 +2841,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[2], { index: "Zx" as FractionalIndex }),
|
||||
h.elements[0],
|
||||
@@ -2847,11 +2876,11 @@ describe("history", () => {
|
||||
const rect2 = API.createElement({ type: "rectangle", x: 20, y: 20 });
|
||||
const rect3 = API.createElement({ type: "rectangle", x: 30, y: 30 });
|
||||
|
||||
API.setElements([rect1, rect2, rect3]);
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
|
||||
mouse.select(rect2);
|
||||
|
||||
API.executeAction(actionSendToBack);
|
||||
act(() => h.app.actionManager.executeAction(actionSendToBack));
|
||||
|
||||
expect(API.getUndoStack().length).toBe(2);
|
||||
expect(API.getRedoStack().length).toBe(0);
|
||||
@@ -2863,7 +2892,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update (fixes all invalid z-indices)
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[2], // rect3
|
||||
h.elements[0], // rect2
|
||||
@@ -2893,7 +2922,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[1], // rect2
|
||||
h.elements[0], // rect3
|
||||
@@ -2927,7 +2956,7 @@ describe("history", () => {
|
||||
const rect = API.createElement({ ...rectProps });
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [...h.elements, rect],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -2979,7 +3008,7 @@ describe("history", () => {
|
||||
const rect3 = API.createElement({ ...rect3Props });
|
||||
|
||||
// // Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [...h.elements, rect3],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -3069,7 +3098,7 @@ describe("history", () => {
|
||||
const rect3 = API.createElement({ ...rect3Props });
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [...h.elements, rect3],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
@@ -3246,13 +3275,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container, text],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3281,7 +3310,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
// no conflicting updates
|
||||
@@ -3333,13 +3362,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container, text],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3374,7 +3403,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: remoteText.id, type: "text" }],
|
||||
@@ -3436,13 +3465,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container, text],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3478,7 +3507,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(remoteContainer, {
|
||||
@@ -3544,13 +3573,13 @@ describe("history", () => {
|
||||
|
||||
it("should rebind remotely added bound text when it's container is added through the history", async () => {
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3605,13 +3634,13 @@ describe("history", () => {
|
||||
|
||||
it("should rebind remotely added container when it's bound text is added through the history", async () => {
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [text],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3665,13 +3694,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3707,7 +3736,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: remoteText.id, type: "text" }],
|
||||
@@ -3772,13 +3801,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [text],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3814,7 +3843,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: remoteText.id, type: "text" }],
|
||||
@@ -3878,13 +3907,13 @@ describe("history", () => {
|
||||
|
||||
it("should unbind remotely deleted bound text from container when the container is added through the history", async () => {
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3935,13 +3964,13 @@ describe("history", () => {
|
||||
|
||||
it("should unbind remotely deleted container from bound text when the text is added through the history", async () => {
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [text],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -3992,13 +4021,13 @@ describe("history", () => {
|
||||
|
||||
it("should redraw remotely added bound text when it's container is updated through the history", async () => {
|
||||
// Initialize the scene
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [container],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
x: 200,
|
||||
@@ -4012,7 +4041,7 @@ describe("history", () => {
|
||||
Keyboard.undo();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -4110,13 +4139,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [text],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
x: 205,
|
||||
@@ -4130,7 +4159,7 @@ describe("history", () => {
|
||||
Keyboard.undo();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(container, {
|
||||
boundElements: [{ id: text.id, type: "text" }],
|
||||
@@ -4228,7 +4257,7 @@ describe("history", () => {
|
||||
rect2 = API.createElement({ ...rect2Props });
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect1, rect2],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
@@ -4304,7 +4333,7 @@ describe("history", () => {
|
||||
]);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
// no conflicting updates
|
||||
@@ -4449,7 +4478,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], { boundElements: [] }),
|
||||
@@ -4560,7 +4589,7 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
arrow,
|
||||
newElementWith(h.elements[0], {
|
||||
@@ -4645,13 +4674,13 @@ describe("history", () => {
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [arrow],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0] as ExcalidrawLinearElement, {
|
||||
startBinding: {
|
||||
@@ -4804,7 +4833,7 @@ describe("history", () => {
|
||||
);
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], { x: 500, y: -500 }),
|
||||
@@ -4880,19 +4909,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
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [frame],
|
||||
storeAction: StoreAction.UPDATE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [rect, h.elements[0]],
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
// Simulate local update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
newElementWith(h.elements[0], {
|
||||
frameId: frame.id,
|
||||
@@ -4936,7 +4965,7 @@ describe("history", () => {
|
||||
Keyboard.undo();
|
||||
|
||||
// Simulate remote update
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
elements: [
|
||||
h.elements[0],
|
||||
newElementWith(h.elements[1], {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from "react";
|
||||
import { vi } from "vitest";
|
||||
import { fireEvent, render, waitFor } from "./test-utils";
|
||||
import { act, queryByTestId } from "@testing-library/react";
|
||||
import { queryByTestId } from "@testing-library/react";
|
||||
|
||||
import { Excalidraw } from "../index";
|
||||
import { API } from "./helpers/api";
|
||||
@@ -44,9 +43,7 @@ vi.mock("../data/filesystem.ts", async (importOriginal) => {
|
||||
describe("library", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
await act(() => {
|
||||
return h.app.library.resetLibrary();
|
||||
});
|
||||
h.app.library.resetLibrary();
|
||||
});
|
||||
|
||||
it("import library via drag&drop", async () => {
|
||||
@@ -211,7 +208,7 @@ describe("library menu", () => {
|
||||
"dropdown-menu-button",
|
||||
)!,
|
||||
);
|
||||
fireEvent.click(queryByTestId(container, "lib-dropdown--load")!);
|
||||
queryByTestId(container, "lib-dropdown--load")!.click();
|
||||
|
||||
const libraryItems = parseLibraryJSON(await libraryJSONPromise);
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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";
|
||||
@@ -19,7 +17,7 @@ import { API } from "../tests/helpers/api";
|
||||
import type { Point } from "../types";
|
||||
import { KEYS } from "../keys";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { act, queryByTestId, queryByText } from "@testing-library/react";
|
||||
import { queryByTestId, queryByText } from "@testing-library/react";
|
||||
import {
|
||||
getBoundTextElementPosition,
|
||||
wrapText,
|
||||
@@ -29,6 +27,7 @@ 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,
|
||||
@@ -81,7 +80,7 @@ describe("Test Linear Elements", () => {
|
||||
],
|
||||
roundness,
|
||||
});
|
||||
API.setElements([line]);
|
||||
h.elements = [line];
|
||||
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
return line;
|
||||
@@ -109,7 +108,7 @@ describe("Test Linear Elements", () => {
|
||||
roundness,
|
||||
});
|
||||
mutateElement(line, { points: line.points });
|
||||
API.setElements([line]);
|
||||
h.elements = [line];
|
||||
mouse.clickAt(p1[0], p1[1]);
|
||||
return line;
|
||||
};
|
||||
@@ -787,7 +786,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;
|
||||
API.setElements([
|
||||
h.elements = [
|
||||
line,
|
||||
API.createElement({
|
||||
type: "rectangle",
|
||||
@@ -798,7 +797,7 @@ describe("Test Linear Elements", () => {
|
||||
backgroundColor: "red",
|
||||
fillStyle: "solid",
|
||||
}),
|
||||
]);
|
||||
];
|
||||
const dragEndPositionOffset = [100, 100] as const;
|
||||
API.setSelectedElements([line]);
|
||||
enterLineEditingMode(line, true);
|
||||
@@ -855,7 +854,7 @@ describe("Test Linear Elements", () => {
|
||||
}
|
||||
});
|
||||
const updatedTextElement = { ...textElement, originalText: text };
|
||||
API.setElements([...elements, updatedTextElement]);
|
||||
h.elements = [...elements, updatedTextElement];
|
||||
return { textElement: updatedTextElement, container };
|
||||
};
|
||||
|
||||
@@ -969,13 +968,17 @@ describe("Test Linear Elements", () => {
|
||||
target: { value: DEFAULT_TEXT },
|
||||
});
|
||||
|
||||
Keyboard.exitTextEditor(editor);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(arrow.boundElements).toStrictEqual([
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElementWithContainer).text,
|
||||
).toMatchSnapshot();
|
||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
|
||||
@@ -995,16 +998,21 @@ describe("Test Linear Elements", () => {
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
fireEvent.change(editor, {
|
||||
target: { value: DEFAULT_TEXT },
|
||||
});
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
expect(arrow.boundElements).toStrictEqual([
|
||||
{ id: textElement.id, type: "text" },
|
||||
]);
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElementWithContainer).text,
|
||||
).toMatchSnapshot();
|
||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not bind text to line when double clicked", async () => {
|
||||
@@ -1051,7 +1059,11 @@ describe("Test Linear Elements", () => {
|
||||
"y": 60,
|
||||
}
|
||||
`);
|
||||
expect(textElement.text).toMatchSnapshot();
|
||||
expect(textElement.text).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
expect(
|
||||
LinearElementEditor.getElementAbsoluteCoords(
|
||||
container,
|
||||
@@ -1091,9 +1103,11 @@ describe("Test Linear Elements", () => {
|
||||
"y": 45,
|
||||
}
|
||||
`);
|
||||
expect(
|
||||
(h.elements[1] as ExcalidrawTextElementWithContainer).text,
|
||||
).toMatchSnapshot();
|
||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`);
|
||||
expect(
|
||||
LinearElementEditor.getElementAbsoluteCoords(
|
||||
container,
|
||||
@@ -1129,7 +1143,11 @@ describe("Test Linear Elements", () => {
|
||||
"y": 10,
|
||||
}
|
||||
`);
|
||||
expect(textElement.text).toMatchSnapshot();
|
||||
expect(textElement.text).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(
|
||||
container,
|
||||
elementsMap,
|
||||
@@ -1153,7 +1171,10 @@ describe("Test Linear Elements", () => {
|
||||
"y": -5,
|
||||
}
|
||||
`);
|
||||
expect(textElement.text).toMatchSnapshot();
|
||||
expect(textElement.text).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not render vertical align tool when element selected", () => {
|
||||
@@ -1186,8 +1207,9 @@ 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 } });
|
||||
Keyboard.exitTextEditor(editor);
|
||||
editor.blur();
|
||||
|
||||
const textElement = h.elements[2] as ExcalidrawTextElementWithContainer;
|
||||
|
||||
@@ -1201,7 +1223,10 @@ describe("Test Linear Elements", () => {
|
||||
font,
|
||||
getBoundTextMaxWidth(arrow, null),
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`);
|
||||
const handleBindTextResizeSpy = vi.spyOn(
|
||||
textElementUtils,
|
||||
"handleBindTextResize",
|
||||
@@ -1227,7 +1252,11 @@ describe("Test Linear Elements", () => {
|
||||
font,
|
||||
getBoundTextMaxWidth(arrow, null),
|
||||
),
|
||||
).toMatchSnapshot();
|
||||
).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
});
|
||||
|
||||
it("should not render horizontal align tool when element selected", () => {
|
||||
@@ -1251,7 +1280,7 @@ describe("Test Linear Elements", () => {
|
||||
expect(text.x).toBe(0);
|
||||
expect(text.y).toBe(0);
|
||||
|
||||
API.setElements([h.elements[0], text]);
|
||||
h.elements = [h.elements[0], text];
|
||||
|
||||
const container = h.elements[0];
|
||||
API.setSelectedElements([container, text]);
|
||||
@@ -1329,25 +1358,20 @@ describe("Test Linear Elements", () => {
|
||||
const line = createThreePointerLinearElement("arrow");
|
||||
const [origStartX, origStartY] = [line.x, line.y];
|
||||
|
||||
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,
|
||||
);
|
||||
});
|
||||
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,
|
||||
);
|
||||
expect(line.x).toBe(origStartX + 10);
|
||||
expect(line.y).toBe(origStartY + 10);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { render, fireEvent, act } from "./test-utils";
|
||||
import { render, fireEvent } from "./test-utils";
|
||||
import { Excalidraw } from "../index";
|
||||
import * as StaticScene from "../renderer/staticScene";
|
||||
import * as InteractiveCanvas from "../renderer/interactiveScene";
|
||||
@@ -81,24 +80,22 @@ 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();
|
||||
act(() => {
|
||||
// bind line to two rectangles
|
||||
bindOrUnbindLinearElement(
|
||||
arrow.get() as NonDeleted<ExcalidrawLinearElement>,
|
||||
rectA.get() as ExcalidrawRectangleElement,
|
||||
rectB.get() as ExcalidrawRectangleElement,
|
||||
elementsMap,
|
||||
{} as Scene,
|
||||
);
|
||||
});
|
||||
// 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(
|
||||
`19`,
|
||||
`20`,
|
||||
);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`16`);
|
||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`17`);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(3);
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
render,
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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;
|
||||
@@ -29,7 +27,7 @@ describe("event callbacks", () => {
|
||||
|
||||
const origBackgroundColor = h.state.viewBackgroundColor;
|
||||
excalidrawAPI.onChange(onChange);
|
||||
API.updateScene({
|
||||
excalidrawAPI.updateScene({
|
||||
appState: { viewBackgroundColor: "red" },
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
});
|
||||
|
||||
@@ -15,5 +15,5 @@ export const updateTextEditor = (
|
||||
value: string,
|
||||
) => {
|
||||
fireEvent.change(editor, { target: { value } });
|
||||
fireEvent.input(editor);
|
||||
editor.dispatchEvent(new Event("input"));
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import type { ExcalidrawElement } from "../element/types";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
@@ -56,7 +55,7 @@ beforeEach(async () => {
|
||||
finger2.reset();
|
||||
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
API.setAppState({ height: 768, width: 1024 });
|
||||
h.setState({ height: 768, width: 1024 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -758,7 +757,7 @@ describe("regression tests", () => {
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
|
||||
mouse.select(rect1);
|
||||
|
||||
@@ -794,7 +793,7 @@ describe("regression tests", () => {
|
||||
width: 500,
|
||||
height: 500,
|
||||
});
|
||||
API.setElements([rect1, rect2]);
|
||||
h.elements = [rect1, rect2];
|
||||
|
||||
mouse.select(rect1);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { render } from "./test-utils";
|
||||
import { reseed } from "../random";
|
||||
@@ -538,7 +537,7 @@ describe("text element", () => {
|
||||
describe("image element", () => {
|
||||
it("resizes", async () => {
|
||||
const image = API.createElement({ type: "image", width: 100, height: 100 });
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
UI.resize(image, "ne", [-20, -30]);
|
||||
|
||||
expect(image.x).toBeCloseTo(0);
|
||||
@@ -551,7 +550,7 @@ describe("image element", () => {
|
||||
|
||||
it("flips while resizing", async () => {
|
||||
const image = API.createElement({ type: "image", width: 100, height: 100 });
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
UI.resize(image, "sw", [150, -150]);
|
||||
|
||||
expect(image.x).toBeCloseTo(100);
|
||||
@@ -564,7 +563,7 @@ describe("image element", () => {
|
||||
|
||||
it("resizes with locked/unlocked aspect ratio", async () => {
|
||||
const image = API.createElement({ type: "image", width: 100, height: 100 });
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
UI.resize(image, "ne", [30, -20]);
|
||||
|
||||
expect(image.x).toBeCloseTo(0);
|
||||
@@ -582,7 +581,7 @@ describe("image element", () => {
|
||||
|
||||
it("resizes from center", async () => {
|
||||
const image = API.createElement({ type: "image", width: 100, height: 100 });
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
UI.resize(image, "nw", [25, 15], { alt: true });
|
||||
|
||||
expect(image.x).toBeCloseTo(15);
|
||||
@@ -599,7 +598,7 @@ describe("image element", () => {
|
||||
width: 100,
|
||||
height: 100,
|
||||
});
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
const arrow = UI.createElement("arrow", {
|
||||
x: -30,
|
||||
y: 50,
|
||||
@@ -972,7 +971,7 @@ describe("multiple selection", () => {
|
||||
width: 120,
|
||||
height: 80,
|
||||
});
|
||||
API.setElements([topImage, bottomImage]);
|
||||
h.elements = [topImage, bottomImage];
|
||||
|
||||
const selectionWidth = 200;
|
||||
const selectionHeight = 230;
|
||||
@@ -1044,7 +1043,7 @@ describe("multiple selection", () => {
|
||||
height: 100,
|
||||
angle: (Math.PI * 7) / 6,
|
||||
});
|
||||
API.setElements([image]);
|
||||
h.elements = [image];
|
||||
|
||||
const line = UI.createElement("line", {
|
||||
x: 60,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { render } from "./test-utils";
|
||||
import { reseed } from "../random";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user