mirror of
https://github.com/excalidraw/excalidraw
synced 2025-07-25 13:58:22 +08:00
Compare commits
46 Commits
zsviczian-
...
mrazator/f
Author | SHA1 | Date | |
---|---|---|---|
0dd1daf0e9 | |||
18a7b97515 | |||
e8def8da8d | |||
a7db41c5ba | |||
d8166d9e1d | |||
81c0259041 | |||
f5c91c3a0f | |||
9b8de8a12e | |||
ea677d4581 | |||
ec2de7205f | |||
d5e3f436dc | |||
dcf4592e79 | |||
d1f8eec174 | |||
0f81c30276 | |||
f098789d16 | |||
f794b0bb90 | |||
104f64f1dc | |||
71ad3c5356 | |||
afea0df141 | |||
d2a508104e | |||
3697618266 | |||
e7cc2337ea | |||
9eb89f9960 | |||
ab1bcc7615 | |||
b1cac35269 | |||
83f86e2b86 | |||
7e38cab76e | |||
2cabb1f1f4 | |||
63650f82d1 | |||
dde3dac931 | |||
5b94cffc74 | |||
aaf73c8ff3 | |||
44d9d5fcac | |||
89a3bbddb7 | |||
b86184a849 | |||
b552166924 | |||
26ff3993bb | |||
7ad02c359a | |||
2523fe82e3 | |||
4ea079eb85 | |||
f20ba90ffa | |||
03da9112cf | |||
a249f332a2 | |||
2e61926a6b | |||
e921bfb1ae | |||
e6f74350ac |
@ -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
|
@ -25,6 +25,9 @@
|
||||
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
|
||||
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" />
|
||||
</a>
|
||||
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||
</a>
|
||||
|
@ -23,7 +23,7 @@ You can pass a `ref` when you want to access some excalidraw APIs. We expose the
|
||||
| ready | `boolean` | This is set to true once Excalidraw is rendered |
|
||||
| [readyPromise](#readypromise) | `function` | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readypromise) |
|
||||
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
|
||||
| [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
|
||||
| [updateLibrary](#updatelibrary) | `function` | updates the the library |
|
||||
| [addFiles](#addfiles) | `function` | add files data to the appState |
|
||||
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
|
||||
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
|
||||
|
@ -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**
|
||||
|
||||
|
22
dev-docs/docs/codebase/frames.mdx
Normal file
22
dev-docs/docs/codebase/frames.mdx
Normal file
@ -0,0 +1,22 @@
|
||||
# Frames
|
||||
|
||||
## Ordering
|
||||
|
||||
Frames should be ordered where frame children come first, followed by the frame element itself:
|
||||
|
||||
```
|
||||
[
|
||||
other_element,
|
||||
frame1_child1,
|
||||
frame1_child2,
|
||||
frame1,
|
||||
other_element,
|
||||
frame2_child1,
|
||||
frame2_child2,
|
||||
frame2,
|
||||
other_element,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
If not oredered correctly, the editor will still function, but the elements may not be rendered and clipped correctly. Further, the renderer relies on this ordering for performance optimizations.
|
@ -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",
|
||||
|
@ -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") {
|
||||
|
@ -6,7 +6,7 @@
|
||||
*
|
||||
* - DataState refers to full state of the app: appState, elements, images,
|
||||
* though some state is saved separately (collab username, library) for one
|
||||
* reason or another. We also save different data to different sotrage
|
||||
* reason or another. We also save different data to different storage
|
||||
* (localStorage, indexedDB).
|
||||
*/
|
||||
|
||||
|
@ -131,5 +131,5 @@ export class Debug {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
window.debug = Debug;
|
||||
|
@ -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 },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -20,6 +20,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@excalidraw/mermaid-to-excalidraw": "0.1.2",
|
||||
"@excalidraw/laser-pointer": "1.2.0",
|
||||
"@excalidraw/random-username": "1.0.0",
|
||||
"@radix-ui/react-popover": "1.0.3",
|
||||
@ -49,11 +50,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.4",
|
||||
"sass": "1.51.0",
|
||||
"socket.io-client": "2.3.1",
|
||||
"tunnel-rat": "0.1.2"
|
||||
@ -125,7 +126,7 @@
|
||||
"test": "yarn test:app",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:coverage:watch": "vitest --coverage --watch",
|
||||
"test:ui": "yarn test --ui",
|
||||
"test:ui": "yarn test --ui --coverage.enabled=true",
|
||||
"autorelease": "node scripts/autorelease.js",
|
||||
"prerelease": "node scripts/prerelease.js",
|
||||
"build:preview": "yarn build && vite preview --port 5000",
|
||||
|
@ -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",
|
||||
|
@ -3,33 +3,43 @@ import { register } from "./register";
|
||||
import {
|
||||
copyTextToSystemClipboard,
|
||||
copyToClipboard,
|
||||
createPasteEvent,
|
||||
probablySupportsClipboardBlob,
|
||||
probablySupportsClipboardWriteText,
|
||||
readSystemClipboard,
|
||||
} from "../clipboard";
|
||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||
import { exportCanvas } from "../data/index";
|
||||
import { getNonDeletedElements, isTextElement } from "../element";
|
||||
import { t } from "../i18n";
|
||||
import { isFirefox } from "../constants";
|
||||
|
||||
export const actionCopy = register({
|
||||
name: "copy",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, _, app) => {
|
||||
perform: async (elements, appState, event: ClipboardEvent | null, app) => {
|
||||
const elementsToCopy = app.scene.getSelectedElements({
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
includeBoundTextElement: true,
|
||||
includeElementsInFrames: true,
|
||||
});
|
||||
|
||||
copyToClipboard(elementsToCopy, app.files);
|
||||
try {
|
||||
await copyToClipboard(elementsToCopy, app.files, event);
|
||||
} catch (error: any) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: error.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
return app.device.isMobile && !!navigator.clipboard;
|
||||
},
|
||||
contextItemLabel: "labels.copy",
|
||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||
keyTest: undefined,
|
||||
@ -38,15 +48,55 @@ export const actionCopy = register({
|
||||
export const actionPaste = register({
|
||||
name: "paste",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements: any, appStates: any, data, app) => {
|
||||
app.pasteFromClipboard(null);
|
||||
perform: async (elements, appState, data, app) => {
|
||||
let types;
|
||||
try {
|
||||
types = await readSystemClipboard();
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError" || error.name === "NotAllowedError") {
|
||||
// user probably aborted the action. Though not 100% sure, it's best
|
||||
// to not annoy them with an error message.
|
||||
return false;
|
||||
}
|
||||
|
||||
console.error(`actionPaste ${error.name}: ${error.message}`);
|
||||
|
||||
if (isFirefox) {
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: t("hints.firefox_clipboard_write"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: t("errors.asyncPasteFailedOnRead"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
app.pasteFromClipboard(createPasteEvent({ types }));
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return {
|
||||
commitToHistory: false,
|
||||
appState: {
|
||||
...appState,
|
||||
errorMessage: t("errors.asyncPasteFailedOnParse"),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
commitToHistory: false,
|
||||
};
|
||||
},
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
return app.device.isMobile && !!navigator.clipboard;
|
||||
},
|
||||
contextItemLabel: "labels.paste",
|
||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||
keyTest: undefined,
|
||||
@ -55,13 +105,10 @@ export const actionPaste = register({
|
||||
export const actionCut = register({
|
||||
name: "cut",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, data, app) => {
|
||||
actionCopy.perform(elements, appState, data, app);
|
||||
perform: (elements, appState, event: ClipboardEvent | null, app) => {
|
||||
actionCopy.perform(elements, appState, event, app);
|
||||
return actionDeleteSelected.perform(elements, appState);
|
||||
},
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
return app.device.isMobile && !!navigator.clipboard;
|
||||
},
|
||||
contextItemLabel: "labels.cut",
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
||||
});
|
||||
|
@ -46,6 +46,7 @@ const deleteSelectedElements = (
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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) => {
|
||||
|
167
src/actions/actionProperties.test.tsx
Normal file
167
src/actions/actionProperties.test.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { Excalidraw } from "../packages/excalidraw/index";
|
||||
import { queryByTestId } from "@testing-library/react";
|
||||
import { render } from "../tests/test-utils";
|
||||
import { UI } from "../tests/helpers/ui";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
||||
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
describe("element locking", () => {
|
||||
beforeEach(async () => {
|
||||
await render(<Excalidraw />);
|
||||
});
|
||||
|
||||
describe("properties when tool selected", () => {
|
||||
it("should show active background top picks", () => {
|
||||
UI.clickTool("rectangle");
|
||||
|
||||
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||
|
||||
// just in case we change it in the future
|
||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||
|
||||
h.setState({
|
||||
currentItemBackgroundColor: color,
|
||||
});
|
||||
const activeColor = queryByTestId(
|
||||
document.body,
|
||||
`color-top-pick-${color}`,
|
||||
);
|
||||
expect(activeColor).toHaveClass("active");
|
||||
});
|
||||
|
||||
it("should show fill style when background non-transparent", () => {
|
||||
UI.clickTool("rectangle");
|
||||
|
||||
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||
|
||||
// just in case we change it in the future
|
||||
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||
|
||||
h.setState({
|
||||
currentItemBackgroundColor: color,
|
||||
currentItemFillStyle: "hachure",
|
||||
});
|
||||
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||
|
||||
expect(hachureFillButton).toHaveClass("active");
|
||||
h.setState({
|
||||
currentItemFillStyle: "solid",
|
||||
});
|
||||
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
||||
expect(solidFillStyle).toHaveClass("active");
|
||||
});
|
||||
|
||||
it("should not show fill style when background transparent", () => {
|
||||
UI.clickTool("rectangle");
|
||||
|
||||
h.setState({
|
||||
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
||||
currentItemFillStyle: "hachure",
|
||||
});
|
||||
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||
|
||||
expect(hachureFillButton).toBe(null);
|
||||
});
|
||||
|
||||
it("should show horizontal text align for text tool", () => {
|
||||
UI.clickTool("text");
|
||||
|
||||
h.setState({
|
||||
currentItemTextAlign: "right",
|
||||
});
|
||||
|
||||
const centerTextAlign = queryByTestId(document.body, `align-right`);
|
||||
expect(centerTextAlign).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
describe("properties when elements selected", () => {
|
||||
it("should show active styles when single element selected", () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
backgroundColor: "red",
|
||||
fillStyle: "cross-hatch",
|
||||
});
|
||||
h.elements = [rect];
|
||||
API.setSelectedElements([rect]);
|
||||
|
||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||
expect(crossHatchButton).toHaveClass("active");
|
||||
});
|
||||
|
||||
it("should not show fill style selected element's background is transparent", () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
backgroundColor: COLOR_PALETTE.transparent,
|
||||
fillStyle: "cross-hatch",
|
||||
});
|
||||
h.elements = [rect];
|
||||
API.setSelectedElements([rect]);
|
||||
|
||||
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||
expect(crossHatchButton).toBe(null);
|
||||
});
|
||||
|
||||
it("should highlight common stroke width of selected elements", () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.thin,
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.thin,
|
||||
});
|
||||
h.elements = [rect1, rect2];
|
||||
API.setSelectedElements([rect1, rect2]);
|
||||
|
||||
const thinStrokeWidthButton = queryByTestId(
|
||||
document.body,
|
||||
`strokeWidth-thin`,
|
||||
);
|
||||
expect(thinStrokeWidthButton).toBeChecked();
|
||||
});
|
||||
|
||||
it("should not highlight any stroke width button if no common style", () => {
|
||||
const rect1 = API.createElement({
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.thin,
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.bold,
|
||||
});
|
||||
h.elements = [rect1, rect2];
|
||||
API.setSelectedElements([rect1, rect2]);
|
||||
|
||||
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
||||
expect(
|
||||
queryByTestId(document.body, `strokeWidth-thin`),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
queryByTestId(document.body, `strokeWidth-bold`),
|
||||
).not.toBeChecked();
|
||||
expect(
|
||||
queryByTestId(document.body, `strokeWidth-extraBold`),
|
||||
).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should show properties of different element types when selected", () => {
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
strokeWidth: STROKE_WIDTH.bold,
|
||||
});
|
||||
const text = API.createElement({
|
||||
type: "text",
|
||||
fontFamily: FONT_FAMILY.Cascadia,
|
||||
});
|
||||
h.elements = [rect, text];
|
||||
API.setSelectedElements([rect, text]);
|
||||
|
||||
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,4 +1,4 @@
|
||||
import { AppState } from "../../src/types";
|
||||
import { AppState, Primitive } from "../../src/types";
|
||||
import {
|
||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||
@ -51,6 +51,7 @@ import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
FONT_FAMILY,
|
||||
ROUNDNESS,
|
||||
STROKE_WIDTH,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import {
|
||||
@ -82,7 +83,6 @@ import { getLanguage, t } from "../i18n";
|
||||
import { KEYS } from "../keys";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
canChangeRoundness,
|
||||
canHaveArrowheads,
|
||||
getCommonAttributeOfSelectedElements,
|
||||
getSelectedElements,
|
||||
@ -118,25 +118,44 @@ export const changeProperty = (
|
||||
});
|
||||
};
|
||||
|
||||
export const getFormValue = function <T>(
|
||||
export const getFormValue = function <T extends Primitive>(
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
getAttribute: (element: ExcalidrawElement) => T,
|
||||
defaultValue: T,
|
||||
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
|
||||
defaultValue: T | ((isSomeElementSelected: boolean) => T),
|
||||
): T {
|
||||
const editingElement = appState.editingElement;
|
||||
const nonDeletedElements = getNonDeletedElements(elements);
|
||||
return (
|
||||
(editingElement && getAttribute(editingElement)) ??
|
||||
(isSomeElementSelected(nonDeletedElements, appState)
|
||||
? getCommonAttributeOfSelectedElements(
|
||||
nonDeletedElements,
|
||||
|
||||
let ret: T | null = null;
|
||||
|
||||
if (editingElement) {
|
||||
ret = getAttribute(editingElement);
|
||||
}
|
||||
|
||||
if (!ret) {
|
||||
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
|
||||
|
||||
if (hasSelection) {
|
||||
ret =
|
||||
getCommonAttributeOfSelectedElements(
|
||||
isRelevantElement === true
|
||||
? nonDeletedElements
|
||||
: nonDeletedElements.filter((el) => isRelevantElement(el)),
|
||||
appState,
|
||||
getAttribute,
|
||||
)
|
||||
: defaultValue) ??
|
||||
defaultValue
|
||||
);
|
||||
) ??
|
||||
(typeof defaultValue === "function"
|
||||
? defaultValue(true)
|
||||
: defaultValue);
|
||||
} else {
|
||||
ret =
|
||||
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
const offsetElementAfterFontResize = (
|
||||
@ -247,6 +266,7 @@ export const actionChangeStrokeColor = register({
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.strokeColor,
|
||||
true,
|
||||
appState.currentItemStrokeColor,
|
||||
)}
|
||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||
@ -289,6 +309,7 @@ export const actionChangeBackgroundColor = register({
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.backgroundColor,
|
||||
true,
|
||||
appState.currentItemBackgroundColor,
|
||||
)}
|
||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||
@ -338,23 +359,28 @@ export const actionChangeFillStyle = register({
|
||||
} (${getShortcutKey("Alt-Click")})`,
|
||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||
active: allElementsZigZag ? true : undefined,
|
||||
testId: `fill-hachure`,
|
||||
},
|
||||
{
|
||||
value: "cross-hatch",
|
||||
text: t("labels.crossHatch"),
|
||||
icon: FillCrossHatchIcon,
|
||||
testId: `fill-cross-hatch`,
|
||||
},
|
||||
{
|
||||
value: "solid",
|
||||
text: t("labels.solid"),
|
||||
icon: FillSolidIcon,
|
||||
testId: `fill-solid`,
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.fillStyle,
|
||||
appState.currentItemFillStyle,
|
||||
(element) => element.hasOwnProperty("fillStyle"),
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemFillStyle,
|
||||
)}
|
||||
onClick={(value, event) => {
|
||||
const nextValue =
|
||||
@ -393,26 +419,31 @@ export const actionChangeStrokeWidth = register({
|
||||
group="stroke-width"
|
||||
options={[
|
||||
{
|
||||
value: 1,
|
||||
value: STROKE_WIDTH.thin,
|
||||
text: t("labels.thin"),
|
||||
icon: StrokeWidthBaseIcon,
|
||||
testId: "strokeWidth-thin",
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
value: STROKE_WIDTH.bold,
|
||||
text: t("labels.bold"),
|
||||
icon: StrokeWidthBoldIcon,
|
||||
testId: "strokeWidth-bold",
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
value: STROKE_WIDTH.extraBold,
|
||||
text: t("labels.extraBold"),
|
||||
icon: StrokeWidthExtraBoldIcon,
|
||||
testId: "strokeWidth-extraBold",
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.strokeWidth,
|
||||
appState.currentItemStrokeWidth,
|
||||
(element) => element.hasOwnProperty("strokeWidth"),
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemStrokeWidth,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -461,7 +492,9 @@ export const actionChangeSloppiness = register({
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.roughness,
|
||||
appState.currentItemRoughness,
|
||||
(element) => element.hasOwnProperty("roughness"),
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemRoughness,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -509,7 +542,9 @@ export const actionChangeStrokeStyle = register({
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.strokeStyle,
|
||||
appState.currentItemStrokeStyle,
|
||||
(element) => element.hasOwnProperty("strokeStyle"),
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemStrokeStyle,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -549,6 +584,7 @@ export const actionChangeOpacity = register({
|
||||
elements,
|
||||
appState,
|
||||
(element) => element.opacity,
|
||||
true,
|
||||
appState.currentItemOpacity,
|
||||
) ?? undefined
|
||||
}
|
||||
@ -607,7 +643,12 @@ export const actionChangeFontSize = register({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -692,21 +733,25 @@ export const actionChangeFontFamily = register({
|
||||
value: FontFamilyValues;
|
||||
text: string;
|
||||
icon: JSX.Element;
|
||||
testId: string;
|
||||
}[] = [
|
||||
{
|
||||
value: FONT_FAMILY.Virgil,
|
||||
text: t("labels.handDrawn"),
|
||||
icon: FreedrawIcon,
|
||||
testId: "font-family-virgil",
|
||||
},
|
||||
{
|
||||
value: FONT_FAMILY.Helvetica,
|
||||
text: t("labels.normal"),
|
||||
icon: FontFamilyNormalIcon,
|
||||
testId: "font-family-normal",
|
||||
},
|
||||
{
|
||||
value: FONT_FAMILY.Cascadia,
|
||||
text: t("labels.code"),
|
||||
icon: FontFamilyCodeIcon,
|
||||
testId: "font-family-code",
|
||||
},
|
||||
];
|
||||
|
||||
@ -729,7 +774,12 @@ export const actionChangeFontFamily = register({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -806,7 +856,10 @@ export const actionChangeTextAlign = register({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
appState.currentItemTextAlign,
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemTextAlign,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -882,7 +935,9 @@ export const actionChangeVerticalAlign = register({
|
||||
}
|
||||
return null;
|
||||
},
|
||||
VERTICAL_ALIGN.MIDDLE,
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -947,9 +1002,9 @@ export const actionChangeRoundness = register({
|
||||
appState,
|
||||
(element) =>
|
||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||
(canChangeRoundness(appState.activeTool.type) &&
|
||||
appState.currentItemRoundness) ||
|
||||
null,
|
||||
(element) => element.hasOwnProperty("roundness"),
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemRoundness,
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
/>
|
||||
@ -1043,6 +1098,7 @@ export const actionChangeArrowhead = register({
|
||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||
? element.startArrowhead
|
||||
: appState.currentItemStartArrowhead,
|
||||
true,
|
||||
appState.currentItemStartArrowhead,
|
||||
)}
|
||||
onChange={(value) => updateData({ position: "start", type: value })}
|
||||
@ -1089,6 +1145,7 @@ export const actionChangeArrowhead = register({
|
||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||
? element.endArrowhead
|
||||
: appState.currentItemEndArrowhead,
|
||||
true,
|
||||
appState.currentItemEndArrowhead,
|
||||
)}
|
||||
onChange={(value) => updateData({ position: "end", type: value })}
|
||||
|
@ -119,10 +119,10 @@ export class ActionManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
executeAction(
|
||||
action: Action,
|
||||
executeAction<T extends Action>(
|
||||
action: T,
|
||||
source: ActionSource = "api",
|
||||
value: any = null,
|
||||
value: Parameters<T["perform"]>[2] = null,
|
||||
) {
|
||||
const elements = this.getElementsIncludingDeleted();
|
||||
const appState = this.getAppState();
|
||||
|
@ -1,27 +1,196 @@
|
||||
import { parseClipboard } from "./clipboard";
|
||||
import {
|
||||
createPasteEvent,
|
||||
parseClipboard,
|
||||
serializeAsClipboardJSON,
|
||||
} from "./clipboard";
|
||||
import { API } from "./tests/helpers/api";
|
||||
|
||||
describe("Test parseClipboard", () => {
|
||||
it("should parse valid json correctly", async () => {
|
||||
let text = "123";
|
||||
|
||||
let clipboardData = await parseClipboard({
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
describe("parseClipboard()", () => {
|
||||
it("should parse JSON as plaintext if not excalidraw-api/clipboard data", async () => {
|
||||
let text;
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
text = "123";
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
);
|
||||
expect(clipboardData.text).toBe(text);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
text = "[123]";
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
);
|
||||
expect(clipboardData.text).toBe(text);
|
||||
|
||||
clipboardData = await parseClipboard({
|
||||
//@ts-ignore
|
||||
clipboardData: {
|
||||
getData: () => text,
|
||||
},
|
||||
});
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
text = JSON.stringify({ val: 42 });
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({ types: { "text/plain": text } }),
|
||||
);
|
||||
expect(clipboardData.text).toBe(text);
|
||||
});
|
||||
|
||||
it("should parse valid excalidraw JSON if inside text/plain", async () => {
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
|
||||
const json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||
const clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": json,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.elements).toEqual([rect]);
|
||||
});
|
||||
|
||||
it("should parse valid excalidraw JSON if inside text/html", async () => {
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
|
||||
let json;
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": json,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.elements).toEqual([rect]);
|
||||
// -------------------------------------------------------------------------
|
||||
json = serializeAsClipboardJSON({ elements: [rect], files: null });
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<div> ${json}</div>`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.elements).toEqual([rect]);
|
||||
// -------------------------------------------------------------------------
|
||||
});
|
||||
|
||||
it("should parse <image> `src` urls out of text/html", async () => {
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<img src="https://example.com/image.png" />`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.mixedContent).toEqual([
|
||||
{
|
||||
type: "imageUrl",
|
||||
value: "https://example.com/image.png",
|
||||
},
|
||||
]);
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<div><img src="https://example.com/image.png" /></div><a><img src="https://example.com/image2.png" /></a>`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.mixedContent).toEqual([
|
||||
{
|
||||
type: "imageUrl",
|
||||
value: "https://example.com/image.png",
|
||||
},
|
||||
{
|
||||
type: "imageUrl",
|
||||
value: "https://example.com/image2.png",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse text content alongside <image> `src` urls out of text/html", async () => {
|
||||
const clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<a href="https://example.com">hello </a><div><img src="https://example.com/image.png" /></div><b>my friend!</b>`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.mixedContent).toEqual([
|
||||
{
|
||||
type: "text",
|
||||
// trimmed
|
||||
value: "hello",
|
||||
},
|
||||
{
|
||||
type: "imageUrl",
|
||||
value: "https://example.com/image.png",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
value: "my friend!",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("should parse spreadsheet from either text/plain and text/html", async () => {
|
||||
let clipboardData;
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/plain": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.spreadsheet).toEqual({
|
||||
title: "b",
|
||||
labels: ["1", "4", "7"],
|
||||
values: [2, 5, 10],
|
||||
});
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.spreadsheet).toEqual({
|
||||
title: "b",
|
||||
labels: ["1", "4", "7"],
|
||||
values: [2, 5, 10],
|
||||
});
|
||||
// -------------------------------------------------------------------------
|
||||
clipboardData = await parseClipboard(
|
||||
createPasteEvent({
|
||||
types: {
|
||||
"text/html": `<html>
|
||||
<body>
|
||||
<!--StartFragment--><google-sheets-html-origin><style type="text/css"><!--td {border: 1px solid #cccccc;}br {mso-data-placement:same-cell;}--></style><table xmlns="http://www.w3.org/1999/xhtml" cellspacing="0" cellpadding="0" dir="ltr" border="1" style="table-layout:fixed;font-size:10pt;font-family:Arial;width:0px;border-collapse:collapse;border:none"><colgroup><col width="100"/><col width="100"/></colgroup><tbody><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"a"}">a</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;" data-sheets-value="{"1":2,"2":"b"}">b</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":1}">1</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":2}">2</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":4}">4</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":5}">5</td></tr><tr style="height:21px;"><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":7}">7</td><td style="overflow:hidden;padding:2px 3px 2px 3px;vertical-align:bottom;text-align:right;" data-sheets-value="{"1":3,"3":10}">10</td></tr></tbody></table><!--EndFragment-->
|
||||
</body>
|
||||
</html>`,
|
||||
"text/plain": `a b
|
||||
1 2
|
||||
4 5
|
||||
7 10`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(clipboardData.spreadsheet).toEqual({
|
||||
title: "b",
|
||||
labels: ["1", "4", "7"],
|
||||
values: [2, 5, 10],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
354
src/clipboard.ts
354
src/clipboard.ts
@ -3,14 +3,18 @@ import {
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { BinaryFiles } from "./types";
|
||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
|
||||
import {
|
||||
ALLOWED_PASTE_MIME_TYPES,
|
||||
EXPORT_DATA_TYPES,
|
||||
MIME_TYPES,
|
||||
} from "./constants";
|
||||
import { isInitializedImageElement } from "./element/typeChecks";
|
||||
import { deepCopyElement } from "./element/newElement";
|
||||
import { mutateElement } from "./element/mutateElement";
|
||||
import { getContainingFrame } from "./frame";
|
||||
import { isPromiseLike, isTestEnv } from "./utils";
|
||||
import { isMemberOf, isPromiseLike } from "./utils";
|
||||
import { t } from "./i18n";
|
||||
|
||||
type ElementsClipboard = {
|
||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||
@ -18,17 +22,23 @@ 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;
|
||||
}
|
||||
|
||||
let CLIPBOARD = "";
|
||||
let PREFER_APP_CLIPBOARD = false;
|
||||
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number];
|
||||
|
||||
type ParsedClipboardEvent =
|
||||
| { type: "text"; value: string }
|
||||
| { type: "mixedContent"; value: PastedMixedContent };
|
||||
|
||||
export const probablySupportsClipboardReadText =
|
||||
"clipboard" in navigator && "readText" in navigator.clipboard;
|
||||
@ -58,10 +68,61 @@ const clipboardContainsElements = (
|
||||
return false;
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
files: BinaryFiles | null,
|
||||
) => {
|
||||
export const createPasteEvent = ({
|
||||
types,
|
||||
files,
|
||||
}: {
|
||||
types?: { [key in AllowedPasteMimeTypes]?: string };
|
||||
files?: File[];
|
||||
}) => {
|
||||
if (!types && !files) {
|
||||
console.warn("createPasteEvent: no types or files provided");
|
||||
}
|
||||
|
||||
const event = new ClipboardEvent("paste", {
|
||||
clipboardData: new DataTransfer(),
|
||||
});
|
||||
|
||||
if (types) {
|
||||
for (const [type, value] of Object.entries(types)) {
|
||||
try {
|
||||
event.clipboardData?.setData(type, value);
|
||||
if (event.clipboardData?.getData(type) !== value) {
|
||||
throw new Error(`Failed to set "${type}" as clipboardData item`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (files) {
|
||||
let idx = -1;
|
||||
for (const file of files) {
|
||||
idx++;
|
||||
try {
|
||||
event.clipboardData?.items.add(file);
|
||||
if (event.clipboardData?.files[idx] !== file) {
|
||||
throw new Error(
|
||||
`Failed to set file "${file.name}" as clipboardData item`,
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
};
|
||||
|
||||
export const serializeAsClipboardJSON = ({
|
||||
elements,
|
||||
files,
|
||||
}: {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
files: BinaryFiles | null;
|
||||
}) => {
|
||||
const framesToCopy = new Set(
|
||||
elements.filter((element) => element.type === "frame"),
|
||||
);
|
||||
@ -83,7 +144,7 @@ export const copyToClipboard = async (
|
||||
);
|
||||
}
|
||||
|
||||
// select binded text elements when copying
|
||||
// select bound text elements when copying
|
||||
const contents: ElementsClipboard = {
|
||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||
elements: elements.map((element) => {
|
||||
@ -102,34 +163,20 @@ export const copyToClipboard = async (
|
||||
}),
|
||||
files: files ? _files : undefined,
|
||||
};
|
||||
const json = JSON.stringify(contents);
|
||||
|
||||
if (isTestEnv()) {
|
||||
return json;
|
||||
}
|
||||
|
||||
CLIPBOARD = json;
|
||||
|
||||
try {
|
||||
PREFER_APP_CLIPBOARD = false;
|
||||
await copyTextToSystemClipboard(json);
|
||||
} catch (error: any) {
|
||||
PREFER_APP_CLIPBOARD = true;
|
||||
console.error(error);
|
||||
}
|
||||
return JSON.stringify(contents);
|
||||
};
|
||||
|
||||
const getAppClipboard = (): Partial<ElementsClipboard> => {
|
||||
if (!CLIPBOARD) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(CLIPBOARD);
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
export const copyToClipboard = async (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
files: BinaryFiles | null,
|
||||
/** supply if available to make the operation more certain to succeed */
|
||||
clipboardEvent?: ClipboardEvent | null,
|
||||
) => {
|
||||
await copyTextToSystemClipboard(
|
||||
serializeAsClipboardJSON({ elements, files }),
|
||||
clipboardEvent,
|
||||
);
|
||||
};
|
||||
|
||||
const parsePotentialSpreadsheet = (
|
||||
@ -142,22 +189,137 @@ const parsePotentialSpreadsheet = (
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves content from system clipboard (either from ClipboardEvent or
|
||||
* via async clipboard API if supported)
|
||||
*/
|
||||
export const getSystemClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
): Promise<string> => {
|
||||
try {
|
||||
const text = event
|
||||
? event.clipboardData?.getData("text/plain")
|
||||
: probablySupportsClipboardReadText &&
|
||||
(await navigator.clipboard.readText());
|
||||
/** 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;
|
||||
}
|
||||
|
||||
return (text || "").trim();
|
||||
const maybeParseHTMLPaste = (
|
||||
event: ClipboardEvent,
|
||||
): { type: "mixedContent"; value: PastedMixedContent } | null => {
|
||||
const html = event.clipboardData?.getData("text/html");
|
||||
|
||||
if (!html) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
|
||||
const content = parseHTMLTree(doc.body);
|
||||
|
||||
if (content.length) {
|
||||
return { type: "mixedContent", value: content };
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(`error in parseHTMLFromPaste: ${error.message}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const readSystemClipboard = async () => {
|
||||
const types: { [key in AllowedPasteMimeTypes]?: string } = {};
|
||||
|
||||
try {
|
||||
if (navigator.clipboard?.readText) {
|
||||
return { "text/plain": await navigator.clipboard?.readText() };
|
||||
}
|
||||
} catch (error: any) {
|
||||
// @ts-ignore
|
||||
if (navigator.clipboard?.read) {
|
||||
console.warn(
|
||||
`navigator.clipboard.readText() failed (${error.message}). Failling back to navigator.clipboard.read()`,
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let clipboardItems: ClipboardItems;
|
||||
|
||||
try {
|
||||
clipboardItems = await navigator.clipboard?.read();
|
||||
} catch (error: any) {
|
||||
if (error.name === "DataError") {
|
||||
console.warn(
|
||||
`navigator.clipboard.read() error, clipboard is probably empty: ${error.message}`,
|
||||
);
|
||||
return types;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const item of clipboardItems) {
|
||||
for (const type of item.types) {
|
||||
if (!isMemberOf(ALLOWED_PASTE_MIME_TYPES, type)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
types[type] = await (await item.getType(type)).text();
|
||||
} catch (error: any) {
|
||||
console.warn(
|
||||
`Cannot retrieve ${type} from clipboardItem: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(types).length === 0) {
|
||||
console.warn("No clipboard data found from clipboard.read().");
|
||||
return types;
|
||||
}
|
||||
|
||||
return types;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses "paste" ClipboardEvent.
|
||||
*/
|
||||
const parseClipboardEvent = async (
|
||||
event: ClipboardEvent,
|
||||
isPlainPaste = false,
|
||||
): Promise<ParsedClipboardEvent> => {
|
||||
try {
|
||||
const mixedContent = !isPlainPaste && event && maybeParseHTMLPaste(event);
|
||||
|
||||
if (mixedContent) {
|
||||
if (mixedContent.value.every((item) => item.type === "text")) {
|
||||
return {
|
||||
type: "text",
|
||||
value:
|
||||
event.clipboardData?.getData("text/plain") ||
|
||||
mixedContent.value
|
||||
.map((item) => item.value)
|
||||
.join("\n")
|
||||
.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
return mixedContent;
|
||||
}
|
||||
|
||||
const text = event.clipboardData?.getData("text/plain");
|
||||
|
||||
return { type: "text", value: (text || "").trim() };
|
||||
} catch {
|
||||
return "";
|
||||
return { type: "text", value: "" };
|
||||
}
|
||||
};
|
||||
|
||||
@ -165,34 +327,32 @@ export const getSystemClipboard = async (
|
||||
* Attempts to parse clipboard. Prefers system clipboard.
|
||||
*/
|
||||
export const parseClipboard = async (
|
||||
event: ClipboardEvent | null,
|
||||
event: ClipboardEvent,
|
||||
isPlainPaste = false,
|
||||
): Promise<ClipboardData> => {
|
||||
const systemClipboard = await getSystemClipboard(event);
|
||||
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
||||
|
||||
// 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))
|
||||
) {
|
||||
return getAppClipboard();
|
||||
if (parsedEventData.type === "mixedContent") {
|
||||
return {
|
||||
mixedContent: parsedEventData.value,
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
if (spreadsheetResult) {
|
||||
return spreadsheetResult;
|
||||
}
|
||||
|
||||
const appClipboardData = getAppClipboard();
|
||||
|
||||
try {
|
||||
const systemClipboardData = JSON.parse(systemClipboard);
|
||||
// if system clipboard contains spreadsheet, use it even though it's
|
||||
// technically possible it's staler than in-app clipboard
|
||||
const spreadsheetResult =
|
||||
!isPlainPaste && parsePotentialSpreadsheet(parsedEventData.value);
|
||||
|
||||
if (spreadsheetResult) {
|
||||
return spreadsheetResult;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
try {
|
||||
const systemClipboardData = JSON.parse(parsedEventData.value);
|
||||
const programmaticAPI =
|
||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||
if (clipboardContainsElements(systemClipboardData)) {
|
||||
@ -205,18 +365,9 @@ export const parseClipboard = async (
|
||||
programmaticAPI,
|
||||
};
|
||||
}
|
||||
} catch (e) {}
|
||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
||||
// support storing to system clipboard on copy
|
||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
||||
? {
|
||||
...appClipboardData,
|
||||
text: isPlainPaste
|
||||
? JSON.stringify(appClipboardData.elements, null, 2)
|
||||
: undefined,
|
||||
}
|
||||
: { text: systemClipboard };
|
||||
} catch {}
|
||||
|
||||
return { text: parsedEventData.value };
|
||||
};
|
||||
|
||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
@ -249,28 +400,49 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
||||
let copied = false;
|
||||
export const copyTextToSystemClipboard = async (
|
||||
text: string | null,
|
||||
clipboardEvent?: ClipboardEvent | null,
|
||||
) => {
|
||||
// (1) first try using Async Clipboard API
|
||||
if (probablySupportsClipboardWriteText) {
|
||||
try {
|
||||
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
||||
// not focused
|
||||
await navigator.clipboard.writeText(text || "");
|
||||
copied = true;
|
||||
return;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Note that execCommand doesn't allow copying empty strings, so if we're
|
||||
// clearing clipboard using this API, we must copy at least an empty char
|
||||
if (!copied && !copyTextViaExecCommand(text || " ")) {
|
||||
throw new Error("couldn't copy");
|
||||
// (2) if fails and we have access to ClipboardEvent, use plain old setData()
|
||||
try {
|
||||
if (clipboardEvent) {
|
||||
clipboardEvent.clipboardData?.setData("text/plain", text || "");
|
||||
if (clipboardEvent.clipboardData?.getData("text/plain") !== text) {
|
||||
throw new Error("Failed to setData on clipboardEvent");
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// (3) if that fails, use document.execCommand
|
||||
if (!copyTextViaExecCommand(text)) {
|
||||
throw new Error(t("errors.copyToSystemClipboardFailed"));
|
||||
}
|
||||
};
|
||||
|
||||
// adapted from https://github.com/zenorocha/clipboard.js/blob/ce79f170aa655c408b6aab33c9472e8e4fa52e19/src/clipboard-action.js#L48
|
||||
const copyTextViaExecCommand = (text: string) => {
|
||||
const copyTextViaExecCommand = (text: string | null) => {
|
||||
// execCommand doesn't allow copying empty strings, so if we're
|
||||
// clearing clipboard using this API, we must copy at least an empty char
|
||||
if (!text) {
|
||||
text = " ";
|
||||
}
|
||||
|
||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||
|
||||
const textarea = document.createElement("textarea");
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
@ -11,7 +11,6 @@ import {
|
||||
hasBackground,
|
||||
hasStrokeStyle,
|
||||
hasStrokeWidth,
|
||||
hasText,
|
||||
} from "../scene";
|
||||
import { SHAPES } from "../shapes";
|
||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||
@ -20,7 +19,7 @@ import Stack from "./Stack";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { hasStrokeColor } from "../scene/comparisons";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { hasBoundTextElement } from "../element/typeChecks";
|
||||
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
|
||||
import clsx from "clsx";
|
||||
import { actionToggleZenMode } from "../actions";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
@ -35,6 +34,7 @@ import {
|
||||
EmbedIcon,
|
||||
extraToolsIcon,
|
||||
frameToolIcon,
|
||||
mermaidLogoIcon,
|
||||
laserPointerToolIcon,
|
||||
} from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
@ -66,7 +66,8 @@ export const SelectedShapeActions = ({
|
||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||
|
||||
const showFillIcons =
|
||||
hasBackground(appState.activeTool.type) ||
|
||||
(hasBackground(appState.activeTool.type) &&
|
||||
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||
targetElements.some(
|
||||
(element) =>
|
||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||
@ -123,14 +124,15 @@ export const SelectedShapeActions = ({
|
||||
<>{renderAction("changeRoundness")}</>
|
||||
)}
|
||||
|
||||
{(hasText(appState.activeTool.type) ||
|
||||
targetElements.some((element) => hasText(element.type))) && (
|
||||
{(appState.activeTool.type === "text" ||
|
||||
targetElements.some(isTextElement)) && (
|
||||
<>
|
||||
{renderAction("changeFontSize")}
|
||||
|
||||
{renderAction("changeFontFamily")}
|
||||
|
||||
{suppportsHorizontalAlign(targetElements) &&
|
||||
{(appState.activeTool.type === "text" ||
|
||||
suppportsHorizontalAlign(targetElements)) &&
|
||||
renderAction("changeTextAlign")}
|
||||
</>
|
||||
)}
|
||||
@ -213,20 +215,15 @@ export const SelectedShapeActions = ({
|
||||
};
|
||||
|
||||
export const ShapesSwitcher = ({
|
||||
interactiveCanvas,
|
||||
activeTool,
|
||||
onImageAction,
|
||||
appState,
|
||||
app,
|
||||
}: {
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
activeTool: UIAppState["activeTool"];
|
||||
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";
|
||||
@ -263,120 +260,76 @@ export const ShapesSwitcher = ({
|
||||
if (appState.activeTool.type !== value) {
|
||||
trackEvent("toolbar", value, "ui");
|
||||
}
|
||||
app.setActiveTool({ type: value });
|
||||
if (value === "image") {
|
||||
onImageAction({ pointerType });
|
||||
app.setActiveTool({
|
||||
type: value,
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
});
|
||||
} else {
|
||||
app.setActiveTool({ type: value });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div className="App-toolbar__divider" />
|
||||
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
||||
{device.isMobile ? (
|
||||
<>
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
|
||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className={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")}
|
||||
>
|
||||
{extraToolsIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||
className="App-toolbar__extra-tools-dropdown"
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "frame" })}
|
||||
icon={frameToolIcon}
|
||||
checked={activeTool.type === "frame"}
|
||||
name="editor-current-shape"
|
||||
title={`${capitalizeString(
|
||||
t("toolBar.frame"),
|
||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid={`toolbar-frame`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "frame", "ui");
|
||||
app.setActiveTool({ type: "frame" });
|
||||
}}
|
||||
selected={activeTool.type === "frame"}
|
||||
/>
|
||||
<ToolButton
|
||||
className={clsx("Shape", { fillable: false })}
|
||||
type="radio"
|
||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid="toolbar-frame"
|
||||
selected={frameToolSelected}
|
||||
>
|
||||
{t("toolBar.frame")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "embeddable" })}
|
||||
icon={EmbedIcon}
|
||||
checked={activeTool.type === "embeddable"}
|
||||
name="editor-current-shape"
|
||||
title={capitalizeString(t("toolBar.embeddable"))}
|
||||
aria-label={capitalizeString(t("toolBar.embeddable"))}
|
||||
data-testid={`toolbar-embeddable`}
|
||||
onPointerDown={({ pointerType }) => {
|
||||
if (!appState.penDetected && pointerType === "pen") {
|
||||
app.togglePenMode(true);
|
||||
}
|
||||
}}
|
||||
onChange={({ pointerType }) => {
|
||||
trackEvent("toolbar", "embeddable", "ui");
|
||||
app.setActiveTool({ type: "embeddable" });
|
||||
}}
|
||||
selected={activeTool.type === "embeddable"}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||
<DropdownMenu.Trigger
|
||||
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||
"App-toolbar__extra-tools-trigger--selected":
|
||||
frameToolSelected ||
|
||||
embeddableToolSelected ||
|
||||
// in collab we're already highlighting the laser button
|
||||
// outside toolbar, so let's not highlight extra-tools button
|
||||
// on top of it
|
||||
(laserToolSelected && !app.props.isCollaborating),
|
||||
})}
|
||||
onToggle={() => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen)}
|
||||
title={t("toolBar.extraTools")}
|
||||
data-testid="toolbar-embeddable"
|
||||
selected={embeddableToolSelected}
|
||||
>
|
||||
{extraToolsIcon}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content
|
||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
||||
className="App-toolbar__extra-tools-dropdown"
|
||||
{t("toolBar.embeddable")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setActiveTool({ type: "laser" })}
|
||||
icon={laserPointerToolIcon}
|
||||
data-testid="toolbar-laser"
|
||||
selected={laserToolSelected}
|
||||
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||
>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
app.setActiveTool({ type: "frame" });
|
||||
}}
|
||||
icon={frameToolIcon}
|
||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||
data-testid="toolbar-frame"
|
||||
selected={frameToolSelected}
|
||||
>
|
||||
{t("toolBar.frame")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
app.setActiveTool({ type: "embeddable" });
|
||||
}}
|
||||
icon={EmbedIcon}
|
||||
data-testid="toolbar-embeddable"
|
||||
selected={embeddableToolSelected}
|
||||
>
|
||||
{t("toolBar.embeddable")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
app.setActiveTool({ type: "laser" });
|
||||
}}
|
||||
icon={laserPointerToolIcon}
|
||||
data-testid="toolbar-laser"
|
||||
selected={laserToolSelected}
|
||||
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||
>
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{t("toolBar.laser")}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => app.setOpenDialog("mermaid")}
|
||||
icon={mermaidLogoIcon}
|
||||
data-testid="toolbar-embeddable"
|
||||
>
|
||||
{t("toolBar.mermaidToExcalidraw")}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -47,7 +47,7 @@ import {
|
||||
isEraserActive,
|
||||
isHandToolActive,
|
||||
} from "../appState";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import { PastedMixedContent, parseClipboard } from "../clipboard";
|
||||
import {
|
||||
APP_NAME,
|
||||
CURSOR_TYPE,
|
||||
@ -241,18 +241,14 @@ import {
|
||||
isInputLike,
|
||||
isToolIcon,
|
||||
isWritableElement,
|
||||
resetCursor,
|
||||
resolvablePromise,
|
||||
sceneCoordsToViewportCoords,
|
||||
setCursor,
|
||||
setCursorForShape,
|
||||
tupleToCoors,
|
||||
viewportCoordsToSceneCoords,
|
||||
withBatchedUpdates,
|
||||
wrapEvent,
|
||||
withBatchedUpdatesThrottled,
|
||||
updateObject,
|
||||
setEraserCursor,
|
||||
updateActiveTool,
|
||||
getShortcutKey,
|
||||
isTransparent,
|
||||
@ -279,6 +275,7 @@ import {
|
||||
generateIdFromFile,
|
||||
getDataURL,
|
||||
getFileFromEvent,
|
||||
ImageURLToFile,
|
||||
isImageFileHandle,
|
||||
isSupportedImageFile,
|
||||
loadSceneOrLibraryFromBlob,
|
||||
@ -369,8 +366,16 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||
import { Renderer } from "../scene/Renderer";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import MermaidToExcalidraw from "./MermaidToExcalidraw";
|
||||
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
||||
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
||||
import {
|
||||
setEraserCursor,
|
||||
setCursor,
|
||||
resetCursor,
|
||||
setCursorForShape,
|
||||
} from "../cursor";
|
||||
import { Emitter } from "../emitter";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
@ -502,6 +507,30 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
laserPathManager: LaserPathManager = new LaserPathManager(this);
|
||||
|
||||
onChangeEmitter = new Emitter<
|
||||
[
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
files: BinaryFiles,
|
||||
]
|
||||
>();
|
||||
|
||||
onPointerDownEmitter = new Emitter<
|
||||
[
|
||||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: PointerDownState,
|
||||
event: React.PointerEvent<HTMLElement>,
|
||||
]
|
||||
>();
|
||||
|
||||
onPointerUpEmitter = new Emitter<
|
||||
[
|
||||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: PointerDownState,
|
||||
event: PointerEvent,
|
||||
]
|
||||
>();
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
const defaultAppState = getDefaultAppState();
|
||||
@ -565,6 +594,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
resetCursor: this.resetCursor,
|
||||
updateFrameRendering: this.updateFrameRendering,
|
||||
toggleSidebar: this.toggleSidebar,
|
||||
onChange: (cb) => this.onChangeEmitter.on(cb),
|
||||
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
|
||||
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
|
||||
} as const;
|
||||
if (typeof excalidrawRef === "function") {
|
||||
excalidrawRef(api);
|
||||
@ -1155,6 +1187,9 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.selectionElement ||
|
||||
this.state.draggingElement ||
|
||||
this.state.resizingElement ||
|
||||
(this.state.activeTool.type === "laser" &&
|
||||
// technically we can just test on this once we make it more safe
|
||||
this.state.cursorButton === "down") ||
|
||||
(this.state.editingElement &&
|
||||
!isTextElement(this.state.editingElement))
|
||||
? POINTER_EVENTS.disabled
|
||||
@ -1183,7 +1218,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
>
|
||||
<LayerUI
|
||||
canvas={this.canvas}
|
||||
interactiveCanvas={this.interactiveCanvas}
|
||||
appState={this.state}
|
||||
files={this.files}
|
||||
setAppState={this.setAppState}
|
||||
@ -1200,7 +1234,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state.zenModeEnabled
|
||||
}
|
||||
UIOptions={this.props.UIOptions}
|
||||
onImageAction={this.onImageAction}
|
||||
onExportImage={this.onExportImage}
|
||||
renderWelcomeScreen={
|
||||
!this.state.isLoading &&
|
||||
@ -1213,7 +1246,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
>
|
||||
{this.props.children}
|
||||
{this.state.openDialog === "mermaid" && (
|
||||
<MermaidToExcalidraw />
|
||||
)}
|
||||
</LayerUI>
|
||||
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
<div className="excalidraw-eye-dropper-container" />
|
||||
@ -1243,6 +1280,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
top={this.state.contextMenu.top}
|
||||
left={this.state.contextMenu.left}
|
||||
actionManager={this.actionManager}
|
||||
onClose={(callback) => {
|
||||
this.setState({ contextMenu: null }, () => {
|
||||
this.focusContainer();
|
||||
callback?.();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<StaticCanvas
|
||||
@ -1746,6 +1789,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.destroy();
|
||||
this.library.destroy();
|
||||
this.laserPathManager.destroy();
|
||||
this.onChangeEmitter.destroy();
|
||||
ShapeCache.destroy();
|
||||
SnapCache.destroy();
|
||||
clearTimeout(touchTimeout);
|
||||
@ -2030,6 +2074,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state,
|
||||
this.files,
|
||||
);
|
||||
this.onChangeEmitter.trigger(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.state,
|
||||
this.files,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2072,7 +2121,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
this.cutAll();
|
||||
this.actionManager.executeAction(actionCut, "keyboard", event);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
@ -2084,19 +2133,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
||||
return;
|
||||
}
|
||||
this.copyAll();
|
||||
this.actionManager.executeAction(actionCopy, "keyboard", event);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
private cutAll = () => {
|
||||
this.actionManager.executeAction(actionCut, "keyboard");
|
||||
};
|
||||
|
||||
private copyAll = () => {
|
||||
this.actionManager.executeAction(actionCopy, "keyboard");
|
||||
};
|
||||
|
||||
private static resetTapTwice() {
|
||||
didTapTwice = false;
|
||||
}
|
||||
@ -2157,8 +2198,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
public pasteFromClipboard = withBatchedUpdates(
|
||||
async (event: ClipboardEvent | null) => {
|
||||
const isPlainPaste = !!(IS_PLAIN_PASTE && event);
|
||||
async (event: ClipboardEvent) => {
|
||||
const isPlainPaste = !!IS_PLAIN_PASTE;
|
||||
|
||||
// #686
|
||||
const target = document.activeElement;
|
||||
@ -2180,21 +2221,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// must be called in the same frame (thus before any awaits) as the paste
|
||||
// event else some browsers (FF...) will clear the clipboardData
|
||||
// (something something security)
|
||||
let file = event?.clipboardData?.files[0];
|
||||
|
||||
const data = await parseClipboard(event, isPlainPaste);
|
||||
if (!file && data.text && !isPlainPaste) {
|
||||
const string = data.text.trim();
|
||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||
// ignore SVG validation/normalization which will be done during image
|
||||
// initialization
|
||||
file = SVGStringToFile(string);
|
||||
}
|
||||
}
|
||||
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: this.lastViewportPosition.x,
|
||||
@ -2203,6 +2229,29 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.state,
|
||||
);
|
||||
|
||||
// must be called in the same frame (thus before any awaits) as the paste
|
||||
// event else some browsers (FF...) will clear the clipboardData
|
||||
// (something something security)
|
||||
let file = event?.clipboardData?.files[0];
|
||||
|
||||
const data = await parseClipboard(event, isPlainPaste);
|
||||
if (!file && !isPlainPaste) {
|
||||
if (data.mixedContent) {
|
||||
return this.addElementsFromMixedContentPaste(data.mixedContent, {
|
||||
isPlainPaste,
|
||||
sceneX,
|
||||
sceneY,
|
||||
});
|
||||
} else if (data.text) {
|
||||
const string = data.text.trim();
|
||||
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
|
||||
// ignore SVG validation/normalization which will be done during image
|
||||
// initialization
|
||||
file = SVGStringToFile(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||
@ -2256,6 +2305,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
} else if (data.text) {
|
||||
const maybeUrl = extractSrc(data.text);
|
||||
|
||||
if (
|
||||
!isPlainPaste &&
|
||||
embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) &&
|
||||
@ -2279,11 +2329,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
);
|
||||
|
||||
private addElementsFromPasteOrLibrary = (opts: {
|
||||
addElementsFromPasteOrLibrary = (opts: {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
files: BinaryFiles | null;
|
||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||
retainSeed?: boolean;
|
||||
fitToContent?: boolean;
|
||||
}) => {
|
||||
const elements = restoreElements(opts.elements, null, undefined);
|
||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||
@ -2388,8 +2439,93 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
);
|
||||
this.setActiveTool({ type: "selection" });
|
||||
|
||||
if (opts.fitToContent) {
|
||||
this.scrollToContent(newElements, {
|
||||
fitToContent: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// TODO rewrite this to paste both text & images at the same time if
|
||||
// pasted data contains both
|
||||
private async addElementsFromMixedContentPaste(
|
||||
mixedContent: PastedMixedContent,
|
||||
{
|
||||
isPlainPaste,
|
||||
sceneX,
|
||||
sceneY,
|
||||
}: { isPlainPaste: boolean; sceneX: number; sceneY: number },
|
||||
) {
|
||||
if (
|
||||
!isPlainPaste &&
|
||||
mixedContent.some((node) => node.type === "imageUrl")
|
||||
) {
|
||||
const imageURLs = mixedContent
|
||||
.filter((node) => node.type === "imageUrl")
|
||||
.map((node) => node.value);
|
||||
const responses = await Promise.all(
|
||||
imageURLs.map(async (url) => {
|
||||
try {
|
||||
return { file: await ImageURLToFile(url) };
|
||||
} catch (error: any) {
|
||||
return { errorMessage: error.message as string };
|
||||
}
|
||||
}),
|
||||
);
|
||||
let y = sceneY;
|
||||
let firstImageYOffsetDone = false;
|
||||
const nextSelectedIds: Record<ExcalidrawElement["id"], true> = {};
|
||||
for (const response of responses) {
|
||||
if (response.file) {
|
||||
const imageElement = this.createImageElement({
|
||||
sceneX,
|
||||
sceneY: y,
|
||||
});
|
||||
|
||||
const initializedImageElement = await this.insertImageElement(
|
||||
imageElement,
|
||||
response.file,
|
||||
);
|
||||
if (initializedImageElement) {
|
||||
// vertically center first image in the batch
|
||||
if (!firstImageYOffsetDone) {
|
||||
firstImageYOffsetDone = true;
|
||||
y -= initializedImageElement.height / 2;
|
||||
}
|
||||
// hack to reset the `y` coord because we vertically center during
|
||||
// insertImageElement
|
||||
mutateElement(initializedImageElement, { y }, false);
|
||||
|
||||
y = imageElement.y + imageElement.height + 25;
|
||||
|
||||
nextSelectedIds[imageElement.id] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedElementIds: makeNextSelectedElementIds(
|
||||
nextSelectedIds,
|
||||
this.state,
|
||||
),
|
||||
});
|
||||
|
||||
const error = responses.find((response) => !!response.errorMessage);
|
||||
if (error && error.errorMessage) {
|
||||
this.setState({ errorMessage: error.errorMessage });
|
||||
}
|
||||
} else {
|
||||
const textNodes = mixedContent.filter((node) => node.type === "text");
|
||||
if (textNodes.length) {
|
||||
this.addTextFromPaste(
|
||||
textNodes.map((node) => node.value).join("\n\n"),
|
||||
isPlainPaste,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addTextFromPaste(text: string, isPlainPaste = false) {
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{
|
||||
@ -3129,11 +3265,16 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
|
||||
setActiveTool = (
|
||||
tool:
|
||||
| {
|
||||
type: ToolType;
|
||||
}
|
||||
| { type: "custom"; customType: string },
|
||||
tool: (
|
||||
| (
|
||||
| { type: Exclude<ToolType, "image"> }
|
||||
| {
|
||||
type: Extract<ToolType, "image">;
|
||||
insertOnCanvasDirectly?: boolean;
|
||||
}
|
||||
)
|
||||
| { type: "custom"; customType: string }
|
||||
) & { locked?: boolean },
|
||||
) => {
|
||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||
if (nextActiveTool.type === "hand") {
|
||||
@ -3148,7 +3289,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ suggestedBindings: [] });
|
||||
}
|
||||
if (nextActiveTool.type === "image") {
|
||||
this.onImageAction();
|
||||
this.onImageAction({
|
||||
insertOnCanvasDirectly:
|
||||
(tool.type === "image" && tool.insertOnCanvasDirectly) ?? false,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState((prevState) => {
|
||||
@ -3176,6 +3320,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
});
|
||||
};
|
||||
|
||||
setOpenDialog = (dialogType: AppState["openDialog"]) => {
|
||||
this.setState({ openDialog: dialogType });
|
||||
};
|
||||
|
||||
private setCursor = (cursor: string) => {
|
||||
setCursor(this.interactiveCanvas, cursor);
|
||||
};
|
||||
@ -4126,6 +4274,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
|
||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||
scenePointer,
|
||||
hitElement,
|
||||
@ -4392,6 +4541,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
}
|
||||
|
||||
private handleCanvasPointerDown = (
|
||||
event: React.PointerEvent<HTMLElement>,
|
||||
) => {
|
||||
@ -4479,12 +4629,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPointerDownEvent = event;
|
||||
|
||||
// we must exit before we set `cursorButton` state and `savePointer`
|
||||
// else it will send pointer state & laser pointer events in collab when
|
||||
// panning
|
||||
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPointerDownEvent = event;
|
||||
|
||||
this.setState({
|
||||
lastPointerDownWith: event.pointerType,
|
||||
cursorButton: "down",
|
||||
@ -4578,7 +4731,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
pointerDownState,
|
||||
);
|
||||
} else if (this.state.activeTool.type === "custom") {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||
setCursorForShape(this.interactiveCanvas, this.state);
|
||||
} else if (this.state.activeTool.type === "frame") {
|
||||
this.createFrameElementOnPointerDown(pointerDownState);
|
||||
} else if (this.state.activeTool.type === "laser") {
|
||||
@ -4597,6 +4750,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
this.props?.onPointerDown?.(this.state.activeTool, pointerDownState);
|
||||
this.onPointerDownEmitter.trigger(
|
||||
this.state.activeTool,
|
||||
pointerDownState,
|
||||
event,
|
||||
);
|
||||
|
||||
const onPointerMove =
|
||||
this.onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||
@ -6449,6 +6607,12 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({ pendingImageElementId: null });
|
||||
}
|
||||
|
||||
this.onPointerUpEmitter.trigger(
|
||||
this.state.activeTool,
|
||||
pointerDownState,
|
||||
childEvent,
|
||||
);
|
||||
|
||||
if (draggingElement?.type === "freedraw") {
|
||||
const pointerCoords = viewportCoordsToSceneCoords(
|
||||
childEvent,
|
||||
@ -7290,7 +7454,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.scene.addNewElement(imageElement);
|
||||
|
||||
try {
|
||||
await this.initializeImage({
|
||||
return await this.initializeImage({
|
||||
imageFile,
|
||||
imageElement,
|
||||
showCursorImagePreview,
|
||||
@ -7303,6 +7467,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
this.setState({
|
||||
errorMessage: error.message || t("errors.imageInsertError"),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@ -7345,9 +7510,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
};
|
||||
|
||||
private onImageAction = async (
|
||||
{ insertOnCanvasDirectly } = { insertOnCanvasDirectly: false },
|
||||
) => {
|
||||
private onImageAction = async ({
|
||||
insertOnCanvasDirectly,
|
||||
}: {
|
||||
insertOnCanvasDirectly: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const clientX = this.state.width / 2 + this.state.offsetLeft;
|
||||
const clientY = this.state.height / 2 + this.state.offsetTop;
|
||||
|
@ -55,6 +55,7 @@ export const TopPicks = ({
|
||||
type="button"
|
||||
title={color}
|
||||
onClick={() => onChange(color)}
|
||||
data-testid={`color-top-pick-${color}`}
|
||||
>
|
||||
<div className="color-picker__button-outline" />
|
||||
</button>
|
||||
|
@ -9,11 +9,7 @@ import {
|
||||
} from "../actions/shortcuts";
|
||||
import { Action } from "../actions/types";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import {
|
||||
useExcalidrawAppState,
|
||||
useExcalidrawElements,
|
||||
useExcalidrawSetAppState,
|
||||
} from "./App";
|
||||
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
|
||||
import React from "react";
|
||||
|
||||
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
||||
@ -25,14 +21,14 @@ type ContextMenuProps = {
|
||||
items: ContextMenuItems;
|
||||
top: number;
|
||||
left: number;
|
||||
onClose: (callback?: () => void) => void;
|
||||
};
|
||||
|
||||
export const CONTEXT_MENU_SEPARATOR = "separator";
|
||||
|
||||
export const ContextMenu = React.memo(
|
||||
({ actionManager, items, top, left }: ContextMenuProps) => {
|
||||
({ actionManager, items, top, left, onClose }: ContextMenuProps) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const setAppState = useExcalidrawSetAppState();
|
||||
const elements = useExcalidrawElements();
|
||||
|
||||
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
||||
@ -54,7 +50,9 @@ export const ContextMenu = React.memo(
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onCloseRequest={() => setAppState({ contextMenu: null })}
|
||||
onCloseRequest={() => {
|
||||
onClose();
|
||||
}}
|
||||
top={top}
|
||||
left={left}
|
||||
fitInViewport={true}
|
||||
@ -102,7 +100,7 @@ export const ContextMenu = React.memo(
|
||||
// we need update state before executing the action in case
|
||||
// the action uses the appState it's being passed (that still
|
||||
// contains a defined contextMenu) to return the next state.
|
||||
setAppState({ contextMenu: null }, () => {
|
||||
onClose(() => {
|
||||
actionManager.executeAction(item, "contextMenu");
|
||||
});
|
||||
}}
|
||||
|
@ -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 {
|
||||
|
@ -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}
|
||||
|
@ -62,7 +62,6 @@ interface LayerUIProps {
|
||||
appState: UIAppState;
|
||||
files: BinaryFiles;
|
||||
canvas: HTMLCanvasElement;
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onLockToggle: () => void;
|
||||
@ -73,7 +72,6 @@ interface LayerUIProps {
|
||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||
UIOptions: AppProps["UIOptions"];
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
onExportImage: AppClassProperties["onExportImage"];
|
||||
renderWelcomeScreen: boolean;
|
||||
children?: React.ReactNode;
|
||||
@ -123,7 +121,6 @@ const LayerUI = ({
|
||||
setAppState,
|
||||
elements,
|
||||
canvas,
|
||||
interactiveCanvas,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
@ -131,7 +128,6 @@ const LayerUI = ({
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
UIOptions,
|
||||
onImageAction,
|
||||
onExportImage,
|
||||
renderWelcomeScreen,
|
||||
children,
|
||||
@ -280,14 +276,8 @@ const LayerUI = ({
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
app={app}
|
||||
onImageAction={({ pointerType }) => {
|
||||
onImageAction({
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
@ -472,8 +462,6 @@ const LayerUI = ({
|
||||
onLockToggle={onLockToggle}
|
||||
onHandToolToggle={onHandToolToggle}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
renderSidebars={renderSidebars}
|
||||
@ -560,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
221
src/components/MermaidToExcalidraw.scss
Normal file
221
src/components/MermaidToExcalidraw.scss
Normal file
@ -0,0 +1,221 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
$verticalBreakpoint: 860px;
|
||||
|
||||
.excalidraw {
|
||||
.dialog-mermaid {
|
||||
&-title {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
&-desc {
|
||||
font-size: 15px;
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.Modal__content .Island {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@at-root .excalidraw:not(.excalidraw--mobile)#{&} {
|
||||
padding: 1.25rem;
|
||||
|
||||
.Modal__content {
|
||||
height: 100%;
|
||||
max-height: 750px;
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
height: auto;
|
||||
// When vertical, we want the height to span whole viewport.
|
||||
// This is also important for the children not to overflow the
|
||||
// modal/viewport (for some reason).
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.Island {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
|
||||
.Dialog__content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-mermaid-body {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr auto;
|
||||
height: 100%;
|
||||
column-gap: 4rem;
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-mermaid-panels {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
justify-content: space-between;
|
||||
gap: 4rem;
|
||||
|
||||
grid-row: 1;
|
||||
grid-column: 1 / 3;
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
textarea {
|
||||
width: 20rem;
|
||||
height: 100%;
|
||||
resize: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--dialog-border-color);
|
||||
white-space: pre-wrap;
|
||||
padding: 0.85rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-family: monospace;
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
width: auto;
|
||||
height: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-preview-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.85rem;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
// acts as min-height
|
||||
height: 200px;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
|
||||
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==")
|
||||
left center;
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--dialog-border-color);
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
// acts as min-height
|
||||
height: 400px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&-preview-canvas-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mermaid-error {
|
||||
color: red;
|
||||
font-weight: 800;
|
||||
font-size: 30px;
|
||||
word-break: break-word;
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
p {
|
||||
font-weight: 500;
|
||||
font-family: Cascadia;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
font-size: 0.875rem;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-mermaid-buttons {
|
||||
grid-column: 2;
|
||||
|
||||
.dialog-mermaid-insert {
|
||||
&.excalidraw-button {
|
||||
font-family: "Assistant";
|
||||
font-weight: 600;
|
||||
height: 2.5rem;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 0.3em;
|
||||
width: 7.5rem;
|
||||
font-size: 12px;
|
||||
color: $oc-white;
|
||||
background-color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-darker);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-darkest);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $verticalBreakpoint) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@at-root .excalidraw.theme--dark#{&} {
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
243
src/components/MermaidToExcalidraw.tsx
Normal file
243
src/components/MermaidToExcalidraw.tsx
Normal file
@ -0,0 +1,243 @@
|
||||
import { useState, useRef, useEffect, useDeferredValue } from "react";
|
||||
import { BinaryFiles } from "../types";
|
||||
import { useApp } from "./App";
|
||||
import { Button } from "./Button";
|
||||
import { Dialog } from "./Dialog";
|
||||
import { DEFAULT_EXPORT_PADDING, DEFAULT_FONT_SIZE } from "../constants";
|
||||
import {
|
||||
convertToExcalidrawElements,
|
||||
exportToCanvas,
|
||||
} from "../packages/excalidraw/index";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { canvasToBlob } from "../data/blob";
|
||||
import { ArrowRightIcon } from "./icons";
|
||||
import Spinner from "./Spinner";
|
||||
import "./MermaidToExcalidraw.scss";
|
||||
|
||||
import { MermaidToExcalidrawResult } from "@excalidraw/mermaid-to-excalidraw/dist/interfaces";
|
||||
import type { MermaidOptions } from "@excalidraw/mermaid-to-excalidraw";
|
||||
import { t } from "../i18n";
|
||||
import Trans from "./Trans";
|
||||
|
||||
const LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW = "mermaid-to-excalidraw";
|
||||
const MERMAID_EXAMPLE =
|
||||
"flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
|
||||
|
||||
const saveMermaidDataToStorage = (data: string) => {
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW, data);
|
||||
} catch (error: any) {
|
||||
// Unable to access window.localStorage
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const importMermaidDataFromStorage = () => {
|
||||
try {
|
||||
const data = localStorage.getItem(LOCAL_STORAGE_KEY_MERMAID_TO_EXCALIDRAW);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Unable to access localStorage
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ErrorComp = ({ error }: { error: string }) => {
|
||||
return (
|
||||
<div data-testid="mermaid-error" className="mermaid-error">
|
||||
Error! <p>{error}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MermaidToExcalidraw = () => {
|
||||
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState<{
|
||||
loaded: boolean;
|
||||
api: {
|
||||
parseMermaidToExcalidraw: (
|
||||
defination: string,
|
||||
options: MermaidOptions,
|
||||
) => Promise<MermaidToExcalidrawResult>;
|
||||
} | null;
|
||||
}>({ loaded: false, api: null });
|
||||
|
||||
const [text, setText] = useState("");
|
||||
const deferredText = useDeferredValue(text.trim());
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const data = useRef<{
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
files: BinaryFiles | null;
|
||||
}>({ elements: [], files: null });
|
||||
|
||||
const app = useApp();
|
||||
|
||||
const resetPreview = () => {
|
||||
const canvasNode = canvasRef.current;
|
||||
|
||||
if (!canvasNode) {
|
||||
return;
|
||||
}
|
||||
const parent = canvasNode.parentElement;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
parent.style.background = "";
|
||||
setError(null);
|
||||
canvasNode.replaceChildren();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const loadMermaidToExcalidrawLib = async () => {
|
||||
const api = await import(
|
||||
/* webpackChunkName:"mermaid-to-excalidraw" */ "@excalidraw/mermaid-to-excalidraw"
|
||||
);
|
||||
setMermaidToExcalidrawLib({ loaded: true, api });
|
||||
};
|
||||
loadMermaidToExcalidrawLib();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const data = importMermaidDataFromStorage() || MERMAID_EXAMPLE;
|
||||
setText(data);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const renderExcalidrawPreview = async () => {
|
||||
const canvasNode = canvasRef.current;
|
||||
const parent = canvasNode?.parentElement;
|
||||
if (
|
||||
!mermaidToExcalidrawLib.loaded ||
|
||||
!canvasNode ||
|
||||
!parent ||
|
||||
!mermaidToExcalidrawLib.api
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (!deferredText) {
|
||||
resetPreview();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { elements, files } =
|
||||
await mermaidToExcalidrawLib.api.parseMermaidToExcalidraw(
|
||||
deferredText,
|
||||
{
|
||||
fontSize: DEFAULT_FONT_SIZE,
|
||||
},
|
||||
);
|
||||
setError(null);
|
||||
|
||||
data.current = {
|
||||
elements: convertToExcalidrawElements(elements, {
|
||||
regenerateIds: true,
|
||||
}),
|
||||
files,
|
||||
};
|
||||
|
||||
const canvas = await exportToCanvas({
|
||||
elements: data.current.elements,
|
||||
files: data.current.files,
|
||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||
maxWidthOrHeight:
|
||||
Math.max(parent.offsetWidth, parent.offsetHeight) *
|
||||
window.devicePixelRatio,
|
||||
});
|
||||
// if converting to blob fails, there's some problem that will
|
||||
// likely prevent preview and export (e.g. canvas too big)
|
||||
await canvasToBlob(canvas);
|
||||
parent.style.background = "var(--default-bg-color)";
|
||||
canvasNode.replaceChildren(canvas);
|
||||
} catch (e: any) {
|
||||
parent.style.background = "var(--default-bg-color)";
|
||||
if (deferredText) {
|
||||
setError(e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
renderExcalidrawPreview();
|
||||
}, [deferredText, mermaidToExcalidrawLib]);
|
||||
|
||||
const onClose = () => {
|
||||
app.setOpenDialog(null);
|
||||
saveMermaidDataToStorage(text);
|
||||
};
|
||||
|
||||
const onSelect = () => {
|
||||
const { elements: newElements, files } = data.current;
|
||||
app.addElementsFromPasteOrLibrary({
|
||||
elements: newElements,
|
||||
files,
|
||||
position: "center",
|
||||
fitToContent: true,
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="dialog-mermaid"
|
||||
onCloseRequest={onClose}
|
||||
size={1200}
|
||||
title={
|
||||
<>
|
||||
<p className="dialog-mermaid-title">{t("mermaid.title")}</p>
|
||||
<span className="dialog-mermaid-desc">
|
||||
<Trans
|
||||
i18nKey="mermaid.description"
|
||||
flowchartLink={(el) => (
|
||||
<a href="https://mermaid.js.org/syntax/flowchart.html">{el}</a>
|
||||
)}
|
||||
sequenceLink={(el) => (
|
||||
<a href="https://mermaid.js.org/syntax/sequenceDiagram.html">
|
||||
{el}
|
||||
</a>
|
||||
)}
|
||||
/>
|
||||
<br />
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className="dialog-mermaid-body">
|
||||
<div className="dialog-mermaid-panels">
|
||||
<div className="dialog-mermaid-panels-text">
|
||||
<label>{t("mermaid.syntax")}</label>
|
||||
|
||||
<textarea
|
||||
onChange={(event) => setText(event.target.value)}
|
||||
value={text}
|
||||
/>
|
||||
</div>
|
||||
<div className="dialog-mermaid-panels-preview">
|
||||
<label>{t("mermaid.preview")}</label>
|
||||
<div className="dialog-mermaid-panels-preview-wrapper">
|
||||
{error && <ErrorComp error={error} />}
|
||||
{mermaidToExcalidrawLib.loaded ? (
|
||||
<div
|
||||
ref={canvasRef}
|
||||
style={{ opacity: error ? "0.15" : 1 }}
|
||||
className="dialog-mermaid-panels-preview-canvas-container"
|
||||
/>
|
||||
) : (
|
||||
<Spinner size="2rem" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="dialog-mermaid-buttons">
|
||||
<Button className="dialog-mermaid-insert" onSelect={onSelect}>
|
||||
{t("mermaid.button")}
|
||||
<span>{ArrowRightIcon}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export default MermaidToExcalidraw;
|
@ -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}
|
||||
app={app}
|
||||
onImageAction={({ pointerType }) => {
|
||||
onImageAction({
|
||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
@ -165,6 +160,15 @@
|
||||
width: var(--lg-button-size);
|
||||
height: var(--lg-button-size);
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
width: 1.8rem;
|
||||
height: 1.8rem;
|
||||
}
|
||||
@media screen and (max-width: 379px) {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: var(--lg-icon-size);
|
||||
height: var(--lg-icon-size);
|
||||
|
@ -16,12 +16,17 @@
|
||||
align-self: center;
|
||||
background-color: var(--default-border-color);
|
||||
margin: 0 0.25rem;
|
||||
|
||||
@include isMobile {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.App-toolbar__extra-tools-trigger {
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-hover-bg);
|
||||
@ -40,5 +45,6 @@
|
||||
margin-top: 0.375rem;
|
||||
right: 0;
|
||||
min-width: 11.875rem;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -7,8 +7,6 @@
|
||||
margin-top: 0.25rem;
|
||||
|
||||
&--mobile {
|
||||
bottom: 55px;
|
||||
top: auto;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
row-gap: 0.75rem;
|
||||
@ -16,7 +14,7 @@
|
||||
.dropdown-menu-container {
|
||||
padding: 8px 8px;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--island-bg-color);
|
||||
// background-color: var(--island-bg-color);
|
||||
box-shadow: var(--shadow-island);
|
||||
border-radius: var(--border-radius-lg);
|
||||
position: relative;
|
||||
@ -29,7 +27,7 @@
|
||||
}
|
||||
|
||||
.dropdown-menu-container {
|
||||
background-color: #fff !important;
|
||||
background-color: var(--island-bg-color);
|
||||
max-height: calc(100vh - 150px);
|
||||
overflow-y: auto;
|
||||
--gap: 2;
|
||||
@ -40,7 +38,7 @@
|
||||
padding: 0 0.625rem;
|
||||
column-gap: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gray-100);
|
||||
color: var(--color-on-surface);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
font-weight: normal;
|
||||
@ -49,7 +47,7 @@
|
||||
|
||||
.dropdown-menu-item {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border: 1px solid transparent;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
cursor: pointer;
|
||||
@ -80,6 +78,11 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--button-hover-bg);
|
||||
border-color: var(--color-brand-active);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@ -98,22 +101,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);
|
||||
|
@ -1654,6 +1654,22 @@ export const frameToolIcon = createIcon(
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const mermaidLogoIcon = createIcon(
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M407.48,111.18C335.587,108.103 269.573,152.338 245.08,220C220.587,152.338 154.573,108.103 82.68,111.18C80.285,168.229 107.577,222.632 154.74,254.82C178.908,271.419 193.35,298.951 193.27,328.27L193.27,379.13L296.9,379.13L296.9,328.27C296.816,298.953 311.255,271.42 335.42,254.82C382.596,222.644 409.892,168.233 407.48,111.18Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
export const ArrowRightIcon = createIcon(
|
||||
<g strokeWidth="1.25">
|
||||
<path d="M4.16602 10H15.8327" />
|
||||
<path d="M12.5 13.3333L15.8333 10" />
|
||||
<path d="M12.5 6.66666L15.8333 9.99999" />
|
||||
</g>,
|
||||
modifiedTablerIconProps,
|
||||
);
|
||||
|
||||
export const laserPointerToolIcon = createIcon(
|
||||
<g
|
||||
fill="none"
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -148,6 +148,8 @@ export const IMAGE_MIME_TYPES = {
|
||||
jfif: "image/jfif",
|
||||
} as const;
|
||||
|
||||
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
|
||||
|
||||
export const MIME_TYPES = {
|
||||
json: "application/json",
|
||||
// excalidraw data
|
||||
@ -296,6 +298,18 @@ export const ROUNDNESS = {
|
||||
* collaboration */
|
||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||
|
||||
export const ROUGHNESS = {
|
||||
architect: 0,
|
||||
artist: 1,
|
||||
cartoonist: 2,
|
||||
} as const;
|
||||
|
||||
export const STROKE_WIDTH = {
|
||||
thin: 1,
|
||||
bold: 2,
|
||||
extraBold: 4,
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_ELEMENT_PROPS: {
|
||||
strokeColor: ExcalidrawElement["strokeColor"];
|
||||
backgroundColor: ExcalidrawElement["backgroundColor"];
|
||||
@ -308,10 +322,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,
|
||||
};
|
||||
|
@ -195,7 +195,7 @@
|
||||
.buttonList label:focus-within,
|
||||
input:focus-visible {
|
||||
outline: transparent;
|
||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||
box-shadow: 0 0 0 1px var(--color-brand-hover);
|
||||
}
|
||||
|
||||
.buttonList {
|
||||
@ -280,6 +280,11 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
|
||||
.dropdown-menu--mobile {
|
||||
bottom: 55px;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.App-mobile-menu {
|
||||
@ -444,13 +449,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);
|
||||
|
||||
@ -536,13 +542,13 @@
|
||||
|
||||
&:not(:focus) {
|
||||
&:hover {
|
||||
background-color: var(--input-hover-bg-color);
|
||||
border-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
||||
border-color: var(--color-brand-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@ -591,6 +597,8 @@
|
||||
background-color: var(--island-bg-color);
|
||||
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@ -600,8 +608,8 @@
|
||||
}
|
||||
|
||||
.App-toolbar--mobile {
|
||||
overflow-x: auto;
|
||||
max-width: 90vw;
|
||||
overflow: visible;
|
||||
max-width: 98vw;
|
||||
|
||||
.ToolIcon__keybinding {
|
||||
display: none;
|
||||
@ -621,6 +629,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 {
|
||||
|
@ -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
src/cursor.ts
Normal file
105
src/cursor.ts
Normal file
@ -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;
|
||||
}
|
||||
};
|
File diff suppressed because it is too large
Load Diff
@ -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>,
|
||||
) => {
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -189,7 +189,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.
|
||||
@ -222,9 +222,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, {
|
||||
@ -299,6 +307,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;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -5,7 +5,31 @@ import {
|
||||
} from "./transform";
|
||||
import { ExcalidrawArrowElement } from "../element/types";
|
||||
|
||||
const opts = { regenerateIds: false };
|
||||
|
||||
describe("Test Transform", () => {
|
||||
it("should generate id unless opts.regenerateIds is set to false explicitly", () => {
|
||||
const elements = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 100,
|
||||
y: 100,
|
||||
id: "rect-1",
|
||||
},
|
||||
];
|
||||
let data = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
);
|
||||
expect(data.length).toBe(1);
|
||||
expect(data[0].id).toBe("id0");
|
||||
|
||||
data = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
expect(data[0].id).toBe("rect-1");
|
||||
});
|
||||
|
||||
it("should transform regular shapes", () => {
|
||||
const elements = [
|
||||
{
|
||||
@ -59,6 +83,7 @@ describe("Test Transform", () => {
|
||||
|
||||
convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
).forEach((ele) => {
|
||||
expect(ele).toMatchSnapshot({
|
||||
seed: expect.any(Number),
|
||||
@ -87,6 +112,7 @@ describe("Test Transform", () => {
|
||||
];
|
||||
convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
).forEach((ele) => {
|
||||
expect(ele).toMatchSnapshot({
|
||||
seed: expect.any(Number),
|
||||
@ -128,6 +154,7 @@ describe("Test Transform", () => {
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(4);
|
||||
@ -210,6 +237,7 @@ describe("Test Transform", () => {
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(12);
|
||||
@ -267,6 +295,7 @@ describe("Test Transform", () => {
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(8);
|
||||
@ -280,6 +309,90 @@ describe("Test Transform", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Frames", () => {
|
||||
it("should transform frames and update frame ids when regenerated", () => {
|
||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 10,
|
||||
y: 10,
|
||||
strokeWidth: 2,
|
||||
id: "1",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
x: 120,
|
||||
y: 20,
|
||||
backgroundColor: "#fff3bf",
|
||||
strokeWidth: 2,
|
||||
label: {
|
||||
text: "HELLO EXCALIDRAW",
|
||||
strokeColor: "#099268",
|
||||
fontSize: 30,
|
||||
},
|
||||
id: "2",
|
||||
},
|
||||
{
|
||||
type: "frame",
|
||||
children: ["1", "2"],
|
||||
name: "My frame",
|
||||
},
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elementsSkeleton,
|
||||
opts,
|
||||
);
|
||||
expect(excaldrawElements.length).toBe(4);
|
||||
|
||||
excaldrawElements.forEach((ele) => {
|
||||
expect(ele).toMatchObject({
|
||||
seed: expect.any(Number),
|
||||
versionNonce: expect.any(Number),
|
||||
id: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should consider max of calculated and frame dimensions when provided", () => {
|
||||
const elementsSkeleton: ExcalidrawElementSkeleton[] = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 10,
|
||||
y: 10,
|
||||
strokeWidth: 2,
|
||||
id: "1",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
x: 120,
|
||||
y: 20,
|
||||
backgroundColor: "#fff3bf",
|
||||
strokeWidth: 2,
|
||||
label: {
|
||||
text: "HELLO EXCALIDRAW",
|
||||
strokeColor: "#099268",
|
||||
fontSize: 30,
|
||||
},
|
||||
id: "2",
|
||||
},
|
||||
{
|
||||
type: "frame",
|
||||
children: ["1", "2"],
|
||||
name: "My frame",
|
||||
width: 800,
|
||||
height: 100,
|
||||
},
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elementsSkeleton,
|
||||
opts,
|
||||
);
|
||||
const frame = excaldrawElements.find((ele) => ele.type === "frame")!;
|
||||
expect(frame.width).toBe(800);
|
||||
expect(frame.height).toBe(126);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test arrow bindings", () => {
|
||||
it("should bind arrows to shapes when start / end provided without ids", () => {
|
||||
const elements = [
|
||||
@ -300,6 +413,7 @@ describe("Test Transform", () => {
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(4);
|
||||
@ -321,7 +435,7 @@ describe("Test Transform", () => {
|
||||
});
|
||||
|
||||
expect(text).toMatchObject({
|
||||
x: 340,
|
||||
x: 240,
|
||||
y: 226.5,
|
||||
type: "text",
|
||||
text: "HELLO WORLD!!",
|
||||
@ -341,7 +455,7 @@ describe("Test Transform", () => {
|
||||
});
|
||||
|
||||
expect(ellipse).toMatchObject({
|
||||
x: 555,
|
||||
x: 355,
|
||||
y: 189,
|
||||
type: "ellipse",
|
||||
boundElements: [
|
||||
@ -383,10 +497,10 @@ describe("Test Transform", () => {
|
||||
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(4);
|
||||
|
||||
const [arrow, text1, text2, text3] = excaldrawElements;
|
||||
|
||||
expect(arrow).toMatchObject({
|
||||
@ -406,7 +520,7 @@ describe("Test Transform", () => {
|
||||
});
|
||||
|
||||
expect(text1).toMatchObject({
|
||||
x: 340,
|
||||
x: 240,
|
||||
y: 226.5,
|
||||
type: "text",
|
||||
text: "HELLO WORLD!!",
|
||||
@ -426,7 +540,7 @@ describe("Test Transform", () => {
|
||||
});
|
||||
|
||||
expect(text3).toMatchObject({
|
||||
x: 555,
|
||||
x: 355,
|
||||
y: 226.5,
|
||||
type: "text",
|
||||
boundElements: [
|
||||
@ -499,6 +613,7 @@ describe("Test Transform", () => {
|
||||
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(5);
|
||||
@ -547,6 +662,7 @@ describe("Test Transform", () => {
|
||||
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(4);
|
||||
@ -600,17 +716,18 @@ describe("Test Transform", () => {
|
||||
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(4);
|
||||
const [, , arrow] = excaldrawElements;
|
||||
const [, , arrow, text] = excaldrawElements;
|
||||
expect(arrow).toMatchObject({
|
||||
type: "arrow",
|
||||
x: 255,
|
||||
y: 239,
|
||||
boundElements: [
|
||||
{
|
||||
id: "id46",
|
||||
id: text.id,
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
@ -650,17 +767,18 @@ describe("Test Transform", () => {
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
expect(excaldrawElements.length).toBe(2);
|
||||
const [arrow, rect] = excaldrawElements;
|
||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||
elementId: "rect-1",
|
||||
focus: 0,
|
||||
gap: 5,
|
||||
gap: 205,
|
||||
});
|
||||
expect(rect.boundElements).toStrictEqual([
|
||||
{
|
||||
id: "id47",
|
||||
id: arrow.id,
|
||||
type: "arrow",
|
||||
},
|
||||
]);
|
||||
@ -692,6 +810,7 @@ describe("Test Transform", () => {
|
||||
];
|
||||
const excaldrawElements = convertToExcalidrawElements(
|
||||
elements as ExcalidrawElementSkeleton[],
|
||||
opts,
|
||||
);
|
||||
|
||||
expect(excaldrawElements.length).toBe(1);
|
||||
|
@ -5,6 +5,7 @@ import {
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import {
|
||||
getCommonBounds,
|
||||
newElement,
|
||||
newLinearElement,
|
||||
redrawTextBoundingBox,
|
||||
@ -12,6 +13,7 @@ import {
|
||||
import { bindLinearElement } from "../element/binding";
|
||||
import {
|
||||
ElementConstructorOpts,
|
||||
newFrameElement,
|
||||
newImageElement,
|
||||
newTextElement,
|
||||
} from "../element/newElement";
|
||||
@ -39,6 +41,8 @@ import {
|
||||
} from "../element/types";
|
||||
import { MarkOptional } from "../utility-types";
|
||||
import { assertNever, getFontString } from "../utils";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { randomId } from "../random";
|
||||
|
||||
export type ValidLinearElement = {
|
||||
type: "arrow" | "line";
|
||||
@ -133,9 +137,7 @@ export type ValidContainer =
|
||||
export type ExcalidrawElementSkeleton =
|
||||
| Extract<
|
||||
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||
| ExcalidrawEmbeddableElement
|
||||
| ExcalidrawFreeDrawElement
|
||||
| ExcalidrawFrameElement
|
||||
ExcalidrawEmbeddableElement | ExcalidrawFreeDrawElement
|
||||
>
|
||||
| ({
|
||||
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
||||
@ -156,10 +158,15 @@ export type ExcalidrawElementSkeleton =
|
||||
x: number;
|
||||
y: number;
|
||||
fileId: FileId;
|
||||
} & Partial<ExcalidrawImageElement>);
|
||||
} & Partial<ExcalidrawImageElement>)
|
||||
| ({
|
||||
type: "frame";
|
||||
children: readonly ExcalidrawElement["id"][];
|
||||
name?: string;
|
||||
} & Partial<ExcalidrawFrameElement>);
|
||||
|
||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||
width: 300,
|
||||
width: 100,
|
||||
height: 0,
|
||||
};
|
||||
|
||||
@ -357,6 +364,48 @@ const bindLinearElementToElement = (
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update start/end points by 0.5 so bindings don't overlap with start/end bound element coordinates.
|
||||
const endPointIndex = linearElement.points.length - 1;
|
||||
const delta = 0.5;
|
||||
const newPoints = JSON.parse(JSON.stringify(linearElement.points));
|
||||
// left to right so shift the arrow towards right
|
||||
if (
|
||||
linearElement.points[endPointIndex][0] >
|
||||
linearElement.points[endPointIndex - 1][0]
|
||||
) {
|
||||
newPoints[0][0] = delta;
|
||||
newPoints[endPointIndex][0] -= delta;
|
||||
}
|
||||
|
||||
// right to left so shift the arrow towards left
|
||||
if (
|
||||
linearElement.points[endPointIndex][0] <
|
||||
linearElement.points[endPointIndex - 1][0]
|
||||
) {
|
||||
newPoints[0][0] = -delta;
|
||||
newPoints[endPointIndex][0] += delta;
|
||||
}
|
||||
// top to bottom so shift the arrow towards top
|
||||
if (
|
||||
linearElement.points[endPointIndex][1] >
|
||||
linearElement.points[endPointIndex - 1][1]
|
||||
) {
|
||||
newPoints[0][1] = delta;
|
||||
newPoints[endPointIndex][1] -= delta;
|
||||
}
|
||||
|
||||
// bottom to top so shift the arrow towards bottom
|
||||
if (
|
||||
linearElement.points[endPointIndex][1] <
|
||||
linearElement.points[endPointIndex - 1][1]
|
||||
) {
|
||||
newPoints[0][1] = -delta;
|
||||
newPoints[endPointIndex][1] += delta;
|
||||
}
|
||||
|
||||
Object.assign(linearElement, { points: newPoints });
|
||||
|
||||
return {
|
||||
linearElement,
|
||||
startBoundElement,
|
||||
@ -384,18 +433,27 @@ class ElementStore {
|
||||
}
|
||||
|
||||
export const convertToExcalidrawElements = (
|
||||
elements: ExcalidrawElementSkeleton[] | null,
|
||||
elementsSkeleton: ExcalidrawElementSkeleton[] | null,
|
||||
opts?: { regenerateIds: boolean },
|
||||
) => {
|
||||
if (!elements) {
|
||||
if (!elementsSkeleton) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const elements: ExcalidrawElementSkeleton[] = JSON.parse(
|
||||
JSON.stringify(elementsSkeleton),
|
||||
);
|
||||
const elementStore = new ElementStore();
|
||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||
const oldToNewElementIdMap = new Map<string, string>();
|
||||
|
||||
// Create individual elements
|
||||
for (const element of elements) {
|
||||
let excalidrawElement: ExcalidrawElement;
|
||||
const originalId = element.id;
|
||||
if (opts?.regenerateIds !== false) {
|
||||
Object.assign(element, { id: randomId() });
|
||||
}
|
||||
|
||||
switch (element.type) {
|
||||
case "rectangle":
|
||||
case "ellipse":
|
||||
@ -444,6 +502,11 @@ export const convertToExcalidrawElements = (
|
||||
],
|
||||
...element,
|
||||
});
|
||||
|
||||
Object.assign(
|
||||
excalidrawElement,
|
||||
getSizeFromPoints(excalidrawElement.points),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "text": {
|
||||
@ -477,8 +540,15 @@ export const convertToExcalidrawElements = (
|
||||
|
||||
break;
|
||||
}
|
||||
case "frame": {
|
||||
excalidrawElement = newFrameElement({
|
||||
x: 0,
|
||||
y: 0,
|
||||
...element,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "freedraw":
|
||||
case "frame":
|
||||
case "embeddable": {
|
||||
excalidrawElement = element;
|
||||
break;
|
||||
@ -499,6 +569,9 @@ export const convertToExcalidrawElements = (
|
||||
} else {
|
||||
elementStore.add(excalidrawElement);
|
||||
elementsWithIds.set(excalidrawElement.id, element);
|
||||
if (originalId) {
|
||||
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,6 +597,18 @@ export const convertToExcalidrawElements = (
|
||||
element.type === "arrow" ? element?.start : undefined;
|
||||
const originalEnd =
|
||||
element.type === "arrow" ? element?.end : undefined;
|
||||
if (originalStart && originalStart.id) {
|
||||
const newStartId = oldToNewElementIdMap.get(originalStart.id);
|
||||
if (newStartId) {
|
||||
Object.assign(originalStart, { id: newStartId });
|
||||
}
|
||||
}
|
||||
if (originalEnd && originalEnd.id) {
|
||||
const newEndId = oldToNewElementIdMap.get(originalEnd.id);
|
||||
if (newEndId) {
|
||||
Object.assign(originalEnd, { id: newEndId });
|
||||
}
|
||||
}
|
||||
const { linearElement, startBoundElement, endBoundElement } =
|
||||
bindLinearElementToElement(
|
||||
container as ExcalidrawArrowElement,
|
||||
@ -539,13 +624,23 @@ export const convertToExcalidrawElements = (
|
||||
} else {
|
||||
switch (element.type) {
|
||||
case "arrow": {
|
||||
const { start, end } = element;
|
||||
if (start && start.id) {
|
||||
const newStartId = oldToNewElementIdMap.get(start.id);
|
||||
Object.assign(start, { id: newStartId });
|
||||
}
|
||||
if (end && end.id) {
|
||||
const newEndId = oldToNewElementIdMap.get(end.id);
|
||||
Object.assign(end, { id: newEndId });
|
||||
}
|
||||
const { linearElement, startBoundElement, endBoundElement } =
|
||||
bindLinearElementToElement(
|
||||
excalidrawElement as ExcalidrawArrowElement,
|
||||
element.start,
|
||||
element.end,
|
||||
start,
|
||||
end,
|
||||
elementStore,
|
||||
);
|
||||
|
||||
elementStore.add(linearElement);
|
||||
elementStore.add(startBoundElement);
|
||||
elementStore.add(endBoundElement);
|
||||
@ -557,5 +652,60 @@ export const convertToExcalidrawElements = (
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once all the excalidraw elements are created, we can add frames since we
|
||||
// need to calculate coordinates and dimensions of frame which is possibe after all
|
||||
// frame children are processed.
|
||||
for (const [id, element] of elementsWithIds) {
|
||||
if (element.type !== "frame") {
|
||||
continue;
|
||||
}
|
||||
const frame = elementStore.getElement(id);
|
||||
|
||||
if (!frame) {
|
||||
throw new Error(`Excalidraw element with id ${id} doesn't exist`);
|
||||
}
|
||||
const childrenElements: ExcalidrawElement[] = [];
|
||||
|
||||
element.children.forEach((id) => {
|
||||
const newElementId = oldToNewElementIdMap.get(id);
|
||||
if (!newElementId) {
|
||||
throw new Error(`Element with ${id} wasn't mapped correctly`);
|
||||
}
|
||||
|
||||
const elementInFrame = elementStore.getElement(newElementId);
|
||||
if (!elementInFrame) {
|
||||
throw new Error(`Frame element with id ${newElementId} doesn't exist`);
|
||||
}
|
||||
Object.assign(elementInFrame, { frameId: frame.id });
|
||||
|
||||
elementInFrame?.boundElements?.forEach((boundElement) => {
|
||||
const ele = elementStore.getElement(boundElement.id);
|
||||
if (!ele) {
|
||||
throw new Error(
|
||||
`Bound element with id ${boundElement.id} doesn't exist`,
|
||||
);
|
||||
}
|
||||
Object.assign(ele, { frameId: frame.id });
|
||||
childrenElements.push(ele);
|
||||
});
|
||||
|
||||
childrenElements.push(elementInFrame);
|
||||
});
|
||||
|
||||
let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements);
|
||||
|
||||
const PADDING = 10;
|
||||
minX = minX - PADDING;
|
||||
minY = minY - PADDING;
|
||||
maxX = maxX + PADDING;
|
||||
maxY = maxY + PADDING;
|
||||
|
||||
// Take the max of calculated and provided frame dimensions, whichever is higher
|
||||
const width = Math.max(frame?.width, maxX - minX);
|
||||
const height = Math.max(frame?.height, maxY - minY);
|
||||
Object.assign(frame, { x: minX, y: minY, width, height });
|
||||
}
|
||||
|
||||
return elementStore.getElements();
|
||||
};
|
||||
|
@ -392,7 +392,7 @@ export const getLinkHandleFromCoords = (
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
angle: number,
|
||||
appState: Pick<UIAppState, "zoom">,
|
||||
): [x: number, y: number, width: number, height: number] => {
|
||||
): Bounds => {
|
||||
const size = DEFAULT_LINK_SIZE;
|
||||
const linkWidth = size / appState.zoom.value;
|
||||
const linkHeight = size / appState.zoom.value;
|
||||
|
@ -34,7 +34,12 @@ export type RectangleBox = {
|
||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
||||
|
||||
// x and y position of top left corner, x and y position of bottom right corner
|
||||
export type Bounds = readonly [x1: number, y1: number, x2: number, y2: number];
|
||||
export type Bounds = readonly [
|
||||
minX: number,
|
||||
minY: number,
|
||||
maxX: number,
|
||||
maxY: number,
|
||||
];
|
||||
|
||||
export class ElementBounds {
|
||||
private static boundsCache = new WeakMap<
|
||||
@ -63,7 +68,7 @@ export class ElementBounds {
|
||||
}
|
||||
|
||||
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
||||
let bounds: [number, number, number, number];
|
||||
let bounds: Bounds;
|
||||
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||
|
||||
@ -387,7 +392,7 @@ const getCubicBezierCurveBound = (
|
||||
export const getMinMaxXYFromCurvePathOps = (
|
||||
ops: Op[],
|
||||
transformXY?: (x: number, y: number) => [number, number],
|
||||
): [number, number, number, number] => {
|
||||
): Bounds => {
|
||||
let currentP: Point = [0, 0];
|
||||
|
||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||
@ -435,9 +440,9 @@ export const getMinMaxXYFromCurvePathOps = (
|
||||
return [minX, minY, maxX, maxY];
|
||||
};
|
||||
|
||||
const getBoundsFromPoints = (
|
||||
export const getBoundsFromPoints = (
|
||||
points: ExcalidrawFreeDrawElement["points"],
|
||||
): [number, number, number, number] => {
|
||||
): Bounds => {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
@ -589,7 +594,7 @@ const getLinearElementRotatedBounds = (
|
||||
element: ExcalidrawLinearElement,
|
||||
cx: number,
|
||||
cy: number,
|
||||
): [number, number, number, number] => {
|
||||
): Bounds => {
|
||||
if (element.points.length < 2) {
|
||||
const [pointX, pointY] = element.points[0];
|
||||
const [x, y] = rotate(
|
||||
@ -600,7 +605,7 @@ const getLinearElementRotatedBounds = (
|
||||
element.angle,
|
||||
);
|
||||
|
||||
let coords: [number, number, number, number] = [x, y, x, y];
|
||||
let coords: Bounds = [x, y, x, y];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
@ -625,12 +630,7 @@ const getLinearElementRotatedBounds = (
|
||||
const transformXY = (x: number, y: number) =>
|
||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
let coords: [number, number, number, number] = [
|
||||
res[0],
|
||||
res[1],
|
||||
res[2],
|
||||
res[3],
|
||||
];
|
||||
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
@ -692,7 +692,7 @@ export const getResizedElementAbsoluteCoords = (
|
||||
nextWidth: number,
|
||||
nextHeight: number,
|
||||
normalizePoints: boolean,
|
||||
): [number, number, number, number] => {
|
||||
): Bounds => {
|
||||
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||
return [
|
||||
element.x,
|
||||
@ -709,7 +709,7 @@ export const getResizedElementAbsoluteCoords = (
|
||||
normalizePoints,
|
||||
);
|
||||
|
||||
let bounds: [number, number, number, number];
|
||||
let bounds: Bounds;
|
||||
|
||||
if (isFreeDrawElement(element)) {
|
||||
// Free Draw
|
||||
@ -740,7 +740,7 @@ export const getResizedElementAbsoluteCoords = (
|
||||
export const getElementPointsCoords = (
|
||||
element: ExcalidrawLinearElement,
|
||||
points: readonly (readonly [number, number])[],
|
||||
): [number, number, number, number] => {
|
||||
): Bounds => {
|
||||
// This might be computationally heavey
|
||||
const gen = rough.generator();
|
||||
const curve =
|
||||
|
@ -494,7 +494,9 @@ const hitTestFreeDrawElement = (
|
||||
// for filled freedraw shapes, support
|
||||
// selecting from inside
|
||||
if (shape && shape.sets.length) {
|
||||
return hitTestRoughShape(shape, x, y, threshold);
|
||||
return element.fillStyle === "solid"
|
||||
? hitTestCurveInside(shape, x, y, "round")
|
||||
: hitTestRoughShape(shape, x, y, threshold);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -8,7 +8,11 @@ import { getBoundTextElement } from "./textElement";
|
||||
import { isSelectedViaGroup } from "../groups";
|
||||
import { getGridPoint } from "../math";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isFrameElement } from "./typeChecks";
|
||||
import {
|
||||
isArrowElement,
|
||||
isBoundToContainer,
|
||||
isFrameElement,
|
||||
} from "./typeChecks";
|
||||
|
||||
export const dragSelectedElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
@ -35,13 +39,18 @@ export const dragSelectedElements = (
|
||||
if (frames.length > 0) {
|
||||
const elementsInFrames = scene
|
||||
.getNonDeletedElements()
|
||||
.filter((e) => !isBoundToContainer(e))
|
||||
.filter((e) => e.frameId !== null)
|
||||
.filter((e) => frames.includes(e.frameId!));
|
||||
|
||||
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
||||
}
|
||||
|
||||
const commonBounds = getCommonBounds(Array.from(elementsToUpdate));
|
||||
const commonBounds = getCommonBounds(
|
||||
Array.from(elementsToUpdate).map(
|
||||
(el) => pointerDownState.originalElements.get(el.id) ?? el,
|
||||
),
|
||||
);
|
||||
const adjustedOffset = calculateOffset(
|
||||
commonBounds,
|
||||
offset,
|
||||
@ -54,20 +63,16 @@ export const dragSelectedElements = (
|
||||
// update coords of bound text only if we're dragging the container directly
|
||||
// (we don't drag the group that it's part of)
|
||||
if (
|
||||
// Don't update coords of arrow label since we calculate its position during render
|
||||
!isArrowElement(element) &&
|
||||
// container isn't part of any group
|
||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
||||
!element.groupIds.length ||
|
||||
// container is part of a group, but we're dragging the container directly
|
||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
||||
(!element.groupIds.length ||
|
||||
// container is part of a group, but we're dragging the container directly
|
||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
|
||||
) {
|
||||
const textElement = getBoundTextElement(element);
|
||||
if (
|
||||
textElement &&
|
||||
// when container is added to a frame, so will its bound text
|
||||
// so the text is already in `elementsToUpdate` and we should avoid
|
||||
// updating its coords again
|
||||
(!textElement.frameId || !frames.includes(textElement.frameId))
|
||||
) {
|
||||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
@ -27,6 +28,7 @@ const embeddedLinkCache = new Map<string, EmbeddedLink>();
|
||||
|
||||
const RE_YOUTUBE =
|
||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||
|
||||
const RE_VIMEO =
|
||||
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
||||
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
|
||||
@ -46,6 +48,9 @@ const RE_VALTOWN =
|
||||
const RE_GENERIC_EMBED =
|
||||
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||
|
||||
const RE_GIPHY =
|
||||
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
||||
|
||||
const ALLOWED_DOMAINS = new Set([
|
||||
"youtube.com",
|
||||
"youtu.be",
|
||||
@ -58,6 +63,7 @@ const ALLOWED_DOMAINS = new Set([
|
||||
"*.simplepdf.eu",
|
||||
"stackblitz.com",
|
||||
"val.town",
|
||||
"giphy.com",
|
||||
]);
|
||||
|
||||
const createSrcDoc = (body: string) => {
|
||||
@ -307,6 +313,10 @@ export const extractSrc = (htmlString: string): string => {
|
||||
return gistMatch[1];
|
||||
}
|
||||
|
||||
if (RE_GIPHY.test(htmlString)) {
|
||||
return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
|
||||
}
|
||||
|
||||
const match = htmlString.match(RE_GENERIC_EMBED);
|
||||
if (match && match.length === 2) {
|
||||
return match[1];
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
} from "../math";
|
||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||
import {
|
||||
Bounds,
|
||||
getCurvePathOps,
|
||||
getElementPointsCoords,
|
||||
getMinMaxXYFromCurvePathOps,
|
||||
@ -1316,7 +1317,7 @@ export class LinearElementEditor {
|
||||
|
||||
static getMinMaxXYWithBoundText = (
|
||||
element: ExcalidrawLinearElement,
|
||||
elementBounds: [number, number, number, number],
|
||||
elementBounds: Bounds,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
): [number, number, number, number, number, number] => {
|
||||
let [x1, y1, x2, y2] = elementBounds;
|
||||
|
@ -140,8 +140,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;
|
||||
|
@ -144,13 +144,15 @@ export const newEmbeddableElement = (
|
||||
};
|
||||
|
||||
export const newFrameElement = (
|
||||
opts: ElementConstructorOpts,
|
||||
opts: {
|
||||
name?: string;
|
||||
} & ElementConstructorOpts,
|
||||
): NonDeleted<ExcalidrawFrameElement> => {
|
||||
const frameElement = newElementWith(
|
||||
{
|
||||
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
||||
type: "frame",
|
||||
name: null,
|
||||
name: opts?.name || null,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
MaybeTransformHandleType,
|
||||
} from "./transformHandles";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { Bounds } from "./bounds";
|
||||
|
||||
const isInsideTransformHandle = (
|
||||
transformHandle: TransformHandle,
|
||||
@ -87,7 +88,7 @@ export const getElementWithTransformHandleType = (
|
||||
};
|
||||
|
||||
export const getTransformHandleTypeFromCoords = (
|
||||
[x1, y1, x2, y2]: readonly [number, number, number, number],
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
scenePointerX: number,
|
||||
scenePointerY: number,
|
||||
zoom: Zoom,
|
||||
|
@ -91,7 +91,7 @@ export const redrawTextBoundingBox = (
|
||||
);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||
|
||||
if (metrics.height > maxContainerHeight) {
|
||||
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
||||
const nextHeight = computeContainerDimensionForBoundText(
|
||||
metrics.height,
|
||||
container.type,
|
||||
|
@ -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, updateTextEditor } from "../tests/queries/dom";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
@ -26,16 +26,7 @@ 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"));
|
||||
};
|
||||
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||
|
||||
describe("textWysiwyg", () => {
|
||||
describe("start text editing", () => {
|
||||
@ -186,7 +177,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 +192,14 @@ describe("textWysiwyg", () => {
|
||||
|
||||
mouse.clickAt(text.x + 50, text.y + 50);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, 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,12 +214,26 @@ describe("textWysiwyg", () => {
|
||||
|
||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, false);
|
||||
|
||||
expect(editor).not.toBe(null);
|
||||
expect(h.state.editingElement?.id).toBe(text.id);
|
||||
expect(h.elements.length).toBe(1);
|
||||
});
|
||||
|
||||
// FIXME too flaky. No one knows why.
|
||||
it.skip("should bump the version of a labeled arrow when the label is updated", async () => {
|
||||
const arrow = UI.createElement("arrow", {
|
||||
width: 300,
|
||||
height: 0,
|
||||
});
|
||||
await UI.editText(arrow, "Hello");
|
||||
const { version } = arrow;
|
||||
|
||||
await UI.editText(arrow, "Hello\nworld!");
|
||||
|
||||
expect(arrow.version).toEqual(version + 1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test container-unbound text", () => {
|
||||
@ -250,7 +255,7 @@ describe("textWysiwyg", () => {
|
||||
textElement = UI.createElement("text");
|
||||
|
||||
mouse.clickOn(textElement);
|
||||
textarea = getTextEditor();
|
||||
textarea = await getTextEditor(textEditorSelector, true);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
@ -460,7 +465,7 @@ describe("textWysiwyg", () => {
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(750, 300);
|
||||
|
||||
textarea = getTextEditor();
|
||||
textarea = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(
|
||||
textarea,
|
||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||
@ -512,7 +517,7 @@ describe("textWysiwyg", () => {
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@ -540,7 +545,7 @@ describe("textWysiwyg", () => {
|
||||
]);
|
||||
expect(text.angle).toBe(rectangle.angle);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@ -567,7 +572,7 @@ describe("textWysiwyg", () => {
|
||||
API.setSelectedElements([diamond]);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
const value = new Array(1000).fill("1").join("\n");
|
||||
@ -602,7 +607,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(null);
|
||||
mouse.down();
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
|
||||
@ -617,7 +622,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
|
||||
mouse.down();
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@ -639,7 +644,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(textEditorSelector, true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
@ -674,7 +679,7 @@ describe("textWysiwyg", () => {
|
||||
{ id: text.id, type: "text" },
|
||||
]);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@ -699,7 +704,7 @@ describe("textWysiwyg", () => {
|
||||
freedraw.y + freedraw.height / 2,
|
||||
);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||
|
||||
@ -733,7 +738,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(null);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@ -748,7 +753,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
UI.clickTool("text");
|
||||
mouse.clickAt(20, 30);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(
|
||||
editor,
|
||||
@ -793,7 +798,7 @@ describe("textWysiwyg", () => {
|
||||
mouse.down();
|
||||
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
@ -806,7 +811,7 @@ describe("textWysiwyg", () => {
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
mouse.down();
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
editor.select();
|
||||
fireEvent.click(screen.getByTitle(/code/i));
|
||||
@ -839,7 +844,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
Keyboard.keyDown(KEYS.ENTER);
|
||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@ -860,7 +865,7 @@ describe("textWysiwyg", () => {
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello");
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
@ -889,7 +894,7 @@ describe("textWysiwyg", () => {
|
||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
@ -926,7 +931,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(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
editor.blur();
|
||||
@ -947,13 +952,13 @@ describe("textWysiwyg", () => {
|
||||
it("should respect text alignment when resizing", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
let editor = getTextEditor();
|
||||
let editor = await getTextEditor(textEditorSelector, 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,
|
||||
@ -964,7 +969,7 @@ describe("textWysiwyg", () => {
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
editor.select();
|
||||
|
||||
@ -977,7 +982,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 +992,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
editor.select();
|
||||
|
||||
@ -999,7 +1004,7 @@ 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(`
|
||||
[
|
||||
374.99999999999994,
|
||||
@ -1025,7 +1030,7 @@ describe("textWysiwyg", () => {
|
||||
expect(text.type).toBe("text");
|
||||
expect(text.containerId).toBe(rectangle.id);
|
||||
mouse.down();
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
|
||||
@ -1040,7 +1045,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(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
@ -1049,7 +1054,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 +1065,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(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
@ -1092,7 +1097,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
it("undo should work", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
@ -1129,7 +1134,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
it("should not allow bound text with only whitespaces", async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
updateTextEditor(editor, " ");
|
||||
@ -1184,19 +1189,19 @@ 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(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
|
||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
UI.resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||
expect(rectangle.height).toBeCloseTo(155, 8);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
editor.blur();
|
||||
@ -1212,7 +1217,7 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
editor.blur();
|
||||
|
||||
@ -1237,7 +1242,7 @@ describe("textWysiwyg", () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||
|
||||
const editor = getTextEditor();
|
||||
const editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello World!");
|
||||
editor.blur();
|
||||
expect(
|
||||
@ -1269,12 +1274,12 @@ describe("textWysiwyg", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
mouse.select(rectangle);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
editor.select();
|
||||
});
|
||||
|
||||
@ -1385,7 +1390,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(textEditorSelector, true);
|
||||
|
||||
updateTextEditor(
|
||||
editor,
|
||||
@ -1431,7 +1436,7 @@ describe("textWysiwyg", () => {
|
||||
type: "text",
|
||||
},
|
||||
],
|
||||
fillStyle: "hachure",
|
||||
fillStyle: "solid",
|
||||
groupIds: [],
|
||||
height: 35,
|
||||
isDeleted: false,
|
||||
@ -1444,7 +1449,7 @@ describe("textWysiwyg", () => {
|
||||
},
|
||||
strokeColor: "#1e1e1e",
|
||||
strokeStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeWidth: 2,
|
||||
type: "rectangle",
|
||||
updated: 1,
|
||||
version: 1,
|
||||
@ -1473,7 +1478,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(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello!");
|
||||
expect(
|
||||
@ -1498,7 +1503,7 @@ describe("textWysiwyg", () => {
|
||||
rectangle.x + rectangle.width / 2,
|
||||
rectangle.y + rectangle.height / 2,
|
||||
);
|
||||
editor = getTextEditor();
|
||||
editor = await getTextEditor(textEditorSelector, true);
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Excalidraw");
|
||||
editor.blur();
|
||||
@ -1512,30 +1517,4 @@ describe("textWysiwyg", () => {
|
||||
expect(text.text).toBe("Excalidraw");
|
||||
});
|
||||
});
|
||||
|
||||
it("should bump the version of labelled arrow when label updated", async () => {
|
||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||
const arrow = UI.createElement("arrow", {
|
||||
width: 300,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
mouse.select(arrow);
|
||||
Keyboard.keyPress(KEYS.ENTER);
|
||||
let editor = getTextEditor();
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
updateTextEditor(editor, "Hello");
|
||||
editor.blur();
|
||||
|
||||
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();
|
||||
|
||||
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);
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
PointerType,
|
||||
} from "./types";
|
||||
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
||||
import { rotate } from "../math";
|
||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
@ -23,7 +23,7 @@ export type TransformHandleDirection =
|
||||
|
||||
export type TransformHandleType = TransformHandleDirection | "rotation";
|
||||
|
||||
export type TransformHandle = [number, number, number, number];
|
||||
export type TransformHandle = Bounds;
|
||||
export type TransformHandles = Partial<{
|
||||
[T in TransformHandleType]: TransformHandle;
|
||||
}>;
|
||||
|
47
src/emitter.ts
Normal file
47
src/emitter.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -123,7 +123,7 @@ describe("adding elements to frames", () => {
|
||||
const commonTestCases = async (
|
||||
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
||||
) => {
|
||||
describe("when frame is in a layer below", async () => {
|
||||
describe.skip("when frame is in a layer below", async () => {
|
||||
it("should add an element", async () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
@ -167,7 +167,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when frame is in a layer above", async () => {
|
||||
describe.skip("when frame is in a layer above", async () => {
|
||||
it("should add an element", async () => {
|
||||
h.elements = [rect2, frame];
|
||||
|
||||
@ -177,7 +177,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect2, frame]);
|
||||
});
|
||||
|
||||
it.skip("should add elements", async () => {
|
||||
it("should add elements", async () => {
|
||||
h.elements = [rect2, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -188,7 +188,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it.skip("should add elements when there are other other elements in between", async () => {
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect1, rect2, rect4, rect3, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -199,7 +199,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect1, rect4, rect3, rect2, frame]);
|
||||
});
|
||||
|
||||
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||
h.elements = [rect3, rect4, rect2, rect1, frame];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -212,7 +212,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
describe("when frame is in an inner layer", async () => {
|
||||
it("should add elements", async () => {
|
||||
it.skip("should add elements", async () => {
|
||||
h.elements = [rect2, frame, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -223,7 +223,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect2, rect3, frame]);
|
||||
});
|
||||
|
||||
it("should add elements when there are other other elements in between", async () => {
|
||||
it.skip("should add elements when there are other other elements in between", async () => {
|
||||
h.elements = [rect2, rect1, frame, rect4, rect3];
|
||||
|
||||
func(frame, rect2);
|
||||
@ -289,7 +289,7 @@ describe("adding elements to frames", () => {
|
||||
describe("resizing frame over elements", async () => {
|
||||
await commonTestCases(resizeFrameOverElement);
|
||||
|
||||
it("resizing over text containers and labelled arrows", async () => {
|
||||
it.skip("resizing over text containers and labelled arrows", async () => {
|
||||
await resizingTest(
|
||||
"rectangle",
|
||||
["frame", "rectangle", "text"],
|
||||
@ -339,7 +339,7 @@ describe("adding elements to frames", () => {
|
||||
// );
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in a layer below", async () => {
|
||||
it.skip("should add arrow bound with text when frame is in a layer below", async () => {
|
||||
h.elements = [frame, arrow, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
@ -359,7 +359,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([arrow, text, frame]);
|
||||
});
|
||||
|
||||
it("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||
it.skip("should add arrow bound with text when frame is in an inner layer", async () => {
|
||||
h.elements = [arrow, frame, text];
|
||||
|
||||
resizeFrameOverElement(frame, arrow);
|
||||
@ -371,7 +371,7 @@ describe("adding elements to frames", () => {
|
||||
});
|
||||
|
||||
describe("resizing frame over elements but downwards", async () => {
|
||||
it("should add elements when frame is in a layer below", async () => {
|
||||
it.skip("should add elements when frame is in a layer below", async () => {
|
||||
h.elements = [frame, rect1, rect2, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
@ -382,7 +382,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||
});
|
||||
|
||||
it("should add elements when frame is in a layer above", async () => {
|
||||
it.skip("should add elements when frame is in a layer above", async () => {
|
||||
h.elements = [rect1, rect2, rect3, rect4, frame];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
@ -393,7 +393,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||
});
|
||||
|
||||
it("should add elements when frame is in an inner layer", async () => {
|
||||
it.skip("should add elements when frame is in an inner layer", async () => {
|
||||
h.elements = [rect1, rect2, frame, rect3, rect4];
|
||||
|
||||
resizeFrameOverElement(frame, rect4);
|
||||
@ -408,7 +408,7 @@ describe("adding elements to frames", () => {
|
||||
describe("dragging elements into the frame", async () => {
|
||||
await commonTestCases(dragElementIntoFrame);
|
||||
|
||||
it("should drag element inside, duplicate it and keep it in frame", () => {
|
||||
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
@ -422,7 +422,7 @@ describe("adding elements to frames", () => {
|
||||
expectEqualIds([rect2_copy, rect2, frame]);
|
||||
});
|
||||
|
||||
it("should drag element inside, duplicate it and remove it from frame", () => {
|
||||
it.skip("should drag element inside, duplicate it and remove it from frame", () => {
|
||||
h.elements = [frame, rect2];
|
||||
|
||||
dragElementIntoFrame(frame, rect2);
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
254
src/frame.ts
254
src/frame.ts
@ -19,10 +19,10 @@ import { mutateElement } from "./element/mutateElement";
|
||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||
import { isFrameElement } from "./element";
|
||||
import { moveOneRight } from "./zindex";
|
||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||
import { getElementLineSegments } from "./element/bounds";
|
||||
import { doLineSegmentsIntersect } from "./packages/utils";
|
||||
|
||||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
@ -56,130 +56,21 @@ export const bindElementsToFramesAfterDuplication = (
|
||||
}
|
||||
};
|
||||
|
||||
// --------------------------- Frame Geometry ---------------------------------
|
||||
class Point {
|
||||
x: number;
|
||||
y: number;
|
||||
export function isElementIntersectingFrame(
|
||||
element: ExcalidrawElement,
|
||||
frame: ExcalidrawFrameElement,
|
||||
) {
|
||||
const frameLineSegments = getElementLineSegments(frame);
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
}
|
||||
const elementLineSegments = getElementLineSegments(element);
|
||||
|
||||
class LineSegment {
|
||||
first: Point;
|
||||
second: Point;
|
||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||
elementLineSegments.some((elementLineSegment) =>
|
||||
doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
||||
),
|
||||
);
|
||||
|
||||
constructor(pointA: Point, pointB: Point) {
|
||||
this.first = pointA;
|
||||
this.second = pointB;
|
||||
}
|
||||
|
||||
public getBoundingBox(): [Point, Point] {
|
||||
return [
|
||||
new Point(
|
||||
Math.min(this.first.x, this.second.x),
|
||||
Math.min(this.first.y, this.second.y),
|
||||
),
|
||||
new Point(
|
||||
Math.max(this.first.x, this.second.x),
|
||||
Math.max(this.first.y, this.second.y),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
||||
class FrameGeometry {
|
||||
private static EPSILON = 0.000001;
|
||||
|
||||
private static crossProduct(a: Point, b: Point) {
|
||||
return a.x * b.y - b.x * a.y;
|
||||
}
|
||||
|
||||
private static doBoundingBoxesIntersect(
|
||||
a: [Point, Point],
|
||||
b: [Point, Point],
|
||||
) {
|
||||
return (
|
||||
a[0].x <= b[1].x &&
|
||||
a[1].x >= b[0].x &&
|
||||
a[0].y <= b[1].y &&
|
||||
a[1].y >= b[0].y
|
||||
);
|
||||
}
|
||||
|
||||
private static isPointOnLine(a: LineSegment, b: Point) {
|
||||
const aTmp = new LineSegment(
|
||||
new Point(0, 0),
|
||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
||||
);
|
||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
||||
const r = this.crossProduct(aTmp.second, bTmp);
|
||||
return Math.abs(r) < this.EPSILON;
|
||||
}
|
||||
|
||||
private static isPointRightOfLine(a: LineSegment, b: Point) {
|
||||
const aTmp = new LineSegment(
|
||||
new Point(0, 0),
|
||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
||||
);
|
||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
||||
return this.crossProduct(aTmp.second, bTmp) < 0;
|
||||
}
|
||||
|
||||
private static lineSegmentTouchesOrCrossesLine(
|
||||
a: LineSegment,
|
||||
b: LineSegment,
|
||||
) {
|
||||
return (
|
||||
this.isPointOnLine(a, b.first) ||
|
||||
this.isPointOnLine(a, b.second) ||
|
||||
(this.isPointRightOfLine(a, b.first)
|
||||
? !this.isPointRightOfLine(a, b.second)
|
||||
: this.isPointRightOfLine(a, b.second))
|
||||
);
|
||||
}
|
||||
|
||||
private static doLineSegmentsIntersect(
|
||||
a: [readonly [number, number], readonly [number, number]],
|
||||
b: [readonly [number, number], readonly [number, number]],
|
||||
) {
|
||||
const aSegment = new LineSegment(
|
||||
new Point(a[0][0], a[0][1]),
|
||||
new Point(a[1][0], a[1][1]),
|
||||
);
|
||||
const bSegment = new LineSegment(
|
||||
new Point(b[0][0], b[0][1]),
|
||||
new Point(b[1][0], b[1][1]),
|
||||
);
|
||||
|
||||
const box1 = aSegment.getBoundingBox();
|
||||
const box2 = bSegment.getBoundingBox();
|
||||
return (
|
||||
this.doBoundingBoxesIntersect(box1, box2) &&
|
||||
this.lineSegmentTouchesOrCrossesLine(aSegment, bSegment) &&
|
||||
this.lineSegmentTouchesOrCrossesLine(bSegment, aSegment)
|
||||
);
|
||||
}
|
||||
|
||||
public static isElementIntersectingFrame(
|
||||
element: ExcalidrawElement,
|
||||
frame: ExcalidrawFrameElement,
|
||||
) {
|
||||
const frameLineSegments = getElementLineSegments(frame);
|
||||
|
||||
const elementLineSegments = getElementLineSegments(element);
|
||||
|
||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||
elementLineSegments.some((elementLineSegment) =>
|
||||
this.doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
||||
),
|
||||
);
|
||||
|
||||
return intersecting;
|
||||
}
|
||||
return intersecting;
|
||||
}
|
||||
|
||||
export const getElementsCompletelyInFrame = (
|
||||
@ -207,10 +98,7 @@ export const isElementContainingFrame = (
|
||||
export const getElementsIntersectingFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameElement,
|
||||
) =>
|
||||
elements.filter((element) =>
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
||||
);
|
||||
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
||||
|
||||
export const elementsAreInFrameBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -236,7 +124,7 @@ export const elementOverlapsWithFrame = (
|
||||
) => {
|
||||
return (
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame) ||
|
||||
isElementIntersectingFrame(element, frame) ||
|
||||
isElementContainingFrame([frame], element, frame)
|
||||
);
|
||||
};
|
||||
@ -273,7 +161,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
|
||||
return !!elementsInGroup.find(
|
||||
(element) =>
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
||||
isElementIntersectingFrame(element, frame),
|
||||
);
|
||||
};
|
||||
|
||||
@ -294,7 +182,7 @@ export const groupsAreCompletelyOutOfFrame = (
|
||||
elementsInGroup.find(
|
||||
(element) =>
|
||||
elementsAreInFrameBounds([element], frame) ||
|
||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
||||
isElementIntersectingFrame(element, frame),
|
||||
) === undefined
|
||||
);
|
||||
};
|
||||
@ -354,7 +242,7 @@ export const getElementsInResizingFrame = (
|
||||
);
|
||||
|
||||
for (const element of elementsNotCompletelyInFrame) {
|
||||
if (!FrameGeometry.isElementIntersectingFrame(element, frame)) {
|
||||
if (!isElementIntersectingFrame(element, frame)) {
|
||||
if (element.groupIds.length === 0) {
|
||||
nextElementsInFrame.delete(element);
|
||||
}
|
||||
@ -452,21 +340,27 @@ export const getContainingFrame = (
|
||||
};
|
||||
|
||||
// --------------------------- 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 currTargetFrameChildrenMap = new Map(
|
||||
allElements.reduce(
|
||||
(acc: [ExcalidrawElement["id"], ExcalidrawElement][], element) => {
|
||||
if (element.frameId === frame.id) {
|
||||
acc.push([element.id, element]);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
),
|
||||
const { currTargetFrameChildrenMap } = allElements.reduce(
|
||||
(acc, element, index) => {
|
||||
if (element.frameId === frame.id) {
|
||||
acc.currTargetFrameChildrenMap.set(element.id, true);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
||||
},
|
||||
);
|
||||
|
||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||
@ -493,42 +387,6 @@ export const addElementsToFrame = (
|
||||
}
|
||||
}
|
||||
|
||||
const finalElementsToAddSet = new Set(finalElementsToAdd.map((el) => el.id));
|
||||
|
||||
const nextElements: ExcalidrawElement[] = [];
|
||||
|
||||
const processedElements = new Set<ExcalidrawElement["id"]>();
|
||||
|
||||
for (const element of allElements) {
|
||||
if (processedElements.has(element.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
// console.log(currFrameChildren, finalElementsToAdd, element);
|
||||
nextElements.push(...currFrameChildren, ...finalElementsToAdd, element);
|
||||
continue;
|
||||
}
|
||||
|
||||
// console.log("(2)", element.frameId);
|
||||
nextElements.push(element);
|
||||
}
|
||||
|
||||
for (const element of finalElementsToAdd) {
|
||||
mutateElement(
|
||||
element,
|
||||
@ -538,8 +396,7 @@ export const addElementsToFrame = (
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return nextElements;
|
||||
return allElements.slice();
|
||||
};
|
||||
|
||||
export const removeElementsFromFrame = (
|
||||
@ -547,20 +404,34 @@ export const removeElementsFromFrame = (
|
||||
elementsToRemove: NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const _elementsToRemove: ExcalidrawElement[] = [];
|
||||
const _elementsToRemove = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
ExcalidrawElement
|
||||
>();
|
||||
|
||||
const toRemoveElementsByFrame = new Map<
|
||||
ExcalidrawFrameElement["id"],
|
||||
ExcalidrawElement[]
|
||||
>();
|
||||
|
||||
for (const element of elementsToRemove) {
|
||||
if (element.frameId) {
|
||||
_elementsToRemove.push(element);
|
||||
_elementsToRemove.set(element.id, element);
|
||||
|
||||
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
|
||||
arr.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
_elementsToRemove.push(boundTextElement);
|
||||
_elementsToRemove.set(boundTextElement.id, boundTextElement);
|
||||
arr.push(boundTextElement);
|
||||
}
|
||||
|
||||
toRemoveElementsByFrame.set(element.frameId, arr);
|
||||
}
|
||||
}
|
||||
|
||||
for (const element of _elementsToRemove) {
|
||||
for (const [, element] of _elementsToRemove) {
|
||||
mutateElement(
|
||||
element,
|
||||
{
|
||||
@ -570,13 +441,7 @@ export const removeElementsFromFrame = (
|
||||
);
|
||||
}
|
||||
|
||||
const nextElements = moveOneRight(
|
||||
allElements,
|
||||
appState,
|
||||
Array.from(_elementsToRemove),
|
||||
);
|
||||
|
||||
return nextElements;
|
||||
return allElements.slice();
|
||||
};
|
||||
|
||||
export const removeAllElementsFromFrame = (
|
||||
@ -707,6 +572,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);
|
||||
}
|
||||
|
@ -17,9 +17,13 @@ export const useCreatePortalContainer = (opts?: {
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (div) {
|
||||
div.className = "";
|
||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||
div.classList.toggle("theme--dark", theme === "dark");
|
||||
}
|
||||
}, [div, device.isMobile]);
|
||||
}, [div, theme, device.isMobile, opts?.className]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = opts?.parentSelector
|
||||
@ -32,10 +36,6 @@ export const useCreatePortalContainer = (opts?: {
|
||||
|
||||
const div = document.createElement("div");
|
||||
|
||||
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
|
||||
div.classList.toggle("theme--dark", theme === "dark");
|
||||
|
||||
container.appendChild(div);
|
||||
|
||||
setDiv(div);
|
||||
@ -43,7 +43,7 @@ export const useCreatePortalContainer = (opts?: {
|
||||
return () => {
|
||||
container.removeChild(div);
|
||||
};
|
||||
}, [excalidrawContainer, theme, opts?.className, opts?.parentSelector]);
|
||||
}, [excalidrawContainer, opts?.parentSelector]);
|
||||
|
||||
return div;
|
||||
};
|
||||
|
@ -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>بإمكانك إجراء النسخ الاحتياطي لرسمتك الحالية باستخدام أحد الخيارات أدناه."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -203,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",
|
||||
@ -217,7 +218,10 @@
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "Embeddable elements cannot be added to the library.",
|
||||
"image": "Support for adding images to the library coming soon!"
|
||||
}
|
||||
},
|
||||
"asyncPasteFailedOnRead": "Couldn't paste (couldn't read from system clipboard).",
|
||||
"asyncPasteFailedOnParse": "Couldn't paste.",
|
||||
"copyToSystemClipboardFailed": "Couldn't copy to clipboard."
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "Selection",
|
||||
@ -238,7 +242,8 @@
|
||||
"embeddable": "Web Embed",
|
||||
"laser": "Laser pointer",
|
||||
"hand": "Hand (panning tool)",
|
||||
"extraTools": "More tools"
|
||||
"extraTools": "More tools",
|
||||
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Canvas actions",
|
||||
@ -497,5 +502,12 @@
|
||||
"description": "Loading external drawing will <bold>replace your existing content</bold>.<br></br>You can back up your drawing first by using one of the options below."
|
||||
}
|
||||
}
|
||||
},
|
||||
"mermaid": {
|
||||
"title": "Mermaid to Excalidraw",
|
||||
"button": "Insert",
|
||||
"description": "Currently only <flowchartLink>Flowcharts</flowchartLink> and <sequenceLink>Sequence Diagrams</sequenceLink> are supported. The other types will be rendered as image in Excalidraw.",
|
||||
"syntax": "Mermaid Syntax",
|
||||
"preview": "Preview"
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "متن را در یک جایگاه بپیچید",
|
||||
"link": {
|
||||
"edit": "ویرایش لینک",
|
||||
"editEmbed": "",
|
||||
"create": "ایجاد پیوند",
|
||||
"label": "لینک"
|
||||
"createEmbed": "",
|
||||
"label": "لینک",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "ویرایش لینک",
|
||||
@ -126,7 +130,7 @@
|
||||
"sidebarLock": "باز نگه داشتن سایدبار",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"eyeDropper": ""
|
||||
"eyeDropper": "انتخاب رنگ از کرباس"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "آیتمی به اینجا اضافه نشده...",
|
||||
@ -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": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "SVG نادرست.",
|
||||
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||
"importLibraryError": "دادهها بارگذاری نشدند",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "این می تواند منجر به شکستن <bold>عناصر متن</bold> در نقاشی های شما شود.",
|
||||
"line3": "اکیداً توصیه می کنیم این تنظیم را غیرفعال کنید. برای نحوه انجام این کار میتوانید <link>این مراحل</link> را دنبال کنید.",
|
||||
"line4": "اگر غیرفعال کردن این تنظیم نمایش عناصر متنی را برطرف نکرد، لطفاً یک <issueLink>مشکل</issueLink> را در GitHub ما باز کنید یا برای ما در <discordLink>Discord</discordLink> بنویسید."
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -224,8 +236,10 @@
|
||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||
"eraser": "پاک کن",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "دست (ابزار پانینگ)",
|
||||
"extraTools": ""
|
||||
"extraTools": "ابزارهای بیشتر"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "عملیات روی بوم",
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
||||
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
||||
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
||||
"embeddable": "",
|
||||
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
|
||||
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
||||
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
||||
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
|
||||
"firefox_clipboard_write": "احتمالاً میتوان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید."
|
||||
"firefox_clipboard_write": "احتمالاً میتوان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید.",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
||||
@ -362,7 +378,7 @@
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"withBackground": "پس زمینه",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
@ -411,24 +427,26 @@
|
||||
"fileSavedToFilename": "ذخیره در {filename}",
|
||||
"canvas": "بوم",
|
||||
"selection": "انتخاب",
|
||||
"pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید"
|
||||
"pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید",
|
||||
"unableToEmbed": "",
|
||||
"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": {
|
||||
@ -445,9 +463,40 @@
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"colors": "رنگها",
|
||||
"shades": "جلوهها",
|
||||
"hexCode": "کدِ هگز",
|
||||
"noShades": ""
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "ذخیره در دیسک",
|
||||
"button": "ذخیره در دیسک",
|
||||
"description": ""
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "بارگذاری از فایل",
|
||||
"button": "بارگذاری از فایل",
|
||||
"description": ""
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "Muokkaa linkkiä",
|
||||
"editEmbed": "",
|
||||
"create": "Luo linkki",
|
||||
"label": "Linkki"
|
||||
"createEmbed": "",
|
||||
"label": "Linkki",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Muokkaa riviä",
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Tumma tila",
|
||||
"lightMode": "Vaalea tila",
|
||||
"zenMode": "Zen-tila",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "Poistu zen-tilasta",
|
||||
"cancel": "Peruuta",
|
||||
"clear": "Pyyhi",
|
||||
"remove": "Poista",
|
||||
"embed": "",
|
||||
"publishLibrary": "Julkaise",
|
||||
"submit": "Lähetä",
|
||||
"confirm": "Vahvista"
|
||||
"confirm": "Vahvista",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?",
|
||||
@ -196,6 +203,7 @@
|
||||
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
|
||||
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
|
||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "Virheellinen SVG.",
|
||||
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
||||
"importLibraryError": "Kokoelman lataaminen epäonnistui",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -224,6 +236,8 @@
|
||||
"link": "Lisää/päivitä linkki valitulle muodolle",
|
||||
"eraser": "Poistotyökalu",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "Käsi (panning-työkalu)",
|
||||
"extraTools": ""
|
||||
},
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
|
||||
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
|
||||
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
|
||||
"embeddable": "",
|
||||
"text_selected": "Kaksoisnapsauta tai paina ENTER muokataksesi tekstiä",
|
||||
"text_editing": "Paina Escape tai CtrlOrCmd+ENTER lopettaaksesi muokkaamisen",
|
||||
"linearElementMulti": "Lopeta klikkaamalla viimeistä pistettä, painamalla Escape- tai Enter-näppäintä",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Lisää tekstiä painamalla enter",
|
||||
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
|
||||
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
|
||||
"firefox_clipboard_write": "Tämä ominaisuus voidaan todennäköisesti ottaa käyttöön asettamalla \"dom.events.asyncClipboard.clipboardItem\" kohta \"true\":ksi. Vaihtaaksesi selaimen kohdan Firefoxissa, käy \"about:config\" sivulla."
|
||||
"firefox_clipboard_write": "Tämä ominaisuus voidaan todennäköisesti ottaa käyttöön asettamalla \"dom.events.asyncClipboard.clipboardItem\" kohta \"true\":ksi. Vaihtaaksesi selaimen kohdan Firefoxissa, käy \"about:config\" sivulla.",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
||||
@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
||||
"canvas": "piirtoalue",
|
||||
"selection": "valinta",
|
||||
"pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin"
|
||||
"pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Läpinäkyvä",
|
||||
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@
|
||||
"veryLarge": "Très grande",
|
||||
"solid": "Solide",
|
||||
"hachure": "Hachures",
|
||||
"zigzag": "",
|
||||
"zigzag": "Zigzag",
|
||||
"crossHatch": "Hachures croisées",
|
||||
"thin": "Fine",
|
||||
"bold": "Épaisse",
|
||||
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Encadrer le texte dans un conteneur",
|
||||
"link": {
|
||||
"edit": "Modifier le lien",
|
||||
"editEmbed": "Éditer le lien & intégrer",
|
||||
"create": "Ajouter un lien",
|
||||
"label": "Lien"
|
||||
"createEmbed": "Créer un lien & intégrer",
|
||||
"label": "Lien",
|
||||
"labelEmbed": "",
|
||||
"empty": "Aucun lien défini"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Modifier la ligne",
|
||||
@ -124,8 +128,8 @@
|
||||
},
|
||||
"statusPublished": "Publié",
|
||||
"sidebarLock": "Maintenir la barre latérale ouverte",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"selectAllElementsInFrame": "Sélectionner tous les éléments du cadre",
|
||||
"removeAllElementsFromFrame": "Supprimer tous les éléments du cadre",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Mode sombre",
|
||||
"lightMode": "Mode clair",
|
||||
"zenMode": "Mode zen",
|
||||
"objectsSnapMode": "Aimanter aux objets",
|
||||
"exitZenMode": "Quitter le mode zen",
|
||||
"cancel": "Annuler",
|
||||
"clear": "Effacer",
|
||||
"remove": "Supprimer",
|
||||
"embed": "Activer/Désactiver l'intégration",
|
||||
"publishLibrary": "Publier",
|
||||
"submit": "Envoyer",
|
||||
"confirm": "Confirmer"
|
||||
"confirm": "Confirmer",
|
||||
"embeddableInteractionButton": "Cliquez pour interagir"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
|
||||
@ -196,16 +203,21 @@
|
||||
"imageInsertError": "Impossible d'insérer l'image. Réessayez plus tard...",
|
||||
"fileTooBig": "Le fichier est trop volumineux. La taille maximale autorisée est de {{maxSize}}.",
|
||||
"svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "SVG invalide.",
|
||||
"cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.",
|
||||
"importLibraryError": "Impossible de charger la bibliothèque",
|
||||
"collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.",
|
||||
"collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail.",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "On dirait que vous utilisez le navigateur Brave avec l'option <bold>Bloquer agressivement le fichage</bold> activée.",
|
||||
"line2": "Cela pourrait entraîner des problèmes avec les <bold>Éléments Textuels</bold> dans vos dessins.",
|
||||
"line3": "Nous recommandons fortement de désactiver cette option. Vous pouvez suivre <link>ces instructions</link> pour savoir comment faire.",
|
||||
"line4": "Si désactiver cette option de résout pas le problème d'affichage des éléments textuels, veuillez ouvrir un <issueLink>ticket</issueLink> sur notre GitHub, ou écrivez-nous sur notre <discordLink>Discord</discordLink>"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "Les éléments intégrés ne peuvent pas être ajoutés à la librairie.",
|
||||
"image": "Le support pour l'ajout d'images à la librairie arrive bientôt !"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -223,9 +235,11 @@
|
||||
"penMode": "Mode stylo - évite le toucher",
|
||||
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
|
||||
"eraser": "Gomme",
|
||||
"frame": "",
|
||||
"frame": "Outil de cadre",
|
||||
"embeddable": "Intégration Web",
|
||||
"laser": "",
|
||||
"hand": "Mains (outil de déplacement de la vue)",
|
||||
"extraTools": ""
|
||||
"extraTools": "Plus d'outils"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Actions du canevas",
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
|
||||
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
|
||||
"text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
|
||||
"embeddable": "Cliquez et glissez pour créer une intégration de site web",
|
||||
"text_selected": "Double-cliquez ou appuyez sur ENTRÉE pour modifier le texte",
|
||||
"text_editing": "Appuyez sur ÉCHAP ou Ctrl/Cmd+ENTRÉE pour terminer l'édition",
|
||||
"linearElementMulti": "Cliquez sur le dernier point ou appuyez sur Échap ou Entrée pour terminer",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
|
||||
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
|
||||
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression",
|
||||
"firefox_clipboard_write": "Cette fonctionnalité devrait pouvoir être activée en définissant l'option \"dom.events.asyncClipboard.clipboard.clipboardItem\" à \"true\". Pour modifier les paramètres du navigateur dans Firefox, visitez la page \"about:config\"."
|
||||
"firefox_clipboard_write": "Cette fonctionnalité devrait pouvoir être activée en définissant l'option \"dom.events.asyncClipboard.clipboard.clipboardItem\" à \"true\". Pour modifier les paramètres du navigateur dans Firefox, visitez la page \"about:config\".",
|
||||
"disableSnapping": "Maintenez CtrlOuCmd pour désactiver l'aimantation"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Impossible d’afficher l’aperçu",
|
||||
@ -360,27 +376,27 @@
|
||||
"removeItemsFromLib": "Enlever les éléments sélectionnés de la bibliothèque"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "Exporter l'image",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"withBackground": "Fond",
|
||||
"onlySelected": "Uniquement la sélection",
|
||||
"darkMode": "Mode sombre",
|
||||
"embedScene": "Intégrer la scène",
|
||||
"scale": "Échelle",
|
||||
"padding": ""
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
"embedScene": "Les données de la scène seront sauvegardées dans le fichier PNG/SVG exporté afin que la scène puisse être restaurée depuis celui-ci.\nCela augmentera la taille du fichier exporté."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "Exporter en PNG",
|
||||
"exportToSvg": "Exporter en SVG",
|
||||
"copyPngToClipboard": "Copier le PNG dans le presse-papier"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Copier dans le presse-papier"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@ -411,24 +427,26 @@
|
||||
"fileSavedToFilename": "Enregistré sous {filename}",
|
||||
"canvas": "canevas",
|
||||
"selection": "sélection",
|
||||
"pasteAsSingleElement": "Utiliser {{shortcut}} pour coller comme un seul élément,\nou coller dans un éditeur de texte existant"
|
||||
"pasteAsSingleElement": "Utiliser {{shortcut}} pour coller comme un seul élément,\nou coller dans un éditeur de texte existant",
|
||||
"unableToEmbed": "Intégrer cet URL n'est actuellement pas autorisé. Ouvrez un ticket sur GitHub pour demander son ajout à la liste blanche",
|
||||
"unrecognizedLinkFormat": "Le lien que vous avez intégré ne correspond pas au format attendu. Veuillez essayer de coller la chaîne d'intégration fournie par le site source"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"black": "Noir",
|
||||
"white": "Blanc",
|
||||
"red": "Rouge",
|
||||
"pink": "Rose",
|
||||
"grape": "Mauve",
|
||||
"violet": "Violet",
|
||||
"gray": "Gris",
|
||||
"blue": "Bleu",
|
||||
"cyan": "Cyan",
|
||||
"teal": "Turquoise",
|
||||
"green": "Vert",
|
||||
"yellow": "Jaune",
|
||||
"orange": "Orange",
|
||||
"bronze": "Bronze"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
@ -444,10 +462,41 @@
|
||||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
"mostUsedCustomColors": "Couleurs personnalisées les plus fréquemment utilisées",
|
||||
"colors": "Couleurs",
|
||||
"shades": "Nuances",
|
||||
"hexCode": "Code hex",
|
||||
"noShades": "Aucune nuance disponible pour cette couleur"
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "Exporter en image",
|
||||
"button": "Exporter en image",
|
||||
"description": "Exporter les données de la scène comme une image que vous pourrez importer ultérieurement."
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "Sauvegarder sur le disque",
|
||||
"button": "Sauvegarder sur le disque",
|
||||
"description": "Exporter les données de la scène comme un fichier que vous pourrez importer ultérieurement."
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "Excalidraw+",
|
||||
"button": "Exporter vers Excalidraw+",
|
||||
"description": "Enregistrer la scène dans votre espace de travail Excalidraw+."
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "Charger depuis un fichier",
|
||||
"button": "Charger depuis un fichier",
|
||||
"description": "Charger depuis un fichier va <bold>remplacer votre contenu existant</bold>.<br></br>Vous pouvez d'abord sauvegarder votre dessin en utilisant l'une des options ci-dessous."
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "Charger depuis un lien",
|
||||
"button": "Remplacer mon contenu",
|
||||
"description": "Charger un dessin externe va <bold>remplacer votre contenu existant</bold>.<br></br>Vous pouvez d'abord sauvegarder votre dessin en utilisant l'une des options ci-dessous."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@
|
||||
"veryLarge": "Moi grande",
|
||||
"solid": "Sólido",
|
||||
"hachure": "Folleto",
|
||||
"zigzag": "",
|
||||
"zigzag": "Zigzag",
|
||||
"crossHatch": "Raiado transversal",
|
||||
"thin": "Estreito",
|
||||
"bold": "Groso",
|
||||
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Envolver o texto nun contedor",
|
||||
"link": {
|
||||
"edit": "Editar ligazón",
|
||||
"editEmbed": "",
|
||||
"create": "Crear ligazón",
|
||||
"label": "Ligazón"
|
||||
"createEmbed": "",
|
||||
"label": "Ligazón",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Editar liña",
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Modo escuro",
|
||||
"lightMode": "Modo claro",
|
||||
"zenMode": "Modo zen",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "Saír do modo zen",
|
||||
"cancel": "Cancelar",
|
||||
"clear": "Limpar",
|
||||
"remove": "Eliminar",
|
||||
"embed": "",
|
||||
"publishLibrary": "Publicar",
|
||||
"submit": "Enviar",
|
||||
"confirm": "Confirmar"
|
||||
"confirm": "Confirmar",
|
||||
"embeddableInteractionButton": "Faga clic para interactuar"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Isto limpará todo o lenzo. Estás seguro?",
|
||||
@ -196,6 +203,7 @@
|
||||
"imageInsertError": "Non se puido inserir a imaxe. Probe de novo máis tarde...",
|
||||
"fileTooBig": "O ficheiro é demasiado grande. O tamaño máximo permitido é {{maxSize}}.",
|
||||
"svgImageInsertError": "Non se puido inserir como imaxe SVG. O marcado SVG semella inválido.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "SVG inválido.",
|
||||
"cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.",
|
||||
"importLibraryError": "Non se puido cargar a biblioteca",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -224,8 +236,10 @@
|
||||
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
|
||||
"eraser": "Goma de borrar",
|
||||
"frame": "",
|
||||
"embeddable": "Inserir na web",
|
||||
"laser": "Punteiro láser",
|
||||
"hand": "Man (ferramenta de desprazamento)",
|
||||
"extraTools": ""
|
||||
"extraTools": "Máis ferramentas"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Accións do lenzo",
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "Faga clic para iniciar varios puntos, arrastre para unha sola liña",
|
||||
"freeDraw": "Fai clic e arrastra, solta cando acabes",
|
||||
"text": "Consello: tamén podes engadir texto facendo dobre-clic en calquera lugar coa ferramenta de selección",
|
||||
"embeddable": "Faga clic e arrastre para crear un sitio web embebido",
|
||||
"text_selected": "Dobre-clic ou prema ENTER para editar o texto",
|
||||
"text_editing": "Prema Escape ou CtrlOrCmd+ENTER para finalizar a edición",
|
||||
"linearElementMulti": "Faga clic no último punto ou prema Escape ou Enter para rematar",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Prema a tecla enter para engadir texto",
|
||||
"deepBoxSelect": "Manteña pulsado CtrlOrCmd para seleccionar en profundidade e evitar o arrastre",
|
||||
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación",
|
||||
"firefox_clipboard_write": "Esta función pódese activar establecendo a opción \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar as opcións do navegador en Firefox, visita a páxina \"about:config\"."
|
||||
"firefox_clipboard_write": "Esta función pódese activar establecendo a opción \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar as opcións do navegador en Firefox, visita a páxina \"about:config\".",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Non se pode mostrar a vista previa",
|
||||
@ -360,11 +376,11 @@
|
||||
"removeItemsFromLib": "Eliminar os elementos seleccionados da biblioteca"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "Exportar imaxe",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"withBackground": "Fondo",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"darkMode": "Modo escuro",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
@ -373,14 +389,14 @@
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "Exportar a PNG",
|
||||
"exportToSvg": "Exportar a SVG",
|
||||
"copyPngToClipboard": "Copiar PNG ao portapapeis"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Copiar ao portapapeis"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@ -411,24 +427,26 @@
|
||||
"fileSavedToFilename": "Gardado en {filename}",
|
||||
"canvas": "lenzo",
|
||||
"selection": "selección",
|
||||
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un único elemento\nou pega nun editor de texto existente"
|
||||
"pasteAsSingleElement": "Usa {{shortcut}} para pegar como un único elemento\nou pega nun editor de texto existente",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparente",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"black": "Negro",
|
||||
"white": "Branco",
|
||||
"red": "Vermello",
|
||||
"pink": "Rosa",
|
||||
"grape": "Uva",
|
||||
"violet": "Violeta",
|
||||
"gray": "Gris",
|
||||
"blue": "Azul",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"bronze": ""
|
||||
"green": "Verde",
|
||||
"yellow": "Marelo",
|
||||
"orange": "Laranxa",
|
||||
"bronze": "Bronce"
|
||||
},
|
||||
"welcomeScreen": {
|
||||
"app": {
|
||||
@ -445,9 +463,40 @@
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"colors": "Cores",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "Exportar como imaxe",
|
||||
"button": "Exportar como imaxe",
|
||||
"description": ""
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "Gardar no disco",
|
||||
"button": "Gardar no disco",
|
||||
"description": ""
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "Excalidraw+",
|
||||
"button": "Exportar a Excalidraw+",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "Cargar dende arquivo",
|
||||
"button": "Cargar dende arquivo",
|
||||
"description": ""
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "Cargar dende un enlace",
|
||||
"button": "Substituír o meu contido",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@
|
||||
"veryLarge": "גדול מאוד",
|
||||
"solid": "מוצק",
|
||||
"hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה",
|
||||
"zigzag": "",
|
||||
"zigzag": "זיגזג",
|
||||
"crossHatch": "קווים מוצלבים שתי וערב",
|
||||
"thin": "דק",
|
||||
"bold": "מודגש",
|
||||
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "ארוז טקסט במיכל",
|
||||
"link": {
|
||||
"edit": "עריכת קישור",
|
||||
"editEmbed": "ערוך קישור ושבץ",
|
||||
"create": "יצירת קישור",
|
||||
"label": "קישור"
|
||||
"createEmbed": "צור קישור ושבץ",
|
||||
"label": "קישור",
|
||||
"labelEmbed": "קשר ושבץ",
|
||||
"empty": "לא נקבע קישור"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "ערוך קו",
|
||||
@ -124,8 +128,8 @@
|
||||
},
|
||||
"statusPublished": "פורסם",
|
||||
"sidebarLock": "שמור את סרגל הצד פתוח",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"selectAllElementsInFrame": "בחר את כל האלמנטים במסגרת",
|
||||
"removeAllElementsFromFrame": "הסר את כל האלמנטים שבמסגרת",
|
||||
"eyeDropper": ""
|
||||
},
|
||||
"library": {
|
||||
@ -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": "לא ניתן היה להוסיף את תמונת ה-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": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה",
|
||||
"embeddable": "",
|
||||
"text_selected": "לחץ לחיצה כפולה או הקש על אנטר לעריכת הטקסט",
|
||||
"text_editing": "כדי לסיים את העריכה לחץ על מקש Escape או על Ctrl (Cmd במחשבי אפל) ומקש Enter",
|
||||
"linearElementMulti": "הקלק על הנקודה האחרונה או הקש Escape או Enter לסיום",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "הקש Enter כדי להוספת טקסט",
|
||||
"deepBoxSelect": "החזק Ctrl / Cmd לבחירה עמוקה ולמניעת גרירה",
|
||||
"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": "",
|
||||
"noShades": ""
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": "अनुचित SVG",
|
||||
"cannotResolveCollabServer": "कॉलेब सर्वर से कनेक्शन नहीं हो पा रहा. कृपया पृष्ठ को पुनः लाने का प्रयास करे.",
|
||||
"importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "यह आपके चित्रों के <bold>पाठ तत्वों</bold>को खंडित कर सकता हैं",
|
||||
"line3": "हमें आपसे ठोस आग्रह है की आप सेट्टिंग में इस विकल्प का चयन ना करे.<link> इस अनुक्रम </link> का पालन करके इसका पता लगा सकते हैं",
|
||||
"line4": "यदि इस सेटिंग्स को अक्षम करने पर भी पृष्ठ ठीक नहीं दिखता हो तो, हमारे GitHub पर एक <issueLink>मुद्दा प्रस्तुत</issueLink> करे, या हमें <discordLink>डिस्कोर्ड</discordLink> पर लिखित सम्पर्क करें"
|
||||
},
|
||||
"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": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए",
|
||||
"firefox_clipboard_write": "\"dom.events.asyncClipboard.clipboardItem\" फ़्लैग को \"true\" पर सेट करके इस सुविधा को संभवतः सक्षम किया जा सकता है। Firefox में ब्राउज़र फ़्लैग बदलने के लिए, \"about:config\" पृष्ठ पर जाएँ।"
|
||||
"firefox_clipboard_write": "\"dom.events.asyncClipboard.clipboardItem\" फ़्लैग को \"true\" पर सेट करके इस सुविधा को संभवतः सक्षम किया जा सकता है। Firefox में ब्राउज़र फ़्लैग बदलने के लिए, \"about:config\" पृष्ठ पर जाएँ।",
|
||||
"disableSnapping": "स्नैपिंग को निष्क्रिय करने के लिए CtrlOrCmd दबाए रखें"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
|
||||
@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "",
|
||||
"canvas": "",
|
||||
"selection": "",
|
||||
"pasteAsSingleElement": "एक अवयव के रूप में चिपकाने के लिए {{shortcut}} का उपयोग करें,\nया किसी मौजूदा पाठ संपादक में चिपकायें"
|
||||
"pasteAsSingleElement": "एक अवयव के रूप में चिपकाने के लिए {{shortcut}} का उपयोग करें,\nया किसी मौजूदा पाठ संपादक में चिपकायें",
|
||||
"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": "फ़ाइल से लोड करने पर <bold>यह आपके कार्य की जगह लेलेगा </bold><br></br>आपकी ड्रॉइंग निम्न दर्शित विकल्पो में से एक चुनके और उपयोग करके सम्हाल सकते हैं"
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "लिंक से लोड करें:",
|
||||
"button": "इस जगह प्रतिस्थापित करे",
|
||||
"description": "बाहर का चित्र लोड करने पर <bold>यह आपके कार्य की जगह लेलेगा </bold><br></br>आप आपकी ड्रॉइंग पहले निम्न दर्शित विकल्पो में से एक चुनके और उपयोग करके सम्हाल सकते हों."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "Hivatkozás szerkesztése",
|
||||
"editEmbed": "",
|
||||
"create": "Hivatkozás létrehozása",
|
||||
"label": "Hivatkozás"
|
||||
"createEmbed": "",
|
||||
"label": "Hivatkozás",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Sötét mód",
|
||||
"lightMode": "Világos mód",
|
||||
"zenMode": "Letisztult mód",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "Kilépés a letisztult módból",
|
||||
"cancel": "Mégsem",
|
||||
"clear": "Kiűrítés",
|
||||
"remove": "Eltávolítás",
|
||||
"embed": "",
|
||||
"publishLibrary": "Közzététel",
|
||||
"submit": "Elküldés",
|
||||
"confirm": "Megerősítés"
|
||||
"confirm": "Megerősítés",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Ez a művelet törli a vászont. Biztos benne?",
|
||||
@ -196,6 +203,7 @@
|
||||
"imageInsertError": "Nem sikerült beszúrni a képet. Próbáld újra később...",
|
||||
"fileTooBig": "A fájl túl nagy. A megengedett maximális méret {{maxSize}}.",
|
||||
"svgImageInsertError": "Nem sikerült beszúrni az SVG-képet. Az SVG szintaktika érvénytelennek tűnik.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "Érvénytelen SVG.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -224,6 +236,8 @@
|
||||
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
|
||||
"eraser": "",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
},
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
|
||||
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
|
||||
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
|
||||
"embeddable": "",
|
||||
"text_selected": "Kattints duplán, vagy nyomj entert a szöveg szerkesztéséhez",
|
||||
"text_editing": "Nyomjd meg az Escape vagy a Ctrl/Cmd+ENTER billentyűkombinációt a szerkesztés befejezéséhez",
|
||||
"linearElementMulti": "Kattints a következő ív pozíciójára, vagy fejezd be a nyilat az Escape vagy Enter megnyomásával",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
|
||||
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Előnézet nem jeleníthető meg",
|
||||
@ -360,7 +376,7 @@
|
||||
"removeItemsFromLib": "A kiválasztott elemek eltávolítása a könyvtárból"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "Kép exportálása",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "Mentve mint {filename}",
|
||||
"canvas": "rajzvászon",
|
||||
"selection": "kijelölés",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Átlátszó",
|
||||
@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Bungkus teks dalam kontainer",
|
||||
"link": {
|
||||
"edit": "Edit tautan",
|
||||
"editEmbed": "",
|
||||
"create": "Buat tautan",
|
||||
"label": "Tautan"
|
||||
"createEmbed": "",
|
||||
"label": "Tautan",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Edit tautan",
|
||||
@ -124,9 +128,9 @@
|
||||
},
|
||||
"statusPublished": "Telah terbit",
|
||||
"sidebarLock": "Biarkan sidebar tetap terbuka",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"eyeDropper": ""
|
||||
"selectAllElementsInFrame": "Pilih semua elemen di bingkai",
|
||||
"removeAllElementsFromFrame": "Hapus semua elemen dari bingkai",
|
||||
"eyeDropper": "Ambil warna dari kanvas"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Belum ada item yang ditambahkan...",
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Mode gelap",
|
||||
"lightMode": "Mode terang",
|
||||
"zenMode": "Mode zen",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "Keluar dari mode zen",
|
||||
"cancel": "Batal",
|
||||
"clear": "Hapus",
|
||||
"remove": "Hapus",
|
||||
"embed": "",
|
||||
"publishLibrary": "Terbitkan",
|
||||
"submit": "Kirimkan",
|
||||
"confirm": "Konfirmasi"
|
||||
"confirm": "Konfirmasi",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Ini akan menghapus semua yang ada dikanvas. Apakah kamu yakin ?",
|
||||
@ -196,6 +203,7 @@
|
||||
"imageInsertError": "Tidak dapat menyisipkan gambar. Coba lagi nanti...",
|
||||
"fileTooBig": "File terlalu besar. Ukuran maksimum yang dibolehkan {{maxSize}}.",
|
||||
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "SVG tidak valid.",
|
||||
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
|
||||
"importLibraryError": "Tidak dapat memuat pustaka",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "Ini dapat membuat <bold>Elemen Teks</bold> dalam gambar mu.",
|
||||
"line3": "Kami sangat menyarankan mematikan pengaturan ini. Anda dapat mengikuti <link>langkah-langkah ini</link> untuk melakukannya.",
|
||||
"line4": "Jika mematikan pengaturan ini tidak membenarkan tampilan elemen teks, mohon buka\n<issueLink>isu</issueLink> di GitHub kami, atau chat kami di <discordLink>Discord</discordLink>"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -223,9 +235,11 @@
|
||||
"penMode": "Mode pena - mencegah sentuhan",
|
||||
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
|
||||
"eraser": "Penghapus",
|
||||
"frame": "",
|
||||
"frame": "Alat bingkai",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "Tangan (alat panning)",
|
||||
"extraTools": ""
|
||||
"extraTools": "Alat-alat lain"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Opsi Kanvas",
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "Klik untuk memulai banyak poin, seret untuk satu baris",
|
||||
"freeDraw": "Klik dan seret, lepaskan jika Anda selesai",
|
||||
"text": "Tip: Anda juga dapat menambahkan teks dengan klik ganda di mana saja dengan alat pemilihan",
|
||||
"embeddable": "",
|
||||
"text_selected": "Klik ganda atau tekan ENTER untuk edit teks",
|
||||
"text_editing": "Tekan Escape atau CtrlAtauCmd+ENTER untuk selesai mengedit",
|
||||
"linearElementMulti": "Klik pada titik akhir atau tekan Escape atau Enter untuk menyelesaikan",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Tekan enter untuk tambahkan teks",
|
||||
"deepBoxSelect": "Tekan Ctrl atau Cmd untuk memilih yang di dalam, dan mencegah penggeseran",
|
||||
"eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus",
|
||||
"firefox_clipboard_write": "Fitur ini dapat diaktifkan melalui pengaturan flag \"dom.events.asyncClipboard.clipboardItem\" ke \"true\". Untuk mengganti flag di Firefox, pergi ke laman \"about:config\"."
|
||||
"firefox_clipboard_write": "Fitur ini dapat diaktifkan melalui pengaturan flag \"dom.events.asyncClipboard.clipboardItem\" ke \"true\". Untuk mengganti flag di Firefox, pergi ke laman \"about:config\".",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Tidak dapat menampilkan pratinjau",
|
||||
@ -360,27 +376,27 @@
|
||||
"removeItemsFromLib": "Hapus item yang dipilih dari pustaka"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "Ekspor gambar",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
"withBackground": "Latar",
|
||||
"onlySelected": "Hanya yang dipilih",
|
||||
"darkMode": "Mode gelap",
|
||||
"embedScene": "Sematkan pemandangan",
|
||||
"scale": "Skala",
|
||||
"padding": "Lapisan"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
"embedScene": "Data pemandangan akan disimpan dalam file PNG/SVG yang diekspor sehingga pemandangan itu dapat dipulihkan darinya.\nAkan membesarkan ukuran file yang diekspor."
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "Ekspor ke PNG",
|
||||
"exportToSvg": "Ekspor ke SVG",
|
||||
"copyPngToClipboard": "Salin PNG ke papan klip"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Salin ke papan klip"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "Disimpan ke {filename}",
|
||||
"canvas": "kanvas",
|
||||
"selection": "pilihan",
|
||||
"pasteAsSingleElement": "Gunakan {{shortcut}} untuk menempelkan sebagai satu elemen,\natau tempelkan ke teks editor yang ada"
|
||||
"pasteAsSingleElement": "Gunakan {{shortcut}} untuk menempelkan sebagai satu elemen,\natau tempelkan ke teks editor yang ada",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparan",
|
||||
@ -449,5 +467,36 @@
|
||||
"shades": "Nuansa",
|
||||
"hexCode": "Kode hexa",
|
||||
"noShades": "Tidak ada nuansa untuk warna ini"
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "Ekspor sebagai gambar",
|
||||
"button": "Ekspor sebagai gambar",
|
||||
"description": "Ekspor data pemandangan sebagai gambar yang dapat anda impor nanti."
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "Simpan ke disk",
|
||||
"button": "Simpan ke disk",
|
||||
"description": "Ekspor data pemandangan ke file yang dapat Anda dapat impor nanti."
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "Excalidraw+",
|
||||
"button": "Ekspor ke Excalidraw+",
|
||||
"description": "Simpan pemandangan ke ruang kerja Excalidraw+ Anda."
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "Muat dari file",
|
||||
"button": "Muat dari file",
|
||||
"description": "Memuat dari file yang akan <bold>menggantikan konten Anda sekarang</bold>.<br></br>Anda dapat mencadangkan gambar anda dulu menggunakan opsi-opsi ini."
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "Muat dari link",
|
||||
"button": "Ganti konten saya",
|
||||
"description": "Memuat dari file yang akan <bold>menggantikan konten Anda sekarang</bold>.<br></br>Anda dapat mencadangkan gambar anda dulu menggunakan opsi-opsi ini."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,8 +109,12 @@
|
||||
"createContainerFromText": "Avvolgi il testo in un container",
|
||||
"link": {
|
||||
"edit": "Modifica link",
|
||||
"editEmbed": "Modifica collegamento e incorpora",
|
||||
"create": "Crea link",
|
||||
"label": "Link"
|
||||
"createEmbed": "Crea collegamento e incorpora",
|
||||
"label": "Link",
|
||||
"labelEmbed": "Collega & incorpora",
|
||||
"empty": "Nessun collegamento impostato"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Modifica linea",
|
||||
@ -124,9 +128,9 @@
|
||||
},
|
||||
"statusPublished": "Pubblicato",
|
||||
"sidebarLock": "Mantieni aperta la barra laterale",
|
||||
"selectAllElementsInFrame": "",
|
||||
"removeAllElementsFromFrame": "",
|
||||
"eyeDropper": ""
|
||||
"selectAllElementsInFrame": "Seleziona tutti gli elementi nel riquadro",
|
||||
"removeAllElementsFromFrame": "Rimuovi tutti gli elementi dal riquadro",
|
||||
"eyeDropper": "Scegli il colore della tela"
|
||||
},
|
||||
"library": {
|
||||
"noItems": "Nessun elemento ancora aggiunto...",
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Tema scuro",
|
||||
"lightMode": "Tema chiaro",
|
||||
"zenMode": "Modalità Zen",
|
||||
"objectsSnapMode": "Aggancia agli oggetti",
|
||||
"exitZenMode": "Uscire dalla modalità zen",
|
||||
"cancel": "Annulla",
|
||||
"clear": "Cancella",
|
||||
"remove": "Rimuovi",
|
||||
"embed": "Attiva/disattiva incorporamento",
|
||||
"publishLibrary": "Pubblica",
|
||||
"submit": "Invia",
|
||||
"confirm": "Conferma"
|
||||
"confirm": "Conferma",
|
||||
"embeddableInteractionButton": "Clicca per interagire"
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "Questa azione cancellerà l'intera tela. Sei sicuro?",
|
||||
@ -196,6 +203,7 @@
|
||||
"imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...",
|
||||
"fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.",
|
||||
"svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "SVG non valido.",
|
||||
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.",
|
||||
"importLibraryError": "Impossibile caricare la libreria",
|
||||
@ -206,6 +214,10 @@
|
||||
"line2": "Ciò potrebbe causare la rottura degli <bold>Elementi di testo</bold> nei tuoi disegni.",
|
||||
"line3": "Consigliamo vivamente di disabilitare questa impostazione. Puoi seguire <link>questi passaggi</link> su come farlo.",
|
||||
"line4": "Se la disattivazione di questa impostazione non risolve la visualizzazione degli elementi di testo, apri un <issueLink>problema</issueLink> sul nostro GitHub o scrivici su <discordLink>Discord</discordLink>"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "Gli elementi incorporabili non possono essere aggiunti alla libreria.",
|
||||
"image": "Il supporto per l'aggiunta d'immagini alla libreria verrà aggiunto a breve!"
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -223,7 +235,9 @@
|
||||
"penMode": "Modalità penna - previene il tocco",
|
||||
"link": "Aggiungi/ aggiorna il link per una forma selezionata",
|
||||
"eraser": "Gomma",
|
||||
"frame": "",
|
||||
"frame": "Strumento riquadro",
|
||||
"embeddable": "Incorporamento Web",
|
||||
"laser": "Puntatore laser",
|
||||
"hand": "Mano (strumento di panoramica)",
|
||||
"extraTools": "Altri strumenti"
|
||||
},
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "Clicca per iniziare una linea in più punti, trascina per singola linea",
|
||||
"freeDraw": "Clicca e trascina, rilascia quando avrai finito",
|
||||
"text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione",
|
||||
"embeddable": "Fare click e trascina per creare un incorporamento web",
|
||||
"text_selected": "Fai doppio click o premi INVIO per modificare il testo",
|
||||
"text_editing": "Premi ESC o CtrlOCmd+INVIO per completare le modifiche",
|
||||
"linearElementMulti": "Clicca sull'ultimo punto o premi Esc o Invio per finire",
|
||||
@ -252,7 +267,8 @@
|
||||
"bindTextToElement": "Premi invio per aggiungere il testo",
|
||||
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento",
|
||||
"eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione",
|
||||
"firefox_clipboard_write": "Questa funzione può essere abilitata impostando il flag \"dom.events.asyncClipboard.clipboardItem\" su \"true\". Per modificare i flag del browser in Firefox, visitare la pagina \"about:config\"."
|
||||
"firefox_clipboard_write": "Questa funzione può essere abilitata impostando il flag \"dom.events.asyncClipboard.clipboardItem\" su \"true\". Per modificare i flag del browser in Firefox, visitare la pagina \"about:config\".",
|
||||
"disableSnapping": "Tieni premuto Ctrl o Cmd per disabilitare lo snap"
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "Impossibile visualizzare l'anteprima",
|
||||
@ -367,7 +383,7 @@
|
||||
"darkMode": "Tema scuro",
|
||||
"embedScene": "Includi scena",
|
||||
"scale": "Scala",
|
||||
"padding": ""
|
||||
"padding": "Rientro"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": "I dati della scena saranno salvati nel file PNG/SVG esportato in modo che la scena possa essere ripristinata da esso.\nQuesto aumenterà la dimensione del file esportato."
|
||||
@ -411,7 +427,9 @@
|
||||
"fileSavedToFilename": "Salvato in {filename}",
|
||||
"canvas": "tela",
|
||||
"selection": "selezione",
|
||||
"pasteAsSingleElement": "Usa {{shortcut}} per incollare come un singolo elemento,\no incollare in un editor di testo esistente"
|
||||
"pasteAsSingleElement": "Usa {{shortcut}} per incollare come un singolo elemento,\no incollare in un editor di testo esistente",
|
||||
"unableToEmbed": "Incorporare questo url non è permesso. Crea una issue su GitHub per richiedere che l'url sia autorizzato",
|
||||
"unrecognizedLinkFormat": "Il link che hai incorporato non corrisponde al formato previsto. Prova a incollare la stringa 'embed' fornita dal sito di origine"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Trasparente",
|
||||
@ -449,5 +467,36 @@
|
||||
"shades": "Sfumature",
|
||||
"hexCode": "Codice esadecimale",
|
||||
"noShades": "Nessuna sfumatura disponibile per questo colore"
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "Esporta come immagine",
|
||||
"button": "Esporta come immagine",
|
||||
"description": "Esporta i dati della scena come immagine, che potrai importare in seguito."
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "Salva su disco",
|
||||
"button": "Salva su disco",
|
||||
"description": "Esporta i dati della scena su file, che potrai importare in seguito."
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "Excalidraw+",
|
||||
"button": "Esporta su Excalidraw+",
|
||||
"description": "Salva la scena sul tuo spazio di lavoro Excalidraw+."
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "Carica da file",
|
||||
"button": "Carica da file",
|
||||
"description": "Il caricamento da file sostituirà <bold>il contenuto esistente</bold>.<br></br>Puoi salvare il tuo disegno prima usando una delle opzioni qui sotto."
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "Carica da link",
|
||||
"button": "Sostituisci il mio contenuto",
|
||||
"description": "Il caricamento da file sostituirà <bold>il contenuto esistente</bold>.<br></br>Puoi salvare il tuo disegno prima usando una delle opzioni qui sotto."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": "Zenモード",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "集中モードをやめる",
|
||||
"cancel": "キャンセル",
|
||||
"clear": "消去",
|
||||
"remove": "削除",
|
||||
"embed": "埋め込みの切り替え",
|
||||
"publishLibrary": "公開",
|
||||
"submit": "送信",
|
||||
"confirm": "確認"
|
||||
"confirm": "確認",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?",
|
||||
@ -196,16 +203,21 @@
|
||||
"imageInsertError": "画像を挿入できませんでした。後でもう一度お試しください...",
|
||||
"fileTooBig": "ファイルが大きすぎます。許可される最大サイズは {{maxSize}} です。",
|
||||
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "無効なSVGです。",
|
||||
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
|
||||
"importLibraryError": "ライブラリを読み込めませんでした。",
|
||||
"collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
|
||||
"collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
|
||||
"brave_measure_text_error": {
|
||||
"line1": "",
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
"line1": "<bold>Aggressly Block Fingerprinting</bold> の設定が有効なBraveブラウザを使用しているようです。",
|
||||
"line2": "これにより、図面の <bold>テキスト要素</bold> が壊れる可能性があります。",
|
||||
"line3": "この設定を無効にすることを強く推奨します。 <link>設定手順</link> をこちらから確認できます。",
|
||||
"line4": "この設定を無効にすると、テキスト要素の表示が修正されません。 GitHub で <issueLink>Issue</issueLink> を開くか、 <discordLink>Discord</discordLink> にご記入ください"
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
@ -223,9 +235,11 @@
|
||||
"penMode": "ペンモード - タッチ防止",
|
||||
"link": "選択した図形のリンクを追加/更新",
|
||||
"eraser": "消しゴム",
|
||||
"frame": "",
|
||||
"frame": "フレームツール",
|
||||
"embeddable": "Web埋め込み",
|
||||
"laser": "",
|
||||
"hand": "手 (パンニングツール)",
|
||||
"extraTools": ""
|
||||
"extraTools": "その他のツール"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "キャンバス操作",
|
||||
@ -237,6 +251,7 @@
|
||||
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
|
||||
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
||||
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
||||
"embeddable": "",
|
||||
"text_selected": "テキストを編集するには、ダブルクリックまたはEnterキーを押します",
|
||||
"text_editing": "Esc キーまたは CtrlOrCmd+ENTER キーを押して編集を終了します",
|
||||
"linearElementMulti": "最後のポイントをクリックするか、エスケープまたは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": "プレビューを表示できません",
|
||||
@ -360,27 +376,27 @@
|
||||
"removeItemsFromLib": "選択したアイテムをライブラリから削除"
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "画像をエクスポート",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"withBackground": "背景",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"darkMode": "ダークモード",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
"scale": "スケール",
|
||||
"padding": "余白"
|
||||
},
|
||||
"tooltip": {
|
||||
"embedScene": ""
|
||||
},
|
||||
"title": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG にエクスポート",
|
||||
"exportToSvg": "SVG にエクスポート",
|
||||
"copyPngToClipboard": "クリップボードにPNGをコピー"
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "クリップボードにコピー"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@ -411,24 +427,26 @@
|
||||
"fileSavedToFilename": "{filename} に保存しました",
|
||||
"canvas": "キャンバス",
|
||||
"selection": "選択",
|
||||
"pasteAsSingleElement": "{{shortcut}} を使用して単一の要素として貼り付けるか、\n既存のテキストエディタに貼り付け"
|
||||
"pasteAsSingleElement": "{{shortcut}} を使用して単一の要素として貼り付けるか、\n既存のテキストエディタに貼り付け",
|
||||
"unableToEmbed": "",
|
||||
"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": {
|
||||
@ -444,10 +462,41 @@
|
||||
}
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"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": ""
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "リンクからロード",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
{
|
||||
"labels": {
|
||||
"paste": "",
|
||||
"pasteAsPlaintext": "",
|
||||
"pasteCharts": "",
|
||||
"paste": "Qoyıw",
|
||||
"pasteAsPlaintext": "Ápiwayı tekst retinde qoyıw",
|
||||
"pasteCharts": "Diagrammalardı qoyıw",
|
||||
"selectAll": "Barlıǵın tańlaw",
|
||||
"multiSelect": "",
|
||||
"moveCanvas": "",
|
||||
"cut": "",
|
||||
"cut": "Qıyıw",
|
||||
"copy": "Kóshirip alıw",
|
||||
"copyAsPng": "",
|
||||
"copyAsPng": "Almasıw buferine PNG retinde kóshirip alıw",
|
||||
"copyAsSvg": "",
|
||||
"copyText": "",
|
||||
"bringForward": "",
|
||||
@ -18,8 +18,8 @@
|
||||
"delete": "Óshiriw",
|
||||
"copyStyles": "",
|
||||
"pasteStyles": "",
|
||||
"stroke": "",
|
||||
"background": "",
|
||||
"stroke": "Jiyek",
|
||||
"background": "Fon",
|
||||
"fill": "",
|
||||
"strokeWidth": "",
|
||||
"strokeStyle": "",
|
||||
@ -29,31 +29,31 @@
|
||||
"sloppiness": "",
|
||||
"opacity": "",
|
||||
"textAlign": "",
|
||||
"edges": "",
|
||||
"edges": "Qırlar",
|
||||
"sharp": "",
|
||||
"round": "",
|
||||
"arrowheads": "",
|
||||
"arrowhead_none": "",
|
||||
"arrowhead_arrow": "",
|
||||
"arrowhead_arrow": "Jebe",
|
||||
"arrowhead_bar": "",
|
||||
"arrowhead_dot": "",
|
||||
"arrowhead_dot": "Noqat",
|
||||
"arrowhead_triangle": "",
|
||||
"fontSize": "Shrift ólshemi",
|
||||
"fontFamily": "",
|
||||
"addWatermark": "",
|
||||
"handDrawn": "",
|
||||
"normal": "",
|
||||
"code": "",
|
||||
"code": "Kod",
|
||||
"small": "",
|
||||
"medium": "",
|
||||
"large": "",
|
||||
"veryLarge": "",
|
||||
"large": "Úlken",
|
||||
"veryLarge": "Júdá úlken",
|
||||
"solid": "",
|
||||
"hachure": "",
|
||||
"zigzag": "",
|
||||
"zigzag": "Zigzag",
|
||||
"crossHatch": "",
|
||||
"thin": "",
|
||||
"bold": "",
|
||||
"thin": "Jińishke",
|
||||
"bold": "Qalıń",
|
||||
"left": "",
|
||||
"center": "",
|
||||
"right": "",
|
||||
@ -71,19 +71,19 @@
|
||||
"language": "Til",
|
||||
"liveCollaboration": "",
|
||||
"duplicateSelection": "Nusqa",
|
||||
"untitled": "",
|
||||
"untitled": "Atamasız",
|
||||
"name": "Ataması",
|
||||
"yourName": "Atıńız",
|
||||
"madeWithExcalidraw": "",
|
||||
"madeWithExcalidraw": "Excalidraw járdeminde islengen",
|
||||
"group": "",
|
||||
"ungroup": "",
|
||||
"collaborators": "",
|
||||
"collaborators": "Qatnasıwshılar",
|
||||
"showGrid": "",
|
||||
"addToLibrary": "Kitapxanaǵa qosıw",
|
||||
"removeFromLibrary": "Kitapxanadan alıp taslaw",
|
||||
"libraryLoadingMessage": "Kitapxana júklenbekte…",
|
||||
"libraries": "Kitapxanalardı kóriw",
|
||||
"loadingScene": "",
|
||||
"loadingScene": "Saxna júklenbekte…",
|
||||
"align": "",
|
||||
"alignTop": "",
|
||||
"alignBottom": "",
|
||||
@ -99,22 +99,26 @@
|
||||
"share": "Bólisiw",
|
||||
"showStroke": "",
|
||||
"showBackground": "",
|
||||
"toggleTheme": "",
|
||||
"personalLib": "",
|
||||
"excalidrawLib": "",
|
||||
"decreaseFontSize": "",
|
||||
"increaseFontSize": "",
|
||||
"toggleTheme": "Temanı ózgertiw",
|
||||
"personalLib": "Jeke kitapxana",
|
||||
"excalidrawLib": "Excalidraw kitapxanası",
|
||||
"decreaseFontSize": "Shrift ólshemin kishireytiw",
|
||||
"increaseFontSize": "Shrift ólshemin úlkeytiw",
|
||||
"unbindText": "",
|
||||
"bindText": "",
|
||||
"createContainerFromText": "",
|
||||
"link": {
|
||||
"edit": "Siltemeni ózgertiw",
|
||||
"editEmbed": "",
|
||||
"create": "Siltemeni jaratıw",
|
||||
"label": "Silteme"
|
||||
"createEmbed": "",
|
||||
"label": "Silteme",
|
||||
"labelEmbed": "",
|
||||
"empty": ""
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "",
|
||||
"exit": ""
|
||||
"edit": "Qatardı ózgertiw",
|
||||
"exit": "Qatardı ózgertiw redaktorınan shıǵıw"
|
||||
},
|
||||
"elementLock": {
|
||||
"lock": "Qulıplaw",
|
||||
@ -136,11 +140,11 @@
|
||||
"buttons": {
|
||||
"clearReset": "",
|
||||
"exportJSON": "",
|
||||
"exportImage": "",
|
||||
"export": "",
|
||||
"exportImage": "Súwretti eksportlaw...",
|
||||
"export": "Retinde saqlaw...",
|
||||
"copyToClipboard": "Almasıw buferine kóshirip alındı",
|
||||
"save": "",
|
||||
"saveAs": "",
|
||||
"save": "Ámeldegi faylǵa saqlaw",
|
||||
"saveAs": "Retinde saqlaw",
|
||||
"load": "Ashıw",
|
||||
"getShareableLink": "",
|
||||
"close": "Jabıw",
|
||||
@ -160,13 +164,16 @@
|
||||
"darkMode": "Qarańǵı tema",
|
||||
"lightMode": "Jaqtı tema",
|
||||
"zenMode": "",
|
||||
"objectsSnapMode": "",
|
||||
"exitZenMode": "",
|
||||
"cancel": "Biykarlaw",
|
||||
"clear": "Tazalaw",
|
||||
"remove": "Óshiriw",
|
||||
"embed": "",
|
||||
"publishLibrary": "",
|
||||
"submit": "Jiberiw",
|
||||
"confirm": "Tastıyıqlaw"
|
||||
"confirm": "Tastıyıqlaw",
|
||||
"embeddableInteractionButton": ""
|
||||
},
|
||||
"alerts": {
|
||||
"clearReset": "",
|
||||
@ -196,6 +203,7 @@
|
||||
"imageInsertError": "",
|
||||
"fileTooBig": "",
|
||||
"svgImageInsertError": "",
|
||||
"failedToFetchImage": "",
|
||||
"invalidSVGString": "Jaramsız SVG.",
|
||||
"cannotResolveCollabServer": "",
|
||||
"importLibraryError": "Kitapxananı júklew ámelge aspadı",
|
||||
@ -206,12 +214,16 @@
|
||||
"line2": "",
|
||||
"line3": "",
|
||||
"line4": ""
|
||||
},
|
||||
"libraryElementTypeError": {
|
||||
"embeddable": "",
|
||||
"image": ""
|
||||
}
|
||||
},
|
||||
"toolBar": {
|
||||
"selection": "",
|
||||
"image": "Súwret qoyıw",
|
||||
"rectangle": "",
|
||||
"rectangle": "Tórt múyeshlik",
|
||||
"diamond": "",
|
||||
"ellipse": "",
|
||||
"arrow": "",
|
||||
@ -224,19 +236,22 @@
|
||||
"link": "",
|
||||
"eraser": "Óshirgish",
|
||||
"frame": "",
|
||||
"embeddable": "",
|
||||
"laser": "",
|
||||
"hand": "",
|
||||
"extraTools": ""
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "",
|
||||
"selectedShapeActions": "",
|
||||
"shapes": ""
|
||||
"shapes": "Figuralar"
|
||||
},
|
||||
"hints": {
|
||||
"canvasPanning": "",
|
||||
"linearElement": "",
|
||||
"freeDraw": "",
|
||||
"text": "",
|
||||
"embeddable": "",
|
||||
"text_selected": "",
|
||||
"text_editing": "",
|
||||
"linearElementMulti": "",
|
||||
@ -249,10 +264,11 @@
|
||||
"lineEditor_nothingSelected": "",
|
||||
"placeImage": "",
|
||||
"publishLibrary": "",
|
||||
"bindTextToElement": "",
|
||||
"bindTextToElement": "Tekst qosıw ushın Enter túymesin basıń",
|
||||
"deepBoxSelect": "",
|
||||
"eraserRevert": "",
|
||||
"firefox_clipboard_write": ""
|
||||
"firefox_clipboard_write": "",
|
||||
"disableSnapping": ""
|
||||
},
|
||||
"canvasError": {
|
||||
"cannotShowPreview": "",
|
||||
@ -281,18 +297,18 @@
|
||||
"title": "Qátelik"
|
||||
},
|
||||
"exportDialog": {
|
||||
"disk_title": "",
|
||||
"disk_title": "Diskke saqlaw",
|
||||
"disk_details": "",
|
||||
"disk_button": "",
|
||||
"disk_button": "Faylǵa saqlaw",
|
||||
"link_title": "",
|
||||
"link_details": "",
|
||||
"link_button": "",
|
||||
"link_button": "Siltemege eksportlaw",
|
||||
"excalidrawplus_description": "",
|
||||
"excalidrawplus_button": "Eksportlaw",
|
||||
"excalidrawplus_exportError": ""
|
||||
},
|
||||
"helpDialog": {
|
||||
"blog": "",
|
||||
"blog": "Biziń blogtı oqıń",
|
||||
"click": "basıw",
|
||||
"deepSelect": "",
|
||||
"deepBoxSelect": "",
|
||||
@ -301,7 +317,7 @@
|
||||
"documentation": "Hújjetshilik",
|
||||
"doubleClick": "",
|
||||
"drag": "",
|
||||
"editor": "",
|
||||
"editor": "Redaktor",
|
||||
"editLineArrowPoints": "",
|
||||
"editText": "",
|
||||
"github": "",
|
||||
@ -333,7 +349,7 @@
|
||||
"libraryDesc": "",
|
||||
"website": "Veb-sayt",
|
||||
"placeholder": {
|
||||
"authorName": "",
|
||||
"authorName": "Atıńız yamasa paydalanıwshı atı",
|
||||
"libraryName": "",
|
||||
"libraryDesc": "",
|
||||
"githubHandle": "",
|
||||
@ -356,15 +372,15 @@
|
||||
"content": ""
|
||||
},
|
||||
"confirmDialog": {
|
||||
"resetLibrary": "",
|
||||
"resetLibrary": "Kitapxananı qayta ornatıw",
|
||||
"removeItemsFromLib": ""
|
||||
},
|
||||
"imageExportDialog": {
|
||||
"header": "",
|
||||
"header": "Súwretti eksportlaw",
|
||||
"label": {
|
||||
"withBackground": "",
|
||||
"withBackground": "Fon",
|
||||
"onlySelected": "",
|
||||
"darkMode": "",
|
||||
"darkMode": "Qarańǵı tema",
|
||||
"embedScene": "",
|
||||
"scale": "",
|
||||
"padding": ""
|
||||
@ -378,9 +394,9 @@
|
||||
"copyPngToClipboard": ""
|
||||
},
|
||||
"button": {
|
||||
"exportToPng": "",
|
||||
"exportToSvg": "",
|
||||
"copyPngToClipboard": ""
|
||||
"exportToPng": "PNG",
|
||||
"exportToSvg": "SVG",
|
||||
"copyPngToClipboard": "Almasıw buferine kóshirip alıw"
|
||||
}
|
||||
},
|
||||
"encrypted": {
|
||||
@ -389,8 +405,8 @@
|
||||
},
|
||||
"stats": {
|
||||
"angle": "",
|
||||
"element": "",
|
||||
"elements": "",
|
||||
"element": "Element",
|
||||
"elements": "Elementler",
|
||||
"height": "",
|
||||
"scene": "Saxna",
|
||||
"selected": "Tańlandı",
|
||||
@ -403,31 +419,33 @@
|
||||
"width": ""
|
||||
},
|
||||
"toast": {
|
||||
"addedToLibrary": "",
|
||||
"addedToLibrary": "Kitapxanaǵa qosıldı",
|
||||
"copyStyles": "",
|
||||
"copyToClipboard": "",
|
||||
"copyToClipboard": "Almasıw buferine kóshirip alındı.",
|
||||
"copyToClipboardAsPng": "",
|
||||
"fileSaved": "Fayl saqlandı.",
|
||||
"fileSavedToFilename": "{filename} saqlandı",
|
||||
"canvas": "",
|
||||
"selection": "",
|
||||
"pasteAsSingleElement": ""
|
||||
"pasteAsSingleElement": "",
|
||||
"unableToEmbed": "",
|
||||
"unrecognizedLinkFormat": ""
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "",
|
||||
"black": "",
|
||||
"white": "",
|
||||
"red": "",
|
||||
"pink": "",
|
||||
"black": "Qara",
|
||||
"white": "Aq",
|
||||
"red": "Qızıl",
|
||||
"pink": "Qızǵılt",
|
||||
"grape": "",
|
||||
"violet": "",
|
||||
"violet": "Qızǵılt kók",
|
||||
"gray": "",
|
||||
"blue": "",
|
||||
"cyan": "",
|
||||
"teal": "",
|
||||
"green": "",
|
||||
"yellow": "",
|
||||
"orange": "",
|
||||
"blue": "Kók",
|
||||
"cyan": "Kók aspan",
|
||||
"teal": "Piruza",
|
||||
"green": "Jasıl",
|
||||
"yellow": "Sarı",
|
||||
"orange": "Qızǵılt sarı",
|
||||
"bronze": ""
|
||||
},
|
||||
"welcomeScreen": {
|
||||
@ -445,9 +463,40 @@
|
||||
},
|
||||
"colorPicker": {
|
||||
"mostUsedCustomColors": "",
|
||||
"colors": "",
|
||||
"colors": "Reńler",
|
||||
"shades": "",
|
||||
"hexCode": "",
|
||||
"noShades": ""
|
||||
},
|
||||
"overwriteConfirm": {
|
||||
"action": {
|
||||
"exportToImage": {
|
||||
"title": "Súwret retinde eksportlaw",
|
||||
"button": "Súwret retinde eksportlaw",
|
||||
"description": ""
|
||||
},
|
||||
"saveToDisk": {
|
||||
"title": "Diskke saqlaw",
|
||||
"button": "Diskke saqlaw",
|
||||
"description": ""
|
||||
},
|
||||
"excalidrawPlus": {
|
||||
"title": "Excalidraw+",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"modal": {
|
||||
"loadFromFile": {
|
||||
"title": "Fayldan júklew",
|
||||
"button": "Fayldan júklew",
|
||||
"description": ""
|
||||
},
|
||||
"shareableLink": {
|
||||
"title": "Siltemeden júklew",
|
||||
"button": "",
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user