Compare commits

..

58 Commits

Author SHA1 Message Date
zsviczian b8ae1d2486 setActiveTool - do not clear selectedElementIds so customData of image can be used & do not render when deferredText is empty 2023-09-26 21:37:13 +02:00
zsviczian 482178b7b6 lint 2023-09-24 19:24:04 +02:00
zsviczian 7da500fe0a store mermaid text in customData of image element 2023-09-24 19:14:49 +02:00
Aakansha Doshi b1d923db3a use common utils to update and get text editor 2023-09-22 18:59:22 +05:30
Aakansha Doshi 7f6789780d Add tests 2023-09-22 18:55:48 +05:30
Aakansha Doshi 17accffee3 enable test coverage in ui 2023-09-22 18:55:29 +05:30
Aakansha Doshi e1b0b559f5 better api 2023-09-22 11:22:39 +05:30
Aakansha Doshi 8654958aba use i18n 2023-09-21 18:18:44 +05:30
Aakansha Doshi 7c1889efc0 fix width of shapes toolbar for smaller screen size and also fix regression of mobile menu 2023-09-21 17:45:08 +05:30
Aakansha Doshi b3e8d6bbf0 upgrade mermaid-to-excalidraw 2023-09-21 12:47:44 +05:30
Aakansha Doshi 26481fbf43 Merge remote-tracking branch 'origin/master' into aakansha-mermaid 2023-09-21 12:34:47 +05:30
Aakansha Doshi b7a4dfb6ef use stable version of mermaid-to-excalidraw 2023-09-13 21:09:19 +05:30
Aakansha Doshi 8a153b822a Merge remote-tracking branch 'origin/master' into aakansha-mermaid 2023-09-13 21:08:50 +05:30
Aakansha Doshi 57da3768b1 upgrade mermaid-to-excalidraw 2023-09-13 20:12:11 +05:30
Aakansha Doshi db163ab89c use types from mermaid-to-excalidraw 2023-09-07 13:40:06 +05:30
Aakansha Doshi 3593cf6a14 upgrade mermaid-to-excalidraw to fix firefox issue 2023-09-07 12:02:33 +05:30
Aakansha Doshi ce6a9986e3 don't pass appState in deps as its not used 2023-09-06 19:43:02 +05:30
Aakansha Doshi c95369ace8 upgrade mermaid-to-excalidraw 2023-09-06 11:23:07 +05:30
Aakansha Doshi a8283b27c5 width auto 2023-09-05 11:25:46 +05:30
Aakansha Doshi 3ea7c7ffa0 fix mobile css 2023-09-05 11:01:19 +05:30
Aakansha Doshi beb2ceaa72 fix: show extra tools dropdown in mobile 2023-09-05 10:32:51 +05:30
Aakansha Doshi 0d49eb3e05 make design responsive 2023-09-04 13:59:47 +05:30
Aakansha Doshi de0f35454a remove only 2023-09-04 13:22:06 +05:30
Aakansha Doshi d393a1c048 Merge remote-tracking branch 'origin/master' into aakansha-mermaid 2023-09-04 13:15:24 +05:30
Aakansha Doshi 23fa44cf5e fix tests 2023-09-04 13:09:35 +05:30
Aakansha Doshi 9d22e84fc8 tweak text 2023-09-04 10:42:05 +05:30
Aakansha Doshi aa6b585b96 defer rendering the preview 2023-09-04 09:05:22 +05:30
Aakansha Doshi 06389f96b9 fix: Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates. 2023-09-04 08:49:50 +05:30
dwelle 9b1c88eed9 fix elements insert position and viewport centering 2023-09-01 14:49:26 +02:00
Aakansha Doshi d070760b2f fix arrow bindings in safari and firefox 2023-08-31 21:05:44 +05:30
Aakansha Doshi acd6db5304 make opts optional and use 100% zoom when inserting to canvas 2023-08-31 13:34:28 +05:30
Aakansha Doshi d203794f70 add option to transform viewport to scene coords in transform api 2023-08-31 11:08:10 +05:30
Aakansha Doshi c8fd157914 tweak 2023-08-30 20:32:56 +05:30
Aakansha Doshi b47d46699a regenerate ids when needed via programmatic api, this makes sure for mermaid diagrams ids are regenerated 2023-08-30 20:28:13 +05:30
Aakansha Doshi 10a5d18fa2 show a spinner unless mermaid loaded 2023-08-30 14:14:24 +05:30
Aakansha Doshi b1bd94c377 tweak design to remove scroll bar 2023-08-30 13:57:49 +05:30
Aakansha Doshi e4e2e044af fix 2023-08-30 13:29:52 +05:30
Aakansha Doshi d3db48e8f4 add example and docs link 2023-08-30 13:21:32 +05:30
Aakansha Doshi cef0f15158 design tweaks 2023-08-30 13:02:13 +05:30
Aakansha Doshi 742e4bc017 show mermaid error messgae react way 2023-08-30 08:58:27 +05:30
Aakansha Doshi a573fbbb5a show error in preview when error 2023-08-29 22:06:45 +05:30
Aakansha Doshi f696d36e03 reset preview when error 2023-08-29 21:59:46 +05:30
Aakansha Doshi 21a804a4e5 add a note in dialog 2023-08-29 21:36:13 +05:30
Aakansha Doshi c313a2cf42 add mermaid icon in dropdown 2023-08-29 21:20:08 +05:30
Aakansha Doshi 6b33ba1d04 fix undefined vertex issue 2023-08-29 16:56:57 +05:30
Aakansha Doshi fe9f89a0fc compute width, height correctly for arrows 2023-08-29 12:49:49 +05:30
Aakansha Doshi d1266280cb tweak design 2023-08-25 15:44:22 +05:30
Aakansha Doshi 41a0258fe1 load mermaid lazily 2023-08-24 13:43:41 +05:30
Aakansha Doshi 3346c72356 store canvas data in refs 2023-08-24 12:50:03 +05:30
Aakansha Doshi 6518e05cab persist mermaid data to storage 2023-08-24 12:29:33 +05:30
Aakansha Doshi f85f890b25 set elements as selected when inserted onto canvas 2023-08-23 16:24:37 +05:30
Aakansha Doshi 7b612bec5e center preview 2023-08-23 14:43:27 +05:30
Aakansha Doshi efdafc8659 fix markdown error by using named export 2023-08-22 21:40:25 +05:30
Aakansha Doshi 554913c58b fix webpack config 2023-08-21 21:31:53 +05:30
Aakansha Doshi 84059086a6 fix 2023-08-21 21:09:42 +05:30
Aakansha Doshi fec984895e allow mermaid syntax and export in preview 2023-08-21 20:52:01 +05:30
Aakansha Doshi c5514974b7 create mermaid to excal dialog 2023-08-17 14:12:57 +05:30
Aakansha Doshi 6a04ebc6db feat: integrate mermaidToExcalidraw 2023-08-17 12:24:00 +05:30
216 changed files with 4181 additions and 12803 deletions
+3
View File
@@ -0,0 +1,3 @@
## 2020-10-13
- Added ability to embed scene source into exported PNG/SVG files so you can import the scene from them (open via `Load` button or drag & drop). #2219
+1 -4
View File
@@ -25,9 +25,6 @@
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE"> <a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" /> <img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
</a> </a>
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" />
</a>
<a href="https://docs.excalidraw.com/docs/introduction/contributing"> <a href="https://docs.excalidraw.com/docs/introduction/contributing">
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /> <img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
</a> </a>
@@ -73,7 +70,7 @@ The Excalidraw editor (npm package) supports:
## Excalidraw.com ## Excalidraw.com
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app) is part of this repository as well, and the app features: The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
- 📡&nbsp;PWA support (works offline). - 📡&nbsp;PWA support (works offline).
- 🤼&nbsp;Real-time collaboration. - 🤼&nbsp;Real-time collaboration.
@@ -38,7 +38,6 @@ To render an item, its recommended to use `MainMenu.Item`.
| Prop | Type | Required | Default | Description | | Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- | | --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. | | `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
| `selected` | `boolean` | No | `false` | Whether item is active |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item | | `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item | | `icon` | `JSX.Element` | No | - | The icon used in the menu item |
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item | | `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
@@ -71,7 +70,6 @@ function App() {
| Prop | Type | Required | Default | Description | | Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- | | --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. | | `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
| `selected` | `boolean` | No | `false` | Whether item is active |
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. | | `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item | | `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item | | `icon` | `JSX.Element` | No | - | The icon used in the menu item |
@@ -23,7 +23,7 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
| ready | `boolean` | This is set to true once Excalidraw is rendered | | ready | `boolean` | This is set to true once Excalidraw is rendered |
| [readyPromise](#readypromise) | `function` | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readypromise) | | [readyPromise](#readypromise) | `function` | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readypromise) |
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData | | [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
| [updateLibrary](#updatelibrary) | `function` | updates the the library | | [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
| [addFiles](#addfiles) | `function` | add files data to the appState | | [addFiles](#addfiles) | `function` | add files data to the appState |
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. | | [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene | | [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
@@ -1,6 +1,6 @@
# Customizing Styles # Customizing Styles
Excalidraw uses CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors. Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector: Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
+2 -2
View File
@@ -2,7 +2,7 @@
### Does this package support collaboration ? ### Does this package support collaboration ?
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same. No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
### Turning off Aggressive Anti-Fingerprinting in Brave browser ### Turning off Aggressive Anti-Fingerprinting in Brave browser
@@ -18,7 +18,7 @@ We strongly recommend turning it off. You can follow the steps below on how to d
2. Once opened, look for **Aggressively Block Fingerprinting** 2. Once opened, look for **Aggressively Block Fingerprinting**
![Aggressive block fingerprinting](../../assets/aggressive-block-fingerprint.png) ![Aggresive block fingerprinting](../../assets/aggressive-block-fingerprint.png)
3. Switch to **Block Fingerprinting** 3. Switch to **Block Fingerprinting**
@@ -34,7 +34,7 @@ function App() {
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`. Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
The following workflow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon. The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
```jsx showLineNumbers ```jsx showLineNumbers
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
-22
View File
@@ -1,22 +0,0 @@
# Frames
## Ordering
Frames should be ordered where frame children come first, followed by the frame element itself:
```
[
other_element,
frame1_child1,
frame1_child2,
frame1,
other_element,
frame2_child1,
frame2_child2,
frame2,
other_element,
...
]
```
If not oredered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
+1 -1
View File
@@ -15,7 +15,7 @@ In case you want to pick up something from the roadmap, comment on that issue an
1. Run `yarn` to install dependencies 1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name` 1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run: > To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
> >
> ```bash > ```bash
> git remote add upstream https://github.com/excalidraw/excalidraw.git > git remote add upstream https://github.com/excalidraw/excalidraw.git
+1 -5
View File
@@ -23,11 +23,7 @@ const sidebars = {
}, },
items: ["introduction/development", "introduction/contributing"], items: ["introduction/development", "introduction/contributing"],
}, },
{ { type: "category", label: "Codebase", items: ["codebase/json-schema"] },
type: "category",
label: "Codebase",
items: ["codebase/json-schema", "codebase/frames"],
},
{ {
type: "category", type: "category",
label: "@excalidraw/excalidraw", label: "@excalidraw/excalidraw",
+1 -1
View File
@@ -15,7 +15,7 @@ const FeatureList = [
Svg: require("@site/static/img/undraw_blank_canvas.svg").default, Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
description: ( description: (
<> <>
Want to build your own app powered by Excalidraw but don't know where to Want to build your own app powered by Excalidraw by don't know where to
start? start?
</> </>
), ),
+11 -116
View File
@@ -145,14 +145,6 @@
dependencies: dependencies:
"@babel/highlight" "^7.18.6" "@babel/highlight" "^7.18.6"
"@babel/code-frame@^7.22.13":
version "7.22.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
dependencies:
"@babel/highlight" "^7.22.13"
chalk "^2.4.2"
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8": "@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8":
version "7.18.8" version "7.18.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
@@ -210,16 +202,6 @@
"@jridgewell/gen-mapping" "^0.3.2" "@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1" jsesc "^2.5.1"
"@babel/generator@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
dependencies:
"@babel/types" "^7.23.0"
"@jridgewell/gen-mapping" "^0.3.2"
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
"@babel/helper-annotate-as-pure@^7.18.6": "@babel/helper-annotate-as-pure@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@@ -283,11 +265,6 @@
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
"@babel/helper-environment-visitor@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
"@babel/helper-explode-assignable-expression@^7.18.6": "@babel/helper-explode-assignable-expression@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
@@ -303,14 +280,6 @@
"@babel/template" "^7.18.6" "@babel/template" "^7.18.6"
"@babel/types" "^7.18.9" "@babel/types" "^7.18.9"
"@babel/helper-function-name@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
dependencies:
"@babel/template" "^7.22.15"
"@babel/types" "^7.23.0"
"@babel/helper-hoist-variables@^7.18.6": "@babel/helper-hoist-variables@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
@@ -318,13 +287,6 @@
dependencies: dependencies:
"@babel/types" "^7.18.6" "@babel/types" "^7.18.6"
"@babel/helper-hoist-variables@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
dependencies:
"@babel/types" "^7.22.5"
"@babel/helper-member-expression-to-functions@^7.18.9": "@babel/helper-member-expression-to-functions@^7.18.9":
version "7.18.9" version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
@@ -412,28 +374,11 @@
dependencies: dependencies:
"@babel/types" "^7.18.6" "@babel/types" "^7.18.6"
"@babel/helper-split-export-declaration@^7.22.6":
version "7.22.6"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
dependencies:
"@babel/types" "^7.22.5"
"@babel/helper-string-parser@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
"@babel/helper-validator-identifier@^7.18.6": "@babel/helper-validator-identifier@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
"@babel/helper-validator-identifier@^7.22.20":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
"@babel/helper-validator-option@^7.18.6": "@babel/helper-validator-option@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
@@ -467,25 +412,11 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/highlight@^7.22.13":
version "7.22.20"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
dependencies:
"@babel/helper-validator-identifier" "^7.22.20"
chalk "^2.4.2"
js-tokens "^4.0.0"
"@babel/parser@^7.12.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.8", "@babel/parser@^7.18.9": "@babel/parser@^7.12.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.8", "@babel/parser@^7.18.9":
version "7.18.9" version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg== integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@@ -1216,28 +1147,19 @@
"@babel/parser" "^7.18.6" "@babel/parser" "^7.18.6"
"@babel/types" "^7.18.6" "@babel/types" "^7.18.6"
"@babel/template@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9": "@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
version "7.23.2" version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
dependencies: dependencies:
"@babel/code-frame" "^7.22.13" "@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.23.0" "@babel/generator" "^7.18.9"
"@babel/helper-environment-visitor" "^7.22.20" "@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.23.0" "@babel/helper-function-name" "^7.18.9"
"@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.23.0" "@babel/parser" "^7.18.9"
"@babel/types" "^7.23.0" "@babel/types" "^7.18.9"
debug "^4.1.0" debug "^4.1.0"
globals "^11.1.0" globals "^11.1.0"
@@ -1249,15 +1171,6 @@
"@babel/helper-validator-identifier" "^7.18.6" "@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
dependencies:
"@babel/helper-string-parser" "^7.22.5"
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
"@colors/colors@1.5.0": "@colors/colors@1.5.0":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -1757,11 +1670,6 @@
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": "@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
@@ -1780,19 +1688,6 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/sourcemap-codec@^1.4.14":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.17":
version "0.3.20"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9": "@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.14" version "0.3.14"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
@@ -80,8 +80,7 @@ export const ExportToExcalidrawPlus: React.FC<{
appState: Partial<AppState>; appState: Partial<AppState>;
files: BinaryFiles; files: BinaryFiles;
onError: (error: Error) => void; onError: (error: Error) => void;
onSuccess: () => void; }> = ({ elements, appState, files, onError }) => {
}> = ({ elements, appState, files, onError, onSuccess }) => {
const { t } = useI18n(); const { t } = useI18n();
return ( return (
<Card color="primary"> <Card color="primary">
@@ -108,7 +107,6 @@ export const ExportToExcalidrawPlus: React.FC<{
try { try {
trackEvent("export", "eplus", `ui (${getFrame()})`); trackEvent("export", "eplus", `ui (${getFrame()})`);
await exportToExcalidrawPlus(elements, appState, files); await exportToExcalidrawPlus(elements, appState, files);
onSuccess();
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
if (error.name !== "AbortError") { if (error.name !== "AbortError") {
+1 -1
View File
@@ -6,7 +6,7 @@
* *
* - DataState refers to full state of the app: appState, elements, images, * - DataState refers to full state of the app: appState, elements, images,
* though some state is saved separately (collab username, library) for one * though some state is saved separately (collab username, library) for one
* reason or another. We also save different data to different storage * reason or another. We also save different data to different sotrage
* (localStorage, indexedDB). * (localStorage, indexedDB).
*/ */
+1 -1
View File
@@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
type: "MOUSE_LOCATION"; type: "MOUSE_LOCATION";
payload: { payload: {
socketId: string; socketId: string;
pointer: { x: number; y: number; tool: "pointer" | "laser" }; pointer: { x: number; y: number };
button: "down" | "up"; button: "down" | "up";
selectedElementIds: AppState["selectedElementIds"]; selectedElementIds: AppState["selectedElementIds"];
username: string; username: string;
+3 -8
View File
@@ -608,7 +608,7 @@ const ExcalidrawWrapper = () => {
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
) => { ) => {
if (exportedElements.length === 0) { if (exportedElements.length === 0) {
throw new Error(t("alerts.cannotExportEmptyCanvas")); return window.alert(t("alerts.cannotExportEmptyCanvas"));
} }
if (canvas) { if (canvas) {
try { try {
@@ -624,7 +624,7 @@ const ExcalidrawWrapper = () => {
); );
if (errorMessage) { if (errorMessage) {
throw new Error(errorMessage); setErrorMessage(errorMessage);
} }
if (url) { if (url) {
@@ -634,7 +634,7 @@ const ExcalidrawWrapper = () => {
if (error.name !== "AbortError") { if (error.name !== "AbortError") {
const { width, height } = canvas; const { width, height } = canvas;
console.error(error, { width, height }); console.error(error, { width, height });
throw new Error(error.message); setErrorMessage(error.message);
} }
} }
} }
@@ -714,11 +714,6 @@ const ExcalidrawWrapper = () => {
}, },
}); });
}} }}
onSuccess={() => {
excalidrawAPI?.updateScene({
appState: { openDialog: null },
});
}}
/> />
); );
}, },
+3 -4
View File
@@ -20,8 +20,7 @@
}, },
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "6.0.2", "@braintree/sanitize-url": "6.0.2",
"@excalidraw/mermaid-to-excalidraw": "0.1.2", "@excalidraw/mermaid-to-excalidraw": "0.1.1",
"@excalidraw/laser-pointer": "1.2.0",
"@excalidraw/random-username": "1.0.0", "@excalidraw/random-username": "1.0.0",
"@radix-ui/react-popover": "1.0.3", "@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2", "@radix-ui/react-tabs": "1.0.2",
@@ -50,11 +49,11 @@
"png-chunk-text": "1.0.0", "png-chunk-text": "1.0.0",
"png-chunks-encode": "1.0.0", "png-chunks-encode": "1.0.0",
"png-chunks-extract": "1.0.0", "png-chunks-extract": "1.0.0",
"points-on-curve": "1.0.1", "points-on-curve": "0.2.0",
"pwacompat": "2.0.17", "pwacompat": "2.0.17",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"roughjs": "4.6.4", "roughjs": "4.5.2",
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.2" "tunnel-rat": "0.1.2"
+1 -1
View File
@@ -11,7 +11,7 @@
{ {
"src": "apple-touch-icon.png", "src": "apple-touch-icon.png",
"type": "image/png", "type": "image/png",
"sizes": "180x180" "sizes": "256x256"
} }
], ],
"start_url": "/", "start_url": "/",
+1 -2
View File
@@ -10,7 +10,7 @@ import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils"; import { getShortcutKey, setCursor, updateActiveTool } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { Tooltip } from "../components/Tooltip"; import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
@@ -21,7 +21,6 @@ import {
} from "../appState"; } from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { Bounds } from "../element/bounds"; import { Bounds } from "../element/bounds";
import { setCursor } from "../cursor";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor", name: "changeViewBackgroundColor",
+15 -62
View File
@@ -3,43 +3,33 @@ import { register } from "./register";
import { import {
copyTextToSystemClipboard, copyTextToSystemClipboard,
copyToClipboard, copyToClipboard,
createPasteEvent,
probablySupportsClipboardBlob, probablySupportsClipboardBlob,
probablySupportsClipboardWriteText, probablySupportsClipboardWriteText,
readSystemClipboard,
} from "../clipboard"; } from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected"; import { actionDeleteSelected } from "./actionDeleteSelected";
import { exportCanvas } from "../data/index"; import { exportCanvas } from "../data/index";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
import { t } from "../i18n"; import { t } from "../i18n";
import { isFirefox } from "../constants";
export const actionCopy = register({ export const actionCopy = register({
name: "copy", name: "copy",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: async (elements, appState, event: ClipboardEvent | null, app) => { perform: (elements, appState, _, app) => {
const elementsToCopy = app.scene.getSelectedElements({ const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds, selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}); });
try { copyToClipboard(elementsToCopy, app.files);
await copyToClipboard(elementsToCopy, app.files, event);
} catch (error: any) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: error.message,
},
};
}
return { return {
commitToHistory: false, commitToHistory: false,
}; };
}, },
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.copy", contextItemLabel: "labels.copy",
// don't supply a shortcut since we handle this conditionally via onCopy event // don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: undefined, keyTest: undefined,
@@ -48,55 +38,15 @@ export const actionCopy = register({
export const actionPaste = register({ export const actionPaste = register({
name: "paste", name: "paste",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: async (elements, appState, data, app) => { perform: (elements: any, appStates: any, data, app) => {
let types; app.pasteFromClipboard(null);
try {
types = await readSystemClipboard();
} catch (error: any) {
if (error.name === "AbortError" || error.name === "NotAllowedError") {
// user probably aborted the action. Though not 100% sure, it's best
// to not annoy them with an error message.
return false;
}
console.error(`actionPaste ${error.name}: ${error.message}`);
if (isFirefox) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: t("hints.firefox_clipboard_write"),
},
};
}
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: t("errors.asyncPasteFailedOnRead"),
},
};
}
try {
app.pasteFromClipboard(createPasteEvent({ types }));
} catch (error: any) {
console.error(error);
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: t("errors.asyncPasteFailedOnParse"),
},
};
}
return { return {
commitToHistory: false, commitToHistory: false,
}; };
}, },
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.paste", contextItemLabel: "labels.paste",
// don't supply a shortcut since we handle this conditionally via onCopy event // don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: undefined, keyTest: undefined,
@@ -105,10 +55,13 @@ export const actionPaste = register({
export const actionCut = register({ export const actionCut = register({
name: "cut", name: "cut",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, event: ClipboardEvent | null, app) => { perform: (elements, appState, data, app) => {
actionCopy.perform(elements, appState, event, app); actionCopy.perform(elements, appState, data, app);
return actionDeleteSelected.perform(elements, appState); return actionDeleteSelected.perform(elements, appState);
}, },
predicate: (elements, appState, appProps, app) => {
return app.device.isMobile && !!navigator.clipboard;
},
contextItemLabel: "labels.cut", contextItemLabel: "labels.cut",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
}); });
-1
View File
@@ -46,7 +46,6 @@ const deleteSelectedElements = (
appState: { appState: {
...appState, ...appState,
selectedElementIds: {}, selectedElementIds: {},
selectedGroupIds: {},
}, },
}; };
}; };
+1 -9
View File
@@ -191,15 +191,7 @@ export const actionSaveFileToDisk = register({
}, },
app.files, app.files,
); );
return { return { commitToHistory: false, appState: { ...appState, fileHandle } };
commitToHistory: false,
appState: {
...appState,
openDialog: null,
fileHandle,
toast: { message: t("toast.fileSaved") },
},
};
} catch (error: any) { } catch (error: any) {
if (error?.name !== "AbortError") { if (error?.name !== "AbortError") {
console.error(error); console.error(error);
+2 -5
View File
@@ -1,6 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element"; import { isInvisiblySmallElement } from "../element";
import { updateActiveTool } from "../utils"; import { updateActiveTool, resetCursor } from "../utils";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons"; import { done } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -15,7 +15,6 @@ import {
} from "../element/binding"; } from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks"; import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { AppState } from "../types"; import { AppState } from "../types";
import { resetCursor } from "../cursor";
export const actionFinalize = register({ export const actionFinalize = register({
name: "finalize", name: "finalize",
@@ -91,9 +90,7 @@ export const actionFinalize = register({
} }
} }
if (isInvisiblySmallElement(multiPointElement)) { if (isInvisiblySmallElement(multiPointElement)) {
newElements = newElements.filter( newElements = newElements.slice(0, -1);
(el) => el.id !== multiPointElement.id,
);
} }
// If the multi point line closes the loop, // If the multi point line closes the loop,
+1 -2
View File
@@ -4,8 +4,7 @@ import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame"; import { getFrameElements } from "../frame";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { updateActiveTool } from "../utils"; import { setCursorForShape, updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { register } from "./register"; import { register } from "./register";
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => { const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
-167
View File
@@ -1,167 +0,0 @@
import { Excalidraw } from "../packages/excalidraw/index";
import { queryByTestId } from "@testing-library/react";
import { render } from "../tests/test-utils";
import { UI } from "../tests/helpers/ui";
import { API } from "../tests/helpers/api";
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
const { h } = window;
describe("element locking", () => {
beforeEach(async () => {
await render(<Excalidraw />);
});
describe("properties when tool selected", () => {
it("should show active background top picks", () => {
UI.clickTool("rectangle");
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
// just in case we change it in the future
expect(color).not.toBe(COLOR_PALETTE.transparent);
h.setState({
currentItemBackgroundColor: color,
});
const activeColor = queryByTestId(
document.body,
`color-top-pick-${color}`,
);
expect(activeColor).toHaveClass("active");
});
it("should show fill style when background non-transparent", () => {
UI.clickTool("rectangle");
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
// just in case we change it in the future
expect(color).not.toBe(COLOR_PALETTE.transparent);
h.setState({
currentItemBackgroundColor: color,
currentItemFillStyle: "hachure",
});
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
expect(hachureFillButton).toHaveClass("active");
h.setState({
currentItemFillStyle: "solid",
});
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
expect(solidFillStyle).toHaveClass("active");
});
it("should not show fill style when background transparent", () => {
UI.clickTool("rectangle");
h.setState({
currentItemBackgroundColor: COLOR_PALETTE.transparent,
currentItemFillStyle: "hachure",
});
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
expect(hachureFillButton).toBe(null);
});
it("should show horizontal text align for text tool", () => {
UI.clickTool("text");
h.setState({
currentItemTextAlign: "right",
});
const centerTextAlign = queryByTestId(document.body, `align-right`);
expect(centerTextAlign).toBeChecked();
});
});
describe("properties when elements selected", () => {
it("should show active styles when single element selected", () => {
const rect = API.createElement({
type: "rectangle",
backgroundColor: "red",
fillStyle: "cross-hatch",
});
h.elements = [rect];
API.setSelectedElements([rect]);
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
expect(crossHatchButton).toHaveClass("active");
});
it("should not show fill style selected element's background is transparent", () => {
const rect = API.createElement({
type: "rectangle",
backgroundColor: COLOR_PALETTE.transparent,
fillStyle: "cross-hatch",
});
h.elements = [rect];
API.setSelectedElements([rect]);
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
expect(crossHatchButton).toBe(null);
});
it("should highlight common stroke width of selected elements", () => {
const rect1 = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.thin,
});
const rect2 = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.thin,
});
h.elements = [rect1, rect2];
API.setSelectedElements([rect1, rect2]);
const thinStrokeWidthButton = queryByTestId(
document.body,
`strokeWidth-thin`,
);
expect(thinStrokeWidthButton).toBeChecked();
});
it("should not highlight any stroke width button if no common style", () => {
const rect1 = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.thin,
});
const rect2 = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.bold,
});
h.elements = [rect1, rect2];
API.setSelectedElements([rect1, rect2]);
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
expect(
queryByTestId(document.body, `strokeWidth-thin`),
).not.toBeChecked();
expect(
queryByTestId(document.body, `strokeWidth-bold`),
).not.toBeChecked();
expect(
queryByTestId(document.body, `strokeWidth-extraBold`),
).not.toBeChecked();
});
it("should show properties of different element types when selected", () => {
const rect = API.createElement({
type: "rectangle",
strokeWidth: STROKE_WIDTH.bold,
});
const text = API.createElement({
type: "text",
fontFamily: FONT_FAMILY.Cascadia,
});
h.elements = [rect, text];
API.setSelectedElements([rect, text]);
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
});
});
});
+27 -84
View File
@@ -1,4 +1,4 @@
import { AppState, Primitive } from "../../src/types"; import { AppState } from "../../src/types";
import { import {
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS, DEFAULT_ELEMENT_BACKGROUND_PICKS,
@@ -51,7 +51,6 @@ import {
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
FONT_FAMILY, FONT_FAMILY,
ROUNDNESS, ROUNDNESS,
STROKE_WIDTH,
VERTICAL_ALIGN, VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import { import {
@@ -83,6 +82,7 @@ import { getLanguage, t } from "../i18n";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { randomInteger } from "../random"; import { randomInteger } from "../random";
import { import {
canChangeRoundness,
canHaveArrowheads, canHaveArrowheads,
getCommonAttributeOfSelectedElements, getCommonAttributeOfSelectedElements,
getSelectedElements, getSelectedElements,
@@ -118,44 +118,25 @@ export const changeProperty = (
}); });
}; };
export const getFormValue = function <T extends Primitive>( export const getFormValue = function <T>(
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
getAttribute: (element: ExcalidrawElement) => T, getAttribute: (element: ExcalidrawElement) => T,
isRelevantElement: true | ((element: ExcalidrawElement) => boolean), defaultValue: T,
defaultValue: T | ((isSomeElementSelected: boolean) => T),
): T { ): T {
const editingElement = appState.editingElement; const editingElement = appState.editingElement;
const nonDeletedElements = getNonDeletedElements(elements); const nonDeletedElements = getNonDeletedElements(elements);
return (
let ret: T | null = null; (editingElement && getAttribute(editingElement)) ??
(isSomeElementSelected(nonDeletedElements, appState)
if (editingElement) { ? getCommonAttributeOfSelectedElements(
ret = getAttribute(editingElement); nonDeletedElements,
}
if (!ret) {
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
if (hasSelection) {
ret =
getCommonAttributeOfSelectedElements(
isRelevantElement === true
? nonDeletedElements
: nonDeletedElements.filter((el) => isRelevantElement(el)),
appState, appState,
getAttribute, getAttribute,
) ?? )
(typeof defaultValue === "function" : defaultValue) ??
? defaultValue(true) defaultValue
: defaultValue); );
} else {
ret =
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
}
}
return ret;
}; };
const offsetElementAfterFontResize = ( const offsetElementAfterFontResize = (
@@ -266,7 +247,6 @@ export const actionChangeStrokeColor = register({
elements, elements,
appState, appState,
(element) => element.strokeColor, (element) => element.strokeColor,
true,
appState.currentItemStrokeColor, appState.currentItemStrokeColor,
)} )}
onChange={(color) => updateData({ currentItemStrokeColor: color })} onChange={(color) => updateData({ currentItemStrokeColor: color })}
@@ -309,7 +289,6 @@ export const actionChangeBackgroundColor = register({
elements, elements,
appState, appState,
(element) => element.backgroundColor, (element) => element.backgroundColor,
true,
appState.currentItemBackgroundColor, appState.currentItemBackgroundColor,
)} )}
onChange={(color) => updateData({ currentItemBackgroundColor: color })} onChange={(color) => updateData({ currentItemBackgroundColor: color })}
@@ -359,28 +338,23 @@ export const actionChangeFillStyle = register({
} (${getShortcutKey("Alt-Click")})`, } (${getShortcutKey("Alt-Click")})`,
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon, icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
active: allElementsZigZag ? true : undefined, active: allElementsZigZag ? true : undefined,
testId: `fill-hachure`,
}, },
{ {
value: "cross-hatch", value: "cross-hatch",
text: t("labels.crossHatch"), text: t("labels.crossHatch"),
icon: FillCrossHatchIcon, icon: FillCrossHatchIcon,
testId: `fill-cross-hatch`,
}, },
{ {
value: "solid", value: "solid",
text: t("labels.solid"), text: t("labels.solid"),
icon: FillSolidIcon, icon: FillSolidIcon,
testId: `fill-solid`,
}, },
]} ]}
value={getFormValue( value={getFormValue(
elements, elements,
appState, appState,
(element) => element.fillStyle, (element) => element.fillStyle,
(element) => element.hasOwnProperty("fillStyle"), appState.currentItemFillStyle,
(hasSelection) =>
hasSelection ? null : appState.currentItemFillStyle,
)} )}
onClick={(value, event) => { onClick={(value, event) => {
const nextValue = const nextValue =
@@ -419,31 +393,26 @@ export const actionChangeStrokeWidth = register({
group="stroke-width" group="stroke-width"
options={[ options={[
{ {
value: STROKE_WIDTH.thin, value: 1,
text: t("labels.thin"), text: t("labels.thin"),
icon: StrokeWidthBaseIcon, icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin",
}, },
{ {
value: STROKE_WIDTH.bold, value: 2,
text: t("labels.bold"), text: t("labels.bold"),
icon: StrokeWidthBoldIcon, icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold",
}, },
{ {
value: STROKE_WIDTH.extraBold, value: 4,
text: t("labels.extraBold"), text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon, icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold",
}, },
]} ]}
value={getFormValue( value={getFormValue(
elements, elements,
appState, appState,
(element) => element.strokeWidth, (element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"), appState.currentItemStrokeWidth,
(hasSelection) =>
hasSelection ? null : appState.currentItemStrokeWidth,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -492,9 +461,7 @@ export const actionChangeSloppiness = register({
elements, elements,
appState, appState,
(element) => element.roughness, (element) => element.roughness,
(element) => element.hasOwnProperty("roughness"), appState.currentItemRoughness,
(hasSelection) =>
hasSelection ? null : appState.currentItemRoughness,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -542,9 +509,7 @@ export const actionChangeStrokeStyle = register({
elements, elements,
appState, appState,
(element) => element.strokeStyle, (element) => element.strokeStyle,
(element) => element.hasOwnProperty("strokeStyle"), appState.currentItemStrokeStyle,
(hasSelection) =>
hasSelection ? null : appState.currentItemStrokeStyle,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -584,7 +549,6 @@ export const actionChangeOpacity = register({
elements, elements,
appState, appState,
(element) => element.opacity, (element) => element.opacity,
true,
appState.currentItemOpacity, appState.currentItemOpacity,
) ?? undefined ) ?? undefined
} }
@@ -643,12 +607,7 @@ export const actionChangeFontSize = register({
} }
return null; return null;
}, },
(element) => appState.currentItemFontSize || DEFAULT_FONT_SIZE,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) =>
hasSelection
? null
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -733,25 +692,21 @@ export const actionChangeFontFamily = register({
value: FontFamilyValues; value: FontFamilyValues;
text: string; text: string;
icon: JSX.Element; icon: JSX.Element;
testId: string;
}[] = [ }[] = [
{ {
value: FONT_FAMILY.Virgil, value: FONT_FAMILY.Virgil,
text: t("labels.handDrawn"), text: t("labels.handDrawn"),
icon: FreedrawIcon, icon: FreedrawIcon,
testId: "font-family-virgil",
}, },
{ {
value: FONT_FAMILY.Helvetica, value: FONT_FAMILY.Helvetica,
text: t("labels.normal"), text: t("labels.normal"),
icon: FontFamilyNormalIcon, icon: FontFamilyNormalIcon,
testId: "font-family-normal",
}, },
{ {
value: FONT_FAMILY.Cascadia, value: FONT_FAMILY.Cascadia,
text: t("labels.code"), text: t("labels.code"),
icon: FontFamilyCodeIcon, icon: FontFamilyCodeIcon,
testId: "font-family-code",
}, },
]; ];
@@ -774,12 +729,7 @@ export const actionChangeFontFamily = register({
} }
return null; return null;
}, },
(element) => appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) =>
hasSelection
? null
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -856,10 +806,7 @@ export const actionChangeTextAlign = register({
} }
return null; return null;
}, },
(element) => appState.currentItemTextAlign,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) =>
hasSelection ? null : appState.currentItemTextAlign,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -935,9 +882,7 @@ export const actionChangeVerticalAlign = register({
} }
return null; return null;
}, },
(element) => VERTICAL_ALIGN.MIDDLE,
isTextElement(element) || getBoundTextElement(element) !== null,
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -1002,9 +947,9 @@ export const actionChangeRoundness = register({
appState, appState,
(element) => (element) =>
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp", hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
(element) => element.hasOwnProperty("roundness"), (canChangeRoundness(appState.activeTool.type) &&
(hasSelection) => appState.currentItemRoundness) ||
hasSelection ? null : appState.currentItemRoundness, null,
)} )}
onChange={(value) => updateData(value)} onChange={(value) => updateData(value)}
/> />
@@ -1098,7 +1043,6 @@ export const actionChangeArrowhead = register({
isLinearElement(element) && canHaveArrowheads(element.type) isLinearElement(element) && canHaveArrowheads(element.type)
? element.startArrowhead ? element.startArrowhead
: appState.currentItemStartArrowhead, : appState.currentItemStartArrowhead,
true,
appState.currentItemStartArrowhead, appState.currentItemStartArrowhead,
)} )}
onChange={(value) => updateData({ position: "start", type: value })} onChange={(value) => updateData({ position: "start", type: value })}
@@ -1145,7 +1089,6 @@ export const actionChangeArrowhead = register({
isLinearElement(element) && canHaveArrowheads(element.type) isLinearElement(element) && canHaveArrowheads(element.type)
? element.endArrowhead ? element.endArrowhead
: appState.currentItemEndArrowhead, : appState.currentItemEndArrowhead,
true,
appState.currentItemEndArrowhead, appState.currentItemEndArrowhead,
)} )}
onChange={(value) => updateData({ position: "end", type: value })} onChange={(value) => updateData({ position: "end", type: value })}
-1
View File
@@ -15,7 +15,6 @@ export const actionToggleGridMode = register({
appState: { appState: {
...appState, ...appState,
gridSize: this.checked!(appState) ? null : GRID_SIZE, gridSize: this.checked!(appState) ? null : GRID_SIZE,
objectsSnapModeEnabled: false,
}, },
commitToHistory: false, commitToHistory: false,
}; };
@@ -1,28 +0,0 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
export const actionToggleObjectsSnapMode = register({
name: "objectsSnapMode",
viewMode: true,
trackEvent: {
category: "canvas",
predicate: (appState) => !appState.objectsSnapModeEnabled,
},
perform(elements, appState) {
return {
appState: {
...appState,
objectsSnapModeEnabled: !this.checked!(appState),
gridSize: null,
},
commitToHistory: false,
};
},
checked: (appState) => appState.objectsSnapModeEnabled,
predicate: (elements, appState, appProps) => {
return typeof appProps.objectsSnapModeEnabled === "undefined";
},
contextItemLabel: "buttons.objectsSnapMode",
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S,
});
-1
View File
@@ -80,7 +80,6 @@ export {
export { actionToggleGridMode } from "./actionToggleGridMode"; export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionUnbindText, actionBindText } from "./actionBoundText";
+3 -3
View File
@@ -119,10 +119,10 @@ export class ActionManager {
return true; return true;
} }
executeAction<T extends Action>( executeAction(
action: T, action: Action,
source: ActionSource = "api", source: ActionSource = "api",
value: Parameters<T["perform"]>[2] = null, value: any = null,
) { ) {
const elements = this.getElementsIncludingDeleted(); const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState(); const appState = this.getAppState();
-2
View File
@@ -28,7 +28,6 @@ export type ShortcutName =
| "ungroup" | "ungroup"
| "gridMode" | "gridMode"
| "zenMode" | "zenMode"
| "objectsSnapMode"
| "stats" | "stats"
| "addToLibrary" | "addToLibrary"
| "viewMode" | "viewMode"
@@ -75,7 +74,6 @@ const shortcutMap: Record<ShortcutName, string[]> = {
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")], ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
gridMode: [getShortcutKey("CtrlOrCmd+'")], gridMode: [getShortcutKey("CtrlOrCmd+'")],
zenMode: [getShortcutKey("Alt+Z")], zenMode: [getShortcutKey("Alt+Z")],
objectsSnapMode: [getShortcutKey("Alt+S")],
stats: [getShortcutKey("Alt+/")], stats: [getShortcutKey("Alt+/")],
addToLibrary: [], addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")], flipHorizontal: [getShortcutKey("Shift+H")],
-1
View File
@@ -51,7 +51,6 @@ export type ActionName =
| "pasteStyles" | "pasteStyles"
| "gridMode" | "gridMode"
| "zenMode" | "zenMode"
| "objectsSnapMode"
| "stats" | "stats"
| "changeStrokeColor" | "changeStrokeColor"
| "changeBackgroundColor" | "changeBackgroundColor"
-9
View File
@@ -99,12 +99,6 @@ export const getDefaultAppState = (): Omit<
pendingImageElementId: null, pendingImageElementId: null,
showHyperlinkPopup: false, showHyperlinkPopup: false,
selectedLinearElement: null, selectedLinearElement: null,
snapLines: [],
originSnapOffset: {
x: 0,
y: 0,
},
objectsSnapModeEnabled: false,
}; };
}; };
@@ -212,9 +206,6 @@ const APP_STATE_STORAGE_CONF = (<
pendingImageElementId: { browser: false, export: false, server: false }, pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false }, showHyperlinkPopup: { browser: false, export: false, server: false },
selectedLinearElement: { browser: true, export: false, server: false }, selectedLinearElement: { browser: true, export: false, server: false },
snapLines: { browser: false, export: false, server: false },
originSnapOffset: { browser: false, export: false, server: false },
objectsSnapModeEnabled: { browser: true, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <
+19 -188
View File
@@ -1,196 +1,27 @@
import { import { parseClipboard } from "./clipboard";
createPasteEvent,
parseClipboard,
serializeAsClipboardJSON,
} from "./clipboard";
import { API } from "./tests/helpers/api";
describe("parseClipboard()", () => { describe("Test parseClipboard", () => {
it("should parse JSON as plaintext if not excalidraw-api/clipboard data", async () => { it("should parse valid json correctly", async () => {
let text; let text = "123";
let clipboardData;
// ------------------------------------------------------------------------- let clipboardData = await parseClipboard({
//@ts-ignore
clipboardData: {
getData: () => text,
},
});
text = "123";
clipboardData = await parseClipboard(
createPasteEvent({ types: { "text/plain": text } }),
);
expect(clipboardData.text).toBe(text); expect(clipboardData.text).toBe(text);
// -------------------------------------------------------------------------
text = "[123]"; text = "[123]";
clipboardData = await parseClipboard(
createPasteEvent({ types: { "text/plain": text } }), clipboardData = await parseClipboard({
); //@ts-ignore
clipboardData: {
getData: () => text,
},
});
expect(clipboardData.text).toBe(text); expect(clipboardData.text).toBe(text);
// -------------------------------------------------------------------------
text = JSON.stringify({ val: 42 });
clipboardData = await parseClipboard(
createPasteEvent({ types: { "text/plain": text } }),
);
expect(clipboardData.text).toBe(text);
});
it("should parse valid excalidraw JSON if inside text/plain", async () => {
const rect = API.createElement({ type: "rectangle" });
const json = serializeAsClipboardJSON({ elements: [rect], files: null });
const clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/plain": json,
},
}),
);
expect(clipboardData.elements).toEqual([rect]);
});
it("should parse valid excalidraw JSON if inside text/html", async () => {
const rect = API.createElement({ type: "rectangle" });
let json;
let clipboardData;
// -------------------------------------------------------------------------
json = serializeAsClipboardJSON({ elements: [rect], files: null });
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": json,
},
}),
);
expect(clipboardData.elements).toEqual([rect]);
// -------------------------------------------------------------------------
json = serializeAsClipboardJSON({ elements: [rect], files: null });
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": `<div> ${json}</div>`,
},
}),
);
expect(clipboardData.elements).toEqual([rect]);
// -------------------------------------------------------------------------
});
it("should parse <image> `src` urls out of text/html", async () => {
let clipboardData;
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": `<img src="https://example.com/image.png" />`,
},
}),
);
expect(clipboardData.mixedContent).toEqual([
{
type: "imageUrl",
value: "https://example.com/image.png",
},
]);
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": `<div><img src="https://example.com/image.png" /></div><a><img src="https://example.com/image2.png" /></a>`,
},
}),
);
expect(clipboardData.mixedContent).toEqual([
{
type: "imageUrl",
value: "https://example.com/image.png",
},
{
type: "imageUrl",
value: "https://example.com/image2.png",
},
]);
});
it("should parse text content alongside <image> `src` urls out of text/html", async () => {
const clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": `<a href="https://example.com">hello </a><div><img src="https://example.com/image.png" /></div><b>my friend!</b>`,
},
}),
);
expect(clipboardData.mixedContent).toEqual([
{
type: "text",
// trimmed
value: "hello",
},
{
type: "imageUrl",
value: "https://example.com/image.png",
},
{
type: "text",
value: "my friend!",
},
]);
});
it("should parse spreadsheet from either text/plain and text/html", async () => {
let clipboardData;
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/plain": `a b
1 2
4 5
7 10`,
},
}),
);
expect(clipboardData.spreadsheet).toEqual({
title: "b",
labels: ["1", "4", "7"],
values: [2, 5, 10],
});
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": `a b
1 2
4 5
7 10`,
},
}),
);
expect(clipboardData.spreadsheet).toEqual({
title: "b",
labels: ["1", "4", "7"],
values: [2, 5, 10],
});
// -------------------------------------------------------------------------
clipboardData = await parseClipboard(
createPasteEvent({
types: {
"text/html": `<html>
<body>
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;a&quot;}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{&quot;1&quot;:2,&quot;2&quot;:&quot;b&quot;}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{&quot;1&quot;:3,&quot;3&quot;:10}">10</td></tr></tbody></table><!--EndFragment-->
</body>
</html>`,
"text/plain": `a b
1 2
4 5
7 10`,
},
}),
);
expect(clipboardData.spreadsheet).toEqual({
title: "b",
labels: ["1", "4", "7"],
values: [2, 5, 10],
});
}); });
}); });
+85 -257
View File
@@ -3,18 +3,14 @@ import {
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import { BinaryFiles } from "./types"; import { BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
ALLOWED_PASTE_MIME_TYPES,
EXPORT_DATA_TYPES,
MIME_TYPES,
} from "./constants";
import { isInitializedImageElement } from "./element/typeChecks"; import { isInitializedImageElement } from "./element/typeChecks";
import { deepCopyElement } from "./element/newElement"; import { deepCopyElement } from "./element/newElement";
import { mutateElement } from "./element/mutateElement"; import { mutateElement } from "./element/mutateElement";
import { getContainingFrame } from "./frame"; import { getContainingFrame } from "./frame";
import { isMemberOf, isPromiseLike } from "./utils"; import { isPromiseLike, isTestEnv } from "./utils";
import { t } from "./i18n";
type ElementsClipboard = { type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
@@ -22,23 +18,17 @@ type ElementsClipboard = {
files: BinaryFiles | undefined; files: BinaryFiles | undefined;
}; };
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
export interface ClipboardData { export interface ClipboardData {
spreadsheet?: Spreadsheet; spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[]; elements?: readonly ExcalidrawElement[];
files?: BinaryFiles; files?: BinaryFiles;
text?: string; text?: string;
mixedContent?: PastedMixedContent;
errorMessage?: string; errorMessage?: string;
programmaticAPI?: boolean; programmaticAPI?: boolean;
} }
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number]; let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false;
type ParsedClipboardEvent =
| { type: "text"; value: string }
| { type: "mixedContent"; value: PastedMixedContent };
export const probablySupportsClipboardReadText = export const probablySupportsClipboardReadText =
"clipboard" in navigator && "readText" in navigator.clipboard; "clipboard" in navigator && "readText" in navigator.clipboard;
@@ -68,61 +58,10 @@ const clipboardContainsElements = (
return false; return false;
}; };
export const createPasteEvent = ({ export const copyToClipboard = async (
types, elements: readonly NonDeletedExcalidrawElement[],
files, files: BinaryFiles | null,
}: { ) => {
types?: { [key in AllowedPasteMimeTypes]?: string };
files?: File[];
}) => {
if (!types && !files) {
console.warn("createPasteEvent: no types or files provided");
}
const event = new ClipboardEvent("paste", {
clipboardData: new DataTransfer(),
});
if (types) {
for (const [type, value] of Object.entries(types)) {
try {
event.clipboardData?.setData(type, value);
if (event.clipboardData?.getData(type) !== value) {
throw new Error(`Failed to set "${type}" as clipboardData item`);
}
} catch (error: any) {
throw new Error(error.message);
}
}
}
if (files) {
let idx = -1;
for (const file of files) {
idx++;
try {
event.clipboardData?.items.add(file);
if (event.clipboardData?.files[idx] !== file) {
throw new Error(
`Failed to set file "${file.name}" as clipboardData item`,
);
}
} catch (error: any) {
throw new Error(error.message);
}
}
}
return event;
};
export const serializeAsClipboardJSON = ({
elements,
files,
}: {
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles | null;
}) => {
const framesToCopy = new Set( const framesToCopy = new Set(
elements.filter((element) => element.type === "frame"), elements.filter((element) => element.type === "frame"),
); );
@@ -144,7 +83,7 @@ export const serializeAsClipboardJSON = ({
); );
} }
// select bound text elements when copying // select binded text elements when copying
const contents: ElementsClipboard = { const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard, type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: elements.map((element) => { elements: elements.map((element) => {
@@ -163,20 +102,34 @@ export const serializeAsClipboardJSON = ({
}), }),
files: files ? _files : undefined, files: files ? _files : undefined,
}; };
const json = JSON.stringify(contents);
return JSON.stringify(contents); if (isTestEnv()) {
return json;
}
CLIPBOARD = json;
try {
PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json);
} catch (error: any) {
PREFER_APP_CLIPBOARD = true;
console.error(error);
}
}; };
export const copyToClipboard = async ( const getAppClipboard = (): Partial<ElementsClipboard> => {
elements: readonly NonDeletedExcalidrawElement[], if (!CLIPBOARD) {
files: BinaryFiles | null, return {};
/** supply if available to make the operation more certain to succeed */ }
clipboardEvent?: ClipboardEvent | null,
) => { try {
await copyTextToSystemClipboard( return JSON.parse(CLIPBOARD);
serializeAsClipboardJSON({ elements, files }), } catch (error: any) {
clipboardEvent, console.error(error);
); return {};
}
}; };
const parsePotentialSpreadsheet = ( const parsePotentialSpreadsheet = (
@@ -189,137 +142,22 @@ const parsePotentialSpreadsheet = (
return null; return null;
}; };
/** internal, specific to parsing paste events. Do not reuse. */
function parseHTMLTree(el: ChildNode) {
let result: PastedMixedContent = [];
for (const node of el.childNodes) {
if (node.nodeType === 3) {
const text = node.textContent?.trim();
if (text) {
result.push({ type: "text", value: text });
}
} else if (node instanceof HTMLImageElement) {
const url = node.getAttribute("src");
if (url && url.startsWith("http")) {
result.push({ type: "imageUrl", value: url });
}
} else {
result = result.concat(parseHTMLTree(node));
}
}
return result;
}
const maybeParseHTMLPaste = (
event: ClipboardEvent,
): { type: "mixedContent"; value: PastedMixedContent } | null => {
const html = event.clipboardData?.getData("text/html");
if (!html) {
return null;
}
try {
const doc = new DOMParser().parseFromString(html, "text/html");
const content = parseHTMLTree(doc.body);
if (content.length) {
return { type: "mixedContent", value: content };
}
} catch (error: any) {
console.error(`error in parseHTMLFromPaste: ${error.message}`);
}
return null;
};
export const readSystemClipboard = async () => {
const types: { [key in AllowedPasteMimeTypes]?: string } = {};
try {
if (navigator.clipboard?.readText) {
return { "text/plain": await navigator.clipboard?.readText() };
}
} catch (error: any) {
// @ts-ignore
if (navigator.clipboard?.read) {
console.warn(
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
);
} else {
throw error;
}
}
let clipboardItems: ClipboardItems;
try {
clipboardItems = await navigator.clipboard?.read();
} catch (error: any) {
if (error.name === "DataError") {
console.warn(
`navigator.clipboard.read() error, clipboard is probably empty: ${error.message}`,
);
return types;
}
throw error;
}
for (const item of clipboardItems) {
for (const type of item.types) {
if (!isMemberOf(ALLOWED_PASTE_MIME_TYPES, type)) {
continue;
}
try {
types[type] = await (await item.getType(type)).text();
} catch (error: any) {
console.warn(
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
);
}
}
}
if (Object.keys(types).length === 0) {
console.warn("No clipboard data found from clipboard.read().");
return types;
}
return types;
};
/** /**
* Parses "paste" ClipboardEvent. * Retrieves content from system clipboard (either from ClipboardEvent or
* via async clipboard API if supported)
*/ */
const parseClipboardEvent = async ( export const getSystemClipboard = async (
event: ClipboardEvent, event: ClipboardEvent | null,
isPlainPaste = false, ): Promise<string> => {
): Promise<ParsedClipboardEvent> => {
try { try {
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event); const text = event
? event.clipboardData?.getData("text/plain")
: probablySupportsClipboardReadText &&
(await navigator.clipboard.readText());
if (mixedContent) { return (text || "").trim();
if (mixedContent.value.every((item) => item.type === "text")) {
return {
type: "text",
value:
event.clipboardData?.getData("text/plain") ||
mixedContent.value
.map((item) => item.value)
.join("\n")
.trim(),
};
}
return mixedContent;
}
const text = event.clipboardData?.getData("text/plain");
return { type: "text", value: (text || "").trim() };
} catch { } catch {
return { type: "text", value: "" }; return "";
} }
}; };
@@ -327,32 +165,34 @@ const parseClipboardEvent = async (
* Attempts to parse clipboard. Prefers system clipboard. * Attempts to parse clipboard. Prefers system clipboard.
*/ */
export const parseClipboard = async ( export const parseClipboard = async (
event: ClipboardEvent, event: ClipboardEvent | null,
isPlainPaste = false, isPlainPaste = false,
): Promise<ClipboardData> => { ): Promise<ClipboardData> => {
const parsedEventData = await parseClipboardEvent(event, isPlainPaste); const systemClipboard = await getSystemClipboard(event);
if (parsedEventData.type === "mixedContent") { // if system clipboard empty, couldn't be resolved, or contains previously
return { // copied excalidraw scene as SVG, fall back to previously copied excalidraw
mixedContent: parsedEventData.value, // elements
}; if (
!systemClipboard ||
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
) {
return getAppClipboard();
} }
try { // if system clipboard contains spreadsheet, use it even though it's
// if system clipboard contains spreadsheet, use it even though it's // technically possible it's staler than in-app clipboard
// technically possible it's staler than in-app clipboard const spreadsheetResult =
const spreadsheetResult = !isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
if (spreadsheetResult) { if (spreadsheetResult) {
return spreadsheetResult; return spreadsheetResult;
}
} catch (error: any) {
console.error(error);
} }
const appClipboardData = getAppClipboard();
try { try {
const systemClipboardData = JSON.parse(parsedEventData.value); const systemClipboardData = JSON.parse(systemClipboard);
const programmaticAPI = const programmaticAPI =
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI; systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
if (clipboardContainsElements(systemClipboardData)) { if (clipboardContainsElements(systemClipboardData)) {
@@ -365,9 +205,18 @@ export const parseClipboard = async (
programmaticAPI, programmaticAPI,
}; };
} }
} catch {} } catch (e) {}
// system clipboard doesn't contain excalidraw elements → return plaintext
return { text: parsedEventData.value }; // unless we set a flag to prefer in-app clipboard because browser didn't
// support storing to system clipboard on copy
return PREFER_APP_CLIPBOARD && appClipboardData.elements
? {
...appClipboardData,
text: isPlainPaste
? JSON.stringify(appClipboardData.elements, null, 2)
: undefined,
}
: { text: systemClipboard };
}; };
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => { export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
@@ -400,49 +249,28 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
} }
}; };
export const copyTextToSystemClipboard = async ( export const copyTextToSystemClipboard = async (text: string | null) => {
text: string | null, let copied = false;
clipboardEvent?: ClipboardEvent | null,
) => {
// (1) first try using Async Clipboard API
if (probablySupportsClipboardWriteText) { if (probablySupportsClipboardWriteText) {
try { try {
// NOTE: doesn't work on FF on non-HTTPS domains, or when document // NOTE: doesn't work on FF on non-HTTPS domains, or when document
// not focused // not focused
await navigator.clipboard.writeText(text || ""); await navigator.clipboard.writeText(text || "");
return; copied = true;
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
} }
} }
// (2) if fails and we have access to ClipboardEvent, use plain old setData() // Note that execCommand doesn't allow copying empty strings, so if we're
try { // clearing clipboard using this API, we must copy at least an empty char
if (clipboardEvent) { if (!copied && !copyTextViaExecCommand(text || " ")) {
clipboardEvent.clipboardData?.setData("text/plain", text || ""); throw new Error("couldn't copy");
if (clipboardEvent.clipboardData?.getData("text/plain") !== text) {
throw new Error("Failed to setData on clipboardEvent");
}
return;
}
} catch (error: any) {
console.error(error);
}
// (3) if that fails, use document.execCommand
if (!copyTextViaExecCommand(text)) {
throw new Error(t("errors.copyToSystemClipboardFailed"));
} }
}; };
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48 // adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
const copyTextViaExecCommand = (text: string | null) => { const copyTextViaExecCommand = (text: string) => {
// execCommand doesn't allow copying empty strings, so if we're
// clearing clipboard using this API, we must copy at least an empty char
if (!text) {
text = " ";
}
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const textarea = document.createElement("textarea"); const textarea = document.createElement("textarea");
+2 -2
View File
@@ -2,13 +2,13 @@
.undo-redo-buttons { .undo-redo-buttons {
background-color: var(--island-bg-color); background-color: var(--island-bg-color);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
box-shadow: 0 0 0 1px var(--color-surface-lowest);
} }
.zoom-button, .zoom-button,
.undo-redo-buttons button { .undo-redo-buttons button {
border: 1px solid var(--default-border-color) !important;
border-radius: 0 !important; border-radius: 0 !important;
background-color: var(--color-surface-low) !important; background-color: transparent !important;
font-size: 0.875rem !important; font-size: 0.875rem !important;
width: var(--lg-button-size); width: var(--lg-button-size);
height: var(--lg-button-size); height: var(--lg-button-size);
+68 -48
View File
@@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { import {
@@ -11,15 +11,21 @@ import {
hasBackground, hasBackground,
hasStrokeStyle, hasStrokeStyle,
hasStrokeWidth, hasStrokeWidth,
hasText,
} from "../scene"; } from "../scene";
import { SHAPES } from "../shapes"; import { SHAPES } from "../shapes";
import { AppClassProperties, UIAppState, Zoom } from "../types"; import { UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils"; import {
capitalizeString,
isTransparent,
updateActiveTool,
setCursorForShape,
} 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 } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { hasBoundTextElement, isTextElement } from "../element/typeChecks"; import { hasBoundTextElement } from "../element/typeChecks";
import clsx from "clsx"; import clsx from "clsx";
import { actionToggleZenMode } from "../actions"; import { actionToggleZenMode } from "../actions";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
@@ -35,7 +41,6 @@ import {
extraToolsIcon, extraToolsIcon,
frameToolIcon, frameToolIcon,
mermaidLogoIcon, mermaidLogoIcon,
laserPointerToolIcon,
} from "./icons"; } from "./icons";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@@ -66,8 +71,7 @@ export const SelectedShapeActions = ({
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons = const showFillIcons =
(hasBackground(appState.activeTool.type) && hasBackground(appState.activeTool.type) ||
!isTransparent(appState.currentItemBackgroundColor)) ||
targetElements.some( targetElements.some(
(element) => (element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor), hasBackground(element.type) && !isTransparent(element.backgroundColor),
@@ -124,15 +128,14 @@ export const SelectedShapeActions = ({
<>{renderAction("changeRoundness")}</> <>{renderAction("changeRoundness")}</>
)} )}
{(appState.activeTool.type === "text" || {(hasText(appState.activeTool.type) ||
targetElements.some(isTextElement)) && ( targetElements.some((element) => hasText(element.type))) && (
<> <>
{renderAction("changeFontSize")} {renderAction("changeFontSize")}
{renderAction("changeFontFamily")} {renderAction("changeFontFamily")}
{(appState.activeTool.type === "text" || {suppportsHorizontalAlign(targetElements) &&
suppportsHorizontalAlign(targetElements)) &&
renderAction("changeTextAlign")} renderAction("changeTextAlign")}
</> </>
)} )}
@@ -215,20 +218,19 @@ export const SelectedShapeActions = ({
}; };
export const ShapesSwitcher = ({ export const ShapesSwitcher = ({
interactiveCanvas,
activeTool, activeTool,
setAppState,
onImageAction,
appState, appState,
app,
}: { }: {
interactiveCanvas: HTMLCanvasElement | null;
activeTool: UIAppState["activeTool"]; activeTool: UIAppState["activeTool"];
setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: UIAppState; appState: UIAppState;
app: AppClassProperties;
}) => { }) => {
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false); const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
const frameToolSelected = activeTool.type === "frame";
const laserToolSelected = activeTool.type === "laser";
const embeddableToolSelected = activeTool.type === "embeddable";
return ( return (
<> <>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => { {SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
@@ -253,20 +255,31 @@ export const ShapesSwitcher = ({
data-testid={`toolbar-${value}`} data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => { onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") { if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true); setAppState({
penDetected: true,
penMode: true,
});
} }
}} }}
onChange={({ pointerType }) => { onChange={({ pointerType }) => {
if (appState.activeTool.type !== value) { if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui"); trackEvent("toolbar", value, "ui");
} }
const nextActiveTool = updateActiveTool(appState, {
type: value,
});
setAppState({
activeTool: nextActiveTool,
activeEmbeddable: null,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(interactiveCanvas, {
...appState,
activeTool: nextActiveTool,
});
if (value === "image") { if (value === "image") {
app.setActiveTool({ onImageAction({ pointerType });
type: value,
insertOnCanvasDirectly: pointerType !== "mouse",
});
} else {
app.setActiveTool({ type: value });
} }
}} }}
/> />
@@ -276,15 +289,7 @@ export const ShapesSwitcher = ({
<DropdownMenu open={isExtraToolsMenuOpen}> <DropdownMenu open={isExtraToolsMenuOpen}>
<DropdownMenu.Trigger <DropdownMenu.Trigger
className={clsx("App-toolbar__extra-tools-trigger", { className="App-toolbar__extra-tools-trigger"
"App-toolbar__extra-tools-trigger--selected":
frameToolSelected ||
embeddableToolSelected ||
// in collab we're already highlighting the laser button
// outside toolbar, so let's not highlight extra-tools button
// on top of it
(laserToolSelected && !app.props.isCollaborating),
})}
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)} onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
title={t("toolBar.extraTools")} title={t("toolBar.extraTools")}
> >
@@ -296,33 +301,48 @@ export const ShapesSwitcher = ({
className="App-toolbar__extra-tools-dropdown" className="App-toolbar__extra-tools-dropdown"
> >
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "frame" })} onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
icon={frameToolIcon} icon={frameToolIcon}
shortcut={KEYS.F.toLocaleUpperCase()} shortcut={KEYS.F.toLocaleUpperCase()}
data-testid="toolbar-frame" data-testid="toolbar-frame"
selected={frameToolSelected}
> >
{t("toolBar.frame")} {t("toolBar.frame")}
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "embeddable" })} onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
icon={EmbedIcon} icon={EmbedIcon}
data-testid="toolbar-embeddable" data-testid="toolbar-embeddable"
selected={embeddableToolSelected}
> >
{t("toolBar.embeddable")} {t("toolBar.embeddable")}
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
onSelect={() => app.setActiveTool({ type: "laser" })} onSelect={() => {
icon={laserPointerToolIcon} const nextActiveTool = updateActiveTool(appState, {
data-testid="toolbar-laser" type: "mermaid",
selected={laserToolSelected} });
shortcut={KEYS.K.toLocaleUpperCase()} setAppState({
> activeTool: nextActiveTool,
{t("toolBar.laser")} multiElement: null,
</DropdownMenu.Item> });
<DropdownMenu.Item }}
onSelect={() => app.setOpenDialog("mermaid")}
icon={mermaidLogoIcon} icon={mermaidLogoIcon}
data-testid="toolbar-embeddable" data-testid="toolbar-embeddable"
> >
+159 -538
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -55,7 +55,6 @@ export const TopPicks = ({
type="button" type="button"
title={color} title={color}
onClick={() => onChange(color)} onClick={() => onChange(color)}
data-testid={`color-top-pick-${color}`}
> >
<div className="color-picker__button-outline" /> <div className="color-picker__button-outline" />
</button> </button>
+9 -7
View File
@@ -9,7 +9,11 @@ import {
} from "../actions/shortcuts"; } from "../actions/shortcuts";
import { Action } from "../actions/types"; import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { useExcalidrawAppState, useExcalidrawElements } from "./App"; import {
useExcalidrawAppState,
useExcalidrawElements,
useExcalidrawSetAppState,
} from "./App";
import React from "react"; import React from "react";
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action; export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
@@ -21,14 +25,14 @@ type ContextMenuProps = {
items: ContextMenuItems; items: ContextMenuItems;
top: number; top: number;
left: number; left: number;
onClose: (callback?: () => void) => void;
}; };
export const CONTEXT_MENU_SEPARATOR = "separator"; export const CONTEXT_MENU_SEPARATOR = "separator";
export const ContextMenu = React.memo( export const ContextMenu = React.memo(
({ actionManager, items, top, left, onClose }: ContextMenuProps) => { ({ actionManager, items, top, left }: ContextMenuProps) => {
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState();
const elements = useExcalidrawElements(); const elements = useExcalidrawElements();
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => { const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
@@ -50,9 +54,7 @@ export const ContextMenu = React.memo(
return ( return (
<Popover <Popover
onCloseRequest={() => { onCloseRequest={() => setAppState({ contextMenu: null })}
onClose();
}}
top={top} top={top}
left={left} left={left}
fitInViewport={true} fitInViewport={true}
@@ -100,7 +102,7 @@ export const ContextMenu = React.memo(
// we need update state before executing the action in case // we need update state before executing the action in case
// the action uses the appState it's being passed (that still // the action uses the appState it's being passed (that still
// contains a defined contextMenu) to return the next state. // contains a defined contextMenu) to return the next state.
onClose(() => { setAppState({ contextMenu: null }, () => {
actionManager.executeAction(item, "contextMenu"); actionManager.executeAction(item, "contextMenu");
}); });
}} }}
+9 -9
View File
@@ -12,32 +12,32 @@
&--color-primary { &--color-primary {
&.ExcButton--variant-filled { &.ExcButton--variant-filled {
--text-color: var(--color-surface-lowest); --text-color: var(--input-bg-color);
--back-color: var(--color-primary); --back-color: var(--color-primary);
&:hover { &:hover {
--back-color: var(--color-brand-hover); --back-color: var(--color-primary-darker);
} }
&:active { &:active {
--back-color: var(--color-brand-active); --back-color: var(--color-primary-darkest);
} }
} }
&.ExcButton--variant-outlined, &.ExcButton--variant-outlined,
&.ExcButton--variant-icon { &.ExcButton--variant-icon {
--text-color: var(--color-primary); --text-color: var(--color-primary);
--border-color: var(--color-border-outline); --border-color: var(--color-primary);
--back-color: transparent; --back-color: var(--input-bg-color);
&:hover { &:hover {
--text-color: var(--color-brand-hover); --text-color: var(--color-primary-darker);
--border-color: var(--color-brand-hover); --border-color: var(--color-primary-darker);
} }
&:active { &:active {
--text-color: var(--color-brand-active); --text-color: var(--color-primary-darkest);
--border-color: var(--color-brand-active); --border-color: var(--color-primary-darkest);
} }
} }
} }
+1 -16
View File
@@ -19,35 +19,20 @@
} }
&__btn { &__btn {
--background: var(--color-surface-mid);
display: flex; display: flex;
column-gap: 0.5rem; column-gap: 0.5rem;
align-items: center; align-items: center;
background-color: var(--background); border: 1px solid var(--default-border-color);
padding: 0.625rem 1rem; padding: 0.625rem 1rem;
border: 1px solid var(--background);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
color: var(--text-primary-color); color: var(--text-primary-color);
font-weight: 600; font-weight: 600;
font-size: 0.75rem; font-size: 0.75rem;
letter-spacing: 0.4px; letter-spacing: 0.4px;
@at-root .excalidraw.theme--dark#{&} {
--background: var(--color-surface-high);
&:hover {
--background: #363541;
}
}
&:hover { &:hover {
--background: var(--color-surface-high);
text-decoration: none; text-decoration: none;
} }
&:active {
border-color: var(--color-primary);
}
} }
&__link-icon { &__link-icon {
-5
View File
@@ -165,7 +165,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={[KEYS.E, KEYS["0"]]} shortcuts={[KEYS.E, KEYS["0"]]}
/> />
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} /> <Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
<Shortcut label={t("toolBar.laser")} shortcuts={[KEYS.K]} />
<Shortcut <Shortcut
label={t("labels.eyeDropper")} label={t("labels.eyeDropper")}
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]} shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
@@ -259,10 +258,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("buttons.zenMode")} label={t("buttons.zenMode")}
shortcuts={[getShortcutKey("Alt+Z")]} shortcuts={[getShortcutKey("Alt+Z")]}
/> />
<Shortcut
label={t("buttons.objectsSnapMode")}
shortcuts={[getShortcutKey("Alt+S")]}
/>
<Shortcut <Shortcut
label={t("labels.showGrid")} label={t("labels.showGrid")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]} shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
+3 -12
View File
@@ -23,15 +23,12 @@ export type ExportCB = (
const JSONExportModal = ({ const JSONExportModal = ({
elements, elements,
appState, appState,
setAppState,
files, files,
actionManager, actionManager,
exportOpts, exportOpts,
canvas, canvas,
onCloseRequest,
}: { }: {
appState: UIAppState; appState: UIAppState;
setAppState: React.Component<any, UIAppState>["setState"];
files: BinaryFiles; files: BinaryFiles;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionManager; actionManager: ActionManager;
@@ -75,14 +72,9 @@ const JSONExportModal = ({
title={t("exportDialog.link_button")} title={t("exportDialog.link_button")}
aria-label={t("exportDialog.link_button")} aria-label={t("exportDialog.link_button")}
showAriaLabel={true} showAriaLabel={true}
onClick={async () => { onClick={() => {
try { onExportToBackend(elements, appState, files, canvas);
trackEvent("export", "link", `ui (${getFrame()})`); trackEvent("export", "link", `ui (${getFrame()})`);
await onExportToBackend(elements, appState, files, canvas);
onCloseRequest();
} catch (error: any) {
setAppState({ errorMessage: error.message });
}
}} }}
/> />
</Card> </Card>
@@ -122,7 +114,6 @@ export const JSONExportDialog = ({
<JSONExportModal <JSONExportModal
elements={elements} elements={elements}
appState={appState} appState={appState}
setAppState={setAppState}
files={files} files={files}
actionManager={actionManager} actionManager={actionManager}
onCloseRequest={handleClose} onCloseRequest={handleClose}
@@ -1,309 +0,0 @@
import { LaserPointer } from "@excalidraw/laser-pointer";
import { sceneCoordsToViewportCoords } from "../../utils";
import App from "../App";
import { getClientColor } from "../../clients";
// decay time in milliseconds
const DECAY_TIME = 1000;
// length of line in points before it starts decaying
const DECAY_LENGTH = 50;
const average = (a: number, b: number) => (a + b) / 2;
function getSvgPathFromStroke(points: number[][], closed = true) {
const len = points.length;
if (len < 4) {
return ``;
}
let a = points[0];
let b = points[1];
const c = points[2];
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
2,
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
b[1],
c[1],
).toFixed(2)} T`;
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
2,
)} `;
}
if (closed) {
result += "Z";
}
return result;
}
declare global {
interface Window {
LPM: LaserPathManager;
}
}
function easeOutCubic(t: number) {
return 1 - Math.pow(1 - t, 3);
}
function instantiateCollabolatorState(): CollabolatorState {
return {
currentPath: undefined,
finishedPaths: [],
lastPoint: [-10000, -10000],
svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
};
}
function instantiatePath() {
LaserPointer.constants.cornerDetectionMaxAngle = 70;
return new LaserPointer({
simplify: 0,
streamline: 0.4,
sizeMapping: (c) => {
const pt = DECAY_TIME;
const pl = DECAY_LENGTH;
const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
return Math.min(easeOutCubic(l), easeOutCubic(t));
},
});
}
type CollabolatorState = {
currentPath: LaserPointer | undefined;
finishedPaths: LaserPointer[];
lastPoint: [number, number];
svg: SVGPathElement;
};
export class LaserPathManager {
private ownState: CollabolatorState;
private collaboratorsState: Map<string, CollabolatorState> = new Map();
private rafId: number | undefined;
private isDrawing = false;
private container: SVGSVGElement | undefined;
constructor(private app: App) {
this.ownState = instantiateCollabolatorState();
}
destroy() {
this.stop();
this.isDrawing = false;
this.ownState = instantiateCollabolatorState();
this.collaboratorsState = new Map();
}
startPath(x: number, y: number) {
this.ownState.currentPath = instantiatePath();
this.ownState.currentPath.addPoint([x, y, performance.now()]);
this.updatePath(this.ownState);
}
addPointToPath(x: number, y: number) {
if (this.ownState.currentPath) {
this.ownState.currentPath?.addPoint([x, y, performance.now()]);
this.updatePath(this.ownState);
}
}
endPath() {
if (this.ownState.currentPath) {
this.ownState.currentPath.close();
this.ownState.finishedPaths.push(this.ownState.currentPath);
this.updatePath(this.ownState);
}
}
private updatePath(state: CollabolatorState) {
this.isDrawing = true;
if (!this.isRunning) {
this.start();
}
}
private isRunning = false;
start(svg?: SVGSVGElement) {
if (svg) {
this.container = svg;
this.container.appendChild(this.ownState.svg);
}
this.stop();
this.isRunning = true;
this.loop();
}
stop() {
this.isRunning = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
}
this.rafId = undefined;
}
loop() {
this.rafId = requestAnimationFrame(this.loop.bind(this));
this.updateCollabolatorsState();
if (this.isDrawing) {
this.update();
} else {
this.isRunning = false;
}
}
draw(path: LaserPointer) {
const stroke = path
.getStrokeOutline(path.options.size / this.app.state.zoom.value)
.map(([x, y]) => {
const result = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y },
this.app.state,
);
return [result.x, result.y];
});
return getSvgPathFromStroke(stroke, true);
}
updateCollabolatorsState() {
if (!this.container || !this.app.state.collaborators.size) {
return;
}
for (const [key, collabolator] of this.app.state.collaborators.entries()) {
if (!this.collaboratorsState.has(key)) {
const state = instantiateCollabolatorState();
this.container.appendChild(state.svg);
this.collaboratorsState.set(key, state);
this.updatePath(state);
}
const state = this.collaboratorsState.get(key)!;
if (collabolator.pointer && collabolator.pointer.tool === "laser") {
if (collabolator.button === "down" && state.currentPath === undefined) {
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
state.currentPath = instantiatePath();
state.currentPath.addPoint([
collabolator.pointer.x,
collabolator.pointer.y,
performance.now(),
]);
this.updatePath(state);
}
if (collabolator.button === "down" && state.currentPath !== undefined) {
if (
collabolator.pointer.x !== state.lastPoint[0] ||
collabolator.pointer.y !== state.lastPoint[1]
) {
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
state.currentPath.addPoint([
collabolator.pointer.x,
collabolator.pointer.y,
performance.now(),
]);
this.updatePath(state);
}
}
if (collabolator.button === "up" && state.currentPath !== undefined) {
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
state.currentPath.addPoint([
collabolator.pointer.x,
collabolator.pointer.y,
performance.now(),
]);
state.currentPath.close();
state.finishedPaths.push(state.currentPath);
state.currentPath = undefined;
this.updatePath(state);
}
}
}
}
update() {
if (!this.container) {
return;
}
let somePathsExist = false;
for (const [key, state] of this.collaboratorsState.entries()) {
if (!this.app.state.collaborators.has(key)) {
state.svg.remove();
this.collaboratorsState.delete(key);
continue;
}
state.finishedPaths = state.finishedPaths.filter((path) => {
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
});
let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
if (state.currentPath) {
paths += ` ${this.draw(state.currentPath)}`;
}
if (paths.trim()) {
somePathsExist = true;
}
state.svg.setAttribute("d", paths);
state.svg.setAttribute("fill", getClientColor(key));
}
this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
});
let paths = this.ownState.finishedPaths
.map((path) => this.draw(path))
.join(" ");
if (this.ownState.currentPath) {
paths += ` ${this.draw(this.ownState.currentPath)}`;
}
paths = paths.trim();
if (paths) {
somePathsExist = true;
}
this.ownState.svg.setAttribute("d", paths);
this.ownState.svg.setAttribute("fill", "red");
if (!somePathsExist) {
this.isDrawing = false;
}
}
}
@@ -1,41 +0,0 @@
import "../ToolIcon.scss";
import clsx from "clsx";
import { ToolButtonSize } from "../ToolButton";
import { laserPointerToolIcon } from "../icons";
type LaserPointerIconProps = {
title?: string;
name?: string;
checked: boolean;
onChange?(): void;
isMobile?: boolean;
};
const DEFAULT_SIZE: ToolButtonSize = "small";
export const LaserPointerButton = (props: LaserPointerIconProps) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon__LaserPointer",
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"is-mobile": props.isMobile,
},
)}
title={`${props.title}`}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name={props.name}
onChange={props.onChange}
checked={props.checked}
aria-label={props.title}
data-testid="toolbar-LaserPointer"
/>
<div className="ToolIcon__icon">{laserPointerToolIcon}</div>
</label>
);
};
-27
View File
@@ -1,27 +0,0 @@
import { useEffect, useRef } from "react";
import { LaserPathManager } from "./LaserPathManager";
import "./LaserToolOverlay.scss";
type LaserToolOverlayProps = {
manager: LaserPathManager;
};
export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => {
const svgRef = useRef<SVGSVGElement | null>(null);
useEffect(() => {
if (svgRef.current) {
manager.start(svgRef.current);
}
return () => {
manager.stop();
};
}, [manager]);
return (
<div className="LaserToolOverlay">
<svg ref={svgRef} className="LaserToolOverlayCanvas" />
</div>
);
};
@@ -1,20 +0,0 @@
.excalidraw {
.LaserToolOverlay {
pointer-events: none;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 2;
.LaserToolOverlayCanvas {
image-rendering: auto;
overflow: visible;
position: absolute;
top: 0;
left: 0;
}
}
}
+25 -24
View File
@@ -55,13 +55,13 @@ import "./Toolbar.scss";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
appState: UIAppState; appState: UIAppState;
files: BinaryFiles; files: BinaryFiles;
canvas: HTMLCanvasElement; canvas: HTMLCanvasElement;
interactiveCanvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void; onLockToggle: () => void;
@@ -72,11 +72,11 @@ interface LayerUIProps {
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"]; renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"]; renderCustomStats?: ExcalidrawProps["renderCustomStats"];
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
onExportImage: AppClassProperties["onExportImage"]; onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
children?: React.ReactNode; children?: React.ReactNode;
app: AppClassProperties; app: AppClassProperties;
isCollaborating: boolean;
} }
const DefaultMainMenu: React.FC<{ const DefaultMainMenu: React.FC<{
@@ -121,6 +121,7 @@ const LayerUI = ({
setAppState, setAppState,
elements, elements,
canvas, canvas,
interactiveCanvas,
onLockToggle, onLockToggle,
onHandToolToggle, onHandToolToggle,
onPenModeToggle, onPenModeToggle,
@@ -128,11 +129,11 @@ const LayerUI = ({
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderCustomStats,
UIOptions, UIOptions,
onImageAction,
onExportImage, onExportImage,
renderWelcomeScreen, renderWelcomeScreen,
children, children,
app, app,
isCollaborating,
}: LayerUIProps) => { }: LayerUIProps) => {
const device = useDevice(); const device = useDevice();
const tunnels = useInitializeTunnels(); const tunnels = useInitializeTunnels();
@@ -276,29 +277,17 @@ const LayerUI = ({
<ShapesSwitcher <ShapesSwitcher
appState={appState} appState={appState}
interactiveCanvas={interactiveCanvas}
activeTool={appState.activeTool} activeTool={appState.activeTool}
app={app} setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/> />
</Stack.Row> </Stack.Row>
</Island> </Island>
{isCollaborating && (
<Island
style={{
marginLeft: 8,
alignSelf: "center",
height: "fit-content",
}}
>
<LaserPointerButton
title={t("toolBar.laser")}
checked={appState.activeTool.type === "laser"}
onChange={() =>
app.setActiveTool({ type: "laser" })
}
isMobile
/>
</Island>
)}
</Stack.Row> </Stack.Row>
</Stack.Col> </Stack.Col>
</div> </div>
@@ -462,6 +451,8 @@ const LayerUI = ({
onLockToggle={onLockToggle} onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle} onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle} onPenModeToggle={onPenModeToggle}
interactiveCanvas={interactiveCanvas}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI} renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats} renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars} renderSidebars={renderSidebars}
@@ -548,8 +539,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
return false; return false;
} }
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps; const {
const { canvas: _nC, appState: nextAppState, ...next } = nextProps; canvas: _pC,
interactiveCanvas: _pIC,
appState: prevAppState,
...prev
} = prevProps;
const {
canvas: _nC,
interactiveCanvas: _nIC,
appState: nextAppState,
...next
} = nextProps;
return ( return (
isShallowEqual( isShallowEqual(
+2 -2
View File
@@ -99,10 +99,10 @@
font-size: 0.75rem; font-size: 0.75rem;
&:hover { &:hover {
background-color: var(--color-brand-hover); background-color: var(--color-primary-darker);
} }
&:active { &:active {
background-color: var(--color-brand-active); background-color: var(--color-primary-darkest);
} }
} }
+35 -174
View File
@@ -1,83 +1,9 @@
@import "../css/variables.module"; @import "../css/variables.module";
$verticalBreakpoint: 860px;
.excalidraw { .excalidraw {
.dialog-mermaid { .mermaid-to-excalidraw-wrapper {
&-title { display: flex;
margin-bottom: 5px;
margin-top: 2px;
}
&-desc {
font-size: 15px;
font-style: italic;
font-weight: 500;
}
.Modal__content .Island {
box-shadow: none;
}
@at-root .excalidraw:not(.excalidraw--mobile)#{&} {
padding: 1.25rem;
.Modal__content {
height: 100%;
max-height: 750px;
@media screen and (max-width: $verticalBreakpoint) {
height: auto;
// When vertical, we want the height to span whole viewport.
// This is also important for the children not to overflow the
// modal/viewport (for some reason).
max-height: 100%;
}
.Island {
height: 100%;
display: flex;
flex-direction: column;
flex: 1 1 auto;
.Dialog__content {
display: flex;
flex: 1 1 auto;
}
}
}
}
}
.dialog-mermaid-body {
width: 100%; width: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr auto;
height: 100%;
column-gap: 4rem;
@media screen and (max-width: $verticalBreakpoint) {
flex-direction: column;
display: flex;
gap: 1rem;
}
}
.dialog-mermaid-panels {
display: grid;
width: 100%;
grid-template-columns: 1fr 1fr;
justify-content: space-between; justify-content: space-between;
gap: 4rem;
grid-row: 1;
grid-column: 1 / 3;
@media screen and (max-width: $verticalBreakpoint) {
flex-direction: column;
display: flex;
gap: 1rem;
}
label { label {
font-size: 14px; font-size: 14px;
@@ -86,7 +12,7 @@ $verticalBreakpoint: 860px;
margin-bottom: 4px; margin-bottom: 4px;
margin-left: 4px; margin-left: 4px;
@media screen and (max-width: $verticalBreakpoint) { @media screen and (max-width: 860px) {
margin-top: 4px; margin-top: 4px;
} }
} }
@@ -97,125 +23,60 @@ $verticalBreakpoint: 860px;
textarea { textarea {
width: 20rem; width: 20rem;
height: 100%; height: 19rem;
resize: none; resize: none;
border-radius: var(--border-radius-lg); border-radius: 8px;
border: 1px solid var(--dialog-border-color); padding: 1rem;
white-space: pre-wrap;
padding: 0.85rem;
box-sizing: border-box;
width: 100%;
font-family: monospace;
@media screen and (max-width: $verticalBreakpoint) { @media screen and (max-width: 860px) {
width: auto; width: auto;
height: 10rem; height: 10rem;
} }
} }
} }
&-preview-wrapper { &-preview-canvas {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 0.85rem; padding: 0.85rem;
box-sizing: border-box;
width: 100%;
// acts as min-height
height: 200px;
flex-grow: 1;
position: relative;
width: 20rem;
height: 19rem;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
left center; left center;
border-radius: var(--border-radius-lg); border-radius: 8px;
border: 1px solid var(--dialog-border-color); border: 1px solid #e4e4eb;
@media screen and (max-width: $verticalBreakpoint) { @media screen and (max-width: 860px) {
// acts as min-height
height: 400px;
width: auto; width: auto;
} }
}
canvas { &-preview-insert.excalidraw-button {
max-width: 100%; font-family: "Assistant";
max-height: 100%; font-weight: 600;
height: 2.5rem;
margin-top: 1em;
margin-bottom: 0.3em;
width: 7.5rem;
font-size: 12px;
background-color: var(--color-primary);
&:hover {
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--color-primary-darkest);
}
color: $oc-white;
@media screen and (max-width: 860px) {
width: 100%;
} }
} }
@media screen and (max-width: 860px) {
&-preview-canvas-container {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
flex-grow: 1;
}
&-preview {
display: flex;
flex-direction: column; flex-direction: column;
} }
.mermaid-error {
color: red;
font-weight: 800;
font-size: 30px;
word-break: break-word;
overflow: auto;
max-height: 100%;
height: 100%;
width: 100%;
text-align: center;
position: absolute;
z-index: 10;
p {
font-weight: 500;
font-family: Cascadia;
text-align: left;
white-space: pre-wrap;
font-size: 0.875rem;
padding: 0 10px;
}
}
}
.dialog-mermaid-buttons {
grid-column: 2;
.dialog-mermaid-insert {
&.excalidraw-button {
font-family: "Assistant";
font-weight: 600;
height: 2.5rem;
margin-top: 1em;
margin-bottom: 0.3em;
width: 7.5rem;
font-size: 12px;
color: $oc-white;
background-color: var(--color-primary);
&:hover {
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--color-primary-darkest);
}
@media screen and (max-width: $verticalBreakpoint) {
width: 100%;
}
@at-root .excalidraw.theme--dark#{&} {
color: var(--color-gray-100);
}
}
span {
padding-left: 0.5rem;
display: flex;
}
}
} }
} }
+105 -75
View File
@@ -21,7 +21,7 @@ import Trans from "./Trans";
const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw"; const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
const MERMAID_EXAMPLE = const MERMAID_EXAMPLE =
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]"; "flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[test]";
const saveMermaidDataToStorage = (data: string) => { const saveMermaidDataToStorage = (data: string) => {
try { try {
@@ -48,25 +48,37 @@ const importMermaidDataFromStorage = () => {
const ErrorComp = ({ error }: { error: string }) => { const ErrorComp = ({ error }: { error: string }) => {
return ( return (
<div data-testid="mermaid-error" className="mermaid-error"> <div
Error! <p>{error}</p> data-testid="mermaid-error"
style={{
color: "red",
fontWeight: 800,
fontSize: "30px",
wordBreak: "break-word",
overflow: "auto",
maxHeight: "100%",
textAlign: "center",
}}
>
Error! <p style={{ fontSize: "18px", fontWeight: "600" }}>{error}</p>
</div> </div>
); );
}; };
const MermaidToExcalidraw = () => { const MermaidToExcalidraw = ({
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{ selectedElements,
loaded: boolean; }: {
api: { selectedElements: readonly NonDeletedExcalidrawElement[];
parseMermaidToExcalidraw: ( }) => {
defination: string, const mermaidToExcalidrawLib = useRef<{
options: MermaidOptions, parseMermaidToExcalidraw: (
) => Promise<MermaidToExcalidrawResult>; defination: string,
} | null; options: MermaidOptions,
}>({ loaded: false, api: null }); ) => Promise<MermaidToExcalidrawResult>;
} | null>(null);
const [text, setText] = useState(""); const [text, setText] = useState("");
const deferredText = useDeferredValue(text.trim()); const deferredText = useDeferredValue(text);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const canvasRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLDivElement>(null);
@@ -88,44 +100,45 @@ const MermaidToExcalidraw = () => {
return; return;
} }
parent.style.background = ""; parent.style.background = "";
setError(null);
canvasNode.replaceChildren(); canvasNode.replaceChildren();
}; };
useEffect(() => { useEffect(() => {
const loadMermaidToExcalidrawLib = async () => { const loadMermaidToExcalidrawLib = async () => {
const api = await import( mermaidToExcalidrawLib.current = await import(
/* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw" /* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
); );
setMermaidToExcalidrawLib({ loaded: true, api }); setLoading(false);
}; };
loadMermaidToExcalidrawLib(); loadMermaidToExcalidrawLib();
}, []); }, []);
useEffect(() => { useEffect(() => {
const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE; if (!loading) {
setText(data); const selectedMermaidImage = selectedElements.filter(
}, []); (el) => el.type === "image" && el.customData?.mermaidText,
)[0];
const data = selectedMermaidImage
? selectedMermaidImage.customData?.mermaidText
: importMermaidDataFromStorage() || MERMAID_EXAMPLE;
setText(data);
}
}, [loading, selectedElements]);
useEffect(() => { useEffect(() => {
const renderExcalidrawPreview = async () => { const renderExcalidrawPreview = async () => {
const canvasNode = canvasRef.current; const canvasNode = canvasRef.current;
const parent = canvasNode?.parentElement;
if ( if (
!mermaidToExcalidrawLib.loaded ||
!canvasNode || !canvasNode ||
!parent || !mermaidToExcalidrawLib.current ||
!mermaidToExcalidrawLib.api deferredText === ""
) { ) {
return; return;
} }
if (!deferredText) {
resetPreview();
return;
}
try { try {
const { elements, files } = const { elements, files } =
await mermaidToExcalidrawLib.api.parseMermaidToExcalidraw( await mermaidToExcalidrawLib.current.parseMermaidToExcalidraw(
deferredText, deferredText,
{ {
fontSize: DEFAULT_FONT_SIZE, fontSize: DEFAULT_FONT_SIZE,
@@ -134,37 +147,50 @@ const MermaidToExcalidraw = () => {
setError(null); setError(null);
data.current = { data.current = {
elements: convertToExcalidrawElements(elements, { elements: convertToExcalidrawElements(
regenerateIds: true, elements.map((el) => {
}), if (el.type === "image") {
el.customData = { mermaidText: text };
}
return el;
}),
{
regenerateIds: true,
},
),
files, files,
}; };
const parent = canvasNode.parentElement!;
const maxWidth = parent.offsetWidth;
const maxHeight = parent.offsetHeight;
let dimension = Math.max(maxWidth, maxHeight);
dimension = Math.min(dimension, parent.offsetWidth - 10);
dimension = Math.min(dimension, parent.offsetHeight - 10);
const canvas = await exportToCanvas({ const canvas = await exportToCanvas({
elements: data.current.elements, elements: data.current.elements,
files: data.current.files, files: data.current.files,
exportPadding: DEFAULT_EXPORT_PADDING, exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight: maxWidthOrHeight: dimension,
Math.max(parent.offsetWidth, parent.offsetHeight) *
window.devicePixelRatio,
}); });
// if converting to blob fails, there's some problem that will // if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big) // likely prevent preview and export (e.g. canvas too big)
await canvasToBlob(canvas); await canvasToBlob(canvas);
parent.style.background = "var(--default-bg-color)"; parent.style.background = "#fff";
canvasNode.replaceChildren(canvas); canvasNode.replaceChildren(canvas);
} catch (e: any) { } catch (e: any) {
parent.style.background = "var(--default-bg-color)"; console.error(e.message);
resetPreview();
if (deferredText) { if (deferredText) {
setError(e.message); setError(e.message);
} }
} }
}; };
renderExcalidrawPreview(); renderExcalidrawPreview();
}, [deferredText, mermaidToExcalidrawLib]); }, [deferredText, text]);
const onClose = () => { const onClose = () => {
app.setOpenDialog(null); app.setActiveTool({ type: "selection" });
saveMermaidDataToStorage(text); saveMermaidDataToStorage(text);
}; };
@@ -183,57 +209,61 @@ const MermaidToExcalidraw = () => {
<Dialog <Dialog
className="dialog-mermaid" className="dialog-mermaid"
onCloseRequest={onClose} onCloseRequest={onClose}
size={1200}
title={ title={
<> <>
<p className="dialog-mermaid-title">{t("mermaid.title")}</p> <p style={{ marginBottom: "5px", marginTop: "2px" }}>
<span className="dialog-mermaid-desc"> {t("mermaid.title")}
</p>
<span
style={{ fontSize: "15px", fontStyle: "italic", fontWeight: 500 }}
>
<Trans <Trans
i18nKey="mermaid.description" i18nKey="mermaid.description"
flowchartLink={(el) => ( flowchartLink={(el) => (
<a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a> <a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
)} )}
sequenceLink={(el) => (
<a href="https://mermaid.js.org/syntax/sequenceDiagram.html">
{el}
</a>
)}
/> />
<br /> <br />
</span> </span>
</> </>
} }
> >
<div className="dialog-mermaid-body"> <div className="mermaid-to-excalidraw-wrapper">
<div className="dialog-mermaid-panels"> <div
<div className="dialog-mermaid-panels-text"> className="mermaid-to-excalidraw-wrapper-text"
<label>{t("mermaid.syntax")}</label> style={{ display: "flex", flexDirection: "column" }}
>
<label>{t("mermaid.syntax")}</label>
<textarea <textarea
onChange={(event) => setText(event.target.value)} style={{
value={text} padding: "0.85rem",
/> borderRadius: "8px",
</div> border: "1px solid #e4e4eb",
<div className="dialog-mermaid-panels-preview"> whiteSpace: "pre-wrap",
<label>{t("mermaid.preview")}</label> }}
<div className="dialog-mermaid-panels-preview-wrapper"> onChange={(event) => setText(event.target.value)}
{error && <ErrorComp error={error} />} value={text}
{mermaidToExcalidrawLib.loaded ? ( />
<div
ref={canvasRef}
style={{ opacity: error ? "0.15" : 1 }}
className="dialog-mermaid-panels-preview-canvas-container"
/>
) : (
<Spinner size="2rem" />
)}
</div>
</div>
</div> </div>
<div className="dialog-mermaid-buttons"> <div
<Button className="dialog-mermaid-insert" onSelect={onSelect}> className="mermaid-to-excalidraw-wrapper-preview"
style={{ display: "flex", flexDirection: "column" }}
>
<label>{t("mermaid.preview")}</label>
<div className="mermaid-to-excalidraw-wrapper-preview-canvas">
{error && <ErrorComp error={error} />}
{loading && <Spinner size="2rem" />}
<div ref={canvasRef} />
</div>
<Button
className="mermaid-to-excalidraw-wrapper-preview-insert"
onSelect={onSelect}
>
{t("mermaid.button")} {t("mermaid.button")}
<span>{ArrowRightIcon}</span> <span style={{ paddingLeft: "8px", display: "flex" }}>
{ArrowRightIcon}
</span>
</Button> </Button>
</div> </div>
</div> </div>
+11 -2
View File
@@ -36,7 +36,9 @@ type MobileMenuProps = {
onLockToggle: () => void; onLockToggle: () => void;
onHandToolToggle: () => void; onHandToolToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
interactiveCanvas: HTMLCanvasElement | null;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: ( renderTopRightUI?: (
isMobile: boolean, isMobile: boolean,
appState: UIAppState, appState: UIAppState,
@@ -56,7 +58,8 @@ export const MobileMenu = ({
onLockToggle, onLockToggle,
onHandToolToggle, onHandToolToggle,
onPenModeToggle, onPenModeToggle,
interactiveCanvas,
onImageAction,
renderTopRightUI, renderTopRightUI,
renderCustomStats, renderCustomStats,
renderSidebars, renderSidebars,
@@ -82,8 +85,14 @@ export const MobileMenu = ({
<Stack.Row gap={1}> <Stack.Row gap={1}>
<ShapesSwitcher <ShapesSwitcher
appState={appState} appState={appState}
interactiveCanvas={interactiveCanvas}
activeTool={appState.activeTool} activeTool={appState.activeTool}
app={app} setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/> />
</Stack.Row> </Stack.Row>
</Island> </Island>
+17 -8
View File
@@ -1,18 +1,27 @@
@import "../css/variables.module"; @import "../css/variables.module";
.excalidraw { .excalidraw {
--RadioGroup-background: var(--island-bg-color); --RadioGroup-background: #ffffff;
--RadioGroup-border: var(--color-surface-high); --RadioGroup-border: var(--color-gray-30);
--RadioGroup-choice-color-off: var(--color-primary); --RadioGroup-choice-color-off: var(--color-primary);
--RadioGroup-choice-color-off-hover: var(--color-brand-hover); --RadioGroup-choice-color-off-hover: var(--color-primary-darkest);
--RadioGroup-choice-background-off: var(--island-bg-color); --RadioGroup-choice-background-off: white;
--RadioGroup-choice-background-off-active: var(--color-surface-high); --RadioGroup-choice-background-off-active: var(--color-gray-20);
--RadioGroup-choice-color-on: var(--color-surface-lowest); --RadioGroup-choice-color-on: white;
--RadioGroup-choice-background-on: var(--color-primary); --RadioGroup-choice-background-on: var(--color-primary);
--RadioGroup-choice-background-on-hover: var(--color-brand-hover); --RadioGroup-choice-background-on-hover: var(--color-primary-darker);
--RadioGroup-choice-background-on-active: var(--color-brand-active); --RadioGroup-choice-background-on-active: var(--color-primary-darkest);
&.theme--dark {
--RadioGroup-background: var(--color-gray-85);
--RadioGroup-border: var(--color-gray-70);
--RadioGroup-choice-background-off: var(--color-gray-85);
--RadioGroup-choice-background-off-active: var(--color-gray-70);
--RadioGroup-choice-color-on: var(--color-gray-85);
}
.RadioGroup { .RadioGroup {
box-sizing: border-box; box-sizing: border-box;
+2 -1
View File
@@ -3,7 +3,8 @@
.excalidraw { .excalidraw {
.sidebar-trigger { .sidebar-trigger {
@include outlineButtonStyles; @include outlineButtonStyles;
@include filledButtonOnCanvas;
background-color: var(--island-bg-color);
width: auto; width: auto;
height: var(--lg-button-size); height: var(--lg-button-size);
+14 -16
View File
@@ -1,13 +1,15 @@
@import "../css/variables.module"; @import "../css/variables.module";
.excalidraw { .excalidraw {
--Switch-disabled-color: var(--color-border-outline); --Switch-disabled-color: #d6d6d6;
--Switch-disabled-toggled-background: var(--color-border-outline-variant); --Switch-track-background: white;
--Switch-disabled-border: var(--color-border-outline-variant); --Switch-thumb-background: #3d3d3d;
--Switch-track-background: var(--island-bg-color);
--Switch-thumb-background: var(--color-on-surface); &.theme--dark {
--Switch-hover-background: var(--color-brand-hover); --Switch-disabled-color: #5c5c5c;
--Switch-active-background: var(--color-brand-active); --Switch-track-background: #242424;
--Switch-thumb-background: #b8b8b8;
}
.Switch { .Switch {
position: relative; position: relative;
@@ -26,11 +28,7 @@
&:hover { &:hover {
background: var(--Switch-track-background); background: var(--Switch-track-background);
border: 1px solid var(--Switch-hover-background); border: 1px solid #999999;
}
&:active {
border: 1px solid var(--Switch-active-background);
} }
&.toggled { &.toggled {
@@ -45,11 +43,11 @@
&.disabled { &.disabled {
background: var(--Switch-track-background); background: var(--Switch-track-background);
border: 1px solid var(--Switch-disabled-border); border: 1px solid var(--Switch-disabled-color);
&.toggled { &.toggled {
background: var(--Switch-disabled-toggled-background); background: var(--Switch-disabled-color);
border: 1px solid var(--Switch-disabled-toggled-background); border: 1px solid var(--Switch-disabled-color);
} }
} }
@@ -94,7 +92,7 @@
} }
&.disabled.toggled:before { &.disabled.toggled:before {
background: var(--Switch-disabled-color); background: var(--color-gray-50);
} }
& input { & input {
+21 -12
View File
@@ -1,16 +1,25 @@
@import "../css/variables.module"; @import "../css/variables.module";
.excalidraw { .excalidraw {
--ExcTextField--color: var(--color-on-surface); --ExcTextField--color: var(--color-gray-80);
--ExcTextField--label-color: var(--color-on-surface); --ExcTextField--label-color: var(--color-gray-80);
--ExcTextField--background: transparent; --ExcTextField--background: white;
--ExcTextField--readonly--background: var(--color-surface-high); --ExcTextField--readonly--background: var(--color-gray-10);
--ExcTextField--readonly--color: var(--color-on-surface); --ExcTextField--readonly--color: var(--color-gray-80);
--ExcTextField--border: var(--color-border-outline); --ExcTextField--border: var(--color-gray-40);
--ExcTextField--readonly--border: var(--color-border-outline-variant); --ExcTextField--border-hover: var(--color-gray-50);
--ExcTextField--border-hover: var(--color-brand-hover); --ExcTextField--placeholder: var(--color-gray-40);
--ExcTextField--border-active: var(--color-brand-active);
--ExcTextField--placeholder: var(--color-border-outline-variant); &.theme--dark {
--ExcTextField--color: var(--color-gray-10);
--ExcTextField--label-color: var(--color-gray-20);
--ExcTextField--background: var(--color-gray-85);
--ExcTextField--readonly--background: var(--color-gray-80);
--ExcTextField--readonly--color: var(--color-gray-40);
--ExcTextField--border: var(--color-gray-70);
--ExcTextField--border-hover: var(--color-gray-60);
--ExcTextField--placeholder: var(--color-gray-80);
}
.ExcTextField { .ExcTextField {
&--fullWidth { &--fullWidth {
@@ -52,7 +61,7 @@
&:active, &:active,
&:focus-within { &:focus-within {
border-color: var(--ExcTextField--border-active); border-color: var(--color-primary);
} }
} }
@@ -98,7 +107,7 @@
&--readonly { &--readonly {
background: var(--ExcTextField--readonly--background); background: var(--ExcTextField--readonly--background);
border-color: var(--ExcTextField--readonly--border); border-color: transparent;
& input { & input {
color: var(--ExcTextField--readonly--color); color: var(--ExcTextField--readonly--color);
+5 -5
View File
@@ -83,12 +83,12 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
} }
}; };
useEffect(() => { useEffect(
isMountedRef.current = true; () => () => {
return () => {
isMountedRef.current = false; isMountedRef.current = false;
}; },
}, []); [],
);
const lastPointerTypeRef = useRef<PointerType | null>(null); const lastPointerTypeRef = useRef<PointerType | null>(null);
+5 -5
View File
@@ -97,6 +97,10 @@
} }
} }
// &:hover {
// background-color: var(--button-gray-2);
// }
&:active { &:active {
background-color: var(--button-gray-3); background-color: var(--button-gray-3);
} }
@@ -106,6 +110,7 @@
} }
&--hide { &--hide {
// visibility: hidden;
display: none !important; display: none !important;
} }
} }
@@ -174,10 +179,5 @@
height: var(--lg-icon-size); height: var(--lg-icon-size);
} }
} }
.ToolIcon__LaserPointer .ToolIcon__icon {
width: var(--default-button-size);
height: var(--default-button-size);
}
} }
} }
-7
View File
@@ -26,19 +26,12 @@
.App-toolbar__extra-tools-trigger { .App-toolbar__extra-tools-trigger {
box-shadow: none; box-shadow: none;
border: 0; border: 0;
background-color: transparent;
&:active { &:active {
background-color: var(--button-hover-bg); background-color: var(--button-hover-bg);
box-shadow: 0 0 0 1px box-shadow: 0 0 0 1px
var(--button-active-border, var(--color-primary-darkest)) inset; var(--button-active-border, var(--color-primary-darkest)) inset;
} }
&--selected,
&--selected:hover {
background: var(--color-primary-light);
color: var(--color-primary);
}
} }
.App-toolbar__extra-tools-dropdown { .App-toolbar__extra-tools-dropdown {
@@ -193,8 +193,6 @@ const getRelevantAppStateProps = (
showHyperlinkPopup: appState.showHyperlinkPopup, showHyperlinkPopup: appState.showHyperlinkPopup,
collaborators: appState.collaborators, // Necessary for collab. sessions collaborators: appState.collaborators, // Necessary for collab. sessions
activeEmbeddable: appState.activeEmbeddable, activeEmbeddable: appState.activeEmbeddable,
snapLines: appState.snapLines,
zenModeEnabled: appState.zenModeEnabled,
}); });
const areEqual = ( const areEqual = (
+5 -7
View File
@@ -114,13 +114,11 @@ const areEqual = (
return false; return false;
} }
return ( return isShallowEqual(
isShallowEqual( // asserting AppState because we're being passed the whole AppState
// asserting AppState because we're being passed the whole AppState // but resolve to only the StaticCanvas-relevant props
// but resolve to only the StaticCanvas-relevant props getRelevantAppStateProps(prevProps.appState as AppState),
getRelevantAppStateProps(prevProps.appState as AppState), getRelevantAppStateProps(nextProps.appState as AppState),
getRelevantAppStateProps(nextProps.appState as AppState),
) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig)
); );
}; };
+14 -35
View File
@@ -14,7 +14,7 @@
.dropdown-menu-container { .dropdown-menu-container {
padding: 8px 8px; padding: 8px 8px;
box-sizing: border-box; box-sizing: border-box;
// background-color: var(--island-bg-color); background-color: var(--island-bg-color);
box-shadow: var(--shadow-island); box-shadow: var(--shadow-island);
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
position: relative; position: relative;
@@ -27,7 +27,7 @@
} }
.dropdown-menu-container { .dropdown-menu-container {
background-color: var(--island-bg-color); background-color: #fff !important;
max-height: calc(100vh - 150px); max-height: calc(100vh - 150px);
overflow-y: auto; overflow-y: auto;
--gap: 2; --gap: 2;
@@ -38,7 +38,7 @@
padding: 0 0.625rem; padding: 0 0.625rem;
column-gap: 0.625rem; column-gap: 0.625rem;
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-on-surface); color: var(--color-gray-100);
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
font-weight: normal; font-weight: normal;
@@ -47,7 +47,7 @@
.dropdown-menu-item { .dropdown-menu-item {
background-color: transparent; background-color: transparent;
border: 1px solid transparent; border: 0;
align-items: center; align-items: center;
height: 2rem; height: 2rem;
cursor: pointer; cursor: pointer;
@@ -57,11 +57,6 @@
height: 2.25rem; height: 2.25rem;
} }
&--selected {
background: var(--color-primary-light);
--icon-fill-color: var(--color-primary-darker);
}
&__text { &__text {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
@@ -78,11 +73,6 @@
text-decoration: none; text-decoration: none;
} }
&:active {
background-color: var(--button-hover-bg);
border-color: var(--color-brand-active);
}
svg { svg {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@@ -101,33 +91,22 @@
font-weight: 500; font-weight: 500;
} }
} }
&.theme--dark {
.dropdown-menu-item {
color: var(--color-gray-40);
}
.dropdown-menu-container {
background-color: var(--color-gray-90) !important;
}
}
.dropdown-menu-button { .dropdown-menu-button {
@include outlineButtonStyles; @include outlineButtonStyles;
background-color: var(--island-bg-color);
width: var(--lg-button-size); width: var(--lg-button-size);
height: var(--lg-button-size); height: var(--lg-button-size);
--background: var(--color-surface-mid);
background-color: var(--background);
@at-root .excalidraw.theme--dark#{&} {
--background: var(--color-surface-high);
&:hover {
--background: #363541;
}
}
&:hover {
--background: var(--color-surface-high);
background-color: var(--background);
text-decoration: none;
}
&:active {
border-color: var(--color-primary);
}
svg { svg {
width: var(--lg-icon-size); width: var(--lg-icon-size);
height: var(--lg-icon-size); height: var(--lg-icon-size);
@@ -11,14 +11,12 @@ const DropdownMenuItem = ({
children, children,
shortcut, shortcut,
className, className,
selected,
...rest ...rest
}: { }: {
icon?: JSX.Element; icon?: JSX.Element;
onSelect: (event: Event) => void; onSelect: (event: Event) => void;
children: React.ReactNode; children: React.ReactNode;
shortcut?: string; shortcut?: string;
selected?: boolean;
className?: string; className?: string;
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => { } & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect); const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
@@ -28,7 +26,7 @@ const DropdownMenuItem = ({
{...rest} {...rest}
onClick={handleClick} onClick={handleClick}
type="button" type="button"
className={getDropdownMenuItemClassName(className, selected)} className={getDropdownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]} title={rest.title ?? rest["aria-label"]}
> >
<MenuItemContent icon={icon} shortcut={shortcut}> <MenuItemContent icon={icon} shortcut={shortcut}>
@@ -3,19 +3,15 @@ import React from "react";
const DropdownMenuItemCustom = ({ const DropdownMenuItemCustom = ({
children, children,
className = "", className = "",
selected,
...rest ...rest
}: { }: {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
selected?: boolean;
} & React.HTMLAttributes<HTMLDivElement>) => { } & React.HTMLAttributes<HTMLDivElement>) => {
return ( return (
<div <div
{...rest} {...rest}
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className} ${ className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
selected ? `dropdown-menu-item--selected` : ``
}`.trim()}
> >
{children} {children}
</div> </div>
@@ -12,7 +12,6 @@ const DropdownMenuItemLink = ({
children, children,
onSelect, onSelect,
className = "", className = "",
selected,
...rest ...rest
}: { }: {
href: string; href: string;
@@ -20,7 +19,6 @@ const DropdownMenuItemLink = ({
children: React.ReactNode; children: React.ReactNode;
shortcut?: string; shortcut?: string;
className?: string; className?: string;
selected?: boolean;
onSelect?: (event: Event) => void; onSelect?: (event: Event) => void;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => { } & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect); const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
@@ -31,7 +29,7 @@ const DropdownMenuItemLink = ({
href={href} href={href}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className={getDropdownMenuItemClassName(className, selected)} className={getDropdownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]} title={rest.title ?? rest["aria-label"]}
onClick={handleClick} onClick={handleClick}
> >
+2 -7
View File
@@ -6,13 +6,8 @@ export const DropdownMenuContentPropsContext = React.createContext<{
onSelect?: (event: Event) => void; onSelect?: (event: Event) => void;
}>({}); }>({});
export const getDropdownMenuItemClassName = ( export const getDropdownMenuItemClassName = (className = "") => {
className = "", return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
selected = false,
) => {
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
selected ? "dropdown-menu-item--selected" : ""
}`.trim();
}; };
export const useHandleDropdownMenuItemClick = ( export const useHandleDropdownMenuItemClick = (
+24 -25
View File
@@ -13,7 +13,7 @@ import clsx from "clsx";
import { Theme } from "../element/types"; import { Theme } from "../element/types";
import { THEME } from "../constants"; import { THEME } from "../constants";
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)"; const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
const handlerColor = (theme: Theme) => const handlerColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.white : "#1e1e1e"; theme === THEME.LIGHT ? oc.white : "#1e1e1e";
@@ -1656,35 +1656,34 @@ export const frameToolIcon = createIcon(
export const mermaidLogoIcon = createIcon( export const mermaidLogoIcon = createIcon(
<path <path
fill="currentColor" fill="black"
d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z" d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
/>, />,
); );
export const ArrowRightIcon = createIcon( export const ArrowRightIcon = createIcon(
<g strokeWidth="1.25"> <>
<path d="M4.16602 10H15.8327" />
<path d="M12.5 13.3333L15.8333 10" />
<path d="M12.5 6.66666L15.8333 9.99999" />
</g>,
modifiedTablerIconProps,
);
export const laserPointerToolIcon = createIcon(
<g
fill="none"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
transform="rotate(90 10 10)"
>
<path <path
clipRule="evenodd" d="M4.16602 10H15.8327"
d="m9.644 13.69 7.774-7.773a2.357 2.357 0 0 0-3.334-3.334l-7.773 7.774L8 12l1.643 1.69Z" stroke="white"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/> />
<path d="m13.25 3.417 3.333 3.333M10 10l2-2M5 15l3-3M2.156 17.894l1-1M5.453 19.029l-.144-1.407M2.377 11.887l.866 1.118M8.354 17.273l-1.194-.758M.953 14.652l1.408.13" /> <path
</g>, d="M12.5 13.3333L15.8333 10"
stroke="white"
20, strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12.5 6.66666L15.8333 9.99999"
stroke="white"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>,
{ width: 20, height: 20 },
); );
@@ -14,8 +14,6 @@
--button-active-bg: var(--color-primary-darker); --button-active-bg: var(--color-primary-darker);
box-shadow: 0 0 0 1px var(--color-surface-lowest);
flex-shrink: 0; flex-shrink: 0;
// double .active to force specificity // double .active to force specificity
-1
View File
@@ -43,7 +43,6 @@ const MainMenu = Object.assign(
}); });
}} }}
data-testid="main-menu-trigger" data-testid="main-menu-trigger"
className="main-menu-trigger"
> >
{HamburgerMenuIcon} {HamburgerMenuIcon}
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
@@ -174,7 +174,7 @@
justify-content: space-between; justify-content: space-between;
background: none; background: none;
border: 1px solid transparent; border: none;
padding: 0.75rem; padding: 0.75rem;
@@ -204,7 +204,7 @@
.welcome-screen-menu-item:hover { .welcome-screen-menu-item:hover {
text-decoration: none; text-decoration: none;
background: var(--button-hover-bg); background: var(--color-gray-10);
.welcome-screen-menu-item__shortcut { .welcome-screen-menu-item__shortcut {
color: var(--color-gray-50); color: var(--color-gray-50);
@@ -216,8 +216,7 @@
} }
.welcome-screen-menu-item:active { .welcome-screen-menu-item:active {
background: var(--button-hover-bg); background: var(--color-gray-20);
border-color: var(--color-brand-active);
.welcome-screen-menu-item__shortcut { .welcome-screen-menu-item__shortcut {
color: var(--color-gray-50); color: var(--color-gray-50);
@@ -248,7 +247,8 @@
} }
.welcome-screen-menu-item:hover { .welcome-screen-menu-item:hover {
background-color: var(--color-surface-low); background: var(--color-gray-85);
.welcome-screen-menu-item__shortcut { .welcome-screen-menu-item__shortcut {
color: var(--color-gray-50); color: var(--color-gray-50);
} }
@@ -259,6 +259,7 @@
} }
.welcome-screen-menu-item:active { .welcome-screen-menu-item:active {
background-color: var(--color-gray-90);
.welcome-screen-menu-item__text { .welcome-screen-menu-item__text {
color: var(--color-gray-10); color: var(--color-gray-10);
} }
+3 -17
View File
@@ -148,8 +148,6 @@ export const IMAGE_MIME_TYPES = {
jfif: "image/jfif", jfif: "image/jfif",
} as const; } as const;
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
export const MIME_TYPES = { export const MIME_TYPES = {
json: "application/json", json: "application/json",
// excalidraw data // excalidraw data
@@ -298,18 +296,6 @@ export const ROUNDNESS = {
* collaboration */ * collaboration */
export const PRECEDING_ELEMENT_KEY = "__precedingElement__"; export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
export const ROUGHNESS = {
architect: 0,
artist: 1,
cartoonist: 2,
} as const;
export const STROKE_WIDTH = {
thin: 1,
bold: 2,
extraBold: 4,
} as const;
export const DEFAULT_ELEMENT_PROPS: { export const DEFAULT_ELEMENT_PROPS: {
strokeColor: ExcalidrawElement["strokeColor"]; strokeColor: ExcalidrawElement["strokeColor"];
backgroundColor: ExcalidrawElement["backgroundColor"]; backgroundColor: ExcalidrawElement["backgroundColor"];
@@ -322,10 +308,10 @@ export const DEFAULT_ELEMENT_PROPS: {
} = { } = {
strokeColor: COLOR_PALETTE.black, strokeColor: COLOR_PALETTE.black,
backgroundColor: COLOR_PALETTE.transparent, backgroundColor: COLOR_PALETTE.transparent,
fillStyle: "solid", fillStyle: "hachure",
strokeWidth: 2, strokeWidth: 1,
strokeStyle: "solid", strokeStyle: "solid",
roughness: ROUGHNESS.artist, roughness: 1,
opacity: 100, opacity: 100,
locked: false, locked: false,
}; };
+5 -20
View File
@@ -195,7 +195,7 @@
.buttonList label:focus-within, .buttonList label:focus-within,
input:focus-visible { input:focus-visible {
outline: transparent; outline: transparent;
box-shadow: 0 0 0 1px var(--color-brand-hover); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
.buttonList { .buttonList {
@@ -449,14 +449,13 @@
} }
&:active { &:active {
border: 1px solid var(--button-active-border); border: 1px solid var(--color-primary-darkest);
} }
} }
.help-icon { .help-icon {
@include outlineButtonStyles; @include outlineButtonStyles;
@include filledButtonOnCanvas; background-color: var(--island-bg-color);
width: var(--lg-button-size); width: var(--lg-button-size);
height: var(--lg-button-size); height: var(--lg-button-size);
@@ -542,13 +541,13 @@
&:not(:focus) { &:not(:focus) {
&:hover { &:hover {
border-color: var(--color-brand-hover); background-color: var(--input-hover-bg-color);
} }
} }
&:focus { &:focus {
outline: none; outline: none;
border-color: var(--color-brand-hover); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
} }
@@ -629,20 +628,6 @@
padding: 0; padding: 0;
} }
} }
.main-menu-trigger {
@include filledButtonOnCanvas;
}
.App-menu__left {
--button-border: transparent;
--button-bg: var(--color-surface-mid);
@at-root .excalidraw.theme--dark#{&} {
--button-hover-bg: #363541;
--button-bg: var(--color-surface-high);
}
}
} }
.ErrorSplash.excalidraw { .ErrorSplash.excalidraw {
+23 -45
View File
@@ -12,30 +12,27 @@
--dialog-border-color: var(--color-gray-20); --dialog-border-color: var(--color-gray-20);
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>'); --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-2}; --focus-highlight-color: #{$oc-blue-2};
--icon-fill-color: var(--color-on-surface); --icon-fill-color: var(--color-gray-80);
--icon-green-fill-color: #{$oc-green-9}; --icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white}; --default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white}; --input-bg-color: #{$oc-white};
--input-border-color: #{$oc-gray-4}; --input-border-color: #{$oc-gray-4};
--input-hover-bg-color: #{$oc-gray-1}; --input-hover-bg-color: #{$oc-gray-1};
--input-label-color: #{$oc-gray-7}; --input-label-color: #{$oc-gray-7};
--island-bg-color: #ffffff; --island-bg-color: rgba(255, 255, 255, 0.96);
--keybinding-color: var(--color-gray-40); --keybinding-color: var(--color-gray-40);
--link-color: #{$oc-blue-7}; --link-color: #{$oc-blue-7};
--overlay-bg-color: #{transparentize($oc-white, 0.12)}; --overlay-bg-color: #{transparentize($oc-white, 0.12)};
--popup-bg-color: var(--island-bg-color); --popup-bg-color: #{$oc-white};
--popup-secondary-bg-color: #{$oc-gray-1}; --popup-secondary-bg-color: #{$oc-gray-1};
--popup-text-color: #{$oc-black}; --popup-text-color: #{$oc-black};
--popup-text-inverted-color: #{$oc-white}; --popup-text-inverted-color: #{$oc-white};
--select-highlight-color: #{$oc-blue-5}; --select-highlight-color: #{$oc-blue-5};
--shadow-island: 0px 0px 0.9310142993927002px 0px rgba(0, 0, 0, 0.17), --shadow-island: 0px 7px 14px rgba(0, 0, 0, 0.05),
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798),
0px 7px 14px 0px rgba(0, 0, 0, 0.05); 0px 0px 0.931014px rgba(0, 0, 0, 0.1702);
--button-hover-bg: var(--color-gray-10);
--button-hover-bg: var(--color-surface-high); --default-border-color: var(--color-gray-30);
--button-active-bg: var(--color-surface-high);
--button-active-border: var(--color-brand-active);
--default-border-color: var(--color-surface-high);
--default-button-size: 2rem; --default-button-size: 2rem;
--default-icon-size: 1rem; --default-icon-size: 1rem;
@@ -66,14 +63,14 @@
0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035), 0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725), 0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802); 0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
--sidebar-border-color: var(--color-surface-high); --sidebar-border-color: var(--color-gray-20);
--sidebar-bg-color: var(--island-bg-color); --sidebar-bg-color: #fff;
--library-dropdown-shadow: 0px 15px 6px rgba(0, 0, 0, 0.01), --library-dropdown-shadow: 0px 15px 6px rgba(0, 0, 0, 0.01),
0px 8px 5px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.09), 0px 8px 5px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.09),
0px 1px 2px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(0, 0, 0, 0.1); 0px 1px 2px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(0, 0, 0, 0.1);
--space-factor: 0.25rem; --space-factor: 0.25rem;
--text-primary-color: var(--color-on-surface); --text-primary-color: var(--color-gray-80);
--color-selection: #6965db; --color-selection: #6965db;
@@ -135,19 +132,6 @@
--border-radius-md: 0.375rem; --border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem; --border-radius-lg: 0.5rem;
--color-surface-high: hsl(244, 100%, 97%);
--color-surface-mid: hsl(240 25% 96%);
--color-surface-low: hsl(240 25% 94%);
--color-surface-lowest: #ffffff;
--color-on-surface: #1b1b1f;
--color-brand-hover: #5753d0;
--color-on-primary-container: #030064;
--color-surface-primary-container: #e0dfff;
--color-brand-active: #4440bf;
--color-border-outline: #767680;
--color-border-outline-variant: #c5c5d0;
--color-surface-primary-container: #e0dfff;
&.theme--dark { &.theme--dark {
&.theme--dark-background-none { &.theme--dark-background-none {
background: none; background: none;
@@ -166,24 +150,29 @@
--dialog-border-color: var(--color-gray-80); --dialog-border-color: var(--color-gray-80);
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>'); --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-6}; --focus-highlight-color: #{$oc-blue-6};
--icon-fill-color: var(--color-gray-40);
--icon-green-fill-color: #{$oc-green-4}; --icon-green-fill-color: #{$oc-green-4};
--default-bg-color: #121212; --default-bg-color: #121212;
--input-bg-color: #121212; --input-bg-color: #121212;
--input-border-color: #2e2e2e; --input-border-color: #2e2e2e;
--input-hover-bg-color: #181818; --input-hover-bg-color: #181818;
--input-label-color: #{$oc-gray-2}; --input-label-color: #{$oc-gray-2};
--island-bg-color: #232329; --island-bg-color: #262627;
--keybinding-color: var(--color-gray-60); --keybinding-color: var(--color-gray-60);
--link-color: #{$oc-blue-4}; --link-color: #{$oc-blue-4};
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)}; --overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
--popup-bg-color: #2c2c2c;
--popup-secondary-bg-color: #222; --popup-secondary-bg-color: #222;
--popup-text-color: #{$oc-gray-4}; --popup-text-color: #{$oc-gray-4};
--popup-text-inverted-color: #2c2c2c; --popup-text-inverted-color: #2c2c2c;
--select-highlight-color: #{$oc-blue-4}; --select-highlight-color: #{$oc-blue-4};
--shadow-island: 0px 0px 0.9310142993927002px 0px rgba(0, 0, 0, 0.17), --text-primary-color: var(--color-gray-40);
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08), --button-hover-bg: var(--color-gray-80);
0px 7px 14px 0px rgba(0, 0, 0, 0.05); --default-border-color: var(--color-gray-80);
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
0px 1.13px 4.13211px rgba(0, 0, 0, 0.035),
0px 0.769896px 1.4945px rgba(0, 0, 0, 0.0243888);
--modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07), --modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198), 0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275), 0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
@@ -191,6 +180,8 @@
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725), 0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802); 0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
--avatar-border-color: var(--color-gray-85); --avatar-border-color: var(--color-gray-85);
--sidebar-border-color: var(--color-gray-85);
--sidebar-bg-color: #191919;
--scrollbar-thumb: #{$oc-gray-8}; --scrollbar-thumb: #{$oc-gray-8};
--scrollbar-thumb-hover: #{$oc-gray-7}; --scrollbar-thumb-hover: #{$oc-gray-7};
@@ -233,18 +224,5 @@
--color-promo: #d297ff; --color-promo: #d297ff;
--color-logo-text: #e2dfff; --color-logo-text: #e2dfff;
--color-surface-high: hsl(245, 10%, 21%);
--color-surface-low: hsl(240, 8%, 15%);
--color-surface-mid: hsl(240 6% 10%);
--color-surface-lowest: hsl(0, 0%, 7%);
--color-on-surface: #e3e3e8;
--color-brand-hover: #bbb8ff;
--color-on-primary-container: #e0dfff;
--color-surface-primary-container: #403e6a;
--color-brand-active: #d0ccff;
--color-border-outline: #8e8d9c;
--color-border-outline-variant: #46464f;
--color-surface-primary-container: #403e6a;
} }
} }
+10 -30
View File
@@ -11,7 +11,7 @@
.ToolIcon_type_radio, .ToolIcon_type_radio,
.ToolIcon_type_checkbox { .ToolIcon_type_checkbox {
&:checked + .ToolIcon__icon { &:checked + .ToolIcon__icon {
--icon-fill-color: var(--color-on-primary-container); --icon-fill-color: var(--color-primary-darker);
svg { svg {
fill: var(--icon-fill-color); fill: var(--icon-fill-color);
@@ -23,11 +23,11 @@
.ToolIcon_type_radio, .ToolIcon_type_radio,
.ToolIcon_type_checkbox { .ToolIcon_type_checkbox {
&:checked + .ToolIcon__icon { &:checked + .ToolIcon__icon {
background: var(--color-surface-primary-container); background: var(--color-primary-light);
--keybinding-color: var(--color-on-primary-container); --keybinding-color: var(--color-gray-60);
svg { svg {
color: var(--color-on-primary-container); color: var(--color-primary-darker);
} }
} }
} }
@@ -44,11 +44,7 @@
&:active { &:active {
background: var(--button-hover-bg); background: var(--button-hover-bg);
border: 1px solid var(--button-active-border); border: 1px solid var(--color-primary-darkest);
svg {
color: var(--color-on-primary-container);
}
} }
} }
} }
@@ -67,7 +63,7 @@
border-radius: var(--border-radius-lg); border-radius: var(--border-radius-lg);
cursor: pointer; cursor: pointer;
background-color: var(--button-bg, var(--island-bg-color)); background-color: var(--button-bg, var(--island-bg-color));
color: var(--button-color, var(--color-on-surface)); color: var(--button-color, var(--text-primary-color));
svg { svg {
width: var(--button-width, var(--lg-icon-size)); width: var(--button-width, var(--lg-icon-size));
@@ -92,38 +88,22 @@
} }
&.active { &.active {
background-color: var( background-color: var(--button-selected-bg, var(--color-primary-light));
--button-selected-bg, border-color: var(--button-selected-border, var(--color-primary-light));
var(--color-surface-primary-container)
);
border-color: var(
--button-selected-border,
var(--color-surface-primary-container)
);
&:hover { &:hover {
background-color: var( background-color: var(
--button-selected-hover-bg, --button-selected-hover-bg,
var(--color-surface-primary-container) var(--color-primary-light)
); );
} }
svg { svg {
color: var(--button-color, var(--color-on-primary-container)); color: var(--button-color, var(--color-primary-darker));
} }
} }
} }
@mixin filledButtonOnCanvas {
border: none;
box-shadow: 0 0 0 1px var(--color-surface-lowest);
background-color: var(--color-surface-low);
&:active {
box-shadow: 0 0 0 1px var(--color-brand-active);
}
}
$theme-filter: "invert(93%) hue-rotate(180deg)"; $theme-filter: "invert(93%) hue-rotate(180deg)";
$right-sidebar-width: "302px"; $right-sidebar-width: "302px";
-105
View File
@@ -1,105 +0,0 @@
import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants";
import OpenColor from "open-color";
import { AppState, DataURL } from "./types";
import { isHandToolActive, isEraserActive } from "./appState";
const laserPointerCursorSVG_tag = `<svg viewBox="0 0 24 24" stroke-width="1" width="28" height="28" xmlns="http://www.w3.org/2000/svg">`;
const laserPointerCursorBackgroundSVG = `<path d="M6.164 11.755a5.314 5.314 0 0 1-4.932-5.298 5.314 5.314 0 0 1 5.311-5.311 5.314 5.314 0 0 1 5.307 5.113l8.773 8.773a3.322 3.322 0 0 1 0 4.696l-.895.895a3.322 3.322 0 0 1-4.696 0l-8.868-8.868Z" style="fill:#fff"/>`;
const laserPointerCursorIconSVG = `<path stroke="#1b1b1f" fill="#fff" d="m7.868 11.113 7.773 7.774a2.359 2.359 0 0 0 1.667.691 2.368 2.368 0 0 0 2.357-2.358c0-.625-.248-1.225-.69-1.667L11.201 7.78 9.558 9.469l-1.69 1.643v.001Zm10.273 3.606-3.333 3.333m-3.25-6.583 2 2m-7-7 3 3M3.664 3.625l1 1M2.529 6.922l1.407-.144m5.735-2.932-1.118.866M4.285 9.823l.758-1.194m1.863-6.207-.13 1.408"/>`;
const laserPointerCursorDataURL_lightMode = `data:${
MIME_TYPES.svg
},${encodeURIComponent(
`${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}</svg>`,
)}`;
const laserPointerCursorDataURL_darkMode = `data:${
MIME_TYPES.svg
},${encodeURIComponent(
`${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}</svg>`,
)}`;
export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
if (interactiveCanvas) {
interactiveCanvas.style.cursor = "";
}
};
export const setCursor = (
interactiveCanvas: HTMLCanvasElement | null,
cursor: string,
) => {
if (interactiveCanvas) {
interactiveCanvas.style.cursor = cursor;
}
};
let eraserCanvasCache: any;
let previewDataURL: string;
export const setEraserCursor = (
interactiveCanvas: HTMLCanvasElement | null,
theme: AppState["theme"],
) => {
const cursorImageSizePx = 20;
const drawCanvas = () => {
const isDarkTheme = theme === THEME.DARK;
eraserCanvasCache = document.createElement("canvas");
eraserCanvasCache.theme = theme;
eraserCanvasCache.height = cursorImageSizePx;
eraserCanvasCache.width = cursorImageSizePx;
const context = eraserCanvasCache.getContext("2d")!;
context.lineWidth = 1;
context.beginPath();
context.arc(
eraserCanvasCache.width / 2,
eraserCanvasCache.height / 2,
5,
0,
2 * Math.PI,
);
context.fillStyle = isDarkTheme ? OpenColor.black : OpenColor.white;
context.fill();
context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black;
context.stroke();
previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
};
if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
drawCanvas();
}
setCursor(
interactiveCanvas,
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
cursorImageSizePx / 2
}, auto`,
);
};
export const setCursorForShape = (
interactiveCanvas: HTMLCanvasElement | null,
appState: Pick<AppState, "activeTool" | "theme">,
) => {
if (!interactiveCanvas) {
return;
}
if (appState.activeTool.type === "selection") {
resetCursor(interactiveCanvas);
} else if (isHandToolActive(appState)) {
interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
} else if (isEraserActive(appState)) {
setEraserCursor(interactiveCanvas, appState.theme);
// do nothing if image tool is selected which suggests there's
// a image-preview set as the cursor
// Ignore custom type as well and let host decide
} else if (appState.activeTool.type === "laser") {
const url =
appState.theme === THEME.LIGHT
? laserPointerCursorDataURL_lightMode
: laserPointerCursorDataURL_darkMode;
interactiveCanvas.style.cursor = `url(${url}), auto`;
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
} else {
interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
}
};
+135 -135
View File
@@ -6,15 +6,15 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "#d8f5a2", "backgroundColor": "#d8f5a2",
"boundElements": [ "boundElements": [
{ {
"id": "id45", "id": "id40",
"type": "arrow", "type": "arrow",
}, },
{ {
"id": "id46", "id": "id41",
"type": "arrow", "type": "arrow",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 300, "height": 300,
@@ -28,7 +28,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#66a80f", "strokeColor": "#66a80f",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "ellipse", "type": "ellipse",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -45,11 +45,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id46", "id": "id41",
"type": "arrow", "type": "arrow",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -63,7 +63,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#9c36b5", "strokeColor": "#9c36b5",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "diamond", "type": "diamond",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -85,7 +85,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"focus": -0.008153707962747813, "focus": -0.008153707962747813,
"gap": 1, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 35, "height": 35,
@@ -110,13 +110,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id47", "elementId": "id42",
"focus": -0.08139534883720931, "focus": -0.08139534883720931,
"gap": 1, "gap": 1,
}, },
"strokeColor": "#1864ab", "strokeColor": "#1864ab",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -138,7 +138,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"focus": 0.10666666666666667, "focus": 0.10666666666666667,
"gap": 3.834326468444573, "gap": 3.834326468444573,
}, },
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -169,7 +169,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
}, },
"strokeColor": "#e67700", "strokeColor": "#e67700",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -186,11 +186,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id45", "id": "id40",
"type": "arrow", "type": "arrow",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 300, "height": 300,
@@ -204,7 +204,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -222,12 +222,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"baseline": 0, "baseline": 0,
"boundElements": [ "boundElements": [
{ {
"id": "id48", "id": "id43",
"type": "arrow", "type": "arrow",
}, },
], ],
"containerId": null, "containerId": null,
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -245,7 +245,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "HEYYYYY", "text": "HEYYYYY",
"textAlign": "left", "textAlign": "left",
"type": "text", "type": "text",
@@ -266,12 +266,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"baseline": 0, "baseline": 0,
"boundElements": [ "boundElements": [
{ {
"id": "id48", "id": "id43",
"type": "arrow", "type": "arrow",
}, },
], ],
"containerId": null, "containerId": null,
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -289,7 +289,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "Whats up ?", "text": "Whats up ?",
"textAlign": "left", "textAlign": "left",
"type": "text", "type": "text",
@@ -309,7 +309,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id49", "id": "id44",
"type": "text", "type": "text",
}, },
], ],
@@ -319,7 +319,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"focus": 0, "focus": 0,
"gap": 205, "gap": 205,
}, },
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -350,7 +350,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -367,8 +367,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id48", "containerId": "id43",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -386,7 +386,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "HELLO WORLD!!", "text": "HELLO WORLD!!",
"textAlign": "center", "textAlign": "center",
"type": "text", "type": "text",
@@ -406,17 +406,17 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id38", "id": "id33",
"type": "text", "type": "text",
}, },
], ],
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id40", "elementId": "id35",
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -441,13 +441,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id39", "elementId": "id34",
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -464,8 +464,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id37", "containerId": "id32",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -483,7 +483,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "HELLO WORLD!!", "text": "HELLO WORLD!!",
"textAlign": "center", "textAlign": "center",
"type": "text", "type": "text",
@@ -503,11 +503,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id37", "id": "id32",
"type": "arrow", "type": "arrow",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -521,7 +521,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -538,11 +538,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id37", "id": "id32",
"type": "arrow", "type": "arrow",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -556,7 +556,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "ellipse", "type": "ellipse",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -573,17 +573,17 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id42", "id": "id37",
"type": "text", "type": "text",
}, },
], ],
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id44", "elementId": "id39",
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -608,13 +608,13 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>, "seed": Any<Number>,
"startArrowhead": null, "startArrowhead": null,
"startBinding": { "startBinding": {
"elementId": "id43", "elementId": "id38",
"focus": 0, "focus": 0,
"gap": 1, "gap": 1,
}, },
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -631,8 +631,8 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id41", "containerId": "id36",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "HELLO WORLD!!", "text": "HELLO WORLD!!",
"textAlign": "center", "textAlign": "center",
"type": "text", "type": "text",
@@ -671,12 +671,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"baseline": 0, "baseline": 0,
"boundElements": [ "boundElements": [
{ {
"id": "id41", "id": "id36",
"type": "arrow", "type": "arrow",
}, },
], ],
"containerId": null, "containerId": null,
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -694,7 +694,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "HEYYYYY", "text": "HEYYYYY",
"textAlign": "left", "textAlign": "left",
"type": "text", "type": "text",
@@ -715,12 +715,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"baseline": 0, "baseline": 0,
"boundElements": [ "boundElements": [
{ {
"id": "id41", "id": "id36",
"type": "arrow", "type": "arrow",
}, },
], ],
"containerId": null, "containerId": null,
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -738,7 +738,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "WHATS UP ?", "text": "WHATS UP ?",
"textAlign": "left", "textAlign": "left",
"type": "text", "type": "text",
@@ -757,7 +757,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 200, "height": 200,
@@ -771,7 +771,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -789,7 +789,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
"boundElements": null, "boundElements": null,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -816,7 +816,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
"startBinding": null, "startBinding": null,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -834,7 +834,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
"boundElements": null, "boundElements": null,
"endArrowhead": "triangle", "endArrowhead": "triangle",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -879,7 +879,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
"boundElements": null, "boundElements": null,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -906,7 +906,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
"startBinding": null, "startBinding": null,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "line", "type": "line",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -924,7 +924,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
"boundElements": null, "boundElements": null,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -967,7 +967,7 @@ exports[`Test Transform > should transform regular shapes 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -981,7 +981,7 @@ exports[`Test Transform > should transform regular shapes 1`] = `
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -997,7 +997,7 @@ exports[`Test Transform > should transform regular shapes 2`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -1011,7 +1011,7 @@ exports[`Test Transform > should transform regular shapes 2`] = `
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "ellipse", "type": "ellipse",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -1027,7 +1027,7 @@ exports[`Test Transform > should transform regular shapes 3`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -1041,7 +1041,7 @@ exports[`Test Transform > should transform regular shapes 3`] = `
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "diamond", "type": "diamond",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -1057,7 +1057,7 @@ exports[`Test Transform > should transform regular shapes 4`] = `
"angle": 0, "angle": 0,
"backgroundColor": "#c0eb75", "backgroundColor": "#c0eb75",
"boundElements": null, "boundElements": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 100, "height": 100,
@@ -1149,7 +1149,7 @@ exports[`Test Transform > should transform text element 1`] = `
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": null, "containerId": null,
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1167,7 +1167,7 @@ exports[`Test Transform > should transform text element 1`] = `
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "HELLO WORLD!", "text": "HELLO WORLD!",
"textAlign": "left", "textAlign": "left",
"type": "text", "type": "text",
@@ -1188,7 +1188,7 @@ exports[`Test Transform > should transform text element 2`] = `
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": null, "containerId": null,
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1206,7 +1206,7 @@ exports[`Test Transform > should transform text element 2`] = `
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#5f3dc4", "strokeColor": "#5f3dc4",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "STYLED HELLO WORLD!", "text": "STYLED HELLO WORLD!",
"textAlign": "left", "textAlign": "left",
"type": "text", "type": "text",
@@ -1226,13 +1226,13 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id29", "id": "id28",
"type": "text", "type": "text",
}, },
], ],
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -1259,7 +1259,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"startBinding": null, "startBinding": null,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -1276,13 +1276,13 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id30", "id": "id29",
"type": "text", "type": "text",
}, },
], ],
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 0,
@@ -1309,7 +1309,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"startBinding": null, "startBinding": null,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 1, "version": 1,
@@ -1326,16 +1326,16 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id31", "id": "id30",
"type": "text", "type": "text",
}, },
], ],
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 130,
"id": Any<String>, "id": Any<String>,
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@@ -1362,7 +1362,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 1, "version": 2,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 100, "width": 100,
"x": 100, "x": 100,
@@ -1376,16 +1376,16 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id32", "id": "id31",
"type": "text", "type": "text",
}, },
], ],
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 0, "height": 130,
"id": Any<String>, "id": Any<String>,
"isDeleted": false, "isDeleted": false,
"lastCommittedPoint": null, "lastCommittedPoint": null,
@@ -1412,7 +1412,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"strokeWidth": 2, "strokeWidth": 2,
"type": "arrow", "type": "arrow",
"updated": 1, "updated": 1,
"version": 1, "version": 2,
"versionNonce": Any<Number>, "versionNonce": Any<Number>,
"width": 100, "width": 100,
"x": 100, "x": 100,
@@ -1426,8 +1426,8 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id25", "containerId": "id24",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1445,7 +1445,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "LABELED ARROW", "text": "LABELED ARROW",
"textAlign": "center", "textAlign": "center",
"type": "text", "type": "text",
@@ -1465,8 +1465,8 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id26", "containerId": "id25",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1484,7 +1484,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#099268", "strokeColor": "#099268",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "STYLED LABELED ARROW", "text": "STYLED LABELED ARROW",
"textAlign": "center", "textAlign": "center",
"type": "text", "type": "text",
@@ -1504,8 +1504,8 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id27", "containerId": "id26",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1523,7 +1523,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1098ad", "strokeColor": "#1098ad",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "ANOTHER STYLED "text": "ANOTHER STYLED
LABELLED ARROW", LABELLED ARROW",
"textAlign": "center", "textAlign": "center",
@@ -1544,8 +1544,8 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id28", "containerId": "id27",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1563,7 +1563,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#099268", "strokeColor": "#099268",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "ANOTHER STYLED "text": "ANOTHER STYLED
LABELLED ARROW", LABELLED ARROW",
"textAlign": "center", "textAlign": "center",
@@ -1584,11 +1584,11 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id19", "id": "id18",
"type": "text", "type": "text",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 35, "height": 35,
@@ -1602,7 +1602,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
@@ -1619,11 +1619,11 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id20", "id": "id19",
"type": "text", "type": "text",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 85, "height": 85,
@@ -1637,7 +1637,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "ellipse", "type": "ellipse",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -1654,11 +1654,11 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id21", "id": "id20",
"type": "text", "type": "text",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 170, "height": 170,
@@ -1672,7 +1672,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "diamond", "type": "diamond",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -1689,11 +1689,11 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "#fff3bf", "backgroundColor": "#fff3bf",
"boundElements": [ "boundElements": [
{ {
"id": "id22", "id": "id21",
"type": "text", "type": "text",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 120, "height": 120,
@@ -1724,11 +1724,11 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [ "boundElements": [
{ {
"id": "id23", "id": "id22",
"type": "text", "type": "text",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 85, "height": 85,
@@ -1742,7 +1742,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -1759,11 +1759,11 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "#ffec99", "backgroundColor": "#ffec99",
"boundElements": [ "boundElements": [
{ {
"id": "id24", "id": "id23",
"type": "text", "type": "text",
}, },
], ],
"fillStyle": "solid", "fillStyle": "hachure",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
"height": 120, "height": 120,
@@ -1777,7 +1777,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#f08c00", "strokeColor": "#f08c00",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"type": "ellipse", "type": "ellipse",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
@@ -1794,8 +1794,8 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id13", "containerId": "id12",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1813,7 +1813,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "RECTANGLE TEXT CONTAINER", "text": "RECTANGLE TEXT CONTAINER",
"textAlign": "center", "textAlign": "center",
"type": "text", "type": "text",
@@ -1833,8 +1833,8 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id14", "containerId": "id13",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1852,7 +1852,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "ELLIPSE TEXT "text": "ELLIPSE TEXT
CONTAINER", CONTAINER",
"textAlign": "center", "textAlign": "center",
@@ -1873,8 +1873,8 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id15", "containerId": "id14",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1893,7 +1893,7 @@ TEXT CONTAINER",
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "DIAMOND "text": "DIAMOND
TEXT TEXT
CONTAINER", CONTAINER",
@@ -1915,8 +1915,8 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id16", "containerId": "id15",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1934,7 +1934,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#099268", "strokeColor": "#099268",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "STYLED DIAMOND "text": "STYLED DIAMOND
TEXT CONTAINER", TEXT CONTAINER",
"textAlign": "center", "textAlign": "center",
@@ -1955,8 +1955,8 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id17", "containerId": "id16",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -1974,7 +1974,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "TOP LEFT ALIGNED "text": "TOP LEFT ALIGNED
RECTANGLE TEXT RECTANGLE TEXT
CONTAINER", CONTAINER",
@@ -1996,8 +1996,8 @@ exports[`Test Transform > should transform to text containers when label provide
"backgroundColor": "transparent", "backgroundColor": "transparent",
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id18", "containerId": "id17",
"fillStyle": "solid", "fillStyle": "hachure",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
"frameId": null, "frameId": null,
@@ -2015,7 +2015,7 @@ exports[`Test Transform > should transform to text containers when label provide
"seed": Any<Number>, "seed": Any<Number>,
"strokeColor": "#c2255c", "strokeColor": "#c2255c",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 2, "strokeWidth": 1,
"text": "STYLED "text": "STYLED
ELLIPSE TEXT ELLIPSE TEXT
CONTAINER", CONTAINER",
+3 -31
View File
@@ -8,7 +8,7 @@ import { t } from "../i18n";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types"; import { AppState, DataURL, LibraryItem } from "../types";
import { ValueOf } from "../utility-types"; import { ValueOf } from "../utility-types";
import { bytesToHexString, isPromiseLike } from "../utils"; import { bytesToHexString } from "../utils";
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem"; import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json"; import { isValidExcalidrawData, isValidLibrary } from "./json";
import { restore, restoreLibraryItems } from "./restore"; import { restore, restoreLibraryItems } from "./restore";
@@ -207,13 +207,10 @@ export const loadLibraryFromBlob = async (
}; };
export const canvasToBlob = async ( export const canvasToBlob = async (
canvas: HTMLCanvasElement | Promise<HTMLCanvasElement>, canvas: HTMLCanvasElement,
): Promise<Blob> => { ): Promise<Blob> => {
return new Promise(async (resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
if (isPromiseLike(canvas)) {
canvas = await canvas;
}
canvas.toBlob((blob) => { canvas.toBlob((blob) => {
if (!blob) { if (!blob) {
return reject( return reject(
@@ -327,31 +324,6 @@ export const SVGStringToFile = (SVGString: string, filename: string = "") => {
}) as File & { type: typeof MIME_TYPES.svg }; }) as File & { type: typeof MIME_TYPES.svg };
}; };
export const ImageURLToFile = async (
imageUrl: string,
filename: string = "",
): Promise<File | undefined> => {
let response;
try {
response = await fetch(imageUrl);
} catch (error: any) {
throw new Error(t("errors.failedToFetchImage"));
}
if (!response.ok) {
throw new Error(t("errors.failedToFetchImage"));
}
const blob = await response.blob();
if (blob.type && isSupportedImageFile(blob)) {
const name = filename || blob.name || "";
return new File([blob], name, { type: blob.type });
}
throw new Error(t("errors.unsupportedFileType"));
};
export const getFileFromEvent = async ( export const getFileFromEvent = async (
event: React.DragEvent<HTMLDivElement>, event: React.DragEvent<HTMLDivElement>,
) => { ) => {
+7 -1
View File
@@ -66,14 +66,17 @@ export const exportCanvas = async (
} }
} }
const tempCanvas = exportToCanvas(elements, appState, files, { const tempCanvas = await exportToCanvas(elements, appState, files, {
exportBackground, exportBackground,
viewBackgroundColor, viewBackgroundColor,
exportPadding, exportPadding,
}); });
tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas);
if (type === "png") { if (type === "png") {
let blob = await canvasToBlob(tempCanvas); let blob = await canvasToBlob(tempCanvas);
tempCanvas.remove();
if (appState.exportEmbedScene) { if (appState.exportEmbedScene) {
blob = await ( blob = await (
await import(/* webpackChunkName: "image" */ "./image") await import(/* webpackChunkName: "image" */ "./image")
@@ -111,8 +114,11 @@ export const exportCanvas = async (
} else { } else {
throw new Error(t("alerts.couldNotCopyToClipboard")); throw new Error(t("alerts.couldNotCopyToClipboard"));
} }
} finally {
tempCanvas.remove();
} }
} else { } else {
tempCanvas.remove();
// shouldn't happen // shouldn't happen
throw new Error("Unsupported export type"); throw new Error("Unsupported export type");
} }
+2 -11
View File
@@ -67,7 +67,7 @@ export const AllowedExcalidrawActiveTools: Record<
frame: true, frame: true,
embeddable: true, embeddable: true,
hand: true, hand: true,
laser: false, mermaid: true,
}; };
export type RestoredDataState = { export type RestoredDataState = {
@@ -189,7 +189,7 @@ const restoreElement = (
fontSize = parseFloat(fontPx); fontSize = parseFloat(fontPx);
fontFamily = getFontFamilyByName(_fontFamily); fontFamily = getFontFamilyByName(_fontFamily);
} }
const text = (typeof element.text === "string" && element.text) || ""; const text = element.text ?? "";
// line-height might not be specified either when creating elements // line-height might not be specified either when creating elements
// programmatically, or when importing old diagrams. // programmatically, or when importing old diagrams.
@@ -222,17 +222,9 @@ const restoreElement = (
baseline, baseline,
}); });
// if empty text, mark as deleted. We keep in array
// for data integrity purposes (collab etc.)
if (!text && !element.isDeleted) {
element = { ...element, originalText: text, isDeleted: true };
element = bumpVersion(element);
}
if (refreshDimensions) { if (refreshDimensions) {
element = { ...element, ...refreshTextDimensions(element) }; element = { ...element, ...refreshTextDimensions(element) };
} }
return element; return element;
case "freedraw": { case "freedraw": {
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
@@ -307,7 +299,6 @@ const restoreElement = (
// We also don't want to throw, but instead return void so we filter // We also don't want to throw, but instead return void so we filter
// out these unsupported elements from the restored array. // out these unsupported elements from the restored array.
} }
return null;
}; };
/** /**
-120
View File
@@ -5,31 +5,7 @@ import {
} from "./transform"; } from "./transform";
import { ExcalidrawArrowElement } from "../element/types"; import { ExcalidrawArrowElement } from "../element/types";
const opts = { regenerateIds: false };
describe("Test Transform", () => { describe("Test Transform", () => {
it("should generate id unless opts.regenerateIds is set to false explicitly", () => {
const elements = [
{
type: "rectangle",
x: 100,
y: 100,
id: "rect-1",
},
];
let data = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
);
expect(data.length).toBe(1);
expect(data[0].id).toBe("id0");
data = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[],
opts,
);
expect(data[0].id).toBe("rect-1");
});
it("should transform regular shapes", () => { it("should transform regular shapes", () => {
const elements = [ const elements = [
{ {
@@ -83,7 +59,6 @@ describe("Test Transform", () => {
convertToExcalidrawElements( convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
).forEach((ele) => { ).forEach((ele) => {
expect(ele).toMatchSnapshot({ expect(ele).toMatchSnapshot({
seed: expect.any(Number), seed: expect.any(Number),
@@ -112,7 +87,6 @@ describe("Test Transform", () => {
]; ];
convertToExcalidrawElements( convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
).forEach((ele) => { ).forEach((ele) => {
expect(ele).toMatchSnapshot({ expect(ele).toMatchSnapshot({
seed: expect.any(Number), seed: expect.any(Number),
@@ -154,7 +128,6 @@ describe("Test Transform", () => {
]; ];
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(4); expect(excaldrawElements.length).toBe(4);
@@ -237,7 +210,6 @@ describe("Test Transform", () => {
]; ];
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(12); expect(excaldrawElements.length).toBe(12);
@@ -295,7 +267,6 @@ describe("Test Transform", () => {
]; ];
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(8); expect(excaldrawElements.length).toBe(8);
@@ -309,90 +280,6 @@ describe("Test Transform", () => {
}); });
}); });
describe("Test Frames", () => {
it("should transform frames and update frame ids when regenerated", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
{
type: "frame",
children: ["1", "2"],
name: "My frame",
},
];
const excaldrawElements = convertToExcalidrawElements(
elementsSkeleton,
opts,
);
expect(excaldrawElements.length).toBe(4);
excaldrawElements.forEach((ele) => {
expect(ele).toMatchObject({
seed: expect.any(Number),
versionNonce: expect.any(Number),
id: expect.any(String),
});
});
});
it("should consider max of calculated and frame dimensions when provided", () => {
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
{
type: "rectangle",
x: 10,
y: 10,
strokeWidth: 2,
id: "1",
},
{
type: "diamond",
x: 120,
y: 20,
backgroundColor: "#fff3bf",
strokeWidth: 2,
label: {
text: "HELLO EXCALIDRAW",
strokeColor: "#099268",
fontSize: 30,
},
id: "2",
},
{
type: "frame",
children: ["1", "2"],
name: "My frame",
width: 800,
height: 100,
},
];
const excaldrawElements = convertToExcalidrawElements(
elementsSkeleton,
opts,
);
const frame = excaldrawElements.find((ele) => ele.type === "frame")!;
expect(frame.width).toBe(800);
expect(frame.height).toBe(126);
});
});
describe("Test arrow bindings", () => { describe("Test arrow bindings", () => {
it("should bind arrows to shapes when start / end provided without ids", () => { it("should bind arrows to shapes when start / end provided without ids", () => {
const elements = [ const elements = [
@@ -413,7 +300,6 @@ describe("Test Transform", () => {
]; ];
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(4); expect(excaldrawElements.length).toBe(4);
@@ -497,7 +383,6 @@ describe("Test Transform", () => {
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(4); expect(excaldrawElements.length).toBe(4);
@@ -613,7 +498,6 @@ describe("Test Transform", () => {
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(5); expect(excaldrawElements.length).toBe(5);
@@ -662,7 +546,6 @@ describe("Test Transform", () => {
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(4); expect(excaldrawElements.length).toBe(4);
@@ -716,7 +599,6 @@ describe("Test Transform", () => {
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(4); expect(excaldrawElements.length).toBe(4);
@@ -767,7 +649,6 @@ describe("Test Transform", () => {
]; ];
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(2); expect(excaldrawElements.length).toBe(2);
const [arrow, rect] = excaldrawElements; const [arrow, rect] = excaldrawElements;
@@ -810,7 +691,6 @@ describe("Test Transform", () => {
]; ];
const excaldrawElements = convertToExcalidrawElements( const excaldrawElements = convertToExcalidrawElements(
elements as ExcalidrawElementSkeleton[], elements as ExcalidrawElementSkeleton[],
opts,
); );
expect(excaldrawElements.length).toBe(1); expect(excaldrawElements.length).toBe(1);
+12 -81
View File
@@ -5,7 +5,6 @@ import {
VERTICAL_ALIGN, VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import { import {
getCommonBounds,
newElement, newElement,
newLinearElement, newLinearElement,
redrawTextBoundingBox, redrawTextBoundingBox,
@@ -13,7 +12,6 @@ import {
import { bindLinearElement } from "../element/binding"; import { bindLinearElement } from "../element/binding";
import { import {
ElementConstructorOpts, ElementConstructorOpts,
newFrameElement,
newImageElement, newImageElement,
newTextElement, newTextElement,
} from "../element/newElement"; } from "../element/newElement";
@@ -42,7 +40,7 @@ import {
import { MarkOptional } from "../utility-types"; import { MarkOptional } from "../utility-types";
import { assertNever, getFontString } from "../utils"; import { assertNever, getFontString } from "../utils";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { randomId } from "../random"; import { nanoid } from "nanoid";
export type ValidLinearElement = { export type ValidLinearElement = {
type: "arrow" | "line"; type: "arrow" | "line";
@@ -137,7 +135,9 @@ export type ValidContainer =
export type ExcalidrawElementSkeleton = export type ExcalidrawElementSkeleton =
| Extract< | Extract<
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>, Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
ExcalidrawEmbeddableElement | ExcalidrawFreeDrawElement | ExcalidrawEmbeddableElement
| ExcalidrawFreeDrawElement
| ExcalidrawFrameElement
> >
| ({ | ({
type: Extract<ExcalidrawLinearElement["type"], "line">; type: Extract<ExcalidrawLinearElement["type"], "line">;
@@ -158,12 +158,7 @@ export type ExcalidrawElementSkeleton =
x: number; x: number;
y: number; y: number;
fileId: FileId; fileId: FileId;
} & Partial<ExcalidrawImageElement>) } & Partial<ExcalidrawImageElement>);
| ({
type: "frame";
children: readonly ExcalidrawElement["id"][];
name?: string;
} & Partial<ExcalidrawFrameElement>);
const DEFAULT_LINEAR_ELEMENT_PROPS = { const DEFAULT_LINEAR_ELEMENT_PROPS = {
width: 100, width: 100,
@@ -416,7 +411,7 @@ const bindLinearElementToElement = (
class ElementStore { class ElementStore {
excalidrawElements = new Map<string, ExcalidrawElement>(); excalidrawElements = new Map<string, ExcalidrawElement>();
add = (ele?: ExcalidrawElement) => { add = (ele?: ExcalidrawElement, originalId?: string) => {
if (!ele) { if (!ele) {
return; return;
} }
@@ -433,15 +428,13 @@ class ElementStore {
} }
export const convertToExcalidrawElements = ( export const convertToExcalidrawElements = (
elementsSkeleton: ExcalidrawElementSkeleton[] | null, elements: ExcalidrawElementSkeleton[] | null,
opts?: { regenerateIds: boolean }, opts?: { regenerateIds: boolean },
) => { ) => {
if (!elementsSkeleton) { if (!elements) {
return []; return [];
} }
const elements: ExcalidrawElementSkeleton[] = JSON.parse(
JSON.stringify(elementsSkeleton),
);
const elementStore = new ElementStore(); const elementStore = new ElementStore();
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>(); const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
const oldToNewElementIdMap = new Map<string, string>(); const oldToNewElementIdMap = new Map<string, string>();
@@ -450,8 +443,8 @@ export const convertToExcalidrawElements = (
for (const element of elements) { for (const element of elements) {
let excalidrawElement: ExcalidrawElement; let excalidrawElement: ExcalidrawElement;
const originalId = element.id; const originalId = element.id;
if (opts?.regenerateIds !== false) { if (opts?.regenerateIds) {
Object.assign(element, { id: randomId() }); Object.assign(element, { id: nanoid() });
} }
switch (element.type) { switch (element.type) {
@@ -540,15 +533,8 @@ export const convertToExcalidrawElements = (
break; break;
} }
case "frame": {
excalidrawElement = newFrameElement({
x: 0,
y: 0,
...element,
});
break;
}
case "freedraw": case "freedraw":
case "frame":
case "embeddable": { case "embeddable": {
excalidrawElement = element; excalidrawElement = element;
break; break;
@@ -652,60 +638,5 @@ export const convertToExcalidrawElements = (
} }
} }
} }
// Once all the excalidraw elements are created, we can add frames since we
// need to calculate coordinates and dimensions of frame which is possibe after all
// frame children are processed.
for (const [id, element] of elementsWithIds) {
if (element.type !== "frame") {
continue;
}
const frame = elementStore.getElement(id);
if (!frame) {
throw new Error(`Excalidraw element with id ${id} doesn't exist`);
}
const childrenElements: ExcalidrawElement[] = [];
element.children.forEach((id) => {
const newElementId = oldToNewElementIdMap.get(id);
if (!newElementId) {
throw new Error(`Element with ${id} wasn't mapped correctly`);
}
const elementInFrame = elementStore.getElement(newElementId);
if (!elementInFrame) {
throw new Error(`Frame element with id ${newElementId} doesn't exist`);
}
Object.assign(elementInFrame, { frameId: frame.id });
elementInFrame?.boundElements?.forEach((boundElement) => {
const ele = elementStore.getElement(boundElement.id);
if (!ele) {
throw new Error(
`Bound element with id ${boundElement.id} doesn't exist`,
);
}
Object.assign(ele, { frameId: frame.id });
childrenElements.push(ele);
});
childrenElements.push(elementInFrame);
});
let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements);
const PADDING = 10;
minX = minX - PADDING;
minY = minY - PADDING;
maxX = maxX + PADDING;
maxY = maxY + PADDING;
// Take the max of calculated and provided frame dimensions, whichever is higher
const width = Math.max(frame?.width, maxX - minX);
const height = Math.max(frame?.height, maxY - minY);
Object.assign(frame, { x: minX, y: minY, width, height });
}
return elementStore.getElements(); return elementStore.getElements();
}; };
+1 -1
View File
@@ -392,7 +392,7 @@ export const getLinkHandleFromCoords = (
[x1, y1, x2, y2]: Bounds, [x1, y1, x2, y2]: Bounds,
angle: number, angle: number,
appState: Pick<UIAppState, "zoom">, appState: Pick<UIAppState, "zoom">,
): Bounds => { ): [x: number, y: number, width: number, height: number] => {
const size = DEFAULT_LINK_SIZE; const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value; const linkWidth = size / appState.zoom.value;
const linkHeight = size / appState.zoom.value; const linkHeight = size / appState.zoom.value;
+17 -30
View File
@@ -34,12 +34,7 @@ export type RectangleBox = {
type MaybeQuadraticSolution = [number | null, number | null] | false; type MaybeQuadraticSolution = [number | null, number | null] | false;
// x and y position of top left corner, x and y position of bottom right corner // x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [ export type Bounds = readonly [x1: number, y1: number, x2: number, y2: number];
minX: number,
minY: number,
maxX: number,
maxY: number,
];
export class ElementBounds { export class ElementBounds {
private static boundsCache = new WeakMap< private static boundsCache = new WeakMap<
@@ -68,7 +63,7 @@ export class ElementBounds {
} }
private static calculateBounds(element: ExcalidrawElement): Bounds { private static calculateBounds(element: ExcalidrawElement): Bounds {
let bounds: Bounds; let bounds: [number, number, number, number];
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
@@ -163,7 +158,7 @@ export const getElementAbsoluteCoords = (
]; ];
}; };
/* /**
* for a given element, `getElementLineSegments` returns line segments * for a given element, `getElementLineSegments` returns line segments
* that can be used for visual collision detection (useful for frames) * that can be used for visual collision detection (useful for frames)
* as opposed to bounding box collision detection * as opposed to bounding box collision detection
@@ -392,7 +387,7 @@ const getCubicBezierCurveBound = (
export const getMinMaxXYFromCurvePathOps = ( export const getMinMaxXYFromCurvePathOps = (
ops: Op[], ops: Op[],
transformXY?: (x: number, y: number) => [number, number], transformXY?: (x: number, y: number) => [number, number],
): Bounds => { ): [number, number, number, number] => {
let currentP: Point = [0, 0]; let currentP: Point = [0, 0];
const { minX, minY, maxX, maxY } = ops.reduce( const { minX, minY, maxX, maxY } = ops.reduce(
@@ -440,9 +435,9 @@ export const getMinMaxXYFromCurvePathOps = (
return [minX, minY, maxX, maxY]; return [minX, minY, maxX, maxY];
}; };
export const getBoundsFromPoints = ( const getBoundsFromPoints = (
points: ExcalidrawFreeDrawElement["points"], points: ExcalidrawFreeDrawElement["points"],
): Bounds => { ): [number, number, number, number] => {
let minX = Infinity; let minX = Infinity;
let minY = Infinity; let minY = Infinity;
let maxX = -Infinity; let maxX = -Infinity;
@@ -594,7 +589,7 @@ const getLinearElementRotatedBounds = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
cx: number, cx: number,
cy: number, cy: number,
): Bounds => { ): [number, number, number, number] => {
if (element.points.length < 2) { if (element.points.length < 2) {
const [pointX, pointY] = element.points[0]; const [pointX, pointY] = element.points[0];
const [x, y] = rotate( const [x, y] = rotate(
@@ -605,7 +600,7 @@ const getLinearElementRotatedBounds = (
element.angle, element.angle,
); );
let coords: Bounds = [x, y, x, y]; let coords: [number, number, number, number] = [x, y, x, y];
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
@@ -630,7 +625,12 @@ const getLinearElementRotatedBounds = (
const transformXY = (x: number, y: number) => const transformXY = (x: number, y: number) =>
rotate(element.x + x, element.y + y, cx, cy, element.angle); rotate(element.x + x, element.y + y, cx, cy, element.angle);
const res = getMinMaxXYFromCurvePathOps(ops, transformXY); const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
let coords: Bounds = [res[0], res[1], res[2], res[3]]; let coords: [number, number, number, number] = [
res[0],
res[1],
res[2],
res[3],
];
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
@@ -674,25 +674,12 @@ export const getCommonBounds = (
return [minX, minY, maxX, maxY]; return [minX, minY, maxX, maxY];
}; };
export const getDraggedElementsBounds = (
elements: ExcalidrawElement[],
dragOffset: { x: number; y: number },
) => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return [
minX + dragOffset.x,
minY + dragOffset.y,
maxX + dragOffset.x,
maxY + dragOffset.y,
];
};
export const getResizedElementAbsoluteCoords = ( export const getResizedElementAbsoluteCoords = (
element: ExcalidrawElement, element: ExcalidrawElement,
nextWidth: number, nextWidth: number,
nextHeight: number, nextHeight: number,
normalizePoints: boolean, normalizePoints: boolean,
): Bounds => { ): [number, number, number, number] => {
if (!(isLinearElement(element) || isFreeDrawElement(element))) { if (!(isLinearElement(element) || isFreeDrawElement(element))) {
return [ return [
element.x, element.x,
@@ -709,7 +696,7 @@ export const getResizedElementAbsoluteCoords = (
normalizePoints, normalizePoints,
); );
let bounds: Bounds; let bounds: [number, number, number, number];
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
// Free Draw // Free Draw
@@ -740,7 +727,7 @@ export const getResizedElementAbsoluteCoords = (
export const getElementPointsCoords = ( export const getElementPointsCoords = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
points: readonly (readonly [number, number])[], points: readonly (readonly [number, number])[],
): Bounds => { ): [number, number, number, number] => {
// This might be computationally heavey // This might be computationally heavey
const gen = rough.generator(); const gen = rough.generator();
const curve = const curve =
+1 -3
View File
@@ -494,9 +494,7 @@ const hitTestFreeDrawElement = (
// for filled freedraw shapes, support // for filled freedraw shapes, support
// selecting from inside // selecting from inside
if (shape && shape.sets.length) { if (shape && shape.sets.length) {
return element.fillStyle === "solid" return hitTestRoughShape(shape, x, y, threshold);
? hitTestCurveInside(shape, x, y, "round")
: hitTestRoughShape(shape, x, y, threshold);
} }
return false; return false;
+45 -68
View File
@@ -1,12 +1,11 @@
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { Bounds, getCommonBounds } from "./bounds"; import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import { NonDeletedExcalidrawElement } from "./types"; import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types"; import { AppState, PointerDownState } from "../types";
import { getBoundTextElement } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { isSelectedViaGroup } from "../groups"; import { isSelectedViaGroup } from "../groups";
import { getGridPoint } from "../math";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { import {
isArrowElement, isArrowElement,
@@ -17,15 +16,17 @@ import {
export const dragSelectedElements = ( export const dragSelectedElements = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
offset: { x: number; y: number }, pointerX: number,
pointerY: number,
lockDirection: boolean = false,
distanceX: number = 0,
distanceY: number = 0,
appState: AppState, appState: AppState,
scene: Scene, scene: Scene,
snapOffset: {
x: number;
y: number;
},
gridSize: AppState["gridSize"],
) => { ) => {
const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 };
// 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
@@ -46,20 +47,15 @@ export const dragSelectedElements = (
elementsInFrames.forEach((element) => elementsToUpdate.add(element)); elementsInFrames.forEach((element) => elementsToUpdate.add(element));
} }
const commonBounds = getCommonBounds(
Array.from(elementsToUpdate).map(
(el) => pointerDownState.originalElements.get(el.id) ?? el,
),
);
const adjustedOffset = calculateOffset(
commonBounds,
offset,
snapOffset,
gridSize,
);
elementsToUpdate.forEach((element) => { elementsToUpdate.forEach((element) => {
updateElementCoords(pointerDownState, element, adjustedOffset); updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
element,
offset,
);
// update coords of bound text only if we're dragging the container directly // update coords of bound text only if we're dragging the container directly
// (we don't drag the group that it's part of) // (we don't drag the group that it's part of)
if ( if (
@@ -73,7 +69,14 @@ export const dragSelectedElements = (
) { ) {
const textElement = getBoundTextElement(element); const textElement = getBoundTextElement(element);
if (textElement) { if (textElement) {
updateElementCoords(pointerDownState, textElement, adjustedOffset); updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
textElement,
offset,
);
} }
} }
updateBoundElements(element, { updateBoundElements(element, {
@@ -82,54 +85,32 @@ export const dragSelectedElements = (
}); });
}; };
const calculateOffset = (
commonBounds: Bounds,
dragOffset: { x: number; y: number },
snapOffset: { x: number; y: number },
gridSize: AppState["gridSize"],
): { x: number; y: number } => {
const [x, y] = commonBounds;
let nextX = x + dragOffset.x + snapOffset.x;
let nextY = y + dragOffset.y + snapOffset.y;
if (snapOffset.x === 0 || snapOffset.y === 0) {
const [nextGridX, nextGridY] = getGridPoint(
x + dragOffset.x,
y + dragOffset.y,
gridSize,
);
if (snapOffset.x === 0) {
nextX = nextGridX;
}
if (snapOffset.y === 0) {
nextY = nextGridY;
}
}
return {
x: nextX - x,
y: nextY - y,
};
};
const updateElementCoords = ( const updateElementCoords = (
lockDirection: boolean,
distanceX: number,
distanceY: number,
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
dragOffset: { x: number; y: number }, offset: { x: number; y: number },
) => { ) => {
const originalElement = let x: number;
pointerDownState.originalElements.get(element.id) ?? element; let y: number;
if (lockDirection) {
const nextX = originalElement.x + dragOffset.x; const lockX = lockDirection && distanceX < distanceY;
const nextY = originalElement.y + dragOffset.y; const lockY = lockDirection && distanceX > distanceY;
const original = pointerDownState.originalElements.get(element.id);
x = lockX && original ? original.x : element.x + offset.x;
y = lockY && original ? original.y : element.y + offset.y;
} else {
x = element.x + offset.x;
y = element.y + offset.y;
}
mutateElement(element, { mutateElement(element, {
x: nextX, x,
y: nextY, y,
}); });
}; };
export const getDragOffsetXY = ( export const getDragOffsetXY = (
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
x: number, x: number,
@@ -153,10 +134,6 @@ export const dragNewElement = (
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is /** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
true */ true */
widthAspectRatio?: number | null, widthAspectRatio?: number | null,
originOffset: {
x: number;
y: number;
} | null = null,
) => { ) => {
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") { if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
if (widthAspectRatio) { if (widthAspectRatio) {
@@ -197,8 +174,8 @@ export const dragNewElement = (
if (width !== 0 && height !== 0) { if (width !== 0 && height !== 0) {
mutateElement(draggingElement, { mutateElement(draggingElement, {
x: newX + (originOffset?.x ?? 0), x: newX,
y: newY + (originOffset?.y ?? 0), y: newY,
width, width,
height, height,
}); });
+1 -11
View File
@@ -2,8 +2,7 @@ import { register } from "../actions/register";
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants"; import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
import { t } from "../i18n"; import { t } from "../i18n";
import { ExcalidrawProps } from "../types"; import { ExcalidrawProps } from "../types";
import { getFontString, updateActiveTool } from "../utils"; import { getFontString, setCursorForShape, updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { newTextElement } from "./newElement"; import { newTextElement } from "./newElement";
import { getContainerElement, wrapText } from "./textElement"; import { getContainerElement, wrapText } from "./textElement";
import { isEmbeddableElement } from "./typeChecks"; import { isEmbeddableElement } from "./typeChecks";
@@ -28,7 +27,6 @@ const embeddedLinkCache = new Map<string, EmbeddedLink>();
const RE_YOUTUBE = const RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/; /^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
const RE_VIMEO = const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/; /^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/; const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
@@ -48,9 +46,6 @@ const RE_VALTOWN =
const RE_GENERIC_EMBED = const RE_GENERIC_EMBED =
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i; /^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
const RE_GIPHY =
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
const ALLOWED_DOMAINS = new Set([ const ALLOWED_DOMAINS = new Set([
"youtube.com", "youtube.com",
"youtu.be", "youtu.be",
@@ -63,7 +58,6 @@ const ALLOWED_DOMAINS = new Set([
"*.simplepdf.eu", "*.simplepdf.eu",
"stackblitz.com", "stackblitz.com",
"val.town", "val.town",
"giphy.com",
]); ]);
const createSrcDoc = (body: string) => { const createSrcDoc = (body: string) => {
@@ -313,10 +307,6 @@ export const extractSrc = (htmlString: string): string => {
return gistMatch[1]; return gistMatch[1];
} }
if (RE_GIPHY.test(htmlString)) {
return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
}
const match = htmlString.match(RE_GENERIC_EMBED); const match = htmlString.match(RE_GENERIC_EMBED);
if (match && match.length === 2) { if (match && match.length === 2) {
return match[1]; return match[1];
+1 -2
View File
@@ -21,7 +21,6 @@ import {
} from "../math"; } from "../math";
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from "."; import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
import { import {
Bounds,
getCurvePathOps, getCurvePathOps,
getElementPointsCoords, getElementPointsCoords,
getMinMaxXYFromCurvePathOps, getMinMaxXYFromCurvePathOps,
@@ -1317,7 +1316,7 @@ export class LinearElementEditor {
static getMinMaxXYWithBoundText = ( static getMinMaxXYWithBoundText = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
elementBounds: Bounds, elementBounds: [number, number, number, number],
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
): [number, number, number, number, number, number] => { ): [number, number, number, number, number, number] => {
let [x1, y1, x2, y2] = elementBounds; let [x1, y1, x2, y2] = elementBounds;
+2 -2
View File
@@ -140,8 +140,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
* *
* NOTE: does not trigger re-render. * NOTE: does not trigger re-render.
*/ */
export const bumpVersion = <T extends Mutable<ExcalidrawElement>>( export const bumpVersion = (
element: T, element: Mutable<ExcalidrawElement>,
version?: ExcalidrawElement["version"], version?: ExcalidrawElement["version"],
) => { ) => {
element.version = (version ?? element.version) + 1; element.version = (version ?? element.version) + 1;
+2 -4
View File
@@ -144,15 +144,13 @@ export const newEmbeddableElement = (
}; };
export const newFrameElement = ( export const newFrameElement = (
opts: { opts: ElementConstructorOpts,
name?: string;
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawFrameElement> => { ): NonDeleted<ExcalidrawFrameElement> => {
const frameElement = newElementWith( const frameElement = newElementWith(
{ {
..._newElementBase<ExcalidrawFrameElement>("frame", opts), ..._newElementBase<ExcalidrawFrameElement>("frame", opts),
type: "frame", type: "frame",
name: opts?.name || null, name: null,
}, },
{}, {},
); );
+13 -27
View File
@@ -41,7 +41,7 @@ import {
MaybeTransformHandleType, MaybeTransformHandleType,
TransformHandleDirection, TransformHandleDirection,
} from "./transformHandles"; } from "./transformHandles";
import { AppState, Point, PointerDownState } from "../types"; import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { import {
getApproxMinLineWidth, getApproxMinLineWidth,
@@ -79,7 +79,6 @@ export const transformElements = (
pointerY: number, pointerY: number,
centerX: number, centerX: number,
centerY: number, centerY: number,
appState: AppState,
) => { ) => {
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const [element] = selectedElements; const [element] = selectedElements;
@@ -467,8 +466,8 @@ export const resizeSingleElement = (
boundTextElement.fontSize, boundTextElement.fontSize,
boundTextElement.lineHeight, boundTextElement.lineHeight,
); );
eleNewWidth = Math.max(eleNewWidth, minWidth); eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.max(eleNewHeight, minHeight); eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
} }
} }
@@ -509,11 +508,8 @@ export const resizeSingleElement = (
} }
} }
const flipX = eleNewWidth < 0;
const flipY = eleNewHeight < 0;
// Flip horizontally // Flip horizontally
if (flipX) { if (eleNewWidth < 0) {
if (transformHandleDirection.includes("e")) { if (transformHandleDirection.includes("e")) {
newTopLeft[0] -= Math.abs(newBoundsWidth); newTopLeft[0] -= Math.abs(newBoundsWidth);
} }
@@ -521,9 +517,8 @@ export const resizeSingleElement = (
newTopLeft[0] += Math.abs(newBoundsWidth); newTopLeft[0] += Math.abs(newBoundsWidth);
} }
} }
// Flip vertically // Flip vertically
if (flipY) { if (eleNewHeight < 0) {
if (transformHandleDirection.includes("s")) { if (transformHandleDirection.includes("s")) {
newTopLeft[1] -= Math.abs(newBoundsHeight); newTopLeft[1] -= Math.abs(newBoundsHeight);
} }
@@ -547,20 +542,10 @@ export const resizeSingleElement = (
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle); const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle); newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
const newOrigin = [...newTopLeft];
const linearElementXOffset = stateAtResizeStart.x - newBoundsX1;
const linearElementYOffset = stateAtResizeStart.y - newBoundsY1;
newOrigin[0] += linearElementXOffset;
newOrigin[1] += linearElementYOffset;
const nextX = newOrigin[0];
const nextY = newOrigin[1];
// Readjust points for linear elements // Readjust points for linear elements
let rescaledElementPointsY; let rescaledElementPointsY;
let rescaledPoints; let rescaledPoints;
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
rescaledElementPointsY = rescalePoints( rescaledElementPointsY = rescalePoints(
1, 1,
@@ -577,11 +562,16 @@ export const resizeSingleElement = (
); );
} }
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
const newOrigin = [...newTopLeft];
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
const resizedElement = { const resizedElement = {
width: Math.abs(eleNewWidth), width: Math.abs(eleNewWidth),
height: Math.abs(eleNewHeight), height: Math.abs(eleNewHeight),
x: nextX, x: newOrigin[0],
y: nextY, y: newOrigin[1],
points: rescaledPoints, points: rescaledPoints,
}; };
@@ -690,10 +680,6 @@ export const resizeMultipleElements = (
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox( const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
targetElements.map(({ orig }) => orig).concat(boundTextElements), targetElements.map(({ orig }) => orig).concat(boundTextElements),
); );
// const originalHeight = maxY - minY;
// const originalWidth = maxX - minX;
const direction = transformHandleType; const direction = transformHandleType;
const mapDirectionsToAnchors: Record<typeof direction, Point> = { const mapDirectionsToAnchors: Record<typeof direction, Point> = {
+1 -2
View File
@@ -13,7 +13,6 @@ import {
MaybeTransformHandleType, MaybeTransformHandleType,
} from "./transformHandles"; } from "./transformHandles";
import { AppState, Zoom } from "../types"; import { AppState, Zoom } from "../types";
import { Bounds } from "./bounds";
const isInsideTransformHandle = ( const isInsideTransformHandle = (
transformHandle: TransformHandle, transformHandle: TransformHandle,
@@ -88,7 +87,7 @@ export const getElementWithTransformHandleType = (
}; };
export const getTransformHandleTypeFromCoords = ( export const getTransformHandleTypeFromCoords = (
[x1, y1, x2, y2]: Bounds, [x1, y1, x2, y2]: readonly [number, number, number, number],
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
zoom: Zoom, zoom: Zoom,
+1 -2
View File
@@ -12,7 +12,6 @@ export const showSelectedShapeActions = (
(appState.editingElement || (appState.editingElement ||
(appState.activeTool.type !== "selection" && (appState.activeTool.type !== "selection" &&
appState.activeTool.type !== "eraser" && appState.activeTool.type !== "eraser" &&
appState.activeTool.type !== "hand" && appState.activeTool.type !== "hand"))) ||
appState.activeTool.type !== "laser"))) ||
getSelectedElements(elements, appState).length), getSelectedElements(elements, appState).length),
); );
+1 -1
View File
@@ -91,7 +91,7 @@ export const redrawTextBoundingBox = (
); );
const maxContainerWidth = getBoundTextMaxWidth(container); const maxContainerWidth = getBoundTextMaxWidth(container);
if (!isArrowElement(container) && metrics.height > maxContainerHeight) { if (metrics.height > maxContainerHeight) {
const nextHeight = computeContainerDimensionForBoundText( const nextHeight = computeContainerDimensionForBoundText(
metrics.height, metrics.height,
container.type, container.type,
+86 -71
View File
@@ -1,6 +1,12 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Excalidraw } from "../packages/excalidraw/index"; import { Excalidraw } from "../packages/excalidraw/index";
import { GlobalTestState, render, screen } from "../tests/test-utils"; import {
GlobalTestState,
getTextEditor,
render,
screen,
updateTextEditor,
} from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { import {
@@ -17,8 +23,8 @@ import {
} from "./types"; } from "./types";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { resize } from "../tests/utils";
import { getOriginalContainerHeightFromCache } from "./textWysiwyg"; import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -177,7 +183,7 @@ describe("textWysiwyg", () => {
expect(h.state.editingElement?.id).toBe(boundText.id); expect(h.state.editingElement?.id).toBe(boundText.id);
}); });
it("should edit text under cursor when clicked with text tool", async () => { it("should edit text under cursor when clicked with text tool", () => {
const text = API.createElement({ const text = API.createElement({
type: "text", type: "text",
text: "ola", text: "ola",
@@ -192,14 +198,14 @@ describe("textWysiwyg", () => {
mouse.clickAt(text.x + 50, text.y + 50); mouse.clickAt(text.x + 50, text.y + 50);
const editor = await getTextEditor(textEditorSelector, false); const editor = getTextEditor(textEditorSelector);
expect(editor).not.toBe(null); expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id); expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1); expect(h.elements.length).toBe(1);
}); });
it("should edit text under cursor when double-clicked with selection tool", async () => { it("should edit text under cursor when double-clicked with selection tool", () => {
const text = API.createElement({ const text = API.createElement({
type: "text", type: "text",
text: "ola", text: "ola",
@@ -214,26 +220,12 @@ describe("textWysiwyg", () => {
mouse.doubleClickAt(text.x + 50, text.y + 50); mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = await getTextEditor(textEditorSelector, false); const editor = getTextEditor(textEditorSelector);
expect(editor).not.toBe(null); expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id); expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1); expect(h.elements.length).toBe(1);
}); });
// FIXME too flaky. No one knows why.
it.skip("should bump the version of a labeled arrow when the label is updated", async () => {
const arrow = UI.createElement("arrow", {
width: 300,
height: 0,
});
await UI.editText(arrow, "Hello");
const { version } = arrow;
await UI.editText(arrow, "Hello\nworld!");
expect(arrow.version).toEqual(version + 1);
});
}); });
describe("Test container-unbound text", () => { describe("Test container-unbound text", () => {
@@ -255,7 +247,7 @@ describe("textWysiwyg", () => {
textElement = UI.createElement("text"); textElement = UI.createElement("text");
mouse.clickOn(textElement); mouse.clickOn(textElement);
textarea = await getTextEditor(textEditorSelector, true); textarea = getTextEditor(textEditorSelector);
}); });
afterAll(() => { afterAll(() => {
@@ -465,7 +457,7 @@ describe("textWysiwyg", () => {
UI.clickTool("text"); UI.clickTool("text");
mouse.clickAt(750, 300); mouse.clickAt(750, 300);
textarea = await getTextEditor(textEditorSelector, true); textarea = getTextEditor(textEditorSelector);
updateTextEditor( updateTextEditor(
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!",
@@ -517,7 +509,7 @@ describe("textWysiwyg", () => {
{ id: text.id, type: "text" }, { id: text.id, type: "text" },
]); ]);
mouse.down(); mouse.down();
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
@@ -545,7 +537,7 @@ describe("textWysiwyg", () => {
]); ]);
expect(text.angle).toBe(rectangle.angle); expect(text.angle).toBe(rectangle.angle);
mouse.down(); mouse.down();
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
@@ -572,7 +564,7 @@ describe("textWysiwyg", () => {
API.setSelectedElements([diamond]); API.setSelectedElements([diamond]);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); 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");
@@ -607,7 +599,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text"); expect(text.type).toBe("text");
expect(text.containerId).toBe(null); expect(text.containerId).toBe(null);
mouse.down(); mouse.down();
let editor = await getTextEditor(textEditorSelector, true); let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
editor.blur(); editor.blur();
@@ -622,7 +614,7 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(rectangle.id); expect(text.containerId).toBe(rectangle.id);
mouse.down(); mouse.down();
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
@@ -644,7 +636,7 @@ describe("textWysiwyg", () => {
const text = h.elements[1] as ExcalidrawTextElementWithContainer; const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text"); expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id); expect(text.containerId).toBe(rectangle.id);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
@@ -679,7 +671,7 @@ describe("textWysiwyg", () => {
{ id: text.id, type: "text" }, { id: text.id, type: "text" },
]); ]);
mouse.down(); mouse.down();
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
@@ -704,7 +696,7 @@ describe("textWysiwyg", () => {
freedraw.y + freedraw.height / 2, freedraw.y + freedraw.height / 2,
); );
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
@@ -738,7 +730,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text"); expect(text.type).toBe("text");
expect(text.containerId).toBe(null); expect(text.containerId).toBe(null);
mouse.down(); mouse.down();
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
@@ -753,7 +745,7 @@ describe("textWysiwyg", () => {
UI.clickTool("text"); UI.clickTool("text");
mouse.clickAt(20, 30); mouse.clickAt(20, 30);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor( updateTextEditor(
editor, editor,
@@ -798,7 +790,7 @@ describe("textWysiwyg", () => {
mouse.down(); mouse.down();
const text = h.elements[1] as ExcalidrawTextElementWithContainer; const text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = await getTextEditor(textEditorSelector, true); let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
@@ -811,7 +803,7 @@ describe("textWysiwyg", () => {
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
); );
mouse.down(); mouse.down();
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
editor.select(); editor.select();
fireEvent.click(screen.getByTitle(/code/i)); fireEvent.click(screen.getByTitle(/code/i));
@@ -844,7 +836,7 @@ describe("textWysiwyg", () => {
Keyboard.keyDown(KEYS.ENTER); Keyboard.keyDown(KEYS.ENTER);
let text = h.elements[1] as ExcalidrawTextElementWithContainer; let text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = await getTextEditor(textEditorSelector, true); let editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
@@ -865,7 +857,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle); mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
@@ -894,7 +886,7 @@ 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 = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
@@ -931,7 +923,7 @@ describe("textWysiwyg", () => {
// Bind first text // Bind first text
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 = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
editor.blur(); editor.blur();
@@ -952,24 +944,24 @@ describe("textWysiwyg", () => {
it("should respect text alignment when resizing", async () => { it("should respect text alignment when resizing", async () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
let editor = await getTextEditor(textEditorSelector, true); let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
editor.blur(); editor.blur();
// 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]); resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
[ [
85, 85,
4.999999999999986, 4.5,
] ]
`); `);
mouse.select(rectangle); mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
editor.select(); editor.select();
@@ -982,7 +974,7 @@ describe("textWysiwyg", () => {
editor.blur(); 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]); resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
[ [
15, 15,
@@ -992,7 +984,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle); mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
editor.select(); editor.select();
@@ -1004,11 +996,11 @@ describe("textWysiwyg", () => {
editor.blur(); 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]); resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(` expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
[ [
374.99999999999994, 375,
-535.0000000000001, -539,
] ]
`); `);
}); });
@@ -1030,7 +1022,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text"); expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id); expect(text.containerId).toBe(rectangle.id);
mouse.down(); mouse.down();
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
@@ -1045,7 +1037,7 @@ describe("textWysiwyg", () => {
it("should scale font size correctly when resizing using shift", async () => { it("should scale font size correctly when resizing using shift", async () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
editor.blur(); editor.blur();
@@ -1054,7 +1046,7 @@ describe("textWysiwyg", () => {
expect(rectangle.height).toBe(75); expect(rectangle.height).toBe(75);
expect(textElement.fontSize).toBe(20); expect(textElement.fontSize).toBe(20);
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], { resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
shift: true, shift: true,
}); });
expect(rectangle.width).toBe(200); expect(rectangle.width).toBe(200);
@@ -1065,7 +1057,7 @@ describe("textWysiwyg", () => {
it("should bind text correctly when container duplicated with alt-drag", async () => { it("should bind text correctly when container duplicated with alt-drag", async () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
editor.blur(); editor.blur();
@@ -1097,7 +1089,7 @@ 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 = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
editor.blur(); editor.blur();
@@ -1134,7 +1126,7 @@ 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 = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, " "); updateTextEditor(editor, " ");
@@ -1189,35 +1181,32 @@ describe("textWysiwyg", () => {
it("should reset the container height cache when resizing", async () => { it("should reset the container height cache when resizing", async () => {
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 = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
editor.blur(); editor.blur();
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]); resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect(rectangle.height).toBeCloseTo(155, 8); expect(rectangle.height).toBe(156);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
mouse.select(rectangle); mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
editor.blur(); editor.blur();
expect(rectangle.height).toBeCloseTo(155, 8); expect(rectangle.height).toBe(156);
// cache updated again // cache updated again
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo( expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
155,
8,
);
}); });
it("should reset the container height cache when font properties updated", async () => { it("should reset the container height cache when font properties updated", async () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
editor.blur(); editor.blur();
@@ -1242,7 +1231,7 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!"); updateTextEditor(editor, "Hello World!");
editor.blur(); editor.blur();
expect( expect(
@@ -1274,12 +1263,12 @@ describe("textWysiwyg", () => {
beforeEach(async () => { beforeEach(async () => {
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello"); updateTextEditor(editor, "Hello");
editor.blur(); editor.blur();
mouse.select(rectangle); mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER); Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
editor.select(); editor.select();
}); });
@@ -1390,7 +1379,7 @@ describe("textWysiwyg", () => {
it("should wrap text in a container when wrap text in container triggered from context menu", async () => { it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
UI.clickTool("text"); UI.clickTool("text");
mouse.clickAt(20, 30); mouse.clickAt(20, 30);
const editor = await getTextEditor(textEditorSelector, true); const editor = getTextEditor(textEditorSelector);
updateTextEditor( updateTextEditor(
editor, editor,
@@ -1436,7 +1425,7 @@ describe("textWysiwyg", () => {
type: "text", type: "text",
}, },
], ],
fillStyle: "solid", fillStyle: "hachure",
groupIds: [], groupIds: [],
height: 35, height: 35,
isDeleted: false, isDeleted: false,
@@ -1449,7 +1438,7 @@ describe("textWysiwyg", () => {
}, },
strokeColor: "#1e1e1e", strokeColor: "#1e1e1e",
strokeStyle: "solid", strokeStyle: "solid",
strokeWidth: 2, strokeWidth: 1,
type: "rectangle", type: "rectangle",
updated: 1, updated: 1,
version: 1, version: 1,
@@ -1478,7 +1467,7 @@ describe("textWysiwyg", () => {
// Bind first text // Bind first text
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 = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello!"); updateTextEditor(editor, "Hello!");
expect( expect(
@@ -1503,7 +1492,7 @@ describe("textWysiwyg", () => {
rectangle.x + rectangle.width / 2, rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
); );
editor = await getTextEditor(textEditorSelector, true); editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Excalidraw"); updateTextEditor(editor, "Excalidraw");
editor.blur(); editor.blur();
@@ -1517,4 +1506,30 @@ describe("textWysiwyg", () => {
expect(text.text).toBe("Excalidraw"); expect(text.text).toBe("Excalidraw");
}); });
}); });
it("should bump the version of labelled arrow when label updated", async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
const arrow = UI.createElement("arrow", {
width: 300,
height: 0,
});
mouse.select(arrow);
Keyboard.keyPress(KEYS.ENTER);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
const { version } = arrow;
mouse.select(arrow);
Keyboard.keyPress(KEYS.ENTER);
editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello\nworld!");
editor.blur();
expect(arrow.version).toEqual(version + 1);
});
}); });
+1 -2
View File
@@ -584,7 +584,7 @@ export const textWysiwyg = ({
window.removeEventListener("pointerdown", onPointerDown); window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", bindBlurEvent); window.removeEventListener("pointerup", bindBlurEvent);
window.removeEventListener("blur", handleSubmit); window.removeEventListener("blur", handleSubmit);
window.removeEventListener("beforeunload", handleSubmit);
unbindUpdate(); unbindUpdate();
editable.remove(); editable.remove();
@@ -701,7 +701,6 @@ export const textWysiwyg = ({
passive: false, passive: false,
capture: true, capture: true,
}); });
window.addEventListener("beforeunload", handleSubmit);
excalidrawContainer excalidrawContainer
?.querySelector(".excalidraw-textEditorContainer")! ?.querySelector(".excalidraw-textEditorContainer")!
.appendChild(editable); .appendChild(editable);
+2 -2
View File
@@ -4,7 +4,7 @@ import {
PointerType, PointerType,
} from "./types"; } from "./types";
import { Bounds, getElementAbsoluteCoords } from "./bounds"; import { getElementAbsoluteCoords } from "./bounds";
import { rotate } from "../math"; import { rotate } from "../math";
import { InteractiveCanvasAppState, Zoom } from "../types"; import { InteractiveCanvasAppState, Zoom } from "../types";
import { isTextElement } from "."; import { isTextElement } from ".";
@@ -23,7 +23,7 @@ export type TransformHandleDirection =
export type TransformHandleType = TransformHandleDirection | "rotation"; export type TransformHandleType = TransformHandleDirection | "rotation";
export type TransformHandle = Bounds; export type TransformHandle = [number, number, number, number];
export type TransformHandles = Partial<{ export type TransformHandles = Partial<{
[T in TransformHandleType]: TransformHandle; [T in TransformHandleType]: TransformHandle;
}>; }>;
-47
View File
@@ -1,47 +0,0 @@
type Subscriber<T extends any[]> = (...payload: T) => void;
export class Emitter<T extends any[] = []> {
public subscribers: Subscriber<T>[] = [];
public value: T | undefined;
private updateOnChangeOnly: boolean;
constructor(opts?: { initialState?: T; updateOnChangeOnly?: boolean }) {
this.updateOnChangeOnly = opts?.updateOnChangeOnly ?? false;
this.value = opts?.initialState;
}
/**
* Attaches subscriber
*
* @returns unsubscribe function
*/
on(...handlers: Subscriber<T>[] | Subscriber<T>[][]) {
const _handlers = handlers
.flat()
.filter((item) => typeof item === "function");
this.subscribers.push(..._handlers);
return () => this.off(_handlers);
}
off(...handlers: Subscriber<T>[] | Subscriber<T>[][]) {
const _handlers = handlers.flat();
this.subscribers = this.subscribers.filter(
(handler) => !_handlers.includes(handler),
);
}
trigger(...payload: T): any[] {
if (this.updateOnChangeOnly && this.value === payload) {
return [];
}
this.value = payload;
return this.subscribers.map((handler) => handler(...payload));
}
destroy() {
this.subscribers = [];
this.value = undefined;
}
}
+13 -129
View File
@@ -123,7 +123,7 @@ describe("adding elements to frames", () => {
const commonTestCases = async ( const commonTestCases = async (
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame, func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
) => { ) => {
describe.skip("when frame is in a layer below", async () => { describe("when frame is in a layer below", async () => {
it("should add an element", async () => { it("should add an element", async () => {
h.elements = [frame, rect2]; h.elements = [frame, rect2];
@@ -167,7 +167,7 @@ describe("adding elements to frames", () => {
}); });
}); });
describe.skip("when frame is in a layer above", async () => { describe("when frame is in a layer above", async () => {
it("should add an element", async () => { it("should add an element", async () => {
h.elements = [rect2, frame]; h.elements = [rect2, frame];
@@ -212,7 +212,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("should add elements", async () => {
h.elements = [rect2, frame, rect3]; h.elements = [rect2, frame, rect3];
func(frame, rect2); func(frame, rect2);
@@ -223,7 +223,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect2, rect3, frame]); expectEqualIds([rect2, rect3, frame]);
}); });
it.skip("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 = [rect2, rect1, frame, rect4, rect3]; h.elements = [rect2, rect1, frame, rect4, rect3];
func(frame, rect2); func(frame, rect2);
@@ -234,7 +234,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect1, rect2, rect3, frame, rect4]); expectEqualIds([rect1, rect2, rect3, frame, rect4]);
}); });
it.skip("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, frame, rect2, rect1]; h.elements = [rect3, rect4, frame, rect2, rect1];
func(frame, rect2); func(frame, rect2);
@@ -289,7 +289,7 @@ describe("adding elements to frames", () => {
describe("resizing frame over elements", async () => { describe("resizing frame over elements", async () => {
await commonTestCases(resizeFrameOverElement); await commonTestCases(resizeFrameOverElement);
it.skip("resizing over text containers and labelled arrows", async () => { it("resizing over text containers and labelled arrows", async () => {
await resizingTest( await resizingTest(
"rectangle", "rectangle",
["frame", "rectangle", "text"], ["frame", "rectangle", "text"],
@@ -339,7 +339,7 @@ describe("adding elements to frames", () => {
// ); // );
}); });
it.skip("should add arrow bound with text when frame is in a layer below", async () => { it("should add arrow bound with text when frame is in a layer below", async () => {
h.elements = [frame, arrow, text]; h.elements = [frame, arrow, text];
resizeFrameOverElement(frame, arrow); resizeFrameOverElement(frame, arrow);
@@ -359,7 +359,7 @@ describe("adding elements to frames", () => {
expectEqualIds([arrow, text, frame]); expectEqualIds([arrow, text, frame]);
}); });
it.skip("should add arrow bound with text when frame is in an inner layer", async () => { it("should add arrow bound with text when frame is in an inner layer", async () => {
h.elements = [arrow, frame, text]; h.elements = [arrow, frame, text];
resizeFrameOverElement(frame, arrow); resizeFrameOverElement(frame, arrow);
@@ -371,7 +371,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("should add elements when frame is in a layer below", async () => {
h.elements = [frame, rect1, rect2, rect3, rect4]; h.elements = [frame, rect1, rect2, rect3, rect4];
resizeFrameOverElement(frame, rect4); resizeFrameOverElement(frame, rect4);
@@ -382,7 +382,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect2, rect3, frame, rect4, rect1]); expectEqualIds([rect2, rect3, frame, rect4, rect1]);
}); });
it.skip("should add elements when frame is in a layer above", async () => { it("should add elements when frame is in a layer above", async () => {
h.elements = [rect1, rect2, rect3, rect4, frame]; h.elements = [rect1, rect2, rect3, rect4, frame];
resizeFrameOverElement(frame, rect4); resizeFrameOverElement(frame, rect4);
@@ -393,7 +393,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect1, rect2, rect3, frame, rect4]); expectEqualIds([rect1, rect2, rect3, frame, rect4]);
}); });
it.skip("should add elements when frame is in an inner layer", async () => { it("should add elements when frame is in an inner layer", async () => {
h.elements = [rect1, rect2, frame, rect3, rect4]; h.elements = [rect1, rect2, frame, rect3, rect4];
resizeFrameOverElement(frame, rect4); resizeFrameOverElement(frame, rect4);
@@ -408,7 +408,7 @@ describe("adding elements to frames", () => {
describe("dragging elements into the frame", async () => { describe("dragging elements into the frame", async () => {
await commonTestCases(dragElementIntoFrame); await commonTestCases(dragElementIntoFrame);
it.skip("should drag element inside, duplicate it and keep it in frame", () => { it("should drag element inside, duplicate it and keep it in frame", () => {
h.elements = [frame, rect2]; h.elements = [frame, rect2];
dragElementIntoFrame(frame, rect2); dragElementIntoFrame(frame, rect2);
@@ -422,7 +422,7 @@ describe("adding elements to frames", () => {
expectEqualIds([rect2_copy, rect2, frame]); expectEqualIds([rect2_copy, rect2, frame]);
}); });
it.skip("should drag element inside, duplicate it and remove it from frame", () => { it("should drag element inside, duplicate it and remove it from frame", () => {
h.elements = [frame, rect2]; h.elements = [frame, rect2];
dragElementIntoFrame(frame, rect2); dragElementIntoFrame(frame, rect2);
@@ -436,121 +436,5 @@ describe("adding elements to frames", () => {
expect(rect2.frameId).toBe(null); expect(rect2.frameId).toBe(null);
expectEqualIds([rect2_copy, frame, rect2]); expectEqualIds([rect2_copy, frame, rect2]);
}); });
it("random order 01", () => {
const frame1 = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
const frame2 = API.createElement({
type: "frame",
x: 200,
y: 0,
width: 100,
height: 100,
});
const frame3 = API.createElement({
type: "frame",
x: 300,
y: 0,
width: 100,
height: 100,
});
const rectangle1 = API.createElement({
type: "rectangle",
x: 25,
y: 25,
width: 50,
height: 50,
frameId: frame1.id,
});
const rectangle2 = API.createElement({
type: "rectangle",
x: 225,
y: 25,
width: 50,
height: 50,
frameId: frame2.id,
});
const rectangle3 = API.createElement({
type: "rectangle",
x: 325,
y: 25,
width: 50,
height: 50,
frameId: frame3.id,
});
const rectangle4 = API.createElement({
type: "rectangle",
x: 350,
y: 25,
width: 50,
height: 50,
frameId: frame3.id,
});
h.elements = [
frame1,
rectangle4,
rectangle1,
rectangle3,
frame3,
rectangle2,
frame2,
];
API.setSelectedElements([rectangle2]);
const origSize = h.elements.length;
expect(h.elements.length).toBe(origSize);
dragElementIntoFrame(frame3, rectangle2);
expect(h.elements.length).toBe(origSize);
});
it("random order 02", () => {
const frame1 = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
const frame2 = API.createElement({
type: "frame",
x: 200,
y: 0,
width: 100,
height: 100,
});
const rectangle1 = API.createElement({
type: "rectangle",
x: 25,
y: 25,
width: 50,
height: 50,
frameId: frame1.id,
});
const rectangle2 = API.createElement({
type: "rectangle",
x: 225,
y: 25,
width: 50,
height: 50,
frameId: frame2.id,
});
h.elements = [rectangle1, rectangle2, frame1, frame2];
API.setSelectedElements([rectangle2]);
expect(h.elements.length).toBe(4);
dragElementIntoFrame(frame2, rectangle1);
expect(h.elements.length).toBe(4);
});
}); });
}); });
+216 -94
View File
@@ -14,15 +14,15 @@ import {
getBoundTextElement, getBoundTextElement,
getContainerElement, getContainerElement,
} from "./element/textElement"; } from "./element/textElement";
import { arrayToMap } from "./utils"; import { arrayToMap, findIndex } from "./utils";
import { mutateElement } from "./element/mutateElement"; import { mutateElement } from "./element/mutateElement";
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types"; import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { isFrameElement } from "./element"; import { isFrameElement } from "./element";
import { moveOneRight } from "./zindex";
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups"; import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene"; import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
import { getElementLineSegments } from "./element/bounds"; import { getElementLineSegments } from "./element/bounds";
import { doLineSegmentsIntersect } from "./packages/utils";
// --------------------------- Frame State ------------------------------------ // --------------------------- Frame State ------------------------------------
export const bindElementsToFramesAfterDuplication = ( export const bindElementsToFramesAfterDuplication = (
@@ -56,21 +56,130 @@ export const bindElementsToFramesAfterDuplication = (
} }
}; };
export function isElementIntersectingFrame( // --------------------------- Frame Geometry ---------------------------------
element: ExcalidrawElement, class Point {
frame: ExcalidrawFrameElement, x: number;
) { y: number;
const frameLineSegments = getElementLineSegments(frame);
const elementLineSegments = getElementLineSegments(element); constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const intersecting = frameLineSegments.some((frameLineSegment) => class LineSegment {
elementLineSegments.some((elementLineSegment) => first: Point;
doLineSegmentsIntersect(frameLineSegment, elementLineSegment), second: Point;
),
);
return intersecting; constructor(pointA: Point, pointB: Point) {
this.first = pointA;
this.second = pointB;
}
public getBoundingBox(): [Point, Point] {
return [
new Point(
Math.min(this.first.x, this.second.x),
Math.min(this.first.y, this.second.y),
),
new Point(
Math.max(this.first.x, this.second.x),
Math.max(this.first.y, this.second.y),
),
];
}
}
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
class FrameGeometry {
private static EPSILON = 0.000001;
private static crossProduct(a: Point, b: Point) {
return a.x * b.y - b.x * a.y;
}
private static doBoundingBoxesIntersect(
a: [Point, Point],
b: [Point, Point],
) {
return (
a[0].x <= b[1].x &&
a[1].x >= b[0].x &&
a[0].y <= b[1].y &&
a[1].y >= b[0].y
);
}
private static isPointOnLine(a: LineSegment, b: Point) {
const aTmp = new LineSegment(
new Point(0, 0),
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
);
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
const r = this.crossProduct(aTmp.second, bTmp);
return Math.abs(r) < this.EPSILON;
}
private static isPointRightOfLine(a: LineSegment, b: Point) {
const aTmp = new LineSegment(
new Point(0, 0),
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
);
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
return this.crossProduct(aTmp.second, bTmp) < 0;
}
private static lineSegmentTouchesOrCrossesLine(
a: LineSegment,
b: LineSegment,
) {
return (
this.isPointOnLine(a, b.first) ||
this.isPointOnLine(a, b.second) ||
(this.isPointRightOfLine(a, b.first)
? !this.isPointRightOfLine(a, b.second)
: this.isPointRightOfLine(a, b.second))
);
}
private static doLineSegmentsIntersect(
a: [readonly [number, number], readonly [number, number]],
b: [readonly [number, number], readonly [number, number]],
) {
const aSegment = new LineSegment(
new Point(a[0][0], a[0][1]),
new Point(a[1][0], a[1][1]),
);
const bSegment = new LineSegment(
new Point(b[0][0], b[0][1]),
new Point(b[1][0], b[1][1]),
);
const box1 = aSegment.getBoundingBox();
const box2 = bSegment.getBoundingBox();
return (
this.doBoundingBoxesIntersect(box1, box2) &&
this.lineSegmentTouchesOrCrossesLine(aSegment, bSegment) &&
this.lineSegmentTouchesOrCrossesLine(bSegment, aSegment)
);
}
public static isElementIntersectingFrame(
element: ExcalidrawElement,
frame: ExcalidrawFrameElement,
) {
const frameLineSegments = getElementLineSegments(frame);
const elementLineSegments = getElementLineSegments(element);
const intersecting = frameLineSegments.some((frameLineSegment) =>
elementLineSegments.some((elementLineSegment) =>
this.doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
),
);
return intersecting;
}
} }
export const getElementsCompletelyInFrame = ( export const getElementsCompletelyInFrame = (
@@ -98,7 +207,10 @@ export const isElementContainingFrame = (
export const getElementsIntersectingFrame = ( export const getElementsIntersectingFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameElement, frame: ExcalidrawFrameElement,
) => elements.filter((element) => isElementIntersectingFrame(element, frame)); ) =>
elements.filter((element) =>
FrameGeometry.isElementIntersectingFrame(element, frame),
);
export const elementsAreInFrameBounds = ( export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -124,7 +236,7 @@ export const elementOverlapsWithFrame = (
) => { ) => {
return ( return (
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame) ||
isElementIntersectingFrame(element, frame) || FrameGeometry.isElementIntersectingFrame(element, frame) ||
isElementContainingFrame([frame], element, frame) isElementContainingFrame([frame], element, frame)
); );
}; };
@@ -161,7 +273,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
return !!elementsInGroup.find( return !!elementsInGroup.find(
(element) => (element) =>
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame) ||
isElementIntersectingFrame(element, frame), FrameGeometry.isElementIntersectingFrame(element, frame),
); );
}; };
@@ -182,7 +294,7 @@ export const groupsAreCompletelyOutOfFrame = (
elementsInGroup.find( elementsInGroup.find(
(element) => (element) =>
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame) ||
isElementIntersectingFrame(element, frame), FrameGeometry.isElementIntersectingFrame(element, frame),
) === undefined ) === undefined
); );
}; };
@@ -242,7 +354,7 @@ export const getElementsInResizingFrame = (
); );
for (const element of elementsNotCompletelyInFrame) { for (const element of elementsNotCompletelyInFrame) {
if (!isElementIntersectingFrame(element, frame)) { if (!FrameGeometry.isElementIntersectingFrame(element, frame)) {
if (element.groupIds.length === 0) { if (element.groupIds.length === 0) {
nextElementsInFrame.delete(element); nextElementsInFrame.delete(element);
} }
@@ -340,63 +452,92 @@ export const getContainingFrame = (
}; };
// --------------------------- Frame Operations ------------------------------- // --------------------------- Frame Operations -------------------------------
/**
* Retains (or repairs for target frame) the ordering invriant where children
* elements come right before the parent frame:
* [el, el, child, child, frame, el]
*/
export const addElementsToFrame = ( export const addElementsToFrame = (
allElements: ExcalidrawElementsIncludingDeleted, allElements: ExcalidrawElementsIncludingDeleted,
elementsToAdd: NonDeletedExcalidrawElement[], elementsToAdd: NonDeletedExcalidrawElement[],
frame: ExcalidrawFrameElement, frame: ExcalidrawFrameElement,
) => { ) => {
const { currTargetFrameChildrenMap } = allElements.reduce( const _elementsToAdd: ExcalidrawElement[] = [];
(acc, element, index) => {
if (element.frameId === frame.id) {
acc.currTargetFrameChildrenMap.set(element.id, true);
}
return acc;
},
{
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
},
);
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id)); for (const element of elementsToAdd) {
_elementsToAdd.push(element);
const finalElementsToAdd: ExcalidrawElement[] = [];
// - add bound text elements if not already in the array
// - filter out elements that are already in the frame
for (const element of omitGroupsContainingFrames(
allElements,
elementsToAdd,
)) {
if (!currTargetFrameChildrenMap.has(element.id)) {
finalElementsToAdd.push(element);
}
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if ( if (boundTextElement) {
boundTextElement && _elementsToAdd.push(boundTextElement);
!suppliedElementsToAddSet.has(boundTextElement.id) &&
!currTargetFrameChildrenMap.has(boundTextElement.id)
) {
finalElementsToAdd.push(boundTextElement);
} }
} }
for (const element of finalElementsToAdd) { const allElementsIndex = allElements.reduce(
mutateElement( (acc: Record<string, number>, element, index) => {
element, acc[element.id] = index;
{ return acc;
frameId: frame.id, },
}, {},
false, );
);
const frameIndex = allElementsIndex[frame.id];
// need to be calculated before the mutation below occurs
const leftFrameBoundaryIndex = findIndex(
allElements,
(e) => e.frameId === frame.id,
);
const existingFrameChildren = allElements.filter(
(element) => element.frameId === frame.id,
);
const addedFrameChildren_left: ExcalidrawElement[] = [];
const addedFrameChildren_right: ExcalidrawElement[] = [];
for (const element of omitGroupsContainingFrames(
allElements,
_elementsToAdd,
)) {
if (element.frameId !== frame.id && !isFrameElement(element)) {
if (allElementsIndex[element.id] > frameIndex) {
addedFrameChildren_right.push(element);
} else {
addedFrameChildren_left.push(element);
}
mutateElement(
element,
{
frameId: frame.id,
},
false,
);
}
} }
return allElements.slice();
const frameElement = allElements[frameIndex];
const nextFrameChildren = addedFrameChildren_left
.concat(existingFrameChildren)
.concat(addedFrameChildren_right);
const nextFrameChildrenMap = nextFrameChildren.reduce(
(acc: Record<string, boolean>, element) => {
acc[element.id] = true;
return acc;
},
{},
);
const nextOtherElements_left = allElements
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
.filter((element) => !nextFrameChildrenMap[element.id]);
const nextOtherElement_right = allElements
.slice(frameIndex + 1)
.filter((element) => !nextFrameChildrenMap[element.id]);
const nextElements = nextOtherElements_left
.concat(nextFrameChildren)
.concat([frameElement])
.concat(nextOtherElement_right);
return nextElements;
}; };
export const removeElementsFromFrame = ( export const removeElementsFromFrame = (
@@ -404,34 +545,20 @@ export const removeElementsFromFrame = (
elementsToRemove: NonDeletedExcalidrawElement[], elementsToRemove: NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
) => { ) => {
const _elementsToRemove = new Map< const _elementsToRemove: ExcalidrawElement[] = [];
ExcalidrawElement["id"],
ExcalidrawElement
>();
const toRemoveElementsByFrame = new Map<
ExcalidrawFrameElement["id"],
ExcalidrawElement[]
>();
for (const element of elementsToRemove) { for (const element of elementsToRemove) {
if (element.frameId) { if (element.frameId) {
_elementsToRemove.set(element.id, element); _elementsToRemove.push(element);
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
arr.push(element);
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
_elementsToRemove.set(boundTextElement.id, boundTextElement); _elementsToRemove.push(boundTextElement);
arr.push(boundTextElement);
} }
toRemoveElementsByFrame.set(element.frameId, arr);
} }
} }
for (const [, element] of _elementsToRemove) { for (const element of _elementsToRemove) {
mutateElement( mutateElement(
element, element,
{ {
@@ -441,7 +568,13 @@ export const removeElementsFromFrame = (
); );
} }
return allElements.slice(); const nextElements = moveOneRight(
allElements,
appState,
Array.from(_elementsToRemove),
);
return nextElements;
}; };
export const removeAllElementsFromFrame = ( export const removeAllElementsFromFrame = (
@@ -572,17 +705,6 @@ export const isElementInFrame = (
: element; : element;
if (frame) { if (frame) {
// Perf improvement:
// For an element that's already in a frame, if it's not being dragged
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
// It has to be in its containing frame.
if (
!appState.selectedElementIds[element.id] ||
!appState.selectedElementsAreBeingDragged
) {
return true;
}
if (_element.groupIds.length === 0) { if (_element.groupIds.length === 0) {
return elementOverlapsWithFrame(_element, frame); return elementOverlapsWithFrame(_element, frame);
} }

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