Compare commits

..
Author SHA1 Message Date
zsviczianandGitHub b8ae1d2486 setActiveTool - do not clear selectedElementIds so customData of image can be used & do not render when deferredText is empty 2023-09-26 21:37:13 +02:00
zsviczianandGitHub 482178b7b6 lint 2023-09-24 19:24:04 +02:00
zsviczianandGitHub 7da500fe0a store mermaid text in customData of image element 2023-09-24 19:14:49 +02:00
Aakansha Doshi b1d923db3a use common utils to update and get text editor 2023-09-22 18:59:22 +05:30
Aakansha Doshi 7f6789780d Add tests 2023-09-22 18:55:48 +05:30
Aakansha Doshi 17accffee3 enable test coverage in ui 2023-09-22 18:55:29 +05:30
Aakansha Doshi e1b0b559f5 better api 2023-09-22 11:22:39 +05:30
Aakansha Doshi 8654958aba use i18n 2023-09-21 18:18:44 +05:30
Aakansha Doshi 7c1889efc0 fix width of shapes toolbar for smaller screen size and also fix regression of mobile menu 2023-09-21 17:45:08 +05:30
Aakansha Doshi b3e8d6bbf0 upgrade mermaid-to-excalidraw 2023-09-21 12:47:44 +05:30
Aakansha Doshi 26481fbf43 Merge remote-tracking branch 'origin/master' into aakansha-mermaid 2023-09-21 12:34:47 +05:30
Aakansha Doshi b7a4dfb6ef use stable version of mermaid-to-excalidraw 2023-09-13 21:09:19 +05:30
Aakansha Doshi 8a153b822a Merge remote-tracking branch 'origin/master' into aakansha-mermaid 2023-09-13 21:08:50 +05:30
Aakansha Doshi 57da3768b1 upgrade mermaid-to-excalidraw 2023-09-13 20:12:11 +05:30
Aakansha Doshi db163ab89c use types from mermaid-to-excalidraw 2023-09-07 13:40:06 +05:30
Aakansha Doshi 3593cf6a14 upgrade mermaid-to-excalidraw to fix firefox issue 2023-09-07 12:02:33 +05:30
Aakansha Doshi ce6a9986e3 don't pass appState in deps as its not used 2023-09-06 19:43:02 +05:30
Aakansha Doshi c95369ace8 upgrade mermaid-to-excalidraw 2023-09-06 11:23:07 +05:30
Aakansha Doshi a8283b27c5 width auto 2023-09-05 11:25:46 +05:30
Aakansha Doshi 3ea7c7ffa0 fix mobile css 2023-09-05 11:01:19 +05:30
Aakansha Doshi beb2ceaa72 fix: show extra tools dropdown in mobile 2023-09-05 10:32:51 +05:30
Aakansha Doshi 0d49eb3e05 make design responsive 2023-09-04 13:59:47 +05:30
Aakansha Doshi de0f35454a remove only 2023-09-04 13:22:06 +05:30
Aakansha Doshi d393a1c048 Merge remote-tracking branch 'origin/master' into aakansha-mermaid 2023-09-04 13:15:24 +05:30
Aakansha Doshi 23fa44cf5e fix tests 2023-09-04 13:09:35 +05:30
Aakansha Doshi 9d22e84fc8 tweak text 2023-09-04 10:42:05 +05:30
Aakansha Doshi aa6b585b96 defer rendering the preview 2023-09-04 09:05:22 +05:30
Aakansha Doshi 06389f96b9 fix: Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates. 2023-09-04 08:49:50 +05:30
dwelle 9b1c88eed9 fix elements insert position and viewport centering 2023-09-01 14:49:26 +02:00
Aakansha Doshi d070760b2f fix arrow bindings in safari and firefox 2023-08-31 21:05:44 +05:30
Aakansha Doshi acd6db5304 make opts optional and use 100% zoom when inserting to canvas 2023-08-31 13:34:28 +05:30
Aakansha Doshi d203794f70 add option to transform viewport to scene coords in transform api 2023-08-31 11:08:10 +05:30
Aakansha Doshi c8fd157914 tweak 2023-08-30 20:32:56 +05:30
Aakansha Doshi b47d46699a regenerate ids when needed via programmatic api, this makes sure for mermaid diagrams ids are regenerated 2023-08-30 20:28:13 +05:30
Aakansha Doshi 10a5d18fa2 show a spinner unless mermaid loaded 2023-08-30 14:14:24 +05:30
Aakansha Doshi b1bd94c377 tweak design to remove scroll bar 2023-08-30 13:57:49 +05:30
Aakansha Doshi e4e2e044af fix 2023-08-30 13:29:52 +05:30
Aakansha Doshi d3db48e8f4 add example and docs link 2023-08-30 13:21:32 +05:30
Aakansha Doshi cef0f15158 design tweaks 2023-08-30 13:02:13 +05:30
Aakansha Doshi 742e4bc017 show mermaid error messgae react way 2023-08-30 08:58:27 +05:30
Aakansha Doshi a573fbbb5a show error in preview when error 2023-08-29 22:06:45 +05:30
Aakansha Doshi f696d36e03 reset preview when error 2023-08-29 21:59:46 +05:30
Aakansha Doshi 21a804a4e5 add a note in dialog 2023-08-29 21:36:13 +05:30
Aakansha Doshi c313a2cf42 add mermaid icon in dropdown 2023-08-29 21:20:08 +05:30
Aakansha Doshi 6b33ba1d04 fix undefined vertex issue 2023-08-29 16:56:57 +05:30
Aakansha Doshi fe9f89a0fc compute width, height correctly for arrows 2023-08-29 12:49:49 +05:30
Aakansha Doshi d1266280cb tweak design 2023-08-25 15:44:22 +05:30
Aakansha Doshi 41a0258fe1 load mermaid lazily 2023-08-24 13:43:41 +05:30
Aakansha Doshi 3346c72356 store canvas data in refs 2023-08-24 12:50:03 +05:30
Aakansha Doshi 6518e05cab persist mermaid data to storage 2023-08-24 12:29:33 +05:30
Aakansha Doshi f85f890b25 set elements as selected when inserted onto canvas 2023-08-23 16:24:37 +05:30
Aakansha Doshi 7b612bec5e center preview 2023-08-23 14:43:27 +05:30
Aakansha Doshi efdafc8659 fix markdown error by using named export 2023-08-22 21:40:25 +05:30
Aakansha Doshi 554913c58b fix webpack config 2023-08-21 21:31:53 +05:30
Aakansha Doshi 84059086a6 fix 2023-08-21 21:09:42 +05:30
Aakansha Doshi fec984895e allow mermaid syntax and export in preview 2023-08-21 20:52:01 +05:30
Aakansha Doshi c5514974b7 create mermaid to excal dialog 2023-08-17 14:12:57 +05:30
Aakansha Doshi 6a04ebc6db feat: integrate mermaidToExcalidraw 2023-08-17 12:24:00 +05:30
180 changed files with 3620 additions and 9303 deletions
+3
View File
@@ -0,0 +1,3 @@
## 2020-10-13
- Added ability to embed scene source into exported PNG/SVG files so you can import the scene from them (open via `Load` button or drag & drop). #2219
+1 -1
View File
@@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports:
## Excalidraw.com
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app) is part of this repository as well, and the app features:
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
- 📡 PWA support (works offline).
- 🤼 Real-time collaboration.
@@ -38,7 +38,6 @@ To render an item, its recommended to use `MainMenu.Item`.
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `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 |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
@@ -71,7 +70,6 @@ function App() {
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `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. |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
@@ -1,6 +1,6 @@
# Customizing Styles
Excalidraw uses CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
+2 -2
View File
@@ -2,7 +2,7 @@
### Does this package support collaboration ?
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
### Turning off Aggressive Anti-Fingerprinting in Brave browser
@@ -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**
![Aggressive block fingerprinting](../../assets/aggressive-block-fingerprint.png)
![Aggresive block fingerprinting](../../assets/aggressive-block-fingerprint.png)
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`.
The following workflow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
```jsx showLineNumbers
import { useState, useEffect } from "react";
-32
View File
@@ -1,32 +0,0 @@
# Frames
## Ordering
Frames should be ordered where frame children come first, followed by the frame element itself:
```
[
other_element,
frame1_child1,
frame1_child2,
frame1,
other_element,
frame2_child1,
frame2_child2,
frame2,
other_element,
...
]
```
If not oredered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
# Arrows
An arrow can be a child of a frame only if it has no binding (either start or end) to any other element, regardless of whether the bound element is inside the frame or not.
This ensures that when an arrow is bound to an element outside the frame, it's rendered and behaves correctly.
Therefore, when an arrow (that's a child of a frame) gets bound to an element, it's automatically removed from the frame.
Bound-arrow is duplicated alongside a frame only if the arrow start is bound to an element within that frame.
+1 -1
View File
@@ -15,7 +15,7 @@ In case you want to pick up something from the roadmap, comment on that issue an
1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run:
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
>
> ```bash
> git remote add upstream https://github.com/excalidraw/excalidraw.git
+1 -5
View File
@@ -23,11 +23,7 @@ const sidebars = {
},
items: ["introduction/development", "introduction/contributing"],
},
{
type: "category",
label: "Codebase",
items: ["codebase/json-schema", "codebase/frames"],
},
{ type: "category", label: "Codebase", items: ["codebase/json-schema"] },
{
type: "category",
label: "@excalidraw/excalidraw",
+1 -1
View File
@@ -15,7 +15,7 @@ const FeatureList = [
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
description: (
<>
Want to build your own app powered by Excalidraw but don't know where to
Want to build your own app powered by Excalidraw by don't know where to
start?
</>
),
+11 -116
View File
@@ -145,14 +145,6 @@
dependencies:
"@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":
version "7.18.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
@@ -210,16 +202,6 @@
"@jridgewell/gen-mapping" "^0.3.2"
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":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
@@ -283,11 +265,6 @@
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
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":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
@@ -303,14 +280,6 @@
"@babel/template" "^7.18.6"
"@babel/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":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
@@ -318,13 +287,6 @@
dependencies:
"@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":
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"
@@ -412,28 +374,11 @@
dependencies:
"@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":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
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":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
@@ -467,25 +412,11 @@
chalk "^2.0.0"
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":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
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":
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"
@@ -1216,28 +1147,19 @@
"@babel/parser" "^7.18.6"
"@babel/types" "^7.18.6"
"@babel/template@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
dependencies:
"@babel/code-frame" "^7.22.13"
"@babel/parser" "^7.22.15"
"@babel/types" "^7.22.15"
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
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"
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.18.9"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.18.9"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.18.9"
"@babel/types" "^7.18.9"
debug "^4.1.0"
globals "^11.1.0"
@@ -1249,15 +1171,6 @@
"@babel/helper-validator-identifier" "^7.18.6"
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":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
@@ -1757,11 +1670,6 @@
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
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":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
@@ -1780,19 +1688,6 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
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":
version "0.3.14"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
@@ -80,8 +80,7 @@ export const ExportToExcalidrawPlus: React.FC<{
appState: Partial<AppState>;
files: BinaryFiles;
onError: (error: Error) => void;
onSuccess: () => void;
}> = ({ elements, appState, files, onError, onSuccess }) => {
}> = ({ elements, appState, files, onError }) => {
const { t } = useI18n();
return (
<Card color="primary">
@@ -108,7 +107,6 @@ export const ExportToExcalidrawPlus: React.FC<{
try {
trackEvent("export", "eplus", `ui (${getFrame()})`);
await exportToExcalidrawPlus(elements, appState, files);
onSuccess();
} catch (error: any) {
console.error(error);
if (error.name !== "AbortError") {
+1 -1
View File
@@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
type: "MOUSE_LOCATION";
payload: {
socketId: string;
pointer: { x: number; y: number; tool: "pointer" | "laser" };
pointer: { x: number; y: number };
button: "down" | "up";
selectedElementIds: AppState["selectedElementIds"];
username: string;
+1 -1
View File
@@ -131,5 +131,5 @@ export class Debug {
};
};
}
//@ts-ignore
window.debug = Debug;
+3 -8
View File
@@ -608,7 +608,7 @@ const ExcalidrawWrapper = () => {
canvas: HTMLCanvasElement,
) => {
if (exportedElements.length === 0) {
throw new Error(t("alerts.cannotExportEmptyCanvas"));
return window.alert(t("alerts.cannotExportEmptyCanvas"));
}
if (canvas) {
try {
@@ -624,7 +624,7 @@ const ExcalidrawWrapper = () => {
);
if (errorMessage) {
throw new Error(errorMessage);
setErrorMessage(errorMessage);
}
if (url) {
@@ -634,7 +634,7 @@ const ExcalidrawWrapper = () => {
if (error.name !== "AbortError") {
const { width, height } = canvas;
console.error(error, { width, height });
throw new Error(error.message);
setErrorMessage(error.message);
}
}
}
@@ -714,11 +714,6 @@ const ExcalidrawWrapper = () => {
},
});
}}
onSuccess={() => {
excalidrawAPI?.updateScene({
appState: { openDialog: null },
});
}}
/>
);
},
+2 -2
View File
@@ -20,7 +20,7 @@
},
"dependencies": {
"@braintree/sanitize-url": "6.0.2",
"@excalidraw/laser-pointer": "1.2.0",
"@excalidraw/mermaid-to-excalidraw": "0.1.1",
"@excalidraw/random-username": "1.0.0",
"@radix-ui/react-popover": "1.0.3",
"@radix-ui/react-tabs": "1.0.2",
@@ -125,7 +125,7 @@
"test": "yarn test:app",
"test:coverage": "vitest --coverage",
"test:coverage:watch": "vitest --coverage --watch",
"test:ui": "yarn test --ui",
"test:ui": "yarn test --ui --coverage.enabled=true",
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",
"build:preview": "yarn build && vite preview --port 5000",
+1 -1
View File
@@ -11,7 +11,7 @@
{
"src": "apple-touch-icon.png",
"type": "image/png",
"sizes": "180x180"
"sizes": "256x256"
}
],
"start_url": "/",
+1 -2
View File
@@ -10,7 +10,7 @@ import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey, updateActiveTool } from "../utils";
import { getShortcutKey, setCursor, updateActiveTool } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement";
@@ -21,7 +21,6 @@ import {
} from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { Bounds } from "../element/bounds";
import { setCursor } from "../cursor";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
-1
View File
@@ -46,7 +46,6 @@ const deleteSelectedElements = (
appState: {
...appState,
selectedElementIds: {},
selectedGroupIds: {},
},
};
};
+2 -9
View File
@@ -155,12 +155,7 @@ const duplicateElements = (
groupId,
).flatMap((element) =>
isFrameElement(element)
? [
...getFrameElements(elements, element.id, {
includeBoundArrows: true,
}),
element,
]
? [...getFrameElements(elements, element.id), element]
: [element],
);
@@ -186,9 +181,7 @@ const duplicateElements = (
continue;
}
if (isElementAFrame) {
const elementsInFrame = getFrameElements(sortedElements, element.id, {
includeBoundArrows: true,
});
const elementsInFrame = getFrameElements(sortedElements, element.id);
elementsWithClones.push(
...markAsProcessed([
+1 -9
View File
@@ -191,15 +191,7 @@ export const actionSaveFileToDisk = register({
},
app.files,
);
return {
commitToHistory: false,
appState: {
...appState,
openDialog: null,
fileHandle,
toast: { message: t("toast.fileSaved") },
},
};
return { commitToHistory: false, appState: { ...appState, fileHandle } };
} catch (error: any) {
if (error?.name !== "AbortError") {
console.error(error);
+2 -5
View File
@@ -1,6 +1,6 @@
import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element";
import { updateActiveTool } from "../utils";
import { updateActiveTool, resetCursor } from "../utils";
import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons";
import { t } from "../i18n";
@@ -15,7 +15,6 @@ import {
} from "../element/binding";
import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { AppState } from "../types";
import { resetCursor } from "../cursor";
export const actionFinalize = register({
name: "finalize",
@@ -91,9 +90,7 @@ export const actionFinalize = register({
}
}
if (isInvisiblySmallElement(multiPointElement)) {
newElements = newElements.filter(
(el) => el.id !== multiPointElement.id,
);
newElements = newElements.slice(0, -1);
}
// If the multi point line closes the loop,
+1 -2
View File
@@ -4,8 +4,7 @@ import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame";
import { KEYS } from "../keys";
import { AppClassProperties, AppState } from "../types";
import { updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { setCursorForShape, updateActiveTool } from "../utils";
import { register } from "./register";
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
-1
View File
@@ -15,7 +15,6 @@ export const actionToggleGridMode = register({
appState: {
...appState,
gridSize: this.checked!(appState) ? null : GRID_SIZE,
objectsSnapModeEnabled: false,
},
commitToHistory: false,
};
@@ -1,28 +0,0 @@
import { CODES, KEYS } from "../keys";
import { register } from "./register";
export const actionToggleObjectsSnapMode = register({
name: "objectsSnapMode",
viewMode: true,
trackEvent: {
category: "canvas",
predicate: (appState) => !appState.objectsSnapModeEnabled,
},
perform(elements, appState) {
return {
appState: {
...appState,
objectsSnapModeEnabled: !this.checked!(appState),
gridSize: null,
},
commitToHistory: false,
};
},
checked: (appState) => appState.objectsSnapModeEnabled,
predicate: (elements, appState, appProps) => {
return typeof appProps.objectsSnapModeEnabled === "undefined";
},
contextItemLabel: "buttons.objectsSnapMode",
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S,
});
-1
View File
@@ -80,7 +80,6 @@ export {
export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText";
-2
View File
@@ -28,7 +28,6 @@ export type ShortcutName =
| "ungroup"
| "gridMode"
| "zenMode"
| "objectsSnapMode"
| "stats"
| "addToLibrary"
| "viewMode"
@@ -75,7 +74,6 @@ const shortcutMap: Record<ShortcutName, string[]> = {
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
gridMode: [getShortcutKey("CtrlOrCmd+'")],
zenMode: [getShortcutKey("Alt+Z")],
objectsSnapMode: [getShortcutKey("Alt+S")],
stats: [getShortcutKey("Alt+/")],
addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")],
-1
View File
@@ -51,7 +51,6 @@ export type ActionName =
| "pasteStyles"
| "gridMode"
| "zenMode"
| "objectsSnapMode"
| "stats"
| "changeStrokeColor"
| "changeBackgroundColor"
-9
View File
@@ -99,12 +99,6 @@ export const getDefaultAppState = (): Omit<
pendingImageElementId: null,
showHyperlinkPopup: false,
selectedLinearElement: null,
snapLines: [],
originSnapOffset: {
x: 0,
y: 0,
},
objectsSnapModeEnabled: false,
};
};
@@ -212,9 +206,6 @@ const APP_STATE_STORAGE_CONF = (<
pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, 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 = <
+12 -7
View File
@@ -1,21 +1,26 @@
import { parseClipboard } from "./clipboard";
import { createPasteEvent } from "./tests/test-utils";
describe("Test parseClipboard", () => {
it("should parse valid json correctly", async () => {
let text = "123";
let clipboardData = await parseClipboard(
createPasteEvent({ "text/plain": text }),
);
let clipboardData = await parseClipboard({
//@ts-ignore
clipboardData: {
getData: () => text,
},
});
expect(clipboardData.text).toBe(text);
text = "[123]";
clipboardData = await parseClipboard(
createPasteEvent({ "text/plain": text }),
);
clipboardData = await parseClipboard({
//@ts-ignore
clipboardData: {
getData: () => text,
},
});
expect(clipboardData.text).toBe(text);
});
+9 -70
View File
@@ -18,14 +18,11 @@ type ElementsClipboard = {
files: BinaryFiles | undefined;
};
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
export interface ClipboardData {
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[];
files?: BinaryFiles;
text?: string;
mixedContent?: PastedMixedContent;
errorMessage?: string;
programmaticAPI?: boolean;
}
@@ -145,74 +142,22 @@ const parsePotentialSpreadsheet = (
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
* via async clipboard API if supported)
*/
const getSystemClipboard = async (
export const getSystemClipboard = async (
event: ClipboardEvent | null,
isPlainPaste = false,
): Promise<
| { type: "text"; value: string }
| { type: "mixedContent"; value: PastedMixedContent }
> => {
): Promise<string> => {
try {
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
if (mixedContent) {
return { type: "mixedContent", value: mixedContent };
}
const text = event
? event.clipboardData?.getData("text/plain")
: probablySupportsClipboardReadText &&
(await navigator.clipboard.readText());
return { type: "text", value: (text || "").trim() };
return (text || "").trim();
} catch {
return { type: "text", value: "" };
return "";
}
};
@@ -223,20 +168,14 @@ export const parseClipboard = async (
event: ClipboardEvent | null,
isPlainPaste = false,
): Promise<ClipboardData> => {
const systemClipboard = await getSystemClipboard(event, isPlainPaste);
if (systemClipboard.type === "mixedContent") {
return {
mixedContent: systemClipboard.value,
};
}
const systemClipboard = await getSystemClipboard(event);
// if system clipboard empty, couldn't be resolved, or contains previously
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
// elements
if (
!systemClipboard ||
(!isPlainPaste && systemClipboard.value.includes(SVG_EXPORT_TAG))
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
) {
return getAppClipboard();
}
@@ -244,7 +183,7 @@ export const parseClipboard = async (
// if system clipboard contains spreadsheet, use it even though it's
// technically possible it's staler than in-app clipboard
const spreadsheetResult =
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard.value);
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard);
if (spreadsheetResult) {
return spreadsheetResult;
@@ -253,7 +192,7 @@ export const parseClipboard = async (
const appClipboardData = getAppClipboard();
try {
const systemClipboardData = JSON.parse(systemClipboard.value);
const systemClipboardData = JSON.parse(systemClipboard);
const programmaticAPI =
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
if (clipboardContainsElements(systemClipboardData)) {
@@ -277,7 +216,7 @@ export const parseClipboard = async (
? JSON.stringify(appClipboardData.elements, null, 2)
: undefined,
}
: { text: systemClipboard.value };
: { text: systemClipboard };
};
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
+2 -2
View File
@@ -2,13 +2,13 @@
.undo-redo-buttons {
background-color: var(--island-bg-color);
border-radius: var(--border-radius-lg);
box-shadow: 0 0 0 1px var(--color-surface-lowest);
}
.zoom-button,
.undo-redo-buttons button {
border: 1px solid var(--default-border-color) !important;
border-radius: 0 !important;
background-color: var(--color-surface-low) !important;
background-color: transparent !important;
font-size: 0.875rem !important;
width: var(--lg-button-size);
height: var(--lg-button-size);
+92 -119
View File
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n";
import { useDevice } from "../components/App";
import {
@@ -14,8 +14,13 @@ import {
hasText,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppClassProperties, UIAppState, Zoom } from "../types";
import { capitalizeString, isTransparent } from "../utils";
import { UIAppState, Zoom } from "../types";
import {
capitalizeString,
isTransparent,
updateActiveTool,
setCursorForShape,
} from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
@@ -35,7 +40,7 @@ import {
EmbedIcon,
extraToolsIcon,
frameToolIcon,
laserPointerToolIcon,
mermaidLogoIcon,
} from "./icons";
import { KEYS } from "../keys";
@@ -213,21 +218,19 @@ export const SelectedShapeActions = ({
};
export const ShapesSwitcher = ({
interactiveCanvas,
activeTool,
setAppState,
onImageAction,
appState,
app,
}: {
interactiveCanvas: HTMLCanvasElement | null;
activeTool: UIAppState["activeTool"];
setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: UIAppState;
app: AppClassProperties;
}) => {
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
const device = useDevice();
const frameToolSelected = activeTool.type === "frame";
const laserToolSelected = activeTool.type === "laser";
const embeddableToolSelected = activeTool.type === "embeddable";
return (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
@@ -252,131 +255,101 @@ export const ShapesSwitcher = ({
data-testid={`toolbar-${value}`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true);
setAppState({
penDetected: true,
penMode: true,
});
}
}}
onChange={({ pointerType }) => {
if (appState.activeTool.type !== value) {
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") {
app.setActiveTool({
type: value,
insertOnCanvasDirectly: pointerType !== "mouse",
});
} else {
app.setActiveTool({ type: value });
onImageAction({ pointerType });
}
}}
/>
);
})}
<div className="App-toolbar__divider" />
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
{device.isMobile ? (
<>
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
<DropdownMenu open={isExtraToolsMenuOpen}>
<DropdownMenu.Trigger
className="App-toolbar__extra-tools-trigger"
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
title={t("toolBar.extraTools")}
>
{extraToolsIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
onSelect={() => setIsExtraToolsMenuOpen(false)}
className="App-toolbar__extra-tools-dropdown"
>
<DropdownMenu.Item
onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "frame",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
icon={frameToolIcon}
checked={activeTool.type === "frame"}
name="editor-current-shape"
title={`${capitalizeString(
t("toolBar.frame"),
)} — ${KEYS.F.toLocaleUpperCase()}`}
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
aria-label={capitalizeString(t("toolBar.frame"))}
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
data-testid={`toolbar-frame`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true);
}
shortcut={KEYS.F.toLocaleUpperCase()}
data-testid="toolbar-frame"
>
{t("toolBar.frame")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "embeddable",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
selectedElementIds: {},
});
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "frame", "ui");
app.setActiveTool({ type: "frame" });
}}
selected={activeTool.type === "frame"}
/>
<ToolButton
className={clsx("Shape", { fillable: false })}
type="radio"
icon={EmbedIcon}
checked={activeTool.type === "embeddable"}
name="editor-current-shape"
title={capitalizeString(t("toolBar.embeddable"))}
aria-label={capitalizeString(t("toolBar.embeddable"))}
data-testid={`toolbar-embeddable`}
onPointerDown={({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true);
}
}}
onChange={({ pointerType }) => {
trackEvent("toolbar", "embeddable", "ui");
app.setActiveTool({ type: "embeddable" });
}}
selected={activeTool.type === "embeddable"}
/>
</>
) : (
<DropdownMenu open={isExtraToolsMenuOpen}>
<DropdownMenu.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)}
title={t("toolBar.extraTools")}
data-testid="toolbar-embeddable"
>
{extraToolsIcon}
</DropdownMenu.Trigger>
<DropdownMenu.Content
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
onSelect={() => setIsExtraToolsMenuOpen(false)}
className="App-toolbar__extra-tools-dropdown"
{t("toolBar.embeddable")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => {
const nextActiveTool = updateActiveTool(appState, {
type: "mermaid",
});
setAppState({
activeTool: nextActiveTool,
multiElement: null,
});
}}
icon={mermaidLogoIcon}
data-testid="toolbar-embeddable"
>
<DropdownMenu.Item
onSelect={() => {
app.setActiveTool({ type: "frame" });
}}
icon={frameToolIcon}
shortcut={KEYS.F.toLocaleUpperCase()}
data-testid="toolbar-frame"
selected={frameToolSelected}
>
{t("toolBar.frame")}
</DropdownMenu.Item>
<DropdownMenu.Item
onSelect={() => {
app.setActiveTool({ type: "embeddable" });
}}
icon={EmbedIcon}
data-testid="toolbar-embeddable"
selected={embeddableToolSelected}
>
{t("toolBar.embeddable")}
</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>
)}
{t("toolBar.mermaidToExcalidraw")}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu>
</>
);
};
+160 -477
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -12,32 +12,32 @@
&--color-primary {
&.ExcButton--variant-filled {
--text-color: var(--color-surface-lowest);
--text-color: var(--input-bg-color);
--back-color: var(--color-primary);
&:hover {
--back-color: var(--color-brand-hover);
--back-color: var(--color-primary-darker);
}
&:active {
--back-color: var(--color-brand-active);
--back-color: var(--color-primary-darkest);
}
}
&.ExcButton--variant-outlined,
&.ExcButton--variant-icon {
--text-color: var(--color-primary);
--border-color: var(--color-border-outline);
--back-color: transparent;
--border-color: var(--color-primary);
--back-color: var(--input-bg-color);
&:hover {
--text-color: var(--color-brand-hover);
--border-color: var(--color-brand-hover);
--text-color: var(--color-primary-darker);
--border-color: var(--color-primary-darker);
}
&:active {
--text-color: var(--color-brand-active);
--border-color: var(--color-brand-active);
--text-color: var(--color-primary-darkest);
--border-color: var(--color-primary-darkest);
}
}
}
+1 -16
View File
@@ -19,35 +19,20 @@
}
&__btn {
--background: var(--color-surface-mid);
display: flex;
column-gap: 0.5rem;
align-items: center;
background-color: var(--background);
border: 1px solid var(--default-border-color);
padding: 0.625rem 1rem;
border: 1px solid var(--background);
border-radius: var(--border-radius-lg);
color: var(--text-primary-color);
font-weight: 600;
font-size: 0.75rem;
letter-spacing: 0.4px;
@at-root .excalidraw.theme--dark#{&} {
--background: var(--color-surface-high);
&:hover {
--background: #363541;
}
}
&:hover {
--background: var(--color-surface-high);
text-decoration: none;
}
&:active {
border-color: var(--color-primary);
}
}
&__link-icon {
-5
View File
@@ -165,7 +165,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={[KEYS.E, KEYS["0"]]}
/>
<Shortcut label={t("toolBar.frame")} shortcuts={[KEYS.F]} />
<Shortcut label={t("toolBar.laser")} shortcuts={[KEYS.K]} />
<Shortcut
label={t("labels.eyeDropper")}
shortcuts={[KEYS.I, "Shift+S", "Shift+G"]}
@@ -259,10 +258,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("buttons.zenMode")}
shortcuts={[getShortcutKey("Alt+Z")]}
/>
<Shortcut
label={t("buttons.objectsSnapMode")}
shortcuts={[getShortcutKey("Alt+S")]}
/>
<Shortcut
label={t("labels.showGrid")}
shortcuts={[getShortcutKey("CtrlOrCmd+'")]}
+3 -12
View File
@@ -23,15 +23,12 @@ export type ExportCB = (
const JSONExportModal = ({
elements,
appState,
setAppState,
files,
actionManager,
exportOpts,
canvas,
onCloseRequest,
}: {
appState: UIAppState;
setAppState: React.Component<any, UIAppState>["setState"];
files: BinaryFiles;
elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionManager;
@@ -75,14 +72,9 @@ const JSONExportModal = ({
title={t("exportDialog.link_button")}
aria-label={t("exportDialog.link_button")}
showAriaLabel={true}
onClick={async () => {
try {
trackEvent("export", "link", `ui (${getFrame()})`);
await onExportToBackend(elements, appState, files, canvas);
onCloseRequest();
} catch (error: any) {
setAppState({ errorMessage: error.message });
}
onClick={() => {
onExportToBackend(elements, appState, files, canvas);
trackEvent("export", "link", `ui (${getFrame()})`);
}}
/>
</Card>
@@ -122,7 +114,6 @@ export const JSONExportDialog = ({
<JSONExportModal
elements={elements}
appState={appState}
setAppState={setAppState}
files={files}
actionManager={actionManager}
onCloseRequest={handleClose}
@@ -1,309 +0,0 @@
import { LaserPointer } from "@excalidraw/laser-pointer";
import { sceneCoordsToViewportCoords } from "../../utils";
import App from "../App";
import { getClientColor } from "../../clients";
// decay time in milliseconds
const DECAY_TIME = 1000;
// length of line in points before it starts decaying
const DECAY_LENGTH = 50;
const average = (a: number, b: number) => (a + b) / 2;
function getSvgPathFromStroke(points: number[][], closed = true) {
const len = points.length;
if (len < 4) {
return ``;
}
let a = points[0];
let b = points[1];
const c = points[2];
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
2,
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
b[1],
c[1],
).toFixed(2)} T`;
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
2,
)} `;
}
if (closed) {
result += "Z";
}
return result;
}
declare global {
interface Window {
LPM: LaserPathManager;
}
}
function easeOutCubic(t: number) {
return 1 - Math.pow(1 - t, 3);
}
function instantiateCollabolatorState(): CollabolatorState {
return {
currentPath: undefined,
finishedPaths: [],
lastPoint: [-10000, -10000],
svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
};
}
function instantiatePath() {
LaserPointer.constants.cornerDetectionMaxAngle = 70;
return new LaserPointer({
simplify: 0,
streamline: 0.4,
sizeMapping: (c) => {
const pt = DECAY_TIME;
const pl = DECAY_LENGTH;
const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
return Math.min(easeOutCubic(l), easeOutCubic(t));
},
});
}
type CollabolatorState = {
currentPath: LaserPointer | undefined;
finishedPaths: LaserPointer[];
lastPoint: [number, number];
svg: SVGPathElement;
};
export class LaserPathManager {
private ownState: CollabolatorState;
private collaboratorsState: Map<string, CollabolatorState> = new Map();
private rafId: number | undefined;
private isDrawing = false;
private container: SVGSVGElement | undefined;
constructor(private app: App) {
this.ownState = instantiateCollabolatorState();
}
destroy() {
this.stop();
this.isDrawing = false;
this.ownState = instantiateCollabolatorState();
this.collaboratorsState = new Map();
}
startPath(x: number, y: number) {
this.ownState.currentPath = instantiatePath();
this.ownState.currentPath.addPoint([x, y, performance.now()]);
this.updatePath(this.ownState);
}
addPointToPath(x: number, y: number) {
if (this.ownState.currentPath) {
this.ownState.currentPath?.addPoint([x, y, performance.now()]);
this.updatePath(this.ownState);
}
}
endPath() {
if (this.ownState.currentPath) {
this.ownState.currentPath.close();
this.ownState.finishedPaths.push(this.ownState.currentPath);
this.updatePath(this.ownState);
}
}
private updatePath(state: CollabolatorState) {
this.isDrawing = true;
if (!this.isRunning) {
this.start();
}
}
private isRunning = false;
start(svg?: SVGSVGElement) {
if (svg) {
this.container = svg;
this.container.appendChild(this.ownState.svg);
}
this.stop();
this.isRunning = true;
this.loop();
}
stop() {
this.isRunning = false;
if (this.rafId) {
cancelAnimationFrame(this.rafId);
}
this.rafId = undefined;
}
loop() {
this.rafId = requestAnimationFrame(this.loop.bind(this));
this.updateCollabolatorsState();
if (this.isDrawing) {
this.update();
} else {
this.isRunning = false;
}
}
draw(path: LaserPointer) {
const stroke = path
.getStrokeOutline(path.options.size / this.app.state.zoom.value)
.map(([x, y]) => {
const result = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y },
this.app.state,
);
return [result.x, result.y];
});
return getSvgPathFromStroke(stroke, true);
}
updateCollabolatorsState() {
if (!this.container || !this.app.state.collaborators.size) {
return;
}
for (const [key, collabolator] of this.app.state.collaborators.entries()) {
if (!this.collaboratorsState.has(key)) {
const state = instantiateCollabolatorState();
this.container.appendChild(state.svg);
this.collaboratorsState.set(key, state);
this.updatePath(state);
}
const state = this.collaboratorsState.get(key)!;
if (collabolator.pointer && collabolator.pointer.tool === "laser") {
if (collabolator.button === "down" && state.currentPath === undefined) {
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
state.currentPath = instantiatePath();
state.currentPath.addPoint([
collabolator.pointer.x,
collabolator.pointer.y,
performance.now(),
]);
this.updatePath(state);
}
if (collabolator.button === "down" && state.currentPath !== undefined) {
if (
collabolator.pointer.x !== state.lastPoint[0] ||
collabolator.pointer.y !== state.lastPoint[1]
) {
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
state.currentPath.addPoint([
collabolator.pointer.x,
collabolator.pointer.y,
performance.now(),
]);
this.updatePath(state);
}
}
if (collabolator.button === "up" && state.currentPath !== undefined) {
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
state.currentPath.addPoint([
collabolator.pointer.x,
collabolator.pointer.y,
performance.now(),
]);
state.currentPath.close();
state.finishedPaths.push(state.currentPath);
state.currentPath = undefined;
this.updatePath(state);
}
}
}
}
update() {
if (!this.container) {
return;
}
let somePathsExist = false;
for (const [key, state] of this.collaboratorsState.entries()) {
if (!this.app.state.collaborators.has(key)) {
state.svg.remove();
this.collaboratorsState.delete(key);
continue;
}
state.finishedPaths = state.finishedPaths.filter((path) => {
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
});
let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
if (state.currentPath) {
paths += ` ${this.draw(state.currentPath)}`;
}
if (paths.trim()) {
somePathsExist = true;
}
state.svg.setAttribute("d", paths);
state.svg.setAttribute("fill", getClientColor(key));
}
this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
});
let paths = this.ownState.finishedPaths
.map((path) => this.draw(path))
.join(" ");
if (this.ownState.currentPath) {
paths += ` ${this.draw(this.ownState.currentPath)}`;
}
paths = paths.trim();
if (paths) {
somePathsExist = true;
}
this.ownState.svg.setAttribute("d", paths);
this.ownState.svg.setAttribute("fill", "red");
if (!somePathsExist) {
this.isDrawing = false;
}
}
}
@@ -1,41 +0,0 @@
import "../ToolIcon.scss";
import clsx from "clsx";
import { ToolButtonSize } from "../ToolButton";
import { laserPointerToolIcon } from "../icons";
type LaserPointerIconProps = {
title?: string;
name?: string;
checked: boolean;
onChange?(): void;
isMobile?: boolean;
};
const DEFAULT_SIZE: ToolButtonSize = "small";
export const LaserPointerButton = (props: LaserPointerIconProps) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon__LaserPointer",
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"is-mobile": props.isMobile,
},
)}
title={`${props.title}`}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name={props.name}
onChange={props.onChange}
checked={props.checked}
aria-label={props.title}
data-testid="toolbar-LaserPointer"
/>
<div className="ToolIcon__icon">{laserPointerToolIcon}</div>
</label>
);
};
-27
View File
@@ -1,27 +0,0 @@
import { useEffect, useRef } from "react";
import { LaserPathManager } from "./LaserPathManager";
import "./LaserToolOverlay.scss";
type LaserToolOverlayProps = {
manager: LaserPathManager;
};
export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => {
const svgRef = useRef<SVGSVGElement | null>(null);
useEffect(() => {
if (svgRef.current) {
manager.start(svgRef.current);
}
return () => {
manager.stop();
};
}, [manager]);
return (
<div className="LaserToolOverlay">
<svg ref={svgRef} className="LaserToolOverlayCanvas" />
</div>
);
};
@@ -1,20 +0,0 @@
.excalidraw {
.LaserToolOverlay {
pointer-events: none;
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 2;
.LaserToolOverlayCanvas {
image-rendering: auto;
overflow: visible;
position: absolute;
top: 0;
left: 0;
}
}
}
+25 -24
View File
@@ -55,13 +55,13 @@ import "./Toolbar.scss";
import { mutateElement } from "../element/mutateElement";
import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene";
import { LaserPointerButton } from "./LaserTool/LaserPointerButton";
interface LayerUIProps {
actionManager: ActionManager;
appState: UIAppState;
files: BinaryFiles;
canvas: HTMLCanvasElement;
interactiveCanvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onLockToggle: () => void;
@@ -72,11 +72,11 @@ interface LayerUIProps {
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
UIOptions: AppProps["UIOptions"];
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean;
children?: React.ReactNode;
app: AppClassProperties;
isCollaborating: boolean;
}
const DefaultMainMenu: React.FC<{
@@ -121,6 +121,7 @@ const LayerUI = ({
setAppState,
elements,
canvas,
interactiveCanvas,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
@@ -128,11 +129,11 @@ const LayerUI = ({
renderTopRightUI,
renderCustomStats,
UIOptions,
onImageAction,
onExportImage,
renderWelcomeScreen,
children,
app,
isCollaborating,
}: LayerUIProps) => {
const device = useDevice();
const tunnels = useInitializeTunnels();
@@ -276,29 +277,17 @@ const LayerUI = ({
<ShapesSwitcher
appState={appState}
interactiveCanvas={interactiveCanvas}
activeTool={appState.activeTool}
app={app}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/>
</Stack.Row>
</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.Col>
</div>
@@ -462,6 +451,8 @@ const LayerUI = ({
onLockToggle={onLockToggle}
onHandToolToggle={onHandToolToggle}
onPenModeToggle={onPenModeToggle}
interactiveCanvas={interactiveCanvas}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars}
@@ -548,8 +539,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
return false;
}
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps;
const { canvas: _nC, appState: nextAppState, ...next } = nextProps;
const {
canvas: _pC,
interactiveCanvas: _pIC,
appState: prevAppState,
...prev
} = prevProps;
const {
canvas: _nC,
interactiveCanvas: _nIC,
appState: nextAppState,
...next
} = nextProps;
return (
isShallowEqual(
+2 -2
View File
@@ -99,10 +99,10 @@
font-size: 0.75rem;
&:hover {
background-color: var(--color-brand-hover);
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--color-brand-active);
background-color: var(--color-primary-darkest);
}
}
+82
View File
@@ -0,0 +1,82 @@
@import "../css/variables.module";
.excalidraw {
.mermaid-to-excalidraw-wrapper {
display: flex;
width: 100%;
justify-content: space-between;
label {
font-size: 14px;
font-style: normal;
font-weight: 600;
margin-bottom: 4px;
margin-left: 4px;
@media screen and (max-width: 860px) {
margin-top: 4px;
}
}
&-text {
display: flex;
flex-direction: column;
textarea {
width: 20rem;
height: 19rem;
resize: none;
border-radius: 8px;
padding: 1rem;
@media screen and (max-width: 860px) {
width: auto;
height: 10rem;
}
}
}
&-preview-canvas {
display: flex;
align-items: center;
justify-content: center;
padding: 0.85rem;
width: 20rem;
height: 19rem;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
left center;
border-radius: 8px;
border: 1px solid #e4e4eb;
@media screen and (max-width: 860px) {
width: auto;
}
}
&-preview-insert.excalidraw-button {
font-family: "Assistant";
font-weight: 600;
height: 2.5rem;
margin-top: 1em;
margin-bottom: 0.3em;
width: 7.5rem;
font-size: 12px;
background-color: var(--color-primary);
&:hover {
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--color-primary-darkest);
}
color: $oc-white;
@media screen and (max-width: 860px) {
width: 100%;
}
}
@media screen and (max-width: 860px) {
flex-direction: column;
}
}
}
+273
View File
@@ -0,0 +1,273 @@
import { useState, useRef, useEffect, useDeferredValue } from "react";
import { BinaryFiles } from "../types";
import { useApp } from "./App";
import { Button } from "./Button";
import { Dialog } from "./Dialog";
import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants";
import {
convertToExcalidrawElements,
exportToCanvas,
} from "../packages/excalidraw/index";
import { NonDeletedExcalidrawElement } from "../element/types";
import { canvasToBlob } from "../data/blob";
import { ArrowRightIcon } from "./icons";
import Spinner from "./Spinner";
import "./MermaidToExcalidraw.scss";
import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
import { t } from "../i18n";
import Trans from "./Trans";
const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
const MERMAID_EXAMPLE =
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[test]";
const saveMermaidDataToStorage = (data: string) => {
try {
localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data);
} catch (error: any) {
// Unable to access window.localStorage
console.error(error);
}
};
const importMermaidDataFromStorage = () => {
try {
const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW);
if (data) {
return data;
}
} catch (error: any) {
// Unable to access localStorage
console.error(error);
}
return null;
};
const ErrorComp = ({ error }: { error: string }) => {
return (
<div
data-testid="mermaid-error"
style={{
color: "red",
fontWeight: 800,
fontSize: "30px",
wordBreak: "break-word",
overflow: "auto",
maxHeight: "100%",
textAlign: "center",
}}
>
Error! <p style={{ fontSize: "18px", fontWeight: "600" }}>{error}</p>
</div>
);
};
const MermaidToExcalidraw = ({
selectedElements,
}: {
selectedElements: readonly NonDeletedExcalidrawElement[];
}) => {
const mermaidToExcalidrawLib = useRef<{
parseMermaidToExcalidraw: (
defination: string,
options: MermaidOptions,
) => Promise<MermaidToExcalidrawResult>;
} | null>(null);
const [text, setText] = useState("");
const deferredText = useDeferredValue(text);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const canvasRef = useRef<HTMLDivElement>(null);
const data = useRef<{
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles | null;
}>({ elements: [], files: null });
const app = useApp();
const resetPreview = () => {
const canvasNode = canvasRef.current;
if (!canvasNode) {
return;
}
const parent = canvasNode.parentElement;
if (!parent) {
return;
}
parent.style.background = "";
canvasNode.replaceChildren();
};
useEffect(() => {
const loadMermaidToExcalidrawLib = async () => {
mermaidToExcalidrawLib.current = await import(
/* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
);
setLoading(false);
};
loadMermaidToExcalidrawLib();
}, []);
useEffect(() => {
if (!loading) {
const selectedMermaidImage = selectedElements.filter(
(el) => el.type === "image" && el.customData?.mermaidText,
)[0];
const data = selectedMermaidImage
? selectedMermaidImage.customData?.mermaidText
: importMermaidDataFromStorage() || MERMAID_EXAMPLE;
setText(data);
}
}, [loading, selectedElements]);
useEffect(() => {
const renderExcalidrawPreview = async () => {
const canvasNode = canvasRef.current;
if (
!canvasNode ||
!mermaidToExcalidrawLib.current ||
deferredText === ""
) {
return;
}
try {
const { elements, files } =
await mermaidToExcalidrawLib.current.parseMermaidToExcalidraw(
deferredText,
{
fontSize: DEFAULT_FONT_SIZE,
},
);
setError(null);
data.current = {
elements: convertToExcalidrawElements(
elements.map((el) => {
if (el.type === "image") {
el.customData = { mermaidText: text };
}
return el;
}),
{
regenerateIds: true,
},
),
files,
};
const parent = canvasNode.parentElement!;
const maxWidth = parent.offsetWidth;
const maxHeight = parent.offsetHeight;
let dimension = Math.max(maxWidth, maxHeight);
dimension = Math.min(dimension, parent.offsetWidth - 10);
dimension = Math.min(dimension, parent.offsetHeight - 10);
const canvas = await exportToCanvas({
elements: data.current.elements,
files: data.current.files,
exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight: dimension,
});
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
await canvasToBlob(canvas);
parent.style.background = "#fff";
canvasNode.replaceChildren(canvas);
} catch (e: any) {
console.error(e.message);
resetPreview();
if (deferredText) {
setError(e.message);
}
}
};
renderExcalidrawPreview();
}, [deferredText, text]);
const onClose = () => {
app.setActiveTool({ type: "selection" });
saveMermaidDataToStorage(text);
};
const onSelect = () => {
const { elements: newElements, files } = data.current;
app.addElementsFromPasteOrLibrary({
elements: newElements,
files,
position: "center",
fitToContent: true,
});
onClose();
};
return (
<Dialog
className="dialog-mermaid"
onCloseRequest={onClose}
title={
<>
<p style={{ marginBottom: "5px", marginTop: "2px" }}>
{t("mermaid.title")}
</p>
<span
style={{ fontSize: "15px", fontStyle: "italic", fontWeight: 500 }}
>
<Trans
i18nKey="mermaid.description"
flowchartLink={(el) => (
<a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
)}
/>
<br />
</span>
</>
}
>
<div className="mermaid-to-excalidraw-wrapper">
<div
className="mermaid-to-excalidraw-wrapper-text"
style={{ display: "flex", flexDirection: "column" }}
>
<label>{t("mermaid.syntax")}</label>
<textarea
style={{
padding: "0.85rem",
borderRadius: "8px",
border: "1px solid #e4e4eb",
whiteSpace: "pre-wrap",
}}
onChange={(event) => setText(event.target.value)}
value={text}
/>
</div>
<div
className="mermaid-to-excalidraw-wrapper-preview"
style={{ display: "flex", flexDirection: "column" }}
>
<label>{t("mermaid.preview")}</label>
<div className="mermaid-to-excalidraw-wrapper-preview-canvas">
{error && <ErrorComp error={error} />}
{loading && <Spinner size="2rem" />}
<div ref={canvasRef} />
</div>
<Button
className="mermaid-to-excalidraw-wrapper-preview-insert"
onSelect={onSelect}
>
{t("mermaid.button")}
<span style={{ paddingLeft: "8px", display: "flex" }}>
{ArrowRightIcon}
</span>
</Button>
</div>
</div>
</Dialog>
);
};
export default MermaidToExcalidraw;
+11 -2
View File
@@ -36,7 +36,9 @@ type MobileMenuProps = {
onLockToggle: () => void;
onHandToolToggle: () => void;
onPenModeToggle: () => void;
interactiveCanvas: HTMLCanvasElement | null;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
isMobile: boolean,
appState: UIAppState,
@@ -56,7 +58,8 @@ export const MobileMenu = ({
onLockToggle,
onHandToolToggle,
onPenModeToggle,
interactiveCanvas,
onImageAction,
renderTopRightUI,
renderCustomStats,
renderSidebars,
@@ -82,8 +85,14 @@ export const MobileMenu = ({
<Stack.Row gap={1}>
<ShapesSwitcher
appState={appState}
interactiveCanvas={interactiveCanvas}
activeTool={appState.activeTool}
app={app}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/>
</Stack.Row>
</Island>
+17 -8
View File
@@ -1,18 +1,27 @@
@import "../css/variables.module";
.excalidraw {
--RadioGroup-background: var(--island-bg-color);
--RadioGroup-border: var(--color-surface-high);
--RadioGroup-background: #ffffff;
--RadioGroup-border: var(--color-gray-30);
--RadioGroup-choice-color-off: var(--color-primary);
--RadioGroup-choice-color-off-hover: var(--color-brand-hover);
--RadioGroup-choice-background-off: var(--island-bg-color);
--RadioGroup-choice-background-off-active: var(--color-surface-high);
--RadioGroup-choice-color-off-hover: var(--color-primary-darkest);
--RadioGroup-choice-background-off: white;
--RadioGroup-choice-background-off-active: var(--color-gray-20);
--RadioGroup-choice-color-on: var(--color-surface-lowest);
--RadioGroup-choice-color-on: white;
--RadioGroup-choice-background-on: var(--color-primary);
--RadioGroup-choice-background-on-hover: var(--color-brand-hover);
--RadioGroup-choice-background-on-active: var(--color-brand-active);
--RadioGroup-choice-background-on-hover: var(--color-primary-darker);
--RadioGroup-choice-background-on-active: var(--color-primary-darkest);
&.theme--dark {
--RadioGroup-background: var(--color-gray-85);
--RadioGroup-border: var(--color-gray-70);
--RadioGroup-choice-background-off: var(--color-gray-85);
--RadioGroup-choice-background-off-active: var(--color-gray-70);
--RadioGroup-choice-color-on: var(--color-gray-85);
}
.RadioGroup {
box-sizing: border-box;
+2 -1
View File
@@ -3,7 +3,8 @@
.excalidraw {
.sidebar-trigger {
@include outlineButtonStyles;
@include filledButtonOnCanvas;
background-color: var(--island-bg-color);
width: auto;
height: var(--lg-button-size);
+14 -16
View File
@@ -1,13 +1,15 @@
@import "../css/variables.module";
.excalidraw {
--Switch-disabled-color: var(--color-border-outline);
--Switch-disabled-toggled-background: var(--color-border-outline-variant);
--Switch-disabled-border: var(--color-border-outline-variant);
--Switch-track-background: var(--island-bg-color);
--Switch-thumb-background: var(--color-on-surface);
--Switch-hover-background: var(--color-brand-hover);
--Switch-active-background: var(--color-brand-active);
--Switch-disabled-color: #d6d6d6;
--Switch-track-background: white;
--Switch-thumb-background: #3d3d3d;
&.theme--dark {
--Switch-disabled-color: #5c5c5c;
--Switch-track-background: #242424;
--Switch-thumb-background: #b8b8b8;
}
.Switch {
position: relative;
@@ -26,11 +28,7 @@
&:hover {
background: var(--Switch-track-background);
border: 1px solid var(--Switch-hover-background);
}
&:active {
border: 1px solid var(--Switch-active-background);
border: 1px solid #999999;
}
&.toggled {
@@ -45,11 +43,11 @@
&.disabled {
background: var(--Switch-track-background);
border: 1px solid var(--Switch-disabled-border);
border: 1px solid var(--Switch-disabled-color);
&.toggled {
background: var(--Switch-disabled-toggled-background);
border: 1px solid var(--Switch-disabled-toggled-background);
background: var(--Switch-disabled-color);
border: 1px solid var(--Switch-disabled-color);
}
}
@@ -94,7 +92,7 @@
}
&.disabled.toggled:before {
background: var(--Switch-disabled-color);
background: var(--color-gray-50);
}
& input {
+21 -12
View File
@@ -1,16 +1,25 @@
@import "../css/variables.module";
.excalidraw {
--ExcTextField--color: var(--color-on-surface);
--ExcTextField--label-color: var(--color-on-surface);
--ExcTextField--background: transparent;
--ExcTextField--readonly--background: var(--color-surface-high);
--ExcTextField--readonly--color: var(--color-on-surface);
--ExcTextField--border: var(--color-border-outline);
--ExcTextField--readonly--border: var(--color-border-outline-variant);
--ExcTextField--border-hover: var(--color-brand-hover);
--ExcTextField--border-active: var(--color-brand-active);
--ExcTextField--placeholder: var(--color-border-outline-variant);
--ExcTextField--color: var(--color-gray-80);
--ExcTextField--label-color: var(--color-gray-80);
--ExcTextField--background: white;
--ExcTextField--readonly--background: var(--color-gray-10);
--ExcTextField--readonly--color: var(--color-gray-80);
--ExcTextField--border: var(--color-gray-40);
--ExcTextField--border-hover: var(--color-gray-50);
--ExcTextField--placeholder: var(--color-gray-40);
&.theme--dark {
--ExcTextField--color: var(--color-gray-10);
--ExcTextField--label-color: var(--color-gray-20);
--ExcTextField--background: var(--color-gray-85);
--ExcTextField--readonly--background: var(--color-gray-80);
--ExcTextField--readonly--color: var(--color-gray-40);
--ExcTextField--border: var(--color-gray-70);
--ExcTextField--border-hover: var(--color-gray-60);
--ExcTextField--placeholder: var(--color-gray-80);
}
.ExcTextField {
&--fullWidth {
@@ -52,7 +61,7 @@
&:active,
&:focus-within {
border-color: var(--ExcTextField--border-active);
border-color: var(--color-primary);
}
}
@@ -98,7 +107,7 @@
&--readonly {
background: var(--ExcTextField--readonly--background);
border-color: var(--ExcTextField--readonly--border);
border-color: transparent;
& input {
color: var(--ExcTextField--readonly--color);
+5 -5
View File
@@ -83,12 +83,12 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
}
};
useEffect(() => {
isMountedRef.current = true;
return () => {
useEffect(
() => () => {
isMountedRef.current = false;
};
}, []);
},
[],
);
const lastPointerTypeRef = useRef<PointerType | null>(null);
+14 -5
View File
@@ -97,6 +97,10 @@
}
}
// &:hover {
// background-color: var(--button-gray-2);
// }
&:active {
background-color: var(--button-gray-3);
}
@@ -106,6 +110,7 @@
}
&--hide {
// visibility: hidden;
display: none !important;
}
}
@@ -160,15 +165,19 @@
width: var(--lg-button-size);
height: var(--lg-button-size);
@media screen and (max-width: 450px) {
width: 1.8rem;
height: 1.8rem;
}
@media screen and (max-width: 379px) {
width: 1.5rem;
height: 1.5rem;
}
svg {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
}
}
.ToolIcon__LaserPointer .ToolIcon__icon {
width: var(--default-button-size);
height: var(--default-button-size);
}
}
}
+5 -7
View File
@@ -16,30 +16,28 @@
align-self: center;
background-color: var(--default-border-color);
margin: 0 0.25rem;
@include isMobile {
margin: 0;
}
}
}
.App-toolbar__extra-tools-trigger {
box-shadow: none;
border: 0;
background-color: transparent;
&:active {
background-color: var(--button-hover-bg);
box-shadow: 0 0 0 1px
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 {
margin-top: 0.375rem;
right: 0;
min-width: 11.875rem;
z-index: 1;
}
}
@@ -193,8 +193,6 @@ const getRelevantAppStateProps = (
showHyperlinkPopup: appState.showHyperlinkPopup,
collaborators: appState.collaborators, // Necessary for collab. sessions
activeEmbeddable: appState.activeEmbeddable,
snapLines: appState.snapLines,
zenModeEnabled: appState.zenModeEnabled,
});
const areEqual = (
+5 -7
View File
@@ -114,13 +114,11 @@ const areEqual = (
return false;
}
return (
isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the StaticCanvas-relevant props
getRelevantAppStateProps(prevProps.appState as AppState),
getRelevantAppStateProps(nextProps.appState as AppState),
) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig)
return isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the StaticCanvas-relevant props
getRelevantAppStateProps(prevProps.appState as AppState),
getRelevantAppStateProps(nextProps.appState as AppState),
);
};
+14 -37
View File
@@ -7,8 +7,6 @@
margin-top: 0.25rem;
&--mobile {
bottom: 55px;
top: auto;
left: 0;
width: 100%;
row-gap: 0.75rem;
@@ -16,7 +14,7 @@
.dropdown-menu-container {
padding: 8px 8px;
box-sizing: border-box;
// background-color: var(--island-bg-color);
background-color: var(--island-bg-color);
box-shadow: var(--shadow-island);
border-radius: var(--border-radius-lg);
position: relative;
@@ -29,7 +27,7 @@
}
.dropdown-menu-container {
background-color: var(--island-bg-color);
background-color: #fff !important;
max-height: calc(100vh - 150px);
overflow-y: auto;
--gap: 2;
@@ -40,7 +38,7 @@
padding: 0 0.625rem;
column-gap: 0.625rem;
font-size: 0.875rem;
color: var(--color-on-surface);
color: var(--color-gray-100);
width: 100%;
box-sizing: border-box;
font-weight: normal;
@@ -49,7 +47,7 @@
.dropdown-menu-item {
background-color: transparent;
border: 1px solid transparent;
border: 0;
align-items: center;
height: 2rem;
cursor: pointer;
@@ -59,11 +57,6 @@
height: 2.25rem;
}
&--selected {
background: var(--color-primary-light);
--icon-fill-color: var(--color-primary-darker);
}
&__text {
text-overflow: ellipsis;
overflow: hidden;
@@ -80,11 +73,6 @@
text-decoration: none;
}
&:active {
background-color: var(--button-hover-bg);
border-color: var(--color-brand-active);
}
svg {
width: 1rem;
height: 1rem;
@@ -103,33 +91,22 @@
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 {
@include outlineButtonStyles;
background-color: var(--island-bg-color);
width: 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 {
width: var(--lg-icon-size);
height: var(--lg-icon-size);
@@ -11,14 +11,12 @@ const DropdownMenuItem = ({
children,
shortcut,
className,
selected,
...rest
}: {
icon?: JSX.Element;
onSelect: (event: Event) => void;
children: React.ReactNode;
shortcut?: string;
selected?: boolean;
className?: string;
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onSelect">) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
@@ -28,7 +26,7 @@ const DropdownMenuItem = ({
{...rest}
onClick={handleClick}
type="button"
className={getDropdownMenuItemClassName(className, selected)}
className={getDropdownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]}
>
<MenuItemContent icon={icon} shortcut={shortcut}>
@@ -3,19 +3,15 @@ import React from "react";
const DropdownMenuItemCustom = ({
children,
className = "",
selected,
...rest
}: {
children: React.ReactNode;
className?: string;
selected?: boolean;
} & React.HTMLAttributes<HTMLDivElement>) => {
return (
<div
{...rest}
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className} ${
selected ? `dropdown-menu-item--selected` : ``
}`.trim()}
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className}`.trim()}
>
{children}
</div>
@@ -12,7 +12,6 @@ const DropdownMenuItemLink = ({
children,
onSelect,
className = "",
selected,
...rest
}: {
href: string;
@@ -20,7 +19,6 @@ const DropdownMenuItemLink = ({
children: React.ReactNode;
shortcut?: string;
className?: string;
selected?: boolean;
onSelect?: (event: Event) => void;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
@@ -31,7 +29,7 @@ const DropdownMenuItemLink = ({
href={href}
target="_blank"
rel="noreferrer"
className={getDropdownMenuItemClassName(className, selected)}
className={getDropdownMenuItemClassName(className)}
title={rest.title ?? rest["aria-label"]}
onClick={handleClick}
>
+2 -7
View File
@@ -6,13 +6,8 @@ export const DropdownMenuContentPropsContext = React.createContext<{
onSelect?: (event: Event) => void;
}>({});
export const getDropdownMenuItemClassName = (
className = "",
selected = false,
) => {
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
selected ? "dropdown-menu-item--selected" : ""
}`.trim();
export const getDropdownMenuItemClassName = (className = "") => {
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
};
export const useHandleDropdownMenuItemClick = (
+31 -16
View File
@@ -13,7 +13,7 @@ import clsx from "clsx";
import { Theme } from "../element/types";
import { THEME } from "../constants";
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
const handlerColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
@@ -1654,21 +1654,36 @@ export const frameToolIcon = createIcon(
tablerIconProps,
);
export const laserPointerToolIcon = createIcon(
<g
fill="none"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
transform="rotate(90 10 10)"
>
export const mermaidLogoIcon = createIcon(
<path
fill="black"
d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
/>,
);
export const ArrowRightIcon = createIcon(
<>
<path
d="M4.16602 10H15.8327"
stroke="white"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<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"
d="M12.5 13.3333L15.8333 10"
stroke="white"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="m13.25 3.417 3.333 3.333M10 10l2-2M5 15l3-3M2.156 17.894l1-1M5.453 19.029l-.144-1.407M2.377 11.887l.866 1.118M8.354 17.273l-1.194-.758M.953 14.652l1.408.13" />
</g>,
20,
<path
d="M12.5 6.66666L15.8333 9.99999"
stroke="white"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</>,
{ width: 20, height: 20 },
);
@@ -14,8 +14,6 @@
--button-active-bg: var(--color-primary-darker);
box-shadow: 0 0 0 1px var(--color-surface-lowest);
flex-shrink: 0;
// double .active to force specificity
-1
View File
@@ -43,7 +43,6 @@ const MainMenu = Object.assign(
});
}}
data-testid="main-menu-trigger"
className="main-menu-trigger"
>
{HamburgerMenuIcon}
</DropdownMenu.Trigger>
@@ -174,7 +174,7 @@
justify-content: space-between;
background: none;
border: 1px solid transparent;
border: none;
padding: 0.75rem;
@@ -204,7 +204,7 @@
.welcome-screen-menu-item:hover {
text-decoration: none;
background: var(--button-hover-bg);
background: var(--color-gray-10);
.welcome-screen-menu-item__shortcut {
color: var(--color-gray-50);
@@ -216,8 +216,7 @@
}
.welcome-screen-menu-item:active {
background: var(--button-hover-bg);
border-color: var(--color-brand-active);
background: var(--color-gray-20);
.welcome-screen-menu-item__shortcut {
color: var(--color-gray-50);
@@ -248,7 +247,8 @@
}
.welcome-screen-menu-item:hover {
background-color: var(--color-surface-low);
background: var(--color-gray-85);
.welcome-screen-menu-item__shortcut {
color: var(--color-gray-50);
}
@@ -259,6 +259,7 @@
}
.welcome-screen-menu-item:active {
background-color: var(--color-gray-90);
.welcome-screen-menu-item__text {
color: var(--color-gray-10);
}
+11 -19
View File
@@ -280,6 +280,11 @@
align-items: center;
justify-content: space-between;
padding: 8px;
.dropdown-menu--mobile {
bottom: 55px;
top: auto;
}
}
.App-mobile-menu {
@@ -444,14 +449,13 @@
}
&:active {
border: 1px solid var(--button-active-border);
border: 1px solid var(--color-primary-darkest);
}
}
.help-icon {
@include outlineButtonStyles;
@include filledButtonOnCanvas;
background-color: var(--island-bg-color);
width: var(--lg-button-size);
height: var(--lg-button-size);
@@ -592,6 +596,8 @@
background-color: var(--island-bg-color);
.ToolIcon__icon {
width: 2rem;
height: 2rem;
border-radius: 0;
}
@@ -601,8 +607,8 @@
}
.App-toolbar--mobile {
overflow-x: auto;
max-width: 90vw;
overflow: visible;
max-width: 98vw;
.ToolIcon__keybinding {
display: none;
@@ -622,20 +628,6 @@
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 {
+23 -45
View File
@@ -12,30 +12,27 @@
--dialog-border-color: var(--color-gray-20);
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-2};
--icon-fill-color: var(--color-on-surface);
--icon-fill-color: var(--color-gray-80);
--icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white};
--input-border-color: #{$oc-gray-4};
--input-hover-bg-color: #{$oc-gray-1};
--input-label-color: #{$oc-gray-7};
--island-bg-color: #ffffff;
--island-bg-color: rgba(255, 255, 255, 0.96);
--keybinding-color: var(--color-gray-40);
--link-color: #{$oc-blue-7};
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
--popup-bg-color: var(--island-bg-color);
--popup-bg-color: #{$oc-white};
--popup-secondary-bg-color: #{$oc-gray-1};
--popup-text-color: #{$oc-black};
--popup-text-inverted-color: #{$oc-white};
--select-highlight-color: #{$oc-blue-5};
--shadow-island: 0px 0px 0.9310142993927002px 0px rgba(0, 0, 0, 0.17),
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
0px 7px 14px 0px rgba(0, 0, 0, 0.05);
--button-hover-bg: var(--color-surface-high);
--button-active-bg: var(--color-surface-high);
--button-active-border: var(--color-brand-active);
--default-border-color: var(--color-surface-high);
--shadow-island: 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);
--button-hover-bg: var(--color-gray-10);
--default-border-color: var(--color-gray-30);
--default-button-size: 2rem;
--default-icon-size: 1rem;
@@ -66,14 +63,14 @@
0px 12.5216px 10.0172px rgba(0, 0, 0, 0.035),
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
--sidebar-border-color: var(--color-surface-high);
--sidebar-bg-color: var(--island-bg-color);
--sidebar-border-color: var(--color-gray-20);
--sidebar-bg-color: #fff;
--library-dropdown-shadow: 0px 15px 6px rgba(0, 0, 0, 0.01),
0px 8px 5px rgba(0, 0, 0, 0.05), 0px 4px 4px rgba(0, 0, 0, 0.09),
0px 1px 2px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(0, 0, 0, 0.1);
--space-factor: 0.25rem;
--text-primary-color: var(--color-on-surface);
--text-primary-color: var(--color-gray-80);
--color-selection: #6965db;
@@ -135,19 +132,6 @@
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
--color-surface-high: hsl(244, 100%, 97%);
--color-surface-mid: hsl(240 25% 96%);
--color-surface-low: hsl(240 25% 94%);
--color-surface-lowest: #ffffff;
--color-on-surface: #1b1b1f;
--color-brand-hover: #5753d0;
--color-on-primary-container: #030064;
--color-surface-primary-container: #e0dfff;
--color-brand-active: #4440bf;
--color-border-outline: #767680;
--color-border-outline-variant: #c5c5d0;
--color-surface-primary-container: #e0dfff;
&.theme--dark {
&.theme--dark-background-none {
background: none;
@@ -166,24 +150,29 @@
--dialog-border-color: var(--color-gray-80);
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path fill="%23ced4da" d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-6};
--icon-fill-color: var(--color-gray-40);
--icon-green-fill-color: #{$oc-green-4};
--default-bg-color: #121212;
--input-bg-color: #121212;
--input-border-color: #2e2e2e;
--input-hover-bg-color: #181818;
--input-label-color: #{$oc-gray-2};
--island-bg-color: #232329;
--island-bg-color: #262627;
--keybinding-color: var(--color-gray-60);
--link-color: #{$oc-blue-4};
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
--popup-bg-color: #2c2c2c;
--popup-secondary-bg-color: #222;
--popup-text-color: #{$oc-gray-4};
--popup-text-inverted-color: #2c2c2c;
--select-highlight-color: #{$oc-blue-4};
--shadow-island: 0px 0px 0.9310142993927002px 0px rgba(0, 0, 0, 0.17),
0px 0px 3.1270833015441895px 0px rgba(0, 0, 0, 0.08),
0px 7px 14px 0px rgba(0, 0, 0, 0.05);
--text-primary-color: var(--color-gray-40);
--button-hover-bg: var(--color-gray-80);
--default-border-color: var(--color-gray-80);
--shadow-island: 0px 13px 33px rgba(0, 0, 0, 0.07),
0px 4.13px 9.94853px rgba(0, 0, 0, 0.0456112),
0px 1.13px 4.13211px rgba(0, 0, 0, 0.035),
0px 0.769896px 1.4945px rgba(0, 0, 0, 0.0243888);
--modal-shadow: 0px 100px 80px rgba(0, 0, 0, 0.07),
0px 41.7776px 33.4221px rgba(0, 0, 0, 0.0503198),
0px 22.3363px 17.869px rgba(0, 0, 0, 0.0417275),
@@ -191,6 +180,8 @@
0px 6.6501px 5.32008px rgba(0, 0, 0, 0.0282725),
0px 2.76726px 2.21381px rgba(0, 0, 0, 0.0196802);
--avatar-border-color: var(--color-gray-85);
--sidebar-border-color: var(--color-gray-85);
--sidebar-bg-color: #191919;
--scrollbar-thumb: #{$oc-gray-8};
--scrollbar-thumb-hover: #{$oc-gray-7};
@@ -233,18 +224,5 @@
--color-promo: #d297ff;
--color-logo-text: #e2dfff;
--color-surface-high: hsl(245, 10%, 21%);
--color-surface-low: hsl(240, 8%, 15%);
--color-surface-mid: hsl(240 6% 10%);
--color-surface-lowest: hsl(0, 0%, 7%);
--color-on-surface: #e3e3e8;
--color-brand-hover: #bbb8ff;
--color-on-primary-container: #e0dfff;
--color-surface-primary-container: #403e6a;
--color-brand-active: #d0ccff;
--color-border-outline: #8e8d9c;
--color-border-outline-variant: #46464f;
--color-surface-primary-container: #403e6a;
}
}
+10 -30
View File
@@ -11,7 +11,7 @@
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
&:checked + .ToolIcon__icon {
--icon-fill-color: var(--color-on-primary-container);
--icon-fill-color: var(--color-primary-darker);
svg {
fill: var(--icon-fill-color);
@@ -23,11 +23,11 @@
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
&:checked + .ToolIcon__icon {
background: var(--color-surface-primary-container);
--keybinding-color: var(--color-on-primary-container);
background: var(--color-primary-light);
--keybinding-color: var(--color-gray-60);
svg {
color: var(--color-on-primary-container);
color: var(--color-primary-darker);
}
}
}
@@ -44,11 +44,7 @@
&:active {
background: var(--button-hover-bg);
border: 1px solid var(--button-active-border);
svg {
color: var(--color-on-primary-container);
}
border: 1px solid var(--color-primary-darkest);
}
}
}
@@ -67,7 +63,7 @@
border-radius: var(--border-radius-lg);
cursor: pointer;
background-color: var(--button-bg, var(--island-bg-color));
color: var(--button-color, var(--color-on-surface));
color: var(--button-color, var(--text-primary-color));
svg {
width: var(--button-width, var(--lg-icon-size));
@@ -92,38 +88,22 @@
}
&.active {
background-color: var(
--button-selected-bg,
var(--color-surface-primary-container)
);
border-color: var(
--button-selected-border,
var(--color-surface-primary-container)
);
background-color: var(--button-selected-bg, var(--color-primary-light));
border-color: var(--button-selected-border, var(--color-primary-light));
&:hover {
background-color: var(
--button-selected-hover-bg,
var(--color-surface-primary-container)
var(--color-primary-light)
);
}
svg {
color: var(--button-color, var(--color-on-primary-container));
color: var(--button-color, var(--color-primary-darker));
}
}
}
@mixin filledButtonOnCanvas {
border: none;
box-shadow: 0 0 0 1px var(--color-surface-lowest);
background-color: var(--color-surface-low);
&:active {
box-shadow: 0 0 0 1px var(--color-brand-active);
}
}
$theme-filter: "invert(93%) hue-rotate(180deg)";
$right-sidebar-width: "302px";
-103
View File
@@ -1,103 +0,0 @@
import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants";
import OpenColor from "open-color";
import { AppState, DataURL } from "./types";
import { isHandToolActive, isEraserActive } from "./appState";
const laserPointerCursorSVG_tag = `<svg viewBox="0 0 24 24" stroke-width="1" width="28" height="28" xmlns="http://www.w3.org/2000/svg">`;
const laserPointerCursorBackgroundSVG = `<path d="M6.164 11.755a5.314 5.314 0 0 1-4.932-5.298 5.314 5.314 0 0 1 5.311-5.311 5.314 5.314 0 0 1 5.307 5.113l8.773 8.773a3.322 3.322 0 0 1 0 4.696l-.895.895a3.322 3.322 0 0 1-4.696 0l-8.868-8.868Z" style="fill:#fff"/>`;
const laserPointerCursorIconSVG = `<path stroke="#1b1b1f" fill="#fff" d="m7.868 11.113 7.773 7.774a2.359 2.359 0 0 0 1.667.691 2.368 2.368 0 0 0 2.357-2.358c0-.625-.248-1.225-.69-1.667L11.201 7.78 9.558 9.469l-1.69 1.643v.001Zm10.273 3.606-3.333 3.333m-3.25-6.583 2 2m-7-7 3 3M3.664 3.625l1 1M2.529 6.922l1.407-.144m5.735-2.932-1.118.866M4.285 9.823l.758-1.194m1.863-6.207-.13 1.408"/>`;
const laserPointerCursorDataURL_lightMode = `data:${
MIME_TYPES.svg
},${encodeURIComponent(
`${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}</svg>`,
)}`;
const laserPointerCursorDataURL_darkMode = `data:${
MIME_TYPES.svg
},${encodeURIComponent(
`${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}</svg>`,
)}`;
export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
if (interactiveCanvas) {
interactiveCanvas.style.cursor = "";
}
};
export const setCursor = (
interactiveCanvas: HTMLCanvasElement | null,
cursor: string,
) => {
if (interactiveCanvas) {
interactiveCanvas.style.cursor = cursor;
}
};
let eraserCanvasCache: any;
let previewDataURL: string;
export const setEraserCursor = (
interactiveCanvas: HTMLCanvasElement | null,
theme: AppState["theme"],
) => {
const cursorImageSizePx = 20;
const drawCanvas = () => {
const isDarkTheme = theme === THEME.DARK;
eraserCanvasCache = document.createElement("canvas");
eraserCanvasCache.theme = theme;
eraserCanvasCache.height = cursorImageSizePx;
eraserCanvasCache.width = cursorImageSizePx;
const context = eraserCanvasCache.getContext("2d")!;
context.lineWidth = 1;
context.beginPath();
context.arc(
eraserCanvasCache.width / 2,
eraserCanvasCache.height / 2,
5,
0,
2 * Math.PI,
);
context.fillStyle = isDarkTheme ? OpenColor.black : OpenColor.white;
context.fill();
context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black;
context.stroke();
previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
};
if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
drawCanvas();
}
setCursor(
interactiveCanvas,
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
cursorImageSizePx / 2
}, auto`,
);
};
export const setCursorForShape = (
interactiveCanvas: HTMLCanvasElement | null,
appState: Pick<AppState, "activeTool" | "theme">,
) => {
if (!interactiveCanvas) {
return;
}
if (appState.activeTool.type === "selection") {
resetCursor(interactiveCanvas);
} else if (isHandToolActive(appState)) {
interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
} else if (isEraserActive(appState)) {
setEraserCursor(interactiveCanvas, appState.theme);
// do nothing if image tool is selected which suggests there's
// a image-preview set as the cursor
// Ignore custom type as well and let host decide
} else if (appState.activeTool.type === "laser") {
const url =
appState.theme === THEME.LIGHT
? laserPointerCursorDataURL_lightMode
: laserPointerCursorDataURL_darkMode;
interactiveCanvas.style.cursor = `url(${url}), auto`;
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
}
};
+47 -47
View File
@@ -97,12 +97,12 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"points": [
[
0,
0,
0.5,
0.5,
],
[
395,
35,
394.5,
34.5,
],
],
"roughness": 1,
@@ -150,11 +150,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
400,
399.5,
0,
],
],
@@ -317,7 +317,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"endBinding": {
"elementId": "text-2",
"focus": 0,
"gap": 5,
"gap": 205,
},
"fillStyle": "hachure",
"frameId": null,
@@ -331,11 +331,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -355,7 +355,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 255,
"y": 239,
}
@@ -395,7 +395,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 130,
"x": 340,
"x": 240,
"y": 226.5,
}
`;
@@ -428,11 +428,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -452,7 +452,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 255,
"y": 239,
}
@@ -492,7 +492,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 130,
"x": 340,
"x": 240,
"y": 226.5,
}
`;
@@ -562,7 +562,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"version": 2,
"versionNonce": Any<Number>,
"width": 100,
"x": 555,
"x": 355,
"y": 189,
}
`;
@@ -595,11 +595,11 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -619,7 +619,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"updated": 1,
"version": 3,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 255,
"y": 239,
}
@@ -659,7 +659,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 130,
"x": 340,
"x": 240,
"y": 226.5,
}
`;
@@ -747,7 +747,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"versionNonce": Any<Number>,
"verticalAlign": "top",
"width": 100,
"x": 555,
"x": 355,
"y": 226.5,
}
`;
@@ -801,11 +801,11 @@ exports[`Test Transform > should transform linear elements 1`] = `
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -821,7 +821,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
"updated": 1,
"version": 1,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 100,
"y": 20,
}
@@ -846,11 +846,11 @@ exports[`Test Transform > should transform linear elements 2`] = `
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -866,7 +866,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
"updated": 1,
"version": 1,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 450,
"y": 20,
}
@@ -895,7 +895,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
0,
],
[
300,
100,
0,
],
],
@@ -911,7 +911,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
"updated": 1,
"version": 1,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 100,
"y": 60,
}
@@ -940,7 +940,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
0,
],
[
300,
100,
0,
],
],
@@ -956,7 +956,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
"updated": 1,
"version": 1,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 450,
"y": 60,
}
@@ -1244,11 +1244,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -1264,7 +1264,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"updated": 1,
"version": 1,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 100,
"y": 100,
}
@@ -1294,11 +1294,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -1314,7 +1314,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"updated": 1,
"version": 1,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 100,
"y": 200,
}
@@ -1344,11 +1344,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -1364,7 +1364,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"updated": 1,
"version": 2,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 100,
"y": 300,
}
@@ -1394,11 +1394,11 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"opacity": 100,
"points": [
[
0,
0.5,
0,
],
[
300,
99.5,
0,
],
],
@@ -1414,7 +1414,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"updated": 1,
"version": 2,
"versionNonce": Any<Number>,
"width": 300,
"width": 100,
"x": 100,
"y": 400,
}
@@ -1454,7 +1454,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 130,
"x": 185,
"x": 85,
"y": 87.5,
}
`;
@@ -1493,7 +1493,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 200,
"x": 150,
"x": 50,
"y": 187.5,
}
`;
@@ -1533,7 +1533,7 @@ LABELLED ARROW",
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 150,
"x": 175,
"x": 75,
"y": 275,
}
`;
@@ -1573,7 +1573,7 @@ LABELLED ARROW",
"versionNonce": Any<Number>,
"verticalAlign": "middle",
"width": 150,
"x": 175,
"x": 75,
"y": 375,
}
`;
+3 -31
View File
@@ -8,7 +8,7 @@ import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types";
import { ValueOf } from "../utility-types";
import { bytesToHexString, isPromiseLike } from "../utils";
import { bytesToHexString } from "../utils";
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json";
import { restore, restoreLibraryItems } from "./restore";
@@ -207,13 +207,10 @@ export const loadLibraryFromBlob = async (
};
export const canvasToBlob = async (
canvas: HTMLCanvasElement | Promise<HTMLCanvasElement>,
canvas: HTMLCanvasElement,
): Promise<Blob> => {
return new Promise(async (resolve, reject) => {
return new Promise((resolve, reject) => {
try {
if (isPromiseLike(canvas)) {
canvas = await canvas;
}
canvas.toBlob((blob) => {
if (!blob) {
return reject(
@@ -327,31 +324,6 @@ export const SVGStringToFile = (SVGString: string, filename: string = "") => {
}) as File & { type: typeof MIME_TYPES.svg };
};
export const ImageURLToFile = async (
imageUrl: string,
filename: string = "",
): Promise<File | undefined> => {
let response;
try {
response = await fetch(imageUrl);
} catch (error: any) {
throw new Error(t("errors.failedToFetchImage"));
}
if (!response.ok) {
throw new Error(t("errors.failedToFetchImage"));
}
const blob = await response.blob();
if (blob.type && isSupportedImageFile(blob)) {
const name = filename || blob.name || "";
return new File([blob], name, { type: blob.type });
}
throw new Error(t("errors.unsupportedFileType"));
};
export const getFileFromEvent = async (
event: React.DragEvent<HTMLDivElement>,
) => {
+7 -1
View File
@@ -66,14 +66,17 @@ export const exportCanvas = async (
}
}
const tempCanvas = exportToCanvas(elements, appState, files, {
const tempCanvas = await exportToCanvas(elements, appState, files, {
exportBackground,
viewBackgroundColor,
exportPadding,
});
tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas);
if (type === "png") {
let blob = await canvasToBlob(tempCanvas);
tempCanvas.remove();
if (appState.exportEmbedScene) {
blob = await (
await import(/* webpackChunkName: "image" */ "./image")
@@ -111,8 +114,11 @@ export const exportCanvas = async (
} else {
throw new Error(t("alerts.couldNotCopyToClipboard"));
}
} finally {
tempCanvas.remove();
}
} else {
tempCanvas.remove();
// shouldn't happen
throw new Error("Unsupported export type");
}
+8 -24
View File
@@ -43,7 +43,6 @@ import {
measureBaseline,
} from "../element/textElement";
import { normalizeLink } from "./url";
import { isValidFrameChild } from "../frame";
type RestoredAppState = Omit<
AppState,
@@ -68,7 +67,7 @@ export const AllowedExcalidrawActiveTools: Record<
frame: true,
embeddable: true,
hand: true,
laser: false,
mermaid: true,
};
export type RestoredDataState = {
@@ -190,7 +189,7 @@ const restoreElement = (
fontSize = parseFloat(fontPx);
fontFamily = getFontFamilyByName(_fontFamily);
}
const text = (typeof element.text === "string" && element.text) || "";
const text = element.text ?? "";
// line-height might not be specified either when creating elements
// programmatically, or when importing old diagrams.
@@ -223,17 +222,9 @@ const restoreElement = (
baseline,
});
// if empty text, mark as deleted. We keep in array
// for data integrity purposes (collab etc.)
if (!text && !element.isDeleted) {
element = { ...element, originalText: text, isDeleted: true };
element = bumpVersion(element);
}
if (refreshDimensions) {
element = { ...element, ...refreshTextDimensions(element) };
}
return element;
case "freedraw": {
return restoreElementWithProperties(element, {
@@ -308,7 +299,6 @@ const restoreElement = (
// We also don't want to throw, but instead return void so we filter
// out these unsupported elements from the restored array.
}
return null;
};
/**
@@ -397,7 +387,7 @@ const repairBoundElement = (
};
/**
* resets `frameId` if no longer applicable.
* Remove an element's frameId if its containing frame is non-existent
*
* NOTE mutates elements.
*/
@@ -405,16 +395,12 @@ const repairFrameMembership = (
element: Mutable<ExcalidrawElement>,
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
) => {
if (!element.frameId) {
return;
}
if (element.frameId) {
const containingFrame = elementsMap.get(element.frameId);
if (
!isValidFrameChild(element) ||
// target frame not exists
!elementsMap.get(element.frameId)
) {
element.frameId = null;
if (!containingFrame) {
element.frameId = null;
}
}
};
@@ -458,8 +444,6 @@ export const restoreElements = (
// repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) {
// repair frame membership *after* bindings we do in restoreElement()
// since we rely on bindings to be correct
if (element.frameId) {
repairFrameMembership(element, restoredElementsMap);
}
+8 -9
View File
@@ -321,7 +321,7 @@ describe("Test Transform", () => {
});
expect(text).toMatchObject({
x: 340,
x: 240,
y: 226.5,
type: "text",
text: "HELLO WORLD!!",
@@ -341,7 +341,7 @@ describe("Test Transform", () => {
});
expect(ellipse).toMatchObject({
x: 555,
x: 355,
y: 189,
type: "ellipse",
boundElements: [
@@ -386,7 +386,6 @@ describe("Test Transform", () => {
);
expect(excaldrawElements.length).toBe(4);
const [arrow, text1, text2, text3] = excaldrawElements;
expect(arrow).toMatchObject({
@@ -406,7 +405,7 @@ describe("Test Transform", () => {
});
expect(text1).toMatchObject({
x: 340,
x: 240,
y: 226.5,
type: "text",
text: "HELLO WORLD!!",
@@ -426,7 +425,7 @@ describe("Test Transform", () => {
});
expect(text3).toMatchObject({
x: 555,
x: 355,
y: 226.5,
type: "text",
boundElements: [
@@ -603,14 +602,14 @@ describe("Test Transform", () => {
);
expect(excaldrawElements.length).toBe(4);
const [, , arrow] = excaldrawElements;
const [, , arrow, text] = excaldrawElements;
expect(arrow).toMatchObject({
type: "arrow",
x: 255,
y: 239,
boundElements: [
{
id: "id46",
id: text.id,
type: "text",
},
],
@@ -656,11 +655,11 @@ describe("Test Transform", () => {
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
elementId: "rect-1",
focus: 0,
gap: 5,
gap: 205,
});
expect(rect.boundElements).toStrictEqual([
{
id: "id47",
id: arrow.id,
type: "arrow",
},
]);
+85 -4
View File
@@ -39,6 +39,8 @@ import {
} from "../element/types";
import { MarkOptional } from "../utility-types";
import { assertNever, getFontString } from "../utils";
import { getSizeFromPoints } from "../points";
import { nanoid } from "nanoid";
export type ValidLinearElement = {
type: "arrow" | "line";
@@ -159,7 +161,7 @@ export type ExcalidrawElementSkeleton =
} & Partial<ExcalidrawImageElement>);
const DEFAULT_LINEAR_ELEMENT_PROPS = {
width: 300,
width: 100,
height: 0,
};
@@ -357,6 +359,48 @@ const bindLinearElementToElement = (
);
}
}
// Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
const endPointIndex = linearElement.points.length - 1;
const delta = 0.5;
const newPoints = JSON.parse(JSON.stringify(linearElement.points));
// left to right so shift the arrow towards right
if (
linearElement.points[endPointIndex][0] >
linearElement.points[endPointIndex - 1][0]
) {
newPoints[0][0] = delta;
newPoints[endPointIndex][0] -= delta;
}
// right to left so shift the arrow towards left
if (
linearElement.points[endPointIndex][0] <
linearElement.points[endPointIndex - 1][0]
) {
newPoints[0][0] = -delta;
newPoints[endPointIndex][0] += delta;
}
// top to bottom so shift the arrow towards top
if (
linearElement.points[endPointIndex][1] >
linearElement.points[endPointIndex - 1][1]
) {
newPoints[0][1] = delta;
newPoints[endPointIndex][1] -= delta;
}
// bottom to top so shift the arrow towards bottom
if (
linearElement.points[endPointIndex][1] <
linearElement.points[endPointIndex - 1][1]
) {
newPoints[0][1] = -delta;
newPoints[endPointIndex][1] += delta;
}
Object.assign(linearElement, { points: newPoints });
return {
linearElement,
startBoundElement,
@@ -367,7 +411,7 @@ const bindLinearElementToElement = (
class ElementStore {
excalidrawElements = new Map<string, ExcalidrawElement>();
add = (ele?: ExcalidrawElement) => {
add = (ele?: ExcalidrawElement, originalId?: string) => {
if (!ele) {
return;
}
@@ -385,6 +429,7 @@ class ElementStore {
export const convertToExcalidrawElements = (
elements: ExcalidrawElementSkeleton[] | null,
opts?: { regenerateIds: boolean },
) => {
if (!elements) {
return [];
@@ -392,10 +437,16 @@ export const convertToExcalidrawElements = (
const elementStore = new ElementStore();
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
const oldToNewElementIdMap = new Map<string, string>();
// Create individual elements
for (const element of elements) {
let excalidrawElement: ExcalidrawElement;
const originalId = element.id;
if (opts?.regenerateIds) {
Object.assign(element, { id: nanoid() });
}
switch (element.type) {
case "rectangle":
case "ellipse":
@@ -444,6 +495,11 @@ export const convertToExcalidrawElements = (
],
...element,
});
Object.assign(
excalidrawElement,
getSizeFromPoints(excalidrawElement.points),
);
break;
}
case "text": {
@@ -499,6 +555,9 @@ export const convertToExcalidrawElements = (
} else {
elementStore.add(excalidrawElement);
elementsWithIds.set(excalidrawElement.id, element);
if (originalId) {
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
}
}
}
@@ -524,6 +583,18 @@ export const convertToExcalidrawElements = (
element.type === "arrow" ? element?.start : undefined;
const originalEnd =
element.type === "arrow" ? element?.end : undefined;
if (originalStart && originalStart.id) {
const newStartId = oldToNewElementIdMap.get(originalStart.id);
if (newStartId) {
Object.assign(originalStart, { id: newStartId });
}
}
if (originalEnd && originalEnd.id) {
const newEndId = oldToNewElementIdMap.get(originalEnd.id);
if (newEndId) {
Object.assign(originalEnd, { id: newEndId });
}
}
const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement(
container as ExcalidrawArrowElement,
@@ -539,13 +610,23 @@ export const convertToExcalidrawElements = (
} else {
switch (element.type) {
case "arrow": {
const { start, end } = element;
if (start && start.id) {
const newStartId = oldToNewElementIdMap.get(start.id);
Object.assign(start, { id: newStartId });
}
if (end && end.id) {
const newEndId = oldToNewElementIdMap.get(end.id);
Object.assign(end, { id: newEndId });
}
const { linearElement, startBoundElement, endBoundElement } =
bindLinearElementToElement(
excalidrawElement as ExcalidrawArrowElement,
element.start,
element.end,
start,
end,
elementStore,
);
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
-10
View File
@@ -27,7 +27,6 @@ import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils";
import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { isValidFrameChild } from "../frame";
export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement>
@@ -212,15 +211,6 @@ export const bindLinearElement = (
}),
});
}
if (linearElement.frameId && !isValidFrameChild(linearElement)) {
mutateElement(
linearElement,
{
frameId: null,
},
false,
);
}
};
// Don't bind both ends of a simple segment
+1 -14
View File
@@ -158,7 +158,7 @@ export const getElementAbsoluteCoords = (
];
};
/*
/**
* for a given element, `getElementLineSegments` returns line segments
* that can be used for visual collision detection (useful for frames)
* as opposed to bounding box collision detection
@@ -674,19 +674,6 @@ export const getCommonBounds = (
return [minX, minY, maxX, maxY];
};
export const getDraggedElementsBounds = (
elements: ExcalidrawElement[],
dragOffset: { x: number; y: number },
) => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return [
minX + dragOffset.x,
minY + dragOffset.y,
maxX + dragOffset.x,
maxY + dragOffset.y,
];
};
export const getResizedElementAbsoluteCoords = (
element: ExcalidrawElement,
nextWidth: number,
+45 -54
View File
@@ -6,22 +6,27 @@ import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types";
import { getBoundTextElement } from "./textElement";
import { isSelectedViaGroup } from "../groups";
import { getGridPoint } from "../math";
import Scene from "../scene/Scene";
import { isFrameElement } from "./typeChecks";
import {
isArrowElement,
isBoundToContainer,
isFrameElement,
} from "./typeChecks";
export const dragSelectedElements = (
pointerDownState: PointerDownState,
selectedElements: NonDeletedExcalidrawElement[],
offset: { x: number; y: number },
pointerX: number,
pointerY: number,
lockDirection: boolean = false,
distanceX: number = 0,
distanceY: number = 0,
appState: AppState,
scene: Scene,
snapOffset: {
x: number;
y: number;
},
gridSize: AppState["gridSize"],
) => {
const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 };
// we do not want a frame and its elements to be selected at the same time
// but when it happens (due to some bug), we want to avoid updating element
// in the frame twice, hence the use of set
@@ -35,6 +40,7 @@ export const dragSelectedElements = (
if (frames.length > 0) {
const elementsInFrames = scene
.getNonDeletedElements()
.filter((e) => !isBoundToContainer(e))
.filter((e) => e.frameId !== null)
.filter((e) => frames.includes(e.frameId!));
@@ -43,35 +49,33 @@ export const dragSelectedElements = (
elementsToUpdate.forEach((element) => {
updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
element,
offset,
snapOffset,
gridSize,
);
// update coords of bound text only if we're dragging the container directly
// (we don't drag the group that it's part of)
if (
// Don't update coords of arrow label since we calculate its position during render
!isArrowElement(element) &&
// container isn't part of any group
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
!element.groupIds.length ||
// container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
(!element.groupIds.length ||
// container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
) {
const textElement = getBoundTextElement(element);
if (
textElement &&
// when container is added to a frame, so will its bound text
// so the text is already in `elementsToUpdate` and we should avoid
// updating its coords again
(!textElement.frameId || !frames.includes(textElement.frameId))
) {
if (textElement) {
updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
textElement,
offset,
snapOffset,
gridSize,
);
}
}
@@ -82,40 +86,31 @@ export const dragSelectedElements = (
};
const updateElementCoords = (
lockDirection: boolean,
distanceX: number,
distanceY: number,
pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement,
dragOffset: { x: number; y: number },
snapOffset: { x: number; y: number },
gridSize: AppState["gridSize"],
offset: { x: number; y: number },
) => {
const originalElement =
pointerDownState.originalElements.get(element.id) ?? element;
let nextX = originalElement.x + dragOffset.x + snapOffset.x;
let nextY = originalElement.y + dragOffset.y + snapOffset.y;
if (snapOffset.x === 0 || snapOffset.y === 0) {
const [nextGridX, nextGridY] = getGridPoint(
originalElement.x + dragOffset.x,
originalElement.y + dragOffset.y,
gridSize,
);
if (snapOffset.x === 0) {
nextX = nextGridX;
}
if (snapOffset.y === 0) {
nextY = nextGridY;
}
let x: number;
let y: number;
if (lockDirection) {
const lockX = lockDirection && distanceX < distanceY;
const lockY = lockDirection && distanceX > distanceY;
const original = pointerDownState.originalElements.get(element.id);
x = lockX && original ? original.x : element.x + offset.x;
y = lockY && original ? original.y : element.y + offset.y;
} else {
x = element.x + offset.x;
y = element.y + offset.y;
}
mutateElement(element, {
x: nextX,
y: nextY,
x,
y,
});
};
export const getDragOffsetXY = (
selectedElements: NonDeletedExcalidrawElement[],
x: number,
@@ -139,10 +134,6 @@ export const dragNewElement = (
/** whether to keep given aspect ratio when `isResizeWithSidesSameLength` is
true */
widthAspectRatio?: number | null,
originOffset: {
x: number;
y: number;
} | null = null,
) => {
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
if (widthAspectRatio) {
@@ -183,8 +174,8 @@ export const dragNewElement = (
if (width !== 0 && height !== 0) {
mutateElement(draggingElement, {
x: newX + (originOffset?.x ?? 0),
y: newY + (originOffset?.y ?? 0),
x: newX,
y: newY,
width,
height,
});
+1 -3
View File
@@ -2,8 +2,7 @@ import { register } from "../actions/register";
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
import { t } from "../i18n";
import { ExcalidrawProps } from "../types";
import { getFontString, updateActiveTool } from "../utils";
import { setCursorForShape } from "../cursor";
import { getFontString, setCursorForShape, updateActiveTool } from "../utils";
import { newTextElement } from "./newElement";
import { getContainerElement, wrapText } from "./textElement";
import { isEmbeddableElement } from "./typeChecks";
@@ -28,7 +27,6 @@ const embeddedLinkCache = new Map<string, EmbeddedLink>();
const RE_YOUTUBE =
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
const RE_VIMEO =
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
+2 -2
View File
@@ -140,8 +140,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
*
* NOTE: does not trigger re-render.
*/
export const bumpVersion = <T extends Mutable<ExcalidrawElement>>(
element: T,
export const bumpVersion = (
element: Mutable<ExcalidrawElement>,
version?: ExcalidrawElement["version"],
) => {
element.version = (version ?? element.version) + 1;
+13 -27
View File
@@ -41,7 +41,7 @@ import {
MaybeTransformHandleType,
TransformHandleDirection,
} from "./transformHandles";
import { AppState, Point, PointerDownState } from "../types";
import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineWidth,
@@ -79,7 +79,6 @@ export const transformElements = (
pointerY: number,
centerX: number,
centerY: number,
appState: AppState,
) => {
if (selectedElements.length === 1) {
const [element] = selectedElements;
@@ -467,8 +466,8 @@ export const resizeSingleElement = (
boundTextElement.fontSize,
boundTextElement.lineHeight,
);
eleNewWidth = Math.max(eleNewWidth, minWidth);
eleNewHeight = Math.max(eleNewHeight, minHeight);
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
}
}
@@ -509,11 +508,8 @@ export const resizeSingleElement = (
}
}
const flipX = eleNewWidth < 0;
const flipY = eleNewHeight < 0;
// Flip horizontally
if (flipX) {
if (eleNewWidth < 0) {
if (transformHandleDirection.includes("e")) {
newTopLeft[0] -= Math.abs(newBoundsWidth);
}
@@ -521,9 +517,8 @@ export const resizeSingleElement = (
newTopLeft[0] += Math.abs(newBoundsWidth);
}
}
// Flip vertically
if (flipY) {
if (eleNewHeight < 0) {
if (transformHandleDirection.includes("s")) {
newTopLeft[1] -= Math.abs(newBoundsHeight);
}
@@ -547,20 +542,10 @@ export const resizeSingleElement = (
const rotatedNewCenter = rotatePoint(newCenter, startCenter, angle);
newTopLeft = rotatePoint(rotatedTopLeft, rotatedNewCenter, -angle);
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
const newOrigin = [...newTopLeft];
const linearElementXOffset = stateAtResizeStart.x - newBoundsX1;
const linearElementYOffset = stateAtResizeStart.y - newBoundsY1;
newOrigin[0] += linearElementXOffset;
newOrigin[1] += linearElementYOffset;
const nextX = newOrigin[0];
const nextY = newOrigin[1];
// Readjust points for linear elements
let rescaledElementPointsY;
let rescaledPoints;
if (isLinearElement(element) || isFreeDrawElement(element)) {
rescaledElementPointsY = rescalePoints(
1,
@@ -577,11 +562,16 @@ export const resizeSingleElement = (
);
}
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
const newOrigin = [...newTopLeft];
newOrigin[0] += stateAtResizeStart.x - newBoundsX1;
newOrigin[1] += stateAtResizeStart.y - newBoundsY1;
const resizedElement = {
width: Math.abs(eleNewWidth),
height: Math.abs(eleNewHeight),
x: nextX,
y: nextY,
x: newOrigin[0],
y: newOrigin[1],
points: rescaledPoints,
};
@@ -690,10 +680,6 @@ export const resizeMultipleElements = (
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
targetElements.map(({ orig }) => orig).concat(boundTextElements),
);
// const originalHeight = maxY - minY;
// const originalWidth = maxX - minX;
const direction = transformHandleType;
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
+1 -2
View File
@@ -12,7 +12,6 @@ export const showSelectedShapeActions = (
(appState.editingElement ||
(appState.activeTool.type !== "selection" &&
appState.activeTool.type !== "eraser" &&
appState.activeTool.type !== "hand" &&
appState.activeTool.type !== "laser"))) ||
appState.activeTool.type !== "hand"))) ||
getSelectedElements(elements, appState).length),
);
+75 -74
View File
@@ -1,6 +1,12 @@
import ReactDOM from "react-dom";
import { Excalidraw } from "../packages/excalidraw/index";
import { GlobalTestState, render, screen } from "../tests/test-utils";
import {
GlobalTestState,
getTextEditor,
render,
screen,
updateTextEditor,
} from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { CODES, KEYS } from "../keys";
import {
@@ -8,7 +14,7 @@ import {
mockBoundingClientRect,
restoreOriginalGetBoundingClientRect,
} from "../tests/test-utils";
import { queryByText, waitFor } from "@testing-library/react";
import { queryByText } from "@testing-library/react";
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
import {
@@ -17,6 +23,7 @@ import {
} from "./types";
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
import { resize } from "../tests/utils";
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
// Unmount ReactDOM from root
@@ -25,22 +32,7 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const tab = " ";
const mouse = new Pointer("mouse");
const getTextEditor = async (waitForEditor = false) => {
const query = () =>
document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
if (waitForEditor) {
waitFor(() => expect(query()).not.toBe(null));
return query();
}
return query();
};
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
fireEvent.change(editor, { target: { value } });
editor.dispatchEvent(new Event("input"));
};
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
describe("textWysiwyg", () => {
describe("start text editing", () => {
@@ -191,7 +183,7 @@ describe("textWysiwyg", () => {
expect(h.state.editingElement?.id).toBe(boundText.id);
});
it("should edit text under cursor when clicked with text tool", async () => {
it("should edit text under cursor when clicked with text tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
@@ -206,14 +198,14 @@ describe("textWysiwyg", () => {
mouse.clickAt(text.x + 50, text.y + 50);
const editor = await getTextEditor();
const editor = getTextEditor(textEditorSelector);
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1);
});
it("should edit text under cursor when double-clicked with selection tool", async () => {
it("should edit text under cursor when double-clicked with selection tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
@@ -228,7 +220,7 @@ describe("textWysiwyg", () => {
mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = await getTextEditor();
const editor = getTextEditor(textEditorSelector);
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
@@ -255,7 +247,7 @@ describe("textWysiwyg", () => {
textElement = UI.createElement("text");
mouse.clickOn(textElement);
textarea = await getTextEditor(true);
textarea = getTextEditor(textEditorSelector);
});
afterAll(() => {
@@ -465,7 +457,7 @@ describe("textWysiwyg", () => {
UI.clickTool("text");
mouse.clickAt(750, 300);
textarea = await getTextEditor(true);
textarea = getTextEditor(textEditorSelector);
updateTextEditor(
textarea,
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
@@ -517,7 +509,7 @@ describe("textWysiwyg", () => {
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
@@ -545,7 +537,7 @@ describe("textWysiwyg", () => {
]);
expect(text.angle).toBe(rectangle.angle);
mouse.down();
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
@@ -572,7 +564,7 @@ describe("textWysiwyg", () => {
API.setSelectedElements([diamond]);
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
const value = new Array(1000).fill("1").join("\n");
@@ -607,7 +599,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
let editor = await getTextEditor(true);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
editor.blur();
@@ -622,7 +614,7 @@ describe("textWysiwyg", () => {
expect(text.containerId).toBe(rectangle.id);
mouse.down();
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
@@ -644,7 +636,7 @@ describe("textWysiwyg", () => {
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
@@ -679,7 +671,7 @@ describe("textWysiwyg", () => {
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
await new Promise((r) => setTimeout(r, 0));
@@ -704,7 +696,7 @@ describe("textWysiwyg", () => {
freedraw.y + freedraw.height / 2,
);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
@@ -738,7 +730,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
@@ -753,7 +745,7 @@ describe("textWysiwyg", () => {
UI.clickTool("text");
mouse.clickAt(20, 30);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(
editor,
@@ -798,7 +790,7 @@ describe("textWysiwyg", () => {
mouse.down();
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = await getTextEditor(true);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
@@ -811,7 +803,7 @@ describe("textWysiwyg", () => {
rectangle.y + rectangle.height / 2,
);
mouse.down();
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
editor.select();
fireEvent.click(screen.getByTitle(/code/i));
@@ -844,7 +836,7 @@ describe("textWysiwyg", () => {
Keyboard.keyDown(KEYS.ENTER);
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = await getTextEditor(true);
let editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
@@ -865,7 +857,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello");
await new Promise((r) => setTimeout(r, 0));
@@ -894,7 +886,7 @@ describe("textWysiwyg", () => {
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
@@ -931,7 +923,7 @@ describe("textWysiwyg", () => {
// Bind first text
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
@@ -952,24 +944,24 @@ describe("textWysiwyg", () => {
it("should respect text alignment when resizing", async () => {
Keyboard.keyPress(KEYS.ENTER);
let editor = await getTextEditor(true);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
// should center align horizontally and vertically by default
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
[
85,
4.999999999999986,
4.5,
]
`);
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
editor.select();
@@ -982,7 +974,7 @@ describe("textWysiwyg", () => {
editor.blur();
// should left align horizontally and bottom vertically after resize
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
[
15,
@@ -992,7 +984,7 @@ describe("textWysiwyg", () => {
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
editor.select();
@@ -1004,11 +996,11 @@ describe("textWysiwyg", () => {
editor.blur();
// should right align horizontally and top vertically after resize
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
[
374.99999999999994,
-535.0000000000001,
375,
-539,
]
`);
});
@@ -1030,7 +1022,7 @@ describe("textWysiwyg", () => {
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
mouse.down();
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
@@ -1045,7 +1037,7 @@ describe("textWysiwyg", () => {
it("should scale font size correctly when resizing using shift", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
@@ -1054,7 +1046,7 @@ describe("textWysiwyg", () => {
expect(rectangle.height).toBe(75);
expect(textElement.fontSize).toBe(20);
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
shift: true,
});
expect(rectangle.width).toBe(200);
@@ -1065,7 +1057,7 @@ describe("textWysiwyg", () => {
it("should bind text correctly when container duplicated with alt-drag", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
@@ -1097,7 +1089,7 @@ describe("textWysiwyg", () => {
it("undo should work", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
@@ -1134,7 +1126,7 @@ describe("textWysiwyg", () => {
it("should not allow bound text with only whitespaces", async () => {
Keyboard.keyPress(KEYS.ENTER);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, " ");
@@ -1189,35 +1181,32 @@ describe("textWysiwyg", () => {
it("should reset the container height cache when resizing", async () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
let editor = await getTextEditor(true);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect(rectangle.height).toBeCloseTo(155, 8);
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect(rectangle.height).toBe(156);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.height).toBeCloseTo(155, 8);
expect(rectangle.height).toBe(156);
// cache updated again
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
155,
8,
);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
});
it("should reset the container height cache when font properties updated", async () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
editor.blur();
@@ -1242,7 +1231,7 @@ describe("textWysiwyg", () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(
@@ -1274,12 +1263,12 @@ describe("textWysiwyg", () => {
beforeEach(async () => {
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
updateTextEditor(editor, "Hello");
editor.blur();
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
editor.select();
});
@@ -1390,7 +1379,7 @@ describe("textWysiwyg", () => {
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
UI.clickTool("text");
mouse.clickAt(20, 30);
const editor = await getTextEditor(true);
const editor = getTextEditor(textEditorSelector);
updateTextEditor(
editor,
@@ -1478,7 +1467,7 @@ describe("textWysiwyg", () => {
// Bind first text
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
let editor = await getTextEditor(true);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello!");
expect(
@@ -1503,7 +1492,7 @@ describe("textWysiwyg", () => {
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
editor = await getTextEditor(true);
editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Excalidraw");
editor.blur();
@@ -1518,16 +1507,28 @@ describe("textWysiwyg", () => {
});
});
it("should bump the version of a labeled arrow when the label is updated", async () => {
it("should bump the version of labelled arrow when label updated", async () => {
await render(<Excalidraw handleKeyboardGlobally={true} />);
const arrow = UI.createElement("arrow", {
width: 300,
height: 0,
});
await UI.editText(arrow, "Hello");
mouse.select(arrow);
Keyboard.keyPress(KEYS.ENTER);
let editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello");
editor.blur();
const { version } = arrow;
await UI.editText(arrow, "Hello\nworld!");
mouse.select(arrow);
Keyboard.keyPress(KEYS.ENTER);
editor = getTextEditor(textEditorSelector);
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello\nworld!");
editor.blur();
expect(arrow.version).toEqual(version + 1);
});
+1 -2
View File
@@ -584,7 +584,7 @@ export const textWysiwyg = ({
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", bindBlurEvent);
window.removeEventListener("blur", handleSubmit);
window.removeEventListener("beforeunload", handleSubmit);
unbindUpdate();
editable.remove();
@@ -701,7 +701,6 @@ export const textWysiwyg = ({
passive: false,
capture: true,
});
window.addEventListener("beforeunload", handleSubmit);
excalidrawContainer
?.querySelector(".excalidraw-textEditorContainer")!
.appendChild(editable);
-116
View File
@@ -436,121 +436,5 @@ describe("adding elements to frames", () => {
expect(rect2.frameId).toBe(null);
expectEqualIds([rect2_copy, frame, rect2]);
});
it("random order 01", () => {
const frame1 = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
const frame2 = API.createElement({
type: "frame",
x: 200,
y: 0,
width: 100,
height: 100,
});
const frame3 = API.createElement({
type: "frame",
x: 300,
y: 0,
width: 100,
height: 100,
});
const rectangle1 = API.createElement({
type: "rectangle",
x: 25,
y: 25,
width: 50,
height: 50,
frameId: frame1.id,
});
const rectangle2 = API.createElement({
type: "rectangle",
x: 225,
y: 25,
width: 50,
height: 50,
frameId: frame2.id,
});
const rectangle3 = API.createElement({
type: "rectangle",
x: 325,
y: 25,
width: 50,
height: 50,
frameId: frame3.id,
});
const rectangle4 = API.createElement({
type: "rectangle",
x: 350,
y: 25,
width: 50,
height: 50,
frameId: frame3.id,
});
h.elements = [
frame1,
rectangle4,
rectangle1,
rectangle3,
frame3,
rectangle2,
frame2,
];
API.setSelectedElements([rectangle2]);
const origSize = h.elements.length;
expect(h.elements.length).toBe(origSize);
dragElementIntoFrame(frame3, rectangle2);
expect(h.elements.length).toBe(origSize);
});
it("random order 02", () => {
const frame1 = API.createElement({
type: "frame",
x: 0,
y: 0,
width: 100,
height: 100,
});
const frame2 = API.createElement({
type: "frame",
x: 200,
y: 0,
width: 100,
height: 100,
});
const rectangle1 = API.createElement({
type: "rectangle",
x: 25,
y: 25,
width: 50,
height: 50,
frameId: frame1.id,
});
const rectangle2 = API.createElement({
type: "rectangle",
x: 225,
y: 25,
width: 50,
height: 50,
frameId: frame2.id,
});
h.elements = [rectangle1, rectangle2, frame1, frame2];
API.setSelectedElements([rectangle2]);
expect(h.elements.length).toBe(4);
dragElementIntoFrame(frame2, rectangle1);
expect(h.elements.length).toBe(4);
});
});
});
+65 -139
View File
@@ -14,7 +14,7 @@ import {
getBoundTextElement,
getContainerElement,
} from "./element/textElement";
import { arrayToMap } from "./utils";
import { arrayToMap, findIndex } from "./utils";
import { mutateElement } from "./element/mutateElement";
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene";
@@ -323,24 +323,7 @@ export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
export const getFrameElements = (
allElements: ExcalidrawElementsIncludingDeleted,
frameId: string,
opts?: { includeBoundArrows?: boolean },
) => {
return allElements.filter((element) => {
if (element.frameId === frameId) {
return true;
}
if (opts?.includeBoundArrows && element.type === "arrow") {
const bindingId = element.startBinding?.elementId;
if (bindingId) {
const boundElement = Scene.getScene(element)?.getElement(bindingId);
if (boundElement?.frameId === frameId) {
return true;
}
}
}
return false;
});
};
) => allElements.filter((element) => element.frameId === frameId);
export const getElementsInResizingFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
@@ -468,137 +451,91 @@ export const getContainingFrame = (
return null;
};
export const isValidFrameChild = (element: ExcalidrawElement) => {
return (
element.type !== "frame" &&
// arrows that are bound to elements cannot be frame children
(element.type !== "arrow" || (!element.startBinding && !element.endBinding))
);
};
// --------------------------- Frame Operations -------------------------------
/**
* Retains (or repairs for target frame) the ordering invriant where children
* elements come right before the parent frame:
* [el, el, child, child, frame, el]
*/
export const addElementsToFrame = (
allElements: ExcalidrawElementsIncludingDeleted,
elementsToAdd: NonDeletedExcalidrawElement[],
frame: ExcalidrawFrameElement,
) => {
const { allElementsIndexMap, currTargetFrameChildrenMap } =
allElements.reduce(
(acc, element, index) => {
acc.allElementsIndexMap.set(element.id, index);
if (element.frameId === frame.id) {
acc.currTargetFrameChildrenMap.set(element.id, true);
}
return acc;
},
{
allElementsIndexMap: new Map<ExcalidrawElement["id"], number>(),
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
},
);
const _elementsToAdd: ExcalidrawElement[] = [];
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
const finalElementsToAdd: ExcalidrawElement[] = [];
// - add bound text elements if not already in the array
// - filter out elements that are already in the frame
for (const element of omitGroupsContainingFrames(
allElements,
elementsToAdd,
)) {
if (!currTargetFrameChildrenMap.has(element.id)) {
if (!isValidFrameChild(element)) {
continue;
}
finalElementsToAdd.push(element);
}
for (const element of elementsToAdd) {
_elementsToAdd.push(element);
const boundTextElement = getBoundTextElement(element);
if (
boundTextElement &&
!suppliedElementsToAddSet.has(boundTextElement.id) &&
!currTargetFrameChildrenMap.has(boundTextElement.id)
) {
finalElementsToAdd.push(boundTextElement);
if (boundTextElement) {
_elementsToAdd.push(boundTextElement);
}
}
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
const allElementsIndex = allElements.reduce(
(acc: Record<string, number>, element, index) => {
acc[element.id] = index;
return acc;
},
{},
);
const nextElements: ExcalidrawElement[] = [];
const frameIndex = allElementsIndex[frame.id];
// need to be calculated before the mutation below occurs
const leftFrameBoundaryIndex = findIndex(
allElements,
(e) => e.frameId === frame.id,
);
const processedElements = new Set<ExcalidrawElement["id"]>();
const existingFrameChildren = allElements.filter(
(element) => element.frameId === frame.id,
);
for (const element of allElements) {
if (processedElements.has(element.id)) {
continue;
}
const addedFrameChildren_left: ExcalidrawElement[] = [];
const addedFrameChildren_right: ExcalidrawElement[] = [];
processedElements.add(element.id);
for (const element of omitGroupsContainingFrames(
allElements,
_elementsToAdd,
)) {
if (element.frameId !== frame.id && !isFrameElement(element)) {
if (allElementsIndex[element.id] > frameIndex) {
addedFrameChildren_right.push(element);
} else {
addedFrameChildren_left.push(element);
}
if (
finalElementsToAddSet.has(element.id) ||
(element.frameId && element.frameId === frame.id)
) {
// will be added in bulk once we process target frame
continue;
}
// target frame
if (element.id === frame.id) {
const currFrameChildren = getFrameElements(allElements, frame.id);
currFrameChildren.forEach((child) => {
processedElements.add(child.id);
});
// if not found, add all children on top by assigning the lowest index
const targetFrameIndex = allElementsIndexMap.get(frame.id) ?? -1;
const { newChildren_left, newChildren_right } = finalElementsToAdd.reduce(
(acc, element) => {
// if index not found, add on top of current frame children
const elementIndex = allElementsIndexMap.get(element.id) ?? Infinity;
if (elementIndex < targetFrameIndex) {
acc.newChildren_left.push(element);
} else {
acc.newChildren_right.push(element);
}
return acc;
},
{
newChildren_left: [] as ExcalidrawElement[],
newChildren_right: [] as ExcalidrawElement[],
},
);
nextElements.push(
...newChildren_left,
...currFrameChildren,
...newChildren_right,
mutateElement(
element,
{
frameId: frame.id,
},
false,
);
continue;
}
nextElements.push(element);
}
for (const element of finalElementsToAdd) {
mutateElement(
element,
{
frameId: frame.id,
},
false,
);
}
const frameElement = allElements[frameIndex];
const nextFrameChildren = addedFrameChildren_left
.concat(existingFrameChildren)
.concat(addedFrameChildren_right);
const nextFrameChildrenMap = nextFrameChildren.reduce(
(acc: Record<string, boolean>, element) => {
acc[element.id] = true;
return acc;
},
{},
);
const nextOtherElements_left = allElements
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
.filter((element) => !nextFrameChildrenMap[element.id]);
const nextOtherElement_right = allElements
.slice(frameIndex + 1)
.filter((element) => !nextFrameChildrenMap[element.id]);
const nextElements = nextOtherElements_left
.concat(nextFrameChildren)
.concat([frameElement])
.concat(nextOtherElement_right);
return nextElements;
};
@@ -768,17 +705,6 @@ export const isElementInFrame = (
: element;
if (frame) {
// Perf improvement:
// For an element that's already in a frame, if it's not being dragged
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
// It has to be in its containing frame.
if (
!appState.selectedElementIds[element.id] ||
!appState.selectedElementsAreBeingDragged
) {
return true;
}
if (_element.groupIds.length === 0) {
return elementOverlapsWithFrame(_element, frame);
}
-1
View File
@@ -21,7 +21,6 @@ export const CODES = {
V: "KeyV",
Z: "KeyZ",
R: "KeyR",
S: "KeyS",
} as const;
export const KEYS = {
+80 -129
View File
@@ -50,7 +50,7 @@
"veryLarge": "كبير جدا",
"solid": "كامل",
"hachure": "خطوط",
"zigzag": "متعرج",
"zigzag": "",
"crossHatch": "خطوط متقطعة",
"thin": "نحيف",
"bold": "داكن",
@@ -106,15 +106,11 @@
"increaseFontSize": "تكبير حجم الخط",
"unbindText": "فك ربط النص",
"bindText": "ربط النص بالحاوية",
"createContainerFromText": "نص مغلف في حاوية",
"createContainerFromText": "",
"link": {
"edit": "تعديل الرابط",
"editEmbed": "تحرير الرابط وإدراجه",
"create": "إنشاء رابط",
"createEmbed": "إنشاء رابط و إدراجه",
"label": "رابط",
"labelEmbed": "رابط و إدراج",
"empty": "لم يتم تعيين رابط"
"label": "رابط"
},
"lineEditor": {
"edit": "تحرير السطر",
@@ -128,9 +124,9 @@
},
"statusPublished": "نُشر",
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
"selectAllElementsInFrame": "تحديد جميع العناصر في الإطار",
"removeAllElementsFromFrame": "إزالة جميع العناصر من الإطار",
"eyeDropper": "اختيار اللون من القماش"
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": ""
},
"library": {
"noItems": "لا توجد عناصر أضيفت بعد...",
@@ -164,16 +160,13 @@
"darkMode": "الوضع المظلم",
"lightMode": "الوضع المضيء",
"zenMode": "وضع التأمل",
"objectsSnapMode": "التقط إلى العناصر",
"exitZenMode": "إلغاء الوضع الليلى",
"cancel": "إلغاء",
"clear": "مسح",
"remove": "إزالة",
"embed": "تبديل الإدراج",
"publishLibrary": "انشر",
"submit": "أرسل",
"confirm": "تأكيد",
"embeddableInteractionButton": "اضغط للتفاعل"
"confirm": "تأكيد"
},
"alerts": {
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
@@ -196,28 +189,23 @@
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
"collabOfflineWarning": "لا يوجد اتصال بالانترنت.\nلن يتم حفظ التغييرات التي قمت بها!"
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "نوع الملف غير مدعوم.",
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
"failedToFetchImage": "",
"invalidSVGString": "SVG غير صالح.",
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
"importLibraryError": "تعذر تحميل المكتبة",
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.",
"brave_measure_text_error": {
"line1": "يبدو أنك تستخدم متصفح Brave مع إعداد <bold>حظر صارم لتتبع البصمة</bold>.",
"line2": "قد يؤدي هذا إلى كسر <bold>عناصر النص</bold> في الرسومات الخاصة بك.",
"line3": "من المستحسن إلغاء تفعيل هذا الإعداد. يمكنك اتباع <link>هذه الخطوات</link> لفعل ذلك.",
"line4": "إذا لم يصلح تعطيل هذا الإعداد طريقة عرض النصوص، الرجاء كتابة <issueLink>بلاغ</issueLink> على حسابنا في GitHub، أو راسلنا على <discordLink>Discord</discordLink>"
},
"libraryElementTypeError": {
"embeddable": "لا يمكن إضافة العناصر القابلة للتضمين في المكتبة.",
"image": "سوف يتم دعم إضافة صور إلى المكتبة قريباً!"
"line1": "",
"line2": "",
"line3": "",
"line4": ""
}
},
"toolBar": {
@@ -235,11 +223,9 @@
"penMode": "وضع القلم - امنع اللمس",
"link": "إضافة/تحديث الرابط للشكل المحدد",
"eraser": "ممحاة",
"frame": "أداة الإطار",
"embeddable": "تضمين ويب",
"laser": "مؤشر ليزر",
"hand": "يد (أداة الإزاحة)",
"extraTools": "المزيد من أﻷدوات"
"frame": "",
"hand": "",
"extraTools": ""
},
"headings": {
"canvasActions": "إجراءات اللوحة",
@@ -251,7 +237,6 @@
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
"embeddable": "اضغط مع السحب لإنشاء موقع ويب مضمّن",
"text_selected": "انقر نقراً مزدوجاً أو اضغط ادخال لتعديل النص",
"text_editing": "اضغط على Esc أو (Ctrl أو Cmd) + Enter لإنهاء التعديل",
"linearElementMulti": "انقر فوق النقطة الأخيرة أو اضغط على Esc أو Enter للإنهاء",
@@ -260,15 +245,14 @@
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
"lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة (النِّقَاط)، Ctrl/Cmd+D للتكرار، أو اسحب للانتقال",
"lineEditor_nothingSelected": "اختر نقطة لتعديلها (اضغط على SHIFT لتحديد عدة نِقَاط),\nأو اضغط على ALT و انقر بالفأرة لإضافة نِقَاط جديدة",
"placeImage": "انقر لوضع الصورة، أو انقر واسحب لتعيين حجمها يدوياً",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "نشر مكتبتك",
"bindTextToElement": "اضغط على إدخال لإضافة نص",
"deepBoxSelect": "اضغط على Ctrl\\Cmd للاختيار العميق، ولمنع السحب",
"eraserRevert": "اضغط على Alt لاستعادة العناصر المعلَّمة للحذف",
"firefox_clipboard_write": "يمكن على الأرجح تمكين هذه الميزة عن طريق تعيين علم \"dom.events.asyncClipboard.clipboardItem\" إلى \"true\". لتغيير أعلام المتصفح في Firefox، قم بزيارة صفحة \"about:config\".",
"disableSnapping": "اضغط على Ctrl أو Cmd لتعطيل الالتقاط"
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "تعذر عرض المعاينة",
@@ -276,11 +260,11 @@
"canvasTooBigTip": "نصيحة: حاول تحريك العناصر البعيدة بشكل أقرب قليلاً."
},
"errorSplash": {
"headingMain": "حدث خطأ. حاول <button>تحديث الصفحة</button>.",
"headingMain": "",
"clearCanvasMessage": "إذا لم تعمل إعادة التحميل، حاول مرة أخرى ",
"clearCanvasCaveat": " هذا سيؤدي إلى فقدان العمل ",
"trackedToSentry": "تم تتبع الخطأ في المعرف {{eventId}} على نظامنا.",
"openIssueMessage": "حرصنا على عدم إضافة معلومات المشهد في بلاغ الخطأ. في حال كون مشهدك لا يحمل أي معلومات خاصة نرجو المتابعة على <button>نظام تتبع الأخطاء</button>. نرجو إضافة المعلومات أدناه بنسخها ولصقها في محتوى البلاغ على GitHub.",
"trackedToSentry": "",
"openIssueMessage": "",
"sceneContent": "محتوى المشهد:"
},
"roomDialog": {
@@ -310,16 +294,16 @@
"helpDialog": {
"blog": "اقرأ مدونتنا",
"click": "انقر",
"deepSelect": "تحديد عميق",
"deepBoxSelect": "تحديد عميق داخل المربع، ومنع السحب",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "سهم مائل",
"curvedLine": "خط مائل",
"documentation": "دليل الاستخدام",
"doubleClick": "انقر مرتين",
"drag": "اسحب",
"editor": "المحرر",
"editLineArrowPoints": "تحرير سطر/نقاط سهم",
"editText": "تعديل النص / إضافة تسمية",
"editLineArrowPoints": "",
"editText": "",
"github": "عثرت على مشكلة؟ إرسال",
"howto": "اتبع التعليمات",
"or": "أو",
@@ -332,9 +316,9 @@
"view": "عرض",
"zoomToFit": "تكبير للملائمة",
"zoomToSelection": "تكبير للعنصر المحدد",
"toggleElementLock": "إغلاق/فتح المحدد",
"movePageUpDown": "نقل الصفحة أعلى/أسفل",
"movePageLeftRight": "نقل الصفحة يسار/يمين"
"toggleElementLock": "",
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": "مسح اللوحة"
@@ -352,20 +336,20 @@
"authorName": "اسمك أو اسم المستخدم",
"libraryName": "اسم مكتبتك",
"libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
"githubHandle": "معالج GitHub (اختياري)، حتى تتمكن من تحرير المكتبة عند إرسالها للمراجعة",
"twitterHandle": "اسم مستخدم تويتر (اختياري)، حتى نعرف من الذي سيتم الإشارة إليه عند الترويج عبر تويتر",
"website": "رابط إلى موقعك الشخصي أو في مكان آخر (اختياري)"
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "مطلوب",
"website": "أدخل عنوان URL صالح"
},
"noteDescription": "تقديم مكتبتك لتضمينها في مستودع المكتبة العامة <link></link> لأشخاص آخرين لاستخدامها في رسومهم.",
"noteGuidelines": "تحتاج المكتبة إلى الموافقة أولا. يرجى قراءة <link>المعايير</link> قبل تقديمها. سوف تحتاج إلى حساب GitHub للتواصل وإجراء التغييرات عند الطلب، ولكن ليس مطلوبا بشكل صارم.",
"noteLicense": "تقديمك يعني موافقتك على نشر المكتبة المقدمة تحت <link>MIT ترخيص</link>، ما يعني أن لأي أحد الحق في استخدامها دون قيود.",
"noteDescription": "",
"noteGuidelines": "",
"noteLicense": "",
"noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
"atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء",
"republishWarning": "ملاحظة: بعض العناصر المحددة معينة على أنه نشرها أو تقديمها من قبل. يجب عليك فقط إعادة إرسال العناصر عند تحديث مكتبة موجودة أو إرسالها."
"republishWarning": ""
},
"publishSuccessDialog": {
"title": "تم إرسال المكتبة",
@@ -376,27 +360,27 @@
"removeItemsFromLib": "إزالة العناصر المحددة من المكتبة"
},
"imageExportDialog": {
"header": "تصدير الصورة",
"header": "",
"label": {
"withBackground": "الخلفية",
"onlySelected": "المحدد فقط",
"darkMode": "الوضع الداكن",
"embedScene": "تضمين المشهد",
"scale": "الحجم",
"padding": "الهوامش"
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
"scale": "",
"padding": ""
},
"tooltip": {
"embedScene": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر."
"embedScene": ""
},
"title": {
"exportToPng": "تصدير بصيغة PNG",
"exportToSvg": "تصدير بصيغة SVG",
"copyPngToClipboard": "نسخ الـ PNG إلى الحافظة"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
},
"button": {
"exportToPng": "PNG",
"exportToSvg": "SVG",
"copyPngToClipboard": "نسخ إلى الحافظة"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
}
},
"encrypted": {
@@ -427,76 +411,43 @@
"fileSavedToFilename": "حفظ باسم {filename}",
"canvas": "لوحة الرسم",
"selection": "العنصر المحدد",
"pasteAsSingleElement": "استخدم {{shortcut}} للصق كعنصر واحد،\nأو لصق في محرر نص موجود",
"unableToEmbed": "تضمين هذا الرابط غير مسموح حاليًا. افتح بلاغاً على GitHub لطلب عنوان Url القائمة البيضاء",
"unrecognizedLinkFormat": "الرابط الذي ضمنته لا يتطابق مع التنسيق المتوقع. الرجاء محاولة لصق النص 'المضمن' المُزوَد من موقع المصدر"
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "شفاف",
"black": "أسود",
"white": "أبيض",
"red": "أحمر",
"pink": "وردي",
"grape": "عنبي",
"violet": "بنفسجي",
"gray": "رمادي",
"blue": "أزرق",
"cyan": "سماوي",
"teal": "أزرق مخضر",
"green": "أخضر",
"yellow": "أصفر",
"orange": "برتقالي",
"bronze": "برونزي"
"black": "",
"white": "",
"red": "",
"pink": "",
"grape": "",
"violet": "",
"gray": "",
"blue": "",
"cyan": "",
"teal": "",
"green": "",
"yellow": "",
"orange": "",
"bronze": ""
},
"welcomeScreen": {
"app": {
"center_heading": "جميع بياناتك محفوظة محليا في المتصفح الخاص بك.",
"center_heading_plus": "هل تريد الذهاب إلى Excalidraw+ بدلاً من ذلك؟",
"menuHint": "التصدير والتفضيلات واللغات ..."
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "التصدير والتفضيلات وغيرها...",
"center_heading": "الرسم البياني التصويري. بشكل مبسط.",
"toolbarHint": "اختر أداة و ابدأ الرسم!",
"helpHint": "الاختصارات و المساعدة"
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
},
"colorPicker": {
"mostUsedCustomColors": "الألوان المخصصة الأكثر استخداما",
"colors": "الألوان",
"shades": "الدرجات",
"hexCode": "رمز Hex",
"noShades": "لا تتوفر درجات لهذا اللون"
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "تصدير كصورة",
"button": "تصدير كصورة",
"description": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقاً."
},
"saveToDisk": {
"title": "حفظ الملف للجهاز",
"button": "حفظ الملف للجهاز",
"description": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقاً."
},
"excalidrawPlus": {
"title": "Excalidraw+",
"button": "تصدير إلى Excalidraw+",
"description": "حفظ المشهد إلى مساحة العمل +Excalidraw الخاصة بك."
}
},
"modal": {
"loadFromFile": {
"title": "تحميل من ملف",
"button": "تحميل من ملف",
"description": "سيتم التحميل من الملف <bold>استبدال المحتوى الموجود</bold>.<br></br>يمكنك النسخ الاحتياطي لرسمك أولاً باستخدام أحد الخيارات أدناه."
},
"shareableLink": {
"title": "تحميل من رابط",
"button": "استبدال محتواي",
"description": "سيتسبب تحميل رسمة خارجية <bold>باستبدال محتواك الموجود حالياً</bold>.<br></br>بإمكانك إجراء النسخ الاحتياطي لرسمتك الحالية باستخدام أحد الخيارات أدناه."
}
}
"mostUsedCustomColors": "",
"colors": "",
"shades": "",
"hexCode": "",
"noShades": ""
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "",
"link": {
"edit": "",
"editEmbed": "",
"create": "",
"createEmbed": "",
"label": "",
"labelEmbed": "",
"empty": ""
"label": ""
},
"lineEditor": {
"edit": "",
@@ -164,16 +160,13 @@
"darkMode": "",
"lightMode": "",
"zenMode": "",
"objectsSnapMode": "",
"exitZenMode": "",
"cancel": "",
"clear": "",
"remove": "",
"embed": "",
"publishLibrary": "",
"submit": "",
"confirm": "",
"embeddableInteractionButton": ""
"confirm": ""
},
"alerts": {
"clearReset": "",
@@ -203,7 +196,6 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"failedToFetchImage": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": "",
@@ -214,10 +206,6 @@
"line2": "",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "",
"eraser": "",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "",
"extraTools": ""
},
@@ -251,7 +237,6 @@
"linearElement": "",
"freeDraw": "",
"text": "",
"embeddable": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
@@ -267,8 +252,7 @@
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": "",
"disableSnapping": ""
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "",
"canvas": "",
"selection": "",
"pasteAsSingleElement": "",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "",
@@ -467,36 +449,5 @@
"shades": "",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "",
"button": "",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "",
"button": "",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}
+96 -145
View File
@@ -1,7 +1,7 @@
{
"labels": {
"paste": "Постави",
"pasteAsPlaintext": "Постави като обикновен текст",
"pasteAsPlaintext": "",
"pasteCharts": "Постави графики",
"selectAll": "Маркирай всичко",
"multiSelect": "Добави елемент към селекция",
@@ -50,7 +50,7 @@
"veryLarge": "Много голям",
"solid": "Солиден",
"hachure": "Хералдика",
"zigzag": "Зигзаг",
"zigzag": "",
"crossHatch": "Двойно-пресечено",
"thin": "Тънък",
"bold": "Ясно очертан",
@@ -63,7 +63,7 @@
"cartoonist": "Карикатурист",
"fileTitle": "Име на файл",
"colorPicker": "Избор на цвят",
"canvasColors": "Използван на платно",
"canvasColors": "",
"canvasBackground": "Фон на платно",
"drawingCanvas": "Платно за рисуване",
"layers": "Слоеве",
@@ -99,41 +99,37 @@
"share": "Сподели",
"showStroke": "",
"showBackground": "",
"toggleTheme": "Включи тема",
"personalLib": "Лична Библиотека",
"excalidrawLib": "Excalidraw Библиотека",
"decreaseFontSize": "Намали размера на шрифта",
"increaseFontSize": "Увеличи размера на шрифта",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"bindText": "",
"createContainerFromText": "",
"link": {
"edit": "Редактирай линк",
"editEmbed": "",
"edit": "",
"create": "",
"createEmbed": "",
"label": "Линк",
"labelEmbed": "",
"empty": ""
"label": ""
},
"lineEditor": {
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "Заключи",
"unlock": "Отключи",
"lockAll": "Заключи всички",
"unlockAll": "Отключи всички"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Публикувани",
"statusPublished": "",
"sidebarLock": "",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "Избери цвят от платното"
"eyeDropper": ""
},
"library": {
"noItems": "Няма добавени неща все още...",
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
@@ -141,11 +137,11 @@
"clearReset": "Нулиране на платно",
"exportJSON": "",
"exportImage": "",
"export": "Запази на...",
"export": "",
"copyToClipboard": "Копиране в клипборда",
"save": "Запази към текущ файл",
"save": "",
"saveAs": "Запиши като",
"load": "Отвори",
"load": "",
"getShareableLink": "Получаване на връзка за споделяне",
"close": "Затвори",
"selectLanguage": "Избор на език",
@@ -164,16 +160,13 @@
"darkMode": "Тъмен режим",
"lightMode": "Светъл режим",
"zenMode": "Режим Zen",
"objectsSnapMode": "",
"exitZenMode": "Спиране на Zen режим",
"cancel": "Отмени",
"clear": "Изчисти",
"remove": "Премахване",
"embed": "",
"publishLibrary": "Публикувай",
"submit": "Изпрати",
"confirm": "Потвърждаване",
"embeddableInteractionButton": ""
"confirm": "Потвърждаване"
},
"alerts": {
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
@@ -182,42 +175,37 @@
"couldNotLoadInvalidFile": "Невалиден файл не може да се зареди",
"importBackendFailed": "Импортирането от бекенд не беше успешно.",
"cannotExportEmptyCanvas": "Не може да се експортира празно платно.",
"couldNotCopyToClipboard": "Не можем да копираме в клипбоарда.",
"couldNotCopyToClipboard": "",
"decryptFailed": "Данните не можаха да се дешифрират.",
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
"errorAddingToLibrary": "Не можем да заредим от библиотеката",
"errorRemovingFromLibrary": "Не можем да премахнем елемент от библиотеката",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "Изтрий {{count}} елемент(а) от библиотеката?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
},
"errors": {
"unsupportedFileType": "Този файлов формат не се поддържа.",
"imageInsertError": "",
"fileTooBig": "Файлът е твърде голям. Максималния допустим размер е {{maxSize}}.",
"fileTooBig": "",
"svgImageInsertError": "",
"failedToFetchImage": "",
"invalidSVGString": "Невалиден SVG.",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": "Не можем да заредим библиотеката",
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"line1": "",
"line2": "",
"line3": "Силно препоръчваме да изключите тази настройка. Можете да следвате <link>тези стъпки</link> за това как да го направите.",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -234,12 +222,10 @@
"lock": "Поддържайте избрания инструмент активен след рисуване",
"penMode": "",
"link": "",
"eraser": "Гума",
"eraser": "",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "",
"extraTools": "Още инструменти"
"extraTools": ""
},
"headings": {
"canvasActions": "Действия по платното",
@@ -251,7 +237,6 @@
"linearElement": "Кликнете, за да стартирате няколко точки, плъзнете за една линия",
"freeDraw": "Натиснете и влачете, пуснете като сте готови",
"text": "Подсказка: Можете също да добавите текст като натиснете някъде два път с инструмента за селекция",
"embeddable": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "Кликнете върху последната точка или натиснете Escape или Enter, за да завършите",
@@ -267,8 +252,7 @@
"bindTextToElement": "Натиснете Enter, за да добавите",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": "",
"disableSnapping": ""
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "Невъзможност за показване на preview",
@@ -304,7 +288,7 @@
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "Експорт",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
},
"helpDialog": {
@@ -315,7 +299,7 @@
"curvedArrow": "Извита стрелка",
"curvedLine": "Извита линия",
"documentation": "Документация",
"doubleClick": "двойно-щракване",
"doubleClick": "",
"drag": "плъзнете",
"editor": "Редактор",
"editLineArrowPoints": "",
@@ -324,41 +308,41 @@
"howto": "Следвайте нашите ръководства",
"or": "или",
"preventBinding": "Спри прилепяне на стрелките",
"tools": "Инструменти",
"tools": "",
"shortcuts": "Клавиши за бърз достъп",
"textFinish": "Завърши редактиране (текстов редактор)",
"textNewLine": "Добави нова линия (текстов редактор)",
"textFinish": "",
"textNewLine": "",
"title": "Помощ",
"view": "Преглед",
"zoomToFit": "Приближи докато се виждат всички елементи",
"zoomToSelection": "Приближи селекцията",
"toggleElementLock": "Заключи/Отключи селекция",
"movePageUpDown": "Премести страница нагоре/надолу",
"movePageLeftRight": "Премести страница наляво/надясно"
"toggleElementLock": "",
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": "Изчисти платното"
"title": ""
},
"publishDialog": {
"title": "Публикувай библиотека",
"itemName": "Име",
"authorName": "Авторско име",
"githubUsername": "GitHub потребителско име",
"twitterUsername": "Twitter потребителско име",
"libraryName": "Име на библиотеката",
"libraryDesc": "Описание на библиотеката",
"website": "Уебсайт",
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "Името или потребителското Ви име",
"libraryName": "Име на библиотеката Ви",
"libraryDesc": "Описание на библиотеката ви, за да помогнете на хората да разберат приложенията ѝ",
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "Задължително",
"website": "Въведете валиден URL адрес"
"required": "",
"website": ""
},
"noteDescription": "",
"noteGuidelines": "",
@@ -372,15 +356,15 @@
"content": ""
},
"confirmDialog": {
"resetLibrary": "Нулирай библиотека",
"resetLibrary": "",
"removeItemsFromLib": ""
},
"imageExportDialog": {
"header": "",
"label": {
"withBackground": "Фон",
"onlySelected": "Само избраното",
"darkMode": "Тъмен режим",
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
"scale": "",
"padding": ""
@@ -389,14 +373,14 @@
"embedScene": ""
},
"title": {
"exportToPng": "Изнасяне в PNG",
"exportToSvg": "Изнасяне в SVG",
"copyPngToClipboard": "Копирай PNG в клипборда"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
},
"button": {
"exportToPng": "PNG",
"exportToSvg": "SVG",
"copyPngToClipboard": "Копиране в клипборда"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
}
},
"encrypted": {
@@ -419,84 +403,51 @@
"width": "Широчина"
},
"toast": {
"addedToLibrary": "Добавена към библиотеката",
"addedToLibrary": "",
"copyStyles": "Копирани стилове.",
"copyToClipboard": "Копирано в клипборда.",
"copyToClipboardAsPng": "Копира {{exportSelection}} в клипборда като PNG\n({{exportColorScheme}})",
"fileSaved": "Файлът е запазен.",
"fileSavedToFilename": "Запазен към {filename}",
"canvas": "платно",
"selection": "селекция",
"pasteAsSingleElement": "",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": "",
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "Прозрачен",
"black": "Черен",
"white": "Бял",
"red": "Червен",
"pink": "Розов",
"grape": "Грозде",
"violet": "Виолетово",
"gray": "Сив",
"blue": "Син",
"cyan": "Синьозелено",
"teal": "Тъмно синьо-зелено",
"green": "Зелено",
"yellow": "Жълто",
"orange": "Оранжево",
"bronze": "Бронзово"
"transparent": "",
"black": "",
"white": "",
"red": "",
"pink": "",
"grape": "",
"violet": "",
"gray": "",
"blue": "",
"cyan": "",
"teal": "",
"green": "",
"yellow": "",
"orange": "",
"bronze": ""
},
"welcomeScreen": {
"app": {
"center_heading": "Всичките Ви данни са запазени локално в браузъра Ви.",
"center_heading": "",
"center_heading_plus": "",
"menuHint": "Експорт, предпочитания, езици, ..."
"menuHint": ""
},
"defaults": {
"menuHint": "Експорт, предпочитания, и още...",
"center_heading": "Диаграми. Направени. Просто.",
"toolbarHint": "Изберете инструмент & Започнете да рисувате!",
"helpHint": "Преки пътища & помощ"
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
},
"colorPicker": {
"mostUsedCustomColors": "Най-често използвани цветове",
"colors": "Цветове",
"shades": "Нюанси",
"hexCode": "Шестнадесетичен код",
"mostUsedCustomColors": "",
"colors": "",
"shades": "",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "Изнеси като изображение",
"button": "Изнеси като изображение",
"description": ""
},
"saveToDisk": {
"title": "Запази към диск",
"button": "Запази към диск",
"description": ""
},
"excalidrawPlus": {
"title": "Excalidraw+",
"button": "Експортирай към Excalidraw+",
"description": "Запази сцената към Excalidraw+ работното място."
}
},
"modal": {
"loadFromFile": {
"title": "Зареди от файл",
"button": "Зареди от файл",
"description": ""
},
"shareableLink": {
"title": "Зареди от линк",
"button": "Замени моето съдържание",
"description": ""
}
}
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "",
"link": {
"edit": "লিঙ্ক সংশোধন",
"editEmbed": "",
"create": "লিঙ্ক তৈরী",
"createEmbed": "",
"label": "লিঙ্ক নামকরণ",
"labelEmbed": "",
"empty": ""
"label": "লিঙ্ক নামকরণ"
},
"lineEditor": {
"edit": "",
@@ -164,16 +160,13 @@
"darkMode": "ডার্ক মোড",
"lightMode": "লাইট মোড",
"zenMode": "জেন মোড",
"objectsSnapMode": "",
"exitZenMode": "জেন মোড বন্ধ করুন",
"cancel": "বাতিল",
"clear": "সাফ",
"remove": "বিয়োগ",
"embed": "",
"publishLibrary": "সংগ্রহ প্রকাশ করুন",
"submit": "জমা করুন",
"confirm": "নিশ্চিত করুন",
"embeddableInteractionButton": ""
"confirm": "নিশ্চিত করুন"
},
"alerts": {
"clearReset": "এটি পুরো ক্যানভাস সাফ করবে। আপনি কি নিশ্চিত?",
@@ -203,7 +196,6 @@
"imageInsertError": "ছবি সন্নিবেশ করা যায়নি। পরে আবার চেষ্টা করুন...",
"fileTooBig": "ফাইলটি খুব বড়। সর্বাধিক অনুমোদিত আকার হল {{maxSize}}৷",
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
"failedToFetchImage": "",
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
"importLibraryError": "সংগ্রহ লোড করা যায়নি",
@@ -214,10 +206,6 @@
"line2": "",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
"eraser": "ঝাড়ন",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "",
"extraTools": ""
},
@@ -251,7 +237,6 @@
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
"embeddable": "",
"text_selected": "লেখা সম্পাদনা করতে ডাবল-ক্লিক করুন বা এন্টার টিপুন",
"text_editing": "লেখা সম্পাদনা শেষ করতে এসকেপ বা কন্ট্রোল/কম্যান্ড যোগে এন্টার টিপুন",
"linearElementMulti": "শেষ বিন্দুতে ক্লিক করুন অথবা শেষ করতে এসকেপ বা এন্টার টিপুন",
@@ -267,8 +252,7 @@
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
"deepBoxSelect": "",
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
"firefox_clipboard_write": "",
"disableSnapping": ""
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "",
"canvas": "",
"selection": "বাছাই",
"pasteAsSingleElement": "",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "",
@@ -467,36 +449,5 @@
"shades": "",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "",
"button": "",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "",
"button": "",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "",
"link": {
"edit": "Edita l'enllaç",
"editEmbed": "",
"create": "Crea un enllaç",
"createEmbed": "",
"label": "Enllaç",
"labelEmbed": "",
"empty": ""
"label": "Enllaç"
},
"lineEditor": {
"edit": "Editar línia",
@@ -164,16 +160,13 @@
"darkMode": "Mode fosc",
"lightMode": "Mode clar",
"zenMode": "Mode zen",
"objectsSnapMode": "",
"exitZenMode": "Surt de mode zen",
"cancel": "Cancel·la",
"clear": "Neteja",
"remove": "Suprimeix",
"embed": "",
"publishLibrary": "Publica",
"submit": "Envia",
"confirm": "Confirma",
"embeddableInteractionButton": ""
"confirm": "Confirma"
},
"alerts": {
"clearReset": "S'esborrarà tot el llenç. N'esteu segur?",
@@ -203,7 +196,6 @@
"imageInsertError": "No s'ha pogut insertar la imatge, torneu-ho a provar més tard...",
"fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
"failedToFetchImage": "",
"invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
"importLibraryError": "No s'ha pogut carregar la biblioteca",
@@ -214,10 +206,6 @@
"line2": "",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
"eraser": "Esborrador",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "Mà (eina de desplaçament)",
"extraTools": ""
},
@@ -251,7 +237,6 @@
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
"embeddable": "",
"text_selected": "Feu doble clic o premeu Retorn per a editar el text",
"text_editing": "Premeu Escapada o Ctrl+Retorn (o Ordre+Retorn) per a finalitzar l'edició",
"linearElementMulti": "Feu clic a l'ultim punt, o pitgeu Esc o Retorn per a finalitzar",
@@ -267,8 +252,7 @@
"bindTextToElement": "Premeu enter per a afegir-hi text",
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar",
"firefox_clipboard_write": "És probable que aquesta funció es pugui activar posant la marca \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Per canviar les marques del navegador al Firefox, visiteu la pàgina \"about:config\".",
"disableSnapping": ""
"firefox_clipboard_write": "És probable que aquesta funció es pugui activar posant la marca \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Per canviar les marques del navegador al Firefox, visiteu la pàgina \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "S'ha desat a {filename}",
"canvas": "el llenç",
"selection": "la selecció",
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent"
},
"colors": {
"transparent": "Transparent",
@@ -467,36 +449,5 @@
"shades": "",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "",
"button": "",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "",
"button": "",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "Zabalit text do kontejneru",
"link": {
"edit": "Upravit odkaz",
"editEmbed": "",
"create": "Vytvořit odkaz",
"createEmbed": "",
"label": "Odkaz",
"labelEmbed": "",
"empty": ""
"label": "Odkaz"
},
"lineEditor": {
"edit": "Upravit čáru",
@@ -164,16 +160,13 @@
"darkMode": "Tmavý režim",
"lightMode": "Světlý režim",
"zenMode": "Zen mód",
"objectsSnapMode": "",
"exitZenMode": "Opustit zen mód",
"cancel": "Zrušit",
"clear": "Vyčistit",
"remove": "Odstranit",
"embed": "",
"publishLibrary": "Zveřejnit",
"submit": "Odeslat",
"confirm": "Potvrdit",
"embeddableInteractionButton": ""
"confirm": "Potvrdit"
},
"alerts": {
"clearReset": "Toto vymaže celé plátno. Jste si jisti?",
@@ -203,7 +196,6 @@
"imageInsertError": "Nelze vložit obrázek. Zkuste to později...",
"fileTooBig": "Soubor je příliš velký. Maximální povolená velikost je {{maxSize}}.",
"svgImageInsertError": "Nelze vložit SVG obrázek. Značení SVG je neplatné.",
"failedToFetchImage": "",
"invalidSVGString": "Neplatný SVG.",
"cannotResolveCollabServer": "Nelze se připojit ke sdílenému serveru. Prosím obnovte stránku a zkuste to znovu.",
"importLibraryError": "Nelze načíst knihovnu",
@@ -214,10 +206,6 @@
"line2": "To by mohlo vést k narušení <bold>Textových elementů</bold> ve vašich výkresech.",
"line3": "Důrazně doporučujeme zakázat toto nastavení. Můžete sledovat <link>tyto kroky</link> jak to udělat.",
"line4": "Pokud vypnutí tohoto nastavení neopravuje zobrazení textových prvků, prosím, otevřete <issueLink>problém</issueLink> na našem GitHubu, nebo nám napište na <discordLink>Discord</discordLink>"
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
"eraser": "Guma",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "Ruka (nástroj pro posouvání)",
"extraTools": ""
},
@@ -251,7 +237,6 @@
"linearElement": "Kliknutím pro více bodů, táhnutím pro jednu čáru",
"freeDraw": "Klikněte a táhněte, pro ukončení pusťte",
"text": "Tip: Text můžete také přidat dvojitým kliknutím kdekoli pomocí nástroje pro výběr",
"embeddable": "",
"text_selected": "Dvojklikem nebo stisknutím klávesy ENTER upravíte text",
"text_editing": "Stiskněte Escape nebo Ctrl/Cmd+ENTER pro dokončení úprav",
"linearElementMulti": "Klikněte na poslední bod nebo stiskněte Escape anebo Enter pro dokončení",
@@ -267,8 +252,7 @@
"bindTextToElement": "Stiskněte Enter pro přidání textu",
"deepBoxSelect": "Podržte Ctrl/Cmd pro hluboký výběr a pro zabránění táhnutí",
"eraserRevert": "Podržením klávesy Alt vrátíte prvky označené pro smazání",
"firefox_clipboard_write": "Tato funkce může být povolena nastavením vlajky \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Chcete-li změnit vlajky prohlížeče ve Firefoxu, navštivte stránku \"about:config\".",
"disableSnapping": ""
"firefox_clipboard_write": "Tato funkce může být povolena nastavením vlajky \"dom.events.asyncClipboard.clipboardItem\" na \"true\". Chcete-li změnit vlajky prohlížeče ve Firefoxu, navštivte stránku \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Náhled nelze zobrazit",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "Uloženo do {filename}",
"canvas": "plátno",
"selection": "výběr",
"pasteAsSingleElement": "Pomocí {{shortcut}} vložte jako jeden prvek,\nnebo vložte do existujícího textového editoru",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": "Pomocí {{shortcut}} vložte jako jeden prvek,\nnebo vložte do existujícího textového editoru"
},
"colors": {
"transparent": "Průhledná",
@@ -467,36 +449,5 @@
"shades": "Stíny",
"hexCode": "Hex kód",
"noShades": "Pro tuto barvu nejsou k dispozici žádné odstíny"
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "",
"button": "",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "",
"button": "",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}
+8 -57
View File
@@ -1,7 +1,7 @@
{
"labels": {
"paste": "Indsæt",
"pasteAsPlaintext": "Indsæt som klartekst",
"pasteAsPlaintext": "",
"pasteCharts": "Indsæt diagrammer",
"selectAll": "Marker alle",
"multiSelect": "Tilføj element til markering",
@@ -50,7 +50,7 @@
"veryLarge": "Meget stor",
"solid": "Solid",
"hachure": "Skravering",
"zigzag": "Zigzag",
"zigzag": "",
"crossHatch": "Krydsskravering",
"thin": "Tynd",
"bold": "Fed",
@@ -69,8 +69,8 @@
"layers": "Lag",
"actions": "Handlinger",
"language": "Sprog",
"liveCollaboration": "Live samarbejde...",
"duplicateSelection": "Duplikér",
"liveCollaboration": "",
"duplicateSelection": "",
"untitled": "Unavngivet",
"name": "Navn",
"yourName": "Dit navn",
@@ -109,12 +109,8 @@
"createContainerFromText": "",
"link": {
"edit": "",
"editEmbed": "",
"create": "",
"createEmbed": "",
"label": "",
"labelEmbed": "",
"empty": ""
"label": ""
},
"lineEditor": {
"edit": "",
@@ -164,16 +160,13 @@
"darkMode": "Mørk tilstand",
"lightMode": "Lys baggrund",
"zenMode": "Zentilstand",
"objectsSnapMode": "",
"exitZenMode": "Stop zentilstand",
"cancel": "Annuller",
"clear": "Ryd",
"remove": "Fjern",
"embed": "",
"publishLibrary": "Publicér",
"submit": "Gem",
"confirm": "Bekræft",
"embeddableInteractionButton": ""
"confirm": "Bekræft"
},
"alerts": {
"clearReset": "Dette vil rydde hele lærredet. Er du sikker?",
@@ -203,7 +196,6 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"failedToFetchImage": "",
"invalidSVGString": "",
"cannotResolveCollabServer": "",
"importLibraryError": "",
@@ -214,10 +206,6 @@
"line2": "",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "",
"eraser": "",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "",
"extraTools": ""
},
@@ -251,7 +237,6 @@
"linearElement": "",
"freeDraw": "Klik og træk, slip når du er færdig",
"text": "",
"embeddable": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
@@ -267,8 +252,7 @@
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": "",
"disableSnapping": ""
"firefox_clipboard_write": ""
},
"canvasError": {
"cannotShowPreview": "",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "Gemt som {filename}",
"canvas": "canvas",
"selection": "markering",
"pasteAsSingleElement": "",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": ""
},
"colors": {
"transparent": "",
@@ -467,36 +449,5 @@
"shades": "",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "",
"button": "",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "",
"button": "",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "Text in Container einbetten",
"link": {
"edit": "Link bearbeiten",
"editEmbed": "Link bearbeiten & einbetten",
"create": "Link erstellen",
"createEmbed": "Link erstellen & einbetten",
"label": "Link",
"labelEmbed": "Verlinken & einbetten",
"empty": "Kein Link festgelegt"
"label": "Link"
},
"lineEditor": {
"edit": "Linie bearbeiten",
@@ -164,16 +160,13 @@
"darkMode": "Dunkles Design",
"lightMode": "Helles Design",
"zenMode": "Zen-Modus",
"objectsSnapMode": "Einrasten an Objekten",
"exitZenMode": "Zen-Modus verlassen",
"cancel": "Abbrechen",
"clear": "Löschen",
"remove": "Entfernen",
"embed": "Einbettung umschalten",
"publishLibrary": "Veröffentlichen",
"submit": "Absenden",
"confirm": "Bestätigen",
"embeddableInteractionButton": "Klicken, um zu interagieren"
"confirm": "Bestätigen"
},
"alerts": {
"clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?",
@@ -203,7 +196,6 @@
"imageInsertError": "Das Bild konnte nicht eingefügt werden. Versuche es später erneut...",
"fileTooBig": "Die Datei ist zu groß. Die maximal zulässige Größe ist {{maxSize}}.",
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
"failedToFetchImage": "Bild konnte nicht abgerufen werden.",
"invalidSVGString": "Ungültige SVG.",
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.",
"importLibraryError": "Bibliothek konnte nicht geladen werden",
@@ -214,10 +206,6 @@
"line2": "Dies könnte dazu führen, dass die <bold>Textelemente</bold> in Ihren Zeichnungen zerstört werden.",
"line3": "Wir empfehlen dringend, diese Einstellung zu deaktivieren. Dazu kannst Du <link>diesen Schritten</link> folgen.",
"line4": "Wenn die Deaktivierung dieser Einstellung die fehlerhafte Anzeige von Textelementen nicht behebt, öffne bitte ein <issueLink>Ticket</issueLink> auf unserem GitHub oder schreibe uns auf <discordLink>Discord</discordLink>"
},
"libraryElementTypeError": {
"embeddable": "Einbettbare Elemente können der Bibliothek nicht hinzugefügt werden.",
"image": "Unterstützung für das Hinzufügen von Bildern in die Bibliothek kommt bald!"
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
"eraser": "Radierer",
"frame": "Rahmenwerkzeug",
"embeddable": "Web-Einbettung",
"laser": "Laserpointer",
"hand": "Hand (Schwenkwerkzeug)",
"extraTools": "Weitere Werkzeuge"
},
@@ -251,7 +237,6 @@
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
"text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst",
"embeddable": "Klicken und ziehen, um eine Webseiten-Einbettung zu erstellen",
"text_selected": "Doppelklicken oder Eingabetaste drücken, um Text zu bearbeiten",
"text_editing": "Drücke Escape oder CtrlOrCmd+Eingabetaste, um die Bearbeitung abzuschließen",
"linearElementMulti": "Zum Beenden auf den letzten Punkt klicken oder Escape oder Eingabe drücken",
@@ -267,8 +252,7 @@
"bindTextToElement": "Zum Hinzufügen Eingabetaste drücken",
"deepBoxSelect": "Halte CtrlOrCmd gedrückt, um innerhalb der Gruppe auszuwählen, und um Ziehen zu vermeiden",
"eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen",
"firefox_clipboard_write": "Diese Funktion kann wahrscheinlich aktiviert werden, indem die Einstellung \"dom.events.asyncClipboard.clipboardItem\" auf \"true\" gesetzt wird. Um die Browsereinstellungen in Firefox zu ändern, besuche die Seite \"about:config\".",
"disableSnapping": "Halte CtrlOrCmd gedrückt, um das Einrasten zu deaktivieren"
"firefox_clipboard_write": "Diese Funktion kann wahrscheinlich aktiviert werden, indem die Einstellung \"dom.events.asyncClipboard.clipboardItem\" auf \"true\" gesetzt wird. Um die Browsereinstellungen in Firefox zu ändern, besuche die Seite \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "Als {filename} gespeichert",
"canvas": "Zeichenfläche",
"selection": "Auswahl",
"pasteAsSingleElement": "Verwende {{shortcut}} , um als einzelnes Element\neinzufügen oder in einen existierenden Texteditor einzufügen",
"unableToEmbed": "Einbetten dieser URL ist derzeit nicht zulässig. Erstelle einen Issue auf GitHub, um die URL freigeben zu lassen",
"unrecognizedLinkFormat": "Der Link, den Du eingebettet hast, stimmt nicht mit dem erwarteten Format überein. Bitte versuche den 'embed' String einzufügen, der von der Quellseite zur Verfügung gestellt wird"
"pasteAsSingleElement": "Verwende {{shortcut}} , um als einzelnes Element\neinzufügen oder in einen existierenden Texteditor einzufügen"
},
"colors": {
"transparent": "Transparent",
@@ -467,36 +449,5 @@
"shades": "Schattierungen",
"hexCode": "Hex-Code",
"noShades": "Keine Schattierungen für diese Farbe verfügbar"
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "Als Bild exportieren",
"button": "Als Bild exportieren",
"description": "Exportiere die Zeichnungsdaten als ein Bild, von dem Du später importieren kannst."
},
"saveToDisk": {
"title": "Auf Festplatte speichern",
"button": "Auf Festplatte speichern",
"description": "Exportiere die Zeichnungsdaten in eine Datei, von der Du später importieren kannst."
},
"excalidrawPlus": {
"title": "Excalidraw+",
"button": "Export nach Excalidraw+",
"description": "Speichere die Szene in deinem Excalidraw+-Arbeitsbereich."
}
},
"modal": {
"loadFromFile": {
"title": "Aus Datei laden",
"button": "Aus Datei laden",
"description": "Das Laden aus einer Datei wird <bold>Deinen vorhandenen Inhalt ersetzen</bold>.<br></br>Du kannst Deine Zeichnung zuerst mit einer der folgenden Optionen sichern."
},
"shareableLink": {
"title": "Aus Link laden",
"button": "Meinen Inhalt ersetzen",
"description": "Das Laden einer externen Zeichnung wird <bold>Deinen vorhandenen Inhalt ersetzen</bold>.<br></br>Du kannst Deine Zeichnung zuerst mit einer der folgenden Optionen sichern."
}
}
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "",
"link": {
"edit": "Επεξεργασία συνδέσμου",
"editEmbed": "",
"create": "Δημιουργία συνδέσμου",
"createEmbed": "",
"label": "Σύνδεσμος",
"labelEmbed": "",
"empty": ""
"label": "Σύνδεσμος"
},
"lineEditor": {
"edit": "Επεξεργασία γραμμής",
@@ -164,16 +160,13 @@
"darkMode": "Σκοτεινή λειτουργία",
"lightMode": "Φωτεινή λειτουργία",
"zenMode": "Λειτουργία Zεν",
"objectsSnapMode": "",
"exitZenMode": "Έξοδος από την λειτουργία Zen",
"cancel": "Ακύρωση",
"clear": "Καθαρισμός",
"remove": "Κατάργηση",
"embed": "",
"publishLibrary": "Δημοσίευση",
"submit": "Υποβολή",
"confirm": "Επιβεβαίωση",
"embeddableInteractionButton": ""
"confirm": "Επιβεβαίωση"
},
"alerts": {
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
@@ -203,7 +196,6 @@
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
"failedToFetchImage": "",
"invalidSVGString": "Μη έγκυρο SVG.",
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
@@ -214,10 +206,6 @@
"line2": "",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "",
"extraTools": ""
},
@@ -251,7 +237,6 @@
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
"embeddable": "",
"text_selected": "Κάντε διπλό κλικ ή πατήστε ENTER για να επεξεργαστείτε το κείμενο",
"text_editing": "Πατήστε Escape ή CtrlOrCmd+ENTER για να ολοκληρώσετε την επεξεργασία",
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
@@ -267,8 +252,7 @@
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή",
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\".",
"disableSnapping": ""
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
"canvas": "καμβάς",
"selection": "επιλογή",
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
},
"colors": {
"transparent": "Διαφανές",
@@ -467,36 +449,5 @@
"shades": "Αποχρώσεις",
"hexCode": "Κωδικός Hex",
"noShades": "Δεν υπάρχουν διαθέσιμες αποχρώσεις για αυτό το χρώμα"
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "",
"button": "",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "",
"button": "",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}
+9 -4
View File
@@ -164,7 +164,6 @@
"darkMode": "Dark mode",
"lightMode": "Light mode",
"zenMode": "Zen mode",
"objectsSnapMode": "Snap to objects",
"exitZenMode": "Exit zen mode",
"cancel": "Cancel",
"clear": "Clear",
@@ -203,7 +202,6 @@
"imageInsertError": "Couldn't insert image. Try again later...",
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
"failedToFetchImage": "Failed to fetch image.",
"invalidSVGString": "Invalid SVG.",
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
"importLibraryError": "Couldn't load library",
@@ -237,9 +235,9 @@
"eraser": "Eraser",
"frame": "Frame tool",
"embeddable": "Web Embed",
"laser": "Laser pointer",
"hand": "Hand (panning tool)",
"extraTools": "More tools"
"extraTools": "More tools",
"mermaidToExcalidraw": "Mermaid to Excalidraw"
},
"headings": {
"canvasActions": "Canvas actions",
@@ -498,5 +496,12 @@
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
}
}
},
"mermaid": {
"title": "Mermaid to Excalidraw",
"button": "Insert",
"description": "Currently only <flowchartLink>flowcharts</flowchartLink> are supported. The other types will be rendered as image in Excalidraw.",
"syntax": "Mermaid Syntax",
"preview": "Preview"
}
}
+41 -90
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "Envolver el texto en un contenedor",
"link": {
"edit": "Editar enlace",
"editEmbed": "Editar enlace e incrustar",
"create": "Crear enlace",
"createEmbed": "Crear enlace e incrustar",
"label": "Enlace",
"labelEmbed": "Enlazar e incrustar",
"empty": "No se ha establecido un enlace"
"label": "Enlace"
},
"lineEditor": {
"edit": "Editar línea",
@@ -128,9 +124,9 @@
},
"statusPublished": "Publicado",
"sidebarLock": "Mantener barra lateral abierta",
"selectAllElementsInFrame": "Seleccionar todos los elementos en el marco",
"removeAllElementsFromFrame": "Eliminar todos los elementos del marco",
"eyeDropper": "Seleccionar un color del lienzo"
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": ""
},
"library": {
"noItems": "No hay elementos añadidos todavía...",
@@ -164,16 +160,13 @@
"darkMode": "Modo oscuro",
"lightMode": "Modo claro",
"zenMode": "Modo Zen",
"objectsSnapMode": "Ajustar a los objetos",
"exitZenMode": "Salir del modo Zen",
"cancel": "Cancelar",
"clear": "Borrar",
"remove": "Eliminar",
"embed": "",
"publishLibrary": "Publicar",
"submit": "Enviar",
"confirm": "Confirmar",
"embeddableInteractionButton": "Pulsa para interactuar"
"confirm": "Confirmar"
},
"alerts": {
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
@@ -203,21 +196,16 @@
"imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...",
"fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.",
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
"failedToFetchImage": "",
"invalidSVGString": "SVG no válido.",
"cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.",
"importLibraryError": "No se pudo cargar la librería",
"collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.",
"collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.",
"brave_measure_text_error": {
"line1": "Parece que estás usando el navegador Brave con el ajuste <bold>Forzar el bloqueo de huellas digitales</bold> habilitado.",
"line2": "Esto podría resultar en errores en los <bold>Elementos de Texto</bold> en tus dibujos.",
"line3": "Recomendamos fuertemente deshabilitar esta configuración. Puedes seguir <link>estos pasos</link> sobre cómo hacerlo.",
"line1": "",
"line2": "",
"line3": "",
"line4": ""
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,10 +224,8 @@
"link": "Añadir/Actualizar enlace para una forma seleccionada",
"eraser": "Borrar",
"frame": "",
"embeddable": "Incrustar Web",
"laser": "Puntero láser",
"hand": "Mano (herramienta de panoramización)",
"extraTools": "Más herramientas"
"extraTools": ""
},
"headings": {
"canvasActions": "Acciones del lienzo",
@@ -251,7 +237,6 @@
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
"freeDraw": "Haz clic y arrastra, suelta al terminar",
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
"embeddable": "Haga clic y arrastre para crear un sitio web incrustado",
"text_selected": "Doble clic o pulse ENTER para editar el texto",
"text_editing": "Pulse Escape o Ctrl/Cmd + ENTER para terminar de editar",
"linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar",
@@ -267,8 +252,7 @@
"bindTextToElement": "Presione Entrar para agregar",
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar",
"eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación",
"firefox_clipboard_write": "Esta característica puede ser habilitada estableciendo la bandera \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar las banderas del navegador en Firefox, visite la página \"about:config\".",
"disableSnapping": "Mantén pulsado CtrlOrCmd para desactivar el ajuste"
"firefox_clipboard_write": "Esta característica puede ser habilitada estableciendo la bandera \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar las banderas del navegador en Firefox, visite la página \"about:config\"."
},
"canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa",
@@ -376,27 +360,27 @@
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
},
"imageExportDialog": {
"header": "Exportar imagen",
"header": "",
"label": {
"withBackground": "Fondo",
"onlySelected": "Sólo seleccionados",
"darkMode": "Modo oscuro",
"embedScene": "Incrustar escena",
"scale": "Escalar",
"padding": "Espaciado"
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
"scale": "",
"padding": ""
},
"tooltip": {
"embedScene": ""
},
"title": {
"exportToPng": "Exportar a PNG",
"exportToSvg": "Exportar a SVG",
"copyPngToClipboard": "Copiar PNG al portapapeles"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
},
"button": {
"exportToPng": "PNG",
"exportToSvg": "SVG",
"copyPngToClipboard": "Copiar al portapapeles"
"exportToPng": "",
"exportToSvg": "",
"copyPngToClipboard": ""
}
},
"encrypted": {
@@ -427,26 +411,24 @@
"fileSavedToFilename": "Guardado en {filename}",
"canvas": "lienzo",
"selection": "selección",
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un solo elemento,\no pegar en un editor de texto existente",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un solo elemento,\no pegar en un editor de texto existente"
},
"colors": {
"transparent": "Transparente",
"black": "Negro",
"white": "Blanco",
"red": "Rojo",
"pink": "Rosa",
"grape": "Uva",
"violet": "Violeta",
"gray": "Gris",
"blue": "Azul",
"cyan": "Cian",
"teal": "Turquesa",
"green": "Verde",
"yellow": "Amarillo",
"orange": "Naranja",
"bronze": "Bronce"
"black": "",
"white": "",
"red": "",
"pink": "",
"grape": "",
"violet": "",
"gray": "",
"blue": "",
"cyan": "",
"teal": "",
"green": "",
"yellow": "",
"orange": "",
"bronze": ""
},
"welcomeScreen": {
"app": {
@@ -462,41 +444,10 @@
}
},
"colorPicker": {
"mostUsedCustomColors": "Colores personalizados más utilizados",
"colors": "Colores",
"mostUsedCustomColors": "",
"colors": "",
"shades": "",
"hexCode": "Código Hexadecimal",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "Exportar como imagen",
"button": "Exportar como imagen",
"description": ""
},
"saveToDisk": {
"title": "Guardar en el disco",
"button": "Guardar en el disco",
"description": "Exporta los datos de la escena a un archivo desde el cual podrás importar más tarde."
},
"excalidrawPlus": {
"title": "",
"button": "Exportar a Excalidraw+",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "Cargar desde un archivo",
"button": "Cargar desde un archivo",
"description": ""
},
"shareableLink": {
"title": "Cargar desde un enlace",
"button": "Reemplazar mi contenido",
"description": "Cargar un dibujo externo <bold>reemplazará tu contenido existente</bold>.<br></br>Puedes primero hacer una copia de seguridad de tu dibujo usando una de las opciones de abajo."
}
}
}
}
+4 -53
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "Bilatu testua edukiontzi batean",
"link": {
"edit": "Editatu esteka",
"editEmbed": "Editatu esteka eta kapsulatu",
"create": "Sortu esteka",
"createEmbed": "Sortu esteka eta kapsulatu",
"label": "Esteka",
"labelEmbed": "Esteka eta kapsula",
"empty": "Ez da estekarik ezarri"
"label": "Esteka"
},
"lineEditor": {
"edit": "Editatu lerroa",
@@ -164,16 +160,13 @@
"darkMode": "Modu iluna",
"lightMode": "Modu argia",
"zenMode": "Zen modua",
"objectsSnapMode": "Atxiki objektuei",
"exitZenMode": "Irten Zen modutik",
"cancel": "Utzi",
"clear": "Garbitu",
"remove": "Kendu",
"embed": "Aldatu kapsulatzea",
"publishLibrary": "Argitaratu",
"submit": "Bidali",
"confirm": "Bai",
"embeddableInteractionButton": "Egin klik elkar eragiteko"
"confirm": "Bai"
},
"alerts": {
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
@@ -203,7 +196,6 @@
"imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...",
"fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.",
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
"failedToFetchImage": "",
"invalidSVGString": "SVG baliogabea.",
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
"importLibraryError": "Ezin izan da liburutegia kargatu",
@@ -214,10 +206,6 @@
"line2": "Honek zure marrazkietako <bold>Testu-elementuak</bold> hautsi ditzake.",
"line3": "Ezarpen hau desgaitzea gomendatzen dugu. <link>urrats hauek</link> jarrai ditzakezu hori nola egin jakiteko.",
"line4": "Ezarpen hau desgaituz gero, testu-elementuen bistaratzea konpontzen ez bada, ireki <issueLink>arazo</issueLink> gure GitHub-en edo idatzi iezaguzu <discordLink>Discord</discordLink> helbidera"
},
"libraryElementTypeError": {
"embeddable": "Kapsulatutako elementuak ezin dira liburutegira gehitu.",
"image": "Laster egongo da irudiak liburutegian gehitzeko laguntza!"
}
},
"toolBar": {
@@ -236,8 +224,6 @@
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
"eraser": "Borragoma",
"frame": "Marko tresna",
"embeddable": "Web kapsulatzea",
"laser": "Laser punteroa",
"hand": "Eskua (panoratze tresna)",
"extraTools": "Tresna gehiago"
},
@@ -251,7 +237,6 @@
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
"embeddable": "Egin klik eta arrastatu webgunea kapsulatzeko",
"text_selected": "Egin klik bikoitza edo sakatu SARTU testua editatzeko",
"text_editing": "Sakatu Esc edo Ctrl+SARTU editatzen amaitzeko",
"linearElementMulti": "Egin klik azken puntuan edo sakatu Esc edo Sartu amaitzeko",
@@ -267,8 +252,7 @@
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
"firefox_clipboard_write": "Ezaugarri hau \"dom.events.asyncClipboard.clipboardItem\" marka \"true\" gisa ezarrita gaitu daiteke. Firefox-en arakatzailearen banderak aldatzeko, bisitatu \"about:config\" orrialdera.",
"disableSnapping": "Eduki sakatuta Ctrl edo Cmd tekla atxikipena desgaitzeko"
"firefox_clipboard_write": "Ezaugarri hau \"dom.events.asyncClipboard.clipboardItem\" marka \"true\" gisa ezarrita gaitu daiteke. Firefox-en arakatzailearen banderak aldatzeko, bisitatu \"about:config\" orrialdera."
},
"canvasError": {
"cannotShowPreview": "Ezin da oihala aurreikusi",
@@ -427,9 +411,7 @@
"fileSavedToFilename": "{filename}-n gorde da",
"canvas": "oihala",
"selection": "hautapena",
"pasteAsSingleElement": "Erabili {{shortcut}} elementu bakar gisa itsasteko,\nedo itsatsi lehendik dagoen testu-editore batean",
"unableToEmbed": "Url hau txertatzea ez da une honetan onartzen. Sortu issue bat GitHub-en Urla zerrenda zurian sartzea eskatzeko",
"unrecognizedLinkFormat": "Kapsulatu duzun esteka ez dator bat espero den formatuarekin. Mesedez, saiatu iturburu-guneak emandako 'kapsulatu' katea itsasten"
"pasteAsSingleElement": "Erabili {{shortcut}} elementu bakar gisa itsasteko,\nedo itsatsi lehendik dagoen testu-editore batean"
},
"colors": {
"transparent": "Gardena",
@@ -467,36 +449,5 @@
"shades": "Ñabardurak",
"hexCode": "Hez kodea",
"noShades": "Kolore honetarako ez dago ñabardurarik eskuragarri"
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "Esportatu irudi gisa",
"button": "Esportatu irudi gisa",
"description": "Esportatu eszenaren datuak geroago inportatu ahal izango duzun irudi gisa."
},
"saveToDisk": {
"title": "Gorde diskoan",
"button": "Gorde diskoan",
"description": "Esportatu eszenaren datuak geroago inportatu ahal izango duzun fitxategi batan."
},
"excalidrawPlus": {
"title": "Excalidraw+",
"button": "Esportatu Excalidraw+ra",
"description": "Gorde eszena zure Excalidraw+ laneko areara."
}
},
"modal": {
"loadFromFile": {
"title": "Fitxategitik kargatu",
"button": "Fitxategitik kargatu",
"description": "Fitxategi batetik kargatzeak <bold>lehendik duzun edukia ordezkatuko du</bold>.<br></br>Lehenengo marrazkiaren babeskopia egin dezakezu beheko aukeretako bat erabiliz."
},
"shareableLink": {
"title": "Estekatik kargatu",
"button": "Ordeztu nire edukia",
"description": "Kanpoko irudi bat kargatzeak <bold>lehendik duzun edukia ordezkatuko du</bold>.<br></br>. Zure marrazkiaren babeskopia egin dezakezu lehenik beheko aukeretako bat erabiliz."
}
}
}
}
+24 -73
View File
@@ -109,12 +109,8 @@
"createContainerFromText": "متن را در یک جایگاه بپیچید",
"link": {
"edit": "ویرایش لینک",
"editEmbed": "",
"create": "ایجاد پیوند",
"createEmbed": "",
"label": "لینک",
"labelEmbed": "",
"empty": ""
"label": "لینک"
},
"lineEditor": {
"edit": "ویرایش لینک",
@@ -130,7 +126,7 @@
"sidebarLock": "باز نگه داشتن سایدبار",
"selectAllElementsInFrame": "",
"removeAllElementsFromFrame": "",
"eyeDropper": "انتخاب رنگ از کرباس"
"eyeDropper": ""
},
"library": {
"noItems": "آیتمی به اینجا اضافه نشده...",
@@ -164,16 +160,13 @@
"darkMode": "حالت تیره",
"lightMode": "حالت روشن",
"zenMode": "حالت ذن",
"objectsSnapMode": "",
"exitZenMode": "خروج از حالت تمرکز",
"cancel": "لغو",
"clear": "پاک کردن",
"remove": "پاک کردن",
"embed": "",
"publishLibrary": "انتشار",
"submit": "ارسال",
"confirm": "تایید",
"embeddableInteractionButton": ""
"confirm": "تایید"
},
"alerts": {
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
@@ -203,7 +196,6 @@
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
"failedToFetchImage": "",
"invalidSVGString": "SVG نادرست.",
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
"importLibraryError": "داده‌ها بارگذاری نشدند",
@@ -214,10 +206,6 @@
"line2": "این می تواند منجر به شکستن <bold>عناصر متن</bold> در نقاشی های شما شود.",
"line3": "اکیداً توصیه می کنیم این تنظیم را غیرفعال کنید. برای نحوه انجام این کار می‌توانید <link>این مراحل</link> را دنبال کنید.",
"line4": "اگر غیرفعال کردن این تنظیم نمایش عناصر متنی را برطرف نکرد، لطفاً یک <issueLink>مشکل</issueLink> را در GitHub ما باز کنید یا برای ما در <discordLink>Discord</discordLink> بنویسید."
},
"libraryElementTypeError": {
"embeddable": "",
"image": ""
}
},
"toolBar": {
@@ -236,10 +224,8 @@
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن",
"frame": "",
"embeddable": "",
"laser": "",
"hand": "دست (ابزار پانینگ)",
"extraTools": "ابزارهای بیشتر"
"extraTools": ""
},
"headings": {
"canvasActions": "عملیات روی بوم",
@@ -251,7 +237,6 @@
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
"embeddable": "",
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
@@ -267,8 +252,7 @@
"bindTextToElement": "برای افزودن اینتر را بزنید",
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
"firefox_clipboard_write": "احتمالاً می‌توان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید.",
"disableSnapping": ""
"firefox_clipboard_write": "احتمالاً می‌توان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید."
},
"canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@@ -378,7 +362,7 @@
"imageExportDialog": {
"header": "",
"label": {
"withBackground": "پس زمینه",
"withBackground": "",
"onlySelected": "",
"darkMode": "",
"embedScene": "",
@@ -427,26 +411,24 @@
"fileSavedToFilename": "ذخیره در {filename}",
"canvas": "بوم",
"selection": "انتخاب",
"pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید",
"unableToEmbed": "",
"unrecognizedLinkFormat": ""
"pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید"
},
"colors": {
"transparent": "شفاف",
"black": "سیاه",
"white": "سفید",
"red": "قرمز",
"pink": "صورتی",
"grape": "یاقوتی",
"violet": "بنفش",
"gray": "خاکستری",
"blue": "آبی",
"cyan": "فیروزه‌ای",
"teal": "سبزآبی",
"green": "سبز",
"yellow": "زرد",
"orange": "نارنجی",
"bronze": "برنزی"
"black": "",
"white": "",
"red": "",
"pink": "",
"grape": "",
"violet": "",
"gray": "",
"blue": "",
"cyan": "",
"teal": "",
"green": "",
"yellow": "",
"orange": "",
"bronze": ""
},
"welcomeScreen": {
"app": {
@@ -463,40 +445,9 @@
},
"colorPicker": {
"mostUsedCustomColors": "",
"colors": "رنگ‌ها",
"shades": "جلوه‌ها",
"hexCode": "کدِ هگز",
"colors": "",
"shades": "",
"hexCode": "",
"noShades": ""
},
"overwriteConfirm": {
"action": {
"exportToImage": {
"title": "",
"button": "",
"description": ""
},
"saveToDisk": {
"title": "ذخیره در دیسک",
"button": "ذخیره در دیسک",
"description": ""
},
"excalidrawPlus": {
"title": "",
"button": "",
"description": ""
}
},
"modal": {
"loadFromFile": {
"title": "بارگذاری از فایل",
"button": "بارگذاری از فایل",
"description": ""
},
"shareableLink": {
"title": "",
"button": "",
"description": ""
}
}
}
}

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