Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c552ff4554 | |||
| 26f9b54199 | |||
| 7f5b7bab69 | |||
| bf7c91536f | |||
| 4372e992e0 | |||
| 1e4bfceb13 | |||
| 539071fcfe | |||
| 3700cf2d10 | |||
| 89218ba596 | |||
| bc5436592e | |||
| 750055ddfa | |||
| 93e4cb8d25 | |||
| a2dd3c6ea2 | |||
| 0360e64219 | |||
| c2867c9a93 | |||
| 14bca119f7 | |||
| afea0df141 | |||
| d2a508104e | |||
| 3697618266 | |||
| e7cc2337ea | |||
| 9eb89f9960 | |||
| ab1bcc7615 | |||
| b1cac35269 | |||
| 83f86e2b86 | |||
| 7e38cab76e | |||
| 2cabb1f1f4 | |||
| 63650f82d1 | |||
| dde3dac931 | |||
| 5b94cffc74 | |||
| aaf73c8ff3 | |||
| 44d9d5fcac | |||
| 89a3bbddb7 | |||
| b86184a849 | |||
| b552166924 | |||
| 26ff3993bb | |||
| 7ad02c359a | |||
| 2523fe82e3 | |||
| 4ea079eb85 | |||
| f20ba90ffa | |||
| 03da9112cf | |||
| a249f332a2 | |||
| 2e61926a6b | |||
| e921bfb1ae | |||
| e6f74350ac | |||
| fa33aa08ab | |||
| 8b838049df | |||
| 1f4f5e11ae | |||
| 12420592ef | |||
| bfd318e765 | |||
| 6a821f3b76 | |||
| 84fd13e872 | |||
| 7d2b6f3374 | |||
| ceb637f5ea | |||
| 4c35eba72d | |||
| 4765f5536e | |||
| 556175558a | |||
| 4db73a7f95 |
@@ -1,3 +0,0 @@
|
||||
## 2020-10-13
|
||||
|
||||
- Added ability to embed scene source into exported PNG/SVG files so you can import the scene from them (open via `Load` button or drag & drop). #2219
|
||||
@@ -70,7 +70,7 @@ The Excalidraw editor (npm package) supports:
|
||||
|
||||
## Excalidraw.com
|
||||
|
||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/excalidraw-app) is part of this repository as well, and the app features:
|
||||
|
||||
- 📡 PWA support (works offline).
|
||||
- 🤼 Real-time collaboration.
|
||||
|
||||
@@ -38,6 +38,7 @@ 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 |
|
||||
@@ -70,6 +71,7 @@ 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 is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
|
||||
Excalidraw uses CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
|
||||
|
||||
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
|
||||
|
||||
|
||||
@@ -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/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
||||
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
|
||||
|
||||
### Turning off Aggressive Anti-Fingerprinting in Brave browser
|
||||
|
||||
@@ -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**
|
||||
|
||||

|
||||

|
||||
|
||||
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 worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||
The following workflow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||
|
||||
```jsx showLineNumbers
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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.
|
||||
@@ -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. To do this, run:
|
||||
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork, run:
|
||||
>
|
||||
> ```bash
|
||||
> git remote add upstream https://github.com/excalidraw/excalidraw.git
|
||||
|
||||
@@ -23,7 +23,11 @@ const sidebars = {
|
||||
},
|
||||
items: ["introduction/development", "introduction/contributing"],
|
||||
},
|
||||
{ type: "category", label: "Codebase", items: ["codebase/json-schema"] },
|
||||
{
|
||||
type: "category",
|
||||
label: "Codebase",
|
||||
items: ["codebase/json-schema", "codebase/frames"],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "@excalidraw/excalidraw",
|
||||
|
||||
@@ -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 by don't know where to
|
||||
Want to build your own app powered by Excalidraw but don't know where to
|
||||
start?
|
||||
</>
|
||||
),
|
||||
|
||||
+117
-12
@@ -145,6 +145,14 @@
|
||||
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"
|
||||
@@ -202,6 +210,16 @@
|
||||
"@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"
|
||||
@@ -265,6 +283,11 @@
|
||||
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"
|
||||
@@ -280,6 +303,14 @@
|
||||
"@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"
|
||||
@@ -287,6 +318,13 @@
|
||||
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"
|
||||
@@ -374,11 +412,28 @@
|
||||
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"
|
||||
@@ -412,11 +467,25 @@
|
||||
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"
|
||||
@@ -1147,19 +1216,28 @@
|
||||
"@babel/parser" "^7.18.6"
|
||||
"@babel/types" "^7.18.6"
|
||||
|
||||
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
||||
version "7.18.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
|
||||
integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
|
||||
"@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.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"
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.22.13"
|
||||
"@babel/generator" "^7.23.0"
|
||||
"@babel/helper-environment-visitor" "^7.22.20"
|
||||
"@babel/helper-function-name" "^7.23.0"
|
||||
"@babel/helper-hoist-variables" "^7.22.5"
|
||||
"@babel/helper-split-export-declaration" "^7.22.6"
|
||||
"@babel/parser" "^7.23.0"
|
||||
"@babel/types" "^7.23.0"
|
||||
debug "^4.1.0"
|
||||
globals "^11.1.0"
|
||||
|
||||
@@ -1171,6 +1249,15 @@
|
||||
"@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"
|
||||
@@ -1670,6 +1757,11 @@
|
||||
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"
|
||||
@@ -1688,6 +1780,19 @@
|
||||
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,7 +80,8 @@ export const ExportToExcalidrawPlus: React.FC<{
|
||||
appState: Partial<AppState>;
|
||||
files: BinaryFiles;
|
||||
onError: (error: Error) => void;
|
||||
}> = ({ elements, appState, files, onError }) => {
|
||||
onSuccess: () => void;
|
||||
}> = ({ elements, appState, files, onError, onSuccess }) => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<Card color="primary">
|
||||
@@ -107,6 +108,7 @@ 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") {
|
||||
|
||||
@@ -107,7 +107,7 @@ export type SocketUpdateDataSource = {
|
||||
type: "MOUSE_LOCATION";
|
||||
payload: {
|
||||
socketId: string;
|
||||
pointer: { x: number; y: number };
|
||||
pointer: { x: number; y: number; tool: "pointer" | "laser" };
|
||||
button: "down" | "up";
|
||||
selectedElementIds: AppState["selectedElementIds"];
|
||||
username: string;
|
||||
|
||||
@@ -608,7 +608,7 @@ const ExcalidrawWrapper = () => {
|
||||
canvas: HTMLCanvasElement,
|
||||
) => {
|
||||
if (exportedElements.length === 0) {
|
||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||
}
|
||||
if (canvas) {
|
||||
try {
|
||||
@@ -624,7 +624,7 @@ const ExcalidrawWrapper = () => {
|
||||
);
|
||||
|
||||
if (errorMessage) {
|
||||
setErrorMessage(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
@@ -634,7 +634,7 @@ const ExcalidrawWrapper = () => {
|
||||
if (error.name !== "AbortError") {
|
||||
const { width, height } = canvas;
|
||||
console.error(error, { width, height });
|
||||
setErrorMessage(error.message);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -714,6 +714,11 @@ const ExcalidrawWrapper = () => {
|
||||
},
|
||||
});
|
||||
}}
|
||||
onSuccess={() => {
|
||||
excalidrawAPI?.updateScene({
|
||||
appState: { openDialog: null },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/laser-pointer": "1.2.0",
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@radix-ui/react-popover": "1.0.3",
|
||||
"@radix-ui/react-tabs": "1.0.2",
|
||||
@@ -48,11 +49,11 @@
|
||||
"png-chunk-text": "1.0.0",
|
||||
"png-chunks-encode": "1.0.0",
|
||||
"png-chunks-extract": "1.0.0",
|
||||
"points-on-curve": "0.2.0",
|
||||
"points-on-curve": "1.0.1",
|
||||
"pwacompat": "2.0.17",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"roughjs": "4.5.2",
|
||||
"roughjs": "4.6.5",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"tunnel-rat": "0.1.2"
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{
|
||||
"src": "apple-touch-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
"sizes": "180x180"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
|
||||
@@ -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, setCursor, updateActiveTool } from "../utils";
|
||||
import { getShortcutKey, updateActiveTool } from "../utils";
|
||||
import { register } from "./register";
|
||||
import { Tooltip } from "../components/Tooltip";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
@@ -21,6 +21,7 @@ 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",
|
||||
|
||||
@@ -46,6 +46,7 @@ const deleteSelectedElements = (
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -155,7 +155,12 @@ const duplicateElements = (
|
||||
groupId,
|
||||
).flatMap((element) =>
|
||||
isFrameElement(element)
|
||||
? [...getFrameElements(elements, element.id), element]
|
||||
? [
|
||||
...getFrameElements(elements, element.id, {
|
||||
includeBoundArrows: true,
|
||||
}),
|
||||
element,
|
||||
]
|
||||
: [element],
|
||||
);
|
||||
|
||||
@@ -181,7 +186,9 @@ const duplicateElements = (
|
||||
continue;
|
||||
}
|
||||
if (isElementAFrame) {
|
||||
const elementsInFrame = getFrameElements(sortedElements, element.id);
|
||||
const elementsInFrame = getFrameElements(sortedElements, element.id, {
|
||||
includeBoundArrows: true,
|
||||
});
|
||||
|
||||
elementsWithClones.push(
|
||||
...markAsProcessed([
|
||||
|
||||
@@ -191,7 +191,15 @@ export const actionSaveFileToDisk = register({
|
||||
},
|
||||
app.files,
|
||||
);
|
||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
openDialog: null,
|
||||
fileHandle,
|
||||
toast: { message: t("toast.fileSaved") },
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error?.name !== "AbortError") {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { KEYS } from "../keys";
|
||||
import { isInvisiblySmallElement } from "../element";
|
||||
import { updateActiveTool, resetCursor } from "../utils";
|
||||
import { updateActiveTool } from "../utils";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { done } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "../element/binding";
|
||||
import { isBindingElement, isLinearElement } from "../element/typeChecks";
|
||||
import { AppState } from "../types";
|
||||
import { resetCursor } from "../cursor";
|
||||
|
||||
export const actionFinalize = register({
|
||||
name: "finalize",
|
||||
@@ -90,7 +91,9 @@ export const actionFinalize = register({
|
||||
}
|
||||
}
|
||||
if (isInvisiblySmallElement(multiPointElement)) {
|
||||
newElements = newElements.slice(0, -1);
|
||||
newElements = newElements.filter(
|
||||
(el) => el.id !== multiPointElement.id,
|
||||
);
|
||||
}
|
||||
|
||||
// If the multi point line closes the loop,
|
||||
|
||||
@@ -4,7 +4,8 @@ import { removeAllElementsFromFrame } from "../frame";
|
||||
import { getFrameElements } from "../frame";
|
||||
import { KEYS } from "../keys";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { setCursorForShape, updateActiveTool } from "../utils";
|
||||
import { updateActiveTool } from "../utils";
|
||||
import { setCursorForShape } from "../cursor";
|
||||
import { register } from "./register";
|
||||
|
||||
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
|
||||
|
||||
@@ -15,6 +15,7 @@ export const actionToggleGridMode = register({
|
||||
appState: {
|
||||
...appState,
|
||||
gridSize: this.checked!(appState) ? null : GRID_SIZE,
|
||||
objectsSnapModeEnabled: false,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { register } from "./register";
|
||||
|
||||
export const actionToggleObjectsSnapMode = register({
|
||||
name: "objectsSnapMode",
|
||||
viewMode: true,
|
||||
trackEvent: {
|
||||
category: "canvas",
|
||||
predicate: (appState) => !appState.objectsSnapModeEnabled,
|
||||
},
|
||||
perform(elements, appState) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
objectsSnapModeEnabled: !this.checked!(appState),
|
||||
gridSize: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
checked: (appState) => appState.objectsSnapModeEnabled,
|
||||
predicate: (elements, appState, appProps) => {
|
||||
return typeof appProps.objectsSnapModeEnabled === "undefined";
|
||||
},
|
||||
contextItemLabel: "buttons.objectsSnapMode",
|
||||
keyTest: (event) =>
|
||||
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S,
|
||||
});
|
||||
@@ -80,6 +80,7 @@ export {
|
||||
|
||||
export { actionToggleGridMode } from "./actionToggleGridMode";
|
||||
export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||
export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
|
||||
|
||||
export { actionToggleStats } from "./actionToggleStats";
|
||||
export { actionUnbindText, actionBindText } from "./actionBoundText";
|
||||
|
||||
@@ -28,6 +28,7 @@ export type ShortcutName =
|
||||
| "ungroup"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "objectsSnapMode"
|
||||
| "stats"
|
||||
| "addToLibrary"
|
||||
| "viewMode"
|
||||
@@ -74,6 +75,7 @@ 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")],
|
||||
|
||||
@@ -51,6 +51,7 @@ export type ActionName =
|
||||
| "pasteStyles"
|
||||
| "gridMode"
|
||||
| "zenMode"
|
||||
| "objectsSnapMode"
|
||||
| "stats"
|
||||
| "changeStrokeColor"
|
||||
| "changeBackgroundColor"
|
||||
|
||||
@@ -99,6 +99,12 @@ export const getDefaultAppState = (): Omit<
|
||||
pendingImageElementId: null,
|
||||
showHyperlinkPopup: false,
|
||||
selectedLinearElement: null,
|
||||
snapLines: [],
|
||||
originSnapOffset: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
objectsSnapModeEnabled: false,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -206,6 +212,9 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
pendingImageElementId: { browser: false, export: false, server: false },
|
||||
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 = <
|
||||
|
||||
+7
-12
@@ -1,26 +1,21 @@
|
||||
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({
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
let clipboardData = await parseClipboard(
|
||||
createPasteEvent({ "text/plain": text }),
|
||||
);
|
||||
|
||||
expect(clipboardData.text).toBe(text);
|
||||
|
||||
text = "[123]";
|
||||
|
||||
clipboardData = await parseClipboard({
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ "text/plain": text }),
|
||||
);
|
||||
|
||||
expect(clipboardData.text).toBe(text);
|
||||
});
|
||||
|
||||
+70
-9
@@ -18,11 +18,14 @@ 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;
|
||||
}
|
||||
@@ -142,22 +145,74 @@ 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)
|
||||
*/
|
||||
export const getSystemClipboard = async (
|
||||
const getSystemClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
): Promise<string> => {
|
||||
isPlainPaste = false,
|
||||
): Promise<
|
||||
| { type: "text"; value: string }
|
||||
| { type: "mixedContent"; value: PastedMixedContent }
|
||||
> => {
|
||||
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 (text || "").trim();
|
||||
return { type: "text", value: (text || "").trim() };
|
||||
} catch {
|
||||
return "";
|
||||
return { type: "text", value: "" };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,14 +223,20 @@ export const parseClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
isPlainPaste = false,
|
||||
): Promise<ClipboardData> => {
|
||||
const systemClipboard = await getSystemClipboard(event);
|
||||
const systemClipboard = await getSystemClipboard(event, isPlainPaste);
|
||||
|
||||
if (systemClipboard.type === "mixedContent") {
|
||||
return {
|
||||
mixedContent: systemClipboard.value,
|
||||
};
|
||||
}
|
||||
|
||||
// if system clipboard empty, couldn't be resolved, or contains previously
|
||||
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
||||
// elements
|
||||
if (
|
||||
!systemClipboard ||
|
||||
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
|
||||
(!isPlainPaste && systemClipboard.value.includes(SVG_EXPORT_TAG))
|
||||
) {
|
||||
return getAppClipboard();
|
||||
}
|
||||
@@ -183,7 +244,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);
|
||||
!isPlainPaste && parsePotentialSpreadsheet(systemClipboard.value);
|
||||
|
||||
if (spreadsheetResult) {
|
||||
return spreadsheetResult;
|
||||
@@ -192,7 +253,7 @@ export const parseClipboard = async (
|
||||
const appClipboardData = getAppClipboard();
|
||||
|
||||
try {
|
||||
const systemClipboardData = JSON.parse(systemClipboard);
|
||||
const systemClipboardData = JSON.parse(systemClipboard.value);
|
||||
const programmaticAPI =
|
||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||
if (clipboardContainsElements(systemClipboardData)) {
|
||||
@@ -216,7 +277,7 @@ export const parseClipboard = async (
|
||||
? JSON.stringify(appClipboardData.elements, null, 2)
|
||||
: undefined,
|
||||
}
|
||||
: { text: systemClipboard };
|
||||
: { text: systemClipboard.value };
|
||||
};
|
||||
|
||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
|
||||
@@ -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: transparent !important;
|
||||
background-color: var(--color-surface-low) !important;
|
||||
font-size: 0.875rem !important;
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
|
||||
+53
-76
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement, PointerType } from "../element/types";
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useDevice } from "../components/App";
|
||||
import {
|
||||
@@ -14,13 +14,8 @@ import {
|
||||
hasText,
|
||||
} from "../scene";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { UIAppState, Zoom } from "../types";
|
||||
import {
|
||||
capitalizeString,
|
||||
isTransparent,
|
||||
updateActiveTool,
|
||||
setCursorForShape,
|
||||
} from "../utils";
|
||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||
import { capitalizeString, isTransparent } from "../utils";
|
||||
import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
@@ -36,7 +31,12 @@ import {
|
||||
|
||||
import "./Actions.scss";
|
||||
import DropdownMenu from "./dropdownMenu/DropdownMenu";
|
||||
import { EmbedIcon, extraToolsIcon, frameToolIcon } from "./icons";
|
||||
import {
|
||||
EmbedIcon,
|
||||
extraToolsIcon,
|
||||
frameToolIcon,
|
||||
laserPointerToolIcon,
|
||||
} from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
export const SelectedShapeActions = ({
|
||||
@@ -213,20 +213,21 @@ 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) => {
|
||||
@@ -251,31 +252,20 @@ export const ShapesSwitcher = ({
|
||||
data-testid={`toolbar-${value}`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
app.togglePenMode(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") {
|
||||
onImageAction({ pointerType });
|
||||
app.setActiveTool({
|
||||
type: value,
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
});
|
||||
} else {
|
||||
app.setActiveTool({ type: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -300,24 +290,14 @@ export const ShapesSwitcher = ({
|
||||
data-testid={`toolbar-frame`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "frame", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "frame",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
app.setActiveTool({ type: "frame" });
|
||||
}}
|
||||
selected={activeTool.type === "frame"}
|
||||
/>
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
@@ -330,30 +310,28 @@ export const ShapesSwitcher = ({
|
||||
data-testid={`toolbar-embeddable`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
setAppState({
|
||||
penDetected: true,
|
||||
penMode: true,
|
||||
});
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "embeddable", "ui");
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
activeEmbeddable: null,
|
||||
});
|
||||
app.setActiveTool({ type: "embeddable" });
|
||||
}}
|
||||
selected={activeTool.type === "embeddable"}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className="App-toolbar__extra-tools-trigger"
|
||||
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||
"App-toolbar__extra-tools-trigger--selected":
|
||||
frameToolSelected ||
|
||||
embeddableToolSelected ||
|
||||
// in collab we're already highlighting the laser button
|
||||
// outside toolbar, so let's not highlight extra-tools button
|
||||
// on top of it
|
||||
(laserToolSelected && !app.props.isCollaborating),
|
||||
})}
|
||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||
title={t("toolBar.extraTools")}
|
||||
>
|
||||
@@ -366,37 +344,36 @@ export const ShapesSwitcher = ({
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "frame",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
app.setActiveTool({ type: "frame" });
|
||||
}}
|
||||
icon={frameToolIcon}
|
||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid="toolbar-frame"
|
||||
selected={frameToolSelected}
|
||||
>
|
||||
{t("toolBar.frame")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
const nextActiveTool = updateActiveTool(appState, {
|
||||
type: "embeddable",
|
||||
});
|
||||
setAppState({
|
||||
activeTool: nextActiveTool,
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
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>
|
||||
)}
|
||||
|
||||
+549
-124
File diff suppressed because it is too large
Load Diff
@@ -12,32 +12,32 @@
|
||||
|
||||
&--color-primary {
|
||||
&.ExcButton--variant-filled {
|
||||
--text-color: var(--input-bg-color);
|
||||
--text-color: var(--color-surface-lowest);
|
||||
--back-color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
--back-color: var(--color-primary-darker);
|
||||
--back-color: var(--color-brand-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--back-color: var(--color-primary-darkest);
|
||||
--back-color: var(--color-brand-active);
|
||||
}
|
||||
}
|
||||
|
||||
&.ExcButton--variant-outlined,
|
||||
&.ExcButton--variant-icon {
|
||||
--text-color: var(--color-primary);
|
||||
--border-color: var(--color-primary);
|
||||
--back-color: var(--input-bg-color);
|
||||
--border-color: var(--color-border-outline);
|
||||
--back-color: transparent;
|
||||
|
||||
&:hover {
|
||||
--text-color: var(--color-primary-darker);
|
||||
--border-color: var(--color-primary-darker);
|
||||
--text-color: var(--color-brand-hover);
|
||||
--border-color: var(--color-brand-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
--text-color: var(--color-primary-darkest);
|
||||
--border-color: var(--color-primary-darkest);
|
||||
--text-color: var(--color-brand-active);
|
||||
--border-color: var(--color-brand-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,20 +19,35 @@
|
||||
}
|
||||
|
||||
&__btn {
|
||||
--background: var(--color-surface-mid);
|
||||
|
||||
display: flex;
|
||||
column-gap: 0.5rem;
|
||||
align-items: center;
|
||||
border: 1px solid var(--default-border-color);
|
||||
background-color: var(--background);
|
||||
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 {
|
||||
|
||||
@@ -165,6 +165,7 @@ 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"]}
|
||||
@@ -258,6 +259,10 @@ 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+'")]}
|
||||
|
||||
@@ -23,12 +23,15 @@ 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;
|
||||
@@ -72,9 +75,14 @@ const JSONExportModal = ({
|
||||
title={t("exportDialog.link_button")}
|
||||
aria-label={t("exportDialog.link_button")}
|
||||
showAriaLabel={true}
|
||||
onClick={() => {
|
||||
onExportToBackend(elements, appState, files, canvas);
|
||||
trackEvent("export", "link", `ui (${getFrame()})`);
|
||||
onClick={async () => {
|
||||
try {
|
||||
trackEvent("export", "link", `ui (${getFrame()})`);
|
||||
await onExportToBackend(elements, appState, files, canvas);
|
||||
onCloseRequest();
|
||||
} catch (error: any) {
|
||||
setAppState({ errorMessage: error.message });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
@@ -114,6 +122,7 @@ export const JSONExportDialog = ({
|
||||
<JSONExportModal
|
||||
elements={elements}
|
||||
appState={appState}
|
||||
setAppState={setAppState}
|
||||
files={files}
|
||||
actionManager={actionManager}
|
||||
onCloseRequest={handleClose}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
import { LaserPointer } from "@excalidraw/laser-pointer";
|
||||
|
||||
import { sceneCoordsToViewportCoords } from "../../utils";
|
||||
import App from "../App";
|
||||
import { getClientColor } from "../../clients";
|
||||
|
||||
// decay time in milliseconds
|
||||
const DECAY_TIME = 1000;
|
||||
// length of line in points before it starts decaying
|
||||
const DECAY_LENGTH = 50;
|
||||
|
||||
const average = (a: number, b: number) => (a + b) / 2;
|
||||
function getSvgPathFromStroke(points: number[][], closed = true) {
|
||||
const len = points.length;
|
||||
|
||||
if (len < 4) {
|
||||
return ``;
|
||||
}
|
||||
|
||||
let a = points[0];
|
||||
let b = points[1];
|
||||
const c = points[2];
|
||||
|
||||
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
|
||||
2,
|
||||
)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
|
||||
b[1],
|
||||
c[1],
|
||||
).toFixed(2)} T`;
|
||||
|
||||
for (let i = 2, max = len - 1; i < max; i++) {
|
||||
a = points[i];
|
||||
b = points[i + 1];
|
||||
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
|
||||
2,
|
||||
)} `;
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
result += "Z";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
LPM: LaserPathManager;
|
||||
}
|
||||
}
|
||||
|
||||
function easeOutCubic(t: number) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function instantiateCollabolatorState(): CollabolatorState {
|
||||
return {
|
||||
currentPath: undefined,
|
||||
finishedPaths: [],
|
||||
lastPoint: [-10000, -10000],
|
||||
svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
|
||||
};
|
||||
}
|
||||
|
||||
function instantiatePath() {
|
||||
LaserPointer.constants.cornerDetectionMaxAngle = 70;
|
||||
|
||||
return new LaserPointer({
|
||||
simplify: 0,
|
||||
streamline: 0.4,
|
||||
sizeMapping: (c) => {
|
||||
const pt = DECAY_TIME;
|
||||
const pl = DECAY_LENGTH;
|
||||
const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
|
||||
const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
|
||||
|
||||
return Math.min(easeOutCubic(l), easeOutCubic(t));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
type CollabolatorState = {
|
||||
currentPath: LaserPointer | undefined;
|
||||
finishedPaths: LaserPointer[];
|
||||
lastPoint: [number, number];
|
||||
svg: SVGPathElement;
|
||||
};
|
||||
|
||||
export class LaserPathManager {
|
||||
private ownState: CollabolatorState;
|
||||
private collaboratorsState: Map<string, CollabolatorState> = new Map();
|
||||
|
||||
private rafId: number | undefined;
|
||||
private isDrawing = false;
|
||||
private container: SVGSVGElement | undefined;
|
||||
|
||||
constructor(private app: App) {
|
||||
this.ownState = instantiateCollabolatorState();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stop();
|
||||
this.isDrawing = false;
|
||||
this.ownState = instantiateCollabolatorState();
|
||||
this.collaboratorsState = new Map();
|
||||
}
|
||||
|
||||
startPath(x: number, y: number) {
|
||||
this.ownState.currentPath = instantiatePath();
|
||||
this.ownState.currentPath.addPoint([x, y, performance.now()]);
|
||||
this.updatePath(this.ownState);
|
||||
}
|
||||
|
||||
addPointToPath(x: number, y: number) {
|
||||
if (this.ownState.currentPath) {
|
||||
this.ownState.currentPath?.addPoint([x, y, performance.now()]);
|
||||
this.updatePath(this.ownState);
|
||||
}
|
||||
}
|
||||
|
||||
endPath() {
|
||||
if (this.ownState.currentPath) {
|
||||
this.ownState.currentPath.close();
|
||||
this.ownState.finishedPaths.push(this.ownState.currentPath);
|
||||
this.updatePath(this.ownState);
|
||||
}
|
||||
}
|
||||
|
||||
private updatePath(state: CollabolatorState) {
|
||||
this.isDrawing = true;
|
||||
|
||||
if (!this.isRunning) {
|
||||
this.start();
|
||||
}
|
||||
}
|
||||
|
||||
private isRunning = false;
|
||||
|
||||
start(svg?: SVGSVGElement) {
|
||||
if (svg) {
|
||||
this.container = svg;
|
||||
this.container.appendChild(this.ownState.svg);
|
||||
}
|
||||
|
||||
this.stop();
|
||||
this.isRunning = true;
|
||||
this.loop();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isRunning = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
this.rafId = undefined;
|
||||
}
|
||||
|
||||
loop() {
|
||||
this.rafId = requestAnimationFrame(this.loop.bind(this));
|
||||
|
||||
this.updateCollabolatorsState();
|
||||
|
||||
if (this.isDrawing) {
|
||||
this.update();
|
||||
} else {
|
||||
this.isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
draw(path: LaserPointer) {
|
||||
const stroke = path
|
||||
.getStrokeOutline(path.options.size / this.app.state.zoom.value)
|
||||
.map(([x, y]) => {
|
||||
const result = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x, sceneY: y },
|
||||
this.app.state,
|
||||
);
|
||||
|
||||
return [result.x, result.y];
|
||||
});
|
||||
|
||||
return getSvgPathFromStroke(stroke, true);
|
||||
}
|
||||
|
||||
updateCollabolatorsState() {
|
||||
if (!this.container || !this.app.state.collaborators.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, collabolator] of this.app.state.collaborators.entries()) {
|
||||
if (!this.collaboratorsState.has(key)) {
|
||||
const state = instantiateCollabolatorState();
|
||||
this.container.appendChild(state.svg);
|
||||
this.collaboratorsState.set(key, state);
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
|
||||
const state = this.collaboratorsState.get(key)!;
|
||||
|
||||
if (collabolator.pointer && collabolator.pointer.tool === "laser") {
|
||||
if (collabolator.button === "down" && state.currentPath === undefined) {
|
||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
||||
state.currentPath = instantiatePath();
|
||||
state.currentPath.addPoint([
|
||||
collabolator.pointer.x,
|
||||
collabolator.pointer.y,
|
||||
performance.now(),
|
||||
]);
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
|
||||
if (collabolator.button === "down" && state.currentPath !== undefined) {
|
||||
if (
|
||||
collabolator.pointer.x !== state.lastPoint[0] ||
|
||||
collabolator.pointer.y !== state.lastPoint[1]
|
||||
) {
|
||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
||||
state.currentPath.addPoint([
|
||||
collabolator.pointer.x,
|
||||
collabolator.pointer.y,
|
||||
performance.now(),
|
||||
]);
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
}
|
||||
|
||||
if (collabolator.button === "up" && state.currentPath !== undefined) {
|
||||
state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
||||
state.currentPath.addPoint([
|
||||
collabolator.pointer.x,
|
||||
collabolator.pointer.y,
|
||||
performance.now(),
|
||||
]);
|
||||
state.currentPath.close();
|
||||
|
||||
state.finishedPaths.push(state.currentPath);
|
||||
state.currentPath = undefined;
|
||||
|
||||
this.updatePath(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.container) {
|
||||
return;
|
||||
}
|
||||
|
||||
let somePathsExist = false;
|
||||
|
||||
for (const [key, state] of this.collaboratorsState.entries()) {
|
||||
if (!this.app.state.collaborators.has(key)) {
|
||||
state.svg.remove();
|
||||
this.collaboratorsState.delete(key);
|
||||
continue;
|
||||
}
|
||||
|
||||
state.finishedPaths = state.finishedPaths.filter((path) => {
|
||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
||||
|
||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
||||
});
|
||||
|
||||
let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
|
||||
|
||||
if (state.currentPath) {
|
||||
paths += ` ${this.draw(state.currentPath)}`;
|
||||
}
|
||||
|
||||
if (paths.trim()) {
|
||||
somePathsExist = true;
|
||||
}
|
||||
|
||||
state.svg.setAttribute("d", paths);
|
||||
state.svg.setAttribute("fill", getClientColor(key));
|
||||
}
|
||||
|
||||
this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
|
||||
const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
||||
|
||||
return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
||||
});
|
||||
|
||||
let paths = this.ownState.finishedPaths
|
||||
.map((path) => this.draw(path))
|
||||
.join(" ");
|
||||
|
||||
if (this.ownState.currentPath) {
|
||||
paths += ` ${this.draw(this.ownState.currentPath)}`;
|
||||
}
|
||||
|
||||
paths = paths.trim();
|
||||
|
||||
if (paths) {
|
||||
somePathsExist = true;
|
||||
}
|
||||
|
||||
this.ownState.svg.setAttribute("d", paths);
|
||||
this.ownState.svg.setAttribute("fill", "red");
|
||||
|
||||
if (!somePathsExist) {
|
||||
this.isDrawing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import "../ToolIcon.scss";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { ToolButtonSize } from "../ToolButton";
|
||||
import { laserPointerToolIcon } from "../icons";
|
||||
|
||||
type LaserPointerIconProps = {
|
||||
title?: string;
|
||||
name?: string;
|
||||
checked: boolean;
|
||||
onChange?(): void;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
const DEFAULT_SIZE: ToolButtonSize = "small";
|
||||
|
||||
export const LaserPointerButton = (props: LaserPointerIconProps) => {
|
||||
return (
|
||||
<label
|
||||
className={clsx(
|
||||
"ToolIcon ToolIcon__LaserPointer",
|
||||
`ToolIcon_size_${DEFAULT_SIZE}`,
|
||||
{
|
||||
"is-mobile": props.isMobile,
|
||||
},
|
||||
)}
|
||||
title={`${props.title}`}
|
||||
>
|
||||
<input
|
||||
className="ToolIcon_type_checkbox"
|
||||
type="checkbox"
|
||||
name={props.name}
|
||||
onChange={props.onChange}
|
||||
checked={props.checked}
|
||||
aria-label={props.title}
|
||||
data-testid="toolbar-LaserPointer"
|
||||
/>
|
||||
<div className="ToolIcon__icon">{laserPointerToolIcon}</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { LaserPathManager } from "./LaserPathManager";
|
||||
import "./LaserToolOverlay.scss";
|
||||
|
||||
type LaserToolOverlayProps = {
|
||||
manager: LaserPathManager;
|
||||
};
|
||||
|
||||
export const LaserToolOverlay = ({ manager }: LaserToolOverlayProps) => {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (svgRef.current) {
|
||||
manager.start(svgRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
manager.stop();
|
||||
};
|
||||
}, [manager]);
|
||||
|
||||
return (
|
||||
<div className="LaserToolOverlay">
|
||||
<svg ref={svgRef} className="LaserToolOverlayCanvas" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
.excalidraw {
|
||||
.LaserToolOverlay {
|
||||
pointer-events: none;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
.LaserToolOverlayCanvas {
|
||||
image-rendering: auto;
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-25
@@ -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,7 +121,6 @@ const LayerUI = ({
|
||||
setAppState,
|
||||
elements,
|
||||
canvas,
|
||||
interactiveCanvas,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
@@ -129,11 +128,11 @@ const LayerUI = ({
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
UIOptions,
|
||||
onImageAction,
|
||||
onExportImage,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
app,
|
||||
isCollaborating,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
@@ -277,17 +276,29 @@ const LayerUI = ({
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
setAppState={setAppState}
|
||||
onImageAction={({ pointerType }) => {
|
||||
onImageAction({
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
});
|
||||
}}
|
||||
app={app}
|
||||
/>
|
||||
</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>
|
||||
@@ -451,8 +462,6 @@ const LayerUI = ({
|
||||
onLockToggle={onLockToggle}
|
||||
onHandToolToggle={onHandToolToggle}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderSidebars={renderSidebars}
|
||||
@@ -539,18 +548,8 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
canvas: _pC,
|
||||
interactiveCanvas: _pIC,
|
||||
appState: prevAppState,
|
||||
...prev
|
||||
} = prevProps;
|
||||
const {
|
||||
canvas: _nC,
|
||||
interactiveCanvas: _nIC,
|
||||
appState: nextAppState,
|
||||
...next
|
||||
} = nextProps;
|
||||
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps;
|
||||
const { canvas: _nC, appState: nextAppState, ...next } = nextProps;
|
||||
|
||||
return (
|
||||
isShallowEqual(
|
||||
|
||||
@@ -99,10 +99,10 @@
|
||||
font-size: 0.75rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-darker);
|
||||
background-color: var(--color-brand-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-darkest);
|
||||
background-color: var(--color-brand-active);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,9 +36,7 @@ type MobileMenuProps = {
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
isMobile: boolean,
|
||||
appState: UIAppState,
|
||||
@@ -58,8 +56,7 @@ export const MobileMenu = ({
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
interactiveCanvas,
|
||||
onImageAction,
|
||||
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
renderSidebars,
|
||||
@@ -85,14 +82,8 @@ export const MobileMenu = ({
|
||||
<Stack.Row gap={1}>
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
setAppState={setAppState}
|
||||
onImageAction={({ pointerType }) => {
|
||||
onImageAction({
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
});
|
||||
}}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--RadioGroup-background: #ffffff;
|
||||
--RadioGroup-border: var(--color-gray-30);
|
||||
--RadioGroup-background: var(--island-bg-color);
|
||||
--RadioGroup-border: var(--color-surface-high);
|
||||
|
||||
--RadioGroup-choice-color-off: var(--color-primary);
|
||||
--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-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-on: white;
|
||||
--RadioGroup-choice-color-on: var(--color-surface-lowest);
|
||||
--RadioGroup-choice-background-on: var(--color-primary);
|
||||
--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-choice-background-on-hover: var(--color-brand-hover);
|
||||
--RadioGroup-choice-background-on-active: var(--color-brand-active);
|
||||
|
||||
.RadioGroup {
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
.excalidraw {
|
||||
.sidebar-trigger {
|
||||
@include outlineButtonStyles;
|
||||
|
||||
background-color: var(--island-bg-color);
|
||||
@include filledButtonOnCanvas;
|
||||
|
||||
width: auto;
|
||||
height: var(--lg-button-size);
|
||||
|
||||
+16
-14
@@ -1,15 +1,13 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--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-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 {
|
||||
position: relative;
|
||||
@@ -28,7 +26,11 @@
|
||||
|
||||
&:hover {
|
||||
background: var(--Switch-track-background);
|
||||
border: 1px solid #999999;
|
||||
border: 1px solid var(--Switch-hover-background);
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 1px solid var(--Switch-active-background);
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
@@ -43,11 +45,11 @@
|
||||
|
||||
&.disabled {
|
||||
background: var(--Switch-track-background);
|
||||
border: 1px solid var(--Switch-disabled-color);
|
||||
border: 1px solid var(--Switch-disabled-border);
|
||||
|
||||
&.toggled {
|
||||
background: var(--Switch-disabled-color);
|
||||
border: 1px solid var(--Switch-disabled-color);
|
||||
background: var(--Switch-disabled-toggled-background);
|
||||
border: 1px solid var(--Switch-disabled-toggled-background);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +94,7 @@
|
||||
}
|
||||
|
||||
&.disabled.toggled:before {
|
||||
background: var(--color-gray-50);
|
||||
background: var(--Switch-disabled-color);
|
||||
}
|
||||
|
||||
& input {
|
||||
|
||||
@@ -1,25 +1,16 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw {
|
||||
--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--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 {
|
||||
&--fullWidth {
|
||||
@@ -61,7 +52,7 @@
|
||||
|
||||
&:active,
|
||||
&:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
border-color: var(--ExcTextField--border-active);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +98,7 @@
|
||||
|
||||
&--readonly {
|
||||
background: var(--ExcTextField--readonly--background);
|
||||
border-color: transparent;
|
||||
border-color: var(--ExcTextField--readonly--border);
|
||||
|
||||
& input {
|
||||
color: var(--ExcTextField--readonly--color);
|
||||
|
||||
@@ -83,12 +83,12 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
||||
|
||||
|
||||
@@ -97,10 +97,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// &:hover {
|
||||
// background-color: var(--button-gray-2);
|
||||
// }
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-gray-3);
|
||||
}
|
||||
@@ -110,7 +106,6 @@
|
||||
}
|
||||
|
||||
&--hide {
|
||||
// visibility: hidden;
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
@@ -170,5 +165,10 @@
|
||||
height: var(--lg-icon-size);
|
||||
}
|
||||
}
|
||||
|
||||
.ToolIcon__LaserPointer .ToolIcon__icon {
|
||||
width: var(--default-button-size);
|
||||
height: var(--default-button-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,12 +22,19 @@
|
||||
.App-toolbar__extra-tools-trigger {
|
||||
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 {
|
||||
|
||||
@@ -193,6 +193,8 @@ const getRelevantAppStateProps = (
|
||||
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||
activeEmbeddable: appState.activeEmbeddable,
|
||||
snapLines: appState.snapLines,
|
||||
zenModeEnabled: appState.zenModeEnabled,
|
||||
});
|
||||
|
||||
const areEqual = (
|
||||
|
||||
@@ -114,11 +114,13 @@ 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),
|
||||
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)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,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 +29,7 @@
|
||||
}
|
||||
|
||||
.dropdown-menu-container {
|
||||
background-color: #fff !important;
|
||||
background-color: var(--island-bg-color);
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow-y: auto;
|
||||
--gap: 2;
|
||||
@@ -40,7 +40,7 @@
|
||||
padding: 0 0.625rem;
|
||||
column-gap: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-100);
|
||||
color: var(--color-on-surface);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-weight: normal;
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
.dropdown-menu-item {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border: 1px solid transparent;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
cursor: pointer;
|
||||
@@ -59,6 +59,11 @@
|
||||
height: 2.25rem;
|
||||
}
|
||||
|
||||
&--selected {
|
||||
background: var(--color-primary-light);
|
||||
--icon-fill-color: var(--color-primary-darker);
|
||||
}
|
||||
|
||||
&__text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -75,6 +80,11 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-hover-bg);
|
||||
border-color: var(--color-brand-active);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -93,22 +103,33 @@
|
||||
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,12 +11,14 @@ 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);
|
||||
@@ -26,7 +28,7 @@ const DropdownMenuItem = ({
|
||||
{...rest}
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className={getDropdownMenuItemClassName(className)}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
>
|
||||
<MenuItemContent icon={icon} shortcut={shortcut}>
|
||||
|
||||
@@ -3,15 +3,19 @@ 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}`.trim()}
|
||||
className={`dropdown-menu-item-base dropdown-menu-item-custom ${className} ${
|
||||
selected ? `dropdown-menu-item--selected` : ``
|
||||
}`.trim()}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -12,6 +12,7 @@ const DropdownMenuItemLink = ({
|
||||
children,
|
||||
onSelect,
|
||||
className = "",
|
||||
selected,
|
||||
...rest
|
||||
}: {
|
||||
href: string;
|
||||
@@ -19,6 +20,7 @@ const DropdownMenuItemLink = ({
|
||||
children: React.ReactNode;
|
||||
shortcut?: string;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
onSelect?: (event: Event) => void;
|
||||
} & React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
|
||||
@@ -29,7 +31,7 @@ const DropdownMenuItemLink = ({
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={getDropdownMenuItemClassName(className)}
|
||||
className={getDropdownMenuItemClassName(className, selected)}
|
||||
title={rest.title ?? rest["aria-label"]}
|
||||
onClick={handleClick}
|
||||
>
|
||||
|
||||
@@ -6,8 +6,13 @@ export const DropdownMenuContentPropsContext = React.createContext<{
|
||||
onSelect?: (event: Event) => void;
|
||||
}>({});
|
||||
|
||||
export const getDropdownMenuItemClassName = (className = "") => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className}`.trim();
|
||||
export const getDropdownMenuItemClassName = (
|
||||
className = "",
|
||||
selected = false,
|
||||
) => {
|
||||
return `dropdown-menu-item dropdown-menu-item-base ${className} ${
|
||||
selected ? "dropdown-menu-item--selected" : ""
|
||||
}`.trim();
|
||||
};
|
||||
|
||||
export const useHandleDropdownMenuItemClick = (
|
||||
|
||||
@@ -13,7 +13,7 @@ import clsx from "clsx";
|
||||
import { Theme } from "../element/types";
|
||||
import { THEME } from "../constants";
|
||||
|
||||
const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||
export const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
|
||||
|
||||
const handlerColor = (theme: Theme) =>
|
||||
theme === THEME.LIGHT ? oc.white : "#1e1e1e";
|
||||
@@ -1653,3 +1653,22 @@ export const frameToolIcon = createIcon(
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const laserPointerToolIcon = createIcon(
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.25"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
transform="rotate(90 10 10)"
|
||||
>
|
||||
<path
|
||||
clipRule="evenodd"
|
||||
d="m9.644 13.69 7.774-7.773a2.357 2.357 0 0 0-3.334-3.334l-7.773 7.774L8 12l1.643 1.69Z"
|
||||
/>
|
||||
<path d="m13.25 3.417 3.333 3.333M10 10l2-2M5 15l3-3M2.156 17.894l1-1M5.453 19.029l-.144-1.407M2.377 11.887l.866 1.118M8.354 17.273l-1.194-.758M.953 14.652l1.408.13" />
|
||||
</g>,
|
||||
|
||||
20,
|
||||
);
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
--button-active-bg: var(--color-primary-darker);
|
||||
|
||||
box-shadow: 0 0 0 1px var(--color-surface-lowest);
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
// double .active to force specificity
|
||||
|
||||
@@ -43,6 +43,7 @@ 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: none;
|
||||
border: 1px solid transparent;
|
||||
|
||||
padding: 0.75rem;
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
|
||||
.welcome-screen-menu-item:hover {
|
||||
text-decoration: none;
|
||||
background: var(--color-gray-10);
|
||||
background: var(--button-hover-bg);
|
||||
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
@@ -216,7 +216,8 @@
|
||||
}
|
||||
|
||||
.welcome-screen-menu-item:active {
|
||||
background: var(--color-gray-20);
|
||||
background: var(--button-hover-bg);
|
||||
border-color: var(--color-brand-active);
|
||||
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
@@ -247,8 +248,7 @@
|
||||
}
|
||||
|
||||
.welcome-screen-menu-item:hover {
|
||||
background: var(--color-gray-85);
|
||||
|
||||
background-color: var(--color-surface-low);
|
||||
.welcome-screen-menu-item__shortcut {
|
||||
color: var(--color-gray-50);
|
||||
}
|
||||
@@ -259,7 +259,6 @@
|
||||
}
|
||||
|
||||
.welcome-screen-menu-item:active {
|
||||
background-color: var(--color-gray-90);
|
||||
.welcome-screen-menu-item__text {
|
||||
color: var(--color-gray-10);
|
||||
}
|
||||
|
||||
+9
-3
@@ -296,6 +296,12 @@ export const ROUNDNESS = {
|
||||
* collaboration */
|
||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||
|
||||
export const ROUGHNESS = {
|
||||
architect: 0,
|
||||
artist: 1,
|
||||
cartoonist: 2,
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_ELEMENT_PROPS: {
|
||||
strokeColor: ExcalidrawElement["strokeColor"];
|
||||
backgroundColor: ExcalidrawElement["backgroundColor"];
|
||||
@@ -308,10 +314,10 @@ export const DEFAULT_ELEMENT_PROPS: {
|
||||
} = {
|
||||
strokeColor: COLOR_PALETTE.black,
|
||||
backgroundColor: COLOR_PALETTE.transparent,
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
roughness: ROUGHNESS.artist,
|
||||
opacity: 100,
|
||||
locked: false,
|
||||
};
|
||||
|
||||
+17
-2
@@ -444,13 +444,14 @@
|
||||
}
|
||||
|
||||
&:active {
|
||||
border: 1px solid var(--color-primary-darkest);
|
||||
border: 1px solid var(--button-active-border);
|
||||
}
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
@include outlineButtonStyles;
|
||||
background-color: var(--island-bg-color);
|
||||
@include filledButtonOnCanvas;
|
||||
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
|
||||
@@ -621,6 +622,20 @@
|
||||
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 {
|
||||
|
||||
+45
-23
@@ -12,27 +12,30 @@
|
||||
--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-gray-80);
|
||||
--icon-fill-color: var(--color-on-surface);
|
||||
--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: rgba(255, 255, 255, 0.96);
|
||||
--island-bg-color: #ffffff;
|
||||
--keybinding-color: var(--color-gray-40);
|
||||
--link-color: #{$oc-blue-7};
|
||||
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
|
||||
--popup-bg-color: #{$oc-white};
|
||||
--popup-bg-color: var(--island-bg-color);
|
||||
--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 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);
|
||||
--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);
|
||||
|
||||
--default-button-size: 2rem;
|
||||
--default-icon-size: 1rem;
|
||||
@@ -63,14 +66,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-gray-20);
|
||||
--sidebar-bg-color: #fff;
|
||||
--sidebar-border-color: var(--color-surface-high);
|
||||
--sidebar-bg-color: var(--island-bg-color);
|
||||
--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-gray-80);
|
||||
--text-primary-color: var(--color-on-surface);
|
||||
|
||||
--color-selection: #6965db;
|
||||
|
||||
@@ -132,6 +135,19 @@
|
||||
--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;
|
||||
@@ -150,29 +166,24 @@
|
||||
--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: #262627;
|
||||
--island-bg-color: #232329;
|
||||
--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};
|
||||
--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);
|
||||
--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);
|
||||
|
||||
--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),
|
||||
@@ -180,8 +191,6 @@
|
||||
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};
|
||||
@@ -224,5 +233,18 @@
|
||||
--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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
.ToolIcon_type_radio,
|
||||
.ToolIcon_type_checkbox {
|
||||
&:checked + .ToolIcon__icon {
|
||||
--icon-fill-color: var(--color-primary-darker);
|
||||
--icon-fill-color: var(--color-on-primary-container);
|
||||
|
||||
svg {
|
||||
fill: var(--icon-fill-color);
|
||||
@@ -23,11 +23,11 @@
|
||||
.ToolIcon_type_radio,
|
||||
.ToolIcon_type_checkbox {
|
||||
&:checked + .ToolIcon__icon {
|
||||
background: var(--color-primary-light);
|
||||
--keybinding-color: var(--color-gray-60);
|
||||
background: var(--color-surface-primary-container);
|
||||
--keybinding-color: var(--color-on-primary-container);
|
||||
|
||||
svg {
|
||||
color: var(--color-primary-darker);
|
||||
color: var(--color-on-primary-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,11 @@
|
||||
|
||||
&:active {
|
||||
background: var(--button-hover-bg);
|
||||
border: 1px solid var(--color-primary-darkest);
|
||||
border: 1px solid var(--button-active-border);
|
||||
|
||||
svg {
|
||||
color: var(--color-on-primary-container);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +67,7 @@
|
||||
border-radius: var(--border-radius-lg);
|
||||
cursor: pointer;
|
||||
background-color: var(--button-bg, var(--island-bg-color));
|
||||
color: var(--button-color, var(--text-primary-color));
|
||||
color: var(--button-color, var(--color-on-surface));
|
||||
|
||||
svg {
|
||||
width: var(--button-width, var(--lg-icon-size));
|
||||
@@ -88,22 +92,38 @@
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--button-selected-bg, var(--color-primary-light));
|
||||
border-color: var(--button-selected-border, var(--color-primary-light));
|
||||
background-color: var(
|
||||
--button-selected-bg,
|
||||
var(--color-surface-primary-container)
|
||||
);
|
||||
border-color: var(
|
||||
--button-selected-border,
|
||||
var(--color-surface-primary-container)
|
||||
);
|
||||
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--button-selected-hover-bg,
|
||||
var(--color-primary-light)
|
||||
var(--color-surface-primary-container)
|
||||
);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--button-color, var(--color-primary-darker));
|
||||
color: var(--button-color, var(--color-on-primary-container));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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";
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { CURSOR_TYPE, MIME_TYPES, THEME } from "./constants";
|
||||
import OpenColor from "open-color";
|
||||
import { AppState, DataURL } from "./types";
|
||||
import { isHandToolActive, isEraserActive } from "./appState";
|
||||
|
||||
const laserPointerCursorSVG_tag = `<svg viewBox="0 0 24 24" stroke-width="1" width="28" height="28" xmlns="http://www.w3.org/2000/svg">`;
|
||||
const laserPointerCursorBackgroundSVG = `<path d="M6.164 11.755a5.314 5.314 0 0 1-4.932-5.298 5.314 5.314 0 0 1 5.311-5.311 5.314 5.314 0 0 1 5.307 5.113l8.773 8.773a3.322 3.322 0 0 1 0 4.696l-.895.895a3.322 3.322 0 0 1-4.696 0l-8.868-8.868Z" style="fill:#fff"/>`;
|
||||
const laserPointerCursorIconSVG = `<path stroke="#1b1b1f" fill="#fff" d="m7.868 11.113 7.773 7.774a2.359 2.359 0 0 0 1.667.691 2.368 2.368 0 0 0 2.357-2.358c0-.625-.248-1.225-.69-1.667L11.201 7.78 9.558 9.469l-1.69 1.643v.001Zm10.273 3.606-3.333 3.333m-3.25-6.583 2 2m-7-7 3 3M3.664 3.625l1 1M2.529 6.922l1.407-.144m5.735-2.932-1.118.866M4.285 9.823l.758-1.194m1.863-6.207-.13 1.408"/>`;
|
||||
|
||||
const laserPointerCursorDataURL_lightMode = `data:${
|
||||
MIME_TYPES.svg
|
||||
},${encodeURIComponent(
|
||||
`${laserPointerCursorSVG_tag}${laserPointerCursorIconSVG}</svg>`,
|
||||
)}`;
|
||||
const laserPointerCursorDataURL_darkMode = `data:${
|
||||
MIME_TYPES.svg
|
||||
},${encodeURIComponent(
|
||||
`${laserPointerCursorSVG_tag}${laserPointerCursorBackgroundSVG}${laserPointerCursorIconSVG}</svg>`,
|
||||
)}`;
|
||||
|
||||
export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
|
||||
if (interactiveCanvas) {
|
||||
interactiveCanvas.style.cursor = "";
|
||||
}
|
||||
};
|
||||
|
||||
export const setCursor = (
|
||||
interactiveCanvas: HTMLCanvasElement | null,
|
||||
cursor: string,
|
||||
) => {
|
||||
if (interactiveCanvas) {
|
||||
interactiveCanvas.style.cursor = cursor;
|
||||
}
|
||||
};
|
||||
|
||||
let eraserCanvasCache: any;
|
||||
let previewDataURL: string;
|
||||
export const setEraserCursor = (
|
||||
interactiveCanvas: HTMLCanvasElement | null,
|
||||
theme: AppState["theme"],
|
||||
) => {
|
||||
const cursorImageSizePx = 20;
|
||||
|
||||
const drawCanvas = () => {
|
||||
const isDarkTheme = theme === THEME.DARK;
|
||||
eraserCanvasCache = document.createElement("canvas");
|
||||
eraserCanvasCache.theme = theme;
|
||||
eraserCanvasCache.height = cursorImageSizePx;
|
||||
eraserCanvasCache.width = cursorImageSizePx;
|
||||
const context = eraserCanvasCache.getContext("2d")!;
|
||||
context.lineWidth = 1;
|
||||
context.beginPath();
|
||||
context.arc(
|
||||
eraserCanvasCache.width / 2,
|
||||
eraserCanvasCache.height / 2,
|
||||
5,
|
||||
0,
|
||||
2 * Math.PI,
|
||||
);
|
||||
context.fillStyle = isDarkTheme ? OpenColor.black : OpenColor.white;
|
||||
context.fill();
|
||||
context.strokeStyle = isDarkTheme ? OpenColor.white : OpenColor.black;
|
||||
context.stroke();
|
||||
previewDataURL = eraserCanvasCache.toDataURL(MIME_TYPES.svg) as DataURL;
|
||||
};
|
||||
if (!eraserCanvasCache || eraserCanvasCache.theme !== theme) {
|
||||
drawCanvas();
|
||||
}
|
||||
|
||||
setCursor(
|
||||
interactiveCanvas,
|
||||
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
||||
cursorImageSizePx / 2
|
||||
}, auto`,
|
||||
);
|
||||
};
|
||||
|
||||
export const setCursorForShape = (
|
||||
interactiveCanvas: HTMLCanvasElement | null,
|
||||
appState: Pick<AppState, "activeTool" | "theme">,
|
||||
) => {
|
||||
if (!interactiveCanvas) {
|
||||
return;
|
||||
}
|
||||
if (appState.activeTool.type === "selection") {
|
||||
resetCursor(interactiveCanvas);
|
||||
} else if (isHandToolActive(appState)) {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
|
||||
} else if (isEraserActive(appState)) {
|
||||
setEraserCursor(interactiveCanvas, appState.theme);
|
||||
// do nothing if image tool is selected which suggests there's
|
||||
// a image-preview set as the cursor
|
||||
// Ignore custom type as well and let host decide
|
||||
} else if (appState.activeTool.type === "laser") {
|
||||
const url =
|
||||
appState.theme === THEME.LIGHT
|
||||
? laserPointerCursorDataURL_lightMode
|
||||
: laserPointerCursorDataURL_darkMode;
|
||||
interactiveCanvas.style.cursor = `url(${url}), auto`;
|
||||
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
||||
} else {
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
|
||||
}
|
||||
};
|
||||
@@ -14,7 +14,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 300,
|
||||
@@ -28,7 +28,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#66a80f",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -49,7 +49,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -63,7 +63,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#9c36b5",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "diamond",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -85,7 +85,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"focus": -0.008153707962747813,
|
||||
"gap": 1,
|
||||
},
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 35,
|
||||
@@ -116,7 +116,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
},
|
||||
"strokeColor": "#1864ab",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -138,7 +138,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"focus": 0.10666666666666667,
|
||||
"gap": 3.834326468444573,
|
||||
},
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -169,7 +169,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
},
|
||||
"strokeColor": "#e67700",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -190,7 +190,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 300,
|
||||
@@ -204,7 +204,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -227,7 +227,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
},
|
||||
],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -245,7 +245,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#c2255c",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "HEYYYYY",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
@@ -271,7 +271,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
},
|
||||
],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -289,7 +289,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "Whats up ?",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
@@ -319,7 +319,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"focus": 0,
|
||||
"gap": 5,
|
||||
},
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -350,7 +350,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -368,7 +368,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id43",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -386,7 +386,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "HELLO WORLD!!",
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
@@ -416,7 +416,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -447,7 +447,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -465,7 +465,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id32",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -483,7 +483,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "HELLO WORLD!!",
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
@@ -507,7 +507,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -521,7 +521,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -542,7 +542,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"type": "arrow",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -556,7 +556,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -583,7 +583,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
},
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -614,7 +614,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
},
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -632,7 +632,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id36",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -650,7 +650,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "HELLO WORLD!!",
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
@@ -676,7 +676,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
},
|
||||
],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -694,7 +694,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "HEYYYYY",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
@@ -720,7 +720,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
},
|
||||
],
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -738,7 +738,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "WHATS UP ?",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
@@ -757,7 +757,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 200,
|
||||
@@ -771,7 +771,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -789,7 +789,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
||||
"boundElements": null,
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -816,7 +816,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -834,7 +834,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
|
||||
"boundElements": null,
|
||||
"endArrowhead": "triangle",
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -879,7 +879,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
||||
"boundElements": null,
|
||||
"endArrowhead": null,
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -906,7 +906,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -924,7 +924,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
|
||||
"boundElements": null,
|
||||
"endArrowhead": null,
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -967,7 +967,7 @@ exports[`Test Transform > should transform regular shapes 1`] = `
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -981,7 +981,7 @@ exports[`Test Transform > should transform regular shapes 1`] = `
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -997,7 +997,7 @@ exports[`Test Transform > should transform regular shapes 2`] = `
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -1011,7 +1011,7 @@ exports[`Test Transform > should transform regular shapes 2`] = `
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -1027,7 +1027,7 @@ exports[`Test Transform > should transform regular shapes 3`] = `
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -1041,7 +1041,7 @@ exports[`Test Transform > should transform regular shapes 3`] = `
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "diamond",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -1057,7 +1057,7 @@ exports[`Test Transform > should transform regular shapes 4`] = `
|
||||
"angle": 0,
|
||||
"backgroundColor": "#c0eb75",
|
||||
"boundElements": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
@@ -1149,7 +1149,7 @@ exports[`Test Transform > should transform text element 1`] = `
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1167,7 +1167,7 @@ exports[`Test Transform > should transform text element 1`] = `
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "HELLO WORLD!",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
@@ -1188,7 +1188,7 @@ exports[`Test Transform > should transform text element 2`] = `
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1206,7 +1206,7 @@ exports[`Test Transform > should transform text element 2`] = `
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#5f3dc4",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "STYLED HELLO WORLD!",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
@@ -1232,7 +1232,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
],
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -1259,7 +1259,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -1282,7 +1282,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
],
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 0,
|
||||
@@ -1309,7 +1309,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 1,
|
||||
@@ -1332,7 +1332,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
],
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 130,
|
||||
@@ -1382,7 +1382,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
],
|
||||
"endArrowhead": "arrow",
|
||||
"endBinding": null,
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 130,
|
||||
@@ -1427,7 +1427,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id24",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1445,7 +1445,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "LABELED ARROW",
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
@@ -1466,7 +1466,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id25",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1484,7 +1484,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#099268",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "STYLED LABELED ARROW",
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
@@ -1505,7 +1505,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id26",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1523,7 +1523,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1098ad",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "ANOTHER STYLED
|
||||
LABELLED ARROW",
|
||||
"textAlign": "center",
|
||||
@@ -1545,7 +1545,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id27",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1563,7 +1563,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#099268",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "ANOTHER STYLED
|
||||
LABELLED ARROW",
|
||||
"textAlign": "center",
|
||||
@@ -1588,7 +1588,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 35,
|
||||
@@ -1602,7 +1602,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
@@ -1623,7 +1623,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 85,
|
||||
@@ -1637,7 +1637,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -1658,7 +1658,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 170,
|
||||
@@ -1672,7 +1672,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "diamond",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -1693,7 +1693,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 120,
|
||||
@@ -1728,7 +1728,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 85,
|
||||
@@ -1742,7 +1742,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#c2255c",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -1763,7 +1763,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 120,
|
||||
@@ -1777,7 +1777,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#f08c00",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
@@ -1795,7 +1795,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id12",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1813,7 +1813,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "RECTANGLE TEXT CONTAINER",
|
||||
"textAlign": "center",
|
||||
"type": "text",
|
||||
@@ -1834,7 +1834,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id13",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1852,7 +1852,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "ELLIPSE TEXT
|
||||
CONTAINER",
|
||||
"textAlign": "center",
|
||||
@@ -1874,7 +1874,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id14",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1893,7 +1893,7 @@ TEXT CONTAINER",
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "DIAMOND
|
||||
TEXT
|
||||
CONTAINER",
|
||||
@@ -1916,7 +1916,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id15",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1934,7 +1934,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#099268",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "STYLED DIAMOND
|
||||
TEXT CONTAINER",
|
||||
"textAlign": "center",
|
||||
@@ -1956,7 +1956,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id16",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -1974,7 +1974,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#c2255c",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "TOP LEFT ALIGNED
|
||||
RECTANGLE TEXT
|
||||
CONTAINER",
|
||||
@@ -1997,7 +1997,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"baseline": 0,
|
||||
"boundElements": null,
|
||||
"containerId": "id17",
|
||||
"fillStyle": "hachure",
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 1,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
@@ -2015,7 +2015,7 @@ exports[`Test Transform > should transform to text containers when label provide
|
||||
"seed": Any<Number>,
|
||||
"strokeColor": "#c2255c",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeWidth": 2,
|
||||
"text": "STYLED
|
||||
ELLIPSE TEXT
|
||||
CONTAINER",
|
||||
|
||||
+31
-3
@@ -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 } from "../utils";
|
||||
import { bytesToHexString, isPromiseLike } from "../utils";
|
||||
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||
import { restore, restoreLibraryItems } from "./restore";
|
||||
@@ -207,10 +207,13 @@ export const loadLibraryFromBlob = async (
|
||||
};
|
||||
|
||||
export const canvasToBlob = async (
|
||||
canvas: HTMLCanvasElement,
|
||||
canvas: HTMLCanvasElement | Promise<HTMLCanvasElement>,
|
||||
): Promise<Blob> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
if (isPromiseLike(canvas)) {
|
||||
canvas = await canvas;
|
||||
}
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) {
|
||||
return reject(
|
||||
@@ -324,6 +327,31 @@ 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>,
|
||||
) => {
|
||||
|
||||
+1
-7
@@ -66,17 +66,14 @@ export const exportCanvas = async (
|
||||
}
|
||||
}
|
||||
|
||||
const tempCanvas = await exportToCanvas(elements, appState, files, {
|
||||
const tempCanvas = 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")
|
||||
@@ -114,11 +111,8 @@ 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");
|
||||
}
|
||||
|
||||
+27
-7
@@ -43,6 +43,7 @@ import {
|
||||
measureBaseline,
|
||||
} from "../element/textElement";
|
||||
import { normalizeLink } from "./url";
|
||||
import { isValidFrameChild } from "../frame";
|
||||
|
||||
type RestoredAppState = Omit<
|
||||
AppState,
|
||||
@@ -67,6 +68,7 @@ export const AllowedExcalidrawActiveTools: Record<
|
||||
frame: true,
|
||||
embeddable: true,
|
||||
hand: true,
|
||||
laser: false,
|
||||
};
|
||||
|
||||
export type RestoredDataState = {
|
||||
@@ -188,7 +190,7 @@ const restoreElement = (
|
||||
fontSize = parseFloat(fontPx);
|
||||
fontFamily = getFontFamilyByName(_fontFamily);
|
||||
}
|
||||
const text = element.text ?? "";
|
||||
const text = (typeof element.text === "string" && element.text) || "";
|
||||
|
||||
// line-height might not be specified either when creating elements
|
||||
// programmatically, or when importing old diagrams.
|
||||
@@ -221,9 +223,17 @@ 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, {
|
||||
@@ -275,6 +285,9 @@ const restoreElement = (
|
||||
points,
|
||||
x,
|
||||
y,
|
||||
segmentSplitIndices: element.segmentSplitIndices
|
||||
? [...element.segmentSplitIndices]
|
||||
: [],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -298,6 +311,7 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -386,7 +400,7 @@ const repairBoundElement = (
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove an element's frameId if its containing frame is non-existent
|
||||
* resets `frameId` if no longer applicable.
|
||||
*
|
||||
* NOTE mutates elements.
|
||||
*/
|
||||
@@ -394,12 +408,16 @@ const repairFrameMembership = (
|
||||
element: Mutable<ExcalidrawElement>,
|
||||
elementsMap: Map<string, Mutable<ExcalidrawElement>>,
|
||||
) => {
|
||||
if (element.frameId) {
|
||||
const containingFrame = elementsMap.get(element.frameId);
|
||||
if (!element.frameId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!containingFrame) {
|
||||
element.frameId = null;
|
||||
}
|
||||
if (
|
||||
!isValidFrameChild(element) ||
|
||||
// target frame not exists
|
||||
!elementsMap.get(element.frameId)
|
||||
) {
|
||||
element.frameId = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -443,6 +461,8 @@ 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);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ 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>
|
||||
@@ -211,6 +212,15 @@ export const bindLinearElement = (
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (linearElement.frameId && !isValidFrameChild(linearElement)) {
|
||||
mutateElement(
|
||||
linearElement,
|
||||
{
|
||||
frameId: null,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Don't bind both ends of a simple segment
|
||||
|
||||
+15
-2
@@ -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,6 +674,19 @@ 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,
|
||||
@@ -728,7 +741,7 @@ export const getElementPointsCoords = (
|
||||
element: ExcalidrawLinearElement,
|
||||
points: readonly (readonly [number, number])[],
|
||||
): [number, number, number, number] => {
|
||||
// This might be computationally heavey
|
||||
// This might be computationally heavy
|
||||
const gen = rough.generator();
|
||||
const curve =
|
||||
element.roundness == null
|
||||
|
||||
@@ -494,7 +494,7 @@ const hitTestFreeDrawElement = (
|
||||
// for filled freedraw shapes, support
|
||||
// selecting from inside
|
||||
if (shape && shape.sets.length) {
|
||||
return hitTestRoughShape(shape, x, y, threshold);
|
||||
return hitTestCurveInside(shape, x, y, "round");
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
+43
-33
@@ -6,23 +6,22 @@ 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";
|
||||
|
||||
export const dragSelectedElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
lockDirection: boolean = false,
|
||||
distanceX: number = 0,
|
||||
distanceY: number = 0,
|
||||
offset: { x: number; y: number },
|
||||
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
|
||||
@@ -44,12 +43,11 @@ 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)
|
||||
@@ -69,12 +67,11 @@ export const dragSelectedElements = (
|
||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
||||
) {
|
||||
updateElementCoords(
|
||||
lockDirection,
|
||||
distanceX,
|
||||
distanceY,
|
||||
pointerDownState,
|
||||
textElement,
|
||||
offset,
|
||||
snapOffset,
|
||||
gridSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -85,31 +82,40 @@ export const dragSelectedElements = (
|
||||
};
|
||||
|
||||
const updateElementCoords = (
|
||||
lockDirection: boolean,
|
||||
distanceX: number,
|
||||
distanceY: number,
|
||||
pointerDownState: PointerDownState,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
offset: { x: number; y: number },
|
||||
dragOffset: { x: number; y: number },
|
||||
snapOffset: { x: number; y: number },
|
||||
gridSize: AppState["gridSize"],
|
||||
) => {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
x,
|
||||
y,
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
});
|
||||
};
|
||||
|
||||
export const getDragOffsetXY = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
x: number,
|
||||
@@ -133,6 +139,10 @@ 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) {
|
||||
@@ -173,8 +183,8 @@ export const dragNewElement = (
|
||||
|
||||
if (width !== 0 && height !== 0) {
|
||||
mutateElement(draggingElement, {
|
||||
x: newX,
|
||||
y: newY,
|
||||
x: newX + (originOffset?.x ?? 0),
|
||||
y: newY + (originOffset?.y ?? 0),
|
||||
width,
|
||||
height,
|
||||
});
|
||||
|
||||
+48
-27
@@ -2,7 +2,8 @@ import { register } from "../actions/register";
|
||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { ExcalidrawProps } from "../types";
|
||||
import { getFontString, setCursorForShape, updateActiveTool } from "../utils";
|
||||
import { getFontString, updateActiveTool } from "../utils";
|
||||
import { setCursorForShape } from "../cursor";
|
||||
import { newTextElement } from "./newElement";
|
||||
import { getContainerElement, wrapText } from "./textElement";
|
||||
import { isEmbeddableElement } from "./typeChecks";
|
||||
@@ -17,7 +18,6 @@ type EmbeddedLink =
|
||||
| ({
|
||||
aspectRatio: { w: number; h: number };
|
||||
warning?: string;
|
||||
sandbox?: { allowSameOrigin?: boolean };
|
||||
} & (
|
||||
| { type: "video" | "generic"; link: string }
|
||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||
@@ -28,21 +28,22 @@ 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]+)(?:\?.*)?$/;
|
||||
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
||||
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
|
||||
|
||||
const RE_GH_GIST = /^https:\/\/gist\.github\.com/;
|
||||
const RE_GH_GIST_EMBED =
|
||||
/https?:\/\/gist\.github\.com\/([\w_-]+)\/([\w_-]+)\.js["']/i;
|
||||
/^<script[\s\S]*?\ssrc=["'](https:\/\/gist.github.com\/.*?)\.js["']/i;
|
||||
|
||||
// not anchored to start to allow <blockquote> twitter embeds
|
||||
const RE_TWITTER = /(?:https?:\/\/)?(?:(?:w){3}\.)?(?:twitter|x)\.com/;
|
||||
const RE_TWITTER = /(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?twitter.com/;
|
||||
const RE_TWITTER_EMBED =
|
||||
/^<blockquote[\s\S]*?\shref=["'](https?:\/\/(?:twitter|x)\.com\/[^"']*)/i;
|
||||
/^<blockquote[\s\S]*?\shref=["'](https:\/\/twitter.com\/[^"']*)/i;
|
||||
|
||||
const RE_VALTOWN =
|
||||
/^https:\/\/(?:www\.)?val\.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
||||
/^https:\/\/(?:www\.)?val.town\/(v|embed)\/[a-zA-Z_$][0-9a-zA-Z_$]+\.[a-zA-Z_$][0-9a-zA-Z_$]+/;
|
||||
|
||||
const RE_GENERIC_EMBED =
|
||||
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||
@@ -137,27 +138,46 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
||||
}
|
||||
|
||||
if (RE_TWITTER.test(link)) {
|
||||
// the embed srcdoc still supports twitter.com domain only
|
||||
link = link.replace(/\bx.com\b/, "twitter.com");
|
||||
|
||||
const ret: EmbeddedLink = {
|
||||
type: "document",
|
||||
srcdoc: (theme: string) =>
|
||||
createSrcDoc(
|
||||
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
|
||||
),
|
||||
aspectRatio: { w: 480, h: 480 },
|
||||
sandbox: { allowSameOrigin: true },
|
||||
};
|
||||
let ret: EmbeddedLink;
|
||||
// assume embed code
|
||||
if (/<blockquote/.test(link)) {
|
||||
const srcDoc = createSrcDoc(link);
|
||||
ret = {
|
||||
type: "document",
|
||||
srcdoc: () => srcDoc,
|
||||
aspectRatio: { w: 480, h: 480 },
|
||||
};
|
||||
// assume regular tweet url
|
||||
} else {
|
||||
ret = {
|
||||
type: "document",
|
||||
srcdoc: (theme: string) =>
|
||||
createSrcDoc(
|
||||
`<blockquote class="twitter-tweet" data-dnt="true" data-theme="${theme}"><a href="${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>`,
|
||||
),
|
||||
aspectRatio: { w: 480, h: 480 },
|
||||
};
|
||||
}
|
||||
embeddedLinkCache.set(originalLink, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (RE_GH_GIST.test(link)) {
|
||||
const ret: EmbeddedLink = {
|
||||
type: "document",
|
||||
srcdoc: () =>
|
||||
createSrcDoc(`
|
||||
let ret: EmbeddedLink;
|
||||
// assume embed code
|
||||
if (/<script>/.test(link)) {
|
||||
const srcDoc = createSrcDoc(link);
|
||||
ret = {
|
||||
type: "document",
|
||||
srcdoc: () => srcDoc,
|
||||
aspectRatio: { w: 550, h: 720 },
|
||||
};
|
||||
// assume regular url
|
||||
} else {
|
||||
ret = {
|
||||
type: "document",
|
||||
srcdoc: () =>
|
||||
createSrcDoc(`
|
||||
<script src="${link}.js"></script>
|
||||
<style type="text/css">
|
||||
* { margin: 0px; }
|
||||
@@ -165,8 +185,9 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
||||
.gist .gist-file { height: calc(100vh - 2px); padding: 0px; display: grid; grid-template-rows: 1fr auto; }
|
||||
</style>
|
||||
`),
|
||||
aspectRatio: { w: 550, h: 720 },
|
||||
};
|
||||
aspectRatio: { w: 550, h: 720 },
|
||||
};
|
||||
}
|
||||
embeddedLinkCache.set(link, ret);
|
||||
return ret;
|
||||
}
|
||||
@@ -284,8 +305,8 @@ export const extractSrc = (htmlString: string): string => {
|
||||
}
|
||||
|
||||
const gistMatch = htmlString.match(RE_GH_GIST_EMBED);
|
||||
if (gistMatch && gistMatch.length === 3) {
|
||||
return `https://gist.github.com/${gistMatch[1]}/${gistMatch[2]}`;
|
||||
if (gistMatch && gistMatch.length === 2) {
|
||||
return gistMatch[1];
|
||||
}
|
||||
|
||||
const match = htmlString.match(RE_GENERIC_EMBED);
|
||||
|
||||
@@ -547,7 +547,10 @@ export class LinearElementEditor {
|
||||
endPointIndex: number,
|
||||
) {
|
||||
let segmentMidPoint = centerPoint(startPoint, endPoint);
|
||||
if (element.points.length > 2 && element.roundness) {
|
||||
const splits = element.segmentSplitIndices || [];
|
||||
const treatAsCurve =
|
||||
splits.includes(endPointIndex) || splits.includes(endPointIndex - 1);
|
||||
if (element.points.length > 2 && (element.roundness || treatAsCurve)) {
|
||||
const controlPoints = getControlPointsForBezierCurve(
|
||||
element,
|
||||
element.points[endPointIndex],
|
||||
@@ -1042,13 +1045,15 @@ export class LinearElementEditor {
|
||||
let offsetX = 0;
|
||||
let offsetY = 0;
|
||||
|
||||
const isDeletingOriginPoint = pointIndices.includes(0);
|
||||
const indexSet = new Set(pointIndices);
|
||||
|
||||
const isDeletingOriginPoint = indexSet.has(0);
|
||||
|
||||
// if deleting first point, make the next to be [0,0] and recalculate
|
||||
// positions of the rest with respect to it
|
||||
if (isDeletingOriginPoint) {
|
||||
const firstNonDeletedPoint = element.points.find((point, idx) => {
|
||||
return !pointIndices.includes(idx);
|
||||
return !indexSet.has(idx);
|
||||
});
|
||||
if (firstNonDeletedPoint) {
|
||||
offsetX = firstNonDeletedPoint[0];
|
||||
@@ -1057,7 +1062,7 @@ export class LinearElementEditor {
|
||||
}
|
||||
|
||||
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
|
||||
if (!pointIndices.includes(idx)) {
|
||||
if (!indexSet.has(idx)) {
|
||||
acc.push(
|
||||
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
|
||||
);
|
||||
@@ -1065,7 +1070,22 @@ export class LinearElementEditor {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
|
||||
const splits: number[] = [];
|
||||
(element.segmentSplitIndices || []).forEach((index) => {
|
||||
if (!indexSet.has(index)) {
|
||||
let shift = 0;
|
||||
for (const pointIndex of pointIndices) {
|
||||
if (index > pointIndex) {
|
||||
shift++;
|
||||
}
|
||||
}
|
||||
splits.push(index - shift);
|
||||
}
|
||||
});
|
||||
|
||||
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY, {
|
||||
segmentSplitIndices: splits.sort((a, b) => a - b),
|
||||
});
|
||||
}
|
||||
|
||||
static addPoints(
|
||||
@@ -1204,9 +1224,13 @@ export class LinearElementEditor {
|
||||
midpoint,
|
||||
...element.points.slice(segmentMidpoint.index!),
|
||||
];
|
||||
const splits = (element.segmentSplitIndices || []).map((index) =>
|
||||
index >= segmentMidpoint.index! ? index + 1 : index,
|
||||
);
|
||||
|
||||
mutateElement(element, {
|
||||
points,
|
||||
segmentSplitIndices: splits.sort((a, b) => a - b),
|
||||
});
|
||||
|
||||
ret.pointerDownState = {
|
||||
@@ -1226,7 +1250,11 @@ export class LinearElementEditor {
|
||||
nextPoints: readonly Point[],
|
||||
offsetX: number,
|
||||
offsetY: number,
|
||||
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
|
||||
otherUpdates?: {
|
||||
startBinding?: PointBinding;
|
||||
endBinding?: PointBinding;
|
||||
segmentSplitIndices?: number[];
|
||||
},
|
||||
) {
|
||||
const nextCoords = getElementPointsCoords(element, nextPoints);
|
||||
const prevCoords = getElementPointsCoords(element, element.points);
|
||||
@@ -1472,6 +1500,27 @@ export class LinearElementEditor {
|
||||
|
||||
return coords;
|
||||
};
|
||||
|
||||
static toggleSegmentSplitAtIndex(
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
index: number,
|
||||
) {
|
||||
let found = false;
|
||||
const splitIndices = (element.segmentSplitIndices || []).filter((idx) => {
|
||||
if (idx === index) {
|
||||
found = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!found) {
|
||||
splitIndices.push(index);
|
||||
}
|
||||
|
||||
mutateElement(element, {
|
||||
segmentSplitIndices: splitIndices.sort((a, b) => a - b),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeSelectedPoints = (
|
||||
|
||||
@@ -25,7 +25,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
|
||||
// casting to any because can't use `in` operator
|
||||
// (see https://github.com/microsoft/TypeScript/issues/21732)
|
||||
const { points, fileId } = updates as any;
|
||||
const { points, fileId, segmentSplitIndices } = updates as any;
|
||||
|
||||
if (typeof points !== "undefined") {
|
||||
updates = { ...getSizeFromPoints(points), ...updates };
|
||||
@@ -86,6 +86,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
if (
|
||||
typeof updates.height !== "undefined" ||
|
||||
typeof updates.width !== "undefined" ||
|
||||
typeof segmentSplitIndices !== "undefined" ||
|
||||
typeof fileId != "undefined" ||
|
||||
typeof points !== "undefined"
|
||||
) {
|
||||
@@ -140,8 +141,8 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
|
||||
*
|
||||
* NOTE: does not trigger re-render.
|
||||
*/
|
||||
export const bumpVersion = (
|
||||
element: Mutable<ExcalidrawElement>,
|
||||
export const bumpVersion = <T extends Mutable<ExcalidrawElement>>(
|
||||
element: T,
|
||||
version?: ExcalidrawElement["version"],
|
||||
) => {
|
||||
element.version = (version ?? element.version) + 1;
|
||||
|
||||
@@ -374,6 +374,7 @@ export const newLinearElement = (
|
||||
endBinding: null,
|
||||
startArrowhead: opts.startArrowhead || null,
|
||||
endArrowhead: opts.endArrowhead || null,
|
||||
segmentSplitIndices: [],
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
MaybeTransformHandleType,
|
||||
TransformHandleDirection,
|
||||
} from "./transformHandles";
|
||||
import { Point, PointerDownState } from "../types";
|
||||
import { AppState, Point, PointerDownState } from "../types";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
getApproxMinLineWidth,
|
||||
@@ -79,6 +79,7 @@ export const transformElements = (
|
||||
pointerY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
@@ -466,8 +467,8 @@ export const resizeSingleElement = (
|
||||
boundTextElement.fontSize,
|
||||
boundTextElement.lineHeight,
|
||||
);
|
||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||
eleNewWidth = Math.max(eleNewWidth, minWidth);
|
||||
eleNewHeight = Math.max(eleNewHeight, minHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,8 +509,11 @@ export const resizeSingleElement = (
|
||||
}
|
||||
}
|
||||
|
||||
const flipX = eleNewWidth < 0;
|
||||
const flipY = eleNewHeight < 0;
|
||||
|
||||
// Flip horizontally
|
||||
if (eleNewWidth < 0) {
|
||||
if (flipX) {
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
newTopLeft[0] -= Math.abs(newBoundsWidth);
|
||||
}
|
||||
@@ -517,8 +521,9 @@ export const resizeSingleElement = (
|
||||
newTopLeft[0] += Math.abs(newBoundsWidth);
|
||||
}
|
||||
}
|
||||
|
||||
// Flip vertically
|
||||
if (eleNewHeight < 0) {
|
||||
if (flipY) {
|
||||
if (transformHandleDirection.includes("s")) {
|
||||
newTopLeft[1] -= Math.abs(newBoundsHeight);
|
||||
}
|
||||
@@ -542,10 +547,20 @@ 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,
|
||||
@@ -562,16 +577,11 @@ 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: newOrigin[0],
|
||||
y: newOrigin[1],
|
||||
x: nextX,
|
||||
y: nextY,
|
||||
points: rescaledPoints,
|
||||
};
|
||||
|
||||
@@ -680,6 +690,10 @@ 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> = {
|
||||
|
||||
@@ -12,6 +12,7 @@ export const showSelectedShapeActions = (
|
||||
(appState.editingElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
appState.activeTool.type !== "eraser" &&
|
||||
appState.activeTool.type !== "hand"))) ||
|
||||
appState.activeTool.type !== "hand" &&
|
||||
appState.activeTool.type !== "laser"))) ||
|
||||
getSelectedElements(elements, appState).length),
|
||||
);
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
} from "./types";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { resize } from "../tests/utils";
|
||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||
import { getTextEditor } from "../tests/queries/dom";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
@@ -26,12 +26,6 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
const tab = " ";
|
||||
const mouse = new Pointer("mouse");
|
||||
|
||||
const getTextEditor = () => {
|
||||
return document.querySelector(
|
||||
".excalidraw-textEditorContainer > textarea",
|
||||
) as HTMLTextAreaElement;
|
||||
};
|
||||
|
||||
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
|
||||
fireEvent.change(editor, { target: { value } });
|
||||
editor.dispatchEvent(new Event("input"));
|
||||
@@ -186,7 +180,7 @@ describe("textWysiwyg", () => {
|
||||
expect(h.state.editingElement?.id).toBe(boundText.id);
|
||||
});
|
||||
|
||||
it("should edit text under cursor when clicked with text tool", () => {
|
||||
it("should edit text under cursor when clicked with text tool", async () => {
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: "ola",
|
||||
@@ -201,14 +195,14 @@ describe("textWysiwyg", () => {
|
||||
|
||||
mouse.clickAt(text.x + 50, text.y + 50);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(false);
|
||||
|
||||
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", () => {
|
||||
it("should edit text under cursor when double-clicked with selection tool", async () => {
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
text: "ola",
|
||||
@@ -223,7 +217,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(false);
|
||||
|
||||
expect(editor).not.toBe(null);
|
||||
expect(h.state.editingElement?.id).toBe(text.id);
|
||||
@@ -250,7 +244,7 @@ describe("textWysiwyg", () => {
|
||||
textElement = UI.createElement("text");
|
||||
|
||||
mouse.clickOn(textElement);
|
||||
textarea = getTextEditor();
|
||||
textarea = await getTextEditor(true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@@ -460,7 +454,7 @@ describe("textWysiwyg", () => {
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(750, 300);
|
||||
|
||||
textarea = getTextEditor();
|
||||
textarea = await getTextEditor(true);
|
||||
updateTextEditor(
|
||||
textarea,
|
||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||
@@ -512,7 +506,7 @@ describe("textWysiwyg", () => {
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@@ -540,7 +534,7 @@ describe("textWysiwyg", () => {
|
||||
]);
|
||||
expect(text.angle).toBe(rectangle.angle);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@@ -567,7 +561,7 @@ describe("textWysiwyg", () => {
|
||||
API.setSelectedElements([diamond]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const value = new Array(1000).fill("1").join("\n");
|
||||
@@ -602,7 +596,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(null);
|
||||
mouse.down();
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
|
||||
@@ -617,7 +611,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
|
||||
mouse.down();
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@@ -639,7 +633,7 @@ describe("textWysiwyg", () => {
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
@@ -674,7 +668,7 @@ describe("textWysiwyg", () => {
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@@ -699,7 +693,7 @@ describe("textWysiwyg", () => {
|
||||
freedraw.y + freedraw.height / 2,
|
||||
);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||
|
||||
@@ -733,7 +727,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(null);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@@ -748,7 +742,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(20, 30);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(
|
||||
editor,
|
||||
@@ -793,7 +787,7 @@ describe("textWysiwyg", () => {
|
||||
mouse.down();
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
@@ -806,7 +800,7 @@ describe("textWysiwyg", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
mouse.down();
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
|
||||
editor.select();
|
||||
fireEvent.click(screen.getByTitle(/code/i));
|
||||
@@ -839,7 +833,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
Keyboard.keyDown(KEYS.ENTER);
|
||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@@ -860,7 +854,7 @@ describe("textWysiwyg", () => {
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
updateTextEditor(editor, "Hello");
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@@ -889,7 +883,7 @@ describe("textWysiwyg", () => {
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
@@ -926,7 +920,7 @@ describe("textWysiwyg", () => {
|
||||
// Bind first text
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
editor.blur();
|
||||
@@ -947,24 +941,24 @@ describe("textWysiwyg", () => {
|
||||
it("should respect text alignment when resizing", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
|
||||
// should center align horizontally and vertically by default
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
[
|
||||
85,
|
||||
4.5,
|
||||
4.999999999999986,
|
||||
]
|
||||
`);
|
||||
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
|
||||
editor.select();
|
||||
|
||||
@@ -977,7 +971,7 @@ describe("textWysiwyg", () => {
|
||||
editor.blur();
|
||||
|
||||
// should left align horizontally and bottom vertically after resize
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
[
|
||||
15,
|
||||
@@ -987,7 +981,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
|
||||
editor.select();
|
||||
|
||||
@@ -999,11 +993,11 @@ describe("textWysiwyg", () => {
|
||||
editor.blur();
|
||||
|
||||
// should right align horizontally and top vertically after resize
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||
[
|
||||
375,
|
||||
-539,
|
||||
374.99999999999994,
|
||||
-535.0000000000001,
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -1025,7 +1019,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@@ -1040,7 +1034,7 @@ describe("textWysiwyg", () => {
|
||||
it("should scale font size correctly when resizing using shift", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
@@ -1049,7 +1043,7 @@ describe("textWysiwyg", () => {
|
||||
expect(rectangle.height).toBe(75);
|
||||
expect(textElement.fontSize).toBe(20);
|
||||
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
|
||||
shift: true,
|
||||
});
|
||||
expect(rectangle.width).toBe(200);
|
||||
@@ -1060,7 +1054,7 @@ describe("textWysiwyg", () => {
|
||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
@@ -1092,7 +1086,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
it("undo should work", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
@@ -1129,7 +1123,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
it("should not allow bound text with only whitespaces", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
updateTextEditor(editor, " ");
|
||||
@@ -1184,32 +1178,35 @@ describe("textWysiwyg", () => {
|
||||
it("should reset the container height cache when resizing", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect(rectangle.height).toBe(156);
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
expect(rectangle.height).toBe(156);
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
// cache updated again
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBeCloseTo(
|
||||
155,
|
||||
8,
|
||||
);
|
||||
});
|
||||
|
||||
it("should reset the container height cache when font properties updated", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
editor.blur();
|
||||
|
||||
@@ -1234,7 +1231,7 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
editor.blur();
|
||||
expect(
|
||||
@@ -1266,12 +1263,12 @@ describe("textWysiwyg", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
editor.select();
|
||||
});
|
||||
|
||||
@@ -1382,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 = getTextEditor();
|
||||
const editor = await getTextEditor(true);
|
||||
|
||||
updateTextEditor(
|
||||
editor,
|
||||
@@ -1428,7 +1425,7 @@ describe("textWysiwyg", () => {
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
fillStyle: "hachure",
|
||||
fillStyle: "solid",
|
||||
groupIds: [],
|
||||
height: 35,
|
||||
isDeleted: false,
|
||||
@@ -1441,7 +1438,7 @@ describe("textWysiwyg", () => {
|
||||
},
|
||||
strokeColor: "#1e1e1e",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeWidth: 2,
|
||||
type: "rectangle",
|
||||
updated: 1,
|
||||
version: 1,
|
||||
@@ -1470,7 +1467,7 @@ describe("textWysiwyg", () => {
|
||||
// Bind first text
|
||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello!");
|
||||
expect(
|
||||
@@ -1495,7 +1492,7 @@ describe("textWysiwyg", () => {
|
||||
rectangle.x + rectangle.width / 2,
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Excalidraw");
|
||||
editor.blur();
|
||||
@@ -1510,28 +1507,16 @@ describe("textWysiwyg", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should bump the version of labelled arrow when label updated", async () => {
|
||||
it("should bump the version of a labeled arrow when the label is updated", async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
const arrow = UI.createElement("arrow", {
|
||||
width: 300,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
mouse.select(arrow);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
let editor = getTextEditor();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
|
||||
await UI.editText(arrow, "Hello");
|
||||
const { version } = arrow;
|
||||
|
||||
mouse.select(arrow);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello\nworld!");
|
||||
editor.blur();
|
||||
await UI.editText(arrow, "Hello\nworld!");
|
||||
|
||||
expect(arrow.version).toEqual(version + 1);
|
||||
});
|
||||
|
||||
@@ -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,6 +701,7 @@ export const textWysiwyg = ({
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
window.addEventListener("beforeunload", handleSubmit);
|
||||
excalidrawContainer
|
||||
?.querySelector(".excalidraw-textEditorContainer")!
|
||||
.appendChild(editable);
|
||||
|
||||
@@ -195,6 +195,7 @@ export type ExcalidrawLinearElement = _ExcalidrawElementBase &
|
||||
Readonly<{
|
||||
type: "line" | "arrow";
|
||||
points: readonly Point[];
|
||||
segmentSplitIndices: readonly number[] | null;
|
||||
lastCommittedPoint: Point | null;
|
||||
startBinding: PointBinding | null;
|
||||
endBinding: PointBinding | null;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
type Subscriber<T extends any[]> = (...payload: T) => void;
|
||||
|
||||
export class Emitter<T extends any[] = []> {
|
||||
public subscribers: Subscriber<T>[] = [];
|
||||
public value: T | undefined;
|
||||
private updateOnChangeOnly: boolean;
|
||||
|
||||
constructor(opts?: { initialState?: T; updateOnChangeOnly?: boolean }) {
|
||||
this.updateOnChangeOnly = opts?.updateOnChangeOnly ?? false;
|
||||
this.value = opts?.initialState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches subscriber
|
||||
*
|
||||
* @returns unsubscribe function
|
||||
*/
|
||||
on(...handlers: Subscriber<T>[] | Subscriber<T>[][]) {
|
||||
const _handlers = handlers
|
||||
.flat()
|
||||
.filter((item) => typeof item === "function");
|
||||
|
||||
this.subscribers.push(..._handlers);
|
||||
|
||||
return () => this.off(_handlers);
|
||||
}
|
||||
|
||||
off(...handlers: Subscriber<T>[] | Subscriber<T>[][]) {
|
||||
const _handlers = handlers.flat();
|
||||
this.subscribers = this.subscribers.filter(
|
||||
(handler) => !_handlers.includes(handler),
|
||||
);
|
||||
}
|
||||
|
||||
trigger(...payload: T): any[] {
|
||||
if (this.updateOnChangeOnly && this.value === payload) {
|
||||
return [];
|
||||
}
|
||||
this.value = payload;
|
||||
return this.subscribers.map((handler) => handler(...payload));
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.subscribers = [];
|
||||
this.value = undefined;
|
||||
}
|
||||
}
|
||||
@@ -436,5 +436,121 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+140
-66
@@ -14,7 +14,7 @@ import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
} from "./element/textElement";
|
||||
import { arrayToMap, findIndex } from "./utils";
|
||||
import { arrayToMap } from "./utils";
|
||||
import { mutateElement } from "./element/mutateElement";
|
||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||
@@ -323,7 +323,24 @@ export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
|
||||
export const getFrameElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
frameId: string,
|
||||
) => allElements.filter((element) => element.frameId === frameId);
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
export const getElementsInResizingFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
@@ -451,91 +468,137 @@ 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 _elementsToAdd: ExcalidrawElement[] = [];
|
||||
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>(),
|
||||
},
|
||||
);
|
||||
|
||||
for (const element of elementsToAdd) {
|
||||
_elementsToAdd.push(element);
|
||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
_elementsToAdd.push(boundTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
const allElementsIndex = allElements.reduce(
|
||||
(acc: Record<string, number>, element, index) => {
|
||||
acc[element.id] = index;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const frameIndex = allElementsIndex[frame.id];
|
||||
// need to be calculated before the mutation below occurs
|
||||
const leftFrameBoundaryIndex = findIndex(
|
||||
allElements,
|
||||
(e) => e.frameId === frame.id,
|
||||
);
|
||||
|
||||
const existingFrameChildren = allElements.filter(
|
||||
(element) => element.frameId === frame.id,
|
||||
);
|
||||
|
||||
const addedFrameChildren_left: ExcalidrawElement[] = [];
|
||||
const addedFrameChildren_right: ExcalidrawElement[] = [];
|
||||
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,
|
||||
elementsToAdd,
|
||||
)) {
|
||||
if (element.frameId !== frame.id && !isFrameElement(element)) {
|
||||
if (allElementsIndex[element.id] > frameIndex) {
|
||||
addedFrameChildren_right.push(element);
|
||||
} else {
|
||||
addedFrameChildren_left.push(element);
|
||||
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||
if (!isValidFrameChild(element)) {
|
||||
continue;
|
||||
}
|
||||
finalElementsToAdd.push(element);
|
||||
}
|
||||
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: frame.id,
|
||||
},
|
||||
false,
|
||||
);
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (
|
||||
boundTextElement &&
|
||||
!suppliedElementsToAddSet.has(boundTextElement.id) &&
|
||||
!currTargetFrameChildrenMap.has(boundTextElement.id)
|
||||
) {
|
||||
finalElementsToAdd.push(boundTextElement);
|
||||
}
|
||||
}
|
||||
|
||||
const frameElement = allElements[frameIndex];
|
||||
const nextFrameChildren = addedFrameChildren_left
|
||||
.concat(existingFrameChildren)
|
||||
.concat(addedFrameChildren_right);
|
||||
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
|
||||
|
||||
const nextFrameChildrenMap = nextFrameChildren.reduce(
|
||||
(acc: Record<string, boolean>, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const nextElements: ExcalidrawElement[] = [];
|
||||
|
||||
const nextOtherElements_left = allElements
|
||||
.slice(0, leftFrameBoundaryIndex >= 0 ? leftFrameBoundaryIndex : frameIndex)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
const processedElements = new Set<ExcalidrawElement["id"]>();
|
||||
|
||||
const nextOtherElement_right = allElements
|
||||
.slice(frameIndex + 1)
|
||||
.filter((element) => !nextFrameChildrenMap[element.id]);
|
||||
for (const element of allElements) {
|
||||
if (processedElements.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextElements = nextOtherElements_left
|
||||
.concat(nextFrameChildren)
|
||||
.concat([frameElement])
|
||||
.concat(nextOtherElement_right);
|
||||
processedElements.add(element.id);
|
||||
|
||||
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,
|
||||
element,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
nextElements.push(element);
|
||||
}
|
||||
|
||||
for (const element of finalElementsToAdd) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
frameId: frame.id,
|
||||
},
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return nextElements;
|
||||
};
|
||||
@@ -705,6 +768,17 @@ 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);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const CODES = {
|
||||
V: "KeyV",
|
||||
Z: "KeyZ",
|
||||
R: "KeyR",
|
||||
S: "KeyS",
|
||||
} as const;
|
||||
|
||||
export const KEYS = {
|
||||
|
||||
+129
-80
@@ -50,7 +50,7 @@
|
||||
"veryLarge": "كبير جدا",
|
||||
"solid": "كامل",
|
||||
"hachure": "خطوط",
|
||||
"zigzag": "",
|
||||
"zigzag": "متعرج",
|
||||
"crossHatch": "خطوط متقطعة",
|
||||
"thin": "نحيف",
|
||||
"bold": "داكن",
|
||||
@@ -106,11 +106,15 @@
|
||||
"increaseFontSize": "تكبير حجم الخط",
|
||||
"unbindText": "فك ربط النص",
|
||||
"bindText": "ربط النص بالحاوية",
|
||||
"createContainerFromText": "",
|
||||
"createContainerFromText": "نص مغلف في حاوية",
|
||||
"link": {
|
||||
"edit": "تعديل الرابط",
|
||||
"editEmbed": "تحرير الرابط وإدراجه",
|
||||
"create": "إنشاء رابط",
|
||||
"label": "رابط"
|
||||
"createEmbed": "إنشاء رابط و إدراجه",
|
||||
"label": "رابط",
|
||||
"labelEmbed": "رابط و إدراج",
|
||||
"empty": "لم يتم تعيين رابط"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "تحرير السطر",
|
||||
@@ -124,9 +128,9 @@
|
||||
},
|
||||
"statusPublished": "نُشر",
|
||||
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"eyeDropper": ""
|
||||
"selectAllElementsInFrame": "تحديد جميع العناصر في الإطار",
|
||||
"removeAllElementsFromFrame": "إزالة جميع العناصر من الإطار",
|
||||
"eyeDropper": "اختيار اللون من القماش"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "لا توجد عناصر أضيفت بعد...",
|
||||
@@ -160,13 +164,16 @@
|
||||
"darkMode": "الوضع المظلم",
|
||||
"lightMode": "الوضع المضيء",
|
||||
"zenMode": "وضع التأمل",
|
||||
"objectsSnapMode": "التقط إلى العناصر",
|
||||
"exitZenMode": "إلغاء الوضع الليلى",
|
||||
"cancel": "إلغاء",
|
||||
"clear": "مسح",
|
||||
"remove": "إزالة",
|
||||
"embed": "تبديل الإدراج",
|
||||
"publishLibrary": "انشر",
|
||||
"submit": "أرسل",
|
||||
"confirm": "تأكيد"
|
||||
"confirm": "تأكيد",
|
||||
"embeddableInteractionButton": "اضغط للتفاعل"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
|
||||
@@ -189,23 +196,28 @@
|
||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
||||
"collabOfflineWarning": ""
|
||||
"collabOfflineWarning": "لا يوجد اتصال بالانترنت.\nلن يتم حفظ التغييرات التي قمت بها!"
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
||||
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
|
||||
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
||||
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "SVG غير صالح.",
|
||||
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
|
||||
"importLibraryError": "تعذر تحميل المكتبة",
|
||||
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
|
||||
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "يبدو أنك تستخدم متصفح Brave مع إعداد <bold>حظر صارم لتتبع البصمة</bold>.",
|
||||
"line2": "قد يؤدي هذا إلى كسر <bold>عناصر النص</bold> في الرسومات الخاصة بك.",
|
||||
"line3": "من المستحسن إلغاء تفعيل هذا الإعداد. يمكنك اتباع <link>هذه الخطوات</link> لفعل ذلك.",
|
||||
"line4": "إذا لم يصلح تعطيل هذا الإعداد طريقة عرض النصوص، الرجاء كتابة <issueLink>بلاغ</issueLink> على حسابنا في GitHub، أو راسلنا على <discordLink>Discord</discordLink>"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "لا يمكن إضافة العناصر القابلة للتضمين في المكتبة.",
|
||||
"image": "سوف يتم دعم إضافة صور إلى المكتبة قريباً!"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -223,9 +235,11 @@
|
||||
"penMode": "وضع القلم - امنع اللمس",
|
||||
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
||||
"eraser": "ممحاة",
|
||||
"frame": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
"frame": "أداة الإطار",
|
||||
"embeddable": "تضمين ويب",
|
||||
"laser": "مؤشر ليزر",
|
||||
"hand": "يد (أداة الإزاحة)",
|
||||
"extraTools": "المزيد من أﻷدوات"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "إجراءات اللوحة",
|
||||
@@ -237,6 +251,7 @@
|
||||
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
||||
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
||||
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
||||
"embeddable": "اضغط مع السحب لإنشاء موقع ويب مضمّن",
|
||||
"text_selected": "انقر نقراً مزدوجاً أو اضغط ادخال لتعديل النص",
|
||||
"text_editing": "اضغط على Esc أو (Ctrl أو Cmd) + Enter لإنهاء التعديل",
|
||||
"linearElementMulti": "انقر فوق النقطة الأخيرة أو اضغط على Esc أو Enter للإنهاء",
|
||||
@@ -245,14 +260,15 @@
|
||||
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
|
||||
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
|
||||
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
|
||||
"lineEditor_pointSelected": "",
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة (النِّقَاط)، Ctrl/Cmd+D للتكرار، أو اسحب للانتقال",
|
||||
"lineEditor_nothingSelected": "اختر نقطة لتعديلها (اضغط على SHIFT لتحديد عدة نِقَاط),\nأو اضغط على ALT و انقر بالفأرة لإضافة نِقَاط جديدة",
|
||||
"placeImage": "انقر لوضع الصورة، أو انقر واسحب لتعيين حجمها يدوياً",
|
||||
"publishLibrary": "نشر مكتبتك",
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"bindTextToElement": "اضغط على إدخال لإضافة نص",
|
||||
"deepBoxSelect": "اضغط على Ctrl\\Cmd للاختيار العميق، ولمنع السحب",
|
||||
"eraserRevert": "اضغط على Alt لاستعادة العناصر المعلَّمة للحذف",
|
||||
"firefox_clipboard_write": "يمكن على الأرجح تمكين هذه الميزة عن طريق تعيين علم \"dom.events.asyncClipboard.clipboardItem\" إلى \"true\". لتغيير أعلام المتصفح في Firefox، قم بزيارة صفحة \"about:config\".",
|
||||
"disableSnapping": "اضغط على Ctrl أو Cmd لتعطيل الالتقاط"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "تعذر عرض المعاينة",
|
||||
@@ -260,11 +276,11 @@
|
||||
"canvasTooBigTip": "نصيحة: حاول تحريك العناصر البعيدة بشكل أقرب قليلاً."
|
||||
},
|
||||
"errorSplash": {
|
||||
"headingMain": "",
|
||||
"headingMain": "حدث خطأ. حاول <button>تحديث الصفحة</button>.",
|
||||
"clearCanvasMessage": "إذا لم تعمل إعادة التحميل، حاول مرة أخرى ",
|
||||
"clearCanvasCaveat": " هذا سيؤدي إلى فقدان العمل ",
|
||||
"trackedToSentry": "",
|
||||
"openIssueMessage": "",
|
||||
"trackedToSentry": "تم تتبع الخطأ في المعرف {{eventId}} على نظامنا.",
|
||||
"openIssueMessage": "حرصنا على عدم إضافة معلومات المشهد في بلاغ الخطأ. في حال كون مشهدك لا يحمل أي معلومات خاصة نرجو المتابعة على <button>نظام تتبع الأخطاء</button>. نرجو إضافة المعلومات أدناه بنسخها ولصقها في محتوى البلاغ على GitHub.",
|
||||
"sceneContent": "محتوى المشهد:"
|
||||
},
|
||||
"roomDialog": {
|
||||
@@ -294,16 +310,16 @@
|
||||
"helpDialog": {
|
||||
"blog": "اقرأ مدونتنا",
|
||||
"click": "انقر",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
"deepSelect": "تحديد عميق",
|
||||
"deepBoxSelect": "تحديد عميق داخل المربع، ومنع السحب",
|
||||
"curvedArrow": "سهم مائل",
|
||||
"curvedLine": "خط مائل",
|
||||
"documentation": "دليل الاستخدام",
|
||||
"doubleClick": "انقر مرتين",
|
||||
"drag": "اسحب",
|
||||
"editor": "المحرر",
|
||||
"editLineArrowPoints": "",
|
||||
"editText": "",
|
||||
"editLineArrowPoints": "تحرير سطر/نقاط سهم",
|
||||
"editText": "تعديل النص / إضافة تسمية",
|
||||
"github": "عثرت على مشكلة؟ إرسال",
|
||||
"howto": "اتبع التعليمات",
|
||||
"or": "أو",
|
||||
@@ -316,9 +332,9 @@
|
||||
"view": "عرض",
|
||||
"zoomToFit": "تكبير للملائمة",
|
||||
"zoomToSelection": "تكبير للعنصر المحدد",
|
||||
"toggleElementLock": "",
|
||||
"movePageUpDown": "",
|
||||
"movePageLeftRight": ""
|
||||
"toggleElementLock": "إغلاق/فتح المحدد",
|
||||
"movePageUpDown": "نقل الصفحة أعلى/أسفل",
|
||||
"movePageLeftRight": "نقل الصفحة يسار/يمين"
|
||||
},
|
||||
"clearCanvasDialog": {
|
||||
"title": "مسح اللوحة"
|
||||
@@ -336,20 +352,20 @@
|
||||
"authorName": "اسمك أو اسم المستخدم",
|
||||
"libraryName": "اسم مكتبتك",
|
||||
"libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
"githubHandle": "معالج GitHub (اختياري)، حتى تتمكن من تحرير المكتبة عند إرسالها للمراجعة",
|
||||
"twitterHandle": "اسم مستخدم تويتر (اختياري)، حتى نعرف من الذي سيتم الإشارة إليه عند الترويج عبر تويتر",
|
||||
"website": "رابط إلى موقعك الشخصي أو في مكان آخر (اختياري)"
|
||||
},
|
||||
"errors": {
|
||||
"required": "مطلوب",
|
||||
"website": "أدخل عنوان URL صالح"
|
||||
},
|
||||
"noteDescription": "",
|
||||
"noteGuidelines": "",
|
||||
"noteLicense": "",
|
||||
"noteDescription": "تقديم مكتبتك لتضمينها في مستودع المكتبة العامة <link></link> لأشخاص آخرين لاستخدامها في رسومهم.",
|
||||
"noteGuidelines": "تحتاج المكتبة إلى الموافقة أولا. يرجى قراءة <link>المعايير</link> قبل تقديمها. سوف تحتاج إلى حساب GitHub للتواصل وإجراء التغييرات عند الطلب، ولكن ليس مطلوبا بشكل صارم.",
|
||||
"noteLicense": "تقديمك يعني موافقتك على نشر المكتبة المقدمة تحت <link>MIT ترخيص</link>، ما يعني أن لأي أحد الحق في استخدامها دون قيود.",
|
||||
"noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
|
||||
"atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء",
|
||||
"republishWarning": ""
|
||||
"republishWarning": "ملاحظة: بعض العناصر المحددة معينة على أنه نشرها أو تقديمها من قبل. يجب عليك فقط إعادة إرسال العناصر عند تحديث مكتبة موجودة أو إرسالها."
|
||||
},
|
||||
"publishSuccessDialog": {
|
||||
"title": "تم إرسال المكتبة",
|
||||
@@ -360,27 +376,27 @@
|
||||
"removeItemsFromLib": "إزالة العناصر المحددة من المكتبة"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "تصدير الصورة",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
"withBackground": "الخلفية",
|
||||
"onlySelected": "المحدد فقط",
|
||||
"darkMode": "الوضع الداكن",
|
||||
"embedScene": "تضمين المشهد",
|
||||
"scale": "الحجم",
|
||||
"padding": "الهوامش"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
"embedScene": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "تصدير بصيغة PNG",
|
||||
"exportToSvg": "تصدير بصيغة SVG",
|
||||
"copyPngToClipboard": "نسخ الـ PNG إلى الحافظة"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "نسخ إلى الحافظة"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@@ -411,43 +427,76 @@
|
||||
"fileSavedToFilename": "حفظ باسم {filename}",
|
||||
"canvas": "لوحة الرسم",
|
||||
"selection": "العنصر المحدد",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "استخدم {{shortcut}} للصق كعنصر واحد،\nأو لصق في محرر نص موجود",
|
||||
"unableToEmbed": "تضمين هذا الرابط غير مسموح حاليًا. افتح بلاغاً على GitHub لطلب عنوان Url القائمة البيضاء",
|
||||
"unrecognizedLinkFormat": "الرابط الذي ضمنته لا يتطابق مع التنسيق المتوقع. الرجاء محاولة لصق النص 'المضمن' المُزوَد من موقع المصدر"
|
||||
},
|
||||
"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": "",
|
||||
"menuHint": ""
|
||||
"center_heading": "جميع بياناتك محفوظة محليا في المتصفح الخاص بك.",
|
||||
"center_heading_plus": "هل تريد الذهاب إلى Excalidraw+ بدلاً من ذلك؟",
|
||||
"menuHint": "التصدير والتفضيلات واللغات ..."
|
||||
},
|
||||
"defaults": {
|
||||
"menuHint": "",
|
||||
"center_heading": "",
|
||||
"toolbarHint": "",
|
||||
"helpHint": ""
|
||||
"menuHint": "التصدير والتفضيلات وغيرها...",
|
||||
"center_heading": "الرسم البياني التصويري. بشكل مبسط.",
|
||||
"toolbarHint": "اختر أداة و ابدأ الرسم!",
|
||||
"helpHint": "الاختصارات و المساعدة"
|
||||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"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>بإمكانك إجراء النسخ الاحتياطي لرسمتك الحالية باستخدام أحد الخيارات أدناه."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"editEmbed": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"createEmbed": "",
|
||||
"label": "",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
@@ -160,13 +164,16 @@
|
||||
"darkMode": "",
|
||||
"lightMode": "",
|
||||
"zenMode": "",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "",
|
||||
"cancel": "",
|
||||
"clear": "",
|
||||
"remove": "",
|
||||
"embed": "",
|
||||
"publishLibrary": "",
|
||||
"submit": "",
|
||||
"confirm": ""
|
||||
"confirm": "",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "",
|
||||
@@ -196,6 +203,7 @@
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "",
|
||||
@@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"link": "",
|
||||
"eraser": "",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"linearElement": "",
|
||||
"freeDraw": "",
|
||||
"text": "",
|
||||
"embeddable": "",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"linearElementMulti": "",
|
||||
@@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "",
|
||||
@@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": "",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+145
-96
@@ -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,37 +99,41 @@
|
||||
"share": "Сподели",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
"toggleTheme": "",
|
||||
"personalLib": "",
|
||||
"excalidrawLib": "",
|
||||
"decreaseFontSize": "",
|
||||
"increaseFontSize": "",
|
||||
"toggleTheme": "Включи тема",
|
||||
"personalLib": "Лична Библиотека",
|
||||
"excalidrawLib": "Excalidraw Библиотека",
|
||||
"decreaseFontSize": "Намали размера на шрифта",
|
||||
"increaseFontSize": "Увеличи размера на шрифта",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"edit": "Редактирай линк",
|
||||
"editEmbed": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"createEmbed": "",
|
||||
"label": "Линк",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"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": ""
|
||||
},
|
||||
@@ -137,11 +141,11 @@
|
||||
"clearReset": "Нулиране на платно",
|
||||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"export": "Запази на...",
|
||||
"copyToClipboard": "Копиране в клипборда",
|
||||
"save": "",
|
||||
"save": "Запази към текущ файл",
|
||||
"saveAs": "Запиши като",
|
||||
"load": "",
|
||||
"load": "Отвори",
|
||||
"getShareableLink": "Получаване на връзка за споделяне",
|
||||
"close": "Затвори",
|
||||
"selectLanguage": "Избор на език",
|
||||
@@ -160,13 +164,16 @@
|
||||
"darkMode": "Тъмен режим",
|
||||
"lightMode": "Светъл режим",
|
||||
"zenMode": "Режим Zen",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "Спиране на Zen режим",
|
||||
"cancel": "Отмени",
|
||||
"clear": "Изчисти",
|
||||
"remove": "Премахване",
|
||||
"embed": "",
|
||||
"publishLibrary": "Публикувай",
|
||||
"submit": "Изпрати",
|
||||
"confirm": "Потвърждаване"
|
||||
"confirm": "Потвърждаване",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
|
||||
@@ -175,37 +182,42 @@
|
||||
"couldNotLoadInvalidFile": "Невалиден файл не може да се зареди",
|
||||
"importBackendFailed": "Импортирането от бекенд не беше успешно.",
|
||||
"cannotExportEmptyCanvas": "Не може да се експортира празно платно.",
|
||||
"couldNotCopyToClipboard": "",
|
||||
"couldNotCopyToClipboard": "Не можем да копираме в клипбоарда.",
|
||||
"decryptFailed": "Данните не можаха да се дешифрират.",
|
||||
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
|
||||
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
||||
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
||||
"errorAddingToLibrary": "",
|
||||
"errorRemovingFromLibrary": "",
|
||||
"errorAddingToLibrary": "Не можем да заредим от библиотеката",
|
||||
"errorRemovingFromLibrary": "Не можем да премахнем елемент от библиотеката",
|
||||
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
||||
"imageDoesNotContainScene": "",
|
||||
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
||||
"invalidSceneUrl": "",
|
||||
"resetLibrary": "",
|
||||
"removeItemsFromsLibrary": "",
|
||||
"removeItemsFromsLibrary": "Изтрий {{count}} елемент(а) от библиотеката?",
|
||||
"invalidEncryptionKey": "",
|
||||
"collabOfflineWarning": ""
|
||||
},
|
||||
"errors": {
|
||||
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"fileTooBig": "Файлът е твърде голям. Максималния допустим размер е {{maxSize}}.",
|
||||
"svgImageInsertError": "",
|
||||
"invalidSVGString": "",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "Невалиден SVG.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "",
|
||||
"importLibraryError": "Не можем да заредим библиотеката",
|
||||
"collabSaveFailed": "",
|
||||
"collabSaveFailed_sizeExceeded": "",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line3": "Силно препоръчваме да изключите тази настройка. Можете да следвате <link>тези стъпки</link> за това как да го направите.",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -222,10 +234,12 @@
|
||||
"lock": "Поддържайте избрания инструмент активен след рисуване",
|
||||
"penMode": "",
|
||||
"link": "",
|
||||
"eraser": "",
|
||||
"eraser": "Гума",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
"extraTools": "Още инструменти"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Действия по платното",
|
||||
@@ -237,6 +251,7 @@
|
||||
"linearElement": "Кликнете, за да стартирате няколко точки, плъзнете за една линия",
|
||||
"freeDraw": "Натиснете и влачете, пуснете като сте готови",
|
||||
"text": "Подсказка: Можете също да добавите текст като натиснете някъде два път с инструмента за селекция",
|
||||
"embeddable": "",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"linearElementMulti": "Кликнете върху последната точка или натиснете Escape или Enter, за да завършите",
|
||||
@@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Натиснете Enter, за да добавите",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Невъзможност за показване на preview",
|
||||
@@ -288,7 +304,7 @@
|
||||
"link_details": "",
|
||||
"link_button": "",
|
||||
"excalidrawplus_description": "",
|
||||
"excalidrawplus_button": "",
|
||||
"excalidrawplus_button": "Експорт",
|
||||
"excalidrawplus_exportError": ""
|
||||
},
|
||||
"helpDialog": {
|
||||
@@ -299,7 +315,7 @@
|
||||
"curvedArrow": "Извита стрелка",
|
||||
"curvedLine": "Извита линия",
|
||||
"documentation": "Документация",
|
||||
"doubleClick": "",
|
||||
"doubleClick": "двойно-щракване",
|
||||
"drag": "плъзнете",
|
||||
"editor": "Редактор",
|
||||
"editLineArrowPoints": "",
|
||||
@@ -308,41 +324,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": "",
|
||||
"twitterUsername": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"website": "",
|
||||
"title": "Публикувай библиотека",
|
||||
"itemName": "Име",
|
||||
"authorName": "Авторско име",
|
||||
"githubUsername": "GitHub потребителско име",
|
||||
"twitterUsername": "Twitter потребителско име",
|
||||
"libraryName": "Име на библиотеката",
|
||||
"libraryDesc": "Описание на библиотеката",
|
||||
"website": "Уебсайт",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"authorName": "Името или потребителското Ви име",
|
||||
"libraryName": "Име на библиотеката Ви",
|
||||
"libraryDesc": "Описание на библиотеката ви, за да помогнете на хората да разберат приложенията ѝ",
|
||||
"githubHandle": "",
|
||||
"twitterHandle": "",
|
||||
"website": ""
|
||||
},
|
||||
"errors": {
|
||||
"required": "",
|
||||
"website": ""
|
||||
"required": "Задължително",
|
||||
"website": "Въведете валиден URL адрес"
|
||||
},
|
||||
"noteDescription": "",
|
||||
"noteGuidelines": "",
|
||||
@@ -356,15 +372,15 @@
|
||||
"content": ""
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "",
|
||||
"resetLibrary": "Нулирай библиотека",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"withBackground": "Фон",
|
||||
"onlySelected": "Само избраното",
|
||||
"darkMode": "Тъмен режим",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
@@ -373,14 +389,14 @@
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "Изнасяне в PNG",
|
||||
"exportToSvg": "Изнасяне в SVG",
|
||||
"copyPngToClipboard": "Копирай PNG в клипборда"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Копиране в клипборда"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@@ -403,51 +419,84 @@
|
||||
"width": "Широчина"
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "",
|
||||
"addedToLibrary": "Добавена към библиотеката",
|
||||
"copyStyles": "Копирани стилове.",
|
||||
"copyToClipboard": "Копирано в клипборда.",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "",
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": "",
|
||||
"pasteAsSingleElement": ""
|
||||
"copyToClipboardAsPng": "Копира {{exportSelection}} в клипборда като PNG\n({{exportColorScheme}})",
|
||||
"fileSaved": "Файлът е запазен.",
|
||||
"fileSavedToFilename": "Запазен към {filename}",
|
||||
"canvas": "платно",
|
||||
"selection": "селекция",
|
||||
"pasteAsSingleElement": "",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "লিঙ্ক সংশোধন",
|
||||
"editEmbed": "",
|
||||
"create": "লিঙ্ক তৈরী",
|
||||
"label": "লিঙ্ক নামকরণ"
|
||||
"createEmbed": "",
|
||||
"label": "লিঙ্ক নামকরণ",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
@@ -160,13 +164,16 @@
|
||||
"darkMode": "ডার্ক মোড",
|
||||
"lightMode": "লাইট মোড",
|
||||
"zenMode": "জেন মোড",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "জেন মোড বন্ধ করুন",
|
||||
"cancel": "বাতিল",
|
||||
"clear": "সাফ",
|
||||
"remove": "বিয়োগ",
|
||||
"embed": "",
|
||||
"publishLibrary": "সংগ্রহ প্রকাশ করুন",
|
||||
"submit": "জমা করুন",
|
||||
"confirm": "নিশ্চিত করুন"
|
||||
"confirm": "নিশ্চিত করুন",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "এটি পুরো ক্যানভাস সাফ করবে। আপনি কি নিশ্চিত?",
|
||||
@@ -196,6 +203,7 @@
|
||||
"imageInsertError": "ছবি সন্নিবেশ করা যায়নি। পরে আবার চেষ্টা করুন...",
|
||||
"fileTooBig": "ফাইলটি খুব বড়। সর্বাধিক অনুমোদিত আকার হল {{maxSize}}৷",
|
||||
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
|
||||
"importLibraryError": "সংগ্রহ লোড করা যায়নি",
|
||||
@@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
||||
"eraser": "ঝাড়ন",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
|
||||
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
|
||||
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
|
||||
"embeddable": "",
|
||||
"text_selected": "লেখা সম্পাদনা করতে ডাবল-ক্লিক করুন বা এন্টার টিপুন",
|
||||
"text_editing": "লেখা সম্পাদনা শেষ করতে এসকেপ বা কন্ট্রোল/কম্যান্ড যোগে এন্টার টিপুন",
|
||||
"linearElementMulti": "শেষ বিন্দুতে ক্লিক করুন অথবা শেষ করতে এসকেপ বা এন্টার টিপুন",
|
||||
@@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
|
||||
@@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": "বাছাই",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "Edita l'enllaç",
|
||||
"editEmbed": "",
|
||||
"create": "Crea un enllaç",
|
||||
"label": "Enllaç"
|
||||
"createEmbed": "",
|
||||
"label": "Enllaç",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Editar línia",
|
||||
@@ -160,13 +164,16 @@
|
||||
"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"
|
||||
"confirm": "Confirma",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "S'esborrarà tot el llenç. N'esteu segur?",
|
||||
@@ -196,6 +203,7 @@
|
||||
"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",
|
||||
@@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||
"eraser": "Esborrador",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "Mà (eina de desplaçament)",
|
||||
"extraTools": ""
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"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",
|
||||
@@ -252,7 +267,8 @@
|
||||
"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\"."
|
||||
"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": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||
@@ -411,7 +427,9 @@
|
||||
"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"
|
||||
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Zabalit text do kontejneru",
|
||||
"link": {
|
||||
"edit": "Upravit odkaz",
|
||||
"editEmbed": "",
|
||||
"create": "Vytvořit odkaz",
|
||||
"label": "Odkaz"
|
||||
"createEmbed": "",
|
||||
"label": "Odkaz",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Upravit čáru",
|
||||
@@ -160,13 +164,16 @@
|
||||
"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"
|
||||
"confirm": "Potvrdit",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Toto vymaže celé plátno. Jste si jisti?",
|
||||
@@ -196,6 +203,7 @@
|
||||
"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",
|
||||
@@ -206,6 +214,10 @@
|
||||
"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": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
|
||||
"eraser": "Guma",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "Ruka (nástroj pro posouvání)",
|
||||
"extraTools": ""
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"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í",
|
||||
@@ -252,7 +267,8 @@
|
||||
"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\"."
|
||||
"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": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Náhled nelze zobrazit",
|
||||
@@ -411,7 +427,9 @@
|
||||
"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"
|
||||
"pasteAsSingleElement": "Pomocí {{shortcut}} vložte jako jeden prvek,\nnebo vložte do existujícího textového editoru",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Průhledná",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+57
-8
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "Indsæt",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteAsPlaintext": "Indsæt som klartekst",
|
||||
"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": "",
|
||||
"duplicateSelection": "",
|
||||
"liveCollaboration": "Live samarbejde...",
|
||||
"duplicateSelection": "Duplikér",
|
||||
"untitled": "Unavngivet",
|
||||
"name": "Navn",
|
||||
"yourName": "Dit navn",
|
||||
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "",
|
||||
"editEmbed": "",
|
||||
"create": "",
|
||||
"label": ""
|
||||
"createEmbed": "",
|
||||
"label": "",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
@@ -160,13 +164,16 @@
|
||||
"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"
|
||||
"confirm": "Bekræft",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Dette vil rydde hele lærredet. Er du sikker?",
|
||||
@@ -196,6 +203,7 @@
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "",
|
||||
@@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"link": "",
|
||||
"eraser": "",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"linearElement": "",
|
||||
"freeDraw": "Klik og træk, slip når du er færdig",
|
||||
"text": "",
|
||||
"embeddable": "",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"linearElementMulti": "",
|
||||
@@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "",
|
||||
@@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "Gemt som {filename}",
|
||||
"canvas": "canvas",
|
||||
"selection": "markering",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Text in Container einbetten",
|
||||
"link": {
|
||||
"edit": "Link bearbeiten",
|
||||
"editEmbed": "Link bearbeiten & einbetten",
|
||||
"create": "Link erstellen",
|
||||
"label": "Link"
|
||||
"createEmbed": "Link erstellen & einbetten",
|
||||
"label": "Link",
|
||||
"labelEmbed": "Verlinken & einbetten",
|
||||
"empty": "Kein Link festgelegt"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Linie bearbeiten",
|
||||
@@ -160,13 +164,16 @@
|
||||
"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"
|
||||
"confirm": "Bestätigen",
|
||||
"embeddableInteractionButton": "Klicken, um zu interagieren"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?",
|
||||
@@ -196,6 +203,7 @@
|
||||
"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",
|
||||
@@ -206,6 +214,10 @@
|
||||
"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": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"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"
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"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",
|
||||
@@ -252,7 +267,8 @@
|
||||
"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\"."
|
||||
"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"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
|
||||
@@ -411,7 +427,9 @@
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "Επεξεργασία συνδέσμου",
|
||||
"editEmbed": "",
|
||||
"create": "Δημιουργία συνδέσμου",
|
||||
"label": "Σύνδεσμος"
|
||||
"createEmbed": "",
|
||||
"label": "Σύνδεσμος",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Επεξεργασία γραμμής",
|
||||
@@ -160,13 +164,16 @@
|
||||
"darkMode": "Σκοτεινή λειτουργία",
|
||||
"lightMode": "Φωτεινή λειτουργία",
|
||||
"zenMode": "Λειτουργία Zεν",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "Έξοδος από την λειτουργία Zen",
|
||||
"cancel": "Ακύρωση",
|
||||
"clear": "Καθαρισμός",
|
||||
"remove": "Κατάργηση",
|
||||
"embed": "",
|
||||
"publishLibrary": "Δημοσίευση",
|
||||
"submit": "Υποβολή",
|
||||
"confirm": "Επιβεβαίωση"
|
||||
"confirm": "Επιβεβαίωση",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
|
||||
@@ -196,6 +203,7 @@
|
||||
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
||||
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
||||
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
||||
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
|
||||
@@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||
"eraser": "Γόμα",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
||||
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
||||
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
||||
"embeddable": "",
|
||||
"text_selected": "Κάντε διπλό κλικ ή πατήστε ENTER για να επεξεργαστείτε το κείμενο",
|
||||
"text_editing": "Πατήστε Escape ή CtrlOrCmd+ENTER για να ολοκληρώσετε την επεξεργασία",
|
||||
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
||||
@@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
||||
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
||||
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή",
|
||||
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\"."
|
||||
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\".",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
||||
@@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
|
||||
"canvas": "καμβάς",
|
||||
"selection": "επιλογή",
|
||||
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
|
||||
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Διαφανές",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"darkMode": "Dark mode",
|
||||
"lightMode": "Light mode",
|
||||
"zenMode": "Zen mode",
|
||||
"objectsSnapMode": "Snap to objects",
|
||||
"exitZenMode": "Exit zen mode",
|
||||
"cancel": "Cancel",
|
||||
"clear": "Clear",
|
||||
@@ -202,6 +203,7 @@
|
||||
"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",
|
||||
@@ -235,6 +237,7 @@
|
||||
"eraser": "Eraser",
|
||||
"frame": "Frame tool",
|
||||
"embeddable": "Web Embed",
|
||||
"laser": "Laser pointer",
|
||||
"hand": "Hand (panning tool)",
|
||||
"extraTools": "More tools"
|
||||
},
|
||||
|
||||
+90
-41
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Envolver el texto en un contenedor",
|
||||
"link": {
|
||||
"edit": "Editar enlace",
|
||||
"editEmbed": "Editar enlace e incrustar",
|
||||
"create": "Crear enlace",
|
||||
"label": "Enlace"
|
||||
"createEmbed": "Crear enlace e incrustar",
|
||||
"label": "Enlace",
|
||||
"labelEmbed": "Enlazar e incrustar",
|
||||
"empty": "No se ha establecido un enlace"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Editar línea",
|
||||
@@ -124,9 +128,9 @@
|
||||
},
|
||||
"statusPublished": "Publicado",
|
||||
"sidebarLock": "Mantener barra lateral abierta",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"eyeDropper": ""
|
||||
"selectAllElementsInFrame": "Seleccionar todos los elementos en el marco",
|
||||
"removeAllElementsFromFrame": "Eliminar todos los elementos del marco",
|
||||
"eyeDropper": "Seleccionar un color del lienzo"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "No hay elementos añadidos todavía...",
|
||||
@@ -160,13 +164,16 @@
|
||||
"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"
|
||||
"confirm": "Confirmar",
|
||||
"embeddableInteractionButton": "Pulsa para interactuar"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
|
||||
@@ -196,16 +203,21 @@
|
||||
"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": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"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.",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@@ -224,8 +236,10 @@
|
||||
"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": ""
|
||||
"extraTools": "Más herramientas"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Acciones del lienzo",
|
||||
@@ -237,6 +251,7 @@
|
||||
"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",
|
||||
@@ -252,7 +267,8 @@
|
||||
"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\"."
|
||||
"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"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "No se puede mostrar la vista previa",
|
||||
@@ -360,27 +376,27 @@
|
||||
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "Exportar imagen",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
"withBackground": "Fondo",
|
||||
"onlySelected": "Sólo seleccionados",
|
||||
"darkMode": "Modo oscuro",
|
||||
"embedScene": "Incrustar escena",
|
||||
"scale": "Escalar",
|
||||
"padding": "Espaciado"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "Exportar a PNG",
|
||||
"exportToSvg": "Exportar a SVG",
|
||||
"copyPngToClipboard": "Copiar PNG al portapapeles"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Copiar al portapapeles"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@@ -411,24 +427,26 @@
|
||||
"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"
|
||||
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un solo elemento,\no pegar en un editor de texto existente",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparente",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"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"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
@@ -444,10 +462,41 @@
|
||||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"mostUsedCustomColors": "Colores personalizados más utilizados",
|
||||
"colors": "Colores",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"hexCode": "Código Hexadecimal",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-4
@@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Bilatu testua edukiontzi batean",
|
||||
"link": {
|
||||
"edit": "Editatu esteka",
|
||||
"editEmbed": "Editatu esteka eta kapsulatu",
|
||||
"create": "Sortu esteka",
|
||||
"label": "Esteka"
|
||||
"createEmbed": "Sortu esteka eta kapsulatu",
|
||||
"label": "Esteka",
|
||||
"labelEmbed": "Esteka eta kapsula",
|
||||
"empty": "Ez da estekarik ezarri"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Editatu lerroa",
|
||||
@@ -160,13 +164,16 @@
|
||||
"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"
|
||||
"confirm": "Bai",
|
||||
"embeddableInteractionButton": "Egin klik elkar eragiteko"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
|
||||
@@ -196,6 +203,7 @@
|
||||
"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",
|
||||
@@ -206,6 +214,10 @@
|
||||
"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": {
|
||||
@@ -224,6 +236,8 @@
|
||||
"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"
|
||||
},
|
||||
@@ -237,6 +251,7 @@
|
||||
"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",
|
||||
@@ -252,7 +267,8 @@
|
||||
"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."
|
||||
"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"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
||||
@@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "{filename}-n gorde da",
|
||||
"canvas": "oihala",
|
||||
"selection": "hautapena",
|
||||
"pasteAsSingleElement": "Erabili {{shortcut}} elementu bakar gisa itsasteko,\nedo itsatsi lehendik dagoen testu-editore batean"
|
||||
"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"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Gardena",
|
||||
@@ -449,5 +467,36 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user