Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 348912f32f | |||
| ec2de7205f | |||
| d5e3f436dc | |||
| dcf4592e79 | |||
| d1f8eec174 | |||
| 0f81c30276 | |||
| f098789d16 | |||
| f794b0bb90 | |||
| 104f64f1dc | |||
| 71ad3c5356 | |||
| afea0df141 | |||
| d2a508104e | |||
| 3697618266 | |||
| e7cc2337ea | |||
| 9eb89f9960 | |||
| ab1bcc7615 | |||
| b1cac35269 | |||
| 83f86e2b86 | |||
| 7e38cab76e | |||
| 2cabb1f1f4 | |||
| 63650f82d1 | |||
| dde3dac931 | |||
| 5b94cffc74 | |||
| aaf73c8ff3 | |||
| 44d9d5fcac | |||
| 89a3bbddb7 | |||
| b86184a849 | |||
| b552166924 | |||
| 26ff3993bb | |||
| 7ad02c359a | |||
| 2523fe82e3 | |||
| 4ea079eb85 | |||
| f20ba90ffa | |||
| 03da9112cf | |||
| a249f332a2 | |||
| 2e61926a6b | |||
| e921bfb1ae | |||
| e6f74350ac | |||
| fa33aa08ab | |||
| 8b838049df | |||
| 1f4f5e11ae | |||
| 12420592ef | |||
| bfd318e765 | |||
| 6a821f3b76 | |||
| 84fd13e872 | |||
| 7d2b6f3374 | |||
| ceb637f5ea | |||
| 4c35eba72d | |||
| 4765f5536e | |||
| 556175558a | |||
| 4db73a7f95 | |||
| f8b3692262 | |||
| 741d5f1a18 |
@@ -1,3 +0,0 @@
|
|||||||
## 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
|
|
||||||
@@ -70,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/src/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/excalidraw-app) is part of this repository as well, and the app features:
|
||||||
|
|
||||||
- 📡 PWA support (works offline).
|
- 📡 PWA support (works offline).
|
||||||
- 🤼 Real-time collaboration.
|
- 🤼 Real-time collaboration.
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ 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 |
|
||||||
@@ -70,6 +71,7 @@ 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 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Customizing Styles
|
# Customizing Styles
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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,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/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.
|
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.
|
||||||
|
|
||||||
### 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**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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 worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
The following workflow 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";
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 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.
|
||||||
@@ -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. To do this, run:
|
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run:
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ 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",
|
||||||
|
|||||||
@@ -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 by don't know where to
|
Want to build your own app powered by Excalidraw but don't know where to
|
||||||
start?
|
start?
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|||||||
+117
-12
@@ -145,6 +145,14 @@
|
|||||||
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"
|
||||||
@@ -202,6 +210,16 @@
|
|||||||
"@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"
|
||||||
@@ -265,6 +283,11 @@
|
|||||||
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"
|
||||||
@@ -280,6 +303,14 @@
|
|||||||
"@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"
|
||||||
@@ -287,6 +318,13 @@
|
|||||||
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"
|
||||||
@@ -374,11 +412,28 @@
|
|||||||
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"
|
||||||
@@ -412,11 +467,25 @@
|
|||||||
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"
|
||||||
@@ -1147,19 +1216,28 @@
|
|||||||
"@babel/parser" "^7.18.6"
|
"@babel/parser" "^7.18.6"
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
"@babel/template@^7.22.15":
|
||||||
version "7.18.9"
|
version "7.22.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
|
||||||
integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
|
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.18.6"
|
"@babel/code-frame" "^7.22.13"
|
||||||
"@babel/generator" "^7.18.9"
|
"@babel/parser" "^7.22.15"
|
||||||
"@babel/helper-environment-visitor" "^7.18.9"
|
"@babel/types" "^7.22.15"
|
||||||
"@babel/helper-function-name" "^7.18.9"
|
|
||||||
"@babel/helper-hoist-variables" "^7.18.6"
|
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
||||||
"@babel/helper-split-export-declaration" "^7.18.6"
|
version "7.23.2"
|
||||||
"@babel/parser" "^7.18.9"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
|
||||||
"@babel/types" "^7.18.9"
|
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.22.13"
|
||||||
|
"@babel/generator" "^7.23.0"
|
||||||
|
"@babel/helper-environment-visitor" "^7.22.20"
|
||||||
|
"@babel/helper-function-name" "^7.23.0"
|
||||||
|
"@babel/helper-hoist-variables" "^7.22.5"
|
||||||
|
"@babel/helper-split-export-declaration" "^7.22.6"
|
||||||
|
"@babel/parser" "^7.23.0"
|
||||||
|
"@babel/types" "^7.23.0"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
@@ -1171,6 +1249,15 @@
|
|||||||
"@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"
|
||||||
@@ -1670,6 +1757,11 @@
|
|||||||
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"
|
||||||
@@ -1688,6 +1780,19 @@
|
|||||||
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"
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { debounce, getVersion, nFormatter } from "../utils";
|
import { debounce, getVersion, nFormatter } from "../src/utils";
|
||||||
import {
|
import {
|
||||||
getElementsStorageSize,
|
getElementsStorageSize,
|
||||||
getTotalStorageSize,
|
getTotalStorageSize,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import { DEFAULT_VERSION } from "../constants";
|
import { DEFAULT_VERSION } from "../src/constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../src/i18n";
|
||||||
import { copyTextToSystemClipboard } from "../clipboard";
|
import { copyTextToSystemClipboard } from "../src/clipboard";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../src/element/types";
|
||||||
import { UIAppState } from "../types";
|
import { UIAppState } from "../src/types";
|
||||||
|
|
||||||
type StorageSizes = { scene: number; total: number };
|
type StorageSizes = { scene: number; total: number };
|
||||||
|
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { PureComponent } from "react";
|
import { PureComponent } from "react";
|
||||||
import { ExcalidrawImperativeAPI } from "../../types";
|
import { ExcalidrawImperativeAPI } from "../../src/types";
|
||||||
import { ErrorDialog } from "../../components/ErrorDialog";
|
import { ErrorDialog } from "../../src/components/ErrorDialog";
|
||||||
import { APP_NAME, ENV, EVENT } from "../../constants";
|
import { APP_NAME, ENV, EVENT } from "../../src/constants";
|
||||||
import { ImportedDataState } from "../../data/types";
|
import { ImportedDataState } from "../../src/data/types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "../../element/types";
|
} from "../../src/element/types";
|
||||||
import {
|
import {
|
||||||
getSceneVersion,
|
getSceneVersion,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
} from "../../packages/excalidraw/index";
|
} from "../../src/packages/excalidraw/index";
|
||||||
import { Collaborator, Gesture } from "../../types";
|
import { Collaborator, Gesture } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
preventUnload,
|
preventUnload,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
withBatchedUpdates,
|
withBatchedUpdates,
|
||||||
} from "../../utils";
|
} from "../../src/utils";
|
||||||
import {
|
import {
|
||||||
CURSOR_SYNC_TIMEOUT,
|
CURSOR_SYNC_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
@@ -48,25 +48,25 @@ import {
|
|||||||
} from "../data/localStorage";
|
} from "../data/localStorage";
|
||||||
import Portal from "./Portal";
|
import Portal from "./Portal";
|
||||||
import RoomDialog from "./RoomDialog";
|
import RoomDialog from "./RoomDialog";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../src/i18n";
|
||||||
import { UserIdleState } from "../../types";
|
import { UserIdleState } from "../../src/types";
|
||||||
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
|
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../src/constants";
|
||||||
import {
|
import {
|
||||||
encodeFilesForUpload,
|
encodeFilesForUpload,
|
||||||
FileManager,
|
FileManager,
|
||||||
updateStaleImageStatuses,
|
updateStaleImageStatuses,
|
||||||
} from "../data/FileManager";
|
} from "../data/FileManager";
|
||||||
import { AbortError } from "../../errors";
|
import { AbortError } from "../../src/errors";
|
||||||
import {
|
import {
|
||||||
isImageElement,
|
isImageElement,
|
||||||
isInitializedImageElement,
|
isInitializedImageElement,
|
||||||
} from "../../element/typeChecks";
|
} from "../../src/element/typeChecks";
|
||||||
import { newElementWith } from "../../element/mutateElement";
|
import { newElementWith } from "../../src/element/mutateElement";
|
||||||
import {
|
import {
|
||||||
ReconciledElements,
|
ReconciledElements,
|
||||||
reconcileElements as _reconcileElements,
|
reconcileElements as _reconcileElements,
|
||||||
} from "./reconciliation";
|
} from "./reconciliation";
|
||||||
import { decryptData } from "../../data/encryption";
|
import { decryptData } from "../../src/data/encryption";
|
||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
@@ -6,19 +6,19 @@ import {
|
|||||||
|
|
||||||
import { TCollabClass } from "./Collab";
|
import { TCollabClass } from "./Collab";
|
||||||
|
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../src/element/types";
|
||||||
import {
|
import {
|
||||||
WS_EVENTS,
|
WS_EVENTS,
|
||||||
FILE_UPLOAD_TIMEOUT,
|
FILE_UPLOAD_TIMEOUT,
|
||||||
WS_SCENE_EVENT_TYPES,
|
WS_SCENE_EVENT_TYPES,
|
||||||
} from "../app_constants";
|
} from "../app_constants";
|
||||||
import { UserIdleState } from "../../types";
|
import { UserIdleState } from "../../src/types";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../src/analytics";
|
||||||
import throttle from "lodash.throttle";
|
import throttle from "lodash.throttle";
|
||||||
import { newElementWith } from "../../element/mutateElement";
|
import { newElementWith } from "../../src/element/mutateElement";
|
||||||
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
import { BroadcastedExcalidrawElement } from "./reconciliation";
|
||||||
import { encryptData } from "../../data/encryption";
|
import { encryptData } from "../../src/data/encryption";
|
||||||
import { PRECEDING_ELEMENT_KEY } from "../../constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||||
|
|
||||||
class Portal {
|
class Portal {
|
||||||
collab: TCollabClass;
|
collab: TCollabClass;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@import "../../css/variables.module";
|
@import "../../src/css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
.RoomDialog {
|
.RoomDialog {
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
|
||||||
import { copyTextToSystemClipboard } from "../../clipboard";
|
import { copyTextToSystemClipboard } from "../../src/clipboard";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../src/analytics";
|
||||||
import { getFrame } from "../../utils";
|
import { getFrame } from "../../src/utils";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../src/i18n";
|
||||||
import { KEYS } from "../../keys";
|
import { KEYS } from "../../src/keys";
|
||||||
|
|
||||||
import { Dialog } from "../../components/Dialog";
|
import { Dialog } from "../../src/components/Dialog";
|
||||||
import {
|
import {
|
||||||
copyIcon,
|
copyIcon,
|
||||||
playerPlayIcon,
|
playerPlayIcon,
|
||||||
@@ -16,11 +16,11 @@ import {
|
|||||||
shareIOS,
|
shareIOS,
|
||||||
shareWindows,
|
shareWindows,
|
||||||
tablerCheckIcon,
|
tablerCheckIcon,
|
||||||
} from "../../components/icons";
|
} from "../../src/components/icons";
|
||||||
import { TextField } from "../../components/TextField";
|
import { TextField } from "../../src/components/TextField";
|
||||||
import { FilledButton } from "../../components/FilledButton";
|
import { FilledButton } from "../../src/components/FilledButton";
|
||||||
|
|
||||||
import { ReactComponent as CollabImage } from "../../assets/lock.svg";
|
import { ReactComponent as CollabImage } from "../../src/assets/lock.svg";
|
||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
|
|
||||||
const getShareIcon = () => {
|
const getShareIcon = () => {
|
||||||
+4
-4
@@ -1,7 +1,7 @@
|
|||||||
import { PRECEDING_ELEMENT_KEY } from "../../constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../src/element/types";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../src/types";
|
||||||
import { arrayToMapWithIndex } from "../../utils";
|
import { arrayToMapWithIndex } from "../../src/utils";
|
||||||
|
|
||||||
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
||||||
_brand: "reconciledElements";
|
_brand: "reconciledElements";
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Footer } from "../../packages/excalidraw/index";
|
import { Footer } from "../../src/packages/excalidraw/index";
|
||||||
import { EncryptedIcon } from "./EncryptedIcon";
|
import { EncryptedIcon } from "./EncryptedIcon";
|
||||||
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
import { ExcalidrawPlusAppLink } from "./ExcalidrawPlusAppLink";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../components/icons";
|
import { PlusPromoIcon } from "../../src/components/icons";
|
||||||
import { MainMenu } from "../../packages/excalidraw/index";
|
import { MainMenu } from "../../src/packages/excalidraw/index";
|
||||||
import { LanguageList } from "./LanguageList";
|
import { LanguageList } from "./LanguageList";
|
||||||
|
|
||||||
export const AppMainMenu: React.FC<{
|
export const AppMainMenu: React.FC<{
|
||||||
+4
-4
@@ -1,9 +1,9 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../components/icons";
|
import { PlusPromoIcon } from "../../src/components/icons";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../src/i18n";
|
||||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
import { WelcomeScreen } from "../../src/packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
import { POINTER_EVENTS } from "../../constants";
|
import { POINTER_EVENTS } from "../../src/constants";
|
||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
import { shield } from "../../components/icons";
|
import { shield } from "../../src/components/icons";
|
||||||
import { Tooltip } from "../../components/Tooltip";
|
import { Tooltip } from "../../src/components/Tooltip";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../src/i18n";
|
||||||
|
|
||||||
export const EncryptedIcon = () => {
|
export const EncryptedIcon = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
+15
-13
@@ -1,20 +1,20 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Card } from "../../components/Card";
|
import { Card } from "../../src/components/Card";
|
||||||
import { ToolButton } from "../../components/ToolButton";
|
import { ToolButton } from "../../src/components/ToolButton";
|
||||||
import { serializeAsJSON } from "../../data/json";
|
import { serializeAsJSON } from "../../src/data/json";
|
||||||
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
||||||
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
import { FileId, NonDeletedExcalidrawElement } from "../../src/element/types";
|
||||||
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../src/i18n";
|
||||||
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
import { encryptData, generateEncryptionKey } from "../../src/data/encryption";
|
||||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
import { isInitializedImageElement } from "../../src/element/typeChecks";
|
||||||
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
import { FILE_UPLOAD_MAX_BYTES } from "../app_constants";
|
||||||
import { encodeFilesForUpload } from "../data/FileManager";
|
import { encodeFilesForUpload } from "../data/FileManager";
|
||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../src/constants";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../src/analytics";
|
||||||
import { getFrame } from "../../utils";
|
import { getFrame } from "../../src/utils";
|
||||||
import { ExcalidrawLogo } from "../../components/ExcalidrawLogo";
|
import { ExcalidrawLogo } from "../../src/components/ExcalidrawLogo";
|
||||||
|
|
||||||
export const exportToExcalidrawPlus = async (
|
export const exportToExcalidrawPlus = async (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
@@ -80,7 +80,8 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
appState: Partial<AppState>;
|
appState: Partial<AppState>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
}> = ({ elements, appState, files, onError }) => {
|
onSuccess: () => void;
|
||||||
|
}> = ({ elements, appState, files, onError, onSuccess }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Card color="primary">
|
<Card color="primary">
|
||||||
@@ -107,6 +108,7 @@ 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") {
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { THEME } from "../../constants";
|
import { THEME } from "../../src/constants";
|
||||||
import { Theme } from "../../element/types";
|
import { Theme } from "../../src/element/types";
|
||||||
|
|
||||||
// https://github.com/tholman/github-corners
|
// https://github.com/tholman/github-corners
|
||||||
export const GitHubCorner = React.memo(
|
export const GitHubCorner = React.memo(
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { appLangCodeAtom } from "..";
|
import { appLangCodeAtom } from "..";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../src/i18n";
|
||||||
import { languages } from "../../i18n";
|
import { languages } from "../../src/i18n";
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
const { t, langCode } = useI18n();
|
const { t, langCode } = useI18n();
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
import { compressData } from "../../data/encode";
|
import { compressData } from "../../src/data/encode";
|
||||||
import { newElementWith } from "../../element/mutateElement";
|
import { newElementWith } from "../../src/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
import { isInitializedImageElement } from "../../src/element/typeChecks";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawImageElement,
|
ExcalidrawImageElement,
|
||||||
FileId,
|
FileId,
|
||||||
InitializedExcalidrawImageElement,
|
InitializedExcalidrawImageElement,
|
||||||
} from "../../element/types";
|
} from "../../src/element/types";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../src/i18n";
|
||||||
import {
|
import {
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../../types";
|
} from "../../src/types";
|
||||||
|
|
||||||
export class FileManager {
|
export class FileManager {
|
||||||
/** files being fetched */
|
/** files being fetched */
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
import { createStore, entries, del, getMany, set, setMany } from "idb-keyval";
|
||||||
import { clearAppStateForLocalStorage } from "../../appState";
|
import { clearAppStateForLocalStorage } from "../../src/appState";
|
||||||
import { clearElementsForLocalStorage } from "../../element";
|
import { clearElementsForLocalStorage } from "../../src/element";
|
||||||
import { ExcalidrawElement, FileId } from "../../element/types";
|
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||||
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
import { AppState, BinaryFileData, BinaryFiles } from "../../src/types";
|
||||||
import { debounce } from "../../utils";
|
import { debounce } from "../../src/utils";
|
||||||
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||||
import { FileManager } from "./FileManager";
|
import { FileManager } from "./FileManager";
|
||||||
import { Locker } from "./Locker";
|
import { Locker } from "./Locker";
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import { ExcalidrawElement, FileId } from "../../element/types";
|
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||||
import { getSceneVersion } from "../../element";
|
import { getSceneVersion } from "../../src/element";
|
||||||
import Portal from "../collab/Portal";
|
import Portal from "../collab/Portal";
|
||||||
import { restoreElements } from "../../data/restore";
|
import { restoreElements } from "../../src/data/restore";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFileMetadata,
|
BinaryFileMetadata,
|
||||||
DataURL,
|
DataURL,
|
||||||
} from "../../types";
|
} from "../../src/types";
|
||||||
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
import { FILE_CACHE_MAX_AGE_SEC } from "../app_constants";
|
||||||
import { decompressData } from "../../data/encode";
|
import { decompressData } from "../../src/data/encode";
|
||||||
import { encryptData, decryptData } from "../../data/encryption";
|
import { encryptData, decryptData } from "../../src/data/encryption";
|
||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../src/constants";
|
||||||
import { reconcileElements } from "../collab/reconciliation";
|
import { reconcileElements } from "../collab/reconciliation";
|
||||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||||
import { ResolutionType } from "../../utility-types";
|
import { ResolutionType } from "../../src/utility-types";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
import { compressData, decompressData } from "../../data/encode";
|
import { compressData, decompressData } from "../../src/data/encode";
|
||||||
import {
|
import {
|
||||||
decryptData,
|
decryptData,
|
||||||
generateEncryptionKey,
|
generateEncryptionKey,
|
||||||
IV_LENGTH_BYTES,
|
IV_LENGTH_BYTES,
|
||||||
} from "../../data/encryption";
|
} from "../../src/data/encryption";
|
||||||
import { serializeAsJSON } from "../../data/json";
|
import { serializeAsJSON } from "../../src/data/json";
|
||||||
import { restore } from "../../data/restore";
|
import { restore } from "../../src/data/restore";
|
||||||
import { ImportedDataState } from "../../data/types";
|
import { ImportedDataState } from "../../src/data/types";
|
||||||
import { isInvisiblySmallElement } from "../../element/sizeHelpers";
|
import { isInvisiblySmallElement } from "../../src/element/sizeHelpers";
|
||||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
import { isInitializedImageElement } from "../../src/element/typeChecks";
|
||||||
import { ExcalidrawElement, FileId } from "../../element/types";
|
import { ExcalidrawElement, FileId } from "../../src/element/types";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../src/i18n";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
BinaryFileData,
|
BinaryFileData,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
UserIdleState,
|
UserIdleState,
|
||||||
} from "../../types";
|
} from "../../src/types";
|
||||||
import { bytesToHexString } from "../../utils";
|
import { bytesToHexString } from "../../src/utils";
|
||||||
import {
|
import {
|
||||||
DELETED_ELEMENT_TIMEOUT,
|
DELETED_ELEMENT_TIMEOUT,
|
||||||
FILE_UPLOAD_MAX_BYTES,
|
FILE_UPLOAD_MAX_BYTES,
|
||||||
@@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
|
|||||||
type: "MOUSE_LOCATION";
|
type: "MOUSE_LOCATION";
|
||||||
payload: {
|
payload: {
|
||||||
socketId: string;
|
socketId: string;
|
||||||
pointer: { x: number; y: number };
|
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||||
button: "down" | "up";
|
button: "down" | "up";
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
username: string;
|
username: string;
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../src/element/types";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
clearAppStateForLocalStorage,
|
clearAppStateForLocalStorage,
|
||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "../../appState";
|
} from "../../src/appState";
|
||||||
import { clearElementsForLocalStorage } from "../../element";
|
import { clearElementsForLocalStorage } from "../../src/element";
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
import { ImportedDataState } from "../../data/types";
|
import { ImportedDataState } from "../../src/data/types";
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -1,31 +1,31 @@
|
|||||||
import polyfill from "../polyfill";
|
import polyfill from "../src/polyfill";
|
||||||
import LanguageDetector from "i18next-browser-languagedetector";
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../src/analytics";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../src/appState";
|
||||||
import { ErrorDialog } from "../components/ErrorDialog";
|
import { ErrorDialog } from "../src/components/ErrorDialog";
|
||||||
import { TopErrorBoundary } from "../components/TopErrorBoundary";
|
import { TopErrorBoundary } from "../src/components/TopErrorBoundary";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EVENT,
|
EVENT,
|
||||||
THEME,
|
THEME,
|
||||||
TITLE_TIMEOUT,
|
TITLE_TIMEOUT,
|
||||||
VERSION_TIMEOUT,
|
VERSION_TIMEOUT,
|
||||||
} from "../constants";
|
} from "../src/constants";
|
||||||
import { loadFromBlob } from "../data/blob";
|
import { loadFromBlob } from "../src/data/blob";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
FileId,
|
FileId,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
Theme,
|
Theme,
|
||||||
} from "../element/types";
|
} from "../src/element/types";
|
||||||
import { useCallbackRefState } from "../hooks/useCallbackRefState";
|
import { useCallbackRefState } from "../src/hooks/useCallbackRefState";
|
||||||
import { t } from "../i18n";
|
import { t } from "../src/i18n";
|
||||||
import {
|
import {
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
defaultLang,
|
defaultLang,
|
||||||
LiveCollaborationTrigger,
|
LiveCollaborationTrigger,
|
||||||
} from "../packages/excalidraw/index";
|
} from "../src/packages/excalidraw/index";
|
||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
ExcalidrawInitialDataState,
|
ExcalidrawInitialDataState,
|
||||||
UIAppState,
|
UIAppState,
|
||||||
} from "../types";
|
} from "../src/types";
|
||||||
import {
|
import {
|
||||||
debounce,
|
debounce,
|
||||||
getVersion,
|
getVersion,
|
||||||
@@ -43,7 +43,7 @@ import {
|
|||||||
ResolvablePromise,
|
ResolvablePromise,
|
||||||
resolvablePromise,
|
resolvablePromise,
|
||||||
isRunningInIframe,
|
isRunningInIframe,
|
||||||
} from "../utils";
|
} from "../src/utils";
|
||||||
import {
|
import {
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
@@ -68,33 +68,40 @@ import {
|
|||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
import {
|
||||||
|
restore,
|
||||||
|
restoreAppState,
|
||||||
|
RestoredDataState,
|
||||||
|
} from "../src/data/restore";
|
||||||
import {
|
import {
|
||||||
ExportToExcalidrawPlus,
|
ExportToExcalidrawPlus,
|
||||||
exportToExcalidrawPlus,
|
exportToExcalidrawPlus,
|
||||||
} from "./components/ExportToExcalidrawPlus";
|
} from "./components/ExportToExcalidrawPlus";
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../src/element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../element/typeChecks";
|
import { isInitializedImageElement } from "../src/element/typeChecks";
|
||||||
import { loadFilesFromFirebase } from "./data/firebase";
|
import { loadFilesFromFirebase } from "./data/firebase";
|
||||||
import { LocalData } from "./data/LocalData";
|
import { LocalData } from "./data/LocalData";
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { reconcileElements } from "./collab/reconciliation";
|
import { reconcileElements } from "./collab/reconciliation";
|
||||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
import {
|
||||||
|
parseLibraryTokensFromUrl,
|
||||||
|
useHandleLibrary,
|
||||||
|
} from "../src/data/library";
|
||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
import { AppFooter } from "./components/AppFooter";
|
import { AppFooter } from "./components/AppFooter";
|
||||||
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||||
import { useAtomWithInitialValue } from "../jotai";
|
import { useAtomWithInitialValue } from "../src/jotai";
|
||||||
import { appJotaiStore } from "./app-jotai";
|
import { appJotaiStore } from "./app-jotai";
|
||||||
|
|
||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { ResolutionType } from "../utility-types";
|
import { ResolutionType } from "../src/utility-types";
|
||||||
import { ShareableLinkDialog } from "../components/ShareableLinkDialog";
|
import { ShareableLinkDialog } from "../src/components/ShareableLinkDialog";
|
||||||
import { openConfirmModal } from "../components/OverwriteConfirm/OverwriteConfirmState";
|
import { openConfirmModal } from "../src/components/OverwriteConfirm/OverwriteConfirmState";
|
||||||
import { OverwriteConfirmDialog } from "../components/OverwriteConfirm/OverwriteConfirm";
|
import { OverwriteConfirmDialog } from "../src/components/OverwriteConfirm/OverwriteConfirm";
|
||||||
import Trans from "../components/Trans";
|
import Trans from "../src/components/Trans";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -601,7 +608,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
) => {
|
) => {
|
||||||
if (exportedElements.length === 0) {
|
if (exportedElements.length === 0) {
|
||||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
try {
|
||||||
@@ -617,7 +624,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
setErrorMessage(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -627,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 });
|
||||||
setErrorMessage(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -707,6 +714,11 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
excalidrawAPI?.updateScene({
|
||||||
|
appState: { openDialog: null },
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { defaultLang } from "../../src/i18n";
|
||||||
|
import { UI } from "../../src/tests/helpers/ui";
|
||||||
|
import { screen, fireEvent, waitFor, render } from "../../src/tests/test-utils";
|
||||||
|
|
||||||
|
import ExcalidrawApp from "../../excalidraw-app";
|
||||||
|
|
||||||
|
describe("Test LanguageList", () => {
|
||||||
|
it("rerenders UI on language change", async () => {
|
||||||
|
await render(<ExcalidrawApp />);
|
||||||
|
|
||||||
|
// select rectangle tool to show properties menu
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
// english lang should display `thin` label
|
||||||
|
expect(screen.queryByTitle(/thin/i)).not.toBeNull();
|
||||||
|
fireEvent.click(document.querySelector(".dropdown-menu-button")!);
|
||||||
|
|
||||||
|
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||||
|
target: { value: "de-DE" },
|
||||||
|
});
|
||||||
|
// switching to german, `thin` label should no longer exist
|
||||||
|
await waitFor(() => expect(screen.queryByTitle(/thin/i)).toBeNull());
|
||||||
|
// reset language
|
||||||
|
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||||
|
target: { value: defaultLang.code },
|
||||||
|
});
|
||||||
|
// switching back to English
|
||||||
|
await waitFor(() => expect(screen.queryByTitle(/thin/i)).not.toBeNull());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../../excalidraw-app";
|
||||||
import {
|
import {
|
||||||
mockBoundingClientRect,
|
mockBoundingClientRect,
|
||||||
render,
|
render,
|
||||||
restoreOriginalGetBoundingClientRect,
|
restoreOriginalGetBoundingClientRect,
|
||||||
} from "./test-utils";
|
} from "../../src/tests/test-utils";
|
||||||
|
|
||||||
import { UI } from "./helpers/ui";
|
import { UI } from "../../src/tests/helpers/ui";
|
||||||
|
|
||||||
describe("Test MobileMenu", () => {
|
describe("Test MobileMenu", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
import { render, updateSceneData, waitFor } from "./test-utils";
|
import { render, updateSceneData, waitFor } from "../../src/tests/test-utils";
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../../excalidraw-app";
|
||||||
import { API } from "./helpers/api";
|
import { API } from "../../src/tests/helpers/api";
|
||||||
import { createUndoAction } from "../actions/actionHistory";
|
import { createUndoAction } from "../../src/actions/actionHistory";
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
Object.defineProperty(window, "crypto", {
|
Object.defineProperty(window, "crypto", {
|
||||||
@@ -16,7 +16,7 @@ Object.defineProperty(window, "crypto", {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
|
vi.mock("../../excalidraw-app/data/index.ts", async (importActual) => {
|
||||||
const module = (await importActual()) as any;
|
const module = (await importActual()) as any;
|
||||||
return {
|
return {
|
||||||
__esmodule: true,
|
__esmodule: true,
|
||||||
@@ -27,7 +27,7 @@ vi.mock("../excalidraw-app/data/index.ts", async (importActual) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../excalidraw-app/data/firebase.ts", () => {
|
vi.mock("../../excalidraw-app/data/firebase.ts", () => {
|
||||||
const loadFromFirebase = async () => null;
|
const loadFromFirebase = async () => null;
|
||||||
const saveToFirebase = () => {};
|
const saveToFirebase = () => {};
|
||||||
const isSavedToFirebase = () => true;
|
const isSavedToFirebase = () => true;
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { PRECEDING_ELEMENT_KEY } from "../constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../src/constants";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../../src/element/types";
|
||||||
import {
|
import {
|
||||||
BroadcastedExcalidrawElement,
|
BroadcastedExcalidrawElement,
|
||||||
ReconciledElements,
|
ReconciledElements,
|
||||||
reconcileElements,
|
reconcileElements,
|
||||||
} from "../excalidraw-app/collab/reconciliation";
|
} from "../../excalidraw-app/collab/reconciliation";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../../src/random";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../../src/types";
|
||||||
|
|
||||||
type Id = string;
|
type Id = string;
|
||||||
type ElementLike = {
|
type ElementLike = {
|
||||||
+3
-2
@@ -20,6 +20,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
|
"@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",
|
||||||
@@ -48,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": "0.2.0",
|
"points-on-curve": "1.0.1",
|
||||||
"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.5.2",
|
"roughjs": "4.6.4",
|
||||||
"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"
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"src": "apple-touch-icon.png",
|
"src": "apple-touch-icon.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "256x256"
|
"sizes": "180x180"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
|
|||||||
@@ -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, setCursor, updateActiveTool } from "../utils";
|
import { getShortcutKey, 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,6 +21,7 @@ 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",
|
||||||
|
|||||||
@@ -10,26 +10,34 @@ 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: (elements, appState, _, app) => {
|
perform: async (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,
|
||||||
});
|
});
|
||||||
|
|
||||||
copyToClipboard(elementsToCopy, app.files);
|
try {
|
||||||
|
await copyToClipboard(elementsToCopy, app.files);
|
||||||
|
} 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,
|
||||||
@@ -38,15 +46,91 @@ export const actionCopy = register({
|
|||||||
export const actionPaste = register({
|
export const actionPaste = register({
|
||||||
name: "paste",
|
name: "paste",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements: any, appStates: any, data, app) => {
|
perform: async (elements, appState, data, app) => {
|
||||||
app.pasteFromClipboard(null);
|
const MIME_TYPES: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
const clipboardItems = await navigator.clipboard?.read();
|
||||||
|
for (const item of clipboardItems) {
|
||||||
|
for (const type of item.types) {
|
||||||
|
try {
|
||||||
|
const blob = await item.getType(type);
|
||||||
|
MIME_TYPES[type] = await blob.text();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(
|
||||||
|
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(MIME_TYPES).length === 0) {
|
||||||
|
console.warn(
|
||||||
|
"No clipboard data found from clipboard.read(). Falling back to clipboard.readText()",
|
||||||
|
);
|
||||||
|
// throw so we fall back onto clipboard.readText()
|
||||||
|
throw new Error("No clipboard data found");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
try {
|
||||||
|
MIME_TYPES["text/plain"] = await navigator.clipboard?.readText();
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(`Cannot readText() from clipboard: ${error.message}`);
|
||||||
|
if (isFirefox) {
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: t("hints.firefox_clipboard_write"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`actionPaste: ${error.message}`);
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
console.log("actionPaste (1)", { MIME_TYPES });
|
||||||
|
const event = new ClipboardEvent("paste", {
|
||||||
|
clipboardData: new DataTransfer(),
|
||||||
|
});
|
||||||
|
for (const [type, value] of Object.entries(MIME_TYPES)) {
|
||||||
|
try {
|
||||||
|
event.clipboardData?.setData(type, value);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn(
|
||||||
|
`Cannot set ${type} as clipboardData item: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.clipboardData?.types.forEach((type) => {
|
||||||
|
console.log(
|
||||||
|
`actionPaste (2) event.clipboardData?.getData(${type})`,
|
||||||
|
event.clipboardData?.getData(type),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
app.pasteFromClipboard(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.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,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const deleteSelectedElements = (
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -191,7 +191,15 @@ export const actionSaveFileToDisk = register({
|
|||||||
},
|
},
|
||||||
app.files,
|
app.files,
|
||||||
);
|
);
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return {
|
||||||
|
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);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { isInvisiblySmallElement } from "../element";
|
import { isInvisiblySmallElement } from "../element";
|
||||||
import { updateActiveTool, resetCursor } from "../utils";
|
import { updateActiveTool } 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,6 +15,7 @@ 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",
|
||||||
@@ -90,7 +91,9 @@ export const actionFinalize = register({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isInvisiblySmallElement(multiPointElement)) {
|
if (isInvisiblySmallElement(multiPointElement)) {
|
||||||
newElements = newElements.slice(0, -1);
|
newElements = newElements.filter(
|
||||||
|
(el) => el.id !== multiPointElement.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the multi point line closes the loop,
|
// If the multi point line closes the loop,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ 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 { setCursorForShape, updateActiveTool } from "../utils";
|
import { 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) => {
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppState } from "../../src/types";
|
import { AppState, Primitive } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@@ -51,6 +51,7 @@ 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 {
|
||||||
@@ -82,7 +83,6 @@ 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,25 +118,44 @@ export const changeProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormValue = function <T>(
|
export const getFormValue = function <T extends Primitive>(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
defaultValue: T,
|
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
|
||||||
|
defaultValue: T | ((isSomeElementSelected: boolean) => T),
|
||||||
): T {
|
): T {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
return (
|
|
||||||
(editingElement && getAttribute(editingElement)) ??
|
let ret: T | null = null;
|
||||||
(isSomeElementSelected(nonDeletedElements, appState)
|
|
||||||
? getCommonAttributeOfSelectedElements(
|
if (editingElement) {
|
||||||
nonDeletedElements,
|
ret = getAttribute(editingElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret) {
|
||||||
|
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
ret =
|
||||||
|
getCommonAttributeOfSelectedElements(
|
||||||
|
isRelevantElement === true
|
||||||
|
? nonDeletedElements
|
||||||
|
: nonDeletedElements.filter((el) => isRelevantElement(el)),
|
||||||
appState,
|
appState,
|
||||||
getAttribute,
|
getAttribute,
|
||||||
)
|
) ??
|
||||||
: defaultValue) ??
|
(typeof defaultValue === "function"
|
||||||
defaultValue
|
? defaultValue(true)
|
||||||
);
|
: defaultValue);
|
||||||
|
} else {
|
||||||
|
ret =
|
||||||
|
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
@@ -247,6 +266,7 @@ 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 })}
|
||||||
@@ -289,6 +309,7 @@ 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 })}
|
||||||
@@ -338,23 +359,28 @@ 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,
|
||||||
appState.currentItemFillStyle,
|
(element) => element.hasOwnProperty("fillStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemFillStyle,
|
||||||
)}
|
)}
|
||||||
onClick={(value, event) => {
|
onClick={(value, event) => {
|
||||||
const nextValue =
|
const nextValue =
|
||||||
@@ -393,26 +419,31 @@ export const actionChangeStrokeWidth = register({
|
|||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 1,
|
value: STROKE_WIDTH.thin,
|
||||||
text: t("labels.thin"),
|
text: t("labels.thin"),
|
||||||
icon: StrokeWidthBaseIcon,
|
icon: StrokeWidthBaseIcon,
|
||||||
|
testId: "strokeWidth-thin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: STROKE_WIDTH.bold,
|
||||||
text: t("labels.bold"),
|
text: t("labels.bold"),
|
||||||
icon: StrokeWidthBoldIcon,
|
icon: StrokeWidthBoldIcon,
|
||||||
|
testId: "strokeWidth-bold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 4,
|
value: STROKE_WIDTH.extraBold,
|
||||||
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,
|
||||||
appState.currentItemStrokeWidth,
|
(element) => element.hasOwnProperty("strokeWidth"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeWidth,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -461,7 +492,9 @@ export const actionChangeSloppiness = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.roughness,
|
(element) => element.roughness,
|
||||||
appState.currentItemRoughness,
|
(element) => element.hasOwnProperty("roughness"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemRoughness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -509,7 +542,9 @@ export const actionChangeStrokeStyle = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeStyle,
|
(element) => element.strokeStyle,
|
||||||
appState.currentItemStrokeStyle,
|
(element) => element.hasOwnProperty("strokeStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeStyle,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -549,6 +584,7 @@ export const actionChangeOpacity = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.opacity,
|
(element) => element.opacity,
|
||||||
|
true,
|
||||||
appState.currentItemOpacity,
|
appState.currentItemOpacity,
|
||||||
) ?? undefined
|
) ?? undefined
|
||||||
}
|
}
|
||||||
@@ -607,7 +643,12 @@ export const actionChangeFontSize = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -692,21 +733,25 @@ 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",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -729,7 +774,12 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -806,7 +856,10 @@ export const actionChangeTextAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemTextAlign,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -882,7 +935,9 @@ export const actionChangeVerticalAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
VERTICAL_ALIGN.MIDDLE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -947,9 +1002,9 @@ export const actionChangeRoundness = register({
|
|||||||
appState,
|
appState,
|
||||||
(element) =>
|
(element) =>
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||||
(canChangeRoundness(appState.activeTool.type) &&
|
(element) => element.hasOwnProperty("roundness"),
|
||||||
appState.currentItemRoundness) ||
|
(hasSelection) =>
|
||||||
null,
|
hasSelection ? null : appState.currentItemRoundness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -1043,6 +1098,7 @@ 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 })}
|
||||||
@@ -1089,6 +1145,7 @@ 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 })}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
@@ -80,6 +80,7 @@ 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";
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type ShortcutName =
|
|||||||
| "ungroup"
|
| "ungroup"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
|
| "objectsSnapMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "addToLibrary"
|
| "addToLibrary"
|
||||||
| "viewMode"
|
| "viewMode"
|
||||||
@@ -74,6 +75,7 @@ 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")],
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ export type ActionName =
|
|||||||
| "pasteStyles"
|
| "pasteStyles"
|
||||||
| "gridMode"
|
| "gridMode"
|
||||||
| "zenMode"
|
| "zenMode"
|
||||||
|
| "objectsSnapMode"
|
||||||
| "stats"
|
| "stats"
|
||||||
| "changeStrokeColor"
|
| "changeStrokeColor"
|
||||||
| "changeBackgroundColor"
|
| "changeBackgroundColor"
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ export const getDefaultAppState = (): Omit<
|
|||||||
pendingImageElementId: null,
|
pendingImageElementId: null,
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
|
snapLines: [],
|
||||||
|
originSnapOffset: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
objectsSnapModeEnabled: false,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -206,6 +212,9 @@ 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 = <
|
||||||
|
|||||||
+7
-12
@@ -1,26 +1,21 @@
|
|||||||
import { parseClipboard } from "./clipboard";
|
import { parseClipboard } from "./clipboard";
|
||||||
|
import { createPasteEvent } from "./tests/test-utils";
|
||||||
|
|
||||||
describe("Test parseClipboard", () => {
|
describe("Test parseClipboard", () => {
|
||||||
it("should parse valid json correctly", async () => {
|
it("should parse valid json correctly", async () => {
|
||||||
let text = "123";
|
let text = "123";
|
||||||
|
|
||||||
let clipboardData = await parseClipboard({
|
let clipboardData = await parseClipboard(
|
||||||
//@ts-ignore
|
createPasteEvent({ "text/plain": text }),
|
||||||
clipboardData: {
|
);
|
||||||
getData: () => text,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
|
|
||||||
text = "[123]";
|
text = "[123]";
|
||||||
|
|
||||||
clipboardData = await parseClipboard({
|
clipboardData = await parseClipboard(
|
||||||
//@ts-ignore
|
createPasteEvent({ "text/plain": text }),
|
||||||
clipboardData: {
|
);
|
||||||
getData: () => text,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
});
|
});
|
||||||
|
|||||||
+74
-16
@@ -18,11 +18,14 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -115,7 +118,7 @@ export const copyToClipboard = async (
|
|||||||
await copyTextToSystemClipboard(json);
|
await copyTextToSystemClipboard(json);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
PREFER_APP_CLIPBOARD = true;
|
PREFER_APP_CLIPBOARD = true;
|
||||||
console.error(error);
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,22 +145,71 @@ 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) => {
|
||||||
|
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 content;
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`error in parseHTMLFromPaste: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves content from system clipboard (either from ClipboardEvent or
|
* Retrieves content from system clipboard (either from ClipboardEvent or
|
||||||
* via async clipboard API if supported)
|
* via async clipboard API if supported)
|
||||||
*/
|
*/
|
||||||
export const getSystemClipboard = async (
|
const getSystemClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent,
|
||||||
): Promise<string> => {
|
isPlainPaste = false,
|
||||||
|
): Promise<
|
||||||
|
| { type: "text"; value: string }
|
||||||
|
| { type: "mixedContent"; value: PastedMixedContent }
|
||||||
|
> => {
|
||||||
try {
|
try {
|
||||||
const text = event
|
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
||||||
? event.clipboardData?.getData("text/plain")
|
if (mixedContent) {
|
||||||
: probablySupportsClipboardReadText &&
|
return { type: "mixedContent", value: mixedContent };
|
||||||
(await navigator.clipboard.readText());
|
}
|
||||||
|
|
||||||
return (text || "").trim();
|
const text = event.clipboardData?.getData("text/plain");
|
||||||
|
|
||||||
|
return { type: "text", value: (text || "").trim() };
|
||||||
} catch {
|
} catch {
|
||||||
return "";
|
return { type: "text", value: "" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,17 +217,23 @@ export const getSystemClipboard = 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 | null,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const systemClipboard = await getSystemClipboard(event);
|
const systemClipboard = await getSystemClipboard(event, isPlainPaste);
|
||||||
|
|
||||||
|
if (systemClipboard.type === "mixedContent") {
|
||||||
|
return {
|
||||||
|
mixedContent: systemClipboard.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// if system clipboard empty, couldn't be resolved, or contains previously
|
// if system clipboard empty, couldn't be resolved, or contains previously
|
||||||
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
||||||
// elements
|
// elements
|
||||||
if (
|
if (
|
||||||
!systemClipboard ||
|
!systemClipboard ||
|
||||||
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
|
(!isPlainPaste && systemClipboard.value.includes(SVG_EXPORT_TAG))
|
||||||
) {
|
) {
|
||||||
return getAppClipboard();
|
return getAppClipboard();
|
||||||
}
|
}
|
||||||
@@ -183,7 +241,7 @@ export const parseClipboard = async (
|
|||||||
// 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(systemClipboard.value);
|
||||||
|
|
||||||
if (spreadsheetResult) {
|
if (spreadsheetResult) {
|
||||||
return spreadsheetResult;
|
return spreadsheetResult;
|
||||||
@@ -192,7 +250,7 @@ export const parseClipboard = async (
|
|||||||
const appClipboardData = getAppClipboard();
|
const appClipboardData = getAppClipboard();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const systemClipboardData = JSON.parse(systemClipboard);
|
const systemClipboardData = JSON.parse(systemClipboard.value);
|
||||||
const programmaticAPI =
|
const programmaticAPI =
|
||||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
@@ -216,7 +274,7 @@ export const parseClipboard = async (
|
|||||||
? JSON.stringify(appClipboardData.elements, null, 2)
|
? JSON.stringify(appClipboardData.elements, null, 2)
|
||||||
: undefined,
|
: undefined,
|
||||||
}
|
}
|
||||||
: { text: systemClipboard };
|
: { text: systemClipboard.value };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||||
|
|||||||
@@ -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: transparent !important;
|
background-color: var(--color-surface-low) !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);
|
||||||
|
|||||||
+60
-82
@@ -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, PointerType } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import {
|
import {
|
||||||
@@ -11,21 +11,15 @@ import {
|
|||||||
hasBackground,
|
hasBackground,
|
||||||
hasStrokeStyle,
|
hasStrokeStyle,
|
||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
hasText,
|
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { UIAppState, Zoom } from "../types";
|
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||||
import {
|
import { capitalizeString, isTransparent } from "../utils";
|
||||||
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 } from "../element/typeChecks";
|
import { hasBoundTextElement, isTextElement } 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";
|
||||||
@@ -36,7 +30,12 @@ import {
|
|||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||||
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
import {
|
||||||
|
EmbedIcon,
|
||||||
|
extraToolsIcon,
|
||||||
|
frameToolIcon,
|
||||||
|
laserPointerToolIcon,
|
||||||
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const SelectedShapeActions = ({
|
export const SelectedShapeActions = ({
|
||||||
@@ -66,7 +65,8 @@ 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),
|
||||||
@@ -123,14 +123,15 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(appState.activeTool.type) ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some(isTextElement)) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{suppportsHorizontalAlign(targetElements) &&
|
{(appState.activeTool.type === "text" ||
|
||||||
|
suppportsHorizontalAlign(targetElements)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -213,20 +214,21 @@ 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 device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
|
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) => {
|
||||||
@@ -251,31 +253,20 @@ 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") {
|
||||||
setAppState({
|
app.togglePenMode(true);
|
||||||
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") {
|
||||||
onImageAction({ pointerType });
|
app.setActiveTool({
|
||||||
|
type: value,
|
||||||
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: value });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -300,24 +291,14 @@ export const ShapesSwitcher = ({
|
|||||||
data-testid={`toolbar-frame`}
|
data-testid={`toolbar-frame`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
setAppState({
|
app.togglePenMode(true);
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
trackEvent("toolbar", "frame", "ui");
|
trackEvent("toolbar", "frame", "ui");
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "frame" });
|
||||||
type: "frame",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
activeEmbeddable: null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
|
selected={activeTool.type === "frame"}
|
||||||
/>
|
/>
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className={clsx("Shape", { fillable: false })}
|
className={clsx("Shape", { fillable: false })}
|
||||||
@@ -330,30 +311,28 @@ export const ShapesSwitcher = ({
|
|||||||
data-testid={`toolbar-embeddable`}
|
data-testid={`toolbar-embeddable`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
setAppState({
|
app.togglePenMode(true);
|
||||||
penDetected: true,
|
|
||||||
penMode: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onChange={({ pointerType }) => {
|
onChange={({ pointerType }) => {
|
||||||
trackEvent("toolbar", "embeddable", "ui");
|
trackEvent("toolbar", "embeddable", "ui");
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "embeddable" });
|
||||||
type: "embeddable",
|
|
||||||
});
|
|
||||||
setAppState({
|
|
||||||
activeTool: nextActiveTool,
|
|
||||||
multiElement: null,
|
|
||||||
selectedElementIds: {},
|
|
||||||
activeEmbeddable: null,
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
|
selected={activeTool.type === "embeddable"}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
<DropdownMenu.Trigger
|
<DropdownMenu.Trigger
|
||||||
className="App-toolbar__extra-tools-trigger"
|
className={clsx("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")}
|
||||||
>
|
>
|
||||||
@@ -366,37 +345,36 @@ export const ShapesSwitcher = ({
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "frame" });
|
||||||
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={() => {
|
onSelect={() => {
|
||||||
const nextActiveTool = updateActiveTool(appState, {
|
app.setActiveTool({ type: "embeddable" });
|
||||||
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
|
||||||
|
onSelect={() => {
|
||||||
|
app.setActiveTool({ type: "laser" });
|
||||||
|
}}
|
||||||
|
icon={laserPointerToolIcon}
|
||||||
|
data-testid="toolbar-laser"
|
||||||
|
selected={laserToolSelected}
|
||||||
|
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||||
|
>
|
||||||
|
{t("toolBar.laser")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+543
-114
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,10 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { getColor } from "./ColorPicker";
|
import { getColor } from "./ColorPicker";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
import {
|
||||||
|
ColorPickerType,
|
||||||
|
activeColorPickerSectionAtom,
|
||||||
|
} from "./colorPickerUtils";
|
||||||
import { eyeDropperIcon } from "../icons";
|
import { eyeDropperIcon } from "../icons";
|
||||||
import { jotaiScope } from "../../jotai";
|
import { jotaiScope } from "../../jotai";
|
||||||
import { KEYS } from "../../keys";
|
import { KEYS } from "../../keys";
|
||||||
@@ -15,14 +18,14 @@ interface ColorInputProps {
|
|||||||
color: string;
|
color: string;
|
||||||
onChange: (color: string) => void;
|
onChange: (color: string) => void;
|
||||||
label: string;
|
label: string;
|
||||||
eyeDropperType: "strokeColor" | "backgroundColor";
|
colorPickerType: ColorPickerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ColorInput = ({
|
export const ColorInput = ({
|
||||||
color,
|
color,
|
||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
eyeDropperType,
|
colorPickerType,
|
||||||
}: ColorInputProps) => {
|
}: ColorInputProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const [innerValue, setInnerValue] = useState(color);
|
const [innerValue, setInnerValue] = useState(color);
|
||||||
@@ -116,7 +119,7 @@ export const ColorInput = ({
|
|||||||
: {
|
: {
|
||||||
keepOpenOnAlt: false,
|
keepOpenOnAlt: false,
|
||||||
onSelect: (color) => onChange(color),
|
onSelect: (color) => onChange(color),
|
||||||
previewType: eyeDropperType,
|
colorPickerType,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,14 +82,7 @@ const ColorPickerPopupContent = ({
|
|||||||
const { container } = useExcalidrawContainer();
|
const { container } = useExcalidrawContainer();
|
||||||
const { isMobile, isLandscape } = useDevice();
|
const { isMobile, isLandscape } = useDevice();
|
||||||
|
|
||||||
const eyeDropperType =
|
const colorInputJSX = (
|
||||||
type === "canvasBackground"
|
|
||||||
? undefined
|
|
||||||
: type === "elementBackground"
|
|
||||||
? "backgroundColor"
|
|
||||||
: "strokeColor";
|
|
||||||
|
|
||||||
const colorInputJSX = eyeDropperType && (
|
|
||||||
<div>
|
<div>
|
||||||
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
|
<PickerHeading>{t("colorPicker.hexCode")}</PickerHeading>
|
||||||
<ColorInput
|
<ColorInput
|
||||||
@@ -98,7 +91,7 @@ const ColorPickerPopupContent = ({
|
|||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
onChange(color);
|
onChange(color);
|
||||||
}}
|
}}
|
||||||
eyeDropperType={eyeDropperType}
|
colorPickerType={type}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -160,7 +153,7 @@ const ColorPickerPopupContent = ({
|
|||||||
"0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
|
"0px 7px 14px rgba(0, 0, 0, 0.05), 0px 0px 3.12708px rgba(0, 0, 0, 0.0798), 0px 0px 0.931014px rgba(0, 0, 0, 0.1702)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{palette && eyeDropperType ? (
|
{palette ? (
|
||||||
<Picker
|
<Picker
|
||||||
palette={palette}
|
palette={palette}
|
||||||
color={color}
|
color={color}
|
||||||
@@ -173,7 +166,7 @@ const ColorPickerPopupContent = ({
|
|||||||
state = state || {
|
state = state || {
|
||||||
keepOpenOnAlt: true,
|
keepOpenOnAlt: true,
|
||||||
onSelect: onChange,
|
onSelect: onChange,
|
||||||
previewType: eyeDropperType,
|
colorPickerType: type,
|
||||||
};
|
};
|
||||||
state.keepOpenOnAlt = true;
|
state.keepOpenOnAlt = true;
|
||||||
return state;
|
return state;
|
||||||
@@ -184,7 +177,7 @@ const ColorPickerPopupContent = ({
|
|||||||
: {
|
: {
|
||||||
keepOpenOnAlt: false,
|
keepOpenOnAlt: false,
|
||||||
onSelect: onChange,
|
onSelect: onChange,
|
||||||
previewType: eyeDropperType,
|
colorPickerType: type,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ 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,11 +9,7 @@ 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 {
|
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
|
||||||
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;
|
||||||
@@ -25,14 +21,14 @@ type ContextMenuProps = {
|
|||||||
items: ContextMenuItems;
|
items: ContextMenuItems;
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
|
onClose: (cb?: () => 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 }: ContextMenuProps) => {
|
({ actionManager, items, top, left, onClose }: 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) => {
|
||||||
@@ -54,7 +50,7 @@ export const ContextMenu = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
onCloseRequest={() => setAppState({ contextMenu: null })}
|
onCloseRequest={() => onClose()}
|
||||||
top={top}
|
top={top}
|
||||||
left={left}
|
left={left}
|
||||||
fitInViewport={true}
|
fitInViewport={true}
|
||||||
@@ -102,7 +98,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.
|
||||||
setAppState({ contextMenu: null }, () => {
|
onClose(() => {
|
||||||
actionManager.executeAction(item, "contextMenu");
|
actionManager.executeAction(item, "contextMenu");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,35 +1,47 @@
|
|||||||
import { atom } from "jotai";
|
import { atom } from "jotai";
|
||||||
import { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { rgbToHex } from "../colors";
|
import { rgbToHex } from "../colors";
|
||||||
import { EVENT } from "../constants";
|
import { EVENT } from "../constants";
|
||||||
import { useUIAppState } from "../context/ui-appState";
|
import { useUIAppState } from "../context/ui-appState";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
|
||||||
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
|
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
|
||||||
import { useOutsideClick } from "../hooks/useOutsideClick";
|
import { useOutsideClick } from "../hooks/useOutsideClick";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
|
||||||
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
|
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
|
||||||
|
import { useStable } from "../hooks/useStable";
|
||||||
|
|
||||||
import "./EyeDropper.scss";
|
import "./EyeDropper.scss";
|
||||||
|
import { ColorPickerType } from "./ColorPicker/colorPickerUtils";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
|
||||||
type EyeDropperProperties = {
|
export type EyeDropperProperties = {
|
||||||
keepOpenOnAlt: boolean;
|
keepOpenOnAlt: boolean;
|
||||||
swapPreviewOnAlt?: boolean;
|
swapPreviewOnAlt?: boolean;
|
||||||
|
/** called when user picks color (on pointerup) */
|
||||||
onSelect: (color: string, event: PointerEvent) => void;
|
onSelect: (color: string, event: PointerEvent) => void;
|
||||||
previewType: "strokeColor" | "backgroundColor";
|
/**
|
||||||
|
* property of selected elements to update live when alt-dragging.
|
||||||
|
* Supply `null` if not applicable (e.g. updating the canvas bg instead of
|
||||||
|
* elements)
|
||||||
|
**/
|
||||||
|
colorPickerType: ColorPickerType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
|
export const activeEyeDropperAtom = atom<null | EyeDropperProperties>(null);
|
||||||
|
|
||||||
export const EyeDropper: React.FC<{
|
export const EyeDropper: React.FC<{
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSelect: Required<EyeDropperProperties>["onSelect"];
|
onSelect: EyeDropperProperties["onSelect"];
|
||||||
swapPreviewOnAlt?: EyeDropperProperties["swapPreviewOnAlt"];
|
/** called when color changes, on pointerdown for preview */
|
||||||
previewType: EyeDropperProperties["previewType"];
|
onChange: (
|
||||||
}> = ({ onCancel, onSelect, swapPreviewOnAlt, previewType }) => {
|
type: ColorPickerType,
|
||||||
|
color: string,
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
event: { altKey: boolean },
|
||||||
|
) => void;
|
||||||
|
colorPickerType: EyeDropperProperties["colorPickerType"];
|
||||||
|
}> = ({ onCancel, onChange, onSelect, colorPickerType }) => {
|
||||||
const eyeDropperContainer = useCreatePortalContainer({
|
const eyeDropperContainer = useCreatePortalContainer({
|
||||||
className: "excalidraw-eye-dropper-backdrop",
|
className: "excalidraw-eye-dropper-backdrop",
|
||||||
parentSelector: ".excalidraw-eye-dropper-container",
|
parentSelector: ".excalidraw-eye-dropper-container",
|
||||||
@@ -40,9 +52,13 @@ export const EyeDropper: React.FC<{
|
|||||||
|
|
||||||
const selectedElements = getSelectedElements(elements, appState);
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
|
||||||
const metaStuffRef = useRef({ selectedElements, app });
|
const stableProps = useStable({
|
||||||
metaStuffRef.current.selectedElements = selectedElements;
|
app,
|
||||||
metaStuffRef.current.app = app;
|
onCancel,
|
||||||
|
onChange,
|
||||||
|
onSelect,
|
||||||
|
selectedElements,
|
||||||
|
});
|
||||||
|
|
||||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
@@ -90,28 +106,28 @@ export const EyeDropper: React.FC<{
|
|||||||
const currentColor = getCurrentColor({ clientX, clientY });
|
const currentColor = getCurrentColor({ clientX, clientY });
|
||||||
|
|
||||||
if (isHoldingPointerDown) {
|
if (isHoldingPointerDown) {
|
||||||
for (const element of metaStuffRef.current.selectedElements) {
|
stableProps.onChange(
|
||||||
mutateElement(
|
colorPickerType,
|
||||||
element,
|
currentColor,
|
||||||
{
|
stableProps.selectedElements,
|
||||||
[altKey && swapPreviewOnAlt
|
{ altKey },
|
||||||
? previewType === "strokeColor"
|
);
|
||||||
? "backgroundColor"
|
|
||||||
: "strokeColor"
|
|
||||||
: previewType]: currentColor,
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
ShapeCache.delete(element);
|
|
||||||
}
|
|
||||||
Scene.getScene(
|
|
||||||
metaStuffRef.current.selectedElements[0],
|
|
||||||
)?.informMutation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
colorPreviewDiv.style.background = currentColor;
|
colorPreviewDiv.style.background = currentColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
stableProps.onCancel();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect: Required<EyeDropperProperties>["onSelect"] = (
|
||||||
|
color,
|
||||||
|
event,
|
||||||
|
) => {
|
||||||
|
stableProps.onSelect(color, event);
|
||||||
|
};
|
||||||
|
|
||||||
const pointerDownListener = (event: PointerEvent) => {
|
const pointerDownListener = (event: PointerEvent) => {
|
||||||
isHoldingPointerDown = true;
|
isHoldingPointerDown = true;
|
||||||
// NOTE we can't event.preventDefault() as that would stop
|
// NOTE we can't event.preventDefault() as that would stop
|
||||||
@@ -148,8 +164,8 @@ export const EyeDropper: React.FC<{
|
|||||||
|
|
||||||
// init color preview else it would show only after the first mouse move
|
// init color preview else it would show only after the first mouse move
|
||||||
mouseMoveListener({
|
mouseMoveListener({
|
||||||
clientX: metaStuffRef.current.app.lastViewportPosition.x,
|
clientX: stableProps.app.lastViewportPosition.x,
|
||||||
clientY: metaStuffRef.current.app.lastViewportPosition.y,
|
clientY: stableProps.app.lastViewportPosition.y,
|
||||||
altKey: false,
|
altKey: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,12 +195,10 @@ export const EyeDropper: React.FC<{
|
|||||||
window.removeEventListener(EVENT.BLUR, onCancel);
|
window.removeEventListener(EVENT.BLUR, onCancel);
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
|
stableProps,
|
||||||
app.canvas,
|
app.canvas,
|
||||||
eyeDropperContainer,
|
eyeDropperContainer,
|
||||||
onCancel,
|
colorPickerType,
|
||||||
onSelect,
|
|
||||||
swapPreviewOnAlt,
|
|
||||||
previewType,
|
|
||||||
excalidrawContainer,
|
excalidrawContainer,
|
||||||
appState.offsetLeft,
|
appState.offsetLeft,
|
||||||
appState.offsetTop,
|
appState.offsetTop,
|
||||||
|
|||||||
@@ -12,32 +12,32 @@
|
|||||||
|
|
||||||
&--color-primary {
|
&--color-primary {
|
||||||
&.ExcButton--variant-filled {
|
&.ExcButton--variant-filled {
|
||||||
--text-color: var(--input-bg-color);
|
--text-color: var(--color-surface-lowest);
|
||||||
--back-color: var(--color-primary);
|
--back-color: var(--color-primary);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--back-color: var(--color-primary-darker);
|
--back-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
--back-color: var(--color-primary-darkest);
|
--back-color: var(--color-brand-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.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-primary);
|
--border-color: var(--color-border-outline);
|
||||||
--back-color: var(--input-bg-color);
|
--back-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
--text-color: var(--color-primary-darker);
|
--text-color: var(--color-brand-hover);
|
||||||
--border-color: var(--color-primary-darker);
|
--border-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
--text-color: var(--color-primary-darkest);
|
--text-color: var(--color-brand-active);
|
||||||
--border-color: var(--color-primary-darkest);
|
--border-color: var(--color-brand-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,20 +19,35 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__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;
|
||||||
border: 1px solid var(--default-border-color);
|
background-color: var(--background);
|
||||||
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 {
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ 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"]}
|
||||||
@@ -258,6 +259,10 @@ 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+'")]}
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ 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;
|
||||||
@@ -72,9 +75,14 @@ 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={() => {
|
onClick={async () => {
|
||||||
onExportToBackend(elements, appState, files, canvas);
|
try {
|
||||||
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>
|
||||||
@@ -114,6 +122,7 @@ 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}
|
||||||
|
|||||||
@@ -0,0 +1,309 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+62
-27
@@ -52,13 +52,16 @@ import { EyeDropper, activeEyeDropperAtom } from "./EyeDropper";
|
|||||||
|
|
||||||
import "./LayerUI.scss";
|
import "./LayerUI.scss";
|
||||||
import "./Toolbar.scss";
|
import "./Toolbar.scss";
|
||||||
|
import { mutateElement } from "../element/mutateElement";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
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;
|
||||||
@@ -69,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<{
|
||||||
@@ -118,7 +121,6 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
interactiveCanvas,
|
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
@@ -126,11 +128,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();
|
||||||
@@ -274,17 +276,29 @@ const LayerUI = ({
|
|||||||
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
interactiveCanvas={interactiveCanvas}
|
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
app={app}
|
||||||
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>
|
||||||
@@ -368,11 +382,44 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
{eyeDropperState && !device.isMobile && (
|
{eyeDropperState && !device.isMobile && (
|
||||||
<EyeDropper
|
<EyeDropper
|
||||||
swapPreviewOnAlt={eyeDropperState.swapPreviewOnAlt}
|
colorPickerType={eyeDropperState.colorPickerType}
|
||||||
previewType={eyeDropperState.previewType}
|
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
setEyeDropperState(null);
|
setEyeDropperState(null);
|
||||||
}}
|
}}
|
||||||
|
onChange={(colorPickerType, color, selectedElements, { altKey }) => {
|
||||||
|
if (
|
||||||
|
colorPickerType !== "elementBackground" &&
|
||||||
|
colorPickerType !== "elementStroke"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedElements.length) {
|
||||||
|
for (const element of selectedElements) {
|
||||||
|
mutateElement(
|
||||||
|
element,
|
||||||
|
{
|
||||||
|
[altKey && eyeDropperState.swapPreviewOnAlt
|
||||||
|
? colorPickerType === "elementBackground"
|
||||||
|
? "strokeColor"
|
||||||
|
: "backgroundColor"
|
||||||
|
: colorPickerType === "elementBackground"
|
||||||
|
? "backgroundColor"
|
||||||
|
: "strokeColor"]: color,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
ShapeCache.delete(element);
|
||||||
|
}
|
||||||
|
Scene.getScene(selectedElements[0])?.informMutation();
|
||||||
|
} else if (colorPickerType === "elementBackground") {
|
||||||
|
setAppState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setAppState({ currentItemStrokeColor: color });
|
||||||
|
}
|
||||||
|
}}
|
||||||
onSelect={(color, event) => {
|
onSelect={(color, event) => {
|
||||||
setEyeDropperState((state) => {
|
setEyeDropperState((state) => {
|
||||||
return state?.keepOpenOnAlt && event.altKey ? state : null;
|
return state?.keepOpenOnAlt && event.altKey ? state : null;
|
||||||
@@ -415,8 +462,6 @@ 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}
|
||||||
@@ -503,18 +548,8 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps;
|
||||||
canvas: _pC,
|
const { canvas: _nC, appState: nextAppState, ...next } = nextProps;
|
||||||
interactiveCanvas: _pIC,
|
|
||||||
appState: prevAppState,
|
|
||||||
...prev
|
|
||||||
} = prevProps;
|
|
||||||
const {
|
|
||||||
canvas: _nC,
|
|
||||||
interactiveCanvas: _nIC,
|
|
||||||
appState: nextAppState,
|
|
||||||
...next
|
|
||||||
} = nextProps;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isShallowEqual(
|
isShallowEqual(
|
||||||
|
|||||||
@@ -99,10 +99,10 @@
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-primary-darker);
|
background-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--color-primary-darkest);
|
background-color: var(--color-brand-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,7 @@ 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,
|
||||||
@@ -58,8 +56,7 @@ export const MobileMenu = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
interactiveCanvas,
|
|
||||||
onImageAction,
|
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
@@ -85,14 +82,8 @@ 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}
|
||||||
setAppState={setAppState}
|
app={app}
|
||||||
onImageAction={({ pointerType }) => {
|
|
||||||
onImageAction({
|
|
||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--RadioGroup-background: #ffffff;
|
--RadioGroup-background: var(--island-bg-color);
|
||||||
--RadioGroup-border: var(--color-gray-30);
|
--RadioGroup-border: var(--color-surface-high);
|
||||||
|
|
||||||
--RadioGroup-choice-color-off: var(--color-primary);
|
--RadioGroup-choice-color-off: var(--color-primary);
|
||||||
--RadioGroup-choice-color-off-hover: var(--color-primary-darkest);
|
--RadioGroup-choice-color-off-hover: var(--color-brand-hover);
|
||||||
--RadioGroup-choice-background-off: white;
|
--RadioGroup-choice-background-off: var(--island-bg-color);
|
||||||
--RadioGroup-choice-background-off-active: var(--color-gray-20);
|
--RadioGroup-choice-background-off-active: var(--color-surface-high);
|
||||||
|
|
||||||
--RadioGroup-choice-color-on: white;
|
--RadioGroup-choice-color-on: var(--color-surface-lowest);
|
||||||
--RadioGroup-choice-background-on: var(--color-primary);
|
--RadioGroup-choice-background-on: var(--color-primary);
|
||||||
--RadioGroup-choice-background-on-hover: var(--color-primary-darker);
|
--RadioGroup-choice-background-on-hover: var(--color-brand-hover);
|
||||||
--RadioGroup-choice-background-on-active: var(--color-primary-darkest);
|
--RadioGroup-choice-background-on-active: var(--color-brand-active);
|
||||||
|
|
||||||
&.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;
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
.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);
|
||||||
|
|||||||
+16
-14
@@ -1,15 +1,13 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--Switch-disabled-color: #d6d6d6;
|
--Switch-disabled-color: var(--color-border-outline);
|
||||||
--Switch-track-background: white;
|
--Switch-disabled-toggled-background: var(--color-border-outline-variant);
|
||||||
--Switch-thumb-background: #3d3d3d;
|
--Switch-disabled-border: var(--color-border-outline-variant);
|
||||||
|
--Switch-track-background: var(--island-bg-color);
|
||||||
&.theme--dark {
|
--Switch-thumb-background: var(--color-on-surface);
|
||||||
--Switch-disabled-color: #5c5c5c;
|
--Switch-hover-background: var(--color-brand-hover);
|
||||||
--Switch-track-background: #242424;
|
--Switch-active-background: var(--color-brand-active);
|
||||||
--Switch-thumb-background: #b8b8b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Switch {
|
.Switch {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -28,7 +26,11 @@
|
|||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--Switch-track-background);
|
background: var(--Switch-track-background);
|
||||||
border: 1px solid #999999;
|
border: 1px solid var(--Switch-hover-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
border: 1px solid var(--Switch-active-background);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.toggled {
|
&.toggled {
|
||||||
@@ -43,11 +45,11 @@
|
|||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
background: var(--Switch-track-background);
|
background: var(--Switch-track-background);
|
||||||
border: 1px solid var(--Switch-disabled-color);
|
border: 1px solid var(--Switch-disabled-border);
|
||||||
|
|
||||||
&.toggled {
|
&.toggled {
|
||||||
background: var(--Switch-disabled-color);
|
background: var(--Switch-disabled-toggled-background);
|
||||||
border: 1px solid var(--Switch-disabled-color);
|
border: 1px solid var(--Switch-disabled-toggled-background);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.disabled.toggled:before {
|
&.disabled.toggled:before {
|
||||||
background: var(--color-gray-50);
|
background: var(--Switch-disabled-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
|
|||||||
@@ -1,25 +1,16 @@
|
|||||||
@import "../css/variables.module";
|
@import "../css/variables.module";
|
||||||
|
|
||||||
.excalidraw {
|
.excalidraw {
|
||||||
--ExcTextField--color: var(--color-gray-80);
|
--ExcTextField--color: var(--color-on-surface);
|
||||||
--ExcTextField--label-color: var(--color-gray-80);
|
--ExcTextField--label-color: var(--color-on-surface);
|
||||||
--ExcTextField--background: white;
|
--ExcTextField--background: transparent;
|
||||||
--ExcTextField--readonly--background: var(--color-gray-10);
|
--ExcTextField--readonly--background: var(--color-surface-high);
|
||||||
--ExcTextField--readonly--color: var(--color-gray-80);
|
--ExcTextField--readonly--color: var(--color-on-surface);
|
||||||
--ExcTextField--border: var(--color-gray-40);
|
--ExcTextField--border: var(--color-border-outline);
|
||||||
--ExcTextField--border-hover: var(--color-gray-50);
|
--ExcTextField--readonly--border: var(--color-border-outline-variant);
|
||||||
--ExcTextField--placeholder: var(--color-gray-40);
|
--ExcTextField--border-hover: var(--color-brand-hover);
|
||||||
|
--ExcTextField--border-active: var(--color-brand-active);
|
||||||
&.theme--dark {
|
--ExcTextField--placeholder: var(--color-border-outline-variant);
|
||||||
--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 {
|
||||||
@@ -61,7 +52,7 @@
|
|||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--ExcTextField--border-active);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +98,7 @@
|
|||||||
|
|
||||||
&--readonly {
|
&--readonly {
|
||||||
background: var(--ExcTextField--readonly--background);
|
background: var(--ExcTextField--readonly--background);
|
||||||
border-color: transparent;
|
border-color: var(--ExcTextField--readonly--border);
|
||||||
|
|
||||||
& input {
|
& input {
|
||||||
color: var(--ExcTextField--readonly--color);
|
color: var(--ExcTextField--readonly--color);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -97,10 +97,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// &:hover {
|
|
||||||
// background-color: var(--button-gray-2);
|
|
||||||
// }
|
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
background-color: var(--button-gray-3);
|
background-color: var(--button-gray-3);
|
||||||
}
|
}
|
||||||
@@ -110,7 +106,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--hide {
|
&--hide {
|
||||||
// visibility: hidden;
|
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,5 +165,10 @@
|
|||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ToolIcon__LaserPointer .ToolIcon__icon {
|
||||||
|
width: var(--default-button-size);
|
||||||
|
height: var(--default-button-size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,19 @@
|
|||||||
.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,6 +193,8 @@ 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 = (
|
||||||
|
|||||||
@@ -114,11 +114,13 @@ const areEqual = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return isShallowEqual(
|
return (
|
||||||
// asserting AppState because we're being passed the whole AppState
|
isShallowEqual(
|
||||||
// but resolve to only the StaticCanvas-relevant props
|
// asserting AppState because we're being passed the whole AppState
|
||||||
getRelevantAppStateProps(prevProps.appState as AppState),
|
// but resolve to only the StaticCanvas-relevant props
|
||||||
getRelevantAppStateProps(nextProps.appState as AppState),
|
getRelevantAppStateProps(prevProps.appState as AppState),
|
||||||
|
getRelevantAppStateProps(nextProps.appState as AppState),
|
||||||
|
) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,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;
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-container {
|
.dropdown-menu-container {
|
||||||
background-color: #fff !important;
|
background-color: var(--island-bg-color);
|
||||||
max-height: calc(100vh - 150px);
|
max-height: calc(100vh - 150px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
--gap: 2;
|
--gap: 2;
|
||||||
@@ -40,7 +40,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-gray-100);
|
color: var(--color-on-surface);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
.dropdown-menu-item {
|
.dropdown-menu-item {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 1px solid transparent;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -59,6 +59,11 @@
|
|||||||
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;
|
||||||
@@ -75,6 +80,11 @@
|
|||||||
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;
|
||||||
@@ -93,22 +103,33 @@
|
|||||||
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,12 +11,14 @@ 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);
|
||||||
@@ -26,7 +28,7 @@ const DropdownMenuItem = ({
|
|||||||
{...rest}
|
{...rest}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
type="button"
|
type="button"
|
||||||
className={getDropdownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
>
|
>
|
||||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ 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}`.trim()}
|
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className} ${
|
||||||
|
selected ? `dropdown-menu-item--selected` : ``
|
||||||
|
}`.trim()}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const DropdownMenuItemLink = ({
|
|||||||
children,
|
children,
|
||||||
onSelect,
|
onSelect,
|
||||||
className = "",
|
className = "",
|
||||||
|
selected,
|
||||||
...rest
|
...rest
|
||||||
}: {
|
}: {
|
||||||
href: string;
|
href: string;
|
||||||
@@ -19,6 +20,7 @@ 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);
|
||||||
@@ -29,7 +31,7 @@ const DropdownMenuItemLink = ({
|
|||||||
href={href}
|
href={href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className={getDropdownMenuItemClassName(className)}
|
className={getDropdownMenuItemClassName(className, selected)}
|
||||||
title={rest.title ?? rest["aria-label"]}
|
title={rest.title ?? rest["aria-label"]}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,8 +6,13 @@ export const DropdownMenuContentPropsContext = React.createContext<{
|
|||||||
onSelect?: (event: Event) => void;
|
onSelect?: (event: Event) => void;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
export const getDropdownMenuItemClassName = (className = "") => {
|
export const getDropdownMenuItemClassName = (
|
||||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
className = "",
|
||||||
|
selected = false,
|
||||||
|
) => {
|
||||||
|
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
||||||
|
selected ? "dropdown-menu-item--selected" : ""
|
||||||
|
}`.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHandleDropdownMenuItemClick = (
|
export const useHandleDropdownMenuItemClick = (
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
export 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";
|
||||||
@@ -1653,3 +1653,22 @@ export const frameToolIcon = createIcon(
|
|||||||
</g>,
|
</g>,
|
||||||
tablerIconProps,
|
tablerIconProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const laserPointerToolIcon = createIcon(
|
||||||
|
<g
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.25"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
transform="rotate(90 10 10)"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
clipRule="evenodd"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<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" />
|
||||||
|
</g>,
|
||||||
|
|
||||||
|
20,
|
||||||
|
);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
--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
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ 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: none;
|
border: 1px solid transparent;
|
||||||
|
|
||||||
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(--color-gray-10);
|
background: var(--button-hover-bg);
|
||||||
|
|
||||||
.welcome-screen-menu-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
@@ -216,7 +216,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-screen-menu-item:active {
|
.welcome-screen-menu-item:active {
|
||||||
background: var(--color-gray-20);
|
background: var(--button-hover-bg);
|
||||||
|
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);
|
||||||
@@ -247,8 +248,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.welcome-screen-menu-item:hover {
|
.welcome-screen-menu-item:hover {
|
||||||
background: var(--color-gray-85);
|
background-color: var(--color-surface-low);
|
||||||
|
|
||||||
.welcome-screen-menu-item__shortcut {
|
.welcome-screen-menu-item__shortcut {
|
||||||
color: var(--color-gray-50);
|
color: var(--color-gray-50);
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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);
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -296,6 +296,18 @@ 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"];
|
||||||
@@ -308,10 +320,10 @@ export const DEFAULT_ELEMENT_PROPS: {
|
|||||||
} = {
|
} = {
|
||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
backgroundColor: COLOR_PALETTE.transparent,
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
fillStyle: "hachure",
|
fillStyle: "solid",
|
||||||
strokeWidth: 1,
|
strokeWidth: 2,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
roughness: 1,
|
roughness: ROUGHNESS.artist,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
locked: false,
|
locked: false,
|
||||||
};
|
};
|
||||||
|
|||||||
+17
-2
@@ -444,13 +444,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&:active {
|
||||||
border: 1px solid var(--color-primary-darkest);
|
border: 1px solid var(--button-active-border);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.help-icon {
|
.help-icon {
|
||||||
@include outlineButtonStyles;
|
@include outlineButtonStyles;
|
||||||
background-color: var(--island-bg-color);
|
@include filledButtonOnCanvas;
|
||||||
|
|
||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: var(--lg-button-size);
|
height: var(--lg-button-size);
|
||||||
|
|
||||||
@@ -621,6 +622,20 @@
|
|||||||
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 {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user