Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c91cf93dd | |||
| dd1370381d | |||
| 72d6ee48fc | |||
| 232242d2e9 | |||
| f19ce30dfe | |||
| 3cf14c73a3 | |||
| 8d530cf102 | |||
| b87925d253 | |||
| a6684b09f2 | |||
| b427617f66 | |||
| 2907fbe34b | |||
| c67815f7b0 | |||
| c641860cb1 | |||
| 84d89b9a8a | |||
| e63dd025c9 | |||
| 15e019706d | |||
| a133a70e87 | |||
| 80ea7ca23f | |||
| e844580b14 | |||
| 5a0771ad9c | |||
| adcdbe2907 | |||
| 230d0edc44 | |||
| d0a380758e |
@@ -8,6 +8,7 @@
|
|||||||
!package.json
|
!package.json
|
||||||
!public/
|
!public/
|
||||||
!packages/
|
!packages/
|
||||||
|
!scripts/
|
||||||
!tsconfig.json
|
!tsconfig.json
|
||||||
!yarn.lock
|
!yarn.lock
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,5 @@ firebase/
|
|||||||
dist/
|
dist/
|
||||||
public/workbox
|
public/workbox
|
||||||
packages/excalidraw/types
|
packages/excalidraw/types
|
||||||
|
examples/**/public
|
||||||
|
dev-dist
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
push:
|
push:
|
||||||
branches: master
|
branches: master
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ ARG NODE_ENV=production
|
|||||||
|
|
||||||
RUN yarn build:app:docker
|
RUN yarn build:app:docker
|
||||||
|
|
||||||
FROM nginx:1.24-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
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 |
|
| [`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`. |
|
| [`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. |
|
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
|
||||||
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
|
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down events |
|
||||||
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
|
||||||
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
|
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when something is pasted into the scene |
|
||||||
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
|
| [`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. |
|
| [`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 |
|
| [`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) |
|
| [`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. |
|
| [`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. |
|
| [`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 |
|
| [`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 |
|
| [`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>` |
|
| [`renderEmbeddable`](/docs/@excalidraw/excalidraw/api/props/render-props#renderEmbeddable) | `function` | \_ | Render function that can override the built-in `<iframe>` |
|
||||||
|
|||||||
@@ -18,13 +18,13 @@
|
|||||||
"@docusaurus/core": "2.2.0",
|
"@docusaurus/core": "2.2.0",
|
||||||
"@docusaurus/preset-classic": "2.2.0",
|
"@docusaurus/preset-classic": "2.2.0",
|
||||||
"@docusaurus/theme-live-codeblock": "2.2.0",
|
"@docusaurus/theme-live-codeblock": "2.2.0",
|
||||||
"@excalidraw/excalidraw": "0.17.0",
|
"@excalidraw/excalidraw": "0.17.6",
|
||||||
"@mdx-js/react": "^1.6.22",
|
"@mdx-js/react": "^1.6.22",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"docusaurus-plugin-sass": "0.2.3",
|
"docusaurus-plugin-sass": "0.2.3",
|
||||||
"prism-react-renderer": "^1.3.5",
|
"prism-react-renderer": "^1.3.5",
|
||||||
"react": "^17.0.2",
|
"react": "18.2.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "18.2.0",
|
||||||
"sass": "1.57.1"
|
"sass": "1.57.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+4
-4
@@ -1718,10 +1718,10 @@
|
|||||||
url-loader "^4.1.1"
|
url-loader "^4.1.1"
|
||||||
webpack "^5.73.0"
|
webpack "^5.73.0"
|
||||||
|
|
||||||
"@excalidraw/excalidraw@0.17.0":
|
"@excalidraw/excalidraw@0.17.6":
|
||||||
version "0.17.0"
|
version "0.17.6"
|
||||||
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.0.tgz#3c64aa8e36406ac171b008cfecbdce5bb0755725"
|
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.17.6.tgz#5fd208ce69d33ca712d1804b50d7d06d5c46ac4d"
|
||||||
integrity sha512-NzP22v5xMqxYW27ZtTHhiGFe7kE8NeBk45aoeM/mDSkXiOXPDH+PcvwzHRN/Ei+Vj/0sTPHxejn8bZyRWKGjXg==
|
integrity sha512-fyCl+zG/Z5yhHDh5Fq2ZGmphcrALmuOdtITm8gN4d8w4ntnaopTXcTfnAAaU3VleDC6LhTkoLOTG6P5kgREiIg==
|
||||||
|
|
||||||
"@hapi/hoek@^9.0.0":
|
"@hapi/hoek@^9.0.0":
|
||||||
version "9.3.0"
|
version "9.3.0"
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@excalidraw/excalidraw": "*",
|
"@excalidraw/excalidraw": "*",
|
||||||
"next": "14.1",
|
"next": "14.1",
|
||||||
"react": "^18",
|
"react": "18.2.0",
|
||||||
"react-dom": "^18"
|
"react-dom": "18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "18.2.0",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "18.2.0",
|
||||||
"path2d-polyfill": "2.0.1",
|
"path2d-polyfill": "2.0.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-41
@@ -95,6 +95,11 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- Warmup the connection for Google fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
|
||||||
<!------------------------------------------------------------------------->
|
<!------------------------------------------------------------------------->
|
||||||
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
||||||
<script>
|
<script>
|
||||||
@@ -114,52 +119,16 @@
|
|||||||
) {
|
) {
|
||||||
window.location.href = "https://app.excalidraw.com";
|
window.location.href = "https://app.excalidraw.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
// point into our CDN in prod
|
|
||||||
window.EXCALIDRAW_ASSET_PATH =
|
|
||||||
"https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Following placeholder is replaced during the build step -->
|
||||||
|
<!-- PLACEHOLDER:EXCALIDRAW_APP_FONTS -->
|
||||||
|
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<script>
|
<script>
|
||||||
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
window.EXCALIDRAW_ASSET_PATH = window.origin;
|
||||||
</script>
|
</script>
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
||||||
|
|
||||||
<!-- Excalidraw version -->
|
|
||||||
<meta name="version" content="{version}" />
|
|
||||||
|
|
||||||
<!-- Warmup the connection for Google fonts -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
|
|
||||||
<!-- Preload all default fonts and Virgil for backwards compatibility to avoid swap on init -->
|
|
||||||
<% if (typeof PROD != 'undefined' && PROD == true) { %>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Excalifont-Regular-C9eKQy_N.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/Virgil-Regular-hO16qHwV.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="preload"
|
|
||||||
href="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/fonts/oss/ComicShanns-Regular-D0c8wzsC.woff2"
|
|
||||||
as="font"
|
|
||||||
type="font/woff2"
|
|
||||||
crossorigin="anonymous"
|
|
||||||
/>
|
|
||||||
<% } else { %>
|
|
||||||
<!-- in DEV we need to preload from the local server and without the hash -->
|
<!-- in DEV we need to preload from the local server and without the hash -->
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
@@ -184,7 +153,7 @@
|
|||||||
/>
|
/>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<!-- For Nunito only preload the latin range, which should be enough for now -->
|
<!-- For Nunito only preload the latin range, which should be good enough for now -->
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
href="https://fonts.gstatic.com/s/nunito/v26/XRXI3I6Li01BKofiOc5wtlZ2di8HDIkhdTQ3j6zbXWjgeg.woff2"
|
||||||
@@ -200,6 +169,13 @@
|
|||||||
type="text/css"
|
type="text/css"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
|
||||||
|
<!-- Excalidraw version -->
|
||||||
|
<meta name="version" content="{version}" />
|
||||||
|
|
||||||
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
<% if (typeof VITE_APP_DEV_DISABLE_LIVE_RELOAD != 'undefined' &&
|
||||||
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
VITE_APP_DEV_DISABLE_LIVE_RELOAD == true) { %>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -26,7 +26,17 @@
|
|||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vite-plugin-html": "3.2.2"
|
"firebase": "8.3.3",
|
||||||
|
"idb-keyval": "6.0.3",
|
||||||
|
"jotai": "1.13.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"vite-plugin-html": "3.2.2",
|
||||||
|
"@excalidraw/random-username": "1.0.0",
|
||||||
|
"@sentry/browser": "6.2.5",
|
||||||
|
"@sentry/integrations": "6.2.5",
|
||||||
|
"i18next-browser-languagedetector": "6.1.4",
|
||||||
|
"socket.io-client": "4.7.2"
|
||||||
},
|
},
|
||||||
"prettier": "@excalidraw/prettier-config",
|
"prettier": "@excalidraw/prettier-config",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { vi } from "vitest";
|
|||||||
import {
|
import {
|
||||||
act,
|
act,
|
||||||
render,
|
render,
|
||||||
updateSceneData,
|
|
||||||
waitFor,
|
waitFor,
|
||||||
} from "../../packages/excalidraw/tests/test-utils";
|
} from "../../packages/excalidraw/tests/test-utils";
|
||||||
import ExcalidrawApp from "../App";
|
import ExcalidrawApp from "../App";
|
||||||
@@ -88,12 +87,12 @@ describe("collaboration", () => {
|
|||||||
const rect1 = API.createElement({ ...rect1Props });
|
const rect1 = API.createElement({ ...rect1Props });
|
||||||
const rect2 = API.createElement({ ...rect2Props });
|
const rect2 = API.createElement({ ...rect2Props });
|
||||||
|
|
||||||
updateSceneData({
|
API.updateScene({
|
||||||
elements: syncInvalidIndices([rect1, rect2]),
|
elements: syncInvalidIndices([rect1, rect2]),
|
||||||
storeAction: StoreAction.CAPTURE,
|
storeAction: StoreAction.CAPTURE,
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSceneData({
|
API.updateScene({
|
||||||
elements: syncInvalidIndices([
|
elements: syncInvalidIndices([
|
||||||
rect1,
|
rect1,
|
||||||
newElementWith(h.elements[1], { isDeleted: true }),
|
newElementWith(h.elements[1], { isDeleted: true }),
|
||||||
@@ -143,7 +142,7 @@ describe("collaboration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// simulate force deleting the element remotely
|
// simulate force deleting the element remotely
|
||||||
updateSceneData({
|
API.updateScene({
|
||||||
elements: syncInvalidIndices([rect1]),
|
elements: syncInvalidIndices([rect1]),
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
@@ -178,7 +177,7 @@ describe("collaboration", () => {
|
|||||||
act(() => h.app.actionManager.executeAction(undoAction));
|
act(() => h.app.actionManager.executeAction(undoAction));
|
||||||
|
|
||||||
// simulate local update
|
// simulate local update
|
||||||
updateSceneData({
|
API.updateScene({
|
||||||
elements: syncInvalidIndices([
|
elements: syncInvalidIndices([
|
||||||
h.elements[0],
|
h.elements[0],
|
||||||
newElementWith(h.elements[1], { x: 100 }),
|
newElementWith(h.elements[1], { x: 100 }),
|
||||||
@@ -216,7 +215,7 @@ describe("collaboration", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// simulate force deleting the element remotely
|
// simulate force deleting the element remotely
|
||||||
updateSceneData({
|
API.updateScene({
|
||||||
elements: syncInvalidIndices([rect1]),
|
elements: syncInvalidIndices([rect1]),
|
||||||
storeAction: StoreAction.UPDATE,
|
storeAction: StoreAction.UPDATE,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export default defineConfig({
|
|||||||
assetFileNames(chunkInfo) {
|
assetFileNames(chunkInfo) {
|
||||||
if (chunkInfo?.name?.endsWith(".woff2")) {
|
if (chunkInfo?.name?.endsWith(".woff2")) {
|
||||||
// put on root so we are flexible about the CDN path
|
// 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
|
// Creating separate chunk for locales except for en and percentages.json so they
|
||||||
// can be cached at runtime and not merged with
|
// can be cached at runtime and not merged with
|
||||||
@@ -44,7 +44,7 @@ export default defineConfig({
|
|||||||
// Taking the substring after "locales/"
|
// Taking the substring after "locales/"
|
||||||
return `locales/${id.substring(index + 8)}`;
|
return `locales/${id.substring(index + 8)}`;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|||||||
+9
-14
@@ -9,19 +9,8 @@
|
|||||||
"examples/excalidraw",
|
"examples/excalidraw",
|
||||||
"examples/excalidraw/*"
|
"examples/excalidraw/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
|
||||||
"@excalidraw/random-username": "1.0.0",
|
|
||||||
"@sentry/browser": "6.2.5",
|
|
||||||
"@sentry/integrations": "6.2.5",
|
|
||||||
"firebase": "8.3.3",
|
|
||||||
"i18next-browser-languagedetector": "6.1.4",
|
|
||||||
"idb-keyval": "6.0.3",
|
|
||||||
"jotai": "1.13.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"socket.io-client": "4.7.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "7.21.11",
|
||||||
"@excalidraw/eslint-config": "1.0.3",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
@@ -51,7 +40,7 @@
|
|||||||
"vite-plugin-ejs": "1.7.0",
|
"vite-plugin-ejs": "1.7.0",
|
||||||
"vite-plugin-pwa": "0.17.4",
|
"vite-plugin-pwa": "0.17.4",
|
||||||
"vite-plugin-svgr": "2.4.0",
|
"vite-plugin-svgr": "2.4.0",
|
||||||
"vitest": "1.5.3",
|
"vitest": "1.6.0",
|
||||||
"vitest-canvas-mock": "0.3.2"
|
"vitest-canvas-mock": "0.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -87,6 +76,12 @@
|
|||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease:excalidraw": "node scripts/prerelease.js",
|
"prerelease:excalidraw": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
"build:preview": "yarn build && vite preview --port 5000",
|
||||||
"release:excalidraw": "node scripts/release.js"
|
"release:excalidraw": "node scripts/release.js",
|
||||||
|
"rm:build": "rm -rf excalidraw-app/{build,dist,dev-dist} && rm -rf packages/*/{dist,build} && rm -rf examples/*/*/{build,dist}",
|
||||||
|
"rm:node_modules": "rm -rf node_modules && rm -rf excalidraw-app/node_modules && rm -rf packages/*/node_modules",
|
||||||
|
"clean-install": "yarn rm:node_modules && yarn install"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "18.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,27 @@ import { t } from "../i18n";
|
|||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import type { ExcalidrawElement } from "../element/types";
|
import type { ExcalidrawElement } from "../element/types";
|
||||||
import type { AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import { getElementsInGroup } from "../groups";
|
import { getElementsInGroup } from "../groups";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { fixBindingsAfterDeletion } from "../element/binding";
|
import { fixBindingsAfterDeletion } from "../element/binding";
|
||||||
import { isBoundToContainer, isFrameLikeElement } from "../element/typeChecks";
|
import {
|
||||||
|
isBoundToContainer,
|
||||||
|
isElbowArrow,
|
||||||
|
isFrameLikeElement,
|
||||||
|
} from "../element/typeChecks";
|
||||||
import { updateActiveTool } from "../utils";
|
import { updateActiveTool } from "../utils";
|
||||||
import { TrashIcon } from "../components/icons";
|
import { TrashIcon } from "../components/icons";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
|
|
||||||
const deleteSelectedElements = (
|
const deleteSelectedElements = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
|
app: AppClassProperties,
|
||||||
) => {
|
) => {
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
const framesToBeDeleted = new Set(
|
const framesToBeDeleted = new Set(
|
||||||
getSelectedElements(
|
getSelectedElements(
|
||||||
elements.filter((el) => isFrameLikeElement(el)),
|
elements.filter((el) => isFrameLikeElement(el)),
|
||||||
@@ -29,6 +36,26 @@ const deleteSelectedElements = (
|
|||||||
return {
|
return {
|
||||||
elements: elements.map((el) => {
|
elements: elements.map((el) => {
|
||||||
if (appState.selectedElementIds[el.id]) {
|
if (appState.selectedElementIds[el.id]) {
|
||||||
|
if (el.boundElements) {
|
||||||
|
el.boundElements.forEach((candidate) => {
|
||||||
|
const bound = app.scene
|
||||||
|
.getNonDeletedElementsMap()
|
||||||
|
.get(candidate.id);
|
||||||
|
if (bound && isElbowArrow(bound)) {
|
||||||
|
mutateElement(bound, {
|
||||||
|
startBinding:
|
||||||
|
el.id === bound.startBinding?.elementId
|
||||||
|
? null
|
||||||
|
: bound.startBinding,
|
||||||
|
endBinding:
|
||||||
|
el.id === bound.endBinding?.elementId
|
||||||
|
? null
|
||||||
|
: bound.endBinding,
|
||||||
|
});
|
||||||
|
mutateElbowArrow(bound, elementsMap, bound.points);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
return newElementWith(el, { isDeleted: true });
|
return newElementWith(el, { isDeleted: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +157,11 @@ export const actionDeleteSelected = register({
|
|||||||
: endBindingElement,
|
: endBindingElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
LinearElementEditor.deletePoints(element, selectedPointsIndices);
|
LinearElementEditor.deletePoints(
|
||||||
|
element,
|
||||||
|
selectedPointsIndices,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements,
|
elements,
|
||||||
@@ -149,7 +180,7 @@ export const actionDeleteSelected = register({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let { elements: nextElements, appState: nextAppState } =
|
let { elements: nextElements, appState: nextAppState } =
|
||||||
deleteSelectedElements(elements, appState);
|
deleteSelectedElements(elements, appState, app);
|
||||||
fixBindingsAfterDeletion(
|
fixBindingsAfterDeletion(
|
||||||
nextElements,
|
nextElements,
|
||||||
elements.filter(({ id }) => appState.selectedElementIds[id]),
|
elements.filter(({ id }) => appState.selectedElementIds[id]),
|
||||||
|
|||||||
@@ -40,12 +40,11 @@ export const actionDuplicateSelection = register({
|
|||||||
icon: DuplicateIcon,
|
icon: DuplicateIcon,
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, formData, app) => {
|
perform: (elements, appState, formData, app) => {
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
|
||||||
// duplicate selected point(s) if editing a line
|
// duplicate selected point(s) if editing a line
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const ret = LinearElementEditor.duplicateSelectedPoints(
|
const ret = LinearElementEditor.duplicateSelectedPoints(
|
||||||
appState,
|
appState,
|
||||||
elementsMap,
|
app.scene.getNonDeletedElementsMap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { queryByTestId, fireEvent } from "@testing-library/react";
|
import { queryByTestId, fireEvent } from "@testing-library/react";
|
||||||
import { render } from "../tests/test-utils";
|
import { render } from "../tests/test-utils";
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const actionFinalize = register({
|
|||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -72,8 +73,8 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
const multiPointElement = appState.multiElement
|
const multiPointElement = appState.multiElement
|
||||||
? appState.multiElement
|
? appState.multiElement
|
||||||
: appState.editingElement?.type === "freedraw"
|
: appState.newElement?.type === "freedraw"
|
||||||
? appState.editingElement
|
? appState.newElement
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (multiPointElement) {
|
if (multiPointElement) {
|
||||||
@@ -136,6 +137,7 @@ export const actionFinalize = register({
|
|||||||
appState,
|
appState,
|
||||||
{ x, y },
|
{ x, y },
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +176,8 @@ export const actionFinalize = register({
|
|||||||
? appState.activeTool
|
? appState.activeTool
|
||||||
: activeTool,
|
: activeTool,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
newElement: null,
|
||||||
|
selectionElement: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
startBoundElement: null,
|
startBoundElement: null,
|
||||||
@@ -202,7 +205,7 @@ export const actionFinalize = register({
|
|||||||
keyTest: (event, appState) =>
|
keyTest: (event, appState) =>
|
||||||
(event.key === KEYS.ESCAPE &&
|
(event.key === KEYS.ESCAPE &&
|
||||||
(appState.editingLinearElement !== null ||
|
(appState.editingLinearElement !== null ||
|
||||||
(!appState.draggingElement && appState.multiElement === null))) ||
|
(!appState.newElement && appState.multiElement === null))) ||
|
||||||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
|
||||||
appState.multiElement !== null),
|
appState.multiElement !== null),
|
||||||
PanelComponent: ({ appState, updateData, data }) => (
|
PanelComponent: ({ appState, updateData, data }) => (
|
||||||
|
|||||||
@@ -125,6 +125,8 @@ const flipElements = (
|
|||||||
bindOrUnbindLinearElements(
|
bindOrUnbindLinearElements(
|
||||||
selectedElements.filter(isLinearElement),
|
selectedElements.filter(isLinearElement),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
app.scene.getNonDeletedElements(),
|
||||||
|
app.scene,
|
||||||
isBindingEnabled(appState),
|
isBindingEnabled(appState),
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ const writeData = (
|
|||||||
!appState.multiElement &&
|
!appState.multiElement &&
|
||||||
!appState.resizingElement &&
|
!appState.resizingElement &&
|
||||||
!appState.editingElement &&
|
!appState.editingElement &&
|
||||||
!appState.draggingElement
|
!appState.newElement &&
|
||||||
|
!appState.selectedElementsAreBeingDragged &&
|
||||||
|
!appState.selectionElement
|
||||||
) {
|
) {
|
||||||
const result = updater();
|
const result = updater();
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ export const createUndoAction: ActionCreator = (history, store) => ({
|
|||||||
icon: UndoIcon,
|
icon: UndoIcon,
|
||||||
trackEvent: { category: "history" },
|
trackEvent: { category: "history" },
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState, value, app) =>
|
||||||
writeData(appState, () =>
|
writeData(appState, () =>
|
||||||
history.undo(
|
history.undo(
|
||||||
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
||||||
@@ -91,7 +93,7 @@ export const createRedoAction: ActionCreator = (history, store) => ({
|
|||||||
icon: RedoIcon,
|
icon: RedoIcon,
|
||||||
trackEvent: { category: "history" },
|
trackEvent: { category: "history" },
|
||||||
viewMode: false,
|
viewMode: false,
|
||||||
perform: (elements, appState) =>
|
perform: (elements, appState, _, app) =>
|
||||||
writeData(appState, () =>
|
writeData(appState, () =>
|
||||||
history.redo(
|
history.redo(
|
||||||
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
arrayToMap(elements) as SceneElementsMap, // TODO: #7348 refactor action manager to already include `SceneElementsMap`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
import { DEFAULT_CATEGORIES } from "../components/CommandPalette/CommandPalette";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isElbowArrow, isLinearElement } from "../element/typeChecks";
|
||||||
import type { ExcalidrawLinearElement } from "../element/types";
|
import type { ExcalidrawLinearElement } from "../element/types";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
@@ -29,7 +29,8 @@ export const actionToggleLinearEditor = register({
|
|||||||
if (
|
if (
|
||||||
!appState.editingLinearElement &&
|
!appState.editingLinearElement &&
|
||||||
selectedElements.length === 1 &&
|
selectedElements.length === 1 &&
|
||||||
isLinearElement(selectedElements[0])
|
isLinearElement(selectedElements[0]) &&
|
||||||
|
!isElbowArrow(selectedElements[0])
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { queryByTestId } from "@testing-library/react";
|
import { queryByTestId } from "@testing-library/react";
|
||||||
import { render } from "../tests/test-utils";
|
import { render } from "../tests/test-utils";
|
||||||
@@ -6,8 +7,6 @@ import { API } from "../tests/helpers/api";
|
|||||||
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
||||||
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
||||||
|
|
||||||
const { h } = window;
|
|
||||||
|
|
||||||
describe("element locking", () => {
|
describe("element locking", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw />);
|
await render(<Excalidraw />);
|
||||||
@@ -22,7 +21,7 @@ describe("element locking", () => {
|
|||||||
// just in case we change it in the future
|
// just in case we change it in the future
|
||||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
h.setState({
|
API.setAppState({
|
||||||
currentItemBackgroundColor: color,
|
currentItemBackgroundColor: color,
|
||||||
});
|
});
|
||||||
const activeColor = queryByTestId(
|
const activeColor = queryByTestId(
|
||||||
@@ -40,14 +39,14 @@ describe("element locking", () => {
|
|||||||
// just in case we change it in the future
|
// just in case we change it in the future
|
||||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
h.setState({
|
API.setAppState({
|
||||||
currentItemBackgroundColor: color,
|
currentItemBackgroundColor: color,
|
||||||
currentItemFillStyle: "hachure",
|
currentItemFillStyle: "hachure",
|
||||||
});
|
});
|
||||||
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
expect(hachureFillButton).toHaveClass("active");
|
expect(hachureFillButton).toHaveClass("active");
|
||||||
h.setState({
|
API.setAppState({
|
||||||
currentItemFillStyle: "solid",
|
currentItemFillStyle: "solid",
|
||||||
});
|
});
|
||||||
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
||||||
@@ -57,7 +56,7 @@ describe("element locking", () => {
|
|||||||
it("should not show fill style when background transparent", () => {
|
it("should not show fill style when background transparent", () => {
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
h.setState({
|
API.setAppState({
|
||||||
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
||||||
currentItemFillStyle: "hachure",
|
currentItemFillStyle: "hachure",
|
||||||
});
|
});
|
||||||
@@ -69,7 +68,7 @@ describe("element locking", () => {
|
|||||||
it("should show horizontal text align for text tool", () => {
|
it("should show horizontal text align for text tool", () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
|
|
||||||
h.setState({
|
API.setAppState({
|
||||||
currentItemTextAlign: "right",
|
currentItemTextAlign: "right",
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ describe("element locking", () => {
|
|||||||
backgroundColor: "red",
|
backgroundColor: "red",
|
||||||
fillStyle: "cross-hatch",
|
fillStyle: "cross-hatch",
|
||||||
});
|
});
|
||||||
h.elements = [rect];
|
API.setElements([rect]);
|
||||||
API.setSelectedElements([rect]);
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
@@ -98,7 +97,7 @@ describe("element locking", () => {
|
|||||||
backgroundColor: COLOR_PALETTE.transparent,
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
fillStyle: "cross-hatch",
|
fillStyle: "cross-hatch",
|
||||||
});
|
});
|
||||||
h.elements = [rect];
|
API.setElements([rect]);
|
||||||
API.setSelectedElements([rect]);
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
@@ -114,7 +113,7 @@ describe("element locking", () => {
|
|||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
strokeWidth: STROKE_WIDTH.thin,
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
});
|
});
|
||||||
h.elements = [rect1, rect2];
|
API.setElements([rect1, rect2]);
|
||||||
API.setSelectedElements([rect1, rect2]);
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
const thinStrokeWidthButton = queryByTestId(
|
const thinStrokeWidthButton = queryByTestId(
|
||||||
@@ -133,7 +132,7 @@ describe("element locking", () => {
|
|||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
strokeWidth: STROKE_WIDTH.bold,
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
});
|
});
|
||||||
h.elements = [rect1, rect2];
|
API.setElements([rect1, rect2]);
|
||||||
API.setSelectedElements([rect1, rect2]);
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
||||||
@@ -157,7 +156,7 @@ describe("element locking", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
fontFamily: FONT_FAMILY["Comic Shanns"],
|
fontFamily: FONT_FAMILY["Comic Shanns"],
|
||||||
});
|
});
|
||||||
h.elements = [rect, text];
|
API.setElements([rect, text]);
|
||||||
API.setSelectedElements([rect, text]);
|
API.setSelectedElements([rect, text]);
|
||||||
|
|
||||||
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { AppClassProperties, AppState, Primitive } from "../types";
|
import type { AppClassProperties, AppState, Point, Primitive } from "../types";
|
||||||
import type { StoreActionType } from "../store";
|
import type { StoreActionType } from "../store";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
@@ -50,8 +50,12 @@ import {
|
|||||||
ArrowheadDiamondIcon,
|
ArrowheadDiamondIcon,
|
||||||
ArrowheadDiamondOutlineIcon,
|
ArrowheadDiamondOutlineIcon,
|
||||||
fontSizeIcon,
|
fontSizeIcon,
|
||||||
|
sharpArrowIcon,
|
||||||
|
roundArrowIcon,
|
||||||
|
elbowArrowIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import {
|
import {
|
||||||
|
ARROW_TYPE,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
@@ -67,12 +71,15 @@ import {
|
|||||||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||||
import { getBoundTextElement } from "../element/textElement";
|
import { getBoundTextElement } from "../element/textElement";
|
||||||
import {
|
import {
|
||||||
|
isArrowElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import type {
|
import type {
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -91,10 +98,23 @@ import {
|
|||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { arrayToMap, getFontFamilyString, getShortcutKey } from "../utils";
|
import {
|
||||||
|
arrayToMap,
|
||||||
|
getFontFamilyString,
|
||||||
|
getShortcutKey,
|
||||||
|
tupleToCoors,
|
||||||
|
} from "../utils";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { StoreAction } from "../store";
|
import { StoreAction } from "../store";
|
||||||
import { Fonts, getLineHeight } from "../fonts";
|
import { Fonts, getLineHeight } from "../fonts";
|
||||||
|
import {
|
||||||
|
bindLinearElement,
|
||||||
|
bindPointToSnapToElementOutline,
|
||||||
|
calculateFixedPointForElbowArrowBinding,
|
||||||
|
getHoveredElementForBinding,
|
||||||
|
} from "../element/binding";
|
||||||
|
import { mutateElbowArrow } from "../element/routing";
|
||||||
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
|
|
||||||
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
|
||||||
|
|
||||||
@@ -830,7 +850,7 @@ export const actionChangeFontFamily = register({
|
|||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawElement | null
|
ExcalidrawElement | null
|
||||||
>();
|
>();
|
||||||
let uniqueGlyphs = new Set<string>();
|
let uniqueChars = new Set<string>();
|
||||||
let skipFontFaceCheck = false;
|
let skipFontFaceCheck = false;
|
||||||
|
|
||||||
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
|
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
|
||||||
@@ -878,8 +898,8 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!skipFontFaceCheck) {
|
if (!skipFontFaceCheck) {
|
||||||
uniqueGlyphs = new Set([
|
uniqueChars = new Set([
|
||||||
...uniqueGlyphs,
|
...uniqueChars,
|
||||||
...Array.from(newElement.originalText),
|
...Array.from(newElement.originalText),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -899,12 +919,9 @@ export const actionChangeFontFamily = register({
|
|||||||
const fontString = `10px ${getFontFamilyString({
|
const fontString = `10px ${getFontFamilyString({
|
||||||
fontFamily: nextFontFamily,
|
fontFamily: nextFontFamily,
|
||||||
})}`;
|
})}`;
|
||||||
const glyphs = Array.from(uniqueGlyphs.values()).join();
|
const chars = Array.from(uniqueChars.values()).join();
|
||||||
|
|
||||||
if (
|
if (skipFontFaceCheck || window.document.fonts.check(fontString, chars)) {
|
||||||
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
|
// 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) {
|
for (const [element, container] of elementContainerMapping) {
|
||||||
// trigger synchronous redraw
|
// trigger synchronous redraw
|
||||||
@@ -916,8 +933,8 @@ export const actionChangeFontFamily = register({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// otherwise try to load all font faces for the given glyphs and redraw elements once our font faces loaded
|
// otherwise try to load all font faces for the given chars and redraw elements once our font faces loaded
|
||||||
window.document.fonts.load(fontString, glyphs).then((fontFaces) => {
|
window.document.fonts.load(fontString, chars).then((fontFaces) => {
|
||||||
for (const [element, container] of elementContainerMapping) {
|
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)
|
// 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);
|
const latestElement = app.scene.getElement(element.id);
|
||||||
@@ -1304,8 +1321,12 @@ export const actionChangeRoundness = register({
|
|||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, value) => {
|
perform: (elements, appState, value) => {
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
newElementWith(el, {
|
if (isElbowArrow(el)) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElementWith(el, {
|
||||||
roundness:
|
roundness:
|
||||||
value === "round"
|
value === "round"
|
||||||
? {
|
? {
|
||||||
@@ -1314,8 +1335,8 @@ export const actionChangeRoundness = register({
|
|||||||
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}),
|
});
|
||||||
),
|
}),
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
currentItemRoundness: value,
|
currentItemRoundness: value,
|
||||||
@@ -1355,7 +1376,8 @@ export const actionChangeRoundness = register({
|
|||||||
appState,
|
appState,
|
||||||
(element) =>
|
(element) =>
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||||
(element) => element.hasOwnProperty("roundness"),
|
(element) =>
|
||||||
|
!isArrowElement(element) && element.hasOwnProperty("roundness"),
|
||||||
(hasSelection) =>
|
(hasSelection) =>
|
||||||
hasSelection ? null : appState.currentItemRoundness,
|
hasSelection ? null : appState.currentItemRoundness,
|
||||||
)}
|
)}
|
||||||
@@ -1518,3 +1540,219 @@ export const actionChangeArrowhead = register({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actionChangeArrowType = register({
|
||||||
|
name: "changeArrowType",
|
||||||
|
label: "Change arrow types",
|
||||||
|
trackEvent: false,
|
||||||
|
perform: (elements, appState, value, app) => {
|
||||||
|
return {
|
||||||
|
elements: changeProperty(elements, appState, (el) => {
|
||||||
|
if (!isArrowElement(el)) {
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
const newElement = newElementWith(el, {
|
||||||
|
roundness:
|
||||||
|
value === ARROW_TYPE.round
|
||||||
|
? {
|
||||||
|
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
elbowed: value === ARROW_TYPE.elbow,
|
||||||
|
points:
|
||||||
|
value === ARROW_TYPE.elbow || el.elbowed
|
||||||
|
? [el.points[0], el.points[el.points.length - 1]]
|
||||||
|
: el.points,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isElbowArrow(newElement)) {
|
||||||
|
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||||
|
|
||||||
|
app.dismissLinearEditor();
|
||||||
|
|
||||||
|
const startGlobalPoint =
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
newElement,
|
||||||
|
0,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const endGlobalPoint =
|
||||||
|
LinearElementEditor.getPointAtIndexGlobalCoordinates(
|
||||||
|
newElement,
|
||||||
|
-1,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
const startHoveredElement =
|
||||||
|
!newElement.startBinding &&
|
||||||
|
getHoveredElementForBinding(
|
||||||
|
tupleToCoors(startGlobalPoint),
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const endHoveredElement =
|
||||||
|
!newElement.endBinding &&
|
||||||
|
getHoveredElementForBinding(
|
||||||
|
tupleToCoors(endGlobalPoint),
|
||||||
|
elements,
|
||||||
|
elementsMap,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
const startElement = startHoveredElement
|
||||||
|
? startHoveredElement
|
||||||
|
: newElement.startBinding &&
|
||||||
|
(elementsMap.get(
|
||||||
|
newElement.startBinding.elementId,
|
||||||
|
) as ExcalidrawBindableElement);
|
||||||
|
const endElement = endHoveredElement
|
||||||
|
? endHoveredElement
|
||||||
|
: newElement.endBinding &&
|
||||||
|
(elementsMap.get(
|
||||||
|
newElement.endBinding.elementId,
|
||||||
|
) as ExcalidrawBindableElement);
|
||||||
|
|
||||||
|
const finalStartPoint = startHoveredElement
|
||||||
|
? bindPointToSnapToElementOutline(
|
||||||
|
startGlobalPoint,
|
||||||
|
endGlobalPoint,
|
||||||
|
startHoveredElement,
|
||||||
|
elementsMap,
|
||||||
|
)
|
||||||
|
: startGlobalPoint;
|
||||||
|
const finalEndPoint = endHoveredElement
|
||||||
|
? bindPointToSnapToElementOutline(
|
||||||
|
endGlobalPoint,
|
||||||
|
startGlobalPoint,
|
||||||
|
endHoveredElement,
|
||||||
|
elementsMap,
|
||||||
|
)
|
||||||
|
: endGlobalPoint;
|
||||||
|
|
||||||
|
startHoveredElement &&
|
||||||
|
bindLinearElement(
|
||||||
|
newElement,
|
||||||
|
startHoveredElement,
|
||||||
|
"start",
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
endHoveredElement &&
|
||||||
|
bindLinearElement(
|
||||||
|
newElement,
|
||||||
|
endHoveredElement,
|
||||||
|
"end",
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
mutateElbowArrow(
|
||||||
|
newElement,
|
||||||
|
elementsMap,
|
||||||
|
[finalStartPoint, finalEndPoint].map(
|
||||||
|
(point) =>
|
||||||
|
[point[0] - newElement.x, point[1] - newElement.y] as Point,
|
||||||
|
),
|
||||||
|
[0, 0],
|
||||||
|
{
|
||||||
|
...(startElement && newElement.startBinding
|
||||||
|
? {
|
||||||
|
startBinding: {
|
||||||
|
// @ts-ignore TS cannot discern check above
|
||||||
|
...newElement.startBinding!,
|
||||||
|
...calculateFixedPointForElbowArrowBinding(
|
||||||
|
newElement,
|
||||||
|
startElement,
|
||||||
|
"start",
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(endElement && newElement.endBinding
|
||||||
|
? {
|
||||||
|
endBinding: {
|
||||||
|
// @ts-ignore TS cannot discern check above
|
||||||
|
...newElement.endBinding,
|
||||||
|
...calculateFixedPointForElbowArrowBinding(
|
||||||
|
newElement,
|
||||||
|
endElement,
|
||||||
|
"end",
|
||||||
|
elementsMap,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
mutateElement(
|
||||||
|
newElement,
|
||||||
|
{
|
||||||
|
startBinding: newElement.startBinding
|
||||||
|
? { ...newElement.startBinding, fixedPoint: null }
|
||||||
|
: null,
|
||||||
|
endBinding: newElement.endBinding
|
||||||
|
? { ...newElement.endBinding, fixedPoint: null }
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newElement;
|
||||||
|
}),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
currentItemArrowType: value,
|
||||||
|
},
|
||||||
|
storeAction: StoreAction.CAPTURE,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
PanelComponent: ({ elements, appState, updateData }) => {
|
||||||
|
return (
|
||||||
|
<fieldset>
|
||||||
|
<legend>{t("labels.arrowtypes")}</legend>
|
||||||
|
<ButtonIconSelect
|
||||||
|
group="arrowtypes"
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
value: ARROW_TYPE.sharp,
|
||||||
|
text: t("labels.arrowtype_sharp"),
|
||||||
|
icon: sharpArrowIcon,
|
||||||
|
testId: "sharp-arrow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ARROW_TYPE.round,
|
||||||
|
text: t("labels.arrowtype_round"),
|
||||||
|
icon: roundArrowIcon,
|
||||||
|
testId: "round-arrow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ARROW_TYPE.elbow,
|
||||||
|
text: t("labels.arrowtype_elbowed"),
|
||||||
|
icon: elbowArrowIcon,
|
||||||
|
testId: "elbow-arrow",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
value={getFormValue(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
(element) => {
|
||||||
|
if (isArrowElement(element)) {
|
||||||
|
return element.elbowed
|
||||||
|
? ARROW_TYPE.elbow
|
||||||
|
: element.roundness
|
||||||
|
? ARROW_TYPE.round
|
||||||
|
: ARROW_TYPE.sharp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
(element) => isArrowElement(element),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemArrowType,
|
||||||
|
)}
|
||||||
|
onChange={(value) => updateData(value)}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export type ActionName =
|
|||||||
| "changeSloppiness"
|
| "changeSloppiness"
|
||||||
| "changeStrokeStyle"
|
| "changeStrokeStyle"
|
||||||
| "changeArrowhead"
|
| "changeArrowhead"
|
||||||
|
| "changeArrowType"
|
||||||
| "changeOpacity"
|
| "changeOpacity"
|
||||||
| "changeFontSize"
|
| "changeFontSize"
|
||||||
| "toggleCanvasMenu"
|
| "toggleCanvasMenu"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { COLOR_PALETTE } from "./colors";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
import {
|
import {
|
||||||
|
ARROW_TYPE,
|
||||||
DEFAULT_ELEMENT_PROPS,
|
DEFAULT_ELEMENT_PROPS,
|
||||||
DEFAULT_FONT_FAMILY,
|
DEFAULT_FONT_FAMILY,
|
||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
@@ -33,13 +34,14 @@ export const getDefaultAppState = (): Omit<
|
|||||||
currentItemStartArrowhead: null,
|
currentItemStartArrowhead: null,
|
||||||
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
|
||||||
currentItemRoundness: "round",
|
currentItemRoundness: "round",
|
||||||
|
currentItemArrowType: ARROW_TYPE.round,
|
||||||
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
|
||||||
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
|
||||||
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
currentItemTextAlign: DEFAULT_TEXT_ALIGN,
|
||||||
currentHoveredFontFamily: null,
|
currentHoveredFontFamily: null,
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
draggingElement: null,
|
newElement: null,
|
||||||
editingElement: null,
|
editingElement: null,
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
editingLinearElement: null,
|
editingLinearElement: null,
|
||||||
@@ -143,6 +145,11 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
export: false,
|
export: false,
|
||||||
server: false,
|
server: false,
|
||||||
},
|
},
|
||||||
|
currentItemArrowType: {
|
||||||
|
browser: true,
|
||||||
|
export: false,
|
||||||
|
server: false,
|
||||||
|
},
|
||||||
currentItemOpacity: { browser: true, export: false, server: false },
|
currentItemOpacity: { browser: true, export: false, server: false },
|
||||||
currentItemRoughness: { browser: true, export: false, server: false },
|
currentItemRoughness: { browser: true, export: false, server: false },
|
||||||
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
currentItemStartArrowhead: { browser: true, export: false, server: false },
|
||||||
@@ -153,7 +160,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
currentHoveredFontFamily: { browser: false, export: false, server: false },
|
currentHoveredFontFamily: { browser: false, export: false, server: false },
|
||||||
cursorButton: { browser: true, export: false, server: false },
|
cursorButton: { browser: true, export: false, server: false },
|
||||||
activeEmbeddable: { browser: false, export: false, server: false },
|
activeEmbeddable: { browser: false, export: false, server: false },
|
||||||
draggingElement: { browser: false, export: false, server: false },
|
newElement: { browser: false, export: false, server: false },
|
||||||
editingElement: { browser: false, export: false, server: false },
|
editingElement: { browser: false, export: false, server: false },
|
||||||
editingGroupId: { browser: true, export: false, server: false },
|
editingGroupId: { browser: true, export: false, server: false },
|
||||||
editingLinearElement: { browser: false, export: false, server: false },
|
editingLinearElement: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
export default class BinaryHeap<T> {
|
||||||
|
private content: T[] = [];
|
||||||
|
|
||||||
|
constructor(private scoreFunction: (node: T) => number) {}
|
||||||
|
|
||||||
|
sinkDown(idx: number) {
|
||||||
|
const node = this.content[idx];
|
||||||
|
while (idx > 0) {
|
||||||
|
const parentN = ((idx + 1) >> 1) - 1;
|
||||||
|
const parent = this.content[parentN];
|
||||||
|
if (this.scoreFunction(node) < this.scoreFunction(parent)) {
|
||||||
|
this.content[parentN] = node;
|
||||||
|
this.content[idx] = parent;
|
||||||
|
idx = parentN; // TODO: Optimize
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bubbleUp(idx: number) {
|
||||||
|
const length = this.content.length;
|
||||||
|
const node = this.content[idx];
|
||||||
|
const score = this.scoreFunction(node);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const child2N = (idx + 1) << 1;
|
||||||
|
const child1N = child2N - 1;
|
||||||
|
let swap = null;
|
||||||
|
let child1Score = 0;
|
||||||
|
|
||||||
|
if (child1N < length) {
|
||||||
|
const child1 = this.content[child1N];
|
||||||
|
child1Score = this.scoreFunction(child1);
|
||||||
|
if (child1Score < score) {
|
||||||
|
swap = child1N;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child2N < length) {
|
||||||
|
const child2 = this.content[child2N];
|
||||||
|
const child2Score = this.scoreFunction(child2);
|
||||||
|
if (child2Score < (swap === null ? score : child1Score)) {
|
||||||
|
swap = child2N;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swap !== null) {
|
||||||
|
this.content[idx] = this.content[swap];
|
||||||
|
this.content[swap] = node;
|
||||||
|
idx = swap; // TODO: Optimize
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push(node: T) {
|
||||||
|
this.content.push(node);
|
||||||
|
this.sinkDown(this.content.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pop(): T | null {
|
||||||
|
if (this.content.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = this.content[0];
|
||||||
|
const end = this.content.pop()!;
|
||||||
|
|
||||||
|
if (this.content.length > 0) {
|
||||||
|
this.content[0] = end;
|
||||||
|
this.bubbleUp(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(node: T) {
|
||||||
|
if (this.content.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const i = this.content.indexOf(node);
|
||||||
|
const end = this.content.pop()!;
|
||||||
|
|
||||||
|
if (i < this.content.length) {
|
||||||
|
this.content[i] = end;
|
||||||
|
|
||||||
|
if (this.scoreFunction(end) < this.scoreFunction(node)) {
|
||||||
|
this.sinkDown(i);
|
||||||
|
} else {
|
||||||
|
this.bubbleUp(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size(): number {
|
||||||
|
return this.content.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
rescoreElement(node: T) {
|
||||||
|
this.sinkDown(this.content.indexOf(node));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1100,7 +1100,6 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||||||
try {
|
try {
|
||||||
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
|
// TODO: #7348 refactor away mutations below, so that we couldn't end up in an incosistent state
|
||||||
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
|
ElementsChange.redrawTextBoundingBoxes(nextElements, changedElements);
|
||||||
ElementsChange.redrawBoundArrows(nextElements, changedElements);
|
|
||||||
|
|
||||||
// the following reorder performs also mutations, but only on new instances of changed elements
|
// 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)
|
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
||||||
@@ -1109,6 +1108,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||||||
changedElements,
|
changedElements,
|
||||||
flags,
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Need ordered nextElements to avoid z-index binding issues
|
||||||
|
ElementsChange.redrawBoundArrows(nextElements, changedElements);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Couldn't mutate elements after applying elements change`,
|
`Couldn't mutate elements after applying elements change`,
|
||||||
@@ -1460,7 +1462,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
|
|||||||
) {
|
) {
|
||||||
for (const element of changed.values()) {
|
for (const element of changed.values()) {
|
||||||
if (!element.isDeleted && isBindableElement(element)) {
|
if (!element.isDeleted && isBindableElement(element)) {
|
||||||
updateBoundElements(element, elements);
|
updateBoundElements(element, elements, {
|
||||||
|
changedElements: changed,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,8 +257,6 @@ const chartLines = (
|
|||||||
type: "line",
|
type: "line",
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
[0, 0],
|
||||||
@@ -273,8 +271,6 @@ const chartLines = (
|
|||||||
type: "line",
|
type: "line",
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
height: chartHeight,
|
height: chartHeight,
|
||||||
points: [
|
points: [
|
||||||
[0, 0],
|
[0, 0],
|
||||||
@@ -289,8 +285,6 @@ const chartLines = (
|
|||||||
type: "line",
|
type: "line",
|
||||||
x,
|
x,
|
||||||
y: y - BAR_HEIGHT - BAR_GAP,
|
y: y - BAR_HEIGHT - BAR_GAP,
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
width: chartWidth,
|
width: chartWidth,
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
@@ -418,8 +412,6 @@ const chartTypeLine = (
|
|||||||
type: "line",
|
type: "line",
|
||||||
x: x + BAR_GAP + BAR_WIDTH / 2,
|
x: x + BAR_GAP + BAR_WIDTH / 2,
|
||||||
y: y - BAR_GAP,
|
y: y - BAR_GAP,
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
height: maxY - minY,
|
height: maxY - minY,
|
||||||
width: maxX - minX,
|
width: maxX - minX,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
@@ -453,8 +445,6 @@ const chartTypeLine = (
|
|||||||
type: "line",
|
type: "line",
|
||||||
x: x + cx + BAR_WIDTH / 2 + BAR_GAP / 2,
|
x: x + cx + BAR_WIDTH / 2 + BAR_GAP / 2,
|
||||||
y: y - cy,
|
y: y - cy,
|
||||||
startArrowhead: null,
|
|
||||||
endArrowhead: null,
|
|
||||||
height: cy,
|
height: cy,
|
||||||
strokeStyle: "dotted",
|
strokeStyle: "dotted",
|
||||||
opacity: GRID_OPACITY,
|
opacity: GRID_OPACITY,
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ import type { AppClassProperties, AppProps, UIAppState, Zoom } from "../types";
|
|||||||
import { capitalizeString, isTransparent } from "../utils";
|
import { capitalizeString, isTransparent } from "../utils";
|
||||||
import Stack from "./Stack";
|
import Stack from "./Stack";
|
||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor, toolIsArrow } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
@@ -121,7 +122,8 @@ export const SelectedShapeActions = ({
|
|||||||
const showLineEditorAction =
|
const showLineEditorAction =
|
||||||
!appState.editingLinearElement &&
|
!appState.editingLinearElement &&
|
||||||
targetElements.length === 1 &&
|
targetElements.length === 1 &&
|
||||||
isLinearElement(targetElements[0]);
|
isLinearElement(targetElements[0]) &&
|
||||||
|
!isElbowArrow(targetElements[0]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panelColumn">
|
<div className="panelColumn">
|
||||||
@@ -155,6 +157,11 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(toolIsArrow(appState.activeTool.type) ||
|
||||||
|
targetElements.some((element) => toolIsArrow(element.type))) && (
|
||||||
|
<>{renderAction("changeArrowType")}</>
|
||||||
|
)}
|
||||||
|
|
||||||
{(appState.activeTool.type === "text" ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some(isTextElement)) && (
|
targetElements.some(isTextElement)) && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
assertExcalidrawWithSidebar,
|
assertExcalidrawWithSidebar,
|
||||||
assertSidebarDockButton,
|
assertSidebarDockButton,
|
||||||
} from "./Sidebar/Sidebar.test";
|
} from "./Sidebar/siderbar.test.helpers";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
|
|||||||
@@ -63,15 +63,15 @@ export const FontPickerList = React.memo(
|
|||||||
() =>
|
() =>
|
||||||
Array.from(Fonts.registered.entries())
|
Array.from(Fonts.registered.entries())
|
||||||
.filter(([_, { metadata }]) => !metadata.serverSide)
|
.filter(([_, { metadata }]) => !metadata.serverSide)
|
||||||
.map(([familyId, { metadata, fontFaces }]) => {
|
.map(([familyId, { metadata, fonts }]) => {
|
||||||
const font = {
|
const fontDescriptor = {
|
||||||
value: familyId,
|
value: familyId,
|
||||||
icon: metadata.icon,
|
icon: metadata.icon,
|
||||||
text: fontFaces[0].fontFace.family,
|
text: fonts[0].fontFace.family,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (metadata.deprecated) {
|
if (metadata.deprecated) {
|
||||||
Object.assign(font, {
|
Object.assign(fontDescriptor, {
|
||||||
deprecated: metadata.deprecated,
|
deprecated: metadata.deprecated,
|
||||||
badge: {
|
badge: {
|
||||||
type: DropDownMenuItemBadgeType.RED,
|
type: DropDownMenuItemBadgeType.RED,
|
||||||
@@ -80,7 +80,7 @@ export const FontPickerList = React.memo(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return font as FontDescriptor;
|
return fontDescriptor as FontDescriptor;
|
||||||
})
|
})
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
|
a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1,
|
||||||
@@ -89,7 +89,7 @@ export const FontPickerList = React.memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const sceneFamilies = useMemo(
|
const sceneFamilies = useMemo(
|
||||||
() => new Set(fonts.sceneFamilies),
|
() => new Set(fonts.getSceneFontFamilies()),
|
||||||
// cache per selected font family, so hover re-render won't mess it up
|
// cache per selected font family, so hover re-render won't mess it up
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[selectedFontFamily],
|
[selectedFontFamily],
|
||||||
|
|||||||
@@ -30,10 +30,13 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
return t("hints.eraserRevert");
|
return t("hints.eraserRevert");
|
||||||
}
|
}
|
||||||
if (activeTool.type === "arrow" || activeTool.type === "line") {
|
if (activeTool.type === "arrow" || activeTool.type === "line") {
|
||||||
if (!multiMode) {
|
if (multiMode) {
|
||||||
return t("hints.linearElement");
|
return t("hints.linearElementMulti");
|
||||||
}
|
}
|
||||||
return t("hints.linearElementMulti");
|
if (activeTool.type === "arrow") {
|
||||||
|
return t("hints.arrowTool", { arrowShortcut: getShortcutKey("A") });
|
||||||
|
}
|
||||||
|
return t("hints.linearElement");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeTool.type === "freedraw") {
|
if (activeTool.type === "freedraw") {
|
||||||
@@ -82,7 +85,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
|
|
||||||
if (activeTool.type === "selection") {
|
if (activeTool.type === "selection") {
|
||||||
if (
|
if (
|
||||||
appState.draggingElement?.type === "selection" &&
|
appState.selectionElement &&
|
||||||
!selectedElements.length &&
|
!selectedElements.length &&
|
||||||
!appState.editingElement &&
|
!appState.editingElement &&
|
||||||
!appState.editingLinearElement
|
!appState.editingLinearElement
|
||||||
@@ -90,7 +93,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
return t("hints.deepBoxSelect");
|
return t("hints.deepBoxSelect");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appState.gridSize && appState.draggingElement) {
|
if (appState.gridSize && appState.selectedElementsAreBeingDragged) {
|
||||||
return t("hints.disableSnapping");
|
return t("hints.disableSnapping");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +111,8 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
return t("hints.lineEditor_info");
|
return t("hints.lineEditor_info");
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!appState.draggingElement &&
|
!appState.newElement &&
|
||||||
|
!appState.selectedElementsAreBeingDragged &&
|
||||||
isTextBindableContainer(selectedElements[0])
|
isTextBindableContainer(selectedElements[0])
|
||||||
) {
|
) {
|
||||||
return t("hints.bindTextToElement");
|
return t("hints.bindTextToElement");
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React from "react";
|
|||||||
import { DEFAULT_SIDEBAR } from "../../constants";
|
import { DEFAULT_SIDEBAR } from "../../constants";
|
||||||
import { Excalidraw, Sidebar } from "../../index";
|
import { Excalidraw, Sidebar } from "../../index";
|
||||||
import {
|
import {
|
||||||
|
act,
|
||||||
fireEvent,
|
fireEvent,
|
||||||
GlobalTestState,
|
|
||||||
queryAllByTestId,
|
queryAllByTestId,
|
||||||
queryByTestId,
|
queryByTestId,
|
||||||
render,
|
render,
|
||||||
@@ -11,39 +11,17 @@ import {
|
|||||||
withExcalidrawDimensions,
|
withExcalidrawDimensions,
|
||||||
} from "../../tests/test-utils";
|
} from "../../tests/test-utils";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
import {
|
||||||
|
assertExcalidrawWithSidebar,
|
||||||
|
assertSidebarDockButton,
|
||||||
|
} from "./siderbar.test.helpers";
|
||||||
|
|
||||||
export const assertSidebarDockButton = async <T extends boolean>(
|
const toggleSidebar = (
|
||||||
hasDockButton: T,
|
...args: Parameters<typeof window.h.app.toggleSidebar>
|
||||||
): Promise<
|
): Promise<boolean> => {
|
||||||
T extends false
|
return act(() => {
|
||||||
? { dockButton: null; sidebar: HTMLElement }
|
return window.h.app.toggleSidebar(...args);
|
||||||
: { 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", () => {
|
describe("Sidebar", () => {
|
||||||
@@ -103,7 +81,7 @@ describe("Sidebar", () => {
|
|||||||
|
|
||||||
// toggle sidebar on
|
// toggle sidebar on
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
|
expect(await toggleSidebar({ name: "customSidebar" })).toBe(true);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
@@ -112,7 +90,7 @@ describe("Sidebar", () => {
|
|||||||
|
|
||||||
// toggle sidebar off
|
// toggle sidebar off
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(false);
|
expect(await toggleSidebar({ name: "customSidebar" })).toBe(false);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
@@ -121,9 +99,9 @@ describe("Sidebar", () => {
|
|||||||
|
|
||||||
// force-toggle sidebar off (=> still hidden)
|
// force-toggle sidebar off (=> still hidden)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
expect(
|
expect(await toggleSidebar({ name: "customSidebar", force: false })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "customSidebar", force: false }),
|
false,
|
||||||
).toBe(false);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
@@ -132,12 +110,12 @@ describe("Sidebar", () => {
|
|||||||
|
|
||||||
// force-toggle sidebar on
|
// force-toggle sidebar on
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
expect(
|
expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(await toggleSidebar({ name: "customSidebar", force: true })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "customSidebar", force: true }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
@@ -146,9 +124,7 @@ describe("Sidebar", () => {
|
|||||||
|
|
||||||
// toggle library (= hide custom sidebar)
|
// toggle library (= hide custom sidebar)
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
expect(window.h.app.toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(
|
expect(await toggleSidebar({ name: DEFAULT_SIDEBAR.name })).toBe(true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
@@ -161,13 +137,13 @@ describe("Sidebar", () => {
|
|||||||
|
|
||||||
// closing sidebar using `{ name: null }`
|
// closing sidebar using `{ name: null }`
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
expect(window.h.app.toggleSidebar({ name: "customSidebar" })).toBe(true);
|
expect(await toggleSidebar({ name: "customSidebar" })).toBe(true);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
expect(node).not.toBe(null);
|
expect(node).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(window.h.app.toggleSidebar({ name: null })).toBe(false);
|
expect(await toggleSidebar({ name: null })).toBe(false);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const node = container.querySelector("#test-sidebar-content");
|
const node = container.querySelector("#test-sidebar-content");
|
||||||
expect(node).toBe(null);
|
expect(node).toBe(null);
|
||||||
@@ -321,6 +297,9 @@ describe("Sidebar", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shouldn't be user-dockable when only `onDock` supplied w/o `docked`", async () => {
|
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(
|
await render(
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
|
initialData={{ appState: { openSidebar: { name: "customSidebar" } } }}
|
||||||
@@ -341,6 +320,8 @@ describe("Sidebar", () => {
|
|||||||
await assertSidebarDockButton(false);
|
await assertSidebarDockButton(false);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
mock.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -367,9 +348,9 @@ describe("Sidebar", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
|
|
||||||
// open library sidebar
|
// open library sidebar
|
||||||
expect(
|
expect(await toggleSidebar({ name: "custom", tab: "library" })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "custom", tab: "library" }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(
|
||||||
container.querySelector<HTMLElement>(
|
container.querySelector<HTMLElement>(
|
||||||
"[role=tabpanel][data-testid=library]",
|
"[role=tabpanel][data-testid=library]",
|
||||||
@@ -377,9 +358,9 @@ describe("Sidebar", () => {
|
|||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
|
|
||||||
// switch to comments tab
|
// switch to comments tab
|
||||||
expect(
|
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(
|
||||||
container.querySelector<HTMLElement>(
|
container.querySelector<HTMLElement>(
|
||||||
"[role=tabpanel][data-testid=comments]",
|
"[role=tabpanel][data-testid=comments]",
|
||||||
@@ -387,9 +368,9 @@ describe("Sidebar", () => {
|
|||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
|
|
||||||
// toggle sidebar closed
|
// toggle sidebar closed
|
||||||
expect(
|
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
|
false,
|
||||||
).toBe(false);
|
);
|
||||||
expect(
|
expect(
|
||||||
container.querySelector<HTMLElement>(
|
container.querySelector<HTMLElement>(
|
||||||
"[role=tabpanel][data-testid=comments]",
|
"[role=tabpanel][data-testid=comments]",
|
||||||
@@ -397,9 +378,9 @@ describe("Sidebar", () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
|
|
||||||
// toggle sidebar open
|
// toggle sidebar open
|
||||||
expect(
|
expect(await toggleSidebar({ name: "custom", tab: "comments" })).toBe(
|
||||||
window.h.app.toggleSidebar({ name: "custom", tab: "comments" }),
|
true,
|
||||||
).toBe(true);
|
);
|
||||||
expect(
|
expect(
|
||||||
container.querySelector<HTMLElement>(
|
container.querySelector<HTMLElement>(
|
||||||
"[role=tabpanel][data-testid=comments]",
|
"[role=tabpanel][data-testid=comments]",
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Excalidraw } from "../..";
|
||||||
|
import {
|
||||||
|
GlobalTestState,
|
||||||
|
queryByTestId,
|
||||||
|
render,
|
||||||
|
withExcalidrawDimensions,
|
||||||
|
} from "../../tests/test-utils";
|
||||||
|
|
||||||
|
export const assertSidebarDockButton = async <T extends boolean>(
|
||||||
|
hasDockButton: T,
|
||||||
|
): Promise<
|
||||||
|
T extends false
|
||||||
|
? { dockButton: null; sidebar: HTMLElement }
|
||||||
|
: { dockButton: HTMLElement; sidebar: HTMLElement }
|
||||||
|
> => {
|
||||||
|
const sidebar =
|
||||||
|
GlobalTestState.renderResult.container.querySelector<HTMLElement>(
|
||||||
|
".sidebar",
|
||||||
|
);
|
||||||
|
expect(sidebar).not.toBe(null);
|
||||||
|
const dockButton = queryByTestId(sidebar!, "sidebar-dock");
|
||||||
|
if (hasDockButton) {
|
||||||
|
expect(dockButton).not.toBe(null);
|
||||||
|
return { dockButton: dockButton!, sidebar: sidebar! } as any;
|
||||||
|
}
|
||||||
|
expect(dockButton).toBe(null);
|
||||||
|
return { dockButton: null, sidebar: sidebar! } as any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assertExcalidrawWithSidebar = async (
|
||||||
|
sidebar: React.ReactNode,
|
||||||
|
name: string,
|
||||||
|
test: () => void,
|
||||||
|
) => {
|
||||||
|
await render(
|
||||||
|
<Excalidraw initialData={{ appState: { openSidebar: { name } } }}>
|
||||||
|
{sidebar}
|
||||||
|
</Excalidraw>,
|
||||||
|
);
|
||||||
|
await withExcalidrawDimensions({ width: 1920, height: 1080 }, test);
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mutateElement } from "../../element/mutateElement";
|
import { mutateElement } from "../../element/mutateElement";
|
||||||
import { getBoundTextElement } from "../../element/textElement";
|
import { getBoundTextElement } from "../../element/textElement";
|
||||||
import { isArrowElement } from "../../element/typeChecks";
|
import { isArrowElement, isElbowArrow } from "../../element/typeChecks";
|
||||||
import type { ExcalidrawElement } from "../../element/types";
|
import type { ExcalidrawElement } from "../../element/types";
|
||||||
import { degreeToRadian, radianToDegree } from "../../math";
|
import { degreeToRadian, radianToDegree } from "../../math";
|
||||||
import { angleIcon } from "../icons";
|
import { angleIcon } from "../icons";
|
||||||
@@ -27,8 +27,9 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement) {
|
if (origElement && !isElbowArrow(origElement)) {
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
if (!latestElement) {
|
if (!latestElement) {
|
||||||
return;
|
return;
|
||||||
@@ -39,7 +40,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap);
|
updateBindings(latestElement, elementsMap, elements, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
@@ -65,7 +66,7 @@ const handleDegreeChange: DragInputCallbackType<AngleProps["property"]> = ({
|
|||||||
mutateElement(latestElement, {
|
mutateElement(latestElement, {
|
||||||
angle: nextAngle,
|
angle: nextAngle,
|
||||||
});
|
});
|
||||||
updateBindings(latestElement, elementsMap);
|
updateBindings(latestElement, elementsMap, elements, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
|
||||||
if (boundTextElement && !isArrowElement(latestElement)) {
|
if (boundTextElement && !isArrowElement(latestElement)) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
if (origElement) {
|
if (origElement) {
|
||||||
const keepAspectRatio =
|
const keepAspectRatio =
|
||||||
@@ -61,6 +62,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
keepAspectRatio,
|
keepAspectRatio,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -103,6 +106,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
keepAspectRatio,
|
keepAspectRatio,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ export type DragInputCallbackType<
|
|||||||
originalElementsMap: ElementsMap;
|
originalElementsMap: ElementsMap;
|
||||||
shouldKeepAspectRatio: boolean;
|
shouldKeepAspectRatio: boolean;
|
||||||
shouldChangeByStepSize: boolean;
|
shouldChangeByStepSize: boolean;
|
||||||
|
scene: Scene;
|
||||||
nextValue?: number;
|
nextValue?: number;
|
||||||
property: P;
|
property: P;
|
||||||
scene: Scene;
|
|
||||||
originalAppState: AppState;
|
originalAppState: AppState;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
@@ -122,9 +122,9 @@ const StatsDragInput = <
|
|||||||
originalElementsMap: app.scene.getNonDeletedElementsMap(),
|
originalElementsMap: app.scene.getNonDeletedElementsMap(),
|
||||||
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
shouldKeepAspectRatio: shouldKeepAspectRatio!!,
|
||||||
shouldChangeByStepSize: false,
|
shouldChangeByStepSize: false,
|
||||||
|
scene,
|
||||||
nextValue: rounded,
|
nextValue: rounded,
|
||||||
property,
|
property,
|
||||||
scene,
|
|
||||||
originalAppState: appState,
|
originalAppState: appState,
|
||||||
});
|
});
|
||||||
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
app.syncActionResult({ storeAction: StoreAction.CAPTURE });
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const resizeElementInGroup = (
|
|||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
|
||||||
|
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||||
|
|
||||||
mutateElement(latestElement, updates, false);
|
mutateElement(latestElement, updates, false);
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
@@ -77,7 +78,7 @@ const resizeElementInGroup = (
|
|||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const newFontSize = boundTextElement.fontSize * scale;
|
const newFontSize = boundTextElement.fontSize * scale;
|
||||||
updateBoundElements(latestElement, elementsMap, {
|
updateBoundElements(latestElement, elementsMap, {
|
||||||
newSize: { width: updates.width, height: updates.height },
|
oldSize: { width: oldWidth, height: oldHeight },
|
||||||
});
|
});
|
||||||
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
|
||||||
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
|
||||||
@@ -149,6 +150,7 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
property,
|
property,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of atomicUnits) {
|
for (const atomicUnit of atomicUnits) {
|
||||||
@@ -227,6 +229,8 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
false,
|
false,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -320,7 +324,15 @@ const handleDimensionChange: DragInputCallbackType<
|
|||||||
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
|
||||||
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
|
||||||
|
|
||||||
resizeElement(nextWidth, nextHeight, false, origElement, elementsMap);
|
resizeElement(
|
||||||
|
nextWidth,
|
||||||
|
nextHeight,
|
||||||
|
false,
|
||||||
|
origElement,
|
||||||
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
} from "../../element/types";
|
} from "../../element/types";
|
||||||
import { rotate } from "../../math";
|
import { rotate } from "../../math";
|
||||||
@@ -33,6 +34,7 @@ const moveElements = (
|
|||||||
originalElements: readonly ExcalidrawElement[],
|
originalElements: readonly ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
const origElement = originalElements[i];
|
const origElement = originalElements[i];
|
||||||
@@ -60,6 +62,8 @@ const moveElements = (
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -71,6 +75,7 @@ const moveGroupTo = (
|
|||||||
nextY: number,
|
nextY: number,
|
||||||
originalElements: ExcalidrawElement[],
|
originalElements: ExcalidrawElement[],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
) => {
|
) => {
|
||||||
@@ -106,6 +111,8 @@ const moveGroupTo = (
|
|||||||
topLeftY + offsetY,
|
topLeftY + offsetY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -126,6 +133,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
originalAppState,
|
originalAppState,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
|
|
||||||
if (nextValue !== undefined) {
|
if (nextValue !== undefined) {
|
||||||
for (const atomicUnit of getAtomicUnits(
|
for (const atomicUnit of getAtomicUnits(
|
||||||
@@ -150,6 +158,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
elementsInUnit.map((el) => el.original),
|
elementsInUnit.map((el) => el.original),
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
scene,
|
scene,
|
||||||
);
|
);
|
||||||
@@ -180,6 +189,8 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
@@ -206,6 +217,7 @@ const handlePositionChange: DragInputCallbackType<
|
|||||||
originalElements,
|
originalElements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
|
|
||||||
scene.triggerUpdate();
|
scene.triggerUpdate();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
scene,
|
scene,
|
||||||
}) => {
|
}) => {
|
||||||
const elementsMap = scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
const origElement = originalElements[0];
|
const origElement = originalElements[0];
|
||||||
const [cx, cy] = [
|
const [cx, cy] = [
|
||||||
origElement.x + origElement.width / 2,
|
origElement.x + origElement.width / 2,
|
||||||
@@ -47,6 +48,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -78,6 +81,8 @@ const handlePositionChange: DragInputCallbackType<"x" | "y"> = ({
|
|||||||
newTopLeftY,
|
newTopLeftY,
|
||||||
origElement,
|
origElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
originalElementsMap,
|
originalElementsMap,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -104,9 +109,9 @@ const Position = ({
|
|||||||
label={property === "x" ? "X" : "Y"}
|
label={property === "x" ? "X" : "Y"}
|
||||||
elements={[element]}
|
elements={[element]}
|
||||||
dragInputCallback={handlePositionChange}
|
dragInputCallback={handlePositionChange}
|
||||||
|
scene={scene}
|
||||||
value={value}
|
value={value}
|
||||||
property={property}
|
property={property}
|
||||||
scene={scene}
|
|
||||||
appState={appState}
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type Scene from "../../scene/Scene";
|
|||||||
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App";
|
||||||
import { getAtomicUnits } from "./utils";
|
import { getAtomicUnits } from "./utils";
|
||||||
import { STATS_PANELS } from "../../constants";
|
import { STATS_PANELS } from "../../constants";
|
||||||
|
import { isElbowArrow } from "../../element/typeChecks";
|
||||||
|
|
||||||
interface StatsProps {
|
interface StatsProps {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
@@ -209,12 +210,14 @@ export const StatsInner = memo(
|
|||||||
scene={scene}
|
scene={scene}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
/>
|
/>
|
||||||
<Angle
|
{!isElbowArrow(singleElement) && (
|
||||||
property="angle"
|
<Angle
|
||||||
element={singleElement}
|
property="angle"
|
||||||
scene={scene}
|
element={singleElement}
|
||||||
appState={appState}
|
scene={scene}
|
||||||
/>
|
appState={appState}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<FontSize
|
<FontSize
|
||||||
property="fontSize"
|
property="fontSize"
|
||||||
element={singleElement}
|
element={singleElement}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { fireEvent, queryByTestId } from "@testing-library/react";
|
import React from "react";
|
||||||
|
import { act, fireEvent, queryByTestId } from "@testing-library/react";
|
||||||
import { Keyboard, Pointer, UI } from "../../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../../tests/helpers/ui";
|
||||||
import { getStepSizedValue } from "./utils";
|
import { getStepSizedValue } from "./utils";
|
||||||
import {
|
import {
|
||||||
@@ -24,7 +25,6 @@ import { getCommonBounds, isTextElement } from "../../element";
|
|||||||
import { API } from "../../tests/helpers/api";
|
import { API } from "../../tests/helpers/api";
|
||||||
import { actionGroup } from "../../actions";
|
import { actionGroup } from "../../actions";
|
||||||
import { isInGroup } from "../../groups";
|
import { isInGroup } from "../../groups";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -32,12 +32,6 @@ const renderStaticScene = vi.spyOn(StaticScene, "renderStaticScene");
|
|||||||
let stats: HTMLElement | null = null;
|
let stats: HTMLElement | null = null;
|
||||||
let elementStats: HTMLElement | null | undefined = 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 getStatsProperty = (label: string) => {
|
||||||
const elementStats = UI.queryStats()?.querySelector("#elementStats");
|
const elementStats = UI.queryStats()?.querySelector("#elementStats");
|
||||||
|
|
||||||
@@ -65,7 +59,7 @@ const testInputProperty = (
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).toBeDefined();
|
expect(input).toBeDefined();
|
||||||
expect(input.value).toBe(initialValue.toString());
|
expect(input.value).toBe(initialValue.toString());
|
||||||
editInput(input, String(nextValue));
|
UI.updateInput(input, String(nextValue));
|
||||||
if (property === "angle") {
|
if (property === "angle") {
|
||||||
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
expect(element[property]).toBe(degreeToRadian(Number(nextValue)));
|
||||||
} else if (property === "fontSize" && isTextElement(element)) {
|
} else if (property === "fontSize" && isTextElement(element)) {
|
||||||
@@ -110,7 +104,7 @@ describe("binding with linear elements", () => {
|
|||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -148,7 +142,7 @@ describe("binding with linear elements", () => {
|
|||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
expect(inputX).not.toBeNull();
|
expect(inputX).not.toBeNull();
|
||||||
editInput(inputX, String("204"));
|
UI.updateInput(inputX, String("204"));
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,7 +153,7 @@ describe("binding with linear elements", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
editInput(inputAngle, String("1"));
|
UI.updateInput(inputAngle, String("1"));
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -171,7 +165,7 @@ describe("binding with linear elements", () => {
|
|||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
expect(inputX).not.toBeNull();
|
expect(inputX).not.toBeNull();
|
||||||
editInput(inputX, String("254"));
|
UI.updateInput(inputX, String("254"));
|
||||||
expect(linear.startBinding).toBe(null);
|
expect(linear.startBinding).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -182,7 +176,7 @@ describe("binding with linear elements", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
|
|
||||||
expect(linear.startBinding).not.toBe(null);
|
expect(linear.startBinding).not.toBe(null);
|
||||||
editInput(inputAngle, String("45"));
|
UI.updateInput(inputAngle, String("45"));
|
||||||
expect(linear.startBinding).toBe(null);
|
expect(linear.startBinding).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -197,7 +191,7 @@ describe("stats for a generic element", () => {
|
|||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -268,13 +262,13 @@ describe("stats for a generic element", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).toBeDefined();
|
expect(input).toBeDefined();
|
||||||
expect(input.value).toBe(rectangle.width.toString());
|
expect(input.value).toBe(rectangle.width.toString());
|
||||||
editInput(input, "123.123");
|
UI.updateInput(input, "123.123");
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(rectangle.id).toBe(rectangleId);
|
expect(rectangle.id).toBe(rectangleId);
|
||||||
expect(input.value).toBe("123.12");
|
expect(input.value).toBe("123.12");
|
||||||
expect(rectangle.width).toBe(123.12);
|
expect(rectangle.width).toBe(123.12);
|
||||||
|
|
||||||
editInput(input, "88.98766");
|
UI.updateInput(input, "88.98766");
|
||||||
expect(input.value).toBe("88.99");
|
expect(input.value).toBe("88.99");
|
||||||
expect(rectangle.width).toBe(88.99);
|
expect(rectangle.width).toBe(88.99);
|
||||||
});
|
});
|
||||||
@@ -387,7 +381,7 @@ describe("stats for a non-generic element", () => {
|
|||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -412,9 +406,10 @@ describe("stats for a non-generic element", () => {
|
|||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello!");
|
updateTextEditor(editor, "Hello!");
|
||||||
editor.blur();
|
act(() => {
|
||||||
|
editor.blur();
|
||||||
|
});
|
||||||
|
|
||||||
const text = h.elements[0] as ExcalidrawTextElement;
|
const text = h.elements[0] as ExcalidrawTextElement;
|
||||||
mouse.clickOn(text);
|
mouse.clickOn(text);
|
||||||
@@ -427,7 +422,7 @@ describe("stats for a non-generic element", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(input).toBeDefined();
|
expect(input).toBeDefined();
|
||||||
expect(input.value).toBe(text.fontSize.toString());
|
expect(input.value).toBe(text.fontSize.toString());
|
||||||
editInput(input, "36");
|
UI.updateInput(input, "36");
|
||||||
expect(text.fontSize).toBe(36);
|
expect(text.fontSize).toBe(36);
|
||||||
|
|
||||||
// cannot change width or height
|
// cannot change width or height
|
||||||
@@ -437,7 +432,7 @@ describe("stats for a non-generic element", () => {
|
|||||||
expect(height).toBeUndefined();
|
expect(height).toBeUndefined();
|
||||||
|
|
||||||
// min font size is 4
|
// min font size is 4
|
||||||
editInput(input, "0");
|
UI.updateInput(input, "0");
|
||||||
expect(text.fontSize).not.toBe(0);
|
expect(text.fontSize).not.toBe(0);
|
||||||
expect(text.fontSize).toBe(4);
|
expect(text.fontSize).toBe(4);
|
||||||
});
|
});
|
||||||
@@ -449,8 +444,8 @@ describe("stats for a non-generic element", () => {
|
|||||||
x: 150,
|
x: 150,
|
||||||
width: 150,
|
width: 150,
|
||||||
});
|
});
|
||||||
h.elements = [frame];
|
API.setElements([frame]);
|
||||||
h.setState({
|
API.setAppState({
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
[frame.id]: true,
|
[frame.id]: true,
|
||||||
},
|
},
|
||||||
@@ -471,9 +466,9 @@ describe("stats for a non-generic element", () => {
|
|||||||
|
|
||||||
it("image element", () => {
|
it("image element", () => {
|
||||||
const image = API.createElement({ type: "image", width: 200, height: 100 });
|
const image = API.createElement({ type: "image", width: 200, height: 100 });
|
||||||
h.elements = [image];
|
API.setElements([image]);
|
||||||
mouse.clickOn(image);
|
mouse.clickOn(image);
|
||||||
h.setState({
|
API.setAppState({
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
[image.id]: true,
|
[image.id]: true,
|
||||||
},
|
},
|
||||||
@@ -508,7 +503,7 @@ describe("stats for a non-generic element", () => {
|
|||||||
mutateElement(container, {
|
mutateElement(container, {
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
});
|
});
|
||||||
h.elements = [container, text];
|
API.setElements([container, text]);
|
||||||
|
|
||||||
API.setSelectedElements([container]);
|
API.setSelectedElements([container]);
|
||||||
const fontSize = getStatsProperty("F")?.querySelector(
|
const fontSize = getStatsProperty("F")?.querySelector(
|
||||||
@@ -516,7 +511,7 @@ describe("stats for a non-generic element", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(fontSize).toBeDefined();
|
expect(fontSize).toBeDefined();
|
||||||
|
|
||||||
editInput(fontSize, "40");
|
UI.updateInput(fontSize, "40");
|
||||||
|
|
||||||
expect(text.fontSize).toBe(40);
|
expect(text.fontSize).toBe(40);
|
||||||
});
|
});
|
||||||
@@ -533,7 +528,7 @@ describe("stats for multiple elements", () => {
|
|||||||
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -566,7 +561,7 @@ describe("stats for multiple elements", () => {
|
|||||||
mouse.down(-100, -100);
|
mouse.down(-100, -100);
|
||||||
mouse.up(125, 145);
|
mouse.up(125, 145);
|
||||||
|
|
||||||
h.setState({
|
API.setAppState({
|
||||||
selectedElementIds: h.elements.reduce((acc, el) => {
|
selectedElementIds: h.elements.reduce((acc, el) => {
|
||||||
acc[el.id] = true;
|
acc[el.id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -588,12 +583,12 @@ describe("stats for multiple elements", () => {
|
|||||||
) as HTMLInputElement;
|
) as HTMLInputElement;
|
||||||
expect(angle.value).toBe("0");
|
expect(angle.value).toBe("0");
|
||||||
|
|
||||||
editInput(width, "250");
|
UI.updateInput(width, "250");
|
||||||
h.elements.forEach((el) => {
|
h.elements.forEach((el) => {
|
||||||
expect(el.width).toBe(250);
|
expect(el.width).toBe(250);
|
||||||
});
|
});
|
||||||
|
|
||||||
editInput(height, "450");
|
UI.updateInput(height, "450");
|
||||||
h.elements.forEach((el) => {
|
h.elements.forEach((el) => {
|
||||||
expect(el.height).toBe(450);
|
expect(el.height).toBe(450);
|
||||||
});
|
});
|
||||||
@@ -605,9 +600,10 @@ describe("stats for multiple elements", () => {
|
|||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello!");
|
updateTextEditor(editor, "Hello!");
|
||||||
editor.blur();
|
act(() => {
|
||||||
|
editor.blur();
|
||||||
|
});
|
||||||
|
|
||||||
UI.clickTool("rectangle");
|
UI.clickTool("rectangle");
|
||||||
mouse.down();
|
mouse.down();
|
||||||
@@ -619,12 +615,12 @@ describe("stats for multiple elements", () => {
|
|||||||
width: 150,
|
width: 150,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [...h.elements, frame];
|
API.setElements([...h.elements, frame]);
|
||||||
|
|
||||||
const text = h.elements.find((el) => el.type === "text");
|
const text = h.elements.find((el) => el.type === "text");
|
||||||
const rectangle = h.elements.find((el) => el.type === "rectangle");
|
const rectangle = h.elements.find((el) => el.type === "rectangle");
|
||||||
|
|
||||||
h.setState({
|
API.setAppState({
|
||||||
selectedElementIds: h.elements.reduce((acc, el) => {
|
selectedElementIds: h.elements.reduce((acc, el) => {
|
||||||
acc[el.id] = true;
|
acc[el.id] = true;
|
||||||
return acc;
|
return acc;
|
||||||
@@ -657,13 +653,13 @@ describe("stats for multiple elements", () => {
|
|||||||
expect(fontSize).toBeDefined();
|
expect(fontSize).toBeDefined();
|
||||||
|
|
||||||
// changing width does not affect text
|
// changing width does not affect text
|
||||||
editInput(width, "200");
|
UI.updateInput(width, "200");
|
||||||
|
|
||||||
expect(rectangle?.width).toBe(200);
|
expect(rectangle?.width).toBe(200);
|
||||||
expect(frame.width).toBe(200);
|
expect(frame.width).toBe(200);
|
||||||
expect(text?.width).not.toBe(200);
|
expect(text?.width).not.toBe(200);
|
||||||
|
|
||||||
editInput(angle, "40");
|
UI.updateInput(angle, "40");
|
||||||
|
|
||||||
const angleInRadian = degreeToRadian(40);
|
const angleInRadian = degreeToRadian(40);
|
||||||
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
expect(rectangle?.angle).toBeCloseTo(angleInRadian, 4);
|
||||||
@@ -686,7 +682,7 @@ describe("stats for multiple elements", () => {
|
|||||||
mouse.click();
|
mouse.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
h.app.actionManager.executeAction(actionGroup);
|
API.executeAction(actionGroup);
|
||||||
};
|
};
|
||||||
|
|
||||||
createAndSelectGroup();
|
createAndSelectGroup();
|
||||||
@@ -703,7 +699,7 @@ describe("stats for multiple elements", () => {
|
|||||||
expect(x).toBeDefined();
|
expect(x).toBeDefined();
|
||||||
expect(Number(x.value)).toBe(x1);
|
expect(Number(x.value)).toBe(x1);
|
||||||
|
|
||||||
editInput(x, "300");
|
UI.updateInput(x, "300");
|
||||||
|
|
||||||
expect(h.elements[0].x).toBe(300);
|
expect(h.elements[0].x).toBe(300);
|
||||||
expect(h.elements[1].x).toBe(400);
|
expect(h.elements[1].x).toBe(400);
|
||||||
@@ -716,7 +712,7 @@ describe("stats for multiple elements", () => {
|
|||||||
expect(y).toBeDefined();
|
expect(y).toBeDefined();
|
||||||
expect(Number(y.value)).toBe(y1);
|
expect(Number(y.value)).toBe(y1);
|
||||||
|
|
||||||
editInput(y, "200");
|
UI.updateInput(y, "200");
|
||||||
|
|
||||||
expect(h.elements[0].y).toBe(200);
|
expect(h.elements[0].y).toBe(200);
|
||||||
expect(h.elements[1].y).toBe(300);
|
expect(h.elements[1].y).toBe(300);
|
||||||
@@ -734,20 +730,20 @@ describe("stats for multiple elements", () => {
|
|||||||
expect(height).toBeDefined();
|
expect(height).toBeDefined();
|
||||||
expect(Number(height.value)).toBe(200);
|
expect(Number(height.value)).toBe(200);
|
||||||
|
|
||||||
editInput(width, "400");
|
UI.updateInput(width, "400");
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||||
let newGroupWidth = x2 - x1;
|
let newGroupWidth = x2 - x1;
|
||||||
|
|
||||||
expect(newGroupWidth).toBeCloseTo(400, 4);
|
expect(newGroupWidth).toBeCloseTo(400, 4);
|
||||||
|
|
||||||
editInput(width, "300");
|
UI.updateInput(width, "300");
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||||
newGroupWidth = x2 - x1;
|
newGroupWidth = x2 - x1;
|
||||||
expect(newGroupWidth).toBeCloseTo(300, 4);
|
expect(newGroupWidth).toBeCloseTo(300, 4);
|
||||||
|
|
||||||
editInput(height, "500");
|
UI.updateInput(height, "500");
|
||||||
|
|
||||||
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
[x1, y1, x2, y2] = getCommonBounds(elementsInGroup);
|
||||||
const newGroupHeight = y2 - y1;
|
const newGroupHeight = y2 - y1;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
isInGroup,
|
isInGroup,
|
||||||
} from "../../groups";
|
} from "../../groups";
|
||||||
import { rotate } from "../../math";
|
import { rotate } from "../../math";
|
||||||
|
import type Scene from "../../scene/Scene";
|
||||||
import type { AppState } from "../../types";
|
import type { AppState } from "../../types";
|
||||||
import { getFontString } from "../../utils";
|
import { getFontString } from "../../utils";
|
||||||
|
|
||||||
@@ -124,6 +125,8 @@ export const resizeElement = (
|
|||||||
keepAspectRatio: boolean,
|
keepAspectRatio: boolean,
|
||||||
origElement: ExcalidrawElement,
|
origElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scene: Scene,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
const latestElement = elementsMap.get(origElement.id);
|
const latestElement = elementsMap.get(origElement.id);
|
||||||
@@ -146,6 +149,8 @@ export const resizeElement = (
|
|||||||
nextHeight = Math.max(nextHeight, minHeight);
|
nextHeight = Math.max(nextHeight, minHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { width: oldWidth, height: oldHeight } = latestElement;
|
||||||
|
|
||||||
mutateElement(
|
mutateElement(
|
||||||
latestElement,
|
latestElement,
|
||||||
{
|
{
|
||||||
@@ -164,7 +169,7 @@ export const resizeElement = (
|
|||||||
},
|
},
|
||||||
shouldInformMutation,
|
shouldInformMutation,
|
||||||
);
|
);
|
||||||
updateBindings(latestElement, elementsMap, {
|
updateBindings(latestElement, elementsMap, elements, scene, {
|
||||||
newSize: {
|
newSize: {
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
@@ -193,6 +198,10 @@ export const resizeElement = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateBoundElements(latestElement, elementsMap, {
|
||||||
|
oldSize: { width: oldWidth, height: oldHeight },
|
||||||
|
});
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont) {
|
if (boundTextElement && boundTextFont) {
|
||||||
mutateElement(boundTextElement, {
|
mutateElement(boundTextElement, {
|
||||||
fontSize: boundTextFont.fontSize,
|
fontSize: boundTextFont.fontSize,
|
||||||
@@ -206,6 +215,8 @@ export const moveElement = (
|
|||||||
newTopLeftY: number,
|
newTopLeftY: number,
|
||||||
originalElement: ExcalidrawElement,
|
originalElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scene: Scene,
|
||||||
originalElementsMap: ElementsMap,
|
originalElementsMap: ElementsMap,
|
||||||
shouldInformMutation = true,
|
shouldInformMutation = true,
|
||||||
) => {
|
) => {
|
||||||
@@ -244,7 +255,7 @@ export const moveElement = (
|
|||||||
},
|
},
|
||||||
shouldInformMutation,
|
shouldInformMutation,
|
||||||
);
|
);
|
||||||
updateBindings(latestElement, elementsMap);
|
updateBindings(latestElement, elementsMap, elements, scene);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(
|
const boundTextElement = getBoundTextElement(
|
||||||
originalElement,
|
originalElement,
|
||||||
@@ -288,13 +299,22 @@ export const getAtomicUnits = (
|
|||||||
export const updateBindings = (
|
export const updateBindings = (
|
||||||
latestElement: ExcalidrawElement,
|
latestElement: ExcalidrawElement,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
scene: Scene,
|
||||||
options?: {
|
options?: {
|
||||||
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
simultaneouslyUpdated?: readonly ExcalidrawElement[];
|
||||||
newSize?: { width: number; height: number };
|
newSize?: { width: number; height: number };
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (isLinearElement(latestElement)) {
|
if (isLinearElement(latestElement)) {
|
||||||
bindOrUnbindLinearElements([latestElement], elementsMap, true, []);
|
bindOrUnbindLinearElements(
|
||||||
|
[latestElement],
|
||||||
|
elementsMap,
|
||||||
|
elements,
|
||||||
|
scene,
|
||||||
|
true,
|
||||||
|
[],
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
updateBoundElements(latestElement, elementsMap, options);
|
updateBoundElements(latestElement, elementsMap, options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
|
import React from "react";
|
||||||
import { Excalidraw } from "../../index";
|
import { Excalidraw } from "../../index";
|
||||||
import { KEYS } from "../../keys";
|
import { KEYS } from "../../keys";
|
||||||
import { Keyboard } from "../../tests/helpers/ui";
|
import { Keyboard } from "../../tests/helpers/ui";
|
||||||
import { render, waitFor, getByTestId } from "../../tests/test-utils";
|
import {
|
||||||
|
render,
|
||||||
|
waitFor,
|
||||||
|
getByTestId,
|
||||||
|
fireEvent,
|
||||||
|
} from "../../tests/test-utils";
|
||||||
|
|
||||||
describe("Test <DropdownMenu/>", () => {
|
describe("Test <DropdownMenu/>", () => {
|
||||||
it("should", async () => {
|
it("should", async () => {
|
||||||
@@ -9,7 +15,7 @@ describe("Test <DropdownMenu/>", () => {
|
|||||||
|
|
||||||
expect(window.h.state.openMenu).toBe(null);
|
expect(window.h.state.openMenu).toBe(null);
|
||||||
|
|
||||||
getByTestId(container, "main-menu-trigger").click();
|
fireEvent.click(getByTestId(container, "main-menu-trigger"));
|
||||||
expect(window.h.state.openMenu).toBe("canvas");
|
expect(window.h.state.openMenu).toBe("canvas");
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { render, queryAllByTestId } from "../../tests/test-utils";
|
import { render, queryAllByTestId } from "../../tests/test-utils";
|
||||||
import { Excalidraw, MainMenu } from "../../index";
|
import { Excalidraw, MainMenu } from "../../index";
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ export const Hyperlink = ({
|
|||||||
const { x, y } = getCoordsForPopover(element, appState, elementsMap);
|
const { x, y } = getCoordsForPopover(element, appState, elementsMap);
|
||||||
if (
|
if (
|
||||||
appState.contextMenu ||
|
appState.contextMenu ||
|
||||||
appState.draggingElement ||
|
appState.selectedElementsAreBeingDragged ||
|
||||||
appState.resizingElement ||
|
appState.resizingElement ||
|
||||||
appState.isRotating ||
|
appState.isRotating ||
|
||||||
appState.openMenu ||
|
appState.openMenu ||
|
||||||
|
|||||||
@@ -2095,6 +2095,35 @@ export const lineEditorIcon = createIcon(
|
|||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// arrow-up-right (modified)
|
||||||
|
export const sharpArrowIcon = createIcon(
|
||||||
|
<g>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M6 18l12 -12" />
|
||||||
|
<path d="M18 10v-4h-4" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
// arrow-guide (modified)
|
||||||
|
export const elbowArrowIcon = createIcon(
|
||||||
|
<g>
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
<path d="M4,19L10,19C11.097,19 12,18.097 12,17L12,9C12,7.903 12.903,7 14,7L21,7" />
|
||||||
|
<path d="M18 4l3 3l-3 3" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
|
// arrow-ramp-right-2 (heavily modified)
|
||||||
|
export const roundArrowIcon = createIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M16,12L20,9L16,6" />
|
||||||
|
<path d="M6 20c0 -6.075 4.925 -11 11 -11h3" />
|
||||||
|
</g>,
|
||||||
|
tablerIconProps,
|
||||||
|
);
|
||||||
|
|
||||||
export const collapseDownIcon = createIcon(
|
export const collapseDownIcon = createIcon(
|
||||||
<g>
|
<g>
|
||||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import cssVariables from "./css/variables.module.scss";
|
import cssVariables from "./css/variables.module.scss";
|
||||||
import type { AppProps } from "./types";
|
import type { AppProps, AppState } from "./types";
|
||||||
import type { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
import type { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
||||||
import { COLOR_PALETTE } from "./colors";
|
import { COLOR_PALETTE } from "./colors";
|
||||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||||
@@ -421,3 +421,9 @@ export const DEFAULT_FILENAME = "Untitled";
|
|||||||
export const STATS_PANELS = { generalStats: 1, elementProperties: 2 } as const;
|
export const STATS_PANELS = { generalStats: 1, elementProperties: 2 } as const;
|
||||||
|
|
||||||
export const MIN_WIDTH_OR_HEIGHT = 1;
|
export const MIN_WIDTH_OR_HEIGHT = 1;
|
||||||
|
|
||||||
|
export const ARROW_TYPE: { [T in AppState["currentItemArrowType"]]: T } = {
|
||||||
|
sharp: "sharp",
|
||||||
|
round: "round",
|
||||||
|
elbow: "elbow",
|
||||||
|
};
|
||||||
|
|||||||
@@ -84,9 +84,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": -0.008153707962747813,
|
"focus": -0.008153707962747813,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -117,6 +119,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id47",
|
"elementId": "id47",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": -0.08139534883720931,
|
"focus": -0.08139534883720931,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -139,9 +142,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "ellipse-1",
|
"elementId": "ellipse-1",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0.10666666666666667,
|
"focus": 0.10666666666666667,
|
||||||
"gap": 3.834326468444573,
|
"gap": 3.834326468444573,
|
||||||
},
|
},
|
||||||
@@ -172,6 +177,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "diamond-1",
|
"elementId": "diamond-1",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -328,9 +334,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "text-2",
|
"elementId": "text-2",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 205,
|
"gap": 205,
|
||||||
},
|
},
|
||||||
@@ -361,6 +369,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "text-1",
|
"elementId": "text-1",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -429,9 +438,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id40",
|
"elementId": "id40",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -462,6 +473,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id39",
|
"elementId": "id39",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -604,9 +616,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id44",
|
"elementId": "id44",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -637,6 +651,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id43",
|
"elementId": "id43",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -824,6 +839,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@@ -871,6 +887,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "triangle",
|
"endArrowhead": "triangle",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@@ -1463,9 +1480,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "Alice",
|
"elementId": "Alice",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 5.299874999999986,
|
"gap": 5.299874999999986,
|
||||||
},
|
},
|
||||||
@@ -1498,6 +1517,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -1525,9 +1545,11 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "B",
|
"elementId": "B",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -1556,6 +1578,7 @@ exports[`Test Transform > should transform the elements correctly when linear el
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "Bob",
|
"elementId": "Bob",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": 0,
|
"focus": 0,
|
||||||
"gap": 1,
|
"gap": 1,
|
||||||
},
|
},
|
||||||
@@ -1837,6 +1860,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@@ -1889,6 +1913,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@@ -1941,6 +1966,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
@@ -1993,6 +2019,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
|
import throttle from "lodash.throttle";
|
||||||
|
import { ENV } from "../constants";
|
||||||
import type { OrderedExcalidrawElement } from "../element/types";
|
import type { OrderedExcalidrawElement } from "../element/types";
|
||||||
import { orderByFractionalIndex, syncInvalidIndices } from "../fractionalIndex";
|
import {
|
||||||
|
orderByFractionalIndex,
|
||||||
|
syncInvalidIndices,
|
||||||
|
validateFractionalIndices,
|
||||||
|
} from "../fractionalIndex";
|
||||||
import type { AppState } from "../types";
|
import type { AppState } from "../types";
|
||||||
import type { MakeBrand } from "../utility-types";
|
import type { MakeBrand } from "../utility-types";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
@@ -20,7 +26,7 @@ const shouldDiscardRemoteElement = (
|
|||||||
// local element is being edited
|
// local element is being edited
|
||||||
(local.id === localAppState.editingElement?.id ||
|
(local.id === localAppState.editingElement?.id ||
|
||||||
local.id === localAppState.resizingElement?.id ||
|
local.id === localAppState.resizingElement?.id ||
|
||||||
local.id === localAppState.draggingElement?.id || // TODO: Is this still valid? As draggingElement is selection element, which is never part of the elements array
|
local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
|
||||||
// local element is newer
|
// local element is newer
|
||||||
local.version > remote.version ||
|
local.version > remote.version ||
|
||||||
// resolve conflicting edits deterministically by taking the one with
|
// resolve conflicting edits deterministically by taking the one with
|
||||||
@@ -33,6 +39,37 @@ const shouldDiscardRemoteElement = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateIndicesThrottled = throttle(
|
||||||
|
(
|
||||||
|
orderedElements: readonly OrderedExcalidrawElement[],
|
||||||
|
localElements: readonly OrderedExcalidrawElement[],
|
||||||
|
remoteElements: readonly RemoteExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
import.meta.env.DEV ||
|
||||||
|
import.meta.env.MODE === ENV.TEST ||
|
||||||
|
window?.DEBUG_FRACTIONAL_INDICES
|
||||||
|
) {
|
||||||
|
// create new instances due to the mutation
|
||||||
|
const elements = syncInvalidIndices(
|
||||||
|
orderedElements.map((x) => ({ ...x })),
|
||||||
|
);
|
||||||
|
|
||||||
|
validateFractionalIndices(elements, {
|
||||||
|
// throw in dev & test only, to remain functional on `DEBUG_FRACTIONAL_INDICES`
|
||||||
|
shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
|
||||||
|
includeBoundTextValidation: true,
|
||||||
|
reconciliationContext: {
|
||||||
|
localElements,
|
||||||
|
remoteElements,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1000 * 60,
|
||||||
|
{ leading: true, trailing: false },
|
||||||
|
);
|
||||||
|
|
||||||
export const reconcileElements = (
|
export const reconcileElements = (
|
||||||
localElements: readonly OrderedExcalidrawElement[],
|
localElements: readonly OrderedExcalidrawElement[],
|
||||||
remoteElements: readonly RemoteExcalidrawElement[],
|
remoteElements: readonly RemoteExcalidrawElement[],
|
||||||
@@ -72,6 +109,8 @@ export const reconcileElements = (
|
|||||||
|
|
||||||
const orderedElements = orderByFractionalIndex(reconciledElements);
|
const orderedElements = orderByFractionalIndex(reconciledElements);
|
||||||
|
|
||||||
|
validateIndicesThrottled(orderedElements, localElements, remoteElements);
|
||||||
|
|
||||||
// de-duplicate indices
|
// de-duplicate indices
|
||||||
syncInvalidIndices(orderedElements);
|
syncInvalidIndices(orderedElements);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawElementType,
|
ExcalidrawElementType,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
} from "../element";
|
} from "../element";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
isElbowArrow,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
isUsingAdaptiveRadius,
|
isUsingAdaptiveRadius,
|
||||||
@@ -92,11 +94,21 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
|
|||||||
return DEFAULT_FONT_FAMILY;
|
return DEFAULT_FONT_FAMILY;
|
||||||
};
|
};
|
||||||
|
|
||||||
const repairBinding = (binding: PointBinding | null) => {
|
const repairBinding = (
|
||||||
|
element: ExcalidrawLinearElement,
|
||||||
|
binding: PointBinding | null,
|
||||||
|
): PointBinding | null => {
|
||||||
if (!binding) {
|
if (!binding) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { ...binding, focus: binding.focus || 0 };
|
|
||||||
|
return {
|
||||||
|
...binding,
|
||||||
|
focus: binding.focus || 0,
|
||||||
|
fixedPoint: isElbowArrow(element)
|
||||||
|
? binding.fixedPoint ?? ([0, 0] as [number, number])
|
||||||
|
: null,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreElementWithProperties = <
|
const restoreElementWithProperties = <
|
||||||
@@ -242,11 +254,7 @@ const restoreElement = (
|
|||||||
// @ts-ignore LEGACY type
|
// @ts-ignore LEGACY type
|
||||||
// eslint-disable-next-line no-fallthrough
|
// eslint-disable-next-line no-fallthrough
|
||||||
case "draw":
|
case "draw":
|
||||||
case "arrow": {
|
const { startArrowhead = null, endArrowhead = null } = element;
|
||||||
const {
|
|
||||||
startArrowhead = null,
|
|
||||||
endArrowhead = element.type === "arrow" ? "arrow" : null,
|
|
||||||
} = element;
|
|
||||||
let x = element.x;
|
let x = element.x;
|
||||||
let y = element.y;
|
let y = element.y;
|
||||||
let points = // migrate old arrow model to new one
|
let points = // migrate old arrow model to new one
|
||||||
@@ -266,8 +274,8 @@ const restoreElement = (
|
|||||||
(element.type as ExcalidrawElementType | "draw") === "draw"
|
(element.type as ExcalidrawElementType | "draw") === "draw"
|
||||||
? "line"
|
? "line"
|
||||||
: element.type,
|
: element.type,
|
||||||
startBinding: repairBinding(element.startBinding),
|
startBinding: repairBinding(element, element.startBinding),
|
||||||
endBinding: repairBinding(element.endBinding),
|
endBinding: repairBinding(element, element.endBinding),
|
||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
@@ -276,6 +284,36 @@ const restoreElement = (
|
|||||||
y,
|
y,
|
||||||
...getSizeFromPoints(points),
|
...getSizeFromPoints(points),
|
||||||
});
|
});
|
||||||
|
case "arrow": {
|
||||||
|
const { startArrowhead = null, endArrowhead = "arrow" } = element;
|
||||||
|
let x = element.x;
|
||||||
|
let y = element.y;
|
||||||
|
let points = // migrate old arrow model to new one
|
||||||
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
|
? [
|
||||||
|
[0, 0],
|
||||||
|
[element.width, element.height],
|
||||||
|
]
|
||||||
|
: element.points;
|
||||||
|
|
||||||
|
if (points[0][0] !== 0 || points[0][1] !== 0) {
|
||||||
|
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Separate arrow from linear element
|
||||||
|
return restoreElementWithProperties(element as ExcalidrawArrowElement, {
|
||||||
|
type: element.type,
|
||||||
|
startBinding: repairBinding(element, element.startBinding),
|
||||||
|
endBinding: repairBinding(element, element.endBinding),
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
startArrowhead,
|
||||||
|
endArrowhead,
|
||||||
|
points,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
elbowed: (element as ExcalidrawArrowElement).elbowed,
|
||||||
|
...getSizeFromPoints(points),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// generic elements
|
// generic elements
|
||||||
|
|||||||
@@ -771,6 +771,7 @@ describe("Test Transform", () => {
|
|||||||
const [arrow, rect] = excalidrawElements;
|
const [arrow, rect] = excalidrawElements;
|
||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
|
fixedPoint: null,
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 205,
|
gap: 205,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { bindLinearElement } from "../element/binding";
|
import { bindLinearElement } from "../element/binding";
|
||||||
import type { ElementConstructorOpts } from "../element/newElement";
|
import type { ElementConstructorOpts } from "../element/newElement";
|
||||||
import {
|
import {
|
||||||
|
newArrowElement,
|
||||||
newFrameElement,
|
newFrameElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
newMagicFrameElement,
|
newMagicFrameElement,
|
||||||
@@ -51,6 +52,7 @@ import { getSizeFromPoints } from "../points";
|
|||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { getLineHeight } from "../fonts";
|
import { getLineHeight } from "../fonts";
|
||||||
|
import { isArrowElement } from "../element/typeChecks";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -545,7 +547,7 @@ export const convertToExcalidrawElements = (
|
|||||||
case "arrow": {
|
case "arrow": {
|
||||||
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
|
||||||
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
|
||||||
excalidrawElement = newLinearElement({
|
excalidrawElement = newArrowElement({
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
endArrowhead: "arrow",
|
endArrowhead: "arrow",
|
||||||
@@ -554,6 +556,7 @@ export const convertToExcalidrawElements = (
|
|||||||
[width, height],
|
[width, height],
|
||||||
],
|
],
|
||||||
...element,
|
...element,
|
||||||
|
type: "arrow",
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.assign(
|
Object.assign(
|
||||||
@@ -655,7 +658,7 @@ export const convertToExcalidrawElements = (
|
|||||||
elementStore.add(container);
|
elementStore.add(container);
|
||||||
elementStore.add(text);
|
elementStore.add(text);
|
||||||
|
|
||||||
if (container.type === "arrow") {
|
if (isArrowElement(container)) {
|
||||||
const originalStart =
|
const originalStart =
|
||||||
element.type === "arrow" ? element?.start : undefined;
|
element.type === "arrow" ? element?.start : undefined;
|
||||||
const originalEnd =
|
const originalEnd =
|
||||||
@@ -674,7 +677,7 @@ export const convertToExcalidrawElements = (
|
|||||||
}
|
}
|
||||||
const { linearElement, startBoundElement, endBoundElement } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
container as ExcalidrawArrowElement,
|
container,
|
||||||
originalStart,
|
originalStart,
|
||||||
originalEnd,
|
originalEnd,
|
||||||
elementStore,
|
elementStore,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const ElementCanvasButtons = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
appState.contextMenu ||
|
appState.contextMenu ||
|
||||||
appState.draggingElement ||
|
appState.newElement ||
|
||||||
appState.resizingElement ||
|
appState.resizingElement ||
|
||||||
appState.isRotating ||
|
appState.isRotating ||
|
||||||
appState.openMenu ||
|
appState.openMenu ||
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import { getGridPoint } from "../math";
|
|||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
@@ -18,9 +19,8 @@ import { TEXT_AUTOWRAP_THRESHOLD } from "../constants";
|
|||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
_selectedElements: NonDeletedExcalidrawElement[],
|
||||||
offset: { x: number; y: number },
|
offset: { x: number; y: number },
|
||||||
appState: AppState,
|
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
snapOffset: {
|
snapOffset: {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -28,6 +28,25 @@ export const dragSelectedElements = (
|
|||||||
},
|
},
|
||||||
gridSize: AppState["gridSize"],
|
gridSize: AppState["gridSize"],
|
||||||
) => {
|
) => {
|
||||||
|
if (
|
||||||
|
_selectedElements.length === 1 &&
|
||||||
|
isArrowElement(_selectedElements[0]) &&
|
||||||
|
isElbowArrow(_selectedElements[0]) &&
|
||||||
|
(_selectedElements[0].startBinding || _selectedElements[0].endBinding)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedElements = _selectedElements.filter(
|
||||||
|
(el) =>
|
||||||
|
!(
|
||||||
|
isArrowElement(el) &&
|
||||||
|
isElbowArrow(el) &&
|
||||||
|
el.startBinding &&
|
||||||
|
el.endBinding
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// we do not want a frame and its elements to be selected at the same time
|
// we do not want a frame and its elements to be selected at the same time
|
||||||
// but when it happens (due to some bug), we want to avoid updating element
|
// but when it happens (due to some bug), we want to avoid updating element
|
||||||
// in the frame twice, hence the use of set
|
// in the frame twice, hence the use of set
|
||||||
@@ -136,7 +155,7 @@ export const getDragOffsetXY = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const dragNewElement = (
|
export const dragNewElement = (
|
||||||
draggingElement: NonDeletedExcalidrawElement,
|
newElement: NonDeletedExcalidrawElement,
|
||||||
elementType: AppState["activeTool"]["type"],
|
elementType: AppState["activeTool"]["type"],
|
||||||
originX: number,
|
originX: number,
|
||||||
originY: number,
|
originY: number,
|
||||||
@@ -155,7 +174,7 @@ export const dragNewElement = (
|
|||||||
y: number;
|
y: number;
|
||||||
} | null = null,
|
} | null = null,
|
||||||
) => {
|
) => {
|
||||||
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
|
if (shouldMaintainAspectRatio && newElement.type !== "selection") {
|
||||||
if (widthAspectRatio) {
|
if (widthAspectRatio) {
|
||||||
height = width / widthAspectRatio;
|
height = width / widthAspectRatio;
|
||||||
} else {
|
} else {
|
||||||
@@ -194,17 +213,14 @@ export const dragNewElement = (
|
|||||||
|
|
||||||
let textAutoResize = null;
|
let textAutoResize = null;
|
||||||
|
|
||||||
// NOTE this should apply only to creating text elements, not existing
|
if (isTextElement(newElement)) {
|
||||||
// (once we rewrite appState.draggingElement to actually mean dragging
|
height = newElement.height;
|
||||||
// elements)
|
|
||||||
if (isTextElement(draggingElement)) {
|
|
||||||
height = draggingElement.height;
|
|
||||||
const minWidth = getMinTextElementWidth(
|
const minWidth = getMinTextElementWidth(
|
||||||
getFontString({
|
getFontString({
|
||||||
fontSize: draggingElement.fontSize,
|
fontSize: newElement.fontSize,
|
||||||
fontFamily: draggingElement.fontFamily,
|
fontFamily: newElement.fontFamily,
|
||||||
}),
|
}),
|
||||||
draggingElement.lineHeight,
|
newElement.lineHeight,
|
||||||
);
|
);
|
||||||
width = Math.max(width, minWidth);
|
width = Math.max(width, minWidth);
|
||||||
|
|
||||||
@@ -221,7 +237,7 @@ export const dragNewElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (width !== 0 && height !== 0) {
|
if (width !== 0 && height !== 0) {
|
||||||
mutateElement(draggingElement, {
|
mutateElement(newElement, {
|
||||||
x: newX + (originOffset?.x ?? 0),
|
x: newX + (originOffset?.x ?? 0),
|
||||||
y: newY + (originOffset?.y ?? 0),
|
y: newY + (originOffset?.y ?? 0),
|
||||||
width,
|
width,
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import { lineAngle } from "../../utils/geometry/geometry";
|
||||||
|
import type { Point, Vector } from "../../utils/geometry/shape";
|
||||||
|
import {
|
||||||
|
getCenterForBounds,
|
||||||
|
PointInTriangle,
|
||||||
|
rotatePoint,
|
||||||
|
scalePointFromOrigin,
|
||||||
|
} from "../math";
|
||||||
|
import type { Bounds } from "./bounds";
|
||||||
|
import type { ExcalidrawBindableElement } from "./types";
|
||||||
|
|
||||||
|
export const HEADING_RIGHT = [1, 0] as Heading;
|
||||||
|
export const HEADING_DOWN = [0, 1] as Heading;
|
||||||
|
export const HEADING_LEFT = [-1, 0] as Heading;
|
||||||
|
export const HEADING_UP = [0, -1] as Heading;
|
||||||
|
export type Heading = [1, 0] | [0, 1] | [-1, 0] | [0, -1];
|
||||||
|
|
||||||
|
export const headingForDiamond = (a: Point, b: Point) => {
|
||||||
|
const angle = lineAngle([a, b]);
|
||||||
|
if (angle >= 315 || angle < 45) {
|
||||||
|
return HEADING_UP;
|
||||||
|
} else if (angle >= 45 && angle < 135) {
|
||||||
|
return HEADING_RIGHT;
|
||||||
|
} else if (angle >= 135 && angle < 225) {
|
||||||
|
return HEADING_DOWN;
|
||||||
|
}
|
||||||
|
return HEADING_LEFT;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const vectorToHeading = (vec: Vector): Heading => {
|
||||||
|
const [x, y] = vec;
|
||||||
|
const absX = Math.abs(x);
|
||||||
|
const absY = Math.abs(y);
|
||||||
|
if (x > absY) {
|
||||||
|
return HEADING_RIGHT;
|
||||||
|
} else if (x <= -absY) {
|
||||||
|
return HEADING_LEFT;
|
||||||
|
} else if (y > absX) {
|
||||||
|
return HEADING_DOWN;
|
||||||
|
}
|
||||||
|
return HEADING_UP;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compareHeading = (a: Heading, b: Heading) =>
|
||||||
|
a[0] === b[0] && a[1] === b[1];
|
||||||
|
|
||||||
|
// Gets the heading for the point by creating a bounding box around the rotated
|
||||||
|
// close fitting bounding box, then creating 4 search cones around the center of
|
||||||
|
// the external bbox.
|
||||||
|
export const headingForPointFromElement = (
|
||||||
|
element: Readonly<ExcalidrawBindableElement>,
|
||||||
|
aabb: Readonly<Bounds>,
|
||||||
|
point: Readonly<Point>,
|
||||||
|
): Heading => {
|
||||||
|
const SEARCH_CONE_MULTIPLIER = 2;
|
||||||
|
|
||||||
|
const midPoint = getCenterForBounds(aabb);
|
||||||
|
|
||||||
|
if (element.type === "diamond") {
|
||||||
|
if (point[0] < element.x) {
|
||||||
|
return HEADING_LEFT;
|
||||||
|
} else if (point[1] < element.y) {
|
||||||
|
return HEADING_UP;
|
||||||
|
} else if (point[0] > element.x + element.width) {
|
||||||
|
return HEADING_RIGHT;
|
||||||
|
} else if (point[1] > element.y + element.height) {
|
||||||
|
return HEADING_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const top = rotatePoint(
|
||||||
|
scalePointFromOrigin(
|
||||||
|
[element.x + element.width / 2, element.y],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const right = rotatePoint(
|
||||||
|
scalePointFromOrigin(
|
||||||
|
[element.x + element.width, element.y + element.height / 2],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const bottom = rotatePoint(
|
||||||
|
scalePointFromOrigin(
|
||||||
|
[element.x + element.width / 2, element.y + element.height],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const left = rotatePoint(
|
||||||
|
scalePointFromOrigin(
|
||||||
|
[element.x, element.y + element.height / 2],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
),
|
||||||
|
midPoint,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (PointInTriangle(point, top, right, midPoint)) {
|
||||||
|
return headingForDiamond(top, right);
|
||||||
|
} else if (PointInTriangle(point, right, bottom, midPoint)) {
|
||||||
|
return headingForDiamond(right, bottom);
|
||||||
|
} else if (PointInTriangle(point, bottom, left, midPoint)) {
|
||||||
|
return headingForDiamond(bottom, left);
|
||||||
|
}
|
||||||
|
|
||||||
|
return headingForDiamond(left, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topLeft = scalePointFromOrigin(
|
||||||
|
[aabb[0], aabb[1]],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
);
|
||||||
|
const topRight = scalePointFromOrigin(
|
||||||
|
[aabb[2], aabb[1]],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
);
|
||||||
|
const bottomLeft = scalePointFromOrigin(
|
||||||
|
[aabb[0], aabb[3]],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
);
|
||||||
|
const bottomRight = scalePointFromOrigin(
|
||||||
|
[aabb[2], aabb[3]],
|
||||||
|
midPoint,
|
||||||
|
SEARCH_CONE_MULTIPLIER,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PointInTriangle(point, topLeft, topRight, midPoint)
|
||||||
|
? HEADING_UP
|
||||||
|
: PointInTriangle(point, topRight, bottomRight, midPoint)
|
||||||
|
? HEADING_RIGHT
|
||||||
|
: PointInTriangle(point, bottomRight, bottomLeft, midPoint)
|
||||||
|
? HEADING_DOWN
|
||||||
|
: HEADING_LEFT;
|
||||||
|
};
|
||||||
@@ -11,6 +11,7 @@ export {
|
|||||||
newTextElement,
|
newTextElement,
|
||||||
refreshTextDimensions,
|
refreshTextDimensions,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
|
newArrowElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
duplicateElement,
|
duplicateElement,
|
||||||
} from "./newElement";
|
} from "./newElement";
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import type {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
NonDeletedSceneElementsMap,
|
NonDeletedSceneElementsMap,
|
||||||
|
OrderedExcalidrawElement,
|
||||||
|
FixedPointBinding,
|
||||||
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
distance2d,
|
distance2d,
|
||||||
@@ -33,7 +36,6 @@ import type {
|
|||||||
AppState,
|
AppState,
|
||||||
PointerCoords,
|
PointerCoords,
|
||||||
InteractiveCanvasAppState,
|
InteractiveCanvasAppState,
|
||||||
AppClassProperties,
|
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
|
|
||||||
@@ -43,13 +45,19 @@ import {
|
|||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "./binding";
|
} from "./binding";
|
||||||
import { tupleToCoors } from "../utils";
|
import { tupleToCoors } from "../utils";
|
||||||
import { isBindingElement } from "./typeChecks";
|
import {
|
||||||
|
isBindingElement,
|
||||||
|
isElbowArrow,
|
||||||
|
isFixedPointBinding,
|
||||||
|
} from "./typeChecks";
|
||||||
import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
|
import { KEYS, shouldRotateWithDiscreteAngle } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import type { Store } from "../store";
|
import type { Store } from "../store";
|
||||||
|
import { mutateElbowArrow } from "./routing";
|
||||||
|
import type Scene from "../scene/Scene";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
@@ -67,6 +75,7 @@ export class LinearElementEditor {
|
|||||||
prevSelectedPointsIndices: readonly number[] | null;
|
prevSelectedPointsIndices: readonly number[] | null;
|
||||||
/** index */
|
/** index */
|
||||||
lastClickedPoint: number;
|
lastClickedPoint: number;
|
||||||
|
lastClickedIsEndPoint: boolean;
|
||||||
origin: Readonly<{ x: number; y: number }> | null;
|
origin: Readonly<{ x: number; y: number }> | null;
|
||||||
segmentMidpoint: {
|
segmentMidpoint: {
|
||||||
value: Point | null;
|
value: Point | null;
|
||||||
@@ -91,7 +100,9 @@ export class LinearElementEditor {
|
|||||||
this.elementId = element.id as string & {
|
this.elementId = element.id as string & {
|
||||||
_brand: "excalidrawLinearElementId";
|
_brand: "excalidrawLinearElementId";
|
||||||
};
|
};
|
||||||
LinearElementEditor.normalizePoints(element);
|
if (!arePointsEqual(element.points[0], [0, 0])) {
|
||||||
|
console.error("Linear element is not normalized", Error().stack);
|
||||||
|
}
|
||||||
|
|
||||||
this.selectedPointsIndices = null;
|
this.selectedPointsIndices = null;
|
||||||
this.lastUncommittedPoint = null;
|
this.lastUncommittedPoint = null;
|
||||||
@@ -102,6 +113,7 @@ export class LinearElementEditor {
|
|||||||
this.pointerDownState = {
|
this.pointerDownState = {
|
||||||
prevSelectedPointsIndices: null,
|
prevSelectedPointsIndices: null,
|
||||||
lastClickedPoint: -1,
|
lastClickedPoint: -1,
|
||||||
|
lastClickedIsEndPoint: false,
|
||||||
origin: null,
|
origin: null,
|
||||||
|
|
||||||
segmentMidpoint: {
|
segmentMidpoint: {
|
||||||
@@ -140,10 +152,7 @@ export class LinearElementEditor {
|
|||||||
setState: React.Component<any, AppState>["setState"],
|
setState: React.Component<any, AppState>["setState"],
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
elementsMap: NonDeletedSceneElementsMap,
|
||||||
) {
|
) {
|
||||||
if (
|
if (!appState.editingLinearElement || !appState.selectionElement) {
|
||||||
!appState.editingLinearElement ||
|
|
||||||
appState.draggingElement?.type !== "selection"
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { editingLinearElement } = appState;
|
const { editingLinearElement } = appState;
|
||||||
@@ -155,15 +164,15 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||||
getElementAbsoluteCoords(appState.draggingElement, elementsMap);
|
getElementAbsoluteCoords(appState.selectionElement, elementsMap);
|
||||||
|
|
||||||
const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
|
const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextSelectedPoints = pointsSceneCoords.reduce(
|
const nextSelectedPoints = pointsSceneCoords
|
||||||
(acc: number[], point, index) => {
|
.reduce((acc: number[], point, index) => {
|
||||||
if (
|
if (
|
||||||
(point[0] >= selectionX1 &&
|
(point[0] >= selectionX1 &&
|
||||||
point[0] <= selectionX2 &&
|
point[0] <= selectionX2 &&
|
||||||
@@ -175,9 +184,17 @@ export class LinearElementEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
},
|
}, [])
|
||||||
[],
|
.filter((index) => {
|
||||||
);
|
if (
|
||||||
|
isElbowArrow(element) &&
|
||||||
|
index !== 0 &&
|
||||||
|
index !== element.points.length - 1
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
setState({
|
setState({
|
||||||
editingLinearElement: {
|
editingLinearElement: {
|
||||||
@@ -200,21 +217,52 @@ export class LinearElementEditor {
|
|||||||
pointSceneCoords: { x: number; y: number }[],
|
pointSceneCoords: { x: number; y: number }[],
|
||||||
) => void,
|
) => void,
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
elementsMap: NonDeletedSceneElementsMap,
|
scene: Scene,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!linearElementEditor) {
|
if (!linearElementEditor) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const { selectedPointsIndices, elementId } = linearElementEditor;
|
const { elementId } = linearElementEditor;
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
isElbowArrow(element) &&
|
||||||
|
!linearElementEditor.pointerDownState.lastClickedIsEndPoint &&
|
||||||
|
linearElementEditor.pointerDownState.lastClickedPoint !== 0
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedPointsIndices = isElbowArrow(element)
|
||||||
|
? linearElementEditor.selectedPointsIndices
|
||||||
|
?.reduce(
|
||||||
|
(startEnd, index) =>
|
||||||
|
(index === 0
|
||||||
|
? [0, startEnd[1]]
|
||||||
|
: [startEnd[0], element.points.length - 1]) as [
|
||||||
|
boolean | number,
|
||||||
|
boolean | number,
|
||||||
|
],
|
||||||
|
[false, false] as [number | boolean, number | boolean],
|
||||||
|
)
|
||||||
|
.filter(
|
||||||
|
(idx: number | boolean): idx is number => typeof idx === "number",
|
||||||
|
)
|
||||||
|
: linearElementEditor.selectedPointsIndices;
|
||||||
|
const lastClickedPoint = isElbowArrow(element)
|
||||||
|
? linearElementEditor.pointerDownState.lastClickedPoint > 0
|
||||||
|
? element.points.length - 1
|
||||||
|
: 0
|
||||||
|
: linearElementEditor.pointerDownState.lastClickedPoint;
|
||||||
|
|
||||||
// point that's being dragged (out of all selected points)
|
// point that's being dragged (out of all selected points)
|
||||||
const draggingPoint = element.points[
|
const draggingPoint = element.points[lastClickedPoint] as
|
||||||
linearElementEditor.pointerDownState.lastClickedPoint
|
| [number, number]
|
||||||
] as [number, number] | undefined;
|
| undefined;
|
||||||
|
|
||||||
if (selectedPointsIndices && draggingPoint) {
|
if (selectedPointsIndices && draggingPoint) {
|
||||||
if (
|
if (
|
||||||
@@ -234,15 +282,17 @@ export class LinearElementEditor {
|
|||||||
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
|
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: selectedIndex,
|
[
|
||||||
point: [width + referencePoint[0], height + referencePoint[1]],
|
{
|
||||||
isDragging:
|
index: selectedIndex,
|
||||||
selectedIndex ===
|
point: [width + referencePoint[0], height + referencePoint[1]],
|
||||||
linearElementEditor.pointerDownState.lastClickedPoint,
|
isDragging: selectedIndex === lastClickedPoint,
|
||||||
},
|
},
|
||||||
]);
|
],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
const newDraggingPointPosition = LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
@@ -259,8 +309,7 @@ export class LinearElementEditor {
|
|||||||
element,
|
element,
|
||||||
selectedPointsIndices.map((pointIndex) => {
|
selectedPointsIndices.map((pointIndex) => {
|
||||||
const newPointPosition =
|
const newPointPosition =
|
||||||
pointIndex ===
|
pointIndex === lastClickedPoint
|
||||||
linearElementEditor.pointerDownState.lastClickedPoint
|
|
||||||
? LinearElementEditor.createPointAt(
|
? LinearElementEditor.createPointAt(
|
||||||
element,
|
element,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
@@ -275,11 +324,10 @@ export class LinearElementEditor {
|
|||||||
return {
|
return {
|
||||||
index: pointIndex,
|
index: pointIndex,
|
||||||
point: newPointPosition,
|
point: newPointPosition,
|
||||||
isDragging:
|
isDragging: pointIndex === lastClickedPoint,
|
||||||
pointIndex ===
|
|
||||||
linearElementEditor.pointerDownState.lastClickedPoint,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
elementsMap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,9 +382,10 @@ export class LinearElementEditor {
|
|||||||
event: PointerEvent,
|
event: PointerEvent,
|
||||||
editingLinearElement: LinearElementEditor,
|
editingLinearElement: LinearElementEditor,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
app: AppClassProperties,
|
scene: Scene,
|
||||||
): LinearElementEditor {
|
): LinearElementEditor {
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
|
|
||||||
const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
|
const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
|
||||||
editingLinearElement;
|
editingLinearElement;
|
||||||
@@ -361,15 +410,19 @@ export class LinearElementEditor {
|
|||||||
selectedPoint === element.points.length - 1
|
selectedPoint === element.points.length - 1
|
||||||
) {
|
) {
|
||||||
if (isPathALoop(element.points, appState.zoom.value)) {
|
if (isPathALoop(element.points, appState.zoom.value)) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: selectedPoint,
|
[
|
||||||
point:
|
{
|
||||||
selectedPoint === 0
|
index: selectedPoint,
|
||||||
? element.points[element.points.length - 1]
|
point:
|
||||||
: element.points[0],
|
selectedPoint === 0
|
||||||
},
|
? element.points[element.points.length - 1]
|
||||||
]);
|
: element.points[0],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingElement = isBindingEnabled(appState)
|
const bindingElement = isBindingEnabled(appState)
|
||||||
@@ -381,6 +434,7 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
@@ -645,13 +699,14 @@ export class LinearElementEditor {
|
|||||||
store: Store,
|
store: Store,
|
||||||
scenePointer: { x: number; y: number },
|
scenePointer: { x: number; y: number },
|
||||||
linearElementEditor: LinearElementEditor,
|
linearElementEditor: LinearElementEditor,
|
||||||
app: AppClassProperties,
|
scene: Scene,
|
||||||
): {
|
): {
|
||||||
didAddPoint: boolean;
|
didAddPoint: boolean;
|
||||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||||
linearElementEditor: LinearElementEditor | null;
|
linearElementEditor: LinearElementEditor | null;
|
||||||
} {
|
} {
|
||||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
const elements = scene.getNonDeletedElements();
|
||||||
|
|
||||||
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
|
||||||
didAddPoint: false,
|
didAddPoint: false,
|
||||||
@@ -685,7 +740,10 @@ export class LinearElementEditor {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (event.altKey && appState.editingLinearElement) {
|
if (event.altKey && appState.editingLinearElement) {
|
||||||
if (linearElementEditor.lastUncommittedPoint == null) {
|
if (
|
||||||
|
linearElementEditor.lastUncommittedPoint == null ||
|
||||||
|
!isElbowArrow(element)
|
||||||
|
) {
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
points: [
|
points: [
|
||||||
...element.points,
|
...element.points,
|
||||||
@@ -706,6 +764,7 @@ export class LinearElementEditor {
|
|||||||
pointerDownState: {
|
pointerDownState: {
|
||||||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||||
lastClickedPoint: -1,
|
lastClickedPoint: -1,
|
||||||
|
lastClickedIsEndPoint: false,
|
||||||
origin: { x: scenePointer.x, y: scenePointer.y },
|
origin: { x: scenePointer.x, y: scenePointer.y },
|
||||||
segmentMidpoint: {
|
segmentMidpoint: {
|
||||||
value: segmentMidpoint,
|
value: segmentMidpoint,
|
||||||
@@ -717,6 +776,7 @@ export class LinearElementEditor {
|
|||||||
lastUncommittedPoint: null,
|
lastUncommittedPoint: null,
|
||||||
endBindingElement: getHoveredElementForBinding(
|
endBindingElement: getHoveredElementForBinding(
|
||||||
scenePointer,
|
scenePointer,
|
||||||
|
elements,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -749,6 +809,7 @@ export class LinearElementEditor {
|
|||||||
startBindingElement,
|
startBindingElement,
|
||||||
endBindingElement,
|
endBindingElement,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
|
scene,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -781,6 +842,7 @@ export class LinearElementEditor {
|
|||||||
pointerDownState: {
|
pointerDownState: {
|
||||||
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
|
||||||
lastClickedPoint: clickedPointIndex,
|
lastClickedPoint: clickedPointIndex,
|
||||||
|
lastClickedIsEndPoint: clickedPointIndex === element.points.length - 1,
|
||||||
origin: { x: scenePointer.x, y: scenePointer.y },
|
origin: { x: scenePointer.x, y: scenePointer.y },
|
||||||
segmentMidpoint: {
|
segmentMidpoint: {
|
||||||
value: segmentMidpoint,
|
value: segmentMidpoint,
|
||||||
@@ -815,7 +877,7 @@ export class LinearElementEditor {
|
|||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
): LinearElementEditor | null {
|
): LinearElementEditor | null {
|
||||||
if (!appState.editingLinearElement) {
|
if (!appState.editingLinearElement) {
|
||||||
return null;
|
return null;
|
||||||
@@ -831,7 +893,11 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
if (!event.altKey) {
|
if (!event.altKey) {
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.deletePoints(element, [points.length - 1]);
|
LinearElementEditor.deletePoints(
|
||||||
|
element,
|
||||||
|
[points.length - 1],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
@@ -862,19 +928,29 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
scenePointerX - appState.editingLinearElement.pointerOffset.x,
|
scenePointerX - appState.editingLinearElement.pointerOffset.x,
|
||||||
scenePointerY - appState.editingLinearElement.pointerOffset.y,
|
scenePointerY - appState.editingLinearElement.pointerOffset.y,
|
||||||
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
|
event[KEYS.CTRL_OR_CMD] || isElbowArrow(element)
|
||||||
|
? null
|
||||||
|
: appState.gridSize,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastPoint === lastUncommittedPoint) {
|
if (lastPoint === lastUncommittedPoint) {
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: element.points.length - 1,
|
[
|
||||||
point: newPoint,
|
{
|
||||||
},
|
index: element.points.length - 1,
|
||||||
]);
|
point: newPoint,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
LinearElementEditor.addPoints(element, appState, [{ point: newPoint }]);
|
LinearElementEditor.addPoints(
|
||||||
|
element,
|
||||||
|
[{ point: newPoint }],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...appState.editingLinearElement,
|
...appState.editingLinearElement,
|
||||||
@@ -938,6 +1014,11 @@ export class LinearElementEditor {
|
|||||||
absoluteCoords: Point,
|
absoluteCoords: Point,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
): Point {
|
): Point {
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
// No rotation for elbow arrows
|
||||||
|
return [absoluteCoords[0] - element.x, absoluteCoords[1] - element.y];
|
||||||
|
}
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
const cx = (x1 + x2) / 2;
|
const cx = (x1 + x2) / 2;
|
||||||
const cy = (y1 + y2) / 2;
|
const cy = (y1 + y2) / 2;
|
||||||
@@ -1013,7 +1094,7 @@ export class LinearElementEditor {
|
|||||||
const offsetY = points[0][1];
|
const offsetY = points[0][1];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
points: points.map((point, _idx) => {
|
points: points.map((point) => {
|
||||||
return [point[0] - offsetX, point[1] - offsetY] as const;
|
return [point[0] - offsetX, point[1] - offsetY] as const;
|
||||||
}),
|
}),
|
||||||
x: element.x + offsetX,
|
x: element.x + offsetX,
|
||||||
@@ -1028,13 +1109,15 @@ export class LinearElementEditor {
|
|||||||
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
static duplicateSelectedPoints(appState: AppState, elementsMap: ElementsMap) {
|
static duplicateSelectedPoints(
|
||||||
|
appState: AppState,
|
||||||
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
) {
|
||||||
if (!appState.editingLinearElement) {
|
if (!appState.editingLinearElement) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
const { selectedPointsIndices, elementId } = appState.editingLinearElement;
|
||||||
|
|
||||||
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
const element = LinearElementEditor.getElement(elementId, elementsMap);
|
||||||
|
|
||||||
if (!element || selectedPointsIndices === null) {
|
if (!element || selectedPointsIndices === null) {
|
||||||
@@ -1077,12 +1160,16 @@ export class LinearElementEditor {
|
|||||||
// potentially expanding the bounding box
|
// potentially expanding the bounding box
|
||||||
if (pointAddedToEnd) {
|
if (pointAddedToEnd) {
|
||||||
const lastPoint = element.points[element.points.length - 1];
|
const lastPoint = element.points[element.points.length - 1];
|
||||||
LinearElementEditor.movePoints(element, [
|
LinearElementEditor.movePoints(
|
||||||
{
|
element,
|
||||||
index: element.points.length - 1,
|
[
|
||||||
point: [lastPoint[0] + 30, lastPoint[1] + 30],
|
{
|
||||||
},
|
index: element.points.length - 1,
|
||||||
]);
|
point: [lastPoint[0] + 30, lastPoint[1] + 30],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -1099,6 +1186,7 @@ export class LinearElementEditor {
|
|||||||
static deletePoints(
|
static deletePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
pointIndices: readonly number[],
|
pointIndices: readonly number[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
) {
|
) {
|
||||||
let offsetX = 0;
|
let offsetX = 0;
|
||||||
let offsetY = 0;
|
let offsetY = 0;
|
||||||
@@ -1126,25 +1214,45 @@ export class LinearElementEditor {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
nextPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static addPoints(
|
static addPoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
|
||||||
targetPoints: { point: Point }[],
|
targetPoints: { point: Point }[],
|
||||||
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
) {
|
) {
|
||||||
const offsetX = 0;
|
const offsetX = 0;
|
||||||
const offsetY = 0;
|
const offsetY = 0;
|
||||||
|
|
||||||
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
|
||||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
LinearElementEditor._updatePoints(
|
||||||
|
element,
|
||||||
|
nextPoints,
|
||||||
|
offsetX,
|
||||||
|
offsetY,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static movePoints(
|
static movePoints(
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
|
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
|
||||||
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
otherUpdates?: {
|
||||||
|
startBinding?: PointBinding | null;
|
||||||
|
endBinding?: PointBinding | null;
|
||||||
|
},
|
||||||
|
options?: {
|
||||||
|
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||||
|
isDragging?: boolean;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const { points } = element;
|
const { points } = element;
|
||||||
|
|
||||||
@@ -1192,7 +1300,16 @@ export class LinearElementEditor {
|
|||||||
nextPoints,
|
nextPoints,
|
||||||
offsetX,
|
offsetX,
|
||||||
offsetY,
|
offsetY,
|
||||||
|
elementsMap,
|
||||||
otherUpdates,
|
otherUpdates,
|
||||||
|
{
|
||||||
|
isDragging: targetPoints.reduce(
|
||||||
|
(dragging, targetPoint): boolean =>
|
||||||
|
dragging || targetPoint.isDragging === true,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
changedElements: options?.changedElements,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1207,6 +1324,11 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Elbow arrows don't allow midpoints
|
||||||
|
if (element && isElbowArrow(element)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1266,7 +1388,7 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
pointerCoords.x,
|
pointerCoords.x,
|
||||||
pointerCoords.y,
|
pointerCoords.y,
|
||||||
snapToGrid ? appState.gridSize : null,
|
snapToGrid && !isElbowArrow(element) ? appState.gridSize : null,
|
||||||
);
|
);
|
||||||
const points = [
|
const points = [
|
||||||
...element.points.slice(0, segmentMidpoint.index!),
|
...element.points.slice(0, segmentMidpoint.index!),
|
||||||
@@ -1295,23 +1417,61 @@ export class LinearElementEditor {
|
|||||||
nextPoints: readonly Point[],
|
nextPoints: readonly Point[],
|
||||||
offsetX: number,
|
offsetX: number,
|
||||||
offsetY: number,
|
offsetY: number,
|
||||||
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
|
otherUpdates?: {
|
||||||
|
startBinding?: PointBinding | null;
|
||||||
|
endBinding?: PointBinding | null;
|
||||||
|
},
|
||||||
|
options?: {
|
||||||
|
changedElements?: Map<string, OrderedExcalidrawElement>;
|
||||||
|
isDragging?: boolean;
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
if (isElbowArrow(element)) {
|
||||||
const prevCoords = getElementPointsCoords(element, element.points);
|
const bindings: {
|
||||||
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
|
startBinding?: FixedPointBinding | null;
|
||||||
const nextCenterY = (nextCoords[1] + nextCoords[3]) / 2;
|
endBinding?: FixedPointBinding | null;
|
||||||
const prevCenterX = (prevCoords[0] + prevCoords[2]) / 2;
|
} = {};
|
||||||
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
|
if (otherUpdates?.startBinding !== undefined) {
|
||||||
const dX = prevCenterX - nextCenterX;
|
bindings.startBinding =
|
||||||
const dY = prevCenterY - nextCenterY;
|
otherUpdates.startBinding !== null &&
|
||||||
const rotated = rotate(offsetX, offsetY, dX, dY, element.angle);
|
isFixedPointBinding(otherUpdates.startBinding)
|
||||||
mutateElement(element, {
|
? otherUpdates.startBinding
|
||||||
...otherUpdates,
|
: null;
|
||||||
points: nextPoints,
|
}
|
||||||
x: element.x + rotated[0],
|
if (otherUpdates?.endBinding !== undefined) {
|
||||||
y: element.y + rotated[1],
|
bindings.endBinding =
|
||||||
});
|
otherUpdates.endBinding !== null &&
|
||||||
|
isFixedPointBinding(otherUpdates.endBinding)
|
||||||
|
? otherUpdates.endBinding
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElbowArrow(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
nextPoints,
|
||||||
|
[offsetX, offsetY],
|
||||||
|
bindings,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||||
|
const prevCoords = getElementPointsCoords(element, element.points);
|
||||||
|
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
|
||||||
|
const nextCenterY = (nextCoords[1] + nextCoords[3]) / 2;
|
||||||
|
const prevCenterX = (prevCoords[0] + prevCoords[2]) / 2;
|
||||||
|
const prevCenterY = (prevCoords[1] + prevCoords[3]) / 2;
|
||||||
|
const dX = prevCenterX - nextCenterX;
|
||||||
|
const dY = prevCenterY - nextCenterY;
|
||||||
|
const rotated = rotate(offsetX, offsetY, dX, dY, element.angle);
|
||||||
|
mutateElement(element, {
|
||||||
|
...otherUpdates,
|
||||||
|
points: nextPoints,
|
||||||
|
x: element.x + rotated[0],
|
||||||
|
y: element.y + rotated[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static _getShiftLockedDelta(
|
private static _getShiftLockedDelta(
|
||||||
@@ -1327,6 +1487,13 @@ export class LinearElementEditor {
|
|||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
return [
|
||||||
|
scenePointer[0] - referencePointCoords[0],
|
||||||
|
scenePointer[1] - referencePointCoords[1],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
scenePointer[0],
|
scenePointer[0],
|
||||||
scenePointer[1],
|
scenePointer[1],
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ describe("duplicating multiple elements", () => {
|
|||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,6 +132,7 @@ describe("duplicating multiple elements", () => {
|
|||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
boundElements: [{ id: "text2", type: "text" }],
|
boundElements: [{ id: "text2", type: "text" }],
|
||||||
});
|
});
|
||||||
@@ -247,6 +249,7 @@ describe("duplicating multiple elements", () => {
|
|||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -263,11 +266,13 @@ describe("duplicating multiple elements", () => {
|
|||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle-not-exists",
|
elementId: "rectangle-not-exists",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,11 +283,13 @@ describe("duplicating multiple elements", () => {
|
|||||||
elementId: "rectangle-not-exists",
|
elementId: "rectangle-not-exists",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
endBinding: {
|
endBinding: {
|
||||||
elementId: "rectangle1",
|
elementId: "rectangle1",
|
||||||
focus: 0.2,
|
focus: 0.2,
|
||||||
gap: 7,
|
gap: 7,
|
||||||
|
fixedPoint: [0.5, 1],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
ExcalidrawMagicFrameElement,
|
ExcalidrawMagicFrameElement,
|
||||||
ExcalidrawIframeElement,
|
ExcalidrawIframeElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
ExcalidrawArrowElement,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
arrayToMap,
|
arrayToMap,
|
||||||
@@ -388,8 +389,6 @@ export const newFreeDrawElement = (
|
|||||||
export const newLinearElement = (
|
export const newLinearElement = (
|
||||||
opts: {
|
opts: {
|
||||||
type: ExcalidrawLinearElement["type"];
|
type: ExcalidrawLinearElement["type"];
|
||||||
startArrowhead?: Arrowhead | null;
|
|
||||||
endArrowhead?: Arrowhead | null;
|
|
||||||
points?: ExcalidrawLinearElement["points"];
|
points?: ExcalidrawLinearElement["points"];
|
||||||
} & ElementConstructorOpts,
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawLinearElement> => {
|
): NonDeleted<ExcalidrawLinearElement> => {
|
||||||
@@ -399,8 +398,29 @@ export const newLinearElement = (
|
|||||||
lastCommittedPoint: null,
|
lastCommittedPoint: null,
|
||||||
startBinding: null,
|
startBinding: null,
|
||||||
endBinding: null,
|
endBinding: null,
|
||||||
|
startArrowhead: null,
|
||||||
|
endArrowhead: null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const newArrowElement = (
|
||||||
|
opts: {
|
||||||
|
type: ExcalidrawArrowElement["type"];
|
||||||
|
startArrowhead?: Arrowhead | null;
|
||||||
|
endArrowhead?: Arrowhead | null;
|
||||||
|
points?: ExcalidrawArrowElement["points"];
|
||||||
|
elbowed?: boolean;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
|
): NonDeleted<ExcalidrawArrowElement> => {
|
||||||
|
return {
|
||||||
|
..._newElementBase<ExcalidrawArrowElement>(opts.type, opts),
|
||||||
|
points: opts.points || [],
|
||||||
|
lastCommittedPoint: null,
|
||||||
|
startBinding: null,
|
||||||
|
endBinding: null,
|
||||||
startArrowhead: opts.startArrowhead || null,
|
startArrowhead: opts.startArrowhead || null,
|
||||||
endArrowhead: opts.endArrowhead || null,
|
endArrowhead: opts.endArrowhead || null,
|
||||||
|
elbowed: opts.elbowed || false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
ElementsMap,
|
ElementsMap,
|
||||||
|
NonDeletedSceneElementsMap,
|
||||||
|
SceneElementsMap,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import type { Mutable } from "../utility-types";
|
import type { Mutable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +24,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
isImageElement,
|
isImageElement,
|
||||||
@@ -30,7 +33,7 @@ import {
|
|||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import { updateBoundElements } from "./binding";
|
import { getArrowLocalFixedPoints, updateBoundElements } from "./binding";
|
||||||
import type {
|
import type {
|
||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
TransformHandleDirection,
|
TransformHandleDirection,
|
||||||
@@ -51,6 +54,7 @@ import {
|
|||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { isInGroup } from "../groups";
|
import { isInGroup } from "../groups";
|
||||||
|
import { mutateElbowArrow } from "./routing";
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle < 0) {
|
if (angle < 0) {
|
||||||
@@ -67,7 +71,7 @@ export const transformElements = (
|
|||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: SceneElementsMap,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
@@ -79,14 +83,16 @@ export const transformElements = (
|
|||||||
if (selectedElements.length === 1) {
|
if (selectedElements.length === 1) {
|
||||||
const [element] = selectedElements;
|
const [element] = selectedElements;
|
||||||
if (transformHandleType === "rotation") {
|
if (transformHandleType === "rotation") {
|
||||||
rotateSingleElement(
|
if (!isElbowArrow(element)) {
|
||||||
element,
|
rotateSingleElement(
|
||||||
elementsMap,
|
element,
|
||||||
pointerX,
|
elementsMap,
|
||||||
pointerY,
|
pointerX,
|
||||||
shouldRotateWithDiscreteAngle,
|
pointerY,
|
||||||
);
|
shouldRotateWithDiscreteAngle,
|
||||||
updateBoundElements(element, elementsMap);
|
);
|
||||||
|
updateBoundElements(element, elementsMap);
|
||||||
|
}
|
||||||
} else if (isTextElement(element) && transformHandleType) {
|
} else if (isTextElement(element) && transformHandleType) {
|
||||||
resizeSingleTextElement(
|
resizeSingleTextElement(
|
||||||
originalElements,
|
originalElements,
|
||||||
@@ -426,7 +432,7 @@ export const resizeSingleElement = (
|
|||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: SceneElementsMap,
|
||||||
transformHandleDirection: TransformHandleDirection,
|
transformHandleDirection: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
@@ -702,7 +708,10 @@ export const resizeSingleElement = (
|
|||||||
mutateElement(element, resizedElement);
|
mutateElement(element, resizedElement);
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
updateBoundElements(element, elementsMap, {
|
||||||
newSize: { width: resizedElement.width, height: resizedElement.height },
|
oldSize: {
|
||||||
|
width: stateAtResizeStart.width,
|
||||||
|
height: stateAtResizeStart.height,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (boundTextElement && boundTextFont != null) {
|
if (boundTextElement && boundTextFont != null) {
|
||||||
@@ -722,7 +731,7 @@ export const resizeSingleElement = (
|
|||||||
export const resizeMultipleElements = (
|
export const resizeMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: NonDeletedSceneElementsMap | SceneElementsMap,
|
||||||
transformHandleType: TransformHandleDirection,
|
transformHandleType: TransformHandleDirection,
|
||||||
shouldResizeFromCenter: boolean,
|
shouldResizeFromCenter: boolean,
|
||||||
shouldMaintainAspectRatio: boolean,
|
shouldMaintainAspectRatio: boolean,
|
||||||
@@ -955,13 +964,27 @@ export const resizeMultipleElements = (
|
|||||||
element,
|
element,
|
||||||
update: { boundTextFontSize, ...update },
|
update: { boundTextFontSize, ...update },
|
||||||
} of elementsAndUpdates) {
|
} of elementsAndUpdates) {
|
||||||
const { width, height, angle } = update;
|
const { angle } = update;
|
||||||
|
const { width: oldWidth, height: oldHeight } = element;
|
||||||
|
|
||||||
mutateElement(element, update, false);
|
mutateElement(element, update, false);
|
||||||
|
|
||||||
|
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||||
|
mutateElbowArrow(
|
||||||
|
element,
|
||||||
|
elementsMap,
|
||||||
|
element.points,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
informMutation: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
updateBoundElements(element, elementsMap, {
|
||||||
simultaneouslyUpdated: elementsToUpdate,
|
simultaneouslyUpdated: elementsToUpdate,
|
||||||
newSize: { width, height },
|
oldSize: { width: oldWidth, height: oldHeight },
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||||
@@ -984,7 +1007,7 @@ export const resizeMultipleElements = (
|
|||||||
const rotateMultipleElements = (
|
const rotateMultipleElements = (
|
||||||
originalElements: PointerDownState["originalElements"],
|
originalElements: PointerDownState["originalElements"],
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: SceneElementsMap,
|
||||||
pointerX: number,
|
pointerX: number,
|
||||||
pointerY: number,
|
pointerY: number,
|
||||||
shouldRotateWithDiscreteAngle: boolean,
|
shouldRotateWithDiscreteAngle: boolean,
|
||||||
@@ -1013,15 +1036,22 @@ const rotateMultipleElements = (
|
|||||||
centerY,
|
centerY,
|
||||||
centerAngle + origAngle - element.angle,
|
centerAngle + origAngle - element.angle,
|
||||||
);
|
);
|
||||||
mutateElement(
|
|
||||||
element,
|
if (isArrowElement(element) && isElbowArrow(element)) {
|
||||||
{
|
const points = getArrowLocalFixedPoints(element, elementsMap);
|
||||||
x: element.x + (rotatedCX - cx),
|
mutateElbowArrow(element, elementsMap, points);
|
||||||
y: element.y + (rotatedCY - cy),
|
} else {
|
||||||
angle: normalizeAngle(centerAngle + origAngle),
|
mutateElement(
|
||||||
},
|
element,
|
||||||
false,
|
{
|
||||||
);
|
x: element.x + (rotatedCX - cx),
|
||||||
|
y: element.y + (rotatedCY - cy),
|
||||||
|
angle: normalizeAngle(centerAngle + origAngle),
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
updateBoundElements(element, elementsMap, {
|
updateBoundElements(element, elementsMap, {
|
||||||
simultaneouslyUpdated: elements,
|
simultaneouslyUpdated: elements,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Scene from "../scene/Scene";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { Pointer, UI } from "../tests/helpers/ui";
|
||||||
|
import {
|
||||||
|
fireEvent,
|
||||||
|
GlobalTestState,
|
||||||
|
queryByTestId,
|
||||||
|
render,
|
||||||
|
} from "../tests/test-utils";
|
||||||
|
import { bindLinearElement } from "./binding";
|
||||||
|
import { Excalidraw } from "../index";
|
||||||
|
import { mutateElbowArrow } from "./routing";
|
||||||
|
import type {
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawBindableElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
} from "./types";
|
||||||
|
import { ARROW_TYPE } from "../constants";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
|
const getStatsProperty = (label: string) => {
|
||||||
|
const elementStats = UI.queryStats()?.querySelector("#elementStats");
|
||||||
|
|
||||||
|
if (elementStats) {
|
||||||
|
const properties = elementStats?.querySelector(".statsItem");
|
||||||
|
return (
|
||||||
|
properties?.querySelector?.(
|
||||||
|
`.drag-input-container[data-testid="${label}"]`,
|
||||||
|
) || null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("elbow arrow routing", () => {
|
||||||
|
it("can properly generate orthogonal arrow points", () => {
|
||||||
|
const scene = new Scene();
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
elbowed: true,
|
||||||
|
}) as ExcalidrawElbowArrowElement;
|
||||||
|
scene.insertElement(arrow);
|
||||||
|
mutateElbowArrow(arrow, scene.getNonDeletedElementsMap(), [
|
||||||
|
[-45 - arrow.x, -100.1 - arrow.y],
|
||||||
|
[45 - arrow.x, 99.9 - arrow.y],
|
||||||
|
]);
|
||||||
|
expect(arrow.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[0, 100],
|
||||||
|
[90, 100],
|
||||||
|
[90, 200],
|
||||||
|
]);
|
||||||
|
expect(arrow.x).toEqual(-45);
|
||||||
|
expect(arrow.y).toEqual(-100.1);
|
||||||
|
expect(arrow.width).toEqual(90);
|
||||||
|
expect(arrow.height).toEqual(200);
|
||||||
|
});
|
||||||
|
it("can generate proper points for bound elbow arrow", () => {
|
||||||
|
const scene = new Scene();
|
||||||
|
const rectangle1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: -150,
|
||||||
|
y: -150,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
}) as ExcalidrawBindableElement;
|
||||||
|
const rectangle2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
}) as ExcalidrawBindableElement;
|
||||||
|
const arrow = API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
elbowed: true,
|
||||||
|
x: -45,
|
||||||
|
y: -100.1,
|
||||||
|
width: 90,
|
||||||
|
height: 200,
|
||||||
|
points: [
|
||||||
|
[0, 0],
|
||||||
|
[90, 200],
|
||||||
|
],
|
||||||
|
}) as ExcalidrawElbowArrowElement;
|
||||||
|
scene.insertElement(rectangle1);
|
||||||
|
scene.insertElement(rectangle2);
|
||||||
|
scene.insertElement(arrow);
|
||||||
|
const elementsMap = scene.getNonDeletedElementsMap();
|
||||||
|
bindLinearElement(arrow, rectangle1, "start", elementsMap);
|
||||||
|
bindLinearElement(arrow, rectangle2, "end", elementsMap);
|
||||||
|
|
||||||
|
expect(arrow.startBinding).not.toBe(null);
|
||||||
|
expect(arrow.endBinding).not.toBe(null);
|
||||||
|
|
||||||
|
mutateElbowArrow(arrow, elementsMap, [
|
||||||
|
[0, 0],
|
||||||
|
[90, 200],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(arrow.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[45, 0],
|
||||||
|
[45, 200],
|
||||||
|
[90, 200],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("elbow arrow ui", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can follow bound shapes", async () => {
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: -150,
|
||||||
|
y: -150,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
|
expect(h.state.currentItemArrowType).toBe(ARROW_TYPE.elbow);
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-43, -99);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(43, 99);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
expect(arrow.type).toBe("arrow");
|
||||||
|
expect(arrow.elbowed).toBe(true);
|
||||||
|
expect(arrow.points).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[35, 0],
|
||||||
|
[35, 200],
|
||||||
|
[90, 200],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("can follow bound rotated shapes", async () => {
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: -150,
|
||||||
|
y: -150,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
UI.createElement("rectangle", {
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
UI.clickTool("arrow");
|
||||||
|
UI.clickOnTestId("elbow-arrow");
|
||||||
|
|
||||||
|
mouse.reset();
|
||||||
|
mouse.moveTo(-43, -99);
|
||||||
|
mouse.click();
|
||||||
|
mouse.moveTo(43, 99);
|
||||||
|
mouse.click();
|
||||||
|
|
||||||
|
const arrow = h.scene.getSelectedElements(
|
||||||
|
h.state,
|
||||||
|
)[0] as ExcalidrawArrowElement;
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 1,
|
||||||
|
clientY: 1,
|
||||||
|
});
|
||||||
|
const contextMenu = UI.queryContextMenu();
|
||||||
|
fireEvent.click(queryByTestId(contextMenu!, "stats")!);
|
||||||
|
|
||||||
|
mouse.click(51, 51);
|
||||||
|
|
||||||
|
const inputAngle = getStatsProperty("A")?.querySelector(
|
||||||
|
".drag-input",
|
||||||
|
) as HTMLInputElement;
|
||||||
|
UI.updateInput(inputAngle, String("40"));
|
||||||
|
|
||||||
|
expect(arrow.points.map((point) => point.map(Math.round))).toEqual([
|
||||||
|
[0, 0],
|
||||||
|
[35, 0],
|
||||||
|
[35, 90],
|
||||||
|
[25, 90],
|
||||||
|
[25, 165],
|
||||||
|
[103, 165],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
||||||
@@ -16,7 +17,6 @@ import type {
|
|||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
|
||||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ describe("textWysiwyg", () => {
|
|||||||
const { h } = window;
|
const { h } = window;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should prefer editing selected text element (non-bindable container present)", async () => {
|
it("should prefer editing selected text element (non-bindable container present)", async () => {
|
||||||
@@ -55,7 +55,7 @@ describe("textWysiwyg", () => {
|
|||||||
width: textSize,
|
width: textSize,
|
||||||
height: textSize,
|
height: textSize,
|
||||||
});
|
});
|
||||||
h.elements = [text, line];
|
API.setElements([text, line]);
|
||||||
|
|
||||||
API.setSelectedElements([text]);
|
API.setSelectedElements([text]);
|
||||||
|
|
||||||
@@ -95,9 +95,9 @@ describe("textWysiwyg", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [container, boundText, boundText2];
|
API.setElements([container, boundText, boundText2]);
|
||||||
|
|
||||||
mutateElement(container, {
|
API.updateElement(container, {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,11 +123,11 @@ describe("textWysiwyg", () => {
|
|||||||
height: textSize,
|
height: textSize,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
mutateElement(container, {
|
API.updateElement(container, {
|
||||||
boundElements: [{ type: "text", id: text.id }],
|
boundElements: [{ type: "text", id: text.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [container, text];
|
API.setElements([container, text]);
|
||||||
|
|
||||||
API.setSelectedElements([container]);
|
API.setSelectedElements([container]);
|
||||||
|
|
||||||
@@ -164,9 +164,9 @@ describe("textWysiwyg", () => {
|
|||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [container, boundText, boundText2];
|
API.setElements([container, boundText, boundText2]);
|
||||||
|
|
||||||
mutateElement(container, {
|
API.updateElement(container, {
|
||||||
boundElements: [{ type: "text", id: boundText.id }],
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ describe("textWysiwyg", () => {
|
|||||||
height: 100,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [text];
|
API.setElements([text]);
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
@@ -209,7 +209,7 @@ describe("textWysiwyg", () => {
|
|||||||
height: 100,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [text];
|
API.setElements([text]);
|
||||||
UI.clickTool("selection");
|
UI.clickTool("selection");
|
||||||
|
|
||||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
@@ -251,7 +251,7 @@ describe("textWysiwyg", () => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
h.app.refreshEditorBreakpoints();
|
h.app.refreshEditorBreakpoints();
|
||||||
|
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -264,7 +264,7 @@ describe("textWysiwyg", () => {
|
|||||||
text: "Excalidraw\nEditor",
|
text: "Excalidraw\nEditor",
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [text];
|
API.setElements([text]);
|
||||||
|
|
||||||
const prevWidth = text.width;
|
const prevWidth = text.width;
|
||||||
const prevHeight = text.height;
|
const prevHeight = text.height;
|
||||||
@@ -291,18 +291,15 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const nextText = `${wrappedText} is great!`;
|
const nextText = `${wrappedText} is great!`;
|
||||||
updateTextEditor(editor, nextText);
|
updateTextEditor(editor, nextText);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
expect(h.elements[0].width).toEqual(wrappedWidth);
|
expect(h.elements[0].width).toEqual(wrappedWidth);
|
||||||
expect(h.elements[0].height).toBeGreaterThan(wrappedHeight);
|
expect(h.elements[0].height).toBeGreaterThan(wrappedHeight);
|
||||||
|
|
||||||
// remove all texts and then add it back editing
|
// remove all texts and then add it back editing
|
||||||
updateTextEditor(editor, "");
|
updateTextEditor(editor, "");
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
|
||||||
updateTextEditor(editor, nextText);
|
updateTextEditor(editor, nextText);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
expect(h.elements[0].width).toEqual(wrappedWidth);
|
expect(h.elements[0].width).toEqual(wrappedWidth);
|
||||||
});
|
});
|
||||||
@@ -313,7 +310,7 @@ describe("textWysiwyg", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
text: originalText,
|
text: originalText,
|
||||||
});
|
});
|
||||||
h.elements = [text];
|
API.setElements([text]);
|
||||||
|
|
||||||
// wrap
|
// wrap
|
||||||
UI.resize(text, "e", [-40, 0]);
|
UI.resize(text, "e", [-40, 0]);
|
||||||
@@ -321,7 +318,7 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("selection");
|
UI.clickTool("selection");
|
||||||
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
const editor = await getTextEditor(textEditorSelector);
|
const editor = await getTextEditor(textEditorSelector);
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
// restore after unwrapping
|
// restore after unwrapping
|
||||||
UI.resize(text, "e", [40, 0]);
|
UI.resize(text, "e", [40, 0]);
|
||||||
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
|
expect((h.elements[0] as ExcalidrawTextElement).text).toBe(originalText);
|
||||||
@@ -332,14 +329,12 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("selection");
|
UI.clickTool("selection");
|
||||||
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
updateTextEditor(editor, `${wrappedText}\nA new line!`);
|
updateTextEditor(editor, `${wrappedText}\nA new line!`);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
// remove the newly added line
|
// remove the newly added line
|
||||||
UI.clickTool("selection");
|
UI.clickTool("selection");
|
||||||
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2);
|
||||||
updateTextEditor(editor, wrappedText);
|
updateTextEditor(editor, wrappedText);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
// unwrap
|
// unwrap
|
||||||
UI.resize(text, "e", [30, 0]);
|
UI.resize(text, "e", [30, 0]);
|
||||||
// expect the text to be restored the same
|
// expect the text to be restored the same
|
||||||
@@ -376,12 +371,11 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add a tab at the start of the first line", () => {
|
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";
|
textarea.value = "Line#1\nLine#2";
|
||||||
// cursor: "|Line#1\nLine#2"
|
// cursor: "|Line#1\nLine#2"
|
||||||
textarea.selectionStart = 0;
|
textarea.selectionStart = 0;
|
||||||
textarea.selectionEnd = 0;
|
textarea.selectionEnd = 0;
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, { key: KEYS.TAB });
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
|
||||||
// cursor: " |Line#1\nLine#2"
|
// cursor: " |Line#1\nLine#2"
|
||||||
@@ -390,13 +384,12 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add a tab at the start of the second line", () => {
|
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";
|
textarea.value = "Line#1\nLine#2";
|
||||||
// cursor: "Line#1\nLin|e#2"
|
// cursor: "Line#1\nLin|e#2"
|
||||||
textarea.selectionStart = 10;
|
textarea.selectionStart = 10;
|
||||||
textarea.selectionEnd = 10;
|
textarea.selectionEnd = 10;
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, { key: KEYS.TAB });
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
|
||||||
|
|
||||||
@@ -406,13 +399,12 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add a tab at the start of the first and second line", () => {
|
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";
|
textarea.value = "Line#1\nLine#2\nLine#3";
|
||||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
textarea.selectionStart = 2;
|
textarea.selectionStart = 2;
|
||||||
textarea.selectionEnd = 9;
|
textarea.selectionEnd = 9;
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, { key: KEYS.TAB });
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
|
||||||
|
|
||||||
@@ -422,16 +414,15 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should remove a tab at the start of the first line", () => {
|
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`;
|
textarea.value = `${tab}Line#1\nLine#2`;
|
||||||
// cursor: "| Line#1\nLine#2"
|
// cursor: "| Line#1\nLine#2"
|
||||||
textarea.selectionStart = 0;
|
textarea.selectionStart = 0;
|
||||||
textarea.selectionEnd = 0;
|
textarea.selectionEnd = 0;
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
|
|
||||||
@@ -441,16 +432,15 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should remove a tab at the start of the second line", () => {
|
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"
|
// cursor: "Line#1\n Lin|e#2"
|
||||||
textarea.value = `Line#1\n${tab}Line#2`;
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
textarea.selectionStart = 15;
|
textarea.selectionStart = 15;
|
||||||
textarea.selectionEnd = 15;
|
textarea.selectionEnd = 15;
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
// cursor: "Line#1\nLin|e#2"
|
// cursor: "Line#1\nLin|e#2"
|
||||||
@@ -459,16 +449,15 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should remove a tab at the start of the first and second line", () => {
|
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"
|
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
|
||||||
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
|
||||||
textarea.selectionStart = 6;
|
textarea.selectionStart = 6;
|
||||||
textarea.selectionEnd = 17;
|
textarea.selectionEnd = 17;
|
||||||
|
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
|
||||||
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
|
||||||
@@ -477,45 +466,41 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
|
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"
|
// cursor: "Line#1\n | Line#2"
|
||||||
textarea.value = `Line#1\n${tab}Line#2`;
|
textarea.value = `Line#1\n${tab}Line#2`;
|
||||||
textarea.selectionStart = 9;
|
textarea.selectionStart = 9;
|
||||||
textarea.selectionEnd = 9;
|
textarea.selectionEnd = 9;
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, {
|
||||||
|
|
||||||
// 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,
|
key: KEYS.TAB,
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// cursor: "Line#1\n|Line#2"
|
||||||
|
expect(textarea.selectionStart).toEqual(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove partial tabs", () => {
|
||||||
// cursor: "Line#1\n Line#|2"
|
// cursor: "Line#1\n Line#|2"
|
||||||
textarea.value = `Line#1\n Line#2`;
|
textarea.value = `Line#1\n Line#2`;
|
||||||
textarea.selectionStart = 15;
|
textarea.selectionStart = 15;
|
||||||
textarea.selectionEnd = 15;
|
textarea.selectionEnd = 15;
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove nothing", () => {
|
it("should remove nothing", () => {
|
||||||
const event = new KeyboardEvent("keydown", {
|
|
||||||
key: KEYS.TAB,
|
|
||||||
shiftKey: true,
|
|
||||||
});
|
|
||||||
// cursor: "Line#1\n Li|ne#2"
|
// cursor: "Line#1\n Li|ne#2"
|
||||||
textarea.value = `Line#1\nLine#2`;
|
textarea.value = `Line#1\nLine#2`;
|
||||||
textarea.selectionStart = 9;
|
textarea.selectionStart = 9;
|
||||||
textarea.selectionEnd = 9;
|
textarea.selectionEnd = 9;
|
||||||
textarea.dispatchEvent(event);
|
fireEvent.keyDown(textarea, {
|
||||||
|
key: KEYS.TAB,
|
||||||
|
shiftKey: true,
|
||||||
|
});
|
||||||
|
|
||||||
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
expect(textarea.value).toEqual(`Line#1\nLine#2`);
|
||||||
});
|
});
|
||||||
@@ -523,54 +508,42 @@ describe("textWysiwyg", () => {
|
|||||||
it("should resize text via shortcuts while in wysiwyg", () => {
|
it("should resize text via shortcuts while in wysiwyg", () => {
|
||||||
textarea.value = "abc def";
|
textarea.value = "abc def";
|
||||||
const origFontSize = textElement.fontSize;
|
const origFontSize = textElement.fontSize;
|
||||||
textarea.dispatchEvent(
|
fireEvent.keyDown(textarea, {
|
||||||
new KeyboardEvent("keydown", {
|
key: KEYS.CHEVRON_RIGHT,
|
||||||
key: KEYS.CHEVRON_RIGHT,
|
ctrlKey: true,
|
||||||
ctrlKey: true,
|
shiftKey: true,
|
||||||
shiftKey: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(textElement.fontSize).toBe(origFontSize * 1.1);
|
expect(textElement.fontSize).toBe(origFontSize * 1.1);
|
||||||
|
|
||||||
textarea.dispatchEvent(
|
fireEvent.keyDown(textarea, {
|
||||||
new KeyboardEvent("keydown", {
|
key: KEYS.CHEVRON_LEFT,
|
||||||
key: KEYS.CHEVRON_LEFT,
|
ctrlKey: true,
|
||||||
ctrlKey: true,
|
shiftKey: true,
|
||||||
shiftKey: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(textElement.fontSize).toBe(origFontSize);
|
expect(textElement.fontSize).toBe(origFontSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("zooming via keyboard should zoom canvas", () => {
|
it("zooming via keyboard should zoom canvas", () => {
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
textarea.dispatchEvent(
|
fireEvent.keyDown(textarea, {
|
||||||
new KeyboardEvent("keydown", {
|
code: CODES.MINUS,
|
||||||
code: CODES.MINUS,
|
ctrlKey: true,
|
||||||
ctrlKey: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(h.state.zoom.value).toBe(0.9);
|
expect(h.state.zoom.value).toBe(0.9);
|
||||||
textarea.dispatchEvent(
|
fireEvent.keyDown(textarea, {
|
||||||
new KeyboardEvent("keydown", {
|
code: CODES.NUM_SUBTRACT,
|
||||||
code: CODES.NUM_SUBTRACT,
|
ctrlKey: true,
|
||||||
ctrlKey: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(h.state.zoom.value).toBe(0.8);
|
expect(h.state.zoom.value).toBe(0.8);
|
||||||
textarea.dispatchEvent(
|
fireEvent.keyDown(textarea, {
|
||||||
new KeyboardEvent("keydown", {
|
code: CODES.NUM_ADD,
|
||||||
code: CODES.NUM_ADD,
|
ctrlKey: true,
|
||||||
ctrlKey: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(h.state.zoom.value).toBe(0.9);
|
expect(h.state.zoom.value).toBe(0.9);
|
||||||
textarea.dispatchEvent(
|
fireEvent.keyDown(textarea, {
|
||||||
new KeyboardEvent("keydown", {
|
code: CODES.EQUAL,
|
||||||
code: CODES.EQUAL,
|
ctrlKey: true,
|
||||||
ctrlKey: true,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,8 +556,8 @@ describe("textWysiwyg", () => {
|
|||||||
textarea,
|
textarea,
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
);
|
);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
Keyboard.exitTextEditor(textarea);
|
||||||
textarea.blur();
|
|
||||||
expect(textarea.style.width).toBe("792px");
|
expect(textarea.style.width).toBe("792px");
|
||||||
expect(h.elements[0].width).toBe(1000);
|
expect(h.elements[0].width).toBe(1000);
|
||||||
});
|
});
|
||||||
@@ -596,7 +569,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
|
|
||||||
rectangle = UI.createElement("rectangle", {
|
rectangle = UI.createElement("rectangle", {
|
||||||
x: 10,
|
x: 10,
|
||||||
@@ -615,7 +588,7 @@ describe("textWysiwyg", () => {
|
|||||||
height: 75,
|
height: 75,
|
||||||
backgroundColor: "red",
|
backgroundColor: "red",
|
||||||
});
|
});
|
||||||
h.elements = [rectangle];
|
API.setElements([rectangle]);
|
||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
@@ -634,8 +607,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -648,7 +620,7 @@ describe("textWysiwyg", () => {
|
|||||||
height: 75,
|
height: 75,
|
||||||
angle: 45,
|
angle: 45,
|
||||||
});
|
});
|
||||||
h.elements = [rectangle];
|
API.setElements([rectangle]);
|
||||||
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
@@ -662,8 +634,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -677,7 +648,7 @@ describe("textWysiwyg", () => {
|
|||||||
width: 90,
|
width: 90,
|
||||||
height: 75,
|
height: 75,
|
||||||
});
|
});
|
||||||
h.elements = [diamond];
|
API.setElements([diamond]);
|
||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements[0].id).toBe(diamond.id);
|
expect(h.elements[0].id).toBe(diamond.id);
|
||||||
@@ -687,7 +658,6 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
const value = new Array(1000).fill("1").join("\n");
|
const value = new Array(1000).fill("1").join("\n");
|
||||||
|
|
||||||
// Pasting large text to simulate height increase
|
// Pasting large text to simulate height increase
|
||||||
@@ -712,7 +682,7 @@ describe("textWysiwyg", () => {
|
|||||||
height: 75,
|
height: 75,
|
||||||
backgroundColor: "transparent",
|
backgroundColor: "transparent",
|
||||||
});
|
});
|
||||||
h.elements = [rectangle];
|
API.setElements([rectangle]);
|
||||||
|
|
||||||
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
@@ -721,8 +691,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
let editor = await getTextEditor(textEditorSelector, true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
mouse.doubleClickAt(
|
mouse.doubleClickAt(
|
||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
@@ -738,8 +707,7 @@ describe("textWysiwyg", () => {
|
|||||||
editor = await getTextEditor(textEditorSelector, true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -759,10 +727,8 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -777,7 +743,7 @@ describe("textWysiwyg", () => {
|
|||||||
height: 75,
|
height: 75,
|
||||||
strokeWidth: 4,
|
strokeWidth: 4,
|
||||||
});
|
});
|
||||||
h.elements = [rectangle];
|
API.setElements([rectangle]);
|
||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
@@ -795,8 +761,7 @@ describe("textWysiwyg", () => {
|
|||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -808,7 +773,7 @@ describe("textWysiwyg", () => {
|
|||||||
width: 100,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
});
|
});
|
||||||
h.elements = [freedraw];
|
API.setElements([freedraw]);
|
||||||
|
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
|
|
||||||
@@ -819,7 +784,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
expect(freedraw.boundElements).toBe(null);
|
expect(freedraw.boundElements).toBe(null);
|
||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
@@ -828,7 +793,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
["freedraw", "line"].forEach((type: any) => {
|
["freedraw", "line"].forEach((type: any) => {
|
||||||
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
||||||
h.elements = [];
|
API.setElements([]);
|
||||||
const element = UI.createElement(type, {
|
const element = UI.createElement(type, {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 50,
|
height: 50,
|
||||||
@@ -855,8 +820,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.boundElements).toBe(null);
|
expect(rectangle.boundElements).toBe(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -872,7 +836,6 @@ describe("textWysiwyg", () => {
|
|||||||
editor,
|
editor,
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
);
|
);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
|
|
||||||
@@ -908,14 +871,18 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
mouse.down();
|
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
|
||||||
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
|
expect(await getTextEditor(textEditorSelector, false)).toBe(null);
|
||||||
|
|
||||||
|
expect(h.state.editingElement).toBe(null);
|
||||||
|
|
||||||
expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
|
expect(text.fontFamily).toEqual(FONT_FAMILY.Excalifont);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
@@ -950,8 +917,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.text).toBe("Hello \nWorld!");
|
expect(text.text).toBe("Hello \nWorld!");
|
||||||
expect(text.originalText).toBe("Hello World!");
|
expect(text.originalText).toBe("Hello World!");
|
||||||
@@ -970,9 +936,7 @@ describe("textWysiwyg", () => {
|
|||||||
editor = await getTextEditor(textEditorSelector, true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
editor.blur();
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
|
||||||
expect(text.text).toBe("Hello");
|
expect(text.text).toBe("Hello");
|
||||||
@@ -998,10 +962,8 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -1034,9 +996,8 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -1055,9 +1016,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
let editor = await getTextEditor(textEditorSelector, true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
// should center align horizontally and vertically by default
|
// should center align horizontally and vertically by default
|
||||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
@@ -1076,12 +1036,8 @@ describe("textWysiwyg", () => {
|
|||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
// should left align horizontally and bottom vertically after resize
|
// should left align horizontally and bottom vertically after resize
|
||||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
@@ -1101,9 +1057,7 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Right"));
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
editor.blur();
|
|
||||||
|
|
||||||
// should right align horizontally and top vertically after resize
|
// should right align horizontally and top vertically after resize
|
||||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
@@ -1136,8 +1090,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
expect(rectangle2.boundElements).toBeNull();
|
expect(rectangle2.boundElements).toBeNull();
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -1148,9 +1101,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(rectangle.width).toBe(90);
|
expect(rectangle.width).toBe(90);
|
||||||
expect(rectangle.height).toBe(75);
|
expect(rectangle.height).toBe(75);
|
||||||
@@ -1168,9 +1120,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@@ -1200,9 +1151,8 @@ describe("textWysiwyg", () => {
|
|||||||
it("undo should work", async () => {
|
it("undo should work", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: h.elements[1].id, type: "text" },
|
{ id: h.elements[1].id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -1237,10 +1187,9 @@ describe("textWysiwyg", () => {
|
|||||||
it("should not allow bound text with only whitespaces", async () => {
|
it("should not allow bound text with only whitespaces", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([]);
|
expect(rectangle.boundElements).toStrictEqual([]);
|
||||||
expect(h.elements[1].isDeleted).toBe(true);
|
expect(h.elements[1].isDeleted).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -1259,7 +1208,7 @@ describe("textWysiwyg", () => {
|
|||||||
text: "Online whiteboard collaboration made easy",
|
text: "Online whiteboard collaboration made easy",
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [container, text];
|
API.setElements([container, text]);
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -1292,9 +1241,8 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
let editor = await getTextEditor(textEditorSelector, true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||||
@@ -1305,8 +1253,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
editor = await getTextEditor(textEditorSelector, true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
Keyboard.exitTextEditor(editor);
|
||||||
editor.blur();
|
|
||||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||||
// cache updated again
|
// cache updated again
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
|
||||||
@@ -1321,7 +1268,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
@@ -1346,7 +1293,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector, true);
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
|
||||||
).toEqual(1.25);
|
).toEqual(1.25);
|
||||||
@@ -1378,7 +1325,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = await getTextEditor(textEditorSelector, true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = await getTextEditor(textEditorSelector, true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
@@ -1498,13 +1445,11 @@ describe("textWysiwyg", () => {
|
|||||||
editor,
|
editor,
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard",
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
);
|
);
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(textElement.width).toBe(600);
|
expect(textElement.width).toBe(600);
|
||||||
@@ -1581,16 +1526,14 @@ describe("textWysiwyg", () => {
|
|||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
let editor = await getTextEditor(textEditorSelector, true);
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Hello!");
|
updateTextEditor(editor, "Hello!");
|
||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign,
|
||||||
).toBe(VERTICAL_ALIGN.MIDDLE);
|
).toBe(VERTICAL_ALIGN.MIDDLE);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
|
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
@@ -1606,9 +1549,8 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
editor = await getTextEditor(textEditorSelector, true);
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
|
||||||
updateTextEditor(editor, "Excalidraw");
|
updateTextEditor(editor, "Excalidraw");
|
||||||
editor.blur();
|
Keyboard.exitTextEditor(editor);
|
||||||
|
|
||||||
expect(h.elements.length).toBe(3);
|
expect(h.elements.length).toBe(3);
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ import type { Bounds } from "./bounds";
|
|||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
import type { Device, InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
import {
|
||||||
|
isElbowArrow,
|
||||||
|
isFrameLikeElement,
|
||||||
|
isLinearElement,
|
||||||
|
} from "./typeChecks";
|
||||||
import {
|
import {
|
||||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||||
isAndroid,
|
isAndroid,
|
||||||
@@ -262,7 +266,11 @@ export const getTransformHandles = (
|
|||||||
// so that when locked element is selected (especially when you toggle lock
|
// so that when locked element is selected (especially when you toggle lock
|
||||||
// via keyboard) the locked element is visually distinct, indicating
|
// via keyboard) the locked element is visually distinct, indicating
|
||||||
// you can't move/resize
|
// you can't move/resize
|
||||||
if (element.locked) {
|
if (
|
||||||
|
element.locked ||
|
||||||
|
// Elbow arrows cannot be rotated
|
||||||
|
isElbowArrow(element)
|
||||||
|
) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +320,10 @@ export const shouldShowBoundingBox = (
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const element = elements[0];
|
const element = elements[0];
|
||||||
|
if (isElbowArrow(element)) {
|
||||||
|
// Elbow arrows cannot be resized as single selected elements
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!isLinearElement(element)) {
|
if (!isLinearElement(element)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import type {
|
|||||||
ExcalidrawIframeLikeElement,
|
ExcalidrawIframeLikeElement,
|
||||||
ExcalidrawMagicFrameElement,
|
ExcalidrawMagicFrameElement,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
|
ExcalidrawElbowArrowElement,
|
||||||
|
PointBinding,
|
||||||
|
FixedPointBinding,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const isInitializedImageElement = (
|
export const isInitializedImageElement = (
|
||||||
@@ -106,6 +109,12 @@ export const isArrowElement = (
|
|||||||
return element != null && element.type === "arrow";
|
return element != null && element.type === "arrow";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isElbowArrow = (
|
||||||
|
element?: ExcalidrawElement,
|
||||||
|
): element is ExcalidrawElbowArrowElement => {
|
||||||
|
return isArrowElement(element) && element.elbowed;
|
||||||
|
};
|
||||||
|
|
||||||
export const isLinearElementType = (
|
export const isLinearElementType = (
|
||||||
elementType: ElementOrToolType,
|
elementType: ElementOrToolType,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
@@ -150,6 +159,22 @@ export const isBindableElement = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isRectanguloidElement = (
|
||||||
|
element?: ExcalidrawElement | null,
|
||||||
|
): element is ExcalidrawBindableElement => {
|
||||||
|
return (
|
||||||
|
element != null &&
|
||||||
|
(element.type === "rectangle" ||
|
||||||
|
element.type === "diamond" ||
|
||||||
|
element.type === "image" ||
|
||||||
|
element.type === "iframe" ||
|
||||||
|
element.type === "embeddable" ||
|
||||||
|
element.type === "frame" ||
|
||||||
|
element.type === "magicframe" ||
|
||||||
|
(element.type === "text" && !element.containerId))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const isTextBindableContainer = (
|
export const isTextBindableContainer = (
|
||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
includeLocked = true,
|
includeLocked = true,
|
||||||
@@ -263,3 +288,9 @@ export const getDefaultRoundnessTypeForElement = (
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFixedPointBinding = (
|
||||||
|
binding: PointBinding,
|
||||||
|
): binding is FixedPointBinding => {
|
||||||
|
return binding.fixedPoint != null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ import type {
|
|||||||
THEME,
|
THEME,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import type { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types";
|
import type {
|
||||||
|
MakeBrand,
|
||||||
|
MarkNonNullable,
|
||||||
|
Merge,
|
||||||
|
ValueOf,
|
||||||
|
} from "../utility-types";
|
||||||
import type { MagicCacheData } from "../data/magic";
|
import type { MagicCacheData } from "../data/magic";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
@@ -171,6 +176,11 @@ export type ExcalidrawElement =
|
|||||||
| ExcalidrawIframeElement
|
| ExcalidrawIframeElement
|
||||||
| ExcalidrawEmbeddableElement;
|
| ExcalidrawEmbeddableElement;
|
||||||
|
|
||||||
|
export type ExcalidrawNonSelectionElement = Exclude<
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawSelectionElement
|
||||||
|
>;
|
||||||
|
|
||||||
export type Ordered<TElement extends ExcalidrawElement> = TElement & {
|
export type Ordered<TElement extends ExcalidrawElement> = TElement & {
|
||||||
index: FractionalIndex;
|
index: FractionalIndex;
|
||||||
};
|
};
|
||||||
@@ -228,12 +238,22 @@ export type ExcalidrawTextElementWithContainer = {
|
|||||||
containerId: ExcalidrawTextContainer["id"];
|
containerId: ExcalidrawTextContainer["id"];
|
||||||
} & ExcalidrawTextElement;
|
} & ExcalidrawTextElement;
|
||||||
|
|
||||||
|
export type FixedPoint = [number, number];
|
||||||
|
|
||||||
export type PointBinding = {
|
export type PointBinding = {
|
||||||
elementId: ExcalidrawBindableElement["id"];
|
elementId: ExcalidrawBindableElement["id"];
|
||||||
focus: number;
|
focus: number;
|
||||||
gap: number;
|
gap: number;
|
||||||
|
// Represents the fixed point binding information in form of a vertical and
|
||||||
|
// horizontal ratio (i.e. a percentage value in the 0.0-1.0 range). This ratio
|
||||||
|
// gives the user selected fixed point by multiplying the bound element width
|
||||||
|
// with fixedPoint[0] and the bound element height with fixedPoint[1] to get the
|
||||||
|
// bound element-local point coordinate.
|
||||||
|
fixedPoint: FixedPoint | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FixedPointBinding = Merge<PointBinding, { fixedPoint: FixedPoint }>;
|
||||||
|
|
||||||
export type Arrowhead =
|
export type Arrowhead =
|
||||||
| "arrow"
|
| "arrow"
|
||||||
| "bar"
|
| "bar"
|
||||||
@@ -259,8 +279,18 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
|||||||
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
export type ExcalidrawArrowElement = ExcalidrawLinearElement &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "arrow";
|
type: "arrow";
|
||||||
|
elbowed: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
export type ExcalidrawElbowArrowElement = Merge<
|
||||||
|
ExcalidrawArrowElement,
|
||||||
|
{
|
||||||
|
elbowed: true;
|
||||||
|
startBinding: FixedPointBinding | null;
|
||||||
|
endBinding: FixedPointBinding | null;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
||||||
Readonly<{
|
Readonly<{
|
||||||
type: "freedraw";
|
type: "freedraw";
|
||||||
|
|||||||
@@ -1,41 +1,32 @@
|
|||||||
import { stringToBase64, toByteString } from "../data/encode";
|
import { LOCAL_FONT_PROTOCOL } from "./metadata";
|
||||||
|
|
||||||
export interface Font {
|
export interface Font {
|
||||||
url: URL;
|
urls: URL[];
|
||||||
fontFace: FontFace;
|
fontFace: FontFace;
|
||||||
getContent(): Promise<string>;
|
getContent(codePoints: ReadonlySet<number>): Promise<string>;
|
||||||
}
|
}
|
||||||
export const UNPKG_PROD_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}/dist/prod/`;
|
? `${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)
|
||||||
|
}/dist/prod/`;
|
||||||
|
|
||||||
|
/** caches for lazy loaded chunks, reused across concurrent calls and separate editor instances */
|
||||||
|
let fontEditorCache: Promise<typeof import("fonteditor-core")> | null = null;
|
||||||
|
let brotliCache: Promise<typeof import("fonteditor-core").woff2> | null = null;
|
||||||
|
|
||||||
export class ExcalidrawFont implements Font {
|
export class ExcalidrawFont implements Font {
|
||||||
public readonly url: URL;
|
public readonly urls: URL[];
|
||||||
public readonly fontFace: FontFace;
|
public readonly fontFace: FontFace;
|
||||||
|
|
||||||
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
constructor(family: string, uri: string, descriptors?: FontFaceDescriptors) {
|
||||||
// absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
|
this.urls = ExcalidrawFont.createUrls(uri);
|
||||||
const assetUrl: string = uri.replace(/^\/+/, "");
|
|
||||||
let baseUrl: string | undefined = undefined;
|
|
||||||
|
|
||||||
// fallback to unpkg to form a valid URL in case of a passed relative assetUrl
|
const sources = this.urls
|
||||||
let baseUrlBuilder = window.EXCALIDRAW_ASSET_PATH || UNPKG_PROD_URL;
|
.map((url) => `url(${url}) ${ExcalidrawFont.getFormat(url)}`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
// in case user passed a root-relative url (~absolute path),
|
this.fontFace = new FontFace(family, sources, {
|
||||||
// like "/" or "/some/path", or relative (starts with "./"),
|
|
||||||
// prepend it with `location.origin`
|
|
||||||
if (/^\.?\//.test(baseUrlBuilder)) {
|
|
||||||
baseUrlBuilder = new URL(
|
|
||||||
baseUrlBuilder.replace(/^\.?\/+/, ""),
|
|
||||||
window?.location?.origin,
|
|
||||||
).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure there is a trailing slash, otherwise url won't be correctly concatenated
|
|
||||||
baseUrl = `${baseUrlBuilder.replace(/\/+$/, "")}/`;
|
|
||||||
|
|
||||||
this.url = new URL(assetUrl, baseUrl);
|
|
||||||
this.fontFace = new FontFace(family, `url(${this.url})`, {
|
|
||||||
display: "swap",
|
display: "swap",
|
||||||
style: "normal",
|
style: "normal",
|
||||||
weight: "400",
|
weight: "400",
|
||||||
@@ -44,35 +35,177 @@ export class ExcalidrawFont implements Font {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches woff2 content based on the registered url (browser).
|
* Tries to fetch woff2 content, based on the registered urls.
|
||||||
*
|
*
|
||||||
* Use dataurl outside the browser environment.
|
* NOTE: assumes usage of `dataurl` outside the browser environment
|
||||||
|
*
|
||||||
|
* @returns base64 with subsetted glyphs based on the passed codepoint, last defined url otherwise
|
||||||
*/
|
*/
|
||||||
public async getContent(): Promise<string> {
|
public async getContent(codePoints: ReadonlySet<number>): Promise<string> {
|
||||||
if (this.url.protocol === "data:") {
|
let i = 0;
|
||||||
|
const errorMessages = [];
|
||||||
|
|
||||||
|
while (i < this.urls.length) {
|
||||||
|
const url = this.urls[i];
|
||||||
|
|
||||||
// it's dataurl, the font is inlined as base64, no need to fetch
|
// it's dataurl, the font is inlined as base64, no need to fetch
|
||||||
return this.url.toString();
|
if (url.protocol === "data:") {
|
||||||
|
const arrayBuffer = Buffer.from(
|
||||||
|
url.toString().split(",")[1],
|
||||||
|
"base64",
|
||||||
|
).buffer;
|
||||||
|
|
||||||
|
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer,
|
||||||
|
codePoints,
|
||||||
|
);
|
||||||
|
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: "font/woff2",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const base64 = await ExcalidrawFont.subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer,
|
||||||
|
codePoints,
|
||||||
|
);
|
||||||
|
return base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
// response not ok, try to continue
|
||||||
|
errorMessages.push(
|
||||||
|
`"${url.toString()}" returned status "${response.status}"`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
errorMessages.push(`"${url.toString()}" returned error "${e}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(this.url, {
|
console.error(
|
||||||
headers: {
|
`Failed to fetch font "${
|
||||||
Accept: "font/woff2",
|
this.fontFace.family
|
||||||
},
|
}" from urls "${this.urls.toString()}`,
|
||||||
|
JSON.stringify(errorMessages, undefined, 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// in case of issues, at least return the last url as a content
|
||||||
|
// defaults to unpkg for bundled fonts (so that we don't have to host them forever) and http url for others
|
||||||
|
return this.urls.length ? this.urls[this.urls.length - 1].toString() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a font data as arraybuffer into a dataurl (base64) with subsetted glyphs based on the specified `codePoints`.
|
||||||
|
*
|
||||||
|
* NOTE: only glyphs are subsetted, other metadata as GPOS tables stay, consider filtering those as well in the future
|
||||||
|
*
|
||||||
|
* @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 converted into a dataurl
|
||||||
|
*/
|
||||||
|
private static async subsetGlyphsByCodePoints(
|
||||||
|
arrayBuffer: ArrayBuffer,
|
||||||
|
codePoints: ReadonlySet<number>,
|
||||||
|
): Promise<string> {
|
||||||
|
// checks for the cache first to avoid triggering the import multiple times in case of concurrent calls
|
||||||
|
if (!fontEditorCache) {
|
||||||
|
fontEditorCache = import("fonteditor-core");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { Font, woff2 } = await fontEditorCache;
|
||||||
|
|
||||||
|
// checks for the cache first to avoid triggering the init multiple times in case of concurrent calls
|
||||||
|
if (!brotliCache) {
|
||||||
|
brotliCache = woff2.init("/wasm/woff2.wasm");
|
||||||
|
}
|
||||||
|
|
||||||
|
await brotliCache;
|
||||||
|
|
||||||
|
const font = Font.create(arrayBuffer, {
|
||||||
|
type: "woff2",
|
||||||
|
kerning: true,
|
||||||
|
hinting: true,
|
||||||
|
// subset the glyhs based on the specified codepoints!
|
||||||
|
subset: [...codePoints],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
return font.toBase64({ type: "woff2", hinting: true });
|
||||||
console.error(
|
}
|
||||||
`Couldn't fetch font-family "${this.fontFace.family}" from url "${this.url}"`,
|
|
||||||
response,
|
private static createUrls(uri: string): URL[] {
|
||||||
);
|
if (uri.startsWith(LOCAL_FONT_PROTOCOL)) {
|
||||||
|
// no url for local fonts
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mimeType = await response.headers.get("Content-Type");
|
if (uri.startsWith("http") || uri.startsWith("data")) {
|
||||||
const buffer = await response.arrayBuffer();
|
// one url for http imports or data url
|
||||||
|
return [new URL(uri)];
|
||||||
|
}
|
||||||
|
|
||||||
return `data:${mimeType};base64,${await stringToBase64(
|
// absolute assets paths, which are found in tests and excalidraw-app build, won't work with base url, so we are stripping initial slash away
|
||||||
await toByteString(buffer),
|
const assetUrl: string = uri.replace(/^\/+/, "");
|
||||||
true,
|
const urls: URL[] = [];
|
||||||
)}`;
|
|
||||||
|
if (typeof window.EXCALIDRAW_ASSET_PATH === "string") {
|
||||||
|
const normalizedBaseUrl = this.normalizeBaseUrl(
|
||||||
|
window.EXCALIDRAW_ASSET_PATH,
|
||||||
|
);
|
||||||
|
|
||||||
|
urls.push(new URL(assetUrl, normalizedBaseUrl));
|
||||||
|
} else if (Array.isArray(window.EXCALIDRAW_ASSET_PATH)) {
|
||||||
|
window.EXCALIDRAW_ASSET_PATH.forEach((path) => {
|
||||||
|
const normalizedBaseUrl = this.normalizeBaseUrl(path);
|
||||||
|
urls.push(new URL(assetUrl, normalizedBaseUrl));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback url for bundled fonts
|
||||||
|
urls.push(new URL(assetUrl, UNPKG_PROD_URL));
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getFormat(url: URL) {
|
||||||
|
try {
|
||||||
|
const pathname = new URL(url).pathname;
|
||||||
|
const parts = pathname.split(".");
|
||||||
|
|
||||||
|
if (parts.length === 1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `format('${parts.pop()}')`;
|
||||||
|
} catch (error) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static normalizeBaseUrl(baseUrl: string) {
|
||||||
|
let result = baseUrl;
|
||||||
|
|
||||||
|
// in case user passed a root-relative url (~absolute path),
|
||||||
|
// like "/" or "/some/path", or relative (starts with "./"),
|
||||||
|
// prepend it with `location.origin`
|
||||||
|
if (/^\.?\//.test(result)) {
|
||||||
|
result = new URL(
|
||||||
|
result.replace(/^\.?\/+/, ""),
|
||||||
|
window?.location?.origin,
|
||||||
|
).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure there is a trailing slash, otherwise url won't be correctly concatenated
|
||||||
|
result = `${result.replace(/\/+$/, "")}/`;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,6 @@
|
|||||||
/* Only UI fonts here, which are needed before the editor initializes. */
|
/* Only UI fonts here, which are needed before the editor initializes. */
|
||||||
/* These also cannot be preprended with `EXCALIDRAW_ASSET_PATH`. */
|
/* These cannot be dynamically prepended with `EXCALIDRAW_ASSET_PATH`. */
|
||||||
|
/* WARN: The following content is replaced during excalidraw-app build */
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Assistant";
|
font-family: "Assistant";
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type Scene from "../scene/Scene";
|
import type Scene from "../scene/Scene";
|
||||||
import type { ValueOf } from "../utility-types";
|
import type { ValueOf } from "../utility-types";
|
||||||
import type { ExcalidrawTextElement, FontFamilyValues } from "../element/types";
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
FontFamilyValues,
|
||||||
|
} from "../element/types";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { isTextElement } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
@@ -16,7 +20,7 @@ import { getContainerElement } from "../element/textElement";
|
|||||||
|
|
||||||
import Virgil from "./assets/Virgil-Regular.woff2";
|
import Virgil from "./assets/Virgil-Regular.woff2";
|
||||||
import Excalifont from "./assets/Excalifont-Regular.woff2";
|
import Excalifont from "./assets/Excalifont-Regular.woff2";
|
||||||
import Cascadia from "./assets/CascadiaMono-Regular.woff2";
|
import Cascadia from "./assets/CascadiaCode-Regular.woff2";
|
||||||
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
import ComicShanns from "./assets/ComicShanns-Regular.woff2";
|
||||||
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
import LiberationSans from "./assets/LiberationSans-Regular.woff2";
|
||||||
|
|
||||||
@@ -39,15 +43,24 @@ export class Fonts {
|
|||||||
number,
|
number,
|
||||||
{
|
{
|
||||||
metadata: FontMetadata;
|
metadata: FontMetadata;
|
||||||
fontFaces: Font[];
|
fonts: Font[];
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
|
private static _initialized: boolean = false;
|
||||||
|
|
||||||
public static get registered() {
|
public static get registered() {
|
||||||
|
// lazy load the font registration
|
||||||
if (!Fonts._registered) {
|
if (!Fonts._registered) {
|
||||||
// lazy load the fonts
|
|
||||||
Fonts._registered = Fonts.init();
|
Fonts._registered = Fonts.init();
|
||||||
|
} else if (!Fonts._initialized) {
|
||||||
|
// case when host app register fonts before they are lazy loaded
|
||||||
|
// don't override whatever has been previously registered
|
||||||
|
Fonts._registered = new Map([
|
||||||
|
...Fonts.init().entries(),
|
||||||
|
...Fonts._registered.entries(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Fonts._registered;
|
return Fonts._registered;
|
||||||
@@ -59,17 +72,6 @@ export class Fonts {
|
|||||||
|
|
||||||
private readonly scene: Scene;
|
private readonly scene: Scene;
|
||||||
|
|
||||||
public get sceneFamilies() {
|
|
||||||
return Array.from(
|
|
||||||
this.scene.getNonDeletedElements().reduce((families, element) => {
|
|
||||||
if (isTextElement(element)) {
|
|
||||||
families.add(element.fontFamily);
|
|
||||||
}
|
|
||||||
return families;
|
|
||||||
}, new Set<number>()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor({ scene }: { scene: Scene }) {
|
constructor({ scene }: { scene: Scene }) {
|
||||||
this.scene = scene;
|
this.scene = scene;
|
||||||
}
|
}
|
||||||
@@ -119,21 +121,52 @@ export class Fonts {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public load = async () => {
|
/**
|
||||||
// Add all registered font faces into the `document.fonts` (if not added already)
|
* Load font faces for a given scene and trigger scene update.
|
||||||
for (const { fontFaces } of Fonts.registered.values()) {
|
*/
|
||||||
for (const { fontFace, url } of fontFaces) {
|
public loadSceneFonts = async (): Promise<FontFace[]> => {
|
||||||
if (
|
const sceneFamilies = this.getSceneFontFamilies();
|
||||||
url.protocol !== LOCAL_FONT_PROTOCOL &&
|
const loaded = await Fonts.loadFontFaces(sceneFamilies);
|
||||||
!window.document.fonts.has(fontFace)
|
this.onLoaded(loaded);
|
||||||
) {
|
return loaded;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the font families for the given scene.
|
||||||
|
*/
|
||||||
|
public getSceneFontFamilies = () => {
|
||||||
|
return Fonts.getFontFamilies(this.scene.getNonDeletedElements());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load font faces for passed elements - use when the scene is unavailable (i.e. export).
|
||||||
|
*/
|
||||||
|
public static loadFontsForElements = async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): Promise<FontFace[]> => {
|
||||||
|
const fontFamilies = Fonts.getFontFamilies(elements);
|
||||||
|
return await Fonts.loadFontFaces(fontFamilies);
|
||||||
|
};
|
||||||
|
|
||||||
|
private static async loadFontFaces(
|
||||||
|
fontFamilies: Array<ExcalidrawTextElement["fontFamily"]>,
|
||||||
|
) {
|
||||||
|
// add all registered font faces into the `document.fonts` (if not added already)
|
||||||
|
for (const { fonts, metadata } of Fonts.registered.values()) {
|
||||||
|
// skip registering font faces for local fonts (i.e. Helvetica)
|
||||||
|
if (metadata.local) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { fontFace } of fonts) {
|
||||||
|
if (!window.document.fonts.has(fontFace)) {
|
||||||
window.document.fonts.add(fontFace);
|
window.document.fonts.add(fontFace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loaded = await Promise.all(
|
const loadedFontFaces = await Promise.all(
|
||||||
this.sceneFamilies.map(async (fontFamily) => {
|
fontFamilies.map(async (fontFamily) => {
|
||||||
const fontString = getFontString({
|
const fontString = getFontString({
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -148,8 +181,10 @@ export class Fonts {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// don't let it all fail if just one font fails to load
|
// don't let it all fail if just one font fails to load
|
||||||
console.error(
|
console.error(
|
||||||
`Failed to load font: "${fontString}" with error "${e}", given the following registered font:`,
|
`Failed to load font "${fontString}" from urls "${Fonts.registered
|
||||||
JSON.stringify(Fonts.registered.get(fontFamily), undefined, 2),
|
.get(fontFamily)
|
||||||
|
?.fonts.map((x) => x.urls)}"`,
|
||||||
|
e,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,8 +193,8 @@ export class Fonts {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.onLoaded(loaded.flat().filter(Boolean) as FontFace[]);
|
return loadedFontFaces.flat().filter(Boolean) as FontFace[];
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WARN: should be called just once on init, even across multiple instances.
|
* WARN: should be called just once on init, even across multiple instances.
|
||||||
@@ -168,10 +203,11 @@ export class Fonts {
|
|||||||
const fonts = {
|
const fonts = {
|
||||||
registered: new Map<
|
registered: new Map<
|
||||||
ValueOf<typeof FONT_FAMILY>,
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
{ metadata: FontMetadata; fontFaces: Font[] }
|
{ metadata: FontMetadata; fonts: Font[] }
|
||||||
>(),
|
>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: let's tweak this once we know how `register` will be exposed as part of the custom fonts API
|
||||||
const _register = register.bind(fonts);
|
const _register = register.bind(fonts);
|
||||||
|
|
||||||
_register("Virgil", FONT_METADATA[FONT_FAMILY.Virgil], {
|
_register("Virgil", FONT_METADATA[FONT_FAMILY.Virgil], {
|
||||||
@@ -236,8 +272,23 @@ export class Fonts {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Fonts._initialized = true;
|
||||||
|
|
||||||
return fonts.registered;
|
return fonts.registered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static getFontFamilies(
|
||||||
|
elements: ReadonlyArray<ExcalidrawElement>,
|
||||||
|
): Array<ExcalidrawTextElement["fontFamily"]> {
|
||||||
|
return Array.from(
|
||||||
|
elements.reduce((families, element) => {
|
||||||
|
if (isTextElement(element)) {
|
||||||
|
families.add(element.fontFamily);
|
||||||
|
}
|
||||||
|
return families;
|
||||||
|
}, new Set<number>()),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -253,7 +304,7 @@ function register(
|
|||||||
| {
|
| {
|
||||||
registered: Map<
|
registered: Map<
|
||||||
ValueOf<typeof FONT_FAMILY>,
|
ValueOf<typeof FONT_FAMILY>,
|
||||||
{ metadata: FontMetadata; fontFaces: Font[] }
|
{ metadata: FontMetadata; fonts: Font[] }
|
||||||
>;
|
>;
|
||||||
},
|
},
|
||||||
family: string,
|
family: string,
|
||||||
@@ -267,7 +318,7 @@ function register(
|
|||||||
if (!registeredFamily) {
|
if (!registeredFamily) {
|
||||||
this.registered.set(familyId, {
|
this.registered.set(familyId, {
|
||||||
metadata,
|
metadata,
|
||||||
fontFaces: params.map(
|
fonts: params.map(
|
||||||
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
|
({ uri, descriptors }) => new ExcalidrawFont(family, uri, descriptors),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface FontMetadata {
|
|||||||
deprecated?: true;
|
deprecated?: true;
|
||||||
/** flag to indicate a server-side only font */
|
/** flag to indicate a server-side only font */
|
||||||
serverSide?: true;
|
serverSide?: true;
|
||||||
|
/** flag to indiccate a local-only font */
|
||||||
|
local?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FONT_METADATA: Record<number, FontMetadata> = {
|
export const FONT_METADATA: Record<number, FontMetadata> = {
|
||||||
@@ -85,6 +87,7 @@ export const FONT_METADATA: Record<number, FontMetadata> = {
|
|||||||
},
|
},
|
||||||
icon: FontFamilyNormalIcon,
|
icon: FontFamilyNormalIcon,
|
||||||
deprecated: true,
|
deprecated: true,
|
||||||
|
local: true,
|
||||||
},
|
},
|
||||||
[FONT_FAMILY.Cascadia]: {
|
[FONT_FAMILY.Cascadia]: {
|
||||||
metrics: {
|
metrics: {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type {
|
|||||||
OrderedExcalidrawElement,
|
OrderedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { InvalidFractionalIndexError } from "./errors";
|
import { InvalidFractionalIndexError } from "./errors";
|
||||||
|
import { hasBoundTextElement } from "./element/typeChecks";
|
||||||
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
|
import { arrayToMap } from "./utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Envisioned relation between array order and fractional indices:
|
* Envisioned relation between array order and fractional indices:
|
||||||
@@ -30,17 +33,84 @@ import { InvalidFractionalIndexError } from "./errors";
|
|||||||
* @throws `InvalidFractionalIndexError` if invalid index is detected.
|
* @throws `InvalidFractionalIndexError` if invalid index is detected.
|
||||||
*/
|
*/
|
||||||
export const validateFractionalIndices = (
|
export const validateFractionalIndices = (
|
||||||
indices: (ExcalidrawElement["index"] | undefined)[],
|
elements: readonly ExcalidrawElement[],
|
||||||
|
{
|
||||||
|
shouldThrow = false,
|
||||||
|
includeBoundTextValidation = false,
|
||||||
|
ignoreLogs,
|
||||||
|
reconciliationContext,
|
||||||
|
}: {
|
||||||
|
shouldThrow: boolean;
|
||||||
|
includeBoundTextValidation: boolean;
|
||||||
|
ignoreLogs?: true;
|
||||||
|
reconciliationContext?: {
|
||||||
|
localElements: ReadonlyArray<ExcalidrawElement>;
|
||||||
|
remoteElements: ReadonlyArray<ExcalidrawElement>;
|
||||||
|
};
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
|
const errorMessages = [];
|
||||||
|
const stringifyElement = (element: ExcalidrawElement | void) =>
|
||||||
|
`${element?.index}:${element?.id}:${element?.type}:${element?.isDeleted}:${element?.version}:${element?.versionNonce}`;
|
||||||
|
|
||||||
|
const indices = elements.map((x) => x.index);
|
||||||
for (const [i, index] of indices.entries()) {
|
for (const [i, index] of indices.entries()) {
|
||||||
const predecessorIndex = indices[i - 1];
|
const predecessorIndex = indices[i - 1];
|
||||||
const successorIndex = indices[i + 1];
|
const successorIndex = indices[i + 1];
|
||||||
|
|
||||||
if (!isValidFractionalIndex(index, predecessorIndex, successorIndex)) {
|
if (!isValidFractionalIndex(index, predecessorIndex, successorIndex)) {
|
||||||
throw new InvalidFractionalIndexError(
|
errorMessages.push(
|
||||||
`Fractional indices invariant for element has been compromised - ["${predecessorIndex}", "${index}", "${successorIndex}"] [predecessor, current, successor]`,
|
`Fractional indices invariant has been compromised: "${stringifyElement(
|
||||||
|
elements[i - 1],
|
||||||
|
)}", "${stringifyElement(elements[i])}", "${stringifyElement(
|
||||||
|
elements[i + 1],
|
||||||
|
)}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// disabled by default, as we don't fix it
|
||||||
|
if (includeBoundTextValidation && hasBoundTextElement(elements[i])) {
|
||||||
|
const container = elements[i];
|
||||||
|
const text = getBoundTextElement(container, arrayToMap(elements));
|
||||||
|
|
||||||
|
if (text && text.index! <= container.index!) {
|
||||||
|
errorMessages.push(
|
||||||
|
`Fractional indices invariant for bound elements has been compromised: "${stringifyElement(
|
||||||
|
text,
|
||||||
|
)}", "${stringifyElement(container)}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessages.length) {
|
||||||
|
const error = new InvalidFractionalIndexError();
|
||||||
|
const additionalContext = [];
|
||||||
|
|
||||||
|
if (reconciliationContext) {
|
||||||
|
additionalContext.push("Additional reconciliation context:");
|
||||||
|
additionalContext.push(
|
||||||
|
reconciliationContext.localElements.map((x) => stringifyElement(x)),
|
||||||
|
);
|
||||||
|
additionalContext.push(
|
||||||
|
reconciliationContext.remoteElements.map((x) => stringifyElement(x)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreLogs) {
|
||||||
|
// report just once and with the stacktrace
|
||||||
|
console.error(
|
||||||
|
errorMessages.join("\n\n"),
|
||||||
|
error.stack,
|
||||||
|
elements.map((x) => stringifyElement(x)),
|
||||||
|
...additionalContext,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldThrow) {
|
||||||
|
// if enabled, gather all the errors first, throw once
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,10 +153,19 @@ export const syncMovedIndices = (
|
|||||||
|
|
||||||
// try generatating indices, throws on invalid movedElements
|
// try generatating indices, throws on invalid movedElements
|
||||||
const elementsUpdates = generateIndices(elements, indicesGroups);
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
||||||
|
const elementsCandidates = elements.map((x) =>
|
||||||
|
elementsUpdates.has(x) ? { ...x, ...elementsUpdates.get(x) } : x,
|
||||||
|
);
|
||||||
|
|
||||||
// ensure next indices are valid before mutation, throws on invalid ones
|
// ensure next indices are valid before mutation, throws on invalid ones
|
||||||
validateFractionalIndices(
|
validateFractionalIndices(
|
||||||
elements.map((x) => elementsUpdates.get(x)?.index || x.index),
|
elementsCandidates,
|
||||||
|
// we don't autofix invalid bound text indices, hence don't include it in the validation
|
||||||
|
{
|
||||||
|
includeBoundTextValidation: false,
|
||||||
|
shouldThrow: true,
|
||||||
|
ignoreLogs: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// split mutation so we don't end up in an incosistent state
|
// split mutation so we don't end up in an incosistent state
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import type { ExcalidrawElement } from "./element/types";
|
import type { ExcalidrawElement } from "./element/types";
|
||||||
import { convertToExcalidrawElements, Excalidraw } from "./index";
|
import { convertToExcalidrawElements, Excalidraw } from "./index";
|
||||||
import { API } from "./tests/helpers/api";
|
import { API } from "./tests/helpers/api";
|
||||||
@@ -122,7 +123,7 @@ describe("adding elements to frames", () => {
|
|||||||
) => {
|
) => {
|
||||||
describe.skip("when frame is in a layer below", async () => {
|
describe.skip("when frame is in a layer below", async () => {
|
||||||
it("should add an element", async () => {
|
it("should add an element", async () => {
|
||||||
h.elements = [frame, rect2];
|
API.setElements([frame, rect2]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
|
|
||||||
@@ -131,7 +132,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements", async () => {
|
it("should add elements", async () => {
|
||||||
h.elements = [frame, rect2, rect3];
|
API.setElements([frame, rect2, rect3]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -142,7 +143,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other other elements in between", async () => {
|
it("should add elements when there are other other elements in between", async () => {
|
||||||
h.elements = [frame, rect1, rect2, rect4, rect3];
|
API.setElements([frame, rect1, rect2, rect4, rect3]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -153,7 +154,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||||
h.elements = [frame, rect3, rect4, rect2, rect1];
|
API.setElements([frame, rect3, rect4, rect2, rect1]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -166,7 +167,7 @@ describe("adding elements to frames", () => {
|
|||||||
|
|
||||||
describe.skip("when frame is in a layer above", async () => {
|
describe.skip("when frame is in a layer above", async () => {
|
||||||
it("should add an element", async () => {
|
it("should add an element", async () => {
|
||||||
h.elements = [rect2, frame];
|
API.setElements([rect2, frame]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
|
|
||||||
@@ -175,7 +176,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements", async () => {
|
it("should add elements", async () => {
|
||||||
h.elements = [rect2, rect3, frame];
|
API.setElements([rect2, rect3, frame]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -186,7 +187,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other other elements in between", async () => {
|
it("should add elements when there are other other elements in between", async () => {
|
||||||
h.elements = [rect1, rect2, rect4, rect3, frame];
|
API.setElements([rect1, rect2, rect4, rect3, frame]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -197,7 +198,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||||
h.elements = [rect3, rect4, rect2, rect1, frame];
|
API.setElements([rect3, rect4, rect2, rect1, frame]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -210,7 +211,7 @@ describe("adding elements to frames", () => {
|
|||||||
|
|
||||||
describe("when frame is in an inner layer", async () => {
|
describe("when frame is in an inner layer", async () => {
|
||||||
it.skip("should add elements", async () => {
|
it.skip("should add elements", async () => {
|
||||||
h.elements = [rect2, frame, rect3];
|
API.setElements([rect2, frame, rect3]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -221,7 +222,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should add elements when there are other other elements in between", async () => {
|
it.skip("should add elements when there are other other elements in between", async () => {
|
||||||
h.elements = [rect2, rect1, frame, rect4, rect3];
|
API.setElements([rect2, rect1, frame, rect4, rect3]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -232,7 +233,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
API.setElements([rect3, rect4, frame, rect2, rect1]);
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
func(frame, rect3);
|
func(frame, rect3);
|
||||||
@@ -253,20 +254,22 @@ describe("adding elements to frames", () => {
|
|||||||
|
|
||||||
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
const frame = API.createElement({ type: "frame", x: 0, y: 0 });
|
||||||
|
|
||||||
h.elements = reorderElements(
|
API.setElements(
|
||||||
[
|
reorderElements(
|
||||||
frame,
|
[
|
||||||
...convertToExcalidrawElements([
|
frame,
|
||||||
{
|
...convertToExcalidrawElements([
|
||||||
type: containerType,
|
{
|
||||||
x: 100,
|
type: containerType,
|
||||||
y: 100,
|
x: 100,
|
||||||
height: 10,
|
y: 100,
|
||||||
label: { text: "xx" },
|
height: 10,
|
||||||
},
|
label: { text: "xx" },
|
||||||
]),
|
},
|
||||||
],
|
]),
|
||||||
initialOrder,
|
],
|
||||||
|
initialOrder,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
assertOrder(h.elements, initialOrder);
|
assertOrder(h.elements, initialOrder);
|
||||||
@@ -337,7 +340,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
|
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
|
||||||
h.elements = [frame, arrow, text];
|
API.setElements([frame, arrow, text]);
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
|
|
||||||
@@ -347,7 +350,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should add arrow bound with text when frame is in a layer above", async () => {
|
it("should add arrow bound with text when frame is in a layer above", async () => {
|
||||||
h.elements = [arrow, text, frame];
|
API.setElements([arrow, text, frame]);
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
|
|
||||||
@@ -357,7 +360,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
|
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||||
h.elements = [arrow, frame, text];
|
API.setElements([arrow, frame, text]);
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
|
|
||||||
@@ -369,7 +372,7 @@ describe("adding elements to frames", () => {
|
|||||||
|
|
||||||
describe("resizing frame over elements but downwards", async () => {
|
describe("resizing frame over elements but downwards", async () => {
|
||||||
it.skip("should add elements when frame is in a layer below", async () => {
|
it.skip("should add elements when frame is in a layer below", async () => {
|
||||||
h.elements = [frame, rect1, rect2, rect3, rect4];
|
API.setElements([frame, rect1, rect2, rect3, rect4]);
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
resizeFrameOverElement(frame, rect3);
|
resizeFrameOverElement(frame, rect3);
|
||||||
@@ -380,7 +383,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should add elements when frame is in a layer above", async () => {
|
it.skip("should add elements when frame is in a layer above", async () => {
|
||||||
h.elements = [rect1, rect2, rect3, rect4, frame];
|
API.setElements([rect1, rect2, rect3, rect4, frame]);
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
resizeFrameOverElement(frame, rect3);
|
resizeFrameOverElement(frame, rect3);
|
||||||
@@ -391,7 +394,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should add elements when frame is in an inner layer", async () => {
|
it.skip("should add elements when frame is in an inner layer", async () => {
|
||||||
h.elements = [rect1, rect2, frame, rect3, rect4];
|
API.setElements([rect1, rect2, frame, rect3, rect4]);
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
resizeFrameOverElement(frame, rect3);
|
resizeFrameOverElement(frame, rect3);
|
||||||
@@ -406,7 +409,7 @@ describe("adding elements to frames", () => {
|
|||||||
await commonTestCases(dragElementIntoFrame);
|
await commonTestCases(dragElementIntoFrame);
|
||||||
|
|
||||||
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
||||||
h.elements = [frame, rect2];
|
API.setElements([frame, rect2]);
|
||||||
|
|
||||||
dragElementIntoFrame(frame, rect2);
|
dragElementIntoFrame(frame, rect2);
|
||||||
|
|
||||||
@@ -420,7 +423,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
|
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
|
||||||
h.elements = [frame, rect2];
|
API.setElements([frame, rect2]);
|
||||||
|
|
||||||
dragElementIntoFrame(frame, rect2);
|
dragElementIntoFrame(frame, rect2);
|
||||||
|
|
||||||
@@ -490,7 +493,7 @@ describe("adding elements to frames", () => {
|
|||||||
frameId: frame3.id,
|
frameId: frame3.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [
|
API.setElements([
|
||||||
frame1,
|
frame1,
|
||||||
rectangle4,
|
rectangle4,
|
||||||
rectangle1,
|
rectangle1,
|
||||||
@@ -498,7 +501,7 @@ describe("adding elements to frames", () => {
|
|||||||
frame3,
|
frame3,
|
||||||
rectangle2,
|
rectangle2,
|
||||||
frame2,
|
frame2,
|
||||||
];
|
]);
|
||||||
|
|
||||||
API.setSelectedElements([rectangle2]);
|
API.setSelectedElements([rectangle2]);
|
||||||
|
|
||||||
@@ -541,7 +544,7 @@ describe("adding elements to frames", () => {
|
|||||||
frameId: frame2.id,
|
frameId: frame2.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
h.elements = [rectangle1, rectangle2, frame1, frame2];
|
API.setElements([rectangle1, rectangle2, frame1, frame2]);
|
||||||
|
|
||||||
API.setSelectedElements([rectangle2]);
|
API.setSelectedElements([rectangle2]);
|
||||||
|
|
||||||
|
|||||||
@@ -755,15 +755,12 @@ export const isElementInFrame = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFrameLikeTitle = (
|
export const getFrameLikeTitle = (element: ExcalidrawFrameLikeElement) => {
|
||||||
element: ExcalidrawFrameLikeElement,
|
|
||||||
frameIdx: number,
|
|
||||||
) => {
|
|
||||||
// TODO name frames "AI" only if specific to AI frames
|
// TODO name frames "AI" only if specific to AI frames
|
||||||
return element.name === null
|
return element.name === null
|
||||||
? isFrameElement(element)
|
? isFrameElement(element)
|
||||||
? `Frame ${frameIdx}`
|
? "Frame"
|
||||||
: `AI Frame $${frameIdx}`
|
: "AI Frame"
|
||||||
: element.name;
|
: element.name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Vendored
+2
-1
@@ -1,9 +1,10 @@
|
|||||||
interface Window {
|
interface Window {
|
||||||
ClipboardItem: any;
|
ClipboardItem: any;
|
||||||
__EXCALIDRAW_SHA__: string | undefined;
|
__EXCALIDRAW_SHA__: string | undefined;
|
||||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
EXCALIDRAW_ASSET_PATH: string | string[] | undefined;
|
||||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||||
|
DEBUG_FRACTIONAL_INDICES: boolean | undefined;
|
||||||
gtag: Function;
|
gtag: Function;
|
||||||
sa_event: Function;
|
sa_event: Function;
|
||||||
fathom: { trackEvent: Function };
|
fathom: { trackEvent: Function };
|
||||||
|
|||||||
@@ -46,6 +46,10 @@
|
|||||||
"arrowhead_triangle_outline": "Triangle (outline)",
|
"arrowhead_triangle_outline": "Triangle (outline)",
|
||||||
"arrowhead_diamond": "Diamond",
|
"arrowhead_diamond": "Diamond",
|
||||||
"arrowhead_diamond_outline": "Diamond (outline)",
|
"arrowhead_diamond_outline": "Diamond (outline)",
|
||||||
|
"arrowtypes": "Arrow type",
|
||||||
|
"arrowtype_sharp": "Sharp arrow",
|
||||||
|
"arrowtype_round": "Curved arrow",
|
||||||
|
"arrowtype_elbowed": "Elbow arrow",
|
||||||
"fontSize": "Font size",
|
"fontSize": "Font size",
|
||||||
"fontFamily": "Font family",
|
"fontFamily": "Font family",
|
||||||
"addWatermark": "Add \"Made with Excalidraw\"",
|
"addWatermark": "Add \"Made with Excalidraw\"",
|
||||||
@@ -295,6 +299,7 @@
|
|||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
"canvasPanning": "To move canvas, hold mouse wheel or spacebar while dragging, or use the hand tool",
|
||||||
"linearElement": "Click to start multiple points, drag for single line",
|
"linearElement": "Click to start multiple points, drag for single line",
|
||||||
|
"arrowTool": "Click to start multiple points, drag for single line. Press {{arrowShortcut}} again to change arrow type.",
|
||||||
"freeDraw": "Click and drag, release when you're finished",
|
"freeDraw": "Click and drag, release when you're finished",
|
||||||
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
"text": "Tip: you can also add text by double-clicking anywhere with the selection tool",
|
||||||
"embeddable": "Click-drag to create a website embed",
|
"embeddable": "Click-drag to create a website embed",
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { rangeIntersection, rangesOverlap, rotate } from "./math";
|
import {
|
||||||
|
isPointOnSymmetricArc,
|
||||||
|
rangeIntersection,
|
||||||
|
rangesOverlap,
|
||||||
|
rotate,
|
||||||
|
} from "./math";
|
||||||
|
|
||||||
describe("rotate", () => {
|
describe("rotate", () => {
|
||||||
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
it("should rotate over (x2, y2) and return the rotated coordinates for (x1, y1)", () => {
|
||||||
@@ -53,3 +58,42 @@ describe("range intersection", () => {
|
|||||||
expect(rangeIntersection([1, 4], [5, 7])).toEqual(null);
|
expect(rangeIntersection([1, 4], [5, 7])).toEqual(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("point on arc", () => {
|
||||||
|
it("should detect point on simple arc", () => {
|
||||||
|
expect(
|
||||||
|
isPointOnSymmetricArc(
|
||||||
|
{
|
||||||
|
radius: 1,
|
||||||
|
startAngle: -Math.PI / 4,
|
||||||
|
endAngle: Math.PI / 4,
|
||||||
|
},
|
||||||
|
[0.92291667, 0.385],
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("should not detect point outside of a simple arc", () => {
|
||||||
|
expect(
|
||||||
|
isPointOnSymmetricArc(
|
||||||
|
{
|
||||||
|
radius: 1,
|
||||||
|
startAngle: -Math.PI / 4,
|
||||||
|
endAngle: Math.PI / 4,
|
||||||
|
},
|
||||||
|
[-0.92291667, 0.385],
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
it("should not detect point with good angle but incorrect radius", () => {
|
||||||
|
expect(
|
||||||
|
isPointOnSymmetricArc(
|
||||||
|
{
|
||||||
|
radius: 1,
|
||||||
|
startAngle: -Math.PI / 4,
|
||||||
|
endAngle: Math.PI / 4,
|
||||||
|
},
|
||||||
|
[-0.5, 0.5],
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ import type {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
|
import type { Bounds } from "./element/bounds";
|
||||||
import { getCurvePathOps } from "./element/bounds";
|
import { getCurvePathOps } from "./element/bounds";
|
||||||
import type { Mutable } from "./utility-types";
|
import type { Mutable } from "./utility-types";
|
||||||
import { ShapeCache } from "./scene/ShapeCache";
|
import { ShapeCache } from "./scene/ShapeCache";
|
||||||
|
import type { Vector } from "../utils/geometry/shape";
|
||||||
|
|
||||||
export const rotate = (
|
export const rotate = (
|
||||||
// target point to rotate
|
// target point to rotate
|
||||||
@@ -153,6 +155,12 @@ export const distance2d = (x1: number, y1: number, x2: number, y2: number) => {
|
|||||||
return Math.hypot(xd, yd);
|
return Math.hypot(xd, yd);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const distanceSq2d = (p1: Point, p2: Point) => {
|
||||||
|
const xd = p2[0] - p1[0];
|
||||||
|
const yd = p2[1] - p1[1];
|
||||||
|
return xd * xd + yd * yd;
|
||||||
|
};
|
||||||
|
|
||||||
export const centerPoint = (a: Point, b: Point): Point => {
|
export const centerPoint = (a: Point, b: Point): Point => {
|
||||||
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
||||||
};
|
};
|
||||||
@@ -519,3 +527,179 @@ export const rangeIntersection = (
|
|||||||
export const isValueInRange = (value: number, min: number, max: number) => {
|
export const isValueInRange = (value: number, min: number, max: number) => {
|
||||||
return value >= min && value <= max;
|
return value >= min && value <= max;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const translatePoint = (p: Point, v: Vector): Point => [
|
||||||
|
p[0] + v[0],
|
||||||
|
p[1] + v[1],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const scaleVector = (v: Vector, scalar: number): Vector => [
|
||||||
|
v[0] * scalar,
|
||||||
|
v[1] * scalar,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const pointToVector = (p: Point, origin: Point = [0, 0]): Vector => [
|
||||||
|
p[0] - origin[0],
|
||||||
|
p[1] - origin[1],
|
||||||
|
];
|
||||||
|
|
||||||
|
export const scalePointFromOrigin = (
|
||||||
|
p: Point,
|
||||||
|
mid: Point,
|
||||||
|
multiplier: number,
|
||||||
|
) => translatePoint(mid, scaleVector(pointToVector(p, mid), multiplier));
|
||||||
|
|
||||||
|
const triangleSign = (p1: Point, p2: Point, p3: Point): number =>
|
||||||
|
(p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]);
|
||||||
|
|
||||||
|
export const PointInTriangle = (pt: Point, v1: Point, v2: Point, v3: Point) => {
|
||||||
|
const d1 = triangleSign(pt, v1, v2);
|
||||||
|
const d2 = triangleSign(pt, v2, v3);
|
||||||
|
const d3 = triangleSign(pt, v3, v1);
|
||||||
|
|
||||||
|
const has_neg = d1 < 0 || d2 < 0 || d3 < 0;
|
||||||
|
const has_pos = d1 > 0 || d2 > 0 || d3 > 0;
|
||||||
|
|
||||||
|
return !(has_neg && has_pos);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const magnitudeSq = (vector: Vector) =>
|
||||||
|
vector[0] * vector[0] + vector[1] * vector[1];
|
||||||
|
|
||||||
|
export const magnitude = (vector: Vector) => Math.sqrt(magnitudeSq(vector));
|
||||||
|
|
||||||
|
export const normalize = (vector: Vector): Vector => {
|
||||||
|
const m = magnitude(vector);
|
||||||
|
|
||||||
|
return [vector[0] / m, vector[1] / m];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addVectors = (
|
||||||
|
vec1: Readonly<Vector>,
|
||||||
|
vec2: Readonly<Vector>,
|
||||||
|
): Vector => [vec1[0] + vec2[0], vec1[1] + vec2[1]];
|
||||||
|
|
||||||
|
export const subtractVectors = (
|
||||||
|
vec1: Readonly<Vector>,
|
||||||
|
vec2: Readonly<Vector>,
|
||||||
|
): Vector => [vec1[0] - vec2[0], vec1[1] - vec2[1]];
|
||||||
|
|
||||||
|
export const pointInsideBounds = (p: Point, bounds: Bounds): boolean =>
|
||||||
|
p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the axis-aligned bounding box for a given element
|
||||||
|
*/
|
||||||
|
export const aabbForElement = (
|
||||||
|
element: Readonly<ExcalidrawElement>,
|
||||||
|
offset?: [number, number, number, number],
|
||||||
|
) => {
|
||||||
|
const bbox = {
|
||||||
|
minX: element.x,
|
||||||
|
minY: element.y,
|
||||||
|
maxX: element.x + element.width,
|
||||||
|
maxY: element.y + element.height,
|
||||||
|
midX: element.x + element.width / 2,
|
||||||
|
midY: element.y + element.height / 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
const center = [bbox.midX, bbox.midY] as Point;
|
||||||
|
const [topLeftX, topLeftY] = rotatePoint(
|
||||||
|
[bbox.minX, bbox.minY],
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [topRightX, topRightY] = rotatePoint(
|
||||||
|
[bbox.maxX, bbox.minY],
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [bottomRightX, bottomRightY] = rotatePoint(
|
||||||
|
[bbox.maxX, bbox.maxY],
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
const [bottomLeftX, bottomLeftY] = rotatePoint(
|
||||||
|
[bbox.minX, bbox.maxY],
|
||||||
|
center,
|
||||||
|
element.angle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const bounds = [
|
||||||
|
Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
||||||
|
Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
||||||
|
Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX),
|
||||||
|
Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY),
|
||||||
|
] as Bounds;
|
||||||
|
|
||||||
|
if (offset) {
|
||||||
|
const [topOffset, rightOffset, downOffset, leftOffset] = offset;
|
||||||
|
return [
|
||||||
|
bounds[0] - leftOffset,
|
||||||
|
bounds[1] - topOffset,
|
||||||
|
bounds[2] + rightOffset,
|
||||||
|
bounds[3] + downOffset,
|
||||||
|
] as Bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PolarCoords = [number, number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the polar coordinates for the given carthesian point represented by
|
||||||
|
* (x, y) for the center point 0,0 where the first number returned is the radius,
|
||||||
|
* the second is the angle in radians.
|
||||||
|
*/
|
||||||
|
export const carthesian2Polar = ([x, y]: Point): PolarCoords => [
|
||||||
|
Math.hypot(x, y),
|
||||||
|
Math.atan2(y, x),
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Angles are in radians and centered on 0, 0. Zero radians on a 1 radius circle
|
||||||
|
* corresponds to (1, 0) carthesian coordinates (point), i.e. to the "right".
|
||||||
|
*/
|
||||||
|
type SymmetricArc = { radius: number; startAngle: number; endAngle: number };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a carthesian point lies on a symmetric arc, i.e. an arc which
|
||||||
|
* is part of a circle contour centered on 0, 0.
|
||||||
|
*/
|
||||||
|
export const isPointOnSymmetricArc = (
|
||||||
|
{ radius: arcRadius, startAngle, endAngle }: SymmetricArc,
|
||||||
|
point: Point,
|
||||||
|
): boolean => {
|
||||||
|
const [radius, angle] = carthesian2Polar(point);
|
||||||
|
|
||||||
|
return startAngle < endAngle
|
||||||
|
? Math.abs(radius - arcRadius) < 0.0000001 &&
|
||||||
|
startAngle <= angle &&
|
||||||
|
endAngle >= angle
|
||||||
|
: startAngle <= angle || endAngle >= angle;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCenterForBounds = (bounds: Bounds): Point => [
|
||||||
|
bounds[0] + (bounds[2] - bounds[0]) / 2,
|
||||||
|
bounds[1] + (bounds[3] - bounds[1]) / 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getCenterForElement = (element: ExcalidrawElement): Point => [
|
||||||
|
element.x + element.width / 2,
|
||||||
|
element.y + element.height / 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
export const aabbsOverlapping = (a: Bounds, b: Bounds) =>
|
||||||
|
pointInsideBounds([a[0], a[1]], b) ||
|
||||||
|
pointInsideBounds([a[2], a[1]], b) ||
|
||||||
|
pointInsideBounds([a[2], a[3]], b) ||
|
||||||
|
pointInsideBounds([a[0], a[3]], b) ||
|
||||||
|
pointInsideBounds([b[0], b[1]], a) ||
|
||||||
|
pointInsideBounds([b[2], b[1]], a) ||
|
||||||
|
pointInsideBounds([b[2], b[3]], a) ||
|
||||||
|
pointInsideBounds([b[0], b[3]], a);
|
||||||
|
|
||||||
|
export const clamp = (value: number, min: number, max: number) => {
|
||||||
|
return Math.min(Math.max(value, min), max);
|
||||||
|
};
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"canvas-roundrect-polyfill": "0.0.1",
|
"canvas-roundrect-polyfill": "0.0.1",
|
||||||
"clsx": "1.1.1",
|
"clsx": "1.1.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
|
"fonteditor-core": "2.4.1",
|
||||||
"fractional-indexing": "3.2.0",
|
"fractional-indexing": "3.2.0",
|
||||||
"fuzzy": "0.1.3",
|
"fuzzy": "0.1.3",
|
||||||
"image-blob-reduce": "3.0.1",
|
"image-blob-reduce": "3.0.1",
|
||||||
@@ -87,17 +88,18 @@
|
|||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.18.9",
|
"@babel/core": "7.24.5",
|
||||||
"@babel/plugin-transform-arrow-functions": "7.18.6",
|
"@babel/plugin-transform-arrow-functions": "7.24.1",
|
||||||
"@babel/plugin-transform-async-to-generator": "7.18.6",
|
"@babel/plugin-transform-async-to-generator": "7.24.1",
|
||||||
"@babel/plugin-transform-runtime": "7.18.9",
|
"@babel/plugin-transform-runtime": "7.24.3",
|
||||||
"@babel/plugin-transform-typescript": "7.18.8",
|
"@babel/plugin-transform-typescript": "7.24.5",
|
||||||
"@babel/preset-env": "7.18.6",
|
"@babel/preset-env": "7.24.5",
|
||||||
"@babel/preset-react": "7.18.6",
|
"@babel/preset-react": "7.24.1",
|
||||||
"@babel/preset-typescript": "7.18.6",
|
"@babel/preset-typescript": "7.24.1",
|
||||||
"@size-limit/preset-big-lib": "9.0.0",
|
"@size-limit/preset-big-lib": "9.0.0",
|
||||||
|
"@testing-library/dom": "10.4.0",
|
||||||
"@testing-library/jest-dom": "5.16.2",
|
"@testing-library/jest-dom": "5.16.2",
|
||||||
"@testing-library/react": "12.1.5",
|
"@testing-library/react": "16.0.0",
|
||||||
"@types/pako": "1.0.3",
|
"@types/pako": "1.0.3",
|
||||||
"@types/pica": "5.1.3",
|
"@types/pica": "5.1.3",
|
||||||
"@types/resize-observer-browser": "0.1.7",
|
"@types/resize-observer-browser": "0.1.7",
|
||||||
@@ -115,8 +117,6 @@
|
|||||||
"import-meta-loader": "1.1.0",
|
"import-meta-loader": "1.1.0",
|
||||||
"mini-css-extract-plugin": "2.6.1",
|
"mini-css-extract-plugin": "2.6.1",
|
||||||
"postcss-loader": "7.0.1",
|
"postcss-loader": "7.0.1",
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"sass-loader": "13.0.2",
|
"sass-loader": "13.0.2",
|
||||||
"size-limit": "9.0.0",
|
"size-limit": "9.0.0",
|
||||||
"style-loader": "3.3.3",
|
"style-loader": "3.3.3",
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ import {
|
|||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import {
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isElbowArrow,
|
||||||
isFrameLikeElement,
|
isFrameLikeElement,
|
||||||
isLinearElement,
|
isLinearElement,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
@@ -67,6 +69,7 @@ import type {
|
|||||||
InteractiveSceneRenderConfig,
|
InteractiveSceneRenderConfig,
|
||||||
RenderableElementsMap,
|
RenderableElementsMap,
|
||||||
} from "../scene/types";
|
} from "../scene/types";
|
||||||
|
import { getCornerRadius } from "../math";
|
||||||
|
|
||||||
const renderLinearElementPointHighlight = (
|
const renderLinearElementPointHighlight = (
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
@@ -212,13 +215,18 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||||
const width = x2 - x1;
|
const width = x2 - x1;
|
||||||
const height = y2 - y1;
|
const height = y2 - y1;
|
||||||
const threshold = maxBindingGap(element, width, height);
|
const thickness = 10;
|
||||||
|
|
||||||
// So that we don't overlap the element itself
|
// So that we don't overlap the element itself
|
||||||
const strokeOffset = 4;
|
const strokeOffset = 4;
|
||||||
context.strokeStyle = "rgba(0,0,0,.05)";
|
context.strokeStyle = "rgba(0,0,0,.05)";
|
||||||
context.lineWidth = threshold - strokeOffset;
|
context.lineWidth = thickness - strokeOffset;
|
||||||
const padding = strokeOffset / 2 + threshold / 2;
|
const padding = strokeOffset / 2 + thickness / 2;
|
||||||
|
|
||||||
|
const radius = getCornerRadius(
|
||||||
|
Math.min(element.width, element.height),
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
@@ -237,6 +245,8 @@ const renderBindingHighlightForBindableElement = (
|
|||||||
x1 + width / 2,
|
x1 + width / 2,
|
||||||
y1 + height / 2,
|
y1 + height / 2,
|
||||||
element.angle,
|
element.angle,
|
||||||
|
undefined,
|
||||||
|
radius,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "diamond":
|
case "diamond":
|
||||||
@@ -474,6 +484,10 @@ const renderLinearPointHandles = (
|
|||||||
? POINT_HANDLE_SIZE
|
? POINT_HANDLE_SIZE
|
||||||
: POINT_HANDLE_SIZE / 2;
|
: POINT_HANDLE_SIZE / 2;
|
||||||
points.forEach((point, idx) => {
|
points.forEach((point, idx) => {
|
||||||
|
if (isElbowArrow(element) && idx !== 0 && idx !== points.length - 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isSelected =
|
const isSelected =
|
||||||
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
!!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
|
||||||
|
|
||||||
@@ -727,7 +741,13 @@ const _renderInteractiveScene = ({
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
appState.selectedLinearElement &&
|
appState.selectedLinearElement &&
|
||||||
appState.selectedLinearElement.hoverPointIndex >= 0
|
appState.selectedLinearElement.hoverPointIndex >= 0 &&
|
||||||
|
!(
|
||||||
|
isElbowArrow(selectedElements[0]) &&
|
||||||
|
appState.selectedLinearElement.hoverPointIndex > 0 &&
|
||||||
|
appState.selectedLinearElement.hoverPointIndex <
|
||||||
|
selectedElements[0].points.length - 1
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
renderLinearElementPointHighlight(context, appState, elementsMap);
|
renderLinearElementPointHighlight(context, appState, elementsMap);
|
||||||
}
|
}
|
||||||
@@ -771,27 +791,39 @@ const _renderInteractiveScene = ({
|
|||||||
|
|
||||||
for (const element of elementsMap.values()) {
|
for (const element of elementsMap.values()) {
|
||||||
const selectionColors = [];
|
const selectionColors = [];
|
||||||
// local user
|
|
||||||
if (
|
|
||||||
locallySelectedIds.has(element.id) &&
|
|
||||||
!isSelectedViaGroup(appState, element)
|
|
||||||
) {
|
|
||||||
selectionColors.push(selectionColor);
|
|
||||||
}
|
|
||||||
// remote users
|
|
||||||
const remoteClients = renderConfig.remoteSelectedElementIds.get(
|
const remoteClients = renderConfig.remoteSelectedElementIds.get(
|
||||||
element.id,
|
element.id,
|
||||||
);
|
);
|
||||||
if (remoteClients) {
|
if (
|
||||||
selectionColors.push(
|
!(
|
||||||
...remoteClients.map((socketId) => {
|
// Elbow arrow elements cannot be selected when bound on either end
|
||||||
const background = getClientColor(
|
(
|
||||||
socketId,
|
isSingleLinearElementSelected &&
|
||||||
appState.collaborators.get(socketId),
|
isArrowElement(element) &&
|
||||||
);
|
isElbowArrow(element) &&
|
||||||
return background;
|
(element.startBinding || element.endBinding)
|
||||||
}),
|
)
|
||||||
);
|
)
|
||||||
|
) {
|
||||||
|
// local user
|
||||||
|
if (
|
||||||
|
locallySelectedIds.has(element.id) &&
|
||||||
|
!isSelectedViaGroup(appState, element)
|
||||||
|
) {
|
||||||
|
selectionColors.push(selectionColor);
|
||||||
|
}
|
||||||
|
// remote users
|
||||||
|
if (remoteClients) {
|
||||||
|
selectionColors.push(
|
||||||
|
...remoteClients.map((socketId) => {
|
||||||
|
const background = getClientColor(
|
||||||
|
socketId,
|
||||||
|
appState.collaborators.get(socketId),
|
||||||
|
);
|
||||||
|
return background;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectionColors.length) {
|
if (selectionColors.length) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import throttle from "lodash.throttle";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
@@ -50,6 +51,24 @@ const getNonDeletedElements = <T extends ExcalidrawElement>(
|
|||||||
return { elementsMap, elements };
|
return { elementsMap, elements };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validateIndicesThrottled = throttle(
|
||||||
|
(elements: readonly ExcalidrawElement[]) => {
|
||||||
|
if (
|
||||||
|
import.meta.env.DEV ||
|
||||||
|
import.meta.env.MODE === ENV.TEST ||
|
||||||
|
window?.DEBUG_FRACTIONAL_INDICES
|
||||||
|
) {
|
||||||
|
validateFractionalIndices(elements, {
|
||||||
|
// throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
|
||||||
|
shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
|
||||||
|
includeBoundTextValidation: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
1000 * 60,
|
||||||
|
{ leading: true, trailing: false },
|
||||||
|
);
|
||||||
|
|
||||||
const hashSelectionOpts = (
|
const hashSelectionOpts = (
|
||||||
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
|
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
|
||||||
) => {
|
) => {
|
||||||
@@ -274,10 +293,7 @@ class Scene {
|
|||||||
: Array.from(nextElements.values());
|
: Array.from(nextElements.values());
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
if (import.meta.env.DEV || import.meta.env.MODE === ENV.TEST) {
|
validateIndicesThrottled(_nextElements);
|
||||||
// throw on invalid indices in test / dev to potentially detect cases were we forgot to sync moved elements
|
|
||||||
validateFractionalIndices(_nextElements.map((x) => x.index));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(_nextElements);
|
this.elements = syncInvalidIndices(_nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ import type {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
Arrowhead,
|
Arrowhead,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { isPathALoop, getCornerRadius } from "../math";
|
import { isPathALoop, getCornerRadius, distanceSq2d } from "../math";
|
||||||
import { generateFreeDrawShape } from "../renderer/renderElement";
|
import { generateFreeDrawShape } from "../renderer/renderElement";
|
||||||
import { isTransparent, assertNever } from "../utils";
|
import { isTransparent, assertNever } from "../utils";
|
||||||
import { simplify } from "points-on-curve";
|
import { simplify } from "points-on-curve";
|
||||||
import { ROUGHNESS } from "../constants";
|
import { ROUGHNESS } from "../constants";
|
||||||
import {
|
import {
|
||||||
|
isElbowArrow,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
isIframeElement,
|
isIframeElement,
|
||||||
isIframeLikeElement,
|
isIframeLikeElement,
|
||||||
@@ -400,9 +401,16 @@ export const _generateElementShape = (
|
|||||||
// initial position to it
|
// initial position to it
|
||||||
const points = element.points.length ? element.points : [[0, 0]];
|
const points = element.points.length ? element.points : [[0, 0]];
|
||||||
|
|
||||||
// curve is always the first element
|
if (isElbowArrow(element)) {
|
||||||
// this simplifies finding the curve for an element
|
shape = [
|
||||||
if (!element.roundness) {
|
generator.path(
|
||||||
|
generateElbowArrowShape(points as [number, number][], 16),
|
||||||
|
generateRoughOptions(element, true),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
} else if (!element.roundness) {
|
||||||
|
// curve is always the first element
|
||||||
|
// this simplifies finding the curve for an element
|
||||||
if (options.fill) {
|
if (options.fill) {
|
||||||
shape = [generator.polygon(points as [number, number][], options)];
|
shape = [generator.polygon(points as [number, number][], options)];
|
||||||
} else {
|
} else {
|
||||||
@@ -482,3 +490,60 @@ export const _generateElementShape = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateElbowArrowShape = (
|
||||||
|
points: [number, number][],
|
||||||
|
radius: number,
|
||||||
|
) => {
|
||||||
|
const subpoints = [] as [number, number][];
|
||||||
|
for (let i = 1; i < points.length - 1; i += 1) {
|
||||||
|
const prev = points[i - 1];
|
||||||
|
const next = points[i + 1];
|
||||||
|
const corner = Math.min(
|
||||||
|
radius,
|
||||||
|
Math.sqrt(distanceSq2d(points[i], next)) / 2,
|
||||||
|
Math.sqrt(distanceSq2d(points[i], prev)) / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (prev[0] < points[i][0] && prev[1] === points[i][1]) {
|
||||||
|
// LEFT
|
||||||
|
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||||
|
} else if (prev[0] === points[i][0] && prev[1] < points[i][1]) {
|
||||||
|
// UP
|
||||||
|
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||||
|
} else if (prev[0] > points[i][0] && prev[1] === points[i][1]) {
|
||||||
|
// RIGHT
|
||||||
|
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||||
|
} else {
|
||||||
|
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||||
|
}
|
||||||
|
|
||||||
|
subpoints.push(points[i] as [number, number]);
|
||||||
|
|
||||||
|
if (next[0] < points[i][0] && next[1] === points[i][1]) {
|
||||||
|
// LEFT
|
||||||
|
subpoints.push([points[i][0] - corner, points[i][1]]);
|
||||||
|
} else if (next[0] === points[i][0] && next[1] < points[i][1]) {
|
||||||
|
// UP
|
||||||
|
subpoints.push([points[i][0], points[i][1] - corner]);
|
||||||
|
} else if (next[0] > points[i][0] && next[1] === points[i][1]) {
|
||||||
|
// RIGHT
|
||||||
|
subpoints.push([points[i][0] + corner, points[i][1]]);
|
||||||
|
} else {
|
||||||
|
subpoints.push([points[i][0], points[i][1] + corner]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const d = [`M ${points[0][0]} ${points[0][1]}`];
|
||||||
|
for (let i = 0; i < subpoints.length; i += 3) {
|
||||||
|
d.push(`L ${subpoints[i][0]} ${subpoints[i][1]}`);
|
||||||
|
d.push(
|
||||||
|
`Q ${subpoints[i + 1][0]} ${subpoints[i + 1][1]}, ${
|
||||||
|
subpoints[i + 2][0]
|
||||||
|
} ${subpoints[i + 2][1]}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
d.push(`L ${points[points.length - 1][0]} ${points[points.length - 1][1]}`);
|
||||||
|
|
||||||
|
return d.join(" ");
|
||||||
|
};
|
||||||
|
|||||||
@@ -40,11 +40,12 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
|||||||
type === "rectangle" ||
|
type === "rectangle" ||
|
||||||
type === "iframe" ||
|
type === "iframe" ||
|
||||||
type === "embeddable" ||
|
type === "embeddable" ||
|
||||||
type === "arrow" ||
|
|
||||||
type === "line" ||
|
type === "line" ||
|
||||||
type === "diamond" ||
|
type === "diamond" ||
|
||||||
type === "image";
|
type === "image";
|
||||||
|
|
||||||
|
export const toolIsArrow = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|
||||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||||
|
|
||||||
export const getElementAtPosition = (
|
export const getElementAtPosition = (
|
||||||
|
|||||||
@@ -34,16 +34,11 @@ import {
|
|||||||
import { newTextElement } from "../element";
|
import { newTextElement } from "../element";
|
||||||
import { type Mutable } from "../utility-types";
|
import { type Mutable } from "../utility-types";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import {
|
import { isFrameLikeElement, isTextElement } from "../element/typeChecks";
|
||||||
isFrameElement,
|
|
||||||
isFrameLikeElement,
|
|
||||||
isTextElement,
|
|
||||||
} from "../element/typeChecks";
|
|
||||||
import type { RenderableElementsMap } from "./types";
|
import type { RenderableElementsMap } from "./types";
|
||||||
import { syncInvalidIndices } from "../fractionalIndex";
|
import { syncInvalidIndices } from "../fractionalIndex";
|
||||||
import { renderStaticScene } from "../renderer/staticScene";
|
import { renderStaticScene } from "../renderer/staticScene";
|
||||||
import { Fonts } from "../fonts";
|
import { Fonts } from "../fonts";
|
||||||
import { LOCAL_FONT_PROTOCOL } from "../fonts/metadata";
|
|
||||||
|
|
||||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||||
|
|
||||||
@@ -89,15 +84,8 @@ const addFrameLabelsAsTextElements = (
|
|||||||
opts: Pick<AppState, "exportWithDarkMode">,
|
opts: Pick<AppState, "exportWithDarkMode">,
|
||||||
) => {
|
) => {
|
||||||
const nextElements: NonDeletedExcalidrawElement[] = [];
|
const nextElements: NonDeletedExcalidrawElement[] = [];
|
||||||
let frameIndex = 0;
|
|
||||||
let magicFrameIndex = 0;
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
if (isFrameLikeElement(element)) {
|
if (isFrameLikeElement(element)) {
|
||||||
if (isFrameElement(element)) {
|
|
||||||
frameIndex++;
|
|
||||||
} else {
|
|
||||||
magicFrameIndex++;
|
|
||||||
}
|
|
||||||
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
let textElement: Mutable<ExcalidrawTextElement> = newTextElement({
|
||||||
x: element.x,
|
x: element.x,
|
||||||
y: element.y - FRAME_STYLE.nameOffsetY,
|
y: element.y - FRAME_STYLE.nameOffsetY,
|
||||||
@@ -108,10 +96,7 @@ const addFrameLabelsAsTextElements = (
|
|||||||
strokeColor: opts.exportWithDarkMode
|
strokeColor: opts.exportWithDarkMode
|
||||||
? FRAME_STYLE.nameColorDarkTheme
|
? FRAME_STYLE.nameColorDarkTheme
|
||||||
: FRAME_STYLE.nameColorLightTheme,
|
: FRAME_STYLE.nameColorLightTheme,
|
||||||
text: getFrameLikeTitle(
|
text: getFrameLikeTitle(element),
|
||||||
element,
|
|
||||||
isFrameElement(element) ? frameIndex : magicFrameIndex,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
textElement.y -= textElement.height;
|
textElement.y -= textElement.height;
|
||||||
|
|
||||||
@@ -188,7 +173,13 @@ export const exportToCanvas = async (
|
|||||||
canvas.height = height * appState.exportScale;
|
canvas.height = height * appState.exportScale;
|
||||||
return { canvas, scale: appState.exportScale };
|
return { canvas, scale: appState.exportScale };
|
||||||
},
|
},
|
||||||
|
loadFonts: () => Promise<void> = async () => {
|
||||||
|
await Fonts.loadFontsForElements(elements);
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
|
// load font faces before continuing, by default leverages browsers' [FontFace API](https://developer.mozilla.org/en-US/docs/Web/API/FontFace)
|
||||||
|
await loadFonts();
|
||||||
|
|
||||||
const frameRendering = getFrameRenderingConfig(
|
const frameRendering = getFrameRenderingConfig(
|
||||||
exportingFrame ?? null,
|
exportingFrame ?? null,
|
||||||
appState.frameRendering ?? null,
|
appState.frameRendering ?? null,
|
||||||
@@ -363,57 +354,14 @@ export const exportToSvg = async (
|
|||||||
</clipPath>`;
|
</clipPath>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontFamilies = elements.reduce((acc, element) => {
|
const fontFaces = opts?.skipInliningFonts ? [] : await getFontFaces(elements);
|
||||||
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 { fontFaces } = Fonts.registered.get(x) ?? {};
|
|
||||||
|
|
||||||
if (!Array.isArray(fontFaces)) {
|
|
||||||
console.error(
|
|
||||||
`Couldn't find registered font-faces for font-family "${x}"`,
|
|
||||||
Fonts.registered,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.all(
|
|
||||||
fontFaces
|
|
||||||
.filter((font) => font.url.protocol !== LOCAL_FONT_PROTOCOL)
|
|
||||||
.map(async (font) => {
|
|
||||||
try {
|
|
||||||
const content = await font.getContent();
|
|
||||||
|
|
||||||
return `@font-face {
|
|
||||||
font-family: ${font.fontFace.family};
|
|
||||||
src: url(${content});
|
|
||||||
}`;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(
|
|
||||||
`Skipped inlining font with URL "${font.url.toString()}"`,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
svgRoot.innerHTML = `
|
svgRoot.innerHTML = `
|
||||||
${SVG_EXPORT_TAG}
|
${SVG_EXPORT_TAG}
|
||||||
${metadata}
|
${metadata}
|
||||||
<defs>
|
<defs>
|
||||||
<style class="style-fonts">
|
<style class="style-fonts">
|
||||||
${fontFaces.flat().filter(Boolean).join("\n")}
|
${fontFaces.join("\n")}
|
||||||
</style>
|
</style>
|
||||||
${exportingFrameClipPath}
|
${exportingFrameClipPath}
|
||||||
</defs>
|
</defs>
|
||||||
@@ -484,3 +432,56 @@ export const getExportSize = (
|
|||||||
|
|
||||||
return [width, height];
|
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);
|
||||||
|
|
||||||
|
for (const codePoint of Array.from(element.originalText, (u) =>
|
||||||
|
u.codePointAt(0),
|
||||||
|
)) {
|
||||||
|
if (codePoint) {
|
||||||
|
codePoints.add(codePoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 font.getContent(codePoints)});
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return fontFaces.flat();
|
||||||
|
};
|
||||||
|
|||||||
@@ -1209,16 +1209,14 @@ export const snapResizingElements = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const snapNewElement = (
|
export const snapNewElement = (
|
||||||
draggingElement: ExcalidrawElement,
|
newElement: ExcalidrawElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
event: KeyboardModifiersObject,
|
event: KeyboardModifiersObject,
|
||||||
origin: Vector2D,
|
origin: Vector2D,
|
||||||
dragOffset: Vector2D,
|
dragOffset: Vector2D,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
if (
|
if (!isSnappingEnabled({ event, selectedElements: [newElement], appState })) {
|
||||||
!isSnappingEnabled({ event, selectedElements: [draggingElement], appState })
|
|
||||||
) {
|
|
||||||
return {
|
return {
|
||||||
snapOffset: { x: 0, y: 0 },
|
snapOffset: { x: 0, y: 0 },
|
||||||
snapLines: [],
|
snapLines: [],
|
||||||
@@ -1240,7 +1238,7 @@ export const snapNewElement = (
|
|||||||
const nearestSnapsY: Snaps = [];
|
const nearestSnapsY: Snaps = [];
|
||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
[draggingElement],
|
[newElement],
|
||||||
selectionSnapPoints,
|
selectionSnapPoints,
|
||||||
appState,
|
appState,
|
||||||
event,
|
event,
|
||||||
@@ -1259,13 +1257,13 @@ export const snapNewElement = (
|
|||||||
nearestSnapsX.length = 0;
|
nearestSnapsX.length = 0;
|
||||||
nearestSnapsY.length = 0;
|
nearestSnapsY.length = 0;
|
||||||
|
|
||||||
const corners = getElementsCorners([draggingElement], elementsMap, {
|
const corners = getElementsCorners([newElement], elementsMap, {
|
||||||
boundingBoxCorners: true,
|
boundingBoxCorners: true,
|
||||||
omitCenter: true,
|
omitCenter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
[draggingElement],
|
[newElement],
|
||||||
corners,
|
corners,
|
||||||
appState,
|
appState,
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import * as StaticScene from "../renderer/staticScene";
|
import * as StaticScene from "../renderer/staticScene";
|
||||||
import { reseed } from "../random";
|
import { reseed } from "../random";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { act, render, waitFor } from "./test-utils";
|
import React from "react";
|
||||||
|
import { render, waitFor } from "./test-utils";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { expect } from "vitest";
|
import { expect } from "vitest";
|
||||||
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
import { getTextEditor, updateTextEditor } from "./queries/dom";
|
||||||
@@ -89,7 +90,7 @@ describe("Test <MermaidToExcalidraw/>", () => {
|
|||||||
|
|
||||||
it("should open mermaid popup when active tool is mermaid", async () => {
|
it("should open mermaid popup when active tool is mermaid", async () => {
|
||||||
const dialog = document.querySelector(".ttd-dialog")!;
|
const dialog = document.querySelector(".ttd-dialog")!;
|
||||||
await waitFor(() => dialog.querySelector("canvas"));
|
await waitFor(() => expect(dialog.querySelector("canvas")).not.toBeNull());
|
||||||
expect(dialog.outerHTML).toMatchSnapshot();
|
expect(dialog.outerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,19 +104,9 @@ describe("Test <MermaidToExcalidraw/>", () => {
|
|||||||
|
|
||||||
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
|
expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull();
|
||||||
|
|
||||||
expect(editor.textContent).toMatchInlineSnapshot(`
|
expect(editor.textContent).toMatchSnapshot();
|
||||||
"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]"
|
|
||||||
`);
|
|
||||||
|
|
||||||
await act(async () => {
|
updateTextEditor(editor, "flowchart TD1");
|
||||||
updateTextEditor(editor, "flowchart TD1");
|
|
||||||
await new Promise((cb) => setTimeout(cb, 0));
|
|
||||||
});
|
|
||||||
editor = await getTextEditor(selector, false);
|
editor = await getTextEditor(selector, false);
|
||||||
|
|
||||||
expect(editor.textContent).toBe("flowchart TD1");
|
expect(editor.textContent).toBe("flowchart TD1");
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
|
exports[`Test <MermaidToExcalidraw/> > should open mermaid popup when active tool is mermaid 1`] = `
|
||||||
"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style="animation-duration: 0s;"><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
|
"<div class="Modal Dialog ttd-dialog" role="dialog" aria-modal="true" aria-labelledby="dialog-title" data-prevent-outside-click="true"><div class="Modal__background"></div><div class="Modal__content" style="--max-width: 1200px;" tabindex="0"><div class="Island"><div class="Dialog__content"><div dir="ltr" data-orientation="horizontal" class="ttd-dialog-tabs-root"><p class="dialog-mermaid-title">Mermaid to Excalidraw</p><div data-state="active" data-orientation="horizontal" role="tabpanel" aria-labelledby="radix-:r0:-trigger-mermaid" id="radix-:r0:-content-mermaid" tabindex="0" class="ttd-dialog-content" style=""><div class="ttd-dialog-desc">Currently only <a href="https://mermaid.js.org/syntax/flowchart.html">Flowchart</a>,<a href="https://mermaid.js.org/syntax/sequenceDiagram.html"> Sequence, </a> and <a href="https://mermaid.js.org/syntax/classDiagram.html">Class </a>Diagrams are supported. The other types will be rendered as image in Excalidraw.</div><div class="ttd-dialog-panels"><div class="ttd-dialog-panel"><div class="ttd-dialog-panel__header"><label>Mermaid Syntax</label></div><textarea class="ttd-dialog-input" placeholder="Write Mermaid diagram defintion here...">flowchart TD
|
||||||
A[Christmas] -->|Get money| B(Go shopping)
|
A[Christmas] -->|Get money| B(Go shopping)
|
||||||
B --> C{Let me think}
|
B --> C{Let me think}
|
||||||
C -->|One| D[Laptop]
|
C -->|One| D[Laptop]
|
||||||
C -->|Two| E[iPhone]
|
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>"
|
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]"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -796,6 +796,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -811,7 +812,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -840,6 +840,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -998,6 +999,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -1013,7 +1015,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -1042,6 +1043,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -1210,6 +1212,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -1225,7 +1228,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -1254,6 +1256,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -1537,6 +1540,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -1552,7 +1556,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -1581,6 +1584,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -1864,6 +1868,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -1879,7 +1884,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -1908,6 +1912,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -2076,6 +2081,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -2091,7 +2097,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -2120,6 +2125,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -2312,6 +2318,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -2327,7 +2334,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -2356,6 +2362,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -2609,6 +2616,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -2624,7 +2632,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -2653,6 +2660,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -2974,6 +2982,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "#a5d8ff",
|
"currentItemBackgroundColor": "#a5d8ff",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "cross-hatch",
|
"currentItemFillStyle": "cross-hatch",
|
||||||
@@ -2989,7 +2998,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -3018,6 +3026,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -3445,6 +3454,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -3460,7 +3470,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -3489,6 +3498,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -3764,6 +3774,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -3779,7 +3790,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -3808,6 +3818,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -4083,6 +4094,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"contextMenu": null,
|
"contextMenu": null,
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -4098,7 +4110,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -4127,6 +4138,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -5265,6 +5277,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -5280,7 +5293,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -5309,6 +5321,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -6388,6 +6401,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -6403,7 +6417,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -6432,6 +6445,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -7319,6 +7333,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -7334,7 +7349,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -7363,6 +7377,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -8227,6 +8242,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -8242,7 +8258,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -8271,6 +8286,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
@@ -9117,6 +9133,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
},
|
},
|
||||||
"currentChartType": "bar",
|
"currentChartType": "bar",
|
||||||
"currentHoveredFontFamily": null,
|
"currentHoveredFontFamily": null,
|
||||||
|
"currentItemArrowType": "round",
|
||||||
"currentItemBackgroundColor": "transparent",
|
"currentItemBackgroundColor": "transparent",
|
||||||
"currentItemEndArrowhead": "arrow",
|
"currentItemEndArrowhead": "arrow",
|
||||||
"currentItemFillStyle": "solid",
|
"currentItemFillStyle": "solid",
|
||||||
@@ -9132,7 +9149,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"currentItemTextAlign": "left",
|
"currentItemTextAlign": "left",
|
||||||
"cursorButton": "up",
|
"cursorButton": "up",
|
||||||
"defaultSidebarDockedPreference": false,
|
"defaultSidebarDockedPreference": false,
|
||||||
"draggingElement": null,
|
|
||||||
"editingElement": null,
|
"editingElement": null,
|
||||||
"editingFrame": null,
|
"editingFrame": null,
|
||||||
"editingGroupId": null,
|
"editingGroupId": null,
|
||||||
@@ -9161,6 +9177,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"lastPointerDownWith": "mouse",
|
"lastPointerDownWith": "mouse",
|
||||||
"multiElement": null,
|
"multiElement": null,
|
||||||
"name": "Untitled-201933152653",
|
"name": "Untitled-201933152653",
|
||||||
|
"newElement": null,
|
||||||
"objectsSnapModeEnabled": false,
|
"objectsSnapModeEnabled": false,
|
||||||
"offsetLeft": 20,
|
"offsetLeft": 20,
|
||||||
"offsetTop": 10,
|
"offsetTop": 10,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,17 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// 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`] = `
|
exports[`Test Linear Elements > Test bound text element > should match styles for text editor 1`] = `
|
||||||
<textarea
|
<textarea
|
||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
@@ -10,3 +22,36 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
|||||||
wrap="off"
|
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"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
|
|||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1984422985,
|
"versionNonce": 745419401,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@@ -186,16 +186,18 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": {
|
"endBinding": {
|
||||||
"elementId": "id1",
|
"elementId": "id1",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": "-0.46667",
|
"focus": "-0.46667",
|
||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
"frameId": null,
|
"frameId": null,
|
||||||
"groupIds": [],
|
"groupIds": [],
|
||||||
"height": "81.48231",
|
"height": "81.47368",
|
||||||
"id": "id2",
|
"id": "id2",
|
||||||
"index": "a2",
|
"index": "a2",
|
||||||
"isDeleted": false,
|
"isDeleted": false,
|
||||||
@@ -210,7 +212,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
81,
|
81,
|
||||||
"81.48231",
|
"81.47368",
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
@@ -221,6 +223,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
|
"fixedPoint": null,
|
||||||
"focus": "-0.60000",
|
"focus": "-0.60000",
|
||||||
"gap": 10,
|
"gap": 10,
|
||||||
},
|
},
|
||||||
@@ -229,10 +232,10 @@ exports[`move element > rectangles with binding arrow 7`] = `
|
|||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 14,
|
"version": 11,
|
||||||
"versionNonce": 2066753033,
|
"versionNonce": 1996028265,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": "49.98179",
|
"y": 50,
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ exports[`select single element on the scene > arrow 1`] = `
|
|||||||
"backgroundColor": "transparent",
|
"backgroundColor": "transparent",
|
||||||
"boundElements": null,
|
"boundElements": null,
|
||||||
"customData": undefined,
|
"customData": undefined,
|
||||||
|
"elbowed": false,
|
||||||
"endArrowhead": "arrow",
|
"endArrowhead": "arrow",
|
||||||
"endBinding": null,
|
"endBinding": null,
|
||||||
"fillStyle": "solid",
|
"fillStyle": "solid",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
import { Excalidraw } from "../index";
|
import { Excalidraw } from "../index";
|
||||||
import { CODES } from "../keys";
|
import { CODES } from "../keys";
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user