Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c34b3f48a | ||
|
|
683b80ad2b | ||
|
|
d636abff79 | ||
|
|
47d8fa542c | ||
|
|
34cf71b0f4 | ||
|
|
ceb255e8ee | ||
|
|
ae5b9a4ffd | ||
|
|
3d4ff59f40 | ||
|
|
7b00089314 | ||
|
|
af6b81df40 | ||
|
|
02cc8440c4 | ||
|
|
6363492cee | ||
|
|
900b317bf3 | ||
|
|
68179356e6 | ||
|
|
3ed15e95da | ||
|
|
798e1fd858 | ||
|
|
f66c93633c | ||
|
|
a30e46b756 | ||
|
|
cee00767df | ||
|
|
71ba0a3f26 | ||
|
|
9bcd0b69dc | ||
|
|
864c0b3ea8 | ||
|
|
a9a6f8eafb | ||
|
|
afed893419 | ||
|
|
3c96943db3 | ||
|
|
9006caff39 | ||
|
|
ce7a847668 | ||
|
|
b1037b342d | ||
|
|
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 |
@@ -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">
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||||
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://www.npmjs.com/package/@excalidraw/excalidraw">
|
||||||
|
<img alt="npm downloads/month" src="https://img.shields.io/npm/dm/@excalidraw/excalidraw" />
|
||||||
|
</a>
|
||||||
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||||
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ Open the `Menu` in the below playground and you will see the `custom footer` ren
|
|||||||
```jsx live noInline
|
```jsx live noInline
|
||||||
const MobileFooter = ({}) => {
|
const MobileFooter = ({}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
if (device.isMobile) {
|
if (device.editor.isMobile) {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ Open the `main menu` in the below example to view the footer.
|
|||||||
```jsx live noInline
|
```jsx live noInline
|
||||||
const MobileFooter = ({}) => {
|
const MobileFooter = ({}) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
if (device.isMobile) {
|
if (device.editor.isMobile) {
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<button
|
<button
|
||||||
@@ -335,7 +335,6 @@ The `device` has the following `attributes`
|
|||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `isSmScreen` | `boolean` | Set to `true` when the device small screen is small (Width < `640px` ) |
|
|
||||||
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
|
||||||
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
|
||||||
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
|
||||||
|
|||||||
@@ -34,19 +34,44 @@ function App() {
|
|||||||
|
|
||||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||||
|
|
||||||
The following workflow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
Here are two ways on how you can render **Excalidraw** on **Next.js**.
|
||||||
|
|
||||||
|
1. Importing Excalidraw once **client** is rendered.
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [Excalidraw, setExcalidraw] = useState(null);
|
const [Excalidraw, setExcalidraw] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
|
import("@excalidraw/excalidraw").then((comp) =>
|
||||||
|
setExcalidraw(comp.Excalidraw),
|
||||||
|
);
|
||||||
}, []);
|
}, []);
|
||||||
return <>{Excalidraw && <Excalidraw />}</>;
|
return <>{Excalidraw && <Excalidraw />}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-5xb3d)
|
||||||
|
|
||||||
|
2. Using **Next.js Dynamic** import.
|
||||||
|
|
||||||
|
Since Excalidraw doesn't server side rendering so you can also use `dynamic import` to render by setting `ssr` to `false`. However one drawback is the `Refs` don't work with dynamic import in Next.js. We are working on overcoming this and have a better API.
|
||||||
|
|
||||||
|
```jsx showLineNumbers
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
const Excalidraw = dynamic(
|
||||||
|
async () => (await import("@excalidraw/excalidraw")).Excalidraw,
|
||||||
|
{
|
||||||
|
ssr: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
export default function App() {
|
||||||
|
return <Excalidraw />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is a working [demo](https://codesandbox.io/p/sandbox/excalidraw-with-next-dynamic-k8yjq2).
|
||||||
|
|
||||||
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
|
||||||
|
|
||||||
## Browser
|
## Browser
|
||||||
|
|||||||
@@ -19,4 +19,4 @@ Frames should be ordered where frame children come first, followed by the frame
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
If not ordered 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.
|
||||||
|
|||||||
+117
-12
@@ -145,6 +145,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/highlight" "^7.18.6"
|
"@babel/highlight" "^7.18.6"
|
||||||
|
|
||||||
|
"@babel/code-frame@^7.22.13":
|
||||||
|
version "7.22.13"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
|
||||||
|
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
|
||||||
|
dependencies:
|
||||||
|
"@babel/highlight" "^7.22.13"
|
||||||
|
chalk "^2.4.2"
|
||||||
|
|
||||||
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8":
|
"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.18.8":
|
||||||
version "7.18.8"
|
version "7.18.8"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
|
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
|
||||||
@@ -202,6 +210,16 @@
|
|||||||
"@jridgewell/gen-mapping" "^0.3.2"
|
"@jridgewell/gen-mapping" "^0.3.2"
|
||||||
jsesc "^2.5.1"
|
jsesc "^2.5.1"
|
||||||
|
|
||||||
|
"@babel/generator@^7.23.0":
|
||||||
|
version "7.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
|
||||||
|
integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.23.0"
|
||||||
|
"@jridgewell/gen-mapping" "^0.3.2"
|
||||||
|
"@jridgewell/trace-mapping" "^0.3.17"
|
||||||
|
jsesc "^2.5.1"
|
||||||
|
|
||||||
"@babel/helper-annotate-as-pure@^7.18.6":
|
"@babel/helper-annotate-as-pure@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
|
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb"
|
||||||
@@ -265,6 +283,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
|
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
|
||||||
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
|
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
|
||||||
|
|
||||||
|
"@babel/helper-environment-visitor@^7.22.20":
|
||||||
|
version "7.22.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
|
||||||
|
integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
|
||||||
|
|
||||||
"@babel/helper-explode-assignable-expression@^7.18.6":
|
"@babel/helper-explode-assignable-expression@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
|
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096"
|
||||||
@@ -280,6 +303,14 @@
|
|||||||
"@babel/template" "^7.18.6"
|
"@babel/template" "^7.18.6"
|
||||||
"@babel/types" "^7.18.9"
|
"@babel/types" "^7.18.9"
|
||||||
|
|
||||||
|
"@babel/helper-function-name@^7.23.0":
|
||||||
|
version "7.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
|
||||||
|
integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/template" "^7.22.15"
|
||||||
|
"@babel/types" "^7.23.0"
|
||||||
|
|
||||||
"@babel/helper-hoist-variables@^7.18.6":
|
"@babel/helper-hoist-variables@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
|
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
|
||||||
@@ -287,6 +318,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
|
"@babel/helper-hoist-variables@^7.22.5":
|
||||||
|
version "7.22.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
|
||||||
|
integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.22.5"
|
||||||
|
|
||||||
"@babel/helper-member-expression-to-functions@^7.18.9":
|
"@babel/helper-member-expression-to-functions@^7.18.9":
|
||||||
version "7.18.9"
|
version "7.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
|
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815"
|
||||||
@@ -374,11 +412,28 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
|
"@babel/helper-split-export-declaration@^7.22.6":
|
||||||
|
version "7.22.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
|
||||||
|
integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
|
||||||
|
dependencies:
|
||||||
|
"@babel/types" "^7.22.5"
|
||||||
|
|
||||||
|
"@babel/helper-string-parser@^7.22.5":
|
||||||
|
version "7.22.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
|
||||||
|
integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
|
||||||
|
|
||||||
"@babel/helper-validator-identifier@^7.18.6":
|
"@babel/helper-validator-identifier@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
|
||||||
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
|
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
|
||||||
|
|
||||||
|
"@babel/helper-validator-identifier@^7.22.20":
|
||||||
|
version "7.22.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
|
||||||
|
integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
|
||||||
|
|
||||||
"@babel/helper-validator-option@^7.18.6":
|
"@babel/helper-validator-option@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
|
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
|
||||||
@@ -412,11 +467,25 @@
|
|||||||
chalk "^2.0.0"
|
chalk "^2.0.0"
|
||||||
js-tokens "^4.0.0"
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
|
"@babel/highlight@^7.22.13":
|
||||||
|
version "7.22.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
|
||||||
|
integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-validator-identifier" "^7.22.20"
|
||||||
|
chalk "^2.4.2"
|
||||||
|
js-tokens "^4.0.0"
|
||||||
|
|
||||||
"@babel/parser@^7.12.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.8", "@babel/parser@^7.18.9":
|
"@babel/parser@^7.12.7", "@babel/parser@^7.18.6", "@babel/parser@^7.18.8", "@babel/parser@^7.18.9":
|
||||||
version "7.18.9"
|
version "7.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
|
||||||
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
|
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
|
||||||
|
|
||||||
|
"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
|
||||||
|
version "7.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
|
||||||
|
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
|
||||||
|
|
||||||
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
|
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
|
||||||
version "7.18.6"
|
version "7.18.6"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
|
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
|
||||||
@@ -1147,19 +1216,28 @@
|
|||||||
"@babel/parser" "^7.18.6"
|
"@babel/parser" "^7.18.6"
|
||||||
"@babel/types" "^7.18.6"
|
"@babel/types" "^7.18.6"
|
||||||
|
|
||||||
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
"@babel/template@^7.22.15":
|
||||||
version "7.18.9"
|
version "7.22.15"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
|
||||||
integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
|
integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/code-frame" "^7.18.6"
|
"@babel/code-frame" "^7.22.13"
|
||||||
"@babel/generator" "^7.18.9"
|
"@babel/parser" "^7.22.15"
|
||||||
"@babel/helper-environment-visitor" "^7.18.9"
|
"@babel/types" "^7.22.15"
|
||||||
"@babel/helper-function-name" "^7.18.9"
|
|
||||||
"@babel/helper-hoist-variables" "^7.18.6"
|
"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.18.9":
|
||||||
"@babel/helper-split-export-declaration" "^7.18.6"
|
version "7.23.2"
|
||||||
"@babel/parser" "^7.18.9"
|
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
|
||||||
"@babel/types" "^7.18.9"
|
integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
|
||||||
|
dependencies:
|
||||||
|
"@babel/code-frame" "^7.22.13"
|
||||||
|
"@babel/generator" "^7.23.0"
|
||||||
|
"@babel/helper-environment-visitor" "^7.22.20"
|
||||||
|
"@babel/helper-function-name" "^7.23.0"
|
||||||
|
"@babel/helper-hoist-variables" "^7.22.5"
|
||||||
|
"@babel/helper-split-export-declaration" "^7.22.6"
|
||||||
|
"@babel/parser" "^7.23.0"
|
||||||
|
"@babel/types" "^7.23.0"
|
||||||
debug "^4.1.0"
|
debug "^4.1.0"
|
||||||
globals "^11.1.0"
|
globals "^11.1.0"
|
||||||
|
|
||||||
@@ -1171,6 +1249,15 @@
|
|||||||
"@babel/helper-validator-identifier" "^7.18.6"
|
"@babel/helper-validator-identifier" "^7.18.6"
|
||||||
to-fast-properties "^2.0.0"
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
|
"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
|
||||||
|
version "7.23.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
|
||||||
|
integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
|
||||||
|
dependencies:
|
||||||
|
"@babel/helper-string-parser" "^7.22.5"
|
||||||
|
"@babel/helper-validator-identifier" "^7.22.20"
|
||||||
|
to-fast-properties "^2.0.0"
|
||||||
|
|
||||||
"@colors/colors@1.5.0":
|
"@colors/colors@1.5.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||||
@@ -1670,6 +1757,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||||
|
|
||||||
|
"@jridgewell/resolve-uri@^3.1.0":
|
||||||
|
version "3.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
|
||||||
|
integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
|
||||||
|
|
||||||
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
|
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||||
@@ -1688,6 +1780,19 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||||
|
|
||||||
|
"@jridgewell/sourcemap-codec@^1.4.14":
|
||||||
|
version "1.4.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
|
||||||
|
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
|
||||||
|
|
||||||
|
"@jridgewell/trace-mapping@^0.3.17":
|
||||||
|
version "0.3.20"
|
||||||
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
|
||||||
|
integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
|
||||||
|
dependencies:
|
||||||
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
|
"@jridgewell/trace-mapping@^0.3.7", "@jridgewell/trace-mapping@^0.3.9":
|
||||||
version "0.3.14"
|
version "0.3.14"
|
||||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
appState: Partial<AppState>;
|
appState: Partial<AppState>;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
}> = ({ elements, appState, files, onError }) => {
|
onSuccess: () => void;
|
||||||
|
}> = ({ elements, appState, files, onError, onSuccess }) => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Card color="primary">
|
<Card color="primary">
|
||||||
@@ -107,6 +108,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
try {
|
try {
|
||||||
trackEvent("export", "eplus", `ui (${getFrame()})`);
|
trackEvent("export", "eplus", `ui (${getFrame()})`);
|
||||||
await exportToExcalidrawPlus(elements, appState, files);
|
await exportToExcalidrawPlus(elements, appState, files);
|
||||||
|
onSuccess();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error.name !== "AbortError") {
|
if (error.name !== "AbortError") {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*
|
*
|
||||||
* - DataState refers to full state of the app: appState, elements, images,
|
* - DataState refers to full state of the app: appState, elements, images,
|
||||||
* though some state is saved separately (collab username, library) for one
|
* though some state is saved separately (collab username, library) for one
|
||||||
* reason or another. We also save different data to different sotrage
|
* reason or another. We also save different data to different storage
|
||||||
* (localStorage, indexedDB).
|
* (localStorage, indexedDB).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -131,5 +131,5 @@ export class Debug {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
//@ts-ignore
|
||||||
window.debug = Debug;
|
window.debug = Debug;
|
||||||
|
|||||||
@@ -608,7 +608,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement,
|
||||||
) => {
|
) => {
|
||||||
if (exportedElements.length === 0) {
|
if (exportedElements.length === 0) {
|
||||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||||
}
|
}
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
try {
|
try {
|
||||||
@@ -624,7 +624,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
setErrorMessage(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
@@ -634,7 +634,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (error.name !== "AbortError") {
|
if (error.name !== "AbortError") {
|
||||||
const { width, height } = canvas;
|
const { width, height } = canvas;
|
||||||
console.error(error, { width, height });
|
console.error(error, { width, height });
|
||||||
setErrorMessage(error.message);
|
throw new Error(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -691,7 +691,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Excalidraw
|
<Excalidraw
|
||||||
ref={excalidrawRefCallback}
|
excalidrawAPI={excalidrawRefCallback}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
initialData={initialStatePromiseRef.current.promise}
|
initialData={initialStatePromiseRef.current.promise}
|
||||||
isCollaborating={isCollaborating}
|
isCollaborating={isCollaborating}
|
||||||
@@ -714,6 +714,11 @@ const ExcalidrawWrapper = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onSuccess={() => {
|
||||||
|
excalidrawAPI?.updateScene({
|
||||||
|
appState: { openDialog: null },
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ describe("Test MobileMenu", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
h.app.refreshViewportBreakpoints();
|
||||||
|
// @ts-ignore
|
||||||
|
h.app.refreshEditorBreakpoints();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -28,11 +30,15 @@ describe("Test MobileMenu", () => {
|
|||||||
it("should set device correctly", () => {
|
it("should set device correctly", () => {
|
||||||
expect(h.app.device).toMatchInlineSnapshot(`
|
expect(h.app.device).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"canDeviceFitSidebar": false,
|
"editor": {
|
||||||
"isLandscape": true,
|
"canFitSidebar": false,
|
||||||
"isMobile": true,
|
"isMobile": true,
|
||||||
"isSmScreen": false,
|
},
|
||||||
"isTouchScreen": false,
|
"isTouchScreen": false,
|
||||||
|
"viewport": {
|
||||||
|
"isLandscape": false,
|
||||||
|
"isMobile": true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "../../excalidraw-app/collab/reconciliation";
|
} from "../../excalidraw-app/collab/reconciliation";
|
||||||
import { randomInteger } from "../../src/random";
|
import { randomInteger } from "../../src/random";
|
||||||
import { AppState } from "../../src/types";
|
import { AppState } from "../../src/types";
|
||||||
|
import { cloneJSON } from "../../src/utils";
|
||||||
|
|
||||||
type Id = string;
|
type Id = string;
|
||||||
type ElementLike = {
|
type ElementLike = {
|
||||||
@@ -93,8 +94,6 @@ const cleanElements = (elements: ReconciledElements) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const cloneDeep = (data: any) => JSON.parse(JSON.stringify(data));
|
|
||||||
|
|
||||||
const test = <U extends `${string}:${"L" | "R"}`>(
|
const test = <U extends `${string}:${"L" | "R"}`>(
|
||||||
local: (Id | ElementLike)[],
|
local: (Id | ElementLike)[],
|
||||||
remote: (Id | ElementLike)[],
|
remote: (Id | ElementLike)[],
|
||||||
@@ -115,15 +114,15 @@ const test = <U extends `${string}:${"L" | "R"}`>(
|
|||||||
"remote reconciliation",
|
"remote reconciliation",
|
||||||
);
|
);
|
||||||
|
|
||||||
const __local = cleanElements(cloneDeep(_remote));
|
const __local = cleanElements(cloneJSON(_remote) as ReconciledElements);
|
||||||
const __remote = addParents(cleanElements(cloneDeep(remoteReconciled)));
|
const __remote = addParents(cleanElements(cloneJSON(remoteReconciled)));
|
||||||
if (bidirectional) {
|
if (bidirectional) {
|
||||||
try {
|
try {
|
||||||
expect(
|
expect(
|
||||||
cleanElements(
|
cleanElements(
|
||||||
reconcileElements(
|
reconcileElements(
|
||||||
cloneDeep(__local),
|
cloneJSON(__local),
|
||||||
cloneDeep(__remote),
|
cloneJSON(__remote),
|
||||||
{} as AppState,
|
{} as AppState,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
+5
-4
@@ -21,7 +21,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@excalidraw/laser-pointer": "1.2.0",
|
"@excalidraw/laser-pointer": "1.2.0",
|
||||||
"@excalidraw/random-username": "1.0.0",
|
"@excalidraw/mermaid-to-excalidraw": "0.1.2",
|
||||||
|
"@excalidraw/random-username": "1.1.0",
|
||||||
"@radix-ui/react-popover": "1.0.3",
|
"@radix-ui/react-popover": "1.0.3",
|
||||||
"@radix-ui/react-tabs": "1.0.2",
|
"@radix-ui/react-tabs": "1.0.2",
|
||||||
"@sentry/browser": "6.2.5",
|
"@sentry/browser": "6.2.5",
|
||||||
@@ -49,11 +50,11 @@
|
|||||||
"png-chunk-text": "1.0.0",
|
"png-chunk-text": "1.0.0",
|
||||||
"png-chunks-encode": "1.0.0",
|
"png-chunks-encode": "1.0.0",
|
||||||
"png-chunks-extract": "1.0.0",
|
"png-chunks-extract": "1.0.0",
|
||||||
"points-on-curve": "0.2.0",
|
"points-on-curve": "1.0.1",
|
||||||
"pwacompat": "2.0.17",
|
"pwacompat": "2.0.17",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"roughjs": "4.5.2",
|
"roughjs": "4.6.4",
|
||||||
"sass": "1.51.0",
|
"sass": "1.51.0",
|
||||||
"socket.io-client": "2.3.1",
|
"socket.io-client": "2.3.1",
|
||||||
"tunnel-rat": "0.1.2"
|
"tunnel-rat": "0.1.2"
|
||||||
@@ -125,7 +126,7 @@
|
|||||||
"test": "yarn test:app",
|
"test": "yarn test:app",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:coverage:watch": "vitest --coverage --watch",
|
"test:coverage:watch": "vitest --coverage --watch",
|
||||||
"test:ui": "yarn test --ui",
|
"test:ui": "yarn test --ui --coverage.enabled=true",
|
||||||
"autorelease": "node scripts/autorelease.js",
|
"autorelease": "node scripts/autorelease.js",
|
||||||
"prerelease": "node scripts/prerelease.js",
|
"prerelease": "node scripts/prerelease.js",
|
||||||
"build:preview": "yarn build && vite preview --port 5000",
|
"build:preview": "yarn build && vite preview --port 5000",
|
||||||
|
|||||||
@@ -438,5 +438,6 @@ export const actionToggleHandTool = register({
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
keyTest: (event) => event.key === KEYS.H,
|
keyTest: (event) =>
|
||||||
|
!event.altKey && !event[KEYS.CTRL_OR_CMD] && event.key === KEYS.H,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,33 +3,43 @@ import { register } from "./register";
|
|||||||
import {
|
import {
|
||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
copyToClipboard,
|
copyToClipboard,
|
||||||
|
createPasteEvent,
|
||||||
probablySupportsClipboardBlob,
|
probablySupportsClipboardBlob,
|
||||||
probablySupportsClipboardWriteText,
|
probablySupportsClipboardWriteText,
|
||||||
|
readSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { actionDeleteSelected } from "./actionDeleteSelected";
|
import { actionDeleteSelected } from "./actionDeleteSelected";
|
||||||
import { exportCanvas } from "../data/index";
|
import { exportCanvas, prepareElementsForExport } from "../data/index";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { isTextElement } from "../element";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { isFirefox } from "../constants";
|
||||||
|
|
||||||
export const actionCopy = register({
|
export const actionCopy = register({
|
||||||
name: "copy",
|
name: "copy",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, _, app) => {
|
perform: async (elements, appState, event: ClipboardEvent | null, app) => {
|
||||||
const elementsToCopy = app.scene.getSelectedElements({
|
const elementsToCopy = app.scene.getSelectedElements({
|
||||||
selectedElementIds: appState.selectedElementIds,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
copyToClipboard(elementsToCopy, app.files);
|
try {
|
||||||
|
await copyToClipboard(elementsToCopy, app.files, event);
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
errorMessage: error.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, appProps, app) => {
|
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
|
||||||
},
|
|
||||||
contextItemLabel: "labels.copy",
|
contextItemLabel: "labels.copy",
|
||||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
@@ -38,15 +48,55 @@ export const actionCopy = register({
|
|||||||
export const actionPaste = register({
|
export const actionPaste = register({
|
||||||
name: "paste",
|
name: "paste",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements: any, appStates: any, data, app) => {
|
perform: async (elements, appState, data, app) => {
|
||||||
app.pasteFromClipboard(null);
|
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 {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, appProps, app) => {
|
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
|
||||||
},
|
|
||||||
contextItemLabel: "labels.paste",
|
contextItemLabel: "labels.paste",
|
||||||
// don't supply a shortcut since we handle this conditionally via onCopy event
|
// don't supply a shortcut since we handle this conditionally via onCopy event
|
||||||
keyTest: undefined,
|
keyTest: undefined,
|
||||||
@@ -55,13 +105,10 @@ export const actionPaste = register({
|
|||||||
export const actionCut = register({
|
export const actionCut = register({
|
||||||
name: "cut",
|
name: "cut",
|
||||||
trackEvent: { category: "element" },
|
trackEvent: { category: "element" },
|
||||||
perform: (elements, appState, data, app) => {
|
perform: (elements, appState, event: ClipboardEvent | null, app) => {
|
||||||
actionCopy.perform(elements, appState, data, app);
|
actionCopy.perform(elements, appState, event, app);
|
||||||
return actionDeleteSelected.perform(elements, appState);
|
return actionDeleteSelected.perform(elements, appState);
|
||||||
},
|
},
|
||||||
predicate: (elements, appState, appProps, app) => {
|
|
||||||
return app.device.isMobile && !!navigator.clipboard;
|
|
||||||
},
|
|
||||||
contextItemLabel: "labels.cut",
|
contextItemLabel: "labels.cut",
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
|
||||||
});
|
});
|
||||||
@@ -75,20 +122,23 @@ export const actionCopyAsSvg = register({
|
|||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const selectedElements = app.scene.getSelectedElements({
|
|
||||||
selectedElementIds: appState.selectedElementIds,
|
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||||
includeBoundTextElement: true,
|
elements,
|
||||||
includeElementsInFrames: true,
|
appState,
|
||||||
});
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas(
|
||||||
"clipboard-svg",
|
"clipboard-svg",
|
||||||
selectedElements.length
|
exportedElements,
|
||||||
? selectedElements
|
|
||||||
: getNonDeletedElements(elements),
|
|
||||||
appState,
|
appState,
|
||||||
app.files,
|
app.files,
|
||||||
appState,
|
{
|
||||||
|
...appState,
|
||||||
|
exportingFrame,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
commitToHistory: false,
|
commitToHistory: false,
|
||||||
@@ -124,16 +174,17 @@ export const actionCopyAsPng = register({
|
|||||||
includeBoundTextElement: true,
|
includeBoundTextElement: true,
|
||||||
includeElementsInFrames: true,
|
includeElementsInFrames: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
true,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await exportCanvas(
|
await exportCanvas("clipboard", exportedElements, appState, app.files, {
|
||||||
"clipboard",
|
...appState,
|
||||||
selectedElements.length
|
exportingFrame,
|
||||||
? selectedElements
|
});
|
||||||
: getNonDeletedElements(elements),
|
|
||||||
appState,
|
|
||||||
app.files,
|
|
||||||
appState,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const deleteSelectedElements = (
|
|||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
|
selectedGroupIds: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { normalizeElementOrder } from "../element/sortElements";
|
|||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
import {
|
import {
|
||||||
bindElementsToFramesAfterDuplication,
|
bindElementsToFramesAfterDuplication,
|
||||||
getFrameElements,
|
getFrameChildren,
|
||||||
} from "../frame";
|
} from "../frame";
|
||||||
import {
|
import {
|
||||||
excludeElementsInFramesFromSelection,
|
excludeElementsInFramesFromSelection,
|
||||||
@@ -155,7 +155,7 @@ const duplicateElements = (
|
|||||||
groupId,
|
groupId,
|
||||||
).flatMap((element) =>
|
).flatMap((element) =>
|
||||||
isFrameElement(element)
|
isFrameElement(element)
|
||||||
? [...getFrameElements(elements, element.id), element]
|
? [...getFrameChildren(elements, element.id), element]
|
||||||
: [element],
|
: [element],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ const duplicateElements = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isElementAFrame) {
|
if (isElementAFrame) {
|
||||||
const elementsInFrame = getFrameElements(sortedElements, element.id);
|
const elementsInFrame = getFrameChildren(sortedElements, element.id);
|
||||||
|
|
||||||
elementsWithClones.push(
|
elementsWithClones.push(
|
||||||
...markAsProcessed([
|
...markAsProcessed([
|
||||||
|
|||||||
@@ -191,7 +191,15 @@ export const actionSaveFileToDisk = register({
|
|||||||
},
|
},
|
||||||
app.files,
|
app.files,
|
||||||
);
|
);
|
||||||
return { commitToHistory: false, appState: { ...appState, fileHandle } };
|
return {
|
||||||
|
commitToHistory: false,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
openDialog: null,
|
||||||
|
fileHandle,
|
||||||
|
toast: { message: t("toast.fileSaved") },
|
||||||
|
},
|
||||||
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error?.name !== "AbortError") {
|
if (error?.name !== "AbortError") {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -209,7 +217,7 @@ export const actionSaveFileToDisk = register({
|
|||||||
icon={saveAs}
|
icon={saveAs}
|
||||||
title={t("buttons.saveAs")}
|
title={t("buttons.saveAs")}
|
||||||
aria-label={t("buttons.saveAs")}
|
aria-label={t("buttons.saveAs")}
|
||||||
showAriaLabel={useDevice().isMobile}
|
showAriaLabel={useDevice().editor.isMobile}
|
||||||
hidden={!nativeFileSystemSupported}
|
hidden={!nativeFileSystemSupported}
|
||||||
onClick={() => updateData(null)}
|
onClick={() => updateData(null)}
|
||||||
data-testid="save-as-button"
|
data-testid="save-as-button"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { removeAllElementsFromFrame } from "../frame";
|
import { removeAllElementsFromFrame } from "../frame";
|
||||||
import { getFrameElements } from "../frame";
|
import { getFrameChildren } from "../frame";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { updateActiveTool } from "../utils";
|
import { updateActiveTool } from "../utils";
|
||||||
@@ -21,7 +21,7 @@ export const actionSelectAllElementsInFrame = register({
|
|||||||
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
const selectedFrame = app.scene.getSelectedElements(appState)[0];
|
||||||
|
|
||||||
if (selectedFrame && selectedFrame.type === "frame") {
|
if (selectedFrame && selectedFrame.type === "frame") {
|
||||||
const elementsInFrame = getFrameElements(
|
const elementsInFrame = getFrameChildren(
|
||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
selectedFrame.id,
|
selectedFrame.id,
|
||||||
).filter((element) => !(element.type === "text" && element.containerId));
|
).filter((element) => !(element.type === "text" && element.containerId));
|
||||||
|
|||||||
+15
-13
@@ -17,15 +17,12 @@ import {
|
|||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { randomId } from "../random";
|
import { randomId } from "../random";
|
||||||
import { ToolButton } from "../components/ToolButton";
|
import { ToolButton } from "../components/ToolButton";
|
||||||
import {
|
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||||
ExcalidrawElement,
|
|
||||||
ExcalidrawFrameElement,
|
|
||||||
ExcalidrawTextElement,
|
|
||||||
} from "../element/types";
|
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import { AppClassProperties, AppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
getElementsInResizingFrame,
|
getElementsInResizingFrame,
|
||||||
|
getFrameElements,
|
||||||
groupByFrames,
|
groupByFrames,
|
||||||
removeElementsFromFrame,
|
removeElementsFromFrame,
|
||||||
replaceAllElementsInFrame,
|
replaceAllElementsInFrame,
|
||||||
@@ -190,13 +187,6 @@ export const actionUngroup = register({
|
|||||||
|
|
||||||
let nextElements = [...elements];
|
let nextElements = [...elements];
|
||||||
|
|
||||||
const selectedElements = app.scene.getSelectedElements(appState);
|
|
||||||
const frames = selectedElements
|
|
||||||
.filter((element) => element.frameId)
|
|
||||||
.map((element) =>
|
|
||||||
app.scene.getElement(element.frameId!),
|
|
||||||
) as ExcalidrawFrameElement[];
|
|
||||||
|
|
||||||
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
|
||||||
nextElements = nextElements.map((element) => {
|
nextElements = nextElements.map((element) => {
|
||||||
if (isBoundToContainer(element)) {
|
if (isBoundToContainer(element)) {
|
||||||
@@ -221,7 +211,19 @@ export const actionUngroup = register({
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
frames.forEach((frame) => {
|
const selectedElements = app.scene.getSelectedElements(appState);
|
||||||
|
|
||||||
|
const selectedElementFrameIds = new Set(
|
||||||
|
selectedElements
|
||||||
|
.filter((element) => element.frameId)
|
||||||
|
.map((element) => element.frameId!),
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetFrames = getFrameElements(elements).filter((frame) =>
|
||||||
|
selectedElementFrameIds.has(frame.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
targetFrames.forEach((frame) => {
|
||||||
if (frame) {
|
if (frame) {
|
||||||
nextElements = replaceAllElementsInFrame(
|
nextElements = replaceAllElementsInFrame(
|
||||||
nextElements,
|
nextElements,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { ToolButton } from "../components/ToolButton";
|
|||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
import { showSelectedShapeActions, getNonDeletedElements } from "../element";
|
||||||
import { register } from "./register";
|
import { register } from "./register";
|
||||||
import { allowFullScreen, exitFullScreen, isFullScreen } from "../utils";
|
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
|
|
||||||
export const actionToggleCanvasMenu = register({
|
export const actionToggleCanvasMenu = register({
|
||||||
@@ -52,23 +51,6 @@ export const actionToggleEditMenu = register({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionFullScreen = register({
|
|
||||||
name: "toggleFullScreen",
|
|
||||||
viewMode: true,
|
|
||||||
trackEvent: { category: "canvas", predicate: (appState) => !isFullScreen() },
|
|
||||||
perform: () => {
|
|
||||||
if (!isFullScreen()) {
|
|
||||||
allowFullScreen();
|
|
||||||
}
|
|
||||||
if (isFullScreen()) {
|
|
||||||
exitFullScreen();
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
commitToHistory: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionShortcuts = register({
|
export const actionShortcuts = register({
|
||||||
name: "toggleShortcuts",
|
name: "toggleShortcuts",
|
||||||
viewMode: true,
|
viewMode: true,
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { Excalidraw } from "../packages/excalidraw/index";
|
||||||
|
import { queryByTestId } from "@testing-library/react";
|
||||||
|
import { render } from "../tests/test-utils";
|
||||||
|
import { UI } from "../tests/helpers/ui";
|
||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_PICKS } from "../colors";
|
||||||
|
import { FONT_FAMILY, STROKE_WIDTH } from "../constants";
|
||||||
|
|
||||||
|
const { h } = window;
|
||||||
|
|
||||||
|
describe("element locking", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await render(<Excalidraw />);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("properties when tool selected", () => {
|
||||||
|
it("should show active background top picks", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||||
|
|
||||||
|
// just in case we change it in the future
|
||||||
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
});
|
||||||
|
const activeColor = queryByTestId(
|
||||||
|
document.body,
|
||||||
|
`color-top-pick-${color}`,
|
||||||
|
);
|
||||||
|
expect(activeColor).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show fill style when background non-transparent", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
const color = DEFAULT_ELEMENT_BACKGROUND_PICKS[1];
|
||||||
|
|
||||||
|
// just in case we change it in the future
|
||||||
|
expect(color).not.toBe(COLOR_PALETTE.transparent);
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: color,
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
});
|
||||||
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
|
expect(hachureFillButton).toHaveClass("active");
|
||||||
|
h.setState({
|
||||||
|
currentItemFillStyle: "solid",
|
||||||
|
});
|
||||||
|
const solidFillStyle = queryByTestId(document.body, `fill-solid`);
|
||||||
|
expect(solidFillStyle).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show fill style when background transparent", () => {
|
||||||
|
UI.clickTool("rectangle");
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemBackgroundColor: COLOR_PALETTE.transparent,
|
||||||
|
currentItemFillStyle: "hachure",
|
||||||
|
});
|
||||||
|
const hachureFillButton = queryByTestId(document.body, `fill-hachure`);
|
||||||
|
|
||||||
|
expect(hachureFillButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show horizontal text align for text tool", () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
|
||||||
|
h.setState({
|
||||||
|
currentItemTextAlign: "right",
|
||||||
|
});
|
||||||
|
|
||||||
|
const centerTextAlign = queryByTestId(document.body, `align-right`);
|
||||||
|
expect(centerTextAlign).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("properties when elements selected", () => {
|
||||||
|
it("should show active styles when single element selected", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: "red",
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
});
|
||||||
|
h.elements = [rect];
|
||||||
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
|
expect(crossHatchButton).toHaveClass("active");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not show fill style selected element's background is transparent", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
|
fillStyle: "cross-hatch",
|
||||||
|
});
|
||||||
|
h.elements = [rect];
|
||||||
|
API.setSelectedElements([rect]);
|
||||||
|
|
||||||
|
const crossHatchButton = queryByTestId(document.body, `fill-cross-hatch`);
|
||||||
|
expect(crossHatchButton).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should highlight common stroke width of selected elements", () => {
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
h.elements = [rect1, rect2];
|
||||||
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
|
const thinStrokeWidthButton = queryByTestId(
|
||||||
|
document.body,
|
||||||
|
`strokeWidth-thin`,
|
||||||
|
);
|
||||||
|
expect(thinStrokeWidthButton).toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not highlight any stroke width button if no common style", () => {
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.thin,
|
||||||
|
});
|
||||||
|
const rect2 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
|
});
|
||||||
|
h.elements = [rect1, rect2];
|
||||||
|
API.setSelectedElements([rect1, rect2]);
|
||||||
|
|
||||||
|
expect(queryByTestId(document.body, `strokeWidth-thin`)).not.toBe(null);
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-thin`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-bold`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
expect(
|
||||||
|
queryByTestId(document.body, `strokeWidth-extraBold`),
|
||||||
|
).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show properties of different element types when selected", () => {
|
||||||
|
const rect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
strokeWidth: STROKE_WIDTH.bold,
|
||||||
|
});
|
||||||
|
const text = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
fontFamily: FONT_FAMILY.Cascadia,
|
||||||
|
});
|
||||||
|
h.elements = [rect, text];
|
||||||
|
API.setSelectedElements([rect, text]);
|
||||||
|
|
||||||
|
expect(queryByTestId(document.body, `strokeWidth-bold`)).toBeChecked();
|
||||||
|
expect(queryByTestId(document.body, `font-family-code`)).toBeChecked();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AppState } from "../../src/types";
|
import { AppState, Primitive } from "../../src/types";
|
||||||
import {
|
import {
|
||||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
DEFAULT_FONT_SIZE,
|
DEFAULT_FONT_SIZE,
|
||||||
FONT_FAMILY,
|
FONT_FAMILY,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
|
STROKE_WIDTH,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
@@ -82,7 +83,6 @@ import { getLanguage, t } from "../i18n";
|
|||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import {
|
import {
|
||||||
canChangeRoundness,
|
|
||||||
canHaveArrowheads,
|
canHaveArrowheads,
|
||||||
getCommonAttributeOfSelectedElements,
|
getCommonAttributeOfSelectedElements,
|
||||||
getSelectedElements,
|
getSelectedElements,
|
||||||
@@ -118,25 +118,44 @@ export const changeProperty = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFormValue = function <T>(
|
export const getFormValue = function <T extends Primitive>(
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
getAttribute: (element: ExcalidrawElement) => T,
|
getAttribute: (element: ExcalidrawElement) => T,
|
||||||
defaultValue: T,
|
isRelevantElement: true | ((element: ExcalidrawElement) => boolean),
|
||||||
|
defaultValue: T | ((isSomeElementSelected: boolean) => T),
|
||||||
): T {
|
): T {
|
||||||
const editingElement = appState.editingElement;
|
const editingElement = appState.editingElement;
|
||||||
const nonDeletedElements = getNonDeletedElements(elements);
|
const nonDeletedElements = getNonDeletedElements(elements);
|
||||||
return (
|
|
||||||
(editingElement && getAttribute(editingElement)) ??
|
let ret: T | null = null;
|
||||||
(isSomeElementSelected(nonDeletedElements, appState)
|
|
||||||
? getCommonAttributeOfSelectedElements(
|
if (editingElement) {
|
||||||
nonDeletedElements,
|
ret = getAttribute(editingElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret) {
|
||||||
|
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
|
||||||
|
|
||||||
|
if (hasSelection) {
|
||||||
|
ret =
|
||||||
|
getCommonAttributeOfSelectedElements(
|
||||||
|
isRelevantElement === true
|
||||||
|
? nonDeletedElements
|
||||||
|
: nonDeletedElements.filter((el) => isRelevantElement(el)),
|
||||||
appState,
|
appState,
|
||||||
getAttribute,
|
getAttribute,
|
||||||
)
|
) ??
|
||||||
: defaultValue) ??
|
(typeof defaultValue === "function"
|
||||||
defaultValue
|
? defaultValue(true)
|
||||||
);
|
: defaultValue);
|
||||||
|
} else {
|
||||||
|
ret =
|
||||||
|
typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const offsetElementAfterFontResize = (
|
const offsetElementAfterFontResize = (
|
||||||
@@ -247,6 +266,7 @@ export const actionChangeStrokeColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeColor,
|
(element) => element.strokeColor,
|
||||||
|
true,
|
||||||
appState.currentItemStrokeColor,
|
appState.currentItemStrokeColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
onChange={(color) => updateData({ currentItemStrokeColor: color })}
|
||||||
@@ -289,6 +309,7 @@ export const actionChangeBackgroundColor = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.backgroundColor,
|
(element) => element.backgroundColor,
|
||||||
|
true,
|
||||||
appState.currentItemBackgroundColor,
|
appState.currentItemBackgroundColor,
|
||||||
)}
|
)}
|
||||||
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
|
||||||
@@ -307,7 +328,7 @@ export const actionChangeFillStyle = register({
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"element",
|
"element",
|
||||||
"changeFillStyle",
|
"changeFillStyle",
|
||||||
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
`${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
elements: changeProperty(elements, appState, (el) =>
|
elements: changeProperty(elements, appState, (el) =>
|
||||||
@@ -338,23 +359,28 @@ export const actionChangeFillStyle = register({
|
|||||||
} (${getShortcutKey("Alt-Click")})`,
|
} (${getShortcutKey("Alt-Click")})`,
|
||||||
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
|
||||||
active: allElementsZigZag ? true : undefined,
|
active: allElementsZigZag ? true : undefined,
|
||||||
|
testId: `fill-hachure`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cross-hatch",
|
value: "cross-hatch",
|
||||||
text: t("labels.crossHatch"),
|
text: t("labels.crossHatch"),
|
||||||
icon: FillCrossHatchIcon,
|
icon: FillCrossHatchIcon,
|
||||||
|
testId: `fill-cross-hatch`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "solid",
|
value: "solid",
|
||||||
text: t("labels.solid"),
|
text: t("labels.solid"),
|
||||||
icon: FillSolidIcon,
|
icon: FillSolidIcon,
|
||||||
|
testId: `fill-solid`,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.fillStyle,
|
(element) => element.fillStyle,
|
||||||
appState.currentItemFillStyle,
|
(element) => element.hasOwnProperty("fillStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemFillStyle,
|
||||||
)}
|
)}
|
||||||
onClick={(value, event) => {
|
onClick={(value, event) => {
|
||||||
const nextValue =
|
const nextValue =
|
||||||
@@ -393,26 +419,31 @@ export const actionChangeStrokeWidth = register({
|
|||||||
group="stroke-width"
|
group="stroke-width"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: 1,
|
value: STROKE_WIDTH.thin,
|
||||||
text: t("labels.thin"),
|
text: t("labels.thin"),
|
||||||
icon: StrokeWidthBaseIcon,
|
icon: StrokeWidthBaseIcon,
|
||||||
|
testId: "strokeWidth-thin",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 2,
|
value: STROKE_WIDTH.bold,
|
||||||
text: t("labels.bold"),
|
text: t("labels.bold"),
|
||||||
icon: StrokeWidthBoldIcon,
|
icon: StrokeWidthBoldIcon,
|
||||||
|
testId: "strokeWidth-bold",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 4,
|
value: STROKE_WIDTH.extraBold,
|
||||||
text: t("labels.extraBold"),
|
text: t("labels.extraBold"),
|
||||||
icon: StrokeWidthExtraBoldIcon,
|
icon: StrokeWidthExtraBoldIcon,
|
||||||
|
testId: "strokeWidth-extraBold",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
value={getFormValue(
|
value={getFormValue(
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeWidth,
|
(element) => element.strokeWidth,
|
||||||
appState.currentItemStrokeWidth,
|
(element) => element.hasOwnProperty("strokeWidth"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeWidth,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -461,7 +492,9 @@ export const actionChangeSloppiness = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.roughness,
|
(element) => element.roughness,
|
||||||
appState.currentItemRoughness,
|
(element) => element.hasOwnProperty("roughness"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemRoughness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -509,7 +542,9 @@ export const actionChangeStrokeStyle = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.strokeStyle,
|
(element) => element.strokeStyle,
|
||||||
appState.currentItemStrokeStyle,
|
(element) => element.hasOwnProperty("strokeStyle"),
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemStrokeStyle,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -549,6 +584,7 @@ export const actionChangeOpacity = register({
|
|||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
(element) => element.opacity,
|
(element) => element.opacity,
|
||||||
|
true,
|
||||||
appState.currentItemOpacity,
|
appState.currentItemOpacity,
|
||||||
) ?? undefined
|
) ?? undefined
|
||||||
}
|
}
|
||||||
@@ -607,7 +643,12 @@ export const actionChangeFontSize = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontSize || DEFAULT_FONT_SIZE,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -692,21 +733,25 @@ export const actionChangeFontFamily = register({
|
|||||||
value: FontFamilyValues;
|
value: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
icon: JSX.Element;
|
icon: JSX.Element;
|
||||||
|
testId: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Virgil,
|
value: FONT_FAMILY.Virgil,
|
||||||
text: t("labels.handDrawn"),
|
text: t("labels.handDrawn"),
|
||||||
icon: FreedrawIcon,
|
icon: FreedrawIcon,
|
||||||
|
testId: "font-family-virgil",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Helvetica,
|
value: FONT_FAMILY.Helvetica,
|
||||||
text: t("labels.normal"),
|
text: t("labels.normal"),
|
||||||
icon: FontFamilyNormalIcon,
|
icon: FontFamilyNormalIcon,
|
||||||
|
testId: "font-family-normal",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: FONT_FAMILY.Cascadia,
|
value: FONT_FAMILY.Cascadia,
|
||||||
text: t("labels.code"),
|
text: t("labels.code"),
|
||||||
icon: FontFamilyCodeIcon,
|
icon: FontFamilyCodeIcon,
|
||||||
|
testId: "font-family-code",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -729,7 +774,12 @@ export const actionChangeFontFamily = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection
|
||||||
|
? null
|
||||||
|
: appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -806,7 +856,10 @@ export const actionChangeTextAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
appState.currentItemTextAlign,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) =>
|
||||||
|
hasSelection ? null : appState.currentItemTextAlign,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -882,7 +935,9 @@ export const actionChangeVerticalAlign = register({
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
VERTICAL_ALIGN.MIDDLE,
|
(element) =>
|
||||||
|
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||||
|
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -947,9 +1002,9 @@ export const actionChangeRoundness = register({
|
|||||||
appState,
|
appState,
|
||||||
(element) =>
|
(element) =>
|
||||||
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
|
||||||
(canChangeRoundness(appState.activeTool.type) &&
|
(element) => element.hasOwnProperty("roundness"),
|
||||||
appState.currentItemRoundness) ||
|
(hasSelection) =>
|
||||||
null,
|
hasSelection ? null : appState.currentItemRoundness,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData(value)}
|
onChange={(value) => updateData(value)}
|
||||||
/>
|
/>
|
||||||
@@ -1043,6 +1098,7 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.startArrowhead
|
? element.startArrowhead
|
||||||
: appState.currentItemStartArrowhead,
|
: appState.currentItemStartArrowhead,
|
||||||
|
true,
|
||||||
appState.currentItemStartArrowhead,
|
appState.currentItemStartArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "start", type: value })}
|
onChange={(value) => updateData({ position: "start", type: value })}
|
||||||
@@ -1089,6 +1145,7 @@ export const actionChangeArrowhead = register({
|
|||||||
isLinearElement(element) && canHaveArrowheads(element.type)
|
isLinearElement(element) && canHaveArrowheads(element.type)
|
||||||
? element.endArrowhead
|
? element.endArrowhead
|
||||||
: appState.currentItemEndArrowhead,
|
: appState.currentItemEndArrowhead,
|
||||||
|
true,
|
||||||
appState.currentItemEndArrowhead,
|
appState.currentItemEndArrowhead,
|
||||||
)}
|
)}
|
||||||
onChange={(value) => updateData({ position: "end", type: value })}
|
onChange={(value) => updateData({ position: "end", type: value })}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import {
|
|||||||
canApplyRoundnessTypeToElement,
|
canApplyRoundnessTypeToElement,
|
||||||
getDefaultRoundnessTypeForElement,
|
getDefaultRoundnessTypeForElement,
|
||||||
isFrameElement,
|
isFrameElement,
|
||||||
|
isArrowElement,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
|
import { ExcalidrawTextElement } from "../element/types";
|
||||||
|
|
||||||
// `copiedStyles` is exported only for tests.
|
// `copiedStyles` is exported only for tests.
|
||||||
export let copiedStyles: string = "{}";
|
export let copiedStyles: string = "{}";
|
||||||
@@ -99,16 +101,19 @@ export const actionPasteStyles = register({
|
|||||||
|
|
||||||
if (isTextElement(newElement)) {
|
if (isTextElement(newElement)) {
|
||||||
const fontSize =
|
const fontSize =
|
||||||
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
|
(elementStylesToCopyFrom as ExcalidrawTextElement).fontSize ||
|
||||||
|
DEFAULT_FONT_SIZE;
|
||||||
const fontFamily =
|
const fontFamily =
|
||||||
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
|
(elementStylesToCopyFrom as ExcalidrawTextElement).fontFamily ||
|
||||||
|
DEFAULT_FONT_FAMILY;
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
textAlign:
|
textAlign:
|
||||||
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
|
(elementStylesToCopyFrom as ExcalidrawTextElement).textAlign ||
|
||||||
|
DEFAULT_TEXT_ALIGN,
|
||||||
lineHeight:
|
lineHeight:
|
||||||
elementStylesToCopyFrom.lineHeight ||
|
(elementStylesToCopyFrom as ExcalidrawTextElement).lineHeight ||
|
||||||
getDefaultLineHeight(fontFamily),
|
getDefaultLineHeight(fontFamily),
|
||||||
});
|
});
|
||||||
let container = null;
|
let container = null;
|
||||||
@@ -123,7 +128,10 @@ export const actionPasteStyles = register({
|
|||||||
redrawTextBoundingBox(newElement, container);
|
redrawTextBoundingBox(newElement, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newElement.type === "arrow") {
|
if (
|
||||||
|
newElement.type === "arrow" &&
|
||||||
|
isArrowElement(elementStylesToCopyFrom)
|
||||||
|
) {
|
||||||
newElement = newElementWith(newElement, {
|
newElement = newElementWith(newElement, {
|
||||||
startArrowhead: elementStylesToCopyFrom.startArrowhead,
|
startArrowhead: elementStylesToCopyFrom.startArrowhead,
|
||||||
endArrowhead: elementStylesToCopyFrom.endArrowhead,
|
endArrowhead: elementStylesToCopyFrom.endArrowhead,
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export { actionCopyStyles, actionPasteStyles } from "./actionStyles";
|
|||||||
export {
|
export {
|
||||||
actionToggleCanvasMenu,
|
actionToggleCanvasMenu,
|
||||||
actionToggleEditMenu,
|
actionToggleEditMenu,
|
||||||
actionFullScreen,
|
|
||||||
actionShortcuts,
|
actionShortcuts,
|
||||||
} from "./actionMenu";
|
} from "./actionMenu";
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const trackAction = (
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
action.trackEvent.category,
|
action.trackEvent.category,
|
||||||
action.trackEvent.action || action.name,
|
action.trackEvent.action || action.name,
|
||||||
`${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
|
`${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,10 +119,10 @@ export class ActionManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
executeAction(
|
executeAction<T extends Action>(
|
||||||
action: Action,
|
action: T,
|
||||||
source: ActionSource = "api",
|
source: ActionSource = "api",
|
||||||
value: any = null,
|
value: Parameters<T["perform"]>[2] = null,
|
||||||
) {
|
) {
|
||||||
const elements = this.getElementsIncludingDeleted();
|
const elements = this.getElementsIncludingDeleted();
|
||||||
const appState = this.getAppState();
|
const appState = this.getAppState();
|
||||||
|
|||||||
+186
-17
@@ -1,27 +1,196 @@
|
|||||||
import { parseClipboard } from "./clipboard";
|
import {
|
||||||
|
createPasteEvent,
|
||||||
|
parseClipboard,
|
||||||
|
serializeAsClipboardJSON,
|
||||||
|
} from "./clipboard";
|
||||||
|
import { API } from "./tests/helpers/api";
|
||||||
|
|
||||||
describe("Test parseClipboard", () => {
|
describe("parseClipboard()", () => {
|
||||||
it("should parse valid json correctly", async () => {
|
it("should parse JSON as plaintext if not excalidraw-api/clipboard data", async () => {
|
||||||
let text = "123";
|
let text;
|
||||||
|
let clipboardData;
|
||||||
let clipboardData = await parseClipboard({
|
// -------------------------------------------------------------------------
|
||||||
//@ts-ignore
|
|
||||||
clipboardData: {
|
|
||||||
getData: () => text,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
text = "123";
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({ types: { "text/plain": text } }),
|
||||||
|
);
|
||||||
expect(clipboardData.text).toBe(text);
|
expect(clipboardData.text).toBe(text);
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
text = "[123]";
|
text = "[123]";
|
||||||
|
clipboardData = await parseClipboard(
|
||||||
|
createPasteEvent({ types: { "text/plain": text } }),
|
||||||
|
);
|
||||||
|
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);
|
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],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+263
-91
@@ -3,14 +3,18 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { BinaryFiles } from "./types";
|
import { BinaryFiles } from "./types";
|
||||||
import { SVG_EXPORT_TAG } from "./scene/export";
|
|
||||||
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
|
||||||
import { 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 { isInitializedImageElement } from "./element/typeChecks";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { getContainingFrame } from "./frame";
|
import { getContainingFrame } from "./frame";
|
||||||
import { isPromiseLike, isTestEnv } from "./utils";
|
import { isMemberOf, isPromiseLike } from "./utils";
|
||||||
|
import { t } from "./i18n";
|
||||||
|
|
||||||
type ElementsClipboard = {
|
type ElementsClipboard = {
|
||||||
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
|
||||||
@@ -18,17 +22,23 @@ type ElementsClipboard = {
|
|||||||
files: BinaryFiles | undefined;
|
files: BinaryFiles | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PastedMixedContent = { type: "text" | "imageUrl"; value: string }[];
|
||||||
|
|
||||||
export interface ClipboardData {
|
export interface ClipboardData {
|
||||||
spreadsheet?: Spreadsheet;
|
spreadsheet?: Spreadsheet;
|
||||||
elements?: readonly ExcalidrawElement[];
|
elements?: readonly ExcalidrawElement[];
|
||||||
files?: BinaryFiles;
|
files?: BinaryFiles;
|
||||||
text?: string;
|
text?: string;
|
||||||
|
mixedContent?: PastedMixedContent;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
programmaticAPI?: boolean;
|
programmaticAPI?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let CLIPBOARD = "";
|
type AllowedPasteMimeTypes = typeof ALLOWED_PASTE_MIME_TYPES[number];
|
||||||
let PREFER_APP_CLIPBOARD = false;
|
|
||||||
|
type ParsedClipboardEvent =
|
||||||
|
| { type: "text"; value: string }
|
||||||
|
| { type: "mixedContent"; value: PastedMixedContent };
|
||||||
|
|
||||||
export const probablySupportsClipboardReadText =
|
export const probablySupportsClipboardReadText =
|
||||||
"clipboard" in navigator && "readText" in navigator.clipboard;
|
"clipboard" in navigator && "readText" in navigator.clipboard;
|
||||||
@@ -58,10 +68,61 @@ const clipboardContainsElements = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyToClipboard = async (
|
export const createPasteEvent = ({
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
types,
|
||||||
files: BinaryFiles | null,
|
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(
|
const framesToCopy = new Set(
|
||||||
elements.filter((element) => element.type === "frame"),
|
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 = {
|
const contents: ElementsClipboard = {
|
||||||
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
type: EXPORT_DATA_TYPES.excalidrawClipboard,
|
||||||
elements: elements.map((element) => {
|
elements: elements.map((element) => {
|
||||||
@@ -102,34 +163,20 @@ export const copyToClipboard = async (
|
|||||||
}),
|
}),
|
||||||
files: files ? _files : undefined,
|
files: files ? _files : undefined,
|
||||||
};
|
};
|
||||||
const json = JSON.stringify(contents);
|
|
||||||
|
|
||||||
if (isTestEnv()) {
|
return JSON.stringify(contents);
|
||||||
return json;
|
|
||||||
}
|
|
||||||
|
|
||||||
CLIPBOARD = json;
|
|
||||||
|
|
||||||
try {
|
|
||||||
PREFER_APP_CLIPBOARD = false;
|
|
||||||
await copyTextToSystemClipboard(json);
|
|
||||||
} catch (error: any) {
|
|
||||||
PREFER_APP_CLIPBOARD = true;
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAppClipboard = (): Partial<ElementsClipboard> => {
|
export const copyToClipboard = async (
|
||||||
if (!CLIPBOARD) {
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
return {};
|
files: BinaryFiles | null,
|
||||||
}
|
/** supply if available to make the operation more certain to succeed */
|
||||||
|
clipboardEvent?: ClipboardEvent | null,
|
||||||
try {
|
) => {
|
||||||
return JSON.parse(CLIPBOARD);
|
await copyTextToSystemClipboard(
|
||||||
} catch (error: any) {
|
serializeAsClipboardJSON({ elements, files }),
|
||||||
console.error(error);
|
clipboardEvent,
|
||||||
return {};
|
);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const parsePotentialSpreadsheet = (
|
const parsePotentialSpreadsheet = (
|
||||||
@@ -142,22 +189,137 @@ const parsePotentialSpreadsheet = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** internal, specific to parsing paste events. Do not reuse. */
|
||||||
* Retrieves content from system clipboard (either from ClipboardEvent or
|
function parseHTMLTree(el: ChildNode) {
|
||||||
* via async clipboard API if supported)
|
let result: PastedMixedContent = [];
|
||||||
*/
|
for (const node of el.childNodes) {
|
||||||
export const getSystemClipboard = async (
|
if (node.nodeType === 3) {
|
||||||
event: ClipboardEvent | null,
|
const text = node.textContent?.trim();
|
||||||
): Promise<string> => {
|
if (text) {
|
||||||
try {
|
result.push({ type: "text", value: text });
|
||||||
const text = event
|
}
|
||||||
? event.clipboardData?.getData("text/plain")
|
} else if (node instanceof HTMLImageElement) {
|
||||||
: probablySupportsClipboardReadText &&
|
const url = node.getAttribute("src");
|
||||||
(await navigator.clipboard.readText());
|
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 {
|
} catch {
|
||||||
return "";
|
return { type: "text", value: "" };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,34 +327,32 @@ export const getSystemClipboard = async (
|
|||||||
* Attempts to parse clipboard. Prefers system clipboard.
|
* Attempts to parse clipboard. Prefers system clipboard.
|
||||||
*/
|
*/
|
||||||
export const parseClipboard = async (
|
export const parseClipboard = async (
|
||||||
event: ClipboardEvent | null,
|
event: ClipboardEvent,
|
||||||
isPlainPaste = false,
|
isPlainPaste = false,
|
||||||
): Promise<ClipboardData> => {
|
): Promise<ClipboardData> => {
|
||||||
const systemClipboard = await getSystemClipboard(event);
|
const parsedEventData = await parseClipboardEvent(event, isPlainPaste);
|
||||||
|
|
||||||
// if system clipboard empty, couldn't be resolved, or contains previously
|
if (parsedEventData.type === "mixedContent") {
|
||||||
// copied excalidraw scene as SVG, fall back to previously copied excalidraw
|
return {
|
||||||
// elements
|
mixedContent: parsedEventData.value,
|
||||||
if (
|
};
|
||||||
!systemClipboard ||
|
|
||||||
(!isPlainPaste && systemClipboard.includes(SVG_EXPORT_TAG))
|
|
||||||
) {
|
|
||||||
return getAppClipboard();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
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 =
|
const programmaticAPI =
|
||||||
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI;
|
||||||
if (clipboardContainsElements(systemClipboardData)) {
|
if (clipboardContainsElements(systemClipboardData)) {
|
||||||
@@ -205,18 +365,9 @@ export const parseClipboard = async (
|
|||||||
programmaticAPI,
|
programmaticAPI,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch {}
|
||||||
// system clipboard doesn't contain excalidraw elements → return plaintext
|
|
||||||
// unless we set a flag to prefer in-app clipboard because browser didn't
|
return { text: parsedEventData.value };
|
||||||
// support storing to system clipboard on copy
|
|
||||||
return PREFER_APP_CLIPBOARD && appClipboardData.elements
|
|
||||||
? {
|
|
||||||
...appClipboardData,
|
|
||||||
text: isPlainPaste
|
|
||||||
? JSON.stringify(appClipboardData.elements, null, 2)
|
|
||||||
: undefined,
|
|
||||||
}
|
|
||||||
: { text: systemClipboard };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
||||||
@@ -249,28 +400,49 @@ export const copyBlobToClipboardAsPng = async (blob: Blob | Promise<Blob>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const copyTextToSystemClipboard = async (text: string | null) => {
|
export const copyTextToSystemClipboard = async (
|
||||||
let copied = false;
|
text: string | null,
|
||||||
|
clipboardEvent?: ClipboardEvent | null,
|
||||||
|
) => {
|
||||||
|
// (1) first try using Async Clipboard API
|
||||||
if (probablySupportsClipboardWriteText) {
|
if (probablySupportsClipboardWriteText) {
|
||||||
try {
|
try {
|
||||||
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
// NOTE: doesn't work on FF on non-HTTPS domains, or when document
|
||||||
// not focused
|
// not focused
|
||||||
await navigator.clipboard.writeText(text || "");
|
await navigator.clipboard.writeText(text || "");
|
||||||
copied = true;
|
return;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that execCommand doesn't allow copying empty strings, so if we're
|
// (2) if fails and we have access to ClipboardEvent, use plain old setData()
|
||||||
// clearing clipboard using this API, we must copy at least an empty char
|
try {
|
||||||
if (!copied && !copyTextViaExecCommand(text || " ")) {
|
if (clipboardEvent) {
|
||||||
throw new Error("couldn't copy");
|
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
|
// 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 isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const textarea = document.createElement("textarea");
|
const textarea = document.createElement("textarea");
|
||||||
|
|||||||
+70
-117
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import { getNonDeletedElements } from "../element";
|
import { getNonDeletedElements } from "../element";
|
||||||
import { ExcalidrawElement, PointerType } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useDevice } from "../components/App";
|
import { useDevice } from "../components/App";
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
hasBackground,
|
hasBackground,
|
||||||
hasStrokeStyle,
|
hasStrokeStyle,
|
||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
hasText,
|
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { SHAPES } from "../shapes";
|
import { SHAPES } from "../shapes";
|
||||||
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
import { AppClassProperties, UIAppState, Zoom } from "../types";
|
||||||
@@ -20,7 +19,7 @@ import Stack from "./Stack";
|
|||||||
import { ToolButton } from "./ToolButton";
|
import { ToolButton } from "./ToolButton";
|
||||||
import { hasStrokeColor } from "../scene/comparisons";
|
import { hasStrokeColor } from "../scene/comparisons";
|
||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { hasBoundTextElement } from "../element/typeChecks";
|
import { hasBoundTextElement, isTextElement } from "../element/typeChecks";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { actionToggleZenMode } from "../actions";
|
import { actionToggleZenMode } from "../actions";
|
||||||
import { Tooltip } from "./Tooltip";
|
import { Tooltip } from "./Tooltip";
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
EmbedIcon,
|
EmbedIcon,
|
||||||
extraToolsIcon,
|
extraToolsIcon,
|
||||||
frameToolIcon,
|
frameToolIcon,
|
||||||
|
mermaidLogoIcon,
|
||||||
laserPointerToolIcon,
|
laserPointerToolIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
@@ -66,7 +66,8 @@ export const SelectedShapeActions = ({
|
|||||||
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
|
||||||
|
|
||||||
const showFillIcons =
|
const showFillIcons =
|
||||||
hasBackground(appState.activeTool.type) ||
|
(hasBackground(appState.activeTool.type) &&
|
||||||
|
!isTransparent(appState.currentItemBackgroundColor)) ||
|
||||||
targetElements.some(
|
targetElements.some(
|
||||||
(element) =>
|
(element) =>
|
||||||
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
hasBackground(element.type) && !isTransparent(element.backgroundColor),
|
||||||
@@ -123,14 +124,15 @@ export const SelectedShapeActions = ({
|
|||||||
<>{renderAction("changeRoundness")}</>
|
<>{renderAction("changeRoundness")}</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(hasText(appState.activeTool.type) ||
|
{(appState.activeTool.type === "text" ||
|
||||||
targetElements.some((element) => hasText(element.type))) && (
|
targetElements.some(isTextElement)) && (
|
||||||
<>
|
<>
|
||||||
{renderAction("changeFontSize")}
|
{renderAction("changeFontSize")}
|
||||||
|
|
||||||
{renderAction("changeFontFamily")}
|
{renderAction("changeFontFamily")}
|
||||||
|
|
||||||
{suppportsHorizontalAlign(targetElements) &&
|
{(appState.activeTool.type === "text" ||
|
||||||
|
suppportsHorizontalAlign(targetElements)) &&
|
||||||
renderAction("changeTextAlign")}
|
renderAction("changeTextAlign")}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -200,8 +202,8 @@ export const SelectedShapeActions = ({
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{t("labels.actions")}</legend>
|
<legend>{t("labels.actions")}</legend>
|
||||||
<div className="buttonList">
|
<div className="buttonList">
|
||||||
{!device.isMobile && renderAction("duplicateSelection")}
|
{!device.editor.isMobile && renderAction("duplicateSelection")}
|
||||||
{!device.isMobile && renderAction("deleteSelectedElements")}
|
{!device.editor.isMobile && renderAction("deleteSelectedElements")}
|
||||||
{renderAction("group")}
|
{renderAction("group")}
|
||||||
{renderAction("ungroup")}
|
{renderAction("ungroup")}
|
||||||
{showLinkIcon && renderAction("hyperlink")}
|
{showLinkIcon && renderAction("hyperlink")}
|
||||||
@@ -213,20 +215,15 @@ export const SelectedShapeActions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
interactiveCanvas,
|
|
||||||
activeTool,
|
activeTool,
|
||||||
onImageAction,
|
|
||||||
appState,
|
appState,
|
||||||
app,
|
app,
|
||||||
}: {
|
}: {
|
||||||
interactiveCanvas: HTMLCanvasElement | null;
|
|
||||||
activeTool: UIAppState["activeTool"];
|
activeTool: UIAppState["activeTool"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
app: AppClassProperties;
|
app: AppClassProperties;
|
||||||
}) => {
|
}) => {
|
||||||
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState(false);
|
||||||
const device = useDevice();
|
|
||||||
|
|
||||||
const frameToolSelected = activeTool.type === "frame";
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
const laserToolSelected = activeTool.type === "laser";
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
@@ -263,120 +260,76 @@ export const ShapesSwitcher = ({
|
|||||||
if (appState.activeTool.type !== value) {
|
if (appState.activeTool.type !== value) {
|
||||||
trackEvent("toolbar", value, "ui");
|
trackEvent("toolbar", value, "ui");
|
||||||
}
|
}
|
||||||
app.setActiveTool({ type: value });
|
|
||||||
if (value === "image") {
|
if (value === "image") {
|
||||||
onImageAction({ pointerType });
|
app.setActiveTool({
|
||||||
|
type: value,
|
||||||
|
insertOnCanvasDirectly: pointerType !== "mouse",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: value });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
{/* TEMP HACK because dropdown doesn't work well inside mobile toolbar */}
|
|
||||||
{device.isMobile ? (
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
<>
|
<DropdownMenu.Trigger
|
||||||
<ToolButton
|
className={clsx("App-toolbar__extra-tools-trigger", {
|
||||||
className={clsx("Shape", { fillable: false })}
|
"App-toolbar__extra-tools-trigger--selected":
|
||||||
type="radio"
|
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}
|
icon={frameToolIcon}
|
||||||
checked={activeTool.type === "frame"}
|
shortcut={KEYS.F.toLocaleUpperCase()}
|
||||||
name="editor-current-shape"
|
data-testid="toolbar-frame"
|
||||||
title={`${capitalizeString(
|
selected={frameToolSelected}
|
||||||
t("toolBar.frame"),
|
>
|
||||||
)} — ${KEYS.F.toLocaleUpperCase()}`}
|
{t("toolBar.frame")}
|
||||||
keyBindingLabel={KEYS.F.toLocaleUpperCase()}
|
</DropdownMenu.Item>
|
||||||
aria-label={capitalizeString(t("toolBar.frame"))}
|
<DropdownMenu.Item
|
||||||
aria-keyshortcuts={KEYS.F.toLocaleUpperCase()}
|
onSelect={() => app.setActiveTool({ type: "embeddable" })}
|
||||||
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"
|
|
||||||
icon={EmbedIcon}
|
icon={EmbedIcon}
|
||||||
checked={activeTool.type === "embeddable"}
|
data-testid="toolbar-embeddable"
|
||||||
name="editor-current-shape"
|
selected={embeddableToolSelected}
|
||||||
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")}
|
|
||||||
>
|
>
|
||||||
{extraToolsIcon}
|
{t("toolBar.embeddable")}
|
||||||
</DropdownMenu.Trigger>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Item
|
||||||
onClickOutside={() => setIsExtraToolsMenuOpen(false)}
|
onSelect={() => app.setActiveTool({ type: "laser" })}
|
||||||
onSelect={() => setIsExtraToolsMenuOpen(false)}
|
icon={laserPointerToolIcon}
|
||||||
className="App-toolbar__extra-tools-dropdown"
|
data-testid="toolbar-laser"
|
||||||
|
selected={laserToolSelected}
|
||||||
|
shortcut={KEYS.K.toLocaleUpperCase()}
|
||||||
>
|
>
|
||||||
<DropdownMenu.Item
|
{t("toolBar.laser")}
|
||||||
onSelect={() => {
|
</DropdownMenu.Item>
|
||||||
app.setActiveTool({ type: "frame" });
|
<DropdownMenu.Item
|
||||||
}}
|
onSelect={() => app.setOpenDialog("mermaid")}
|
||||||
icon={frameToolIcon}
|
icon={mermaidLogoIcon}
|
||||||
shortcut={KEYS.F.toLocaleUpperCase()}
|
data-testid="toolbar-embeddable"
|
||||||
data-testid="toolbar-frame"
|
>
|
||||||
selected={frameToolSelected}
|
{t("toolBar.mermaidToExcalidraw")}
|
||||||
>
|
</DropdownMenu.Item>
|
||||||
{t("toolBar.frame")}
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+339
-157
@@ -47,7 +47,7 @@ import {
|
|||||||
isEraserActive,
|
isEraserActive,
|
||||||
isHandToolActive,
|
isHandToolActive,
|
||||||
} from "../appState";
|
} from "../appState";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { PastedMixedContent, parseClipboard } from "../clipboard";
|
||||||
import {
|
import {
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
CURSOR_TYPE,
|
CURSOR_TYPE,
|
||||||
@@ -74,7 +74,6 @@ import {
|
|||||||
MQ_MAX_WIDTH_LANDSCAPE,
|
MQ_MAX_WIDTH_LANDSCAPE,
|
||||||
MQ_MAX_WIDTH_PORTRAIT,
|
MQ_MAX_WIDTH_PORTRAIT,
|
||||||
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
|
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
|
||||||
MQ_SM_MAX_WIDTH,
|
|
||||||
POINTER_BUTTON,
|
POINTER_BUTTON,
|
||||||
ROUNDNESS,
|
ROUNDNESS,
|
||||||
SCROLL_TIMEOUT,
|
SCROLL_TIMEOUT,
|
||||||
@@ -88,7 +87,7 @@ import {
|
|||||||
ZOOM_STEP,
|
ZOOM_STEP,
|
||||||
POINTER_EVENTS,
|
POINTER_EVENTS,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { exportCanvas, loadFromBlob } from "../data";
|
import { ExportedElements, exportCanvas, loadFromBlob } from "../data";
|
||||||
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
|
||||||
import { restore, restoreElements } from "../data/restore";
|
import { restore, restoreElements } from "../data/restore";
|
||||||
import {
|
import {
|
||||||
@@ -241,7 +240,6 @@ import {
|
|||||||
isInputLike,
|
isInputLike,
|
||||||
isToolIcon,
|
isToolIcon,
|
||||||
isWritableElement,
|
isWritableElement,
|
||||||
resolvablePromise,
|
|
||||||
sceneCoordsToViewportCoords,
|
sceneCoordsToViewportCoords,
|
||||||
tupleToCoors,
|
tupleToCoors,
|
||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
@@ -275,6 +273,7 @@ import {
|
|||||||
generateIdFromFile,
|
generateIdFromFile,
|
||||||
getDataURL,
|
getDataURL,
|
||||||
getFileFromEvent,
|
getFileFromEvent,
|
||||||
|
ImageURLToFile,
|
||||||
isImageFileHandle,
|
isImageFileHandle,
|
||||||
isSupportedImageFile,
|
isSupportedImageFile,
|
||||||
loadSceneOrLibraryFromBlob,
|
loadSceneOrLibraryFromBlob,
|
||||||
@@ -317,7 +316,7 @@ import { shouldShowBoundingBox } from "../element/transformHandles";
|
|||||||
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
||||||
import { Fonts } from "../scene/Fonts";
|
import { Fonts } from "../scene/Fonts";
|
||||||
import {
|
import {
|
||||||
getFrameElements,
|
getFrameChildren,
|
||||||
isCursorInFrame,
|
isCursorInFrame,
|
||||||
bindElementsToFramesAfterDuplication,
|
bindElementsToFramesAfterDuplication,
|
||||||
addElementsToFrame,
|
addElementsToFrame,
|
||||||
@@ -365,6 +364,7 @@ import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
|||||||
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
import { StaticCanvas, InteractiveCanvas } from "./canvases";
|
||||||
import { Renderer } from "../scene/Renderer";
|
import { Renderer } from "../scene/Renderer";
|
||||||
import { ShapeCache } from "../scene/ShapeCache";
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
import MermaidToExcalidraw from "./MermaidToExcalidraw";
|
||||||
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
import { LaserToolOverlay } from "./LaserTool/LaserTool";
|
||||||
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
import { LaserPathManager } from "./LaserTool/LaserPathManager";
|
||||||
import {
|
import {
|
||||||
@@ -373,16 +373,21 @@ import {
|
|||||||
resetCursor,
|
resetCursor,
|
||||||
setCursorForShape,
|
setCursorForShape,
|
||||||
} from "../cursor";
|
} from "../cursor";
|
||||||
|
import { Emitter } from "../emitter";
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
isSmScreen: false,
|
viewport: {
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
|
isLandscape: false,
|
||||||
|
},
|
||||||
|
editor: {
|
||||||
|
isMobile: false,
|
||||||
|
canFitSidebar: false,
|
||||||
|
},
|
||||||
isTouchScreen: false,
|
isTouchScreen: false,
|
||||||
canDeviceFitSidebar: false,
|
|
||||||
isLandscape: false,
|
|
||||||
};
|
};
|
||||||
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
const DeviceContext = React.createContext<Device>(deviceContextInitialValue);
|
||||||
DeviceContext.displayName = "DeviceContext";
|
DeviceContext.displayName = "DeviceContext";
|
||||||
@@ -433,6 +438,9 @@ export const useExcalidrawSetAppState = () =>
|
|||||||
export const useExcalidrawActionManager = () =>
|
export const useExcalidrawActionManager = () =>
|
||||||
useContext(ExcalidrawActionManagerContext);
|
useContext(ExcalidrawActionManagerContext);
|
||||||
|
|
||||||
|
const supportsResizeObserver =
|
||||||
|
typeof window !== "undefined" && "ResizeObserver" in window;
|
||||||
|
|
||||||
let didTapTwice: boolean = false;
|
let didTapTwice: boolean = false;
|
||||||
let tappedTwiceTimer = 0;
|
let tappedTwiceTimer = 0;
|
||||||
let isHoldingSpace: boolean = false;
|
let isHoldingSpace: boolean = false;
|
||||||
@@ -469,7 +477,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
unmounted: boolean = false;
|
unmounted: boolean = false;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
device: Device = deviceContextInitialValue;
|
device: Device = deviceContextInitialValue;
|
||||||
detachIsMobileMqHandler?: () => void;
|
|
||||||
|
|
||||||
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
private excalidrawContainerRef = React.createRef<HTMLDivElement>();
|
||||||
|
|
||||||
@@ -504,11 +511,35 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
laserPathManager: LaserPathManager = new LaserPathManager(this);
|
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) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
const {
|
const {
|
||||||
excalidrawRef,
|
excalidrawAPI,
|
||||||
viewModeEnabled = false,
|
viewModeEnabled = false,
|
||||||
zenModeEnabled = false,
|
zenModeEnabled = false,
|
||||||
gridModeEnabled = false,
|
gridModeEnabled = false,
|
||||||
@@ -539,14 +570,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.rc = rough.canvas(this.canvas);
|
this.rc = rough.canvas(this.canvas);
|
||||||
this.renderer = new Renderer(this.scene);
|
this.renderer = new Renderer(this.scene);
|
||||||
|
|
||||||
if (excalidrawRef) {
|
if (excalidrawAPI) {
|
||||||
const readyPromise =
|
|
||||||
("current" in excalidrawRef && excalidrawRef.current?.readyPromise) ||
|
|
||||||
resolvablePromise<ExcalidrawImperativeAPI>();
|
|
||||||
|
|
||||||
const api: ExcalidrawImperativeAPI = {
|
const api: ExcalidrawImperativeAPI = {
|
||||||
ready: true,
|
|
||||||
readyPromise,
|
|
||||||
updateScene: this.updateScene,
|
updateScene: this.updateScene,
|
||||||
updateLibrary: this.library.updateLibrary,
|
updateLibrary: this.library.updateLibrary,
|
||||||
addFiles: this.addFiles,
|
addFiles: this.addFiles,
|
||||||
@@ -567,13 +592,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
resetCursor: this.resetCursor,
|
resetCursor: this.resetCursor,
|
||||||
updateFrameRendering: this.updateFrameRendering,
|
updateFrameRendering: this.updateFrameRendering,
|
||||||
toggleSidebar: this.toggleSidebar,
|
toggleSidebar: this.toggleSidebar,
|
||||||
|
onChange: (cb) => this.onChangeEmitter.on(cb),
|
||||||
|
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
|
||||||
|
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
|
||||||
} as const;
|
} as const;
|
||||||
if (typeof excalidrawRef === "function") {
|
if (typeof excalidrawAPI === "function") {
|
||||||
excalidrawRef(api);
|
excalidrawAPI(api);
|
||||||
} else {
|
} else {
|
||||||
excalidrawRef.current = api;
|
console.error("excalidrawAPI should be a function!");
|
||||||
}
|
}
|
||||||
readyPromise.resolve(api);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.excalidrawContainerValue = {
|
this.excalidrawContainerValue = {
|
||||||
@@ -1013,12 +1040,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { x: x2 } = sceneCoordsToViewportCoords(
|
|
||||||
{ sceneX: f.x + f.width, sceneY: f.y + f.height },
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
|
|
||||||
const FRAME_NAME_GAP = 20;
|
|
||||||
const FRAME_NAME_EDIT_PADDING = 6;
|
const FRAME_NAME_EDIT_PADDING = 6;
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
@@ -1063,13 +1084,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
boxShadow: "inset 0 0 0 1px var(--color-primary)",
|
boxShadow: "inset 0 0 0 1px var(--color-primary)",
|
||||||
fontFamily: "Assistant",
|
fontFamily: "Assistant",
|
||||||
fontSize: "14px",
|
fontSize: "14px",
|
||||||
transform: `translateY(-${FRAME_NAME_EDIT_PADDING}px)`,
|
transform: `translate(-${FRAME_NAME_EDIT_PADDING}px, ${FRAME_NAME_EDIT_PADDING}px)`,
|
||||||
color: "var(--color-gray-80)",
|
color: "var(--color-gray-80)",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
maxWidth: `${Math.min(
|
maxWidth: `${
|
||||||
x2 - x1 - FRAME_NAME_EDIT_PADDING,
|
document.body.clientWidth - x1 - FRAME_NAME_EDIT_PADDING
|
||||||
document.body.clientWidth - x1 - FRAME_NAME_EDIT_PADDING,
|
}px`,
|
||||||
)}px`,
|
|
||||||
}}
|
}}
|
||||||
size={frameNameInEdit.length + 1 || 1}
|
size={frameNameInEdit.length + 1 || 1}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
@@ -1091,19 +1111,26 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
key={f.id}
|
key={f.id}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
top: `${y1 - FRAME_NAME_GAP - this.state.offsetTop}px`,
|
// Positioning from bottom so that we don't to either
|
||||||
left: `${
|
// calculate text height or adjust using transform (which)
|
||||||
x1 -
|
// messes up input position when editing the frame name.
|
||||||
this.state.offsetLeft -
|
// This makes the positioning deterministic and we can calculate
|
||||||
(this.state.editingFrame === f.id ? FRAME_NAME_EDIT_PADDING : 0)
|
// the same position when rendering to canvas / svg.
|
||||||
|
bottom: `${
|
||||||
|
this.state.height +
|
||||||
|
FRAME_STYLE.nameOffsetY -
|
||||||
|
y1 +
|
||||||
|
this.state.offsetTop
|
||||||
}px`,
|
}px`,
|
||||||
|
left: `${x1 - this.state.offsetLeft}px`,
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
fontSize: "14px",
|
fontSize: FRAME_STYLE.nameFontSize,
|
||||||
color: isDarkTheme
|
color: isDarkTheme
|
||||||
? "var(--color-gray-60)"
|
? FRAME_STYLE.nameColorDarkTheme
|
||||||
: "var(--color-gray-50)",
|
: FRAME_STYLE.nameColorLightTheme,
|
||||||
|
lineHeight: FRAME_STYLE.nameLineHeight,
|
||||||
width: "max-content",
|
width: "max-content",
|
||||||
maxWidth: `${x2 - x1 + FRAME_NAME_EDIT_PADDING * 2}px`,
|
maxWidth: `${f.width}px`,
|
||||||
overflow: f.id === this.state.editingFrame ? "visible" : "hidden",
|
overflow: f.id === this.state.editingFrame ? "visible" : "hidden",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
@@ -1146,24 +1173,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pendingImageElementId: this.state.pendingImageElementId,
|
pendingImageElementId: this.state.pendingImageElementId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const shouldBlockPointerEvents =
|
||||||
|
!(
|
||||||
|
this.state.editingElement && isLinearElement(this.state.editingElement)
|
||||||
|
) &&
|
||||||
|
(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)));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx("excalidraw excalidraw-container", {
|
className={clsx("excalidraw excalidraw-container", {
|
||||||
"excalidraw--view-mode": this.state.viewModeEnabled,
|
"excalidraw--view-mode": this.state.viewModeEnabled,
|
||||||
"excalidraw--mobile": this.device.isMobile,
|
"excalidraw--mobile": this.device.editor.isMobile,
|
||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
["--ui-pointerEvents" as any]:
|
["--ui-pointerEvents" as any]: shouldBlockPointerEvents
|
||||||
this.state.selectionElement ||
|
? POINTER_EVENTS.disabled
|
||||||
this.state.draggingElement ||
|
: POINTER_EVENTS.enabled,
|
||||||
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
|
|
||||||
: POINTER_EVENTS.enabled,
|
|
||||||
}}
|
}}
|
||||||
ref={this.excalidrawContainerRef}
|
ref={this.excalidrawContainerRef}
|
||||||
onDrop={this.handleAppOnDrop}
|
onDrop={this.handleAppOnDrop}
|
||||||
@@ -1188,7 +1220,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
>
|
>
|
||||||
<LayerUI
|
<LayerUI
|
||||||
canvas={this.canvas}
|
canvas={this.canvas}
|
||||||
interactiveCanvas={this.interactiveCanvas}
|
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
files={this.files}
|
files={this.files}
|
||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
@@ -1205,7 +1236,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.zenModeEnabled
|
this.state.zenModeEnabled
|
||||||
}
|
}
|
||||||
UIOptions={this.props.UIOptions}
|
UIOptions={this.props.UIOptions}
|
||||||
onImageAction={this.onImageAction}
|
|
||||||
onExportImage={this.onExportImage}
|
onExportImage={this.onExportImage}
|
||||||
renderWelcomeScreen={
|
renderWelcomeScreen={
|
||||||
!this.state.isLoading &&
|
!this.state.isLoading &&
|
||||||
@@ -1218,7 +1248,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isCollaborating={this.props.isCollaborating}
|
isCollaborating={this.props.isCollaborating}
|
||||||
>
|
>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
|
{this.state.openDialog === "mermaid" && (
|
||||||
|
<MermaidToExcalidraw />
|
||||||
|
)}
|
||||||
</LayerUI>
|
</LayerUI>
|
||||||
|
|
||||||
<div className="excalidraw-textEditorContainer" />
|
<div className="excalidraw-textEditorContainer" />
|
||||||
<div className="excalidraw-contextMenuContainer" />
|
<div className="excalidraw-contextMenuContainer" />
|
||||||
<div className="excalidraw-eye-dropper-container" />
|
<div className="excalidraw-eye-dropper-container" />
|
||||||
@@ -1248,6 +1282,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
top={this.state.contextMenu.top}
|
top={this.state.contextMenu.top}
|
||||||
left={this.state.contextMenu.left}
|
left={this.state.contextMenu.left}
|
||||||
actionManager={this.actionManager}
|
actionManager={this.actionManager}
|
||||||
|
onClose={(callback) => {
|
||||||
|
this.setState({ contextMenu: null }, () => {
|
||||||
|
this.focusContainer();
|
||||||
|
callback?.();
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<StaticCanvas
|
<StaticCanvas
|
||||||
@@ -1327,7 +1367,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
public onExportImage = async (
|
public onExportImage = async (
|
||||||
type: keyof typeof EXPORT_IMAGE_TYPES,
|
type: keyof typeof EXPORT_IMAGE_TYPES,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: ExportedElements,
|
||||||
|
opts: { exportingFrame: ExcalidrawFrameElement | null },
|
||||||
) => {
|
) => {
|
||||||
trackEvent("export", type, "ui");
|
trackEvent("export", type, "ui");
|
||||||
const fileHandle = await exportCanvas(
|
const fileHandle = await exportCanvas(
|
||||||
@@ -1339,6 +1380,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
exportBackground: this.state.exportBackground,
|
exportBackground: this.state.exportBackground,
|
||||||
name: this.state.name,
|
name: this.state.name,
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
viewBackgroundColor: this.state.viewBackgroundColor,
|
||||||
|
exportingFrame: opts.exportingFrame,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.catch(muteFSAbortError)
|
.catch(muteFSAbortError)
|
||||||
@@ -1619,20 +1661,62 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private refreshDeviceState = (container: HTMLDivElement) => {
|
private isMobileBreakpoint = (width: number, height: number) => {
|
||||||
const { width, height } = container.getBoundingClientRect();
|
return (
|
||||||
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||||
|
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private refreshViewportBreakpoints = () => {
|
||||||
|
const container = this.excalidrawContainerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { clientWidth: viewportWidth, clientHeight: viewportHeight } =
|
||||||
|
document.body;
|
||||||
|
|
||||||
|
const prevViewportState = this.device.viewport;
|
||||||
|
|
||||||
|
const nextViewportState = updateObject(prevViewportState, {
|
||||||
|
isLandscape: viewportWidth > viewportHeight,
|
||||||
|
isMobile: this.isMobileBreakpoint(viewportWidth, viewportHeight),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prevViewportState !== nextViewportState) {
|
||||||
|
this.device = { ...this.device, viewport: nextViewportState };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
private refreshEditorBreakpoints = () => {
|
||||||
|
const container = this.excalidrawContainerRef.current;
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width: editorWidth, height: editorHeight } =
|
||||||
|
container.getBoundingClientRect();
|
||||||
|
|
||||||
const sidebarBreakpoint =
|
const sidebarBreakpoint =
|
||||||
this.props.UIOptions.dockedSidebarBreakpoint != null
|
this.props.UIOptions.dockedSidebarBreakpoint != null
|
||||||
? this.props.UIOptions.dockedSidebarBreakpoint
|
? this.props.UIOptions.dockedSidebarBreakpoint
|
||||||
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
: MQ_RIGHT_SIDEBAR_MIN_WIDTH;
|
||||||
this.device = updateObject(this.device, {
|
|
||||||
isLandscape: width > height,
|
const prevEditorState = this.device.editor;
|
||||||
isSmScreen: width < MQ_SM_MAX_WIDTH,
|
|
||||||
isMobile:
|
const nextEditorState = updateObject(prevEditorState, {
|
||||||
width < MQ_MAX_WIDTH_PORTRAIT ||
|
isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
|
||||||
(height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE),
|
canFitSidebar: editorWidth > sidebarBreakpoint,
|
||||||
canDeviceFitSidebar: width > sidebarBreakpoint,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (prevEditorState !== nextEditorState) {
|
||||||
|
this.device = { ...this.device, editor: nextEditorState };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
@@ -1674,52 +1758,21 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.excalidrawContainerRef.current &&
|
|
||||||
// bounding rects don't work in tests so updating
|
// bounding rects don't work in tests so updating
|
||||||
// the state on init would result in making the test enviro run
|
// the state on init would result in making the test enviro run
|
||||||
// in mobile breakpoint (0 width/height), making everything fail
|
// in mobile breakpoint (0 width/height), making everything fail
|
||||||
!isTestEnv()
|
!isTestEnv()
|
||||||
) {
|
) {
|
||||||
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
this.refreshViewportBreakpoints();
|
||||||
|
this.refreshEditorBreakpoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
if (supportsResizeObserver && this.excalidrawContainerRef.current) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
// recompute device dimensions state
|
this.refreshEditorBreakpoints();
|
||||||
// ---------------------------------------------------------------------
|
|
||||||
this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
|
||||||
// refresh offsets
|
|
||||||
// ---------------------------------------------------------------------
|
|
||||||
this.updateDOMRect();
|
this.updateDOMRect();
|
||||||
});
|
});
|
||||||
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
|
||||||
} else if (window.matchMedia) {
|
|
||||||
const mdScreenQuery = window.matchMedia(
|
|
||||||
`(max-width: ${MQ_MAX_WIDTH_PORTRAIT}px), (max-height: ${MQ_MAX_HEIGHT_LANDSCAPE}px) and (max-width: ${MQ_MAX_WIDTH_LANDSCAPE}px)`,
|
|
||||||
);
|
|
||||||
const smScreenQuery = window.matchMedia(
|
|
||||||
`(max-width: ${MQ_SM_MAX_WIDTH}px)`,
|
|
||||||
);
|
|
||||||
const canDeviceFitSidebarMediaQuery = window.matchMedia(
|
|
||||||
`(min-width: ${
|
|
||||||
// NOTE this won't update if a different breakpoint is supplied
|
|
||||||
// after mount
|
|
||||||
this.props.UIOptions.dockedSidebarBreakpoint != null
|
|
||||||
? this.props.UIOptions.dockedSidebarBreakpoint
|
|
||||||
: MQ_RIGHT_SIDEBAR_MIN_WIDTH
|
|
||||||
}px)`,
|
|
||||||
);
|
|
||||||
const handler = () => {
|
|
||||||
this.excalidrawContainerRef.current!.getBoundingClientRect();
|
|
||||||
this.device = updateObject(this.device, {
|
|
||||||
isSmScreen: smScreenQuery.matches,
|
|
||||||
isMobile: mdScreenQuery.matches,
|
|
||||||
canDeviceFitSidebar: canDeviceFitSidebarMediaQuery.matches,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
mdScreenQuery.addListener(handler);
|
|
||||||
this.detachIsMobileMqHandler = () =>
|
|
||||||
mdScreenQuery.removeListener(handler);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
const searchParams = new URLSearchParams(window.location.search.slice(1));
|
||||||
@@ -1751,6 +1804,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.destroy();
|
this.scene.destroy();
|
||||||
this.library.destroy();
|
this.library.destroy();
|
||||||
this.laserPathManager.destroy();
|
this.laserPathManager.destroy();
|
||||||
|
this.onChangeEmitter.destroy();
|
||||||
ShapeCache.destroy();
|
ShapeCache.destroy();
|
||||||
SnapCache.destroy();
|
SnapCache.destroy();
|
||||||
clearTimeout(touchTimeout);
|
clearTimeout(touchTimeout);
|
||||||
@@ -1763,6 +1817,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene
|
this.scene
|
||||||
.getElementsIncludingDeleted()
|
.getElementsIncludingDeleted()
|
||||||
.forEach((element) => ShapeCache.delete(element));
|
.forEach((element) => ShapeCache.delete(element));
|
||||||
|
this.refreshViewportBreakpoints();
|
||||||
|
this.updateDOMRect();
|
||||||
|
if (!supportsResizeObserver) {
|
||||||
|
this.refreshEditorBreakpoints();
|
||||||
|
}
|
||||||
this.setState({});
|
this.setState({});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1816,7 +1875,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.detachIsMobileMqHandler?.();
|
|
||||||
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
|
window.removeEventListener(EVENT.MESSAGE, this.onWindowMessage, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1901,11 +1959,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.excalidrawContainerRef.current &&
|
|
||||||
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
prevProps.UIOptions.dockedSidebarBreakpoint !==
|
||||||
this.props.UIOptions.dockedSidebarBreakpoint
|
this.props.UIOptions.dockedSidebarBreakpoint
|
||||||
) {
|
) {
|
||||||
this.refreshDeviceState(this.excalidrawContainerRef.current);
|
this.refreshEditorBreakpoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -2035,6 +2092,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
this.state,
|
||||||
this.files,
|
this.files,
|
||||||
);
|
);
|
||||||
|
this.onChangeEmitter.trigger(
|
||||||
|
this.scene.getElementsIncludingDeleted(),
|
||||||
|
this.state,
|
||||||
|
this.files,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2077,7 +2139,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cutAll();
|
this.actionManager.executeAction(actionCut, "keyboard", event);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
@@ -2089,19 +2151,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
if (!isExcalidrawActive || isWritableElement(event.target)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.copyAll();
|
this.actionManager.executeAction(actionCopy, "keyboard", event);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
private cutAll = () => {
|
|
||||||
this.actionManager.executeAction(actionCut, "keyboard");
|
|
||||||
};
|
|
||||||
|
|
||||||
private copyAll = () => {
|
|
||||||
this.actionManager.executeAction(actionCopy, "keyboard");
|
|
||||||
};
|
|
||||||
|
|
||||||
private static resetTapTwice() {
|
private static resetTapTwice() {
|
||||||
didTapTwice = false;
|
didTapTwice = false;
|
||||||
}
|
}
|
||||||
@@ -2162,8 +2216,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public pasteFromClipboard = withBatchedUpdates(
|
public pasteFromClipboard = withBatchedUpdates(
|
||||||
async (event: ClipboardEvent | null) => {
|
async (event: ClipboardEvent) => {
|
||||||
const isPlainPaste = !!(IS_PLAIN_PASTE && event);
|
const isPlainPaste = !!IS_PLAIN_PASTE;
|
||||||
|
|
||||||
// #686
|
// #686
|
||||||
const target = document.activeElement;
|
const target = document.activeElement;
|
||||||
@@ -2185,21 +2239,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
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(
|
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
{
|
{
|
||||||
clientX: this.lastViewportPosition.x,
|
clientX: this.lastViewportPosition.x,
|
||||||
@@ -2208,6 +2247,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state,
|
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)
|
// prefer spreadsheet data over image file (MS Office/Libre Office)
|
||||||
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
if (isSupportedImageFile(file) && !data.spreadsheet) {
|
||||||
const imageElement = this.createImageElement({ sceneX, sceneY });
|
const imageElement = this.createImageElement({ sceneX, sceneY });
|
||||||
@@ -2261,6 +2323,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
const maybeUrl = extractSrc(data.text);
|
const maybeUrl = extractSrc(data.text);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!isPlainPaste &&
|
!isPlainPaste &&
|
||||||
embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) &&
|
embeddableURLValidator(maybeUrl, this.props.validateEmbeddable) &&
|
||||||
@@ -2284,11 +2347,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
private addElementsFromPasteOrLibrary = (opts: {
|
addElementsFromPasteOrLibrary = (opts: {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
files: BinaryFiles | null;
|
files: BinaryFiles | null;
|
||||||
position: { clientX: number; clientY: number } | "cursor" | "center";
|
position: { clientX: number; clientY: number } | "cursor" | "center";
|
||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
const elements = restoreElements(opts.elements, null, undefined);
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
@@ -2364,7 +2428,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// from library, not when pasting from clipboard. Alas.
|
// from library, not when pasting from clipboard. Alas.
|
||||||
openSidebar:
|
openSidebar:
|
||||||
this.state.openSidebar &&
|
this.state.openSidebar &&
|
||||||
this.device.canDeviceFitSidebar &&
|
this.device.editor.canFitSidebar &&
|
||||||
jotaiStore.get(isSidebarDockedAtom)
|
jotaiStore.get(isSidebarDockedAtom)
|
||||||
? this.state.openSidebar
|
? this.state.openSidebar
|
||||||
: null,
|
: null,
|
||||||
@@ -2393,8 +2457,93 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.setActiveTool({ type: "selection" });
|
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) {
|
private addTextFromPaste(text: string, isPlainPaste = false) {
|
||||||
const { x, y } = viewportCoordsToSceneCoords(
|
const { x, y } = viewportCoordsToSceneCoords(
|
||||||
{
|
{
|
||||||
@@ -2493,7 +2642,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isPlainPaste &&
|
!isPlainPaste &&
|
||||||
textElements.length > 1 &&
|
textElements.length > 1 &&
|
||||||
PLAIN_PASTE_TOAST_SHOWN === false &&
|
PLAIN_PASTE_TOAST_SHOWN === false &&
|
||||||
!this.device.isMobile
|
!this.device.editor.isMobile
|
||||||
) {
|
) {
|
||||||
this.setToast({
|
this.setToast({
|
||||||
message: t("toast.pasteAsSingleElement", {
|
message: t("toast.pasteAsSingleElement", {
|
||||||
@@ -2527,7 +2676,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"toolbar",
|
"toolbar",
|
||||||
"toggleLock",
|
"toggleLock",
|
||||||
`${source} (${this.device.isMobile ? "mobile" : "desktop"})`,
|
`${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
@@ -2567,7 +2716,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
togglePenMode = (force?: boolean) => {
|
togglePenMode = (force: boolean | null) => {
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
return {
|
return {
|
||||||
penMode: force ?? !prevState.penMode,
|
penMode: force ?? !prevState.penMode,
|
||||||
@@ -3022,7 +3171,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"toolbar",
|
"toolbar",
|
||||||
shape,
|
shape,
|
||||||
`keyboard (${this.device.isMobile ? "mobile" : "desktop"})`,
|
`keyboard (${
|
||||||
|
this.device.editor.isMobile ? "mobile" : "desktop"
|
||||||
|
})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.setActiveTool({ type: shape });
|
this.setActiveTool({ type: shape });
|
||||||
@@ -3134,11 +3285,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setActiveTool = (
|
setActiveTool = (
|
||||||
tool:
|
tool: (
|
||||||
| {
|
| (
|
||||||
type: ToolType;
|
| { type: Exclude<ToolType, "image"> }
|
||||||
}
|
| {
|
||||||
| { type: "custom"; customType: string },
|
type: Extract<ToolType, "image">;
|
||||||
|
insertOnCanvasDirectly?: boolean;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
| { type: "custom"; customType: string }
|
||||||
|
) & { locked?: boolean },
|
||||||
) => {
|
) => {
|
||||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||||
if (nextActiveTool.type === "hand") {
|
if (nextActiveTool.type === "hand") {
|
||||||
@@ -3153,7 +3309,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ suggestedBindings: [] });
|
this.setState({ suggestedBindings: [] });
|
||||||
}
|
}
|
||||||
if (nextActiveTool.type === "image") {
|
if (nextActiveTool.type === "image") {
|
||||||
this.onImageAction();
|
this.onImageAction({
|
||||||
|
insertOnCanvasDirectly:
|
||||||
|
(tool.type === "image" && tool.insertOnCanvasDirectly) ?? false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
@@ -3181,6 +3340,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setOpenDialog = (dialogType: AppState["openDialog"]) => {
|
||||||
|
this.setState({ openDialog: dialogType });
|
||||||
|
};
|
||||||
|
|
||||||
private setCursor = (cursor: string) => {
|
private setCursor = (cursor: string) => {
|
||||||
setCursor(this.interactiveCanvas, cursor);
|
setCursor(this.interactiveCanvas, cursor);
|
||||||
};
|
};
|
||||||
@@ -3744,7 +3907,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element,
|
element,
|
||||||
this.state,
|
this.state,
|
||||||
[scenePointer.x, scenePointer.y],
|
[scenePointer.x, scenePointer.y],
|
||||||
this.device.isMobile,
|
this.device.editor.isMobile,
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -3776,7 +3939,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.state,
|
this.state,
|
||||||
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
||||||
this.device.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||||
this.lastPointerUpEvent!,
|
this.lastPointerUpEvent!,
|
||||||
@@ -3786,7 +3949,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement,
|
this.hitLinkElement,
|
||||||
this.state,
|
this.state,
|
||||||
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
||||||
this.device.isMobile,
|
this.device.editor.isMobile,
|
||||||
);
|
);
|
||||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||||
let url = this.hitLinkElement.link;
|
let url = this.hitLinkElement.link;
|
||||||
@@ -4131,6 +4294,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||||
scenePointer,
|
scenePointer,
|
||||||
hitElement,
|
hitElement,
|
||||||
@@ -4397,6 +4561,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleCanvasPointerDown = (
|
private handleCanvasPointerDown = (
|
||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -4586,7 +4751,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState,
|
pointerDownState,
|
||||||
);
|
);
|
||||||
} else if (this.state.activeTool.type === "custom") {
|
} 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") {
|
} else if (this.state.activeTool.type === "frame") {
|
||||||
this.createFrameElementOnPointerDown(pointerDownState);
|
this.createFrameElementOnPointerDown(pointerDownState);
|
||||||
} else if (this.state.activeTool.type === "laser") {
|
} else if (this.state.activeTool.type === "laser") {
|
||||||
@@ -4605,6 +4770,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.props?.onPointerDown?.(this.state.activeTool, pointerDownState);
|
this.props?.onPointerDown?.(this.state.activeTool, pointerDownState);
|
||||||
|
this.onPointerDownEmitter.trigger(
|
||||||
|
this.state.activeTool,
|
||||||
|
pointerDownState,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
|
||||||
const onPointerMove =
|
const onPointerMove =
|
||||||
this.onPointerMoveFromPointerDownHandler(pointerDownState);
|
this.onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||||
@@ -4641,7 +4811,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
const clicklength =
|
const clicklength =
|
||||||
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
|
||||||
if (this.device.isMobile && clicklength < 300) {
|
if (this.device.editor.isMobile && clicklength < 300) {
|
||||||
const hitElement = this.getElementAtPosition(
|
const hitElement = this.getElementAtPosition(
|
||||||
scenePointer.x,
|
scenePointer.x,
|
||||||
scenePointer.y,
|
scenePointer.y,
|
||||||
@@ -5159,7 +5329,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
// if hitElement is frame, deselect all of its elements if they are selected
|
// if hitElement is frame, deselect all of its elements if they are selected
|
||||||
if (hitElement.type === "frame") {
|
if (hitElement.type === "frame") {
|
||||||
getFrameElements(
|
getFrameChildren(
|
||||||
previouslySelectedElements,
|
previouslySelectedElements,
|
||||||
hitElement.id,
|
hitElement.id,
|
||||||
).forEach((element) => {
|
).forEach((element) => {
|
||||||
@@ -6457,6 +6627,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ pendingImageElementId: null });
|
this.setState({ pendingImageElementId: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.onPointerUpEmitter.trigger(
|
||||||
|
this.state.activeTool,
|
||||||
|
pointerDownState,
|
||||||
|
childEvent,
|
||||||
|
);
|
||||||
|
|
||||||
if (draggingElement?.type === "freedraw") {
|
if (draggingElement?.type === "freedraw") {
|
||||||
const pointerCoords = viewportCoordsToSceneCoords(
|
const pointerCoords = viewportCoordsToSceneCoords(
|
||||||
childEvent,
|
childEvent,
|
||||||
@@ -6692,10 +6868,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
topLayerFrame &&
|
topLayerFrame &&
|
||||||
!this.state.selectedElementIds[topLayerFrame.id]
|
!this.state.selectedElementIds[topLayerFrame.id]
|
||||||
) {
|
) {
|
||||||
|
const processedGroupIds = new Map<string, boolean>();
|
||||||
const elementsToAdd = selectedElements.filter(
|
const elementsToAdd = selectedElements.filter(
|
||||||
(element) =>
|
(element) =>
|
||||||
element.frameId !== topLayerFrame.id &&
|
element.frameId !== topLayerFrame.id &&
|
||||||
isElementInFrame(element, nextElements, this.state),
|
isElementInFrame(element, nextElements, this.state, {
|
||||||
|
processedGroupIds,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.editingGroupId) {
|
if (this.state.editingGroupId) {
|
||||||
@@ -7298,7 +7477,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.scene.addNewElement(imageElement);
|
this.scene.addNewElement(imageElement);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.initializeImage({
|
return await this.initializeImage({
|
||||||
imageFile,
|
imageFile,
|
||||||
imageElement,
|
imageElement,
|
||||||
showCursorImagePreview,
|
showCursorImagePreview,
|
||||||
@@ -7311,6 +7490,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
errorMessage: error.message || t("errors.imageInsertError"),
|
errorMessage: error.message || t("errors.imageInsertError"),
|
||||||
});
|
});
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -7353,9 +7533,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onImageAction = async (
|
private onImageAction = async ({
|
||||||
{ insertOnCanvasDirectly } = { insertOnCanvasDirectly: false },
|
insertOnCanvasDirectly,
|
||||||
) => {
|
}: {
|
||||||
|
insertOnCanvasDirectly: boolean;
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const clientX = this.state.width / 2 + this.state.offsetLeft;
|
const clientX = this.state.width / 2 + this.state.offsetLeft;
|
||||||
const clientY = this.state.height / 2 + this.state.offsetTop;
|
const clientY = this.state.height / 2 + this.state.offsetTop;
|
||||||
@@ -8014,7 +8196,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
>();
|
>();
|
||||||
|
|
||||||
selectedFrames.forEach((frame) => {
|
selectedFrames.forEach((frame) => {
|
||||||
const elementsInFrame = getFrameElements(
|
const elementsInFrame = getFrameChildren(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
frame.id,
|
frame.id,
|
||||||
);
|
);
|
||||||
@@ -8084,7 +8266,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const elementsToHighlight = new Set<ExcalidrawElement>();
|
const elementsToHighlight = new Set<ExcalidrawElement>();
|
||||||
selectedFrames.forEach((frame) => {
|
selectedFrames.forEach((frame) => {
|
||||||
const elementsInFrame = getFrameElements(
|
const elementsInFrame = getFrameChildren(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
frame.id,
|
frame.id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export const ColorInput = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* TODO reenable on mobile with a better UX */}
|
{/* TODO reenable on mobile with a better UX */}
|
||||||
{!device.isMobile && (
|
{!device.editor.isMobile && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const ColorPickerPopupContent = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { container } = useExcalidrawContainer();
|
const { container } = useExcalidrawContainer();
|
||||||
const { isMobile, isLandscape } = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
const colorInputJSX = (
|
const colorInputJSX = (
|
||||||
<div>
|
<div>
|
||||||
@@ -136,8 +136,16 @@ const ColorPickerPopupContent = ({
|
|||||||
updateData({ openPopup: null });
|
updateData({ openPopup: null });
|
||||||
setActiveColorPickerSection(null);
|
setActiveColorPickerSection(null);
|
||||||
}}
|
}}
|
||||||
side={isMobile && !isLandscape ? "bottom" : "right"}
|
side={
|
||||||
align={isMobile && !isLandscape ? "center" : "start"}
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "bottom"
|
||||||
|
: "right"
|
||||||
|
}
|
||||||
|
align={
|
||||||
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
? "center"
|
||||||
|
: "start"
|
||||||
|
}
|
||||||
alignOffset={-16}
|
alignOffset={-16}
|
||||||
sideOffset={20}
|
sideOffset={20}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const TopPicks = ({
|
|||||||
type="button"
|
type="button"
|
||||||
title={color}
|
title={color}
|
||||||
onClick={() => onChange(color)}
|
onClick={() => onChange(color)}
|
||||||
|
data-testid={`color-top-pick-${color}`}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import {
|
|||||||
} from "../actions/shortcuts";
|
} from "../actions/shortcuts";
|
||||||
import { Action } from "../actions/types";
|
import { Action } from "../actions/types";
|
||||||
import { ActionManager } from "../actions/manager";
|
import { ActionManager } from "../actions/manager";
|
||||||
import {
|
import { useExcalidrawAppState, useExcalidrawElements } from "./App";
|
||||||
useExcalidrawAppState,
|
|
||||||
useExcalidrawElements,
|
|
||||||
useExcalidrawSetAppState,
|
|
||||||
} from "./App";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
export type ContextMenuItem = typeof CONTEXT_MENU_SEPARATOR | Action;
|
||||||
@@ -25,14 +21,14 @@ type ContextMenuProps = {
|
|||||||
items: ContextMenuItems;
|
items: ContextMenuItems;
|
||||||
top: number;
|
top: number;
|
||||||
left: number;
|
left: number;
|
||||||
|
onClose: (callback?: () => void) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CONTEXT_MENU_SEPARATOR = "separator";
|
export const CONTEXT_MENU_SEPARATOR = "separator";
|
||||||
|
|
||||||
export const ContextMenu = React.memo(
|
export const ContextMenu = React.memo(
|
||||||
({ actionManager, items, top, left }: ContextMenuProps) => {
|
({ actionManager, items, top, left, onClose }: ContextMenuProps) => {
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
|
||||||
const elements = useExcalidrawElements();
|
const elements = useExcalidrawElements();
|
||||||
|
|
||||||
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
const filteredItems = items.reduce((acc: ContextMenuItem[], item) => {
|
||||||
@@ -54,7 +50,9 @@ export const ContextMenu = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
onCloseRequest={() => setAppState({ contextMenu: null })}
|
onCloseRequest={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
top={top}
|
top={top}
|
||||||
left={left}
|
left={left}
|
||||||
fitInViewport={true}
|
fitInViewport={true}
|
||||||
@@ -102,7 +100,7 @@ export const ContextMenu = React.memo(
|
|||||||
// we need update state before executing the action in case
|
// we need update state before executing the action in case
|
||||||
// the action uses the appState it's being passed (that still
|
// the action uses the appState it's being passed (that still
|
||||||
// contains a defined contextMenu) to return the next state.
|
// contains a defined contextMenu) to return the next state.
|
||||||
setAppState({ contextMenu: null }, () => {
|
onClose(() => {
|
||||||
actionManager.executeAction(item, "contextMenu");
|
actionManager.executeAction(item, "contextMenu");
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -33,14 +33,16 @@
|
|||||||
color: var(--color-gray-40);
|
color: var(--color-gray-40);
|
||||||
}
|
}
|
||||||
|
|
||||||
@include isMobile {
|
|
||||||
top: 1.25rem;
|
|
||||||
right: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Dialog--fullscreen {
|
||||||
|
.Dialog__close {
|
||||||
|
top: 1.25rem;
|
||||||
|
right: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
|
||||||
const [lastActiveElement] = useState(document.activeElement);
|
const [lastActiveElement] = useState(document.activeElement);
|
||||||
const { id } = useExcalidrawContainer();
|
const { id } = useExcalidrawContainer();
|
||||||
const device = useDevice();
|
const isFullscreen = useDevice().viewport.isMobile;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!islandNode) {
|
if (!islandNode) {
|
||||||
@@ -101,7 +101,9 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className={clsx("Dialog", props.className)}
|
className={clsx("Dialog", props.className, {
|
||||||
|
"Dialog--fullscreen": isFullscreen,
|
||||||
|
})}
|
||||||
labelledBy="dialog-title"
|
labelledBy="dialog-title"
|
||||||
maxWidth={getDialogSize(props.size)}
|
maxWidth={getDialogSize(props.size)}
|
||||||
onCloseRequest={onClose}
|
onCloseRequest={onClose}
|
||||||
@@ -119,7 +121,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
title={t("buttons.close")}
|
title={t("buttons.close")}
|
||||||
aria-label={t("buttons.close")}
|
aria-label={t("buttons.close")}
|
||||||
>
|
>
|
||||||
{device.isMobile ? back : CloseIcon}
|
{isFullscreen ? back : CloseIcon}
|
||||||
</button>
|
</button>
|
||||||
<div className="Dialog__content">{props.children}</div>
|
<div className="Dialog__content">{props.children}</div>
|
||||||
</Island>
|
</Island>
|
||||||
|
|||||||
@@ -254,7 +254,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("helpDialog.movePageLeftRight")}
|
label={t("helpDialog.movePageLeftRight")}
|
||||||
shortcuts={["Shift+PgUp/PgDn"]}
|
shortcuts={["Shift+PgUp/PgDn"]}
|
||||||
/>
|
/>
|
||||||
<Shortcut label={t("buttons.fullScreen")} shortcuts={["F"]} />
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("buttons.zenMode")}
|
label={t("buttons.zenMode")}
|
||||||
shortcuts={[getShortcutKey("Alt+Z")]}
|
shortcuts={[getShortcutKey("Alt+Z")]}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
|
|||||||
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
|
||||||
const multiMode = appState.multiElement !== null;
|
const multiMode = appState.multiElement !== null;
|
||||||
|
|
||||||
if (appState.openSidebar && !device.canDeviceFitSidebar) {
|
if (appState.openSidebar && !device.editor.canFitSidebar) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { canvasToBlob } from "../data/blob";
|
|||||||
import { nativeFileSystemSupported } from "../data/filesystem";
|
import { nativeFileSystemSupported } from "../data/filesystem";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
import { isSomeElementSelected } from "../scene";
|
||||||
import { exportToCanvas } from "../packages/utils";
|
import { exportToCanvas } from "../packages/utils";
|
||||||
|
|
||||||
import { copyIcon, downloadIcon, helpIcon } from "./icons";
|
import { copyIcon, downloadIcon, helpIcon } from "./icons";
|
||||||
@@ -34,6 +34,8 @@ import { Tooltip } from "./Tooltip";
|
|||||||
import "./ImageExportDialog.scss";
|
import "./ImageExportDialog.scss";
|
||||||
import { useAppProps } from "./App";
|
import { useAppProps } from "./App";
|
||||||
import { FilledButton } from "./FilledButton";
|
import { FilledButton } from "./FilledButton";
|
||||||
|
import { cloneJSON } from "../utils";
|
||||||
|
import { prepareElementsForExport } from "../data";
|
||||||
|
|
||||||
const supportsContextFilters =
|
const supportsContextFilters =
|
||||||
"filter" in document.createElement("canvas").getContext("2d")!;
|
"filter" in document.createElement("canvas").getContext("2d")!;
|
||||||
@@ -51,44 +53,47 @@ export const ErrorCanvasPreview = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ImageExportModalProps = {
|
type ImageExportModalProps = {
|
||||||
appState: UIAppState;
|
appStateSnapshot: Readonly<UIAppState>;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elementsSnapshot: readonly NonDeletedExcalidrawElement[];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ImageExportModal = ({
|
const ImageExportModal = ({
|
||||||
appState,
|
appStateSnapshot,
|
||||||
elements,
|
elementsSnapshot,
|
||||||
files,
|
files,
|
||||||
actionManager,
|
actionManager,
|
||||||
onExportImage,
|
onExportImage,
|
||||||
}: ImageExportModalProps) => {
|
}: ImageExportModalProps) => {
|
||||||
|
const hasSelection = isSomeElementSelected(
|
||||||
|
elementsSnapshot,
|
||||||
|
appStateSnapshot,
|
||||||
|
);
|
||||||
|
|
||||||
const appProps = useAppProps();
|
const appProps = useAppProps();
|
||||||
const [projectName, setProjectName] = useState(appState.name);
|
const [projectName, setProjectName] = useState(appStateSnapshot.name);
|
||||||
|
const [exportSelectionOnly, setExportSelectionOnly] = useState(hasSelection);
|
||||||
const someElementIsSelected = isSomeElementSelected(elements, appState);
|
|
||||||
|
|
||||||
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
|
|
||||||
const [exportWithBackground, setExportWithBackground] = useState(
|
const [exportWithBackground, setExportWithBackground] = useState(
|
||||||
appState.exportBackground,
|
appStateSnapshot.exportBackground,
|
||||||
);
|
);
|
||||||
const [exportDarkMode, setExportDarkMode] = useState(
|
const [exportDarkMode, setExportDarkMode] = useState(
|
||||||
appState.exportWithDarkMode,
|
appStateSnapshot.exportWithDarkMode,
|
||||||
);
|
);
|
||||||
const [embedScene, setEmbedScene] = useState(appState.exportEmbedScene);
|
const [embedScene, setEmbedScene] = useState(
|
||||||
const [exportScale, setExportScale] = useState(appState.exportScale);
|
appStateSnapshot.exportEmbedScene,
|
||||||
|
);
|
||||||
|
const [exportScale, setExportScale] = useState(appStateSnapshot.exportScale);
|
||||||
|
|
||||||
const previewRef = useRef<HTMLDivElement>(null);
|
const previewRef = useRef<HTMLDivElement>(null);
|
||||||
const [renderError, setRenderError] = useState<Error | null>(null);
|
const [renderError, setRenderError] = useState<Error | null>(null);
|
||||||
|
|
||||||
const exportedElements = exportSelected
|
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||||
? getSelectedElements(elements, appState, {
|
elementsSnapshot,
|
||||||
includeBoundTextElement: true,
|
appStateSnapshot,
|
||||||
includeElementsInFrames: true,
|
exportSelectionOnly,
|
||||||
})
|
);
|
||||||
: elements;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const previewNode = previewRef.current;
|
const previewNode = previewRef.current;
|
||||||
@@ -102,10 +107,18 @@ const ImageExportModal = ({
|
|||||||
}
|
}
|
||||||
exportToCanvas({
|
exportToCanvas({
|
||||||
elements: exportedElements,
|
elements: exportedElements,
|
||||||
appState,
|
appState: {
|
||||||
|
...appStateSnapshot,
|
||||||
|
name: projectName,
|
||||||
|
exportBackground: exportWithBackground,
|
||||||
|
exportWithDarkMode: exportDarkMode,
|
||||||
|
exportScale,
|
||||||
|
exportEmbedScene: embedScene,
|
||||||
|
},
|
||||||
files,
|
files,
|
||||||
exportPadding: DEFAULT_EXPORT_PADDING,
|
exportPadding: DEFAULT_EXPORT_PADDING,
|
||||||
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
|
||||||
|
exportingFrame,
|
||||||
})
|
})
|
||||||
.then((canvas) => {
|
.then((canvas) => {
|
||||||
setRenderError(null);
|
setRenderError(null);
|
||||||
@@ -119,7 +132,17 @@ const ImageExportModal = ({
|
|||||||
console.error(error);
|
console.error(error);
|
||||||
setRenderError(error);
|
setRenderError(error);
|
||||||
});
|
});
|
||||||
}, [appState, files, exportedElements]);
|
}, [
|
||||||
|
appStateSnapshot,
|
||||||
|
files,
|
||||||
|
exportedElements,
|
||||||
|
exportingFrame,
|
||||||
|
projectName,
|
||||||
|
exportWithBackground,
|
||||||
|
exportDarkMode,
|
||||||
|
exportScale,
|
||||||
|
embedScene,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ImageExportModal">
|
<div className="ImageExportModal">
|
||||||
@@ -136,7 +159,8 @@ const ImageExportModal = ({
|
|||||||
value={projectName}
|
value={projectName}
|
||||||
style={{ width: "30ch" }}
|
style={{ width: "30ch" }}
|
||||||
disabled={
|
disabled={
|
||||||
typeof appProps.name !== "undefined" || appState.viewModeEnabled
|
typeof appProps.name !== "undefined" ||
|
||||||
|
appStateSnapshot.viewModeEnabled
|
||||||
}
|
}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setProjectName(event.target.value);
|
setProjectName(event.target.value);
|
||||||
@@ -152,16 +176,16 @@ const ImageExportModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="ImageExportModal__settings">
|
<div className="ImageExportModal__settings">
|
||||||
<h3>{t("imageExportDialog.header")}</h3>
|
<h3>{t("imageExportDialog.header")}</h3>
|
||||||
{someElementIsSelected && (
|
{hasSelection && (
|
||||||
<ExportSetting
|
<ExportSetting
|
||||||
label={t("imageExportDialog.label.onlySelected")}
|
label={t("imageExportDialog.label.onlySelected")}
|
||||||
name="exportOnlySelected"
|
name="exportOnlySelected"
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
name="exportOnlySelected"
|
name="exportOnlySelected"
|
||||||
checked={exportSelected}
|
checked={exportSelectionOnly}
|
||||||
onChange={(checked) => {
|
onChange={(checked) => {
|
||||||
setExportSelected(checked);
|
setExportSelectionOnly(checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ExportSetting>
|
</ExportSetting>
|
||||||
@@ -243,7 +267,9 @@ const ImageExportModal = ({
|
|||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.exportToPng")}
|
label={t("imageExportDialog.title.exportToPng")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements)
|
onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements, {
|
||||||
|
exportingFrame,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
startIcon={downloadIcon}
|
startIcon={downloadIcon}
|
||||||
>
|
>
|
||||||
@@ -253,7 +279,9 @@ const ImageExportModal = ({
|
|||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.exportToSvg")}
|
label={t("imageExportDialog.title.exportToSvg")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements)
|
onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements, {
|
||||||
|
exportingFrame,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
startIcon={downloadIcon}
|
startIcon={downloadIcon}
|
||||||
>
|
>
|
||||||
@@ -264,7 +292,9 @@ const ImageExportModal = ({
|
|||||||
className="ImageExportModal__settings__buttons__button"
|
className="ImageExportModal__settings__buttons__button"
|
||||||
label={t("imageExportDialog.title.copyPngToClipboard")}
|
label={t("imageExportDialog.title.copyPngToClipboard")}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements)
|
onExportImage(EXPORT_IMAGE_TYPES.clipboard, exportedElements, {
|
||||||
|
exportingFrame,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
startIcon={copyIcon}
|
startIcon={copyIcon}
|
||||||
>
|
>
|
||||||
@@ -325,15 +355,20 @@ export const ImageExportDialog = ({
|
|||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
if (appState.openDialog !== "imageExport") {
|
// we need to take a snapshot so that the exported state can't be modified
|
||||||
return null;
|
// while the dialog is open
|
||||||
}
|
const [{ appStateSnapshot, elementsSnapshot }] = useState(() => {
|
||||||
|
return {
|
||||||
|
appStateSnapshot: cloneJSON(appState),
|
||||||
|
elementsSnapshot: cloneJSON(elements),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog onCloseRequest={onCloseRequest} size="wide" title={false}>
|
<Dialog onCloseRequest={onCloseRequest} size="wide" title={false}>
|
||||||
<ImageExportModal
|
<ImageExportModal
|
||||||
elements={elements}
|
elementsSnapshot={elementsSnapshot}
|
||||||
appState={appState}
|
appStateSnapshot={appStateSnapshot}
|
||||||
files={files}
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onExportImage={onExportImage}
|
onExportImage={onExportImage}
|
||||||
|
|||||||
@@ -23,12 +23,15 @@ export type ExportCB = (
|
|||||||
const JSONExportModal = ({
|
const JSONExportModal = ({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState,
|
||||||
|
setAppState,
|
||||||
files,
|
files,
|
||||||
actionManager,
|
actionManager,
|
||||||
exportOpts,
|
exportOpts,
|
||||||
canvas,
|
canvas,
|
||||||
|
onCloseRequest,
|
||||||
}: {
|
}: {
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@@ -72,9 +75,14 @@ const JSONExportModal = ({
|
|||||||
title={t("exportDialog.link_button")}
|
title={t("exportDialog.link_button")}
|
||||||
aria-label={t("exportDialog.link_button")}
|
aria-label={t("exportDialog.link_button")}
|
||||||
showAriaLabel={true}
|
showAriaLabel={true}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
onExportToBackend(elements, appState, files, canvas);
|
try {
|
||||||
trackEvent("export", "link", `ui (${getFrame()})`);
|
trackEvent("export", "link", `ui (${getFrame()})`);
|
||||||
|
await onExportToBackend(elements, appState, files, canvas);
|
||||||
|
onCloseRequest();
|
||||||
|
} catch (error: any) {
|
||||||
|
setAppState({ errorMessage: error.message });
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -114,6 +122,7 @@ export const JSONExportDialog = ({
|
|||||||
<JSONExportModal
|
<JSONExportModal
|
||||||
elements={elements}
|
elements={elements}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
|
setAppState={setAppState}
|
||||||
files={files}
|
files={files}
|
||||||
actionManager={actionManager}
|
actionManager={actionManager}
|
||||||
onCloseRequest={handleClose}
|
onCloseRequest={handleClose}
|
||||||
|
|||||||
+16
-35
@@ -62,18 +62,16 @@ interface LayerUIProps {
|
|||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
interactiveCanvas: HTMLCanvasElement | null;
|
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||||
showExitZenModeBtn: boolean;
|
showExitZenModeBtn: boolean;
|
||||||
langCode: Language["code"];
|
langCode: Language["code"];
|
||||||
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
|
||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
UIOptions: AppProps["UIOptions"];
|
UIOptions: AppProps["UIOptions"];
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
|
||||||
onExportImage: AppClassProperties["onExportImage"];
|
onExportImage: AppClassProperties["onExportImage"];
|
||||||
renderWelcomeScreen: boolean;
|
renderWelcomeScreen: boolean;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -123,7 +121,6 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
interactiveCanvas,
|
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
@@ -131,7 +128,6 @@ const LayerUI = ({
|
|||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
UIOptions,
|
UIOptions,
|
||||||
onImageAction,
|
|
||||||
onExportImage,
|
onExportImage,
|
||||||
renderWelcomeScreen,
|
renderWelcomeScreen,
|
||||||
children,
|
children,
|
||||||
@@ -165,7 +161,10 @@ const LayerUI = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderImageExportDialog = () => {
|
const renderImageExportDialog = () => {
|
||||||
if (!UIOptions.canvasActions.saveAsImage) {
|
if (
|
||||||
|
!UIOptions.canvasActions.saveAsImage ||
|
||||||
|
appState.openDialog !== "imageExport"
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +249,7 @@ const LayerUI = ({
|
|||||||
>
|
>
|
||||||
<HintViewer
|
<HintViewer
|
||||||
appState={appState}
|
appState={appState}
|
||||||
isMobile={device.isMobile}
|
isMobile={device.editor.isMobile}
|
||||||
device={device}
|
device={device}
|
||||||
app={app}
|
app={app}
|
||||||
/>
|
/>
|
||||||
@@ -259,7 +258,7 @@ const LayerUI = ({
|
|||||||
<PenModeButton
|
<PenModeButton
|
||||||
zenModeEnabled={appState.zenModeEnabled}
|
zenModeEnabled={appState.zenModeEnabled}
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={onPenModeToggle}
|
onChange={() => onPenModeToggle(null)}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
/>
|
/>
|
||||||
@@ -280,14 +279,8 @@ const LayerUI = ({
|
|||||||
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
interactiveCanvas={interactiveCanvas}
|
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
app={app}
|
app={app}
|
||||||
onImageAction={({ pointerType }) => {
|
|
||||||
onImageAction({
|
|
||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
@@ -324,7 +317,7 @@ const LayerUI = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<UserList collaborators={appState.collaborators} />
|
<UserList collaborators={appState.collaborators} />
|
||||||
{renderTopRightUI?.(device.isMobile, appState)}
|
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
||||||
{!appState.viewModeEnabled &&
|
{!appState.viewModeEnabled &&
|
||||||
// hide button when sidebar docked
|
// hide button when sidebar docked
|
||||||
(!isSidebarDocked ||
|
(!isSidebarDocked ||
|
||||||
@@ -345,7 +338,7 @@ const LayerUI = ({
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"sidebar",
|
"sidebar",
|
||||||
`toggleDock (${docked ? "dock" : "undock"})`,
|
`toggleDock (${docked ? "dock" : "undock"})`,
|
||||||
`(${device.isMobile ? "mobile" : "desktop"})`,
|
`(${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -373,7 +366,7 @@ const LayerUI = ({
|
|||||||
trackEvent(
|
trackEvent(
|
||||||
"sidebar",
|
"sidebar",
|
||||||
`${DEFAULT_SIDEBAR.name} (open)`,
|
`${DEFAULT_SIDEBAR.name} (open)`,
|
||||||
`button (${device.isMobile ? "mobile" : "desktop"})`,
|
`button (${device.editor.isMobile ? "mobile" : "desktop"})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -390,7 +383,7 @@ const LayerUI = ({
|
|||||||
{appState.errorMessage}
|
{appState.errorMessage}
|
||||||
</ErrorDialog>
|
</ErrorDialog>
|
||||||
)}
|
)}
|
||||||
{eyeDropperState && !device.isMobile && (
|
{eyeDropperState && !device.editor.isMobile && (
|
||||||
<EyeDropper
|
<EyeDropper
|
||||||
colorPickerType={eyeDropperState.colorPickerType}
|
colorPickerType={eyeDropperState.colorPickerType}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
@@ -460,7 +453,7 @@ const LayerUI = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{device.isMobile && (
|
{device.editor.isMobile && (
|
||||||
<MobileMenu
|
<MobileMenu
|
||||||
app={app}
|
app={app}
|
||||||
appState={appState}
|
appState={appState}
|
||||||
@@ -472,8 +465,6 @@ const LayerUI = ({
|
|||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
onHandToolToggle={onHandToolToggle}
|
onHandToolToggle={onHandToolToggle}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
interactiveCanvas={interactiveCanvas}
|
|
||||||
onImageAction={onImageAction}
|
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
renderSidebars={renderSidebars}
|
renderSidebars={renderSidebars}
|
||||||
@@ -481,14 +472,14 @@ const LayerUI = ({
|
|||||||
renderWelcomeScreen={renderWelcomeScreen}
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!device.isMobile && (
|
{!device.editor.isMobile && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="layer-ui__wrapper"
|
className="layer-ui__wrapper"
|
||||||
style={
|
style={
|
||||||
appState.openSidebar &&
|
appState.openSidebar &&
|
||||||
isSidebarDocked &&
|
isSidebarDocked &&
|
||||||
device.canDeviceFitSidebar
|
device.editor.canFitSidebar
|
||||||
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
|
||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
@@ -560,18 +551,8 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps;
|
||||||
canvas: _pC,
|
const { canvas: _nC, appState: nextAppState, ...next } = nextProps;
|
||||||
interactiveCanvas: _pIC,
|
|
||||||
appState: prevAppState,
|
|
||||||
...prev
|
|
||||||
} = prevProps;
|
|
||||||
const {
|
|
||||||
canvas: _nC,
|
|
||||||
interactiveCanvas: _nIC,
|
|
||||||
appState: nextAppState,
|
|
||||||
...next
|
|
||||||
} = nextProps;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isShallowEqual(
|
isShallowEqual(
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export const LibraryUnit = memo(
|
|||||||
}, [svg]);
|
}, [svg]);
|
||||||
|
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const isMobile = useDevice().isMobile;
|
const isMobile = useDevice().editor.isMobile;
|
||||||
const adder = isPending && (
|
const adder = isPending && (
|
||||||
<div className="library-unit__adder">{PlusIcon}</div>
|
<div className="library-unit__adder">{PlusIcon}</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -35,10 +35,8 @@ type MobileMenuProps = {
|
|||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: AppClassProperties["togglePenMode"];
|
||||||
interactiveCanvas: HTMLCanvasElement | null;
|
|
||||||
|
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
isMobile: boolean,
|
isMobile: boolean,
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
@@ -58,8 +56,7 @@ export const MobileMenu = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
interactiveCanvas,
|
|
||||||
onImageAction,
|
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
@@ -85,14 +82,8 @@ export const MobileMenu = ({
|
|||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
interactiveCanvas={interactiveCanvas}
|
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
app={app}
|
app={app}
|
||||||
onImageAction={({ pointerType }) => {
|
|
||||||
onImageAction({
|
|
||||||
insertOnCanvasDirectly: pointerType !== "mouse",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Stack.Row>
|
</Stack.Row>
|
||||||
</Island>
|
</Island>
|
||||||
@@ -103,7 +94,7 @@ export const MobileMenu = ({
|
|||||||
)}
|
)}
|
||||||
<PenModeButton
|
<PenModeButton
|
||||||
checked={appState.penMode}
|
checked={appState.penMode}
|
||||||
onChange={onPenModeToggle}
|
onChange={() => onPenModeToggle(null)}
|
||||||
title={t("toolBar.penMode")}
|
title={t("toolBar.penMode")}
|
||||||
isMobile
|
isMobile
|
||||||
penDetected={appState.penDetected}
|
penDetected={appState.penDetected}
|
||||||
|
|||||||
@@ -59,12 +59,6 @@
|
|||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include isMobile {
|
|
||||||
max-width: 100%;
|
|
||||||
border: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes Modal__background__fade-in {
|
@keyframes Modal__background__fade-in {
|
||||||
@@ -105,7 +99,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@include isMobile {
|
.Dialog--fullscreen {
|
||||||
.Modal {
|
.Modal {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -116,6 +110,9 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,11 +113,11 @@ export const SidebarInner = forwardRef(
|
|||||||
if ((event.target as Element).closest(".sidebar-trigger")) {
|
if ((event.target as Element).closest(".sidebar-trigger")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!docked || !device.canDeviceFitSidebar) {
|
if (!docked || !device.editor.canFitSidebar) {
|
||||||
closeLibrary();
|
closeLibrary();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[closeLibrary, docked, device.canDeviceFitSidebar],
|
[closeLibrary, docked, device.editor.canFitSidebar],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ export const SidebarInner = forwardRef(
|
|||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
event.key === KEYS.ESCAPE &&
|
event.key === KEYS.ESCAPE &&
|
||||||
(!docked || !device.canDeviceFitSidebar)
|
(!docked || !device.editor.canFitSidebar)
|
||||||
) {
|
) {
|
||||||
closeLibrary();
|
closeLibrary();
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ export const SidebarInner = forwardRef(
|
|||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
|
||||||
};
|
};
|
||||||
}, [closeLibrary, docked, device.canDeviceFitSidebar]);
|
}, [closeLibrary, docked, device.editor.canFitSidebar]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Island
|
<Island
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const SidebarHeader = ({
|
|||||||
const props = useContext(SidebarPropsContext);
|
const props = useContext(SidebarPropsContext);
|
||||||
|
|
||||||
const renderDockButton = !!(
|
const renderDockButton = !!(
|
||||||
device.canDeviceFitSidebar && props.shouldRenderDockButton
|
device.editor.canFitSidebar && props.shouldRenderDockButton
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -83,12 +83,12 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => () => {
|
isMountedRef.current = true;
|
||||||
|
return () => {
|
||||||
isMountedRef.current = false;
|
isMountedRef.current = false;
|
||||||
},
|
};
|
||||||
[],
|
}, []);
|
||||||
);
|
|
||||||
|
|
||||||
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
const lastPointerTypeRef = useRef<PointerType | null>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,15 @@
|
|||||||
width: var(--lg-button-size);
|
width: var(--lg-button-size);
|
||||||
height: 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 {
|
svg {
|
||||||
width: var(--lg-icon-size);
|
width: var(--lg-icon-size);
|
||||||
height: var(--lg-icon-size);
|
height: var(--lg-icon-size);
|
||||||
|
|||||||
@@ -16,6 +16,10 @@
|
|||||||
align-self: center;
|
align-self: center;
|
||||||
background-color: var(--default-border-color);
|
background-color: var(--default-border-color);
|
||||||
margin: 0 0.25rem;
|
margin: 0 0.25rem;
|
||||||
|
|
||||||
|
@include isMobile {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,5 +45,6 @@
|
|||||||
margin-top: 0.375rem;
|
margin-top: 0.375rem;
|
||||||
right: 0;
|
right: 0;
|
||||||
min-width: 11.875rem;
|
min-width: 11.875rem;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
|
||||||
&--mobile {
|
&--mobile {
|
||||||
bottom: 55px;
|
|
||||||
top: auto;
|
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
row-gap: 0.75rem;
|
row-gap: 0.75rem;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const MenuContent = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const classNames = clsx(`dropdown-menu ${className}`, {
|
const classNames = clsx(`dropdown-menu ${className}`, {
|
||||||
"dropdown-menu--mobile": device.isMobile,
|
"dropdown-menu--mobile": device.editor.isMobile,
|
||||||
}).trim();
|
}).trim();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,7 +43,7 @@ const MenuContent = ({
|
|||||||
>
|
>
|
||||||
{/* the zIndex ensures this menu has higher stacking order,
|
{/* the zIndex ensures this menu has higher stacking order,
|
||||||
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
see https://github.com/excalidraw/excalidraw/pull/1445 */}
|
||||||
{device.isMobile ? (
|
{device.editor.isMobile ? (
|
||||||
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
<Stack.Col className="dropdown-menu-container">{children}</Stack.Col>
|
||||||
) : (
|
) : (
|
||||||
<Island
|
<Island
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const MenuItemContent = ({
|
|||||||
<>
|
<>
|
||||||
<div className="dropdown-menu-item__icon">{icon}</div>
|
<div className="dropdown-menu-item__icon">{icon}</div>
|
||||||
<div className="dropdown-menu-item__text">{children}</div>
|
<div className="dropdown-menu-item__text">{children}</div>
|
||||||
{shortcut && !device.isMobile && (
|
{shortcut && !device.editor.isMobile && (
|
||||||
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
<div className="dropdown-menu-item__shortcut">{shortcut}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const MenuTrigger = ({
|
|||||||
`dropdown-menu-button ${className}`,
|
`dropdown-menu-button ${className}`,
|
||||||
"zen-mode-transition",
|
"zen-mode-transition",
|
||||||
{
|
{
|
||||||
"dropdown-menu-button--mobile": device.isMobile,
|
"dropdown-menu-button--mobile": device.editor.isMobile,
|
||||||
},
|
},
|
||||||
).trim();
|
).trim();
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1654,6 +1654,22 @@ export const frameToolIcon = createIcon(
|
|||||||
tablerIconProps,
|
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(
|
export const laserPointerToolIcon = createIcon(
|
||||||
<g
|
<g
|
||||||
fill="none"
|
fill="none"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const MainMenu = Object.assign(
|
|||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const appState = useUIAppState();
|
const appState = useUIAppState();
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const onClickOutside = device.isMobile
|
const onClickOutside = device.editor.isMobile
|
||||||
? undefined
|
? undefined
|
||||||
: () => setAppState({ openMenu: null });
|
: () => setAppState({ openMenu: null });
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ const MainMenu = Object.assign(
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{device.isMobile && appState.collaborators.size > 0 && (
|
{device.editor.isMobile && appState.collaborators.size > 0 && (
|
||||||
<fieldset className="UserList-Wrapper">
|
<fieldset className="UserList-Wrapper">
|
||||||
<legend>{t("labels.collaborators")}</legend>
|
<legend>{t("labels.collaborators")}</legend>
|
||||||
<UserList
|
<UserList
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const WelcomeScreenMenuItemContent = ({
|
|||||||
<>
|
<>
|
||||||
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
<div className="welcome-screen-menu-item__icon">{icon}</div>
|
||||||
<div className="welcome-screen-menu-item__text">{children}</div>
|
<div className="welcome-screen-menu-item__text">{children}</div>
|
||||||
{shortcut && !device.isMobile && (
|
{shortcut && !device.editor.isMobile && (
|
||||||
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
<div className="welcome-screen-menu-item__shortcut">{shortcut}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
+24
-6
@@ -105,6 +105,7 @@ export const FONT_FAMILY = {
|
|||||||
Virgil: 1,
|
Virgil: 1,
|
||||||
Helvetica: 2,
|
Helvetica: 2,
|
||||||
Cascadia: 3,
|
Cascadia: 3,
|
||||||
|
Assistant: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const THEME = {
|
export const THEME = {
|
||||||
@@ -114,13 +115,18 @@ export const THEME = {
|
|||||||
|
|
||||||
export const FRAME_STYLE = {
|
export const FRAME_STYLE = {
|
||||||
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
strokeColor: "#bbb" as ExcalidrawElement["strokeColor"],
|
||||||
strokeWidth: 1 as ExcalidrawElement["strokeWidth"],
|
strokeWidth: 2 as ExcalidrawElement["strokeWidth"],
|
||||||
strokeStyle: "solid" as ExcalidrawElement["strokeStyle"],
|
strokeStyle: "solid" as ExcalidrawElement["strokeStyle"],
|
||||||
fillStyle: "solid" as ExcalidrawElement["fillStyle"],
|
fillStyle: "solid" as ExcalidrawElement["fillStyle"],
|
||||||
roughness: 0 as ExcalidrawElement["roughness"],
|
roughness: 0 as ExcalidrawElement["roughness"],
|
||||||
roundness: null as ExcalidrawElement["roundness"],
|
roundness: null as ExcalidrawElement["roundness"],
|
||||||
backgroundColor: "transparent" as ExcalidrawElement["backgroundColor"],
|
backgroundColor: "transparent" as ExcalidrawElement["backgroundColor"],
|
||||||
radius: 8,
|
radius: 8,
|
||||||
|
nameOffsetY: 3,
|
||||||
|
nameColorLightTheme: "#999999",
|
||||||
|
nameColorDarkTheme: "#7a7a7a",
|
||||||
|
nameFontSize: 14,
|
||||||
|
nameLineHeight: 1.25,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
|
||||||
@@ -148,6 +154,8 @@ export const IMAGE_MIME_TYPES = {
|
|||||||
jfif: "image/jfif",
|
jfif: "image/jfif",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const ALLOWED_PASTE_MIME_TYPES = ["text/plain", "text/html"] as const;
|
||||||
|
|
||||||
export const MIME_TYPES = {
|
export const MIME_TYPES = {
|
||||||
json: "application/json",
|
json: "application/json",
|
||||||
// excalidraw data
|
// excalidraw data
|
||||||
@@ -218,8 +226,6 @@ export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
|
|||||||
|
|
||||||
// breakpoints
|
// breakpoints
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// sm screen
|
|
||||||
export const MQ_SM_MAX_WIDTH = 640;
|
|
||||||
// md screen
|
// md screen
|
||||||
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
export const MQ_MAX_WIDTH_PORTRAIT = 730;
|
||||||
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
|
||||||
@@ -296,6 +302,18 @@ export const ROUNDNESS = {
|
|||||||
* collaboration */
|
* collaboration */
|
||||||
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
|
||||||
|
|
||||||
|
export const ROUGHNESS = {
|
||||||
|
architect: 0,
|
||||||
|
artist: 1,
|
||||||
|
cartoonist: 2,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const STROKE_WIDTH = {
|
||||||
|
thin: 1,
|
||||||
|
bold: 2,
|
||||||
|
extraBold: 4,
|
||||||
|
} as const;
|
||||||
|
|
||||||
export const DEFAULT_ELEMENT_PROPS: {
|
export const DEFAULT_ELEMENT_PROPS: {
|
||||||
strokeColor: ExcalidrawElement["strokeColor"];
|
strokeColor: ExcalidrawElement["strokeColor"];
|
||||||
backgroundColor: ExcalidrawElement["backgroundColor"];
|
backgroundColor: ExcalidrawElement["backgroundColor"];
|
||||||
@@ -308,10 +326,10 @@ export const DEFAULT_ELEMENT_PROPS: {
|
|||||||
} = {
|
} = {
|
||||||
strokeColor: COLOR_PALETTE.black,
|
strokeColor: COLOR_PALETTE.black,
|
||||||
backgroundColor: COLOR_PALETTE.transparent,
|
backgroundColor: COLOR_PALETTE.transparent,
|
||||||
fillStyle: "hachure",
|
fillStyle: "solid",
|
||||||
strokeWidth: 1,
|
strokeWidth: 2,
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
roughness: 1,
|
roughness: ROUGHNESS.artist,
|
||||||
opacity: 100,
|
opacity: 100,
|
||||||
locked: false,
|
locked: false,
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-5
@@ -195,7 +195,7 @@
|
|||||||
.buttonList label:focus-within,
|
.buttonList label:focus-within,
|
||||||
input:focus-visible {
|
input:focus-visible {
|
||||||
outline: transparent;
|
outline: transparent;
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
box-shadow: 0 0 0 1px var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonList {
|
.buttonList {
|
||||||
@@ -280,6 +280,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|
||||||
|
.dropdown-menu--mobile {
|
||||||
|
bottom: 55px;
|
||||||
|
top: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-mobile-menu {
|
.App-mobile-menu {
|
||||||
@@ -537,13 +542,13 @@
|
|||||||
|
|
||||||
&:not(:focus) {
|
&:not(:focus) {
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--input-hover-bg-color);
|
border-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 0 2px var(--focus-highlight-color);
|
border-color: var(--color-brand-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +597,8 @@
|
|||||||
background-color: var(--island-bg-color);
|
background-color: var(--island-bg-color);
|
||||||
|
|
||||||
.ToolIcon__icon {
|
.ToolIcon__icon {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -601,8 +608,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar--mobile {
|
.App-toolbar--mobile {
|
||||||
overflow-x: auto;
|
overflow: visible;
|
||||||
max-width: 90vw;
|
max-width: 98vw;
|
||||||
|
|
||||||
.ToolIcon__keybinding {
|
.ToolIcon__keybinding {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -99,5 +99,7 @@ export const setCursorForShape = (
|
|||||||
interactiveCanvas.style.cursor = `url(${url}), auto`;
|
interactiveCanvas.style.cursor = `url(${url}), auto`;
|
||||||
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
||||||
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
||||||
|
} else {
|
||||||
|
interactiveCanvas.style.cursor = CURSOR_TYPE.AUTO;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+31
-3
@@ -8,7 +8,7 @@ import { t } from "../i18n";
|
|||||||
import { calculateScrollCenter } from "../scene";
|
import { calculateScrollCenter } from "../scene";
|
||||||
import { AppState, DataURL, LibraryItem } from "../types";
|
import { AppState, DataURL, LibraryItem } from "../types";
|
||||||
import { ValueOf } from "../utility-types";
|
import { ValueOf } from "../utility-types";
|
||||||
import { bytesToHexString } from "../utils";
|
import { bytesToHexString, isPromiseLike } from "../utils";
|
||||||
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
||||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||||
import { restore, restoreLibraryItems } from "./restore";
|
import { restore, restoreLibraryItems } from "./restore";
|
||||||
@@ -207,10 +207,13 @@ export const loadLibraryFromBlob = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const canvasToBlob = async (
|
export const canvasToBlob = async (
|
||||||
canvas: HTMLCanvasElement,
|
canvas: HTMLCanvasElement | Promise<HTMLCanvasElement>,
|
||||||
): Promise<Blob> => {
|
): Promise<Blob> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
|
if (isPromiseLike(canvas)) {
|
||||||
|
canvas = await canvas;
|
||||||
|
}
|
||||||
canvas.toBlob((blob) => {
|
canvas.toBlob((blob) => {
|
||||||
if (!blob) {
|
if (!blob) {
|
||||||
return reject(
|
return reject(
|
||||||
@@ -324,6 +327,31 @@ export const SVGStringToFile = (SVGString: string, filename: string = "") => {
|
|||||||
}) as File & { type: typeof MIME_TYPES.svg };
|
}) as File & { type: typeof MIME_TYPES.svg };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ImageURLToFile = async (
|
||||||
|
imageUrl: string,
|
||||||
|
filename: string = "",
|
||||||
|
): Promise<File | undefined> => {
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(imageUrl);
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(t("errors.failedToFetchImage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(t("errors.failedToFetchImage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob();
|
||||||
|
|
||||||
|
if (blob.type && isSupportedImageFile(blob)) {
|
||||||
|
const name = filename || blob.name || "";
|
||||||
|
return new File([blob], name, { type: blob.type });
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(t("errors.unsupportedFileType"));
|
||||||
|
};
|
||||||
|
|
||||||
export const getFileFromEvent = async (
|
export const getFileFromEvent = async (
|
||||||
event: React.DragEvent<HTMLDivElement>,
|
event: React.DragEvent<HTMLDivElement>,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
+67
-9
@@ -3,11 +3,19 @@ import {
|
|||||||
copyTextToSystemClipboard,
|
copyTextToSystemClipboard,
|
||||||
} from "../clipboard";
|
} from "../clipboard";
|
||||||
import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { getNonDeletedElements, isFrameElement } from "../element";
|
||||||
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { elementsOverlappingBBox } from "../packages/withinBounds";
|
||||||
|
import { isSomeElementSelected, getSelectedElements } from "../scene";
|
||||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||||
import { ExportType } from "../scene/types";
|
import { ExportType } from "../scene/types";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
|
import { cloneJSON } from "../utils";
|
||||||
import { canvasToBlob } from "./blob";
|
import { canvasToBlob } from "./blob";
|
||||||
import { fileSave, FileSystemHandle } from "./filesystem";
|
import { fileSave, FileSystemHandle } from "./filesystem";
|
||||||
import { serializeAsJSON } from "./json";
|
import { serializeAsJSON } from "./json";
|
||||||
@@ -15,9 +23,61 @@ import { serializeAsJSON } from "./json";
|
|||||||
export { loadFromBlob } from "./blob";
|
export { loadFromBlob } from "./blob";
|
||||||
export { loadFromJSON, saveAsJSON } from "./json";
|
export { loadFromJSON, saveAsJSON } from "./json";
|
||||||
|
|
||||||
|
export type ExportedElements = readonly NonDeletedExcalidrawElement[] & {
|
||||||
|
_brand: "exportedElements";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareElementsForExport = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
{ selectedElementIds }: Pick<AppState, "selectedElementIds">,
|
||||||
|
exportSelectionOnly: boolean,
|
||||||
|
) => {
|
||||||
|
elements = getNonDeletedElements(elements);
|
||||||
|
|
||||||
|
const isExportingSelection =
|
||||||
|
exportSelectionOnly &&
|
||||||
|
isSomeElementSelected(elements, { selectedElementIds });
|
||||||
|
|
||||||
|
let exportingFrame: ExcalidrawFrameElement | null = null;
|
||||||
|
let exportedElements = isExportingSelection
|
||||||
|
? getSelectedElements(
|
||||||
|
elements,
|
||||||
|
{ selectedElementIds },
|
||||||
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: elements;
|
||||||
|
|
||||||
|
if (isExportingSelection) {
|
||||||
|
if (exportedElements.length === 1 && isFrameElement(exportedElements[0])) {
|
||||||
|
exportingFrame = exportedElements[0];
|
||||||
|
exportedElements = elementsOverlappingBBox({
|
||||||
|
elements,
|
||||||
|
bounds: exportingFrame,
|
||||||
|
type: "overlap",
|
||||||
|
});
|
||||||
|
} else if (exportedElements.length > 1) {
|
||||||
|
exportedElements = getSelectedElements(
|
||||||
|
elements,
|
||||||
|
{ selectedElementIds },
|
||||||
|
{
|
||||||
|
includeBoundTextElement: true,
|
||||||
|
includeElementsInFrames: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exportingFrame,
|
||||||
|
exportedElements: cloneJSON(exportedElements) as ExportedElements,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const exportCanvas = async (
|
export const exportCanvas = async (
|
||||||
type: Omit<ExportType, "backend">,
|
type: Omit<ExportType, "backend">,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: ExportedElements,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
{
|
{
|
||||||
@@ -26,12 +86,14 @@ export const exportCanvas = async (
|
|||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
name,
|
name,
|
||||||
fileHandle = null,
|
fileHandle = null,
|
||||||
|
exportingFrame = null,
|
||||||
}: {
|
}: {
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportPadding?: number;
|
exportPadding?: number;
|
||||||
viewBackgroundColor: string;
|
viewBackgroundColor: string;
|
||||||
name: string;
|
name: string;
|
||||||
fileHandle?: FileSystemHandle | null;
|
fileHandle?: FileSystemHandle | null;
|
||||||
|
exportingFrame: ExcalidrawFrameElement | null;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
@@ -49,6 +111,7 @@ export const exportCanvas = async (
|
|||||||
exportEmbedScene: appState.exportEmbedScene && type === "svg",
|
exportEmbedScene: appState.exportEmbedScene && type === "svg",
|
||||||
},
|
},
|
||||||
files,
|
files,
|
||||||
|
{ exportingFrame },
|
||||||
);
|
);
|
||||||
if (type === "svg") {
|
if (type === "svg") {
|
||||||
return await fileSave(
|
return await fileSave(
|
||||||
@@ -66,17 +129,15 @@ export const exportCanvas = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tempCanvas = await exportToCanvas(elements, appState, files, {
|
const tempCanvas = exportToCanvas(elements, appState, files, {
|
||||||
exportBackground,
|
exportBackground,
|
||||||
viewBackgroundColor,
|
viewBackgroundColor,
|
||||||
exportPadding,
|
exportPadding,
|
||||||
|
exportingFrame,
|
||||||
});
|
});
|
||||||
tempCanvas.style.display = "none";
|
|
||||||
document.body.appendChild(tempCanvas);
|
|
||||||
|
|
||||||
if (type === "png") {
|
if (type === "png") {
|
||||||
let blob = await canvasToBlob(tempCanvas);
|
let blob = await canvasToBlob(tempCanvas);
|
||||||
tempCanvas.remove();
|
|
||||||
if (appState.exportEmbedScene) {
|
if (appState.exportEmbedScene) {
|
||||||
blob = await (
|
blob = await (
|
||||||
await import(/* webpackChunkName: "image" */ "./image")
|
await import(/* webpackChunkName: "image" */ "./image")
|
||||||
@@ -114,11 +175,8 @@ export const exportCanvas = async (
|
|||||||
} else {
|
} else {
|
||||||
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
throw new Error(t("alerts.couldNotCopyToClipboard"));
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
tempCanvas.remove();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tempCanvas.remove();
|
|
||||||
// shouldn't happen
|
// shouldn't happen
|
||||||
throw new Error("Unsupported export type");
|
throw new Error("Unsupported export type");
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -23,6 +23,7 @@ import {
|
|||||||
LIBRARY_SIDEBAR_TAB,
|
LIBRARY_SIDEBAR_TAB,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
|
import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
|
||||||
|
import { cloneJSON } from "../utils";
|
||||||
|
|
||||||
export const libraryItemsAtom = atom<{
|
export const libraryItemsAtom = atom<{
|
||||||
status: "loading" | "loaded";
|
status: "loading" | "loaded";
|
||||||
@@ -31,7 +32,7 @@ export const libraryItemsAtom = atom<{
|
|||||||
}>({ status: "loaded", isInitialized: true, libraryItems: [] });
|
}>({ status: "loaded", isInitialized: true, libraryItems: [] });
|
||||||
|
|
||||||
const cloneLibraryItems = (libraryItems: LibraryItems): LibraryItems =>
|
const cloneLibraryItems = (libraryItems: LibraryItems): LibraryItems =>
|
||||||
JSON.parse(JSON.stringify(libraryItems));
|
cloneJSON(libraryItems);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* checks if library item does not exist already in current library
|
* checks if library item does not exist already in current library
|
||||||
|
|||||||
+12
-12
@@ -1,7 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import { exportCanvas } from ".";
|
import { exportCanvas, prepareElementsForExport } from ".";
|
||||||
import { getNonDeletedElements } from "../element";
|
|
||||||
import { getFileHandleType, isImageFileHandleType } from "./blob";
|
import { getFileHandleType, isImageFileHandleType } from "./blob";
|
||||||
|
|
||||||
export const resaveAsImageWithScene = async (
|
export const resaveAsImageWithScene = async (
|
||||||
@@ -23,18 +22,19 @@ export const resaveAsImageWithScene = async (
|
|||||||
exportEmbedScene: true,
|
exportEmbedScene: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
await exportCanvas(
|
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||||
fileHandleType,
|
elements,
|
||||||
getNonDeletedElements(elements),
|
|
||||||
appState,
|
appState,
|
||||||
files,
|
false,
|
||||||
{
|
|
||||||
exportBackground,
|
|
||||||
viewBackgroundColor,
|
|
||||||
name,
|
|
||||||
fileHandle,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await exportCanvas(fileHandleType, exportedElements, appState, files, {
|
||||||
|
exportBackground,
|
||||||
|
viewBackgroundColor,
|
||||||
|
name,
|
||||||
|
fileHandle,
|
||||||
|
exportingFrame,
|
||||||
|
});
|
||||||
|
|
||||||
return { fileHandle };
|
return { fileHandle };
|
||||||
};
|
};
|
||||||
|
|||||||
+128
-9
@@ -5,7 +5,31 @@ import {
|
|||||||
} from "./transform";
|
} from "./transform";
|
||||||
import { ExcalidrawArrowElement } from "../element/types";
|
import { ExcalidrawArrowElement } from "../element/types";
|
||||||
|
|
||||||
|
const opts = { regenerateIds: false };
|
||||||
|
|
||||||
describe("Test Transform", () => {
|
describe("Test Transform", () => {
|
||||||
|
it("should generate id unless opts.regenerateIds is set to false explicitly", () => {
|
||||||
|
const elements = [
|
||||||
|
{
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
id: "rect-1",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let data = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
);
|
||||||
|
expect(data.length).toBe(1);
|
||||||
|
expect(data[0].id).toBe("id0");
|
||||||
|
|
||||||
|
data = convertToExcalidrawElements(
|
||||||
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
|
);
|
||||||
|
expect(data[0].id).toBe("rect-1");
|
||||||
|
});
|
||||||
|
|
||||||
it("should transform regular shapes", () => {
|
it("should transform regular shapes", () => {
|
||||||
const elements = [
|
const elements = [
|
||||||
{
|
{
|
||||||
@@ -59,6 +83,7 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
convertToExcalidrawElements(
|
convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
).forEach((ele) => {
|
).forEach((ele) => {
|
||||||
expect(ele).toMatchSnapshot({
|
expect(ele).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
@@ -87,6 +112,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
convertToExcalidrawElements(
|
convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
).forEach((ele) => {
|
).forEach((ele) => {
|
||||||
expect(ele).toMatchSnapshot({
|
expect(ele).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
@@ -128,6 +154,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
@@ -210,6 +237,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(12);
|
expect(excaldrawElements.length).toBe(12);
|
||||||
@@ -267,6 +295,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(8);
|
expect(excaldrawElements.length).toBe(8);
|
||||||
@@ -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", () => {
|
describe("Test arrow bindings", () => {
|
||||||
it("should bind arrows to shapes when start / end provided without ids", () => {
|
it("should bind arrows to shapes when start / end provided without ids", () => {
|
||||||
const elements = [
|
const elements = [
|
||||||
@@ -300,6 +413,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
@@ -321,7 +435,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text).toMatchObject({
|
expect(text).toMatchObject({
|
||||||
x: 340,
|
x: 240,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "HELLO WORLD!!",
|
text: "HELLO WORLD!!",
|
||||||
@@ -341,7 +455,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(ellipse).toMatchObject({
|
expect(ellipse).toMatchObject({
|
||||||
x: 555,
|
x: 355,
|
||||||
y: 189,
|
y: 189,
|
||||||
type: "ellipse",
|
type: "ellipse",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@@ -383,10 +497,10 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
|
|
||||||
const [arrow, text1, text2, text3] = excaldrawElements;
|
const [arrow, text1, text2, text3] = excaldrawElements;
|
||||||
|
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
@@ -406,7 +520,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text1).toMatchObject({
|
expect(text1).toMatchObject({
|
||||||
x: 340,
|
x: 240,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "HELLO WORLD!!",
|
text: "HELLO WORLD!!",
|
||||||
@@ -426,7 +540,7 @@ describe("Test Transform", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(text3).toMatchObject({
|
expect(text3).toMatchObject({
|
||||||
x: 555,
|
x: 355,
|
||||||
y: 226.5,
|
y: 226.5,
|
||||||
type: "text",
|
type: "text",
|
||||||
boundElements: [
|
boundElements: [
|
||||||
@@ -499,6 +613,7 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(5);
|
expect(excaldrawElements.length).toBe(5);
|
||||||
@@ -547,6 +662,7 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
@@ -600,17 +716,18 @@ describe("Test Transform", () => {
|
|||||||
|
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(4);
|
expect(excaldrawElements.length).toBe(4);
|
||||||
const [, , arrow] = excaldrawElements;
|
const [, , arrow, text] = excaldrawElements;
|
||||||
expect(arrow).toMatchObject({
|
expect(arrow).toMatchObject({
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
x: 255,
|
x: 255,
|
||||||
y: 239,
|
y: 239,
|
||||||
boundElements: [
|
boundElements: [
|
||||||
{
|
{
|
||||||
id: "id46",
|
id: text.id,
|
||||||
type: "text",
|
type: "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -650,17 +767,18 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
expect(excaldrawElements.length).toBe(2);
|
expect(excaldrawElements.length).toBe(2);
|
||||||
const [arrow, rect] = excaldrawElements;
|
const [arrow, rect] = excaldrawElements;
|
||||||
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({
|
||||||
elementId: "rect-1",
|
elementId: "rect-1",
|
||||||
focus: 0,
|
focus: 0,
|
||||||
gap: 5,
|
gap: 205,
|
||||||
});
|
});
|
||||||
expect(rect.boundElements).toStrictEqual([
|
expect(rect.boundElements).toStrictEqual([
|
||||||
{
|
{
|
||||||
id: "id47",
|
id: arrow.id,
|
||||||
type: "arrow",
|
type: "arrow",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -692,6 +810,7 @@ describe("Test Transform", () => {
|
|||||||
];
|
];
|
||||||
const excaldrawElements = convertToExcalidrawElements(
|
const excaldrawElements = convertToExcalidrawElements(
|
||||||
elements as ExcalidrawElementSkeleton[],
|
elements as ExcalidrawElementSkeleton[],
|
||||||
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(excaldrawElements.length).toBe(1);
|
expect(excaldrawElements.length).toBe(1);
|
||||||
|
|||||||
+161
-12
@@ -5,6 +5,7 @@ import {
|
|||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
import {
|
import {
|
||||||
|
getCommonBounds,
|
||||||
newElement,
|
newElement,
|
||||||
newLinearElement,
|
newLinearElement,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
import { bindLinearElement } from "../element/binding";
|
import { bindLinearElement } from "../element/binding";
|
||||||
import {
|
import {
|
||||||
ElementConstructorOpts,
|
ElementConstructorOpts,
|
||||||
|
newFrameElement,
|
||||||
newImageElement,
|
newImageElement,
|
||||||
newTextElement,
|
newTextElement,
|
||||||
} from "../element/newElement";
|
} from "../element/newElement";
|
||||||
@@ -38,7 +40,9 @@ import {
|
|||||||
VerticalAlign,
|
VerticalAlign,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { MarkOptional } from "../utility-types";
|
import { MarkOptional } from "../utility-types";
|
||||||
import { assertNever, getFontString } from "../utils";
|
import { assertNever, cloneJSON, getFontString } from "../utils";
|
||||||
|
import { getSizeFromPoints } from "../points";
|
||||||
|
import { randomId } from "../random";
|
||||||
|
|
||||||
export type ValidLinearElement = {
|
export type ValidLinearElement = {
|
||||||
type: "arrow" | "line";
|
type: "arrow" | "line";
|
||||||
@@ -133,9 +137,7 @@ export type ValidContainer =
|
|||||||
export type ExcalidrawElementSkeleton =
|
export type ExcalidrawElementSkeleton =
|
||||||
| Extract<
|
| Extract<
|
||||||
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
| ExcalidrawEmbeddableElement
|
ExcalidrawEmbeddableElement | ExcalidrawFreeDrawElement
|
||||||
| ExcalidrawFreeDrawElement
|
|
||||||
| ExcalidrawFrameElement
|
|
||||||
>
|
>
|
||||||
| ({
|
| ({
|
||||||
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
type: Extract<ExcalidrawLinearElement["type"], "line">;
|
||||||
@@ -156,10 +158,15 @@ export type ExcalidrawElementSkeleton =
|
|||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
fileId: FileId;
|
fileId: FileId;
|
||||||
} & Partial<ExcalidrawImageElement>);
|
} & Partial<ExcalidrawImageElement>)
|
||||||
|
| ({
|
||||||
|
type: "frame";
|
||||||
|
children: readonly ExcalidrawElement["id"][];
|
||||||
|
name?: string;
|
||||||
|
} & Partial<ExcalidrawFrameElement>);
|
||||||
|
|
||||||
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
const DEFAULT_LINEAR_ELEMENT_PROPS = {
|
||||||
width: 300,
|
width: 100,
|
||||||
height: 0,
|
height: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -357,6 +364,49 @@ 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 = cloneJSON(linearElement.points) as [number, number][];
|
||||||
|
// 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 {
|
return {
|
||||||
linearElement,
|
linearElement,
|
||||||
startBoundElement,
|
startBoundElement,
|
||||||
@@ -384,18 +434,25 @@ class ElementStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const convertToExcalidrawElements = (
|
export const convertToExcalidrawElements = (
|
||||||
elements: ExcalidrawElementSkeleton[] | null,
|
elementsSkeleton: ExcalidrawElementSkeleton[] | null,
|
||||||
|
opts?: { regenerateIds: boolean },
|
||||||
) => {
|
) => {
|
||||||
if (!elements) {
|
if (!elementsSkeleton) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const elements = cloneJSON(elementsSkeleton);
|
||||||
const elementStore = new ElementStore();
|
const elementStore = new ElementStore();
|
||||||
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
const elementsWithIds = new Map<string, ExcalidrawElementSkeleton>();
|
||||||
|
const oldToNewElementIdMap = new Map<string, string>();
|
||||||
|
|
||||||
// Create individual elements
|
// Create individual elements
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
let excalidrawElement: ExcalidrawElement;
|
let excalidrawElement: ExcalidrawElement;
|
||||||
|
const originalId = element.id;
|
||||||
|
if (opts?.regenerateIds !== false) {
|
||||||
|
Object.assign(element, { id: randomId() });
|
||||||
|
}
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
case "ellipse":
|
case "ellipse":
|
||||||
@@ -444,6 +501,11 @@ export const convertToExcalidrawElements = (
|
|||||||
],
|
],
|
||||||
...element,
|
...element,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.assign(
|
||||||
|
excalidrawElement,
|
||||||
|
getSizeFromPoints(excalidrawElement.points),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text": {
|
case "text": {
|
||||||
@@ -477,8 +539,15 @@ export const convertToExcalidrawElements = (
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "frame": {
|
||||||
|
excalidrawElement = newFrameElement({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
...element,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "freedraw":
|
case "freedraw":
|
||||||
case "frame":
|
|
||||||
case "embeddable": {
|
case "embeddable": {
|
||||||
excalidrawElement = element;
|
excalidrawElement = element;
|
||||||
break;
|
break;
|
||||||
@@ -499,6 +568,9 @@ export const convertToExcalidrawElements = (
|
|||||||
} else {
|
} else {
|
||||||
elementStore.add(excalidrawElement);
|
elementStore.add(excalidrawElement);
|
||||||
elementsWithIds.set(excalidrawElement.id, element);
|
elementsWithIds.set(excalidrawElement.id, element);
|
||||||
|
if (originalId) {
|
||||||
|
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +596,18 @@ export const convertToExcalidrawElements = (
|
|||||||
element.type === "arrow" ? element?.start : undefined;
|
element.type === "arrow" ? element?.start : undefined;
|
||||||
const originalEnd =
|
const originalEnd =
|
||||||
element.type === "arrow" ? element?.end : undefined;
|
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 } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
container as ExcalidrawArrowElement,
|
container as ExcalidrawArrowElement,
|
||||||
@@ -539,13 +623,23 @@ export const convertToExcalidrawElements = (
|
|||||||
} else {
|
} else {
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "arrow": {
|
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 } =
|
const { linearElement, startBoundElement, endBoundElement } =
|
||||||
bindLinearElementToElement(
|
bindLinearElementToElement(
|
||||||
excalidrawElement as ExcalidrawArrowElement,
|
excalidrawElement as ExcalidrawArrowElement,
|
||||||
element.start,
|
start,
|
||||||
element.end,
|
end,
|
||||||
elementStore,
|
elementStore,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementStore.add(linearElement);
|
elementStore.add(linearElement);
|
||||||
elementStore.add(startBoundElement);
|
elementStore.add(startBoundElement);
|
||||||
elementStore.add(endBoundElement);
|
elementStore.add(endBoundElement);
|
||||||
@@ -557,5 +651,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();
|
return elementStore.getElements();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -392,7 +392,7 @@ export const getLinkHandleFromCoords = (
|
|||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: number,
|
angle: number,
|
||||||
appState: Pick<UIAppState, "zoom">,
|
appState: Pick<UIAppState, "zoom">,
|
||||||
): [x: number, y: number, width: number, height: number] => {
|
): Bounds => {
|
||||||
const size = DEFAULT_LINK_SIZE;
|
const size = DEFAULT_LINK_SIZE;
|
||||||
const linkWidth = size / appState.zoom.value;
|
const linkWidth = size / appState.zoom.value;
|
||||||
const linkHeight = size / appState.zoom.value;
|
const linkHeight = size / appState.zoom.value;
|
||||||
|
|||||||
+16
-16
@@ -34,7 +34,12 @@ export type RectangleBox = {
|
|||||||
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
type MaybeQuadraticSolution = [number | null, number | null] | false;
|
||||||
|
|
||||||
// x and y position of top left corner, x and y position of bottom right corner
|
// x and y position of top left corner, x and y position of bottom right corner
|
||||||
export type Bounds = readonly [x1: number, y1: number, x2: number, y2: number];
|
export type Bounds = readonly [
|
||||||
|
minX: number,
|
||||||
|
minY: number,
|
||||||
|
maxX: number,
|
||||||
|
maxY: number,
|
||||||
|
];
|
||||||
|
|
||||||
export class ElementBounds {
|
export class ElementBounds {
|
||||||
private static boundsCache = new WeakMap<
|
private static boundsCache = new WeakMap<
|
||||||
@@ -63,7 +68,7 @@ export class ElementBounds {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
||||||
let bounds: [number, number, number, number];
|
let bounds: Bounds;
|
||||||
|
|
||||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||||
|
|
||||||
@@ -387,7 +392,7 @@ const getCubicBezierCurveBound = (
|
|||||||
export const getMinMaxXYFromCurvePathOps = (
|
export const getMinMaxXYFromCurvePathOps = (
|
||||||
ops: Op[],
|
ops: Op[],
|
||||||
transformXY?: (x: number, y: number) => [number, number],
|
transformXY?: (x: number, y: number) => [number, number],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
let currentP: Point = [0, 0];
|
let currentP: Point = [0, 0];
|
||||||
|
|
||||||
const { minX, minY, maxX, maxY } = ops.reduce(
|
const { minX, minY, maxX, maxY } = ops.reduce(
|
||||||
@@ -435,9 +440,9 @@ export const getMinMaxXYFromCurvePathOps = (
|
|||||||
return [minX, minY, maxX, maxY];
|
return [minX, minY, maxX, maxY];
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBoundsFromPoints = (
|
export const getBoundsFromPoints = (
|
||||||
points: ExcalidrawFreeDrawElement["points"],
|
points: ExcalidrawFreeDrawElement["points"],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
let minX = Infinity;
|
let minX = Infinity;
|
||||||
let minY = Infinity;
|
let minY = Infinity;
|
||||||
let maxX = -Infinity;
|
let maxX = -Infinity;
|
||||||
@@ -589,7 +594,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
cx: number,
|
cx: number,
|
||||||
cy: number,
|
cy: number,
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
if (element.points.length < 2) {
|
if (element.points.length < 2) {
|
||||||
const [pointX, pointY] = element.points[0];
|
const [pointX, pointY] = element.points[0];
|
||||||
const [x, y] = rotate(
|
const [x, y] = rotate(
|
||||||
@@ -600,7 +605,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
element.angle,
|
element.angle,
|
||||||
);
|
);
|
||||||
|
|
||||||
let coords: [number, number, number, number] = [x, y, x, y];
|
let coords: Bounds = [x, y, x, y];
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
@@ -625,12 +630,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
const transformXY = (x: number, y: number) =>
|
const transformXY = (x: number, y: number) =>
|
||||||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||||
let coords: [number, number, number, number] = [
|
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||||
res[0],
|
|
||||||
res[1],
|
|
||||||
res[2],
|
|
||||||
res[3],
|
|
||||||
];
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||||
@@ -692,7 +692,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
nextHeight: number,
|
||||||
normalizePoints: boolean,
|
normalizePoints: boolean,
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
|
||||||
return [
|
return [
|
||||||
element.x,
|
element.x,
|
||||||
@@ -709,7 +709,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
normalizePoints,
|
normalizePoints,
|
||||||
);
|
);
|
||||||
|
|
||||||
let bounds: [number, number, number, number];
|
let bounds: Bounds;
|
||||||
|
|
||||||
if (isFreeDrawElement(element)) {
|
if (isFreeDrawElement(element)) {
|
||||||
// Free Draw
|
// Free Draw
|
||||||
@@ -740,7 +740,7 @@ export const getResizedElementAbsoluteCoords = (
|
|||||||
export const getElementPointsCoords = (
|
export const getElementPointsCoords = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
points: readonly (readonly [number, number])[],
|
points: readonly (readonly [number, number])[],
|
||||||
): [number, number, number, number] => {
|
): Bounds => {
|
||||||
// This might be computationally heavey
|
// This might be computationally heavey
|
||||||
const gen = rough.generator();
|
const gen = rough.generator();
|
||||||
const curve =
|
const curve =
|
||||||
|
|||||||
@@ -494,7 +494,9 @@ const hitTestFreeDrawElement = (
|
|||||||
// for filled freedraw shapes, support
|
// for filled freedraw shapes, support
|
||||||
// selecting from inside
|
// selecting from inside
|
||||||
if (shape && shape.sets.length) {
|
if (shape && shape.sets.length) {
|
||||||
return hitTestRoughShape(shape, x, y, threshold);
|
return element.fillStyle === "solid"
|
||||||
|
? hitTestCurveInside(shape, x, y, "round")
|
||||||
|
: hitTestRoughShape(shape, x, y, threshold);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+51
-37
@@ -1,5 +1,5 @@
|
|||||||
import { updateBoundElements } from "./binding";
|
import { updateBoundElements } from "./binding";
|
||||||
import { getCommonBounds } from "./bounds";
|
import { Bounds, getCommonBounds } from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getPerfectElementSize } from "./sizeHelpers";
|
import { getPerfectElementSize } from "./sizeHelpers";
|
||||||
import { NonDeletedExcalidrawElement } from "./types";
|
import { NonDeletedExcalidrawElement } from "./types";
|
||||||
@@ -8,7 +8,11 @@ import { getBoundTextElement } from "./textElement";
|
|||||||
import { isSelectedViaGroup } from "../groups";
|
import { isSelectedViaGroup } from "../groups";
|
||||||
import { getGridPoint } from "../math";
|
import { getGridPoint } from "../math";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isFrameElement } from "./typeChecks";
|
import {
|
||||||
|
isArrowElement,
|
||||||
|
isBoundToContainer,
|
||||||
|
isFrameElement,
|
||||||
|
} from "./typeChecks";
|
||||||
|
|
||||||
export const dragSelectedElements = (
|
export const dragSelectedElements = (
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
@@ -35,44 +39,41 @@ export const dragSelectedElements = (
|
|||||||
if (frames.length > 0) {
|
if (frames.length > 0) {
|
||||||
const elementsInFrames = scene
|
const elementsInFrames = scene
|
||||||
.getNonDeletedElements()
|
.getNonDeletedElements()
|
||||||
|
.filter((e) => !isBoundToContainer(e))
|
||||||
.filter((e) => e.frameId !== null)
|
.filter((e) => e.frameId !== null)
|
||||||
.filter((e) => frames.includes(e.frameId!));
|
.filter((e) => frames.includes(e.frameId!));
|
||||||
|
|
||||||
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
elementsInFrames.forEach((element) => elementsToUpdate.add(element));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commonBounds = getCommonBounds(
|
||||||
|
Array.from(elementsToUpdate).map(
|
||||||
|
(el) => pointerDownState.originalElements.get(el.id) ?? el,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const adjustedOffset = calculateOffset(
|
||||||
|
commonBounds,
|
||||||
|
offset,
|
||||||
|
snapOffset,
|
||||||
|
gridSize,
|
||||||
|
);
|
||||||
|
|
||||||
elementsToUpdate.forEach((element) => {
|
elementsToUpdate.forEach((element) => {
|
||||||
updateElementCoords(
|
updateElementCoords(pointerDownState, element, adjustedOffset);
|
||||||
pointerDownState,
|
|
||||||
element,
|
|
||||||
offset,
|
|
||||||
snapOffset,
|
|
||||||
gridSize,
|
|
||||||
);
|
|
||||||
// update coords of bound text only if we're dragging the container directly
|
// update coords of bound text only if we're dragging the container directly
|
||||||
// (we don't drag the group that it's part of)
|
// (we don't drag the group that it's part of)
|
||||||
if (
|
if (
|
||||||
|
// Don't update coords of arrow label since we calculate its position during render
|
||||||
|
!isArrowElement(element) &&
|
||||||
// container isn't part of any group
|
// container isn't part of any group
|
||||||
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
|
||||||
!element.groupIds.length ||
|
(!element.groupIds.length ||
|
||||||
// container is part of a group, but we're dragging the container directly
|
// container is part of a group, but we're dragging the container directly
|
||||||
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
|
(appState.editingGroupId && !isSelectedViaGroup(appState, element)))
|
||||||
) {
|
) {
|
||||||
const textElement = getBoundTextElement(element);
|
const textElement = getBoundTextElement(element);
|
||||||
if (
|
if (textElement) {
|
||||||
textElement &&
|
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||||
// 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))
|
|
||||||
) {
|
|
||||||
updateElementCoords(
|
|
||||||
pointerDownState,
|
|
||||||
textElement,
|
|
||||||
offset,
|
|
||||||
snapOffset,
|
|
||||||
gridSize,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateBoundElements(element, {
|
updateBoundElements(element, {
|
||||||
@@ -81,23 +82,20 @@ export const dragSelectedElements = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateElementCoords = (
|
const calculateOffset = (
|
||||||
pointerDownState: PointerDownState,
|
commonBounds: Bounds,
|
||||||
element: NonDeletedExcalidrawElement,
|
|
||||||
dragOffset: { x: number; y: number },
|
dragOffset: { x: number; y: number },
|
||||||
snapOffset: { x: number; y: number },
|
snapOffset: { x: number; y: number },
|
||||||
gridSize: AppState["gridSize"],
|
gridSize: AppState["gridSize"],
|
||||||
) => {
|
): { x: number; y: number } => {
|
||||||
const originalElement =
|
const [x, y] = commonBounds;
|
||||||
pointerDownState.originalElements.get(element.id) ?? element;
|
let nextX = x + dragOffset.x + snapOffset.x;
|
||||||
|
let nextY = y + dragOffset.y + snapOffset.y;
|
||||||
let nextX = originalElement.x + dragOffset.x + snapOffset.x;
|
|
||||||
let nextY = originalElement.y + dragOffset.y + snapOffset.y;
|
|
||||||
|
|
||||||
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
if (snapOffset.x === 0 || snapOffset.y === 0) {
|
||||||
const [nextGridX, nextGridY] = getGridPoint(
|
const [nextGridX, nextGridY] = getGridPoint(
|
||||||
originalElement.x + dragOffset.x,
|
x + dragOffset.x,
|
||||||
originalElement.y + dragOffset.y,
|
y + dragOffset.y,
|
||||||
gridSize,
|
gridSize,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -109,6 +107,22 @@ const updateElementCoords = (
|
|||||||
nextY = nextGridY;
|
nextY = nextGridY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
x: nextX - x,
|
||||||
|
y: nextY - y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateElementCoords = (
|
||||||
|
pointerDownState: PointerDownState,
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
dragOffset: { x: number; y: number },
|
||||||
|
) => {
|
||||||
|
const originalElement =
|
||||||
|
pointerDownState.originalElements.get(element.id) ?? element;
|
||||||
|
|
||||||
|
const nextX = originalElement.x + dragOffset.x;
|
||||||
|
const nextY = originalElement.y + dragOffset.y;
|
||||||
|
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
x: nextX,
|
x: nextX,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const embeddedLinkCache = new Map<string, EmbeddedLink>();
|
|||||||
|
|
||||||
const RE_YOUTUBE =
|
const RE_YOUTUBE =
|
||||||
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
/^(?:http(?:s)?:\/\/)?(?:www\.)?youtu(?:be\.com|\.be)\/(embed\/|watch\?v=|shorts\/|playlist\?list=|embed\/videoseries\?list=)?([a-zA-Z0-9_-]+)(?:\?t=|&t=|\?start=|&start=)?([a-zA-Z0-9_-]+)?[^\s]*$/;
|
||||||
|
|
||||||
const RE_VIMEO =
|
const RE_VIMEO =
|
||||||
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
/^(?:http(?:s)?:\/\/)?(?:(?:w){3}.)?(?:player\.)?vimeo\.com\/(?:video\/)?([^?\s]+)(?:\?.*)?$/;
|
||||||
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
|
const RE_FIGMA = /^https:\/\/(?:www\.)?figma\.com/;
|
||||||
@@ -47,6 +48,9 @@ const RE_VALTOWN =
|
|||||||
const RE_GENERIC_EMBED =
|
const RE_GENERIC_EMBED =
|
||||||
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
/^<(?:iframe|blockquote)[\s\S]*?\s(?:src|href)=["']([^"']*)["'][\s\S]*?>$/i;
|
||||||
|
|
||||||
|
const RE_GIPHY =
|
||||||
|
/giphy.com\/(?:clips|embed|gifs)\/[a-zA-Z0-9]*?-?([a-zA-Z0-9]+)(?:[^a-zA-Z0-9]|$)/;
|
||||||
|
|
||||||
const ALLOWED_DOMAINS = new Set([
|
const ALLOWED_DOMAINS = new Set([
|
||||||
"youtube.com",
|
"youtube.com",
|
||||||
"youtu.be",
|
"youtu.be",
|
||||||
@@ -59,6 +63,8 @@ const ALLOWED_DOMAINS = new Set([
|
|||||||
"*.simplepdf.eu",
|
"*.simplepdf.eu",
|
||||||
"stackblitz.com",
|
"stackblitz.com",
|
||||||
"val.town",
|
"val.town",
|
||||||
|
"giphy.com",
|
||||||
|
"dddice.com",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const createSrcDoc = (body: string) => {
|
const createSrcDoc = (body: string) => {
|
||||||
@@ -195,7 +201,7 @@ export const getEmbedLink = (link: string | null | undefined): EmbeddedLink => {
|
|||||||
return { link, aspectRatio, type };
|
return { link, aspectRatio, type };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isEmbeddableOrFrameLabel = (
|
export const isEmbeddableOrLabel = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
): Boolean => {
|
): Boolean => {
|
||||||
if (isEmbeddableElement(element)) {
|
if (isEmbeddableElement(element)) {
|
||||||
@@ -308,6 +314,10 @@ export const extractSrc = (htmlString: string): string => {
|
|||||||
return gistMatch[1];
|
return gistMatch[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RE_GIPHY.test(htmlString)) {
|
||||||
|
return `https://giphy.com/embed/${RE_GIPHY.exec(htmlString)![1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
const match = htmlString.match(RE_GENERIC_EMBED);
|
const match = htmlString.match(RE_GENERIC_EMBED);
|
||||||
if (match && match.length === 2) {
|
if (match && match.length === 2) {
|
||||||
return match[1];
|
return match[1];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "../math";
|
} from "../math";
|
||||||
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
|
||||||
import {
|
import {
|
||||||
|
Bounds,
|
||||||
getCurvePathOps,
|
getCurvePathOps,
|
||||||
getElementPointsCoords,
|
getElementPointsCoords,
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
@@ -1316,7 +1317,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static getMinMaxXYWithBoundText = (
|
static getMinMaxXYWithBoundText = (
|
||||||
element: ExcalidrawLinearElement,
|
element: ExcalidrawLinearElement,
|
||||||
elementBounds: [number, number, number, number],
|
elementBounds: Bounds,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
): [number, number, number, number, number, number] => {
|
): [number, number, number, number, number, number] => {
|
||||||
let [x1, y1, x2, y2] = elementBounds;
|
let [x1, y1, x2, y2] = elementBounds;
|
||||||
|
|||||||
@@ -144,13 +144,15 @@ export const newEmbeddableElement = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const newFrameElement = (
|
export const newFrameElement = (
|
||||||
opts: ElementConstructorOpts,
|
opts: {
|
||||||
|
name?: string;
|
||||||
|
} & ElementConstructorOpts,
|
||||||
): NonDeleted<ExcalidrawFrameElement> => {
|
): NonDeleted<ExcalidrawFrameElement> => {
|
||||||
const frameElement = newElementWith(
|
const frameElement = newElementWith(
|
||||||
{
|
{
|
||||||
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
..._newElementBase<ExcalidrawFrameElement>("frame", opts),
|
||||||
type: "frame",
|
type: "frame",
|
||||||
name: null,
|
name: opts?.name || null,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
MaybeTransformHandleType,
|
MaybeTransformHandleType,
|
||||||
} from "./transformHandles";
|
} from "./transformHandles";
|
||||||
import { AppState, Zoom } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
|
import { Bounds } from "./bounds";
|
||||||
|
|
||||||
const isInsideTransformHandle = (
|
const isInsideTransformHandle = (
|
||||||
transformHandle: TransformHandle,
|
transformHandle: TransformHandle,
|
||||||
@@ -87,7 +88,7 @@ export const getElementWithTransformHandleType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getTransformHandleTypeFromCoords = (
|
export const getTransformHandleTypeFromCoords = (
|
||||||
[x1, y1, x2, y2]: readonly [number, number, number, number],
|
[x1, y1, x2, y2]: Bounds,
|
||||||
scenePointerX: number,
|
scenePointerX: number,
|
||||||
scenePointerY: number,
|
scenePointerY: number,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const redrawTextBoundingBox = (
|
|||||||
);
|
);
|
||||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||||
|
|
||||||
if (metrics.height > maxContainerHeight) {
|
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
||||||
const nextHeight = computeContainerDimensionForBoundText(
|
const nextHeight = computeContainerDimensionForBoundText(
|
||||||
metrics.height,
|
metrics.height,
|
||||||
container.type,
|
container.type,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||||
|
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
@@ -25,16 +26,7 @@ ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
|||||||
const tab = " ";
|
const tab = " ";
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
|
|
||||||
const getTextEditor = () => {
|
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
||||||
return document.querySelector(
|
|
||||||
".excalidraw-textEditorContainer > textarea",
|
|
||||||
) as HTMLTextAreaElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateTextEditor = (editor: HTMLTextAreaElement, value: string) => {
|
|
||||||
fireEvent.change(editor, { target: { value } });
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("textWysiwyg", () => {
|
describe("textWysiwyg", () => {
|
||||||
describe("start text editing", () => {
|
describe("start text editing", () => {
|
||||||
@@ -185,7 +177,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(h.state.editingElement?.id).toBe(boundText.id);
|
expect(h.state.editingElement?.id).toBe(boundText.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should edit text under cursor when clicked with text tool", () => {
|
it("should edit text under cursor when clicked with text tool", async () => {
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "ola",
|
text: "ola",
|
||||||
@@ -200,14 +192,14 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.clickAt(text.x + 50, text.y + 50);
|
mouse.clickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, false);
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should edit text under cursor when double-clicked with selection tool", () => {
|
it("should edit text under cursor when double-clicked with selection tool", async () => {
|
||||||
const text = API.createElement({
|
const text = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "ola",
|
text: "ola",
|
||||||
@@ -222,12 +214,26 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
mouse.doubleClickAt(text.x + 50, text.y + 50);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, false);
|
||||||
|
|
||||||
expect(editor).not.toBe(null);
|
expect(editor).not.toBe(null);
|
||||||
expect(h.state.editingElement?.id).toBe(text.id);
|
expect(h.state.editingElement?.id).toBe(text.id);
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// FIXME too flaky. No one knows why.
|
||||||
|
it.skip("should bump the version of a labeled arrow when the label is updated", async () => {
|
||||||
|
const arrow = UI.createElement("arrow", {
|
||||||
|
width: 300,
|
||||||
|
height: 0,
|
||||||
|
});
|
||||||
|
await UI.editText(arrow, "Hello");
|
||||||
|
const { version } = arrow;
|
||||||
|
|
||||||
|
await UI.editText(arrow, "Hello\nworld!");
|
||||||
|
|
||||||
|
expect(arrow.version).toEqual(version + 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test container-unbound text", () => {
|
describe("Test container-unbound text", () => {
|
||||||
@@ -243,13 +249,15 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
h.app.refreshViewportBreakpoints();
|
||||||
|
// @ts-ignore
|
||||||
|
h.app.refreshEditorBreakpoints();
|
||||||
|
|
||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
mouse.clickOn(textElement);
|
mouse.clickOn(textElement);
|
||||||
textarea = getTextEditor();
|
textarea = await getTextEditor(textEditorSelector, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -459,7 +467,7 @@ describe("textWysiwyg", () => {
|
|||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(750, 300);
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
textarea = getTextEditor();
|
textarea = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
textarea,
|
textarea,
|
||||||
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
@@ -511,7 +519,7 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -539,7 +547,7 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
expect(text.angle).toBe(rectangle.angle);
|
expect(text.angle).toBe(rectangle.angle);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -566,7 +574,7 @@ describe("textWysiwyg", () => {
|
|||||||
API.setSelectedElements([diamond]);
|
API.setSelectedElements([diamond]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
const value = new Array(1000).fill("1").join("\n");
|
const value = new Array(1000).fill("1").join("\n");
|
||||||
@@ -601,7 +609,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
let editor = getTextEditor();
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -616,7 +624,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -638,7 +646,7 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -673,7 +681,7 @@ describe("textWysiwyg", () => {
|
|||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -698,7 +706,7 @@ describe("textWysiwyg", () => {
|
|||||||
freedraw.y + freedraw.height / 2,
|
freedraw.y + freedraw.height / 2,
|
||||||
);
|
);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
|
||||||
|
|
||||||
@@ -732,7 +740,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(null);
|
expect(text.containerId).toBe(null);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -747,7 +755,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
editor,
|
editor,
|
||||||
@@ -792,7 +800,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.down();
|
mouse.down();
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = getTextEditor();
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
@@ -805,7 +813,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
fireEvent.click(screen.getByTitle(/code/i));
|
fireEvent.click(screen.getByTitle(/code/i));
|
||||||
@@ -838,7 +846,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
Keyboard.keyDown(KEYS.ENTER);
|
Keyboard.keyDown(KEYS.ENTER);
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
let editor = getTextEditor();
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -859,7 +867,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -888,7 +896,7 @@ describe("textWysiwyg", () => {
|
|||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -925,7 +933,7 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -946,7 +954,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should respect text alignment when resizing", async () => {
|
it("should respect text alignment when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
let editor = getTextEditor();
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -963,7 +971,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -986,7 +994,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
@@ -1024,7 +1032,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
|
|
||||||
@@ -1039,7 +1047,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should scale font size correctly when resizing using shift", async () => {
|
it("should scale font size correctly when resizing using shift", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1059,7 +1067,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1091,7 +1099,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("undo should work", async () => {
|
it("undo should work", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1128,7 +1136,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
it("should not allow bound text with only whitespaces", async () => {
|
it("should not allow bound text with only whitespaces", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
@@ -1183,7 +1191,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should reset the container height cache when resizing", async () => {
|
it("should reset the container height cache when resizing", async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
let editor = getTextEditor();
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1195,7 +1203,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1211,7 +1219,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
@@ -1236,7 +1244,7 @@ describe("textWysiwyg", () => {
|
|||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello World!");
|
updateTextEditor(editor, "Hello World!");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(
|
expect(
|
||||||
@@ -1268,12 +1276,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
updateTextEditor(editor, "Hello");
|
updateTextEditor(editor, "Hello");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
editor.select();
|
editor.select();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1384,7 +1392,7 @@ describe("textWysiwyg", () => {
|
|||||||
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||||
UI.clickTool("text");
|
UI.clickTool("text");
|
||||||
mouse.clickAt(20, 30);
|
mouse.clickAt(20, 30);
|
||||||
const editor = getTextEditor();
|
const editor = await getTextEditor(textEditorSelector, true);
|
||||||
|
|
||||||
updateTextEditor(
|
updateTextEditor(
|
||||||
editor,
|
editor,
|
||||||
@@ -1430,7 +1438,7 @@ describe("textWysiwyg", () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fillStyle: "hachure",
|
fillStyle: "solid",
|
||||||
groupIds: [],
|
groupIds: [],
|
||||||
height: 35,
|
height: 35,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
@@ -1443,7 +1451,7 @@ describe("textWysiwyg", () => {
|
|||||||
},
|
},
|
||||||
strokeColor: "#1e1e1e",
|
strokeColor: "#1e1e1e",
|
||||||
strokeStyle: "solid",
|
strokeStyle: "solid",
|
||||||
strokeWidth: 1,
|
strokeWidth: 2,
|
||||||
type: "rectangle",
|
type: "rectangle",
|
||||||
updated: 1,
|
updated: 1,
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -1472,7 +1480,7 @@ describe("textWysiwyg", () => {
|
|||||||
// Bind first text
|
// Bind first text
|
||||||
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.containerId).toBe(rectangle.id);
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
let editor = getTextEditor();
|
let editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Hello!");
|
updateTextEditor(editor, "Hello!");
|
||||||
expect(
|
expect(
|
||||||
@@ -1497,7 +1505,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
editor = getTextEditor();
|
editor = await getTextEditor(textEditorSelector, true);
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
updateTextEditor(editor, "Excalidraw");
|
updateTextEditor(editor, "Excalidraw");
|
||||||
editor.blur();
|
editor.blur();
|
||||||
@@ -1511,18 +1519,4 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.text).toBe("Excalidraw");
|
expect(text.text).toBe("Excalidraw");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bump the version of a labeled arrow when the label is updated", async () => {
|
|
||||||
await render(<Excalidraw handleKeyboardGlobally={true} />);
|
|
||||||
const arrow = UI.createElement("arrow", {
|
|
||||||
width: 300,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
await UI.editText(arrow, "Hello");
|
|
||||||
const { version } = arrow;
|
|
||||||
|
|
||||||
await UI.editText(arrow, "Hello\nworld!");
|
|
||||||
|
|
||||||
expect(arrow.version).toEqual(version + 1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
PointerType,
|
PointerType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { Bounds, getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
@@ -23,7 +23,7 @@ export type TransformHandleDirection =
|
|||||||
|
|
||||||
export type TransformHandleType = TransformHandleDirection | "rotation";
|
export type TransformHandleType = TransformHandleDirection | "rotation";
|
||||||
|
|
||||||
export type TransformHandle = [number, number, number, number];
|
export type TransformHandle = Bounds;
|
||||||
export type TransformHandles = Partial<{
|
export type TransformHandles = Partial<{
|
||||||
[T in TransformHandleType]: TransformHandle;
|
[T in TransformHandleType]: TransformHandle;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
+27
-11
@@ -1,6 +1,7 @@
|
|||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { MarkNonNullable } from "../utility-types";
|
import { MarkNonNullable } from "../utility-types";
|
||||||
|
import { assertNever } from "../utils";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -140,17 +141,32 @@ export const isTextBindableContainer = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isExcalidrawElement = (element: any): boolean => {
|
export const isExcalidrawElement = (
|
||||||
return (
|
element: any,
|
||||||
element?.type === "text" ||
|
): element is ExcalidrawElement => {
|
||||||
element?.type === "diamond" ||
|
const type: ExcalidrawElement["type"] | undefined = element?.type;
|
||||||
element?.type === "rectangle" ||
|
if (!type) {
|
||||||
element?.type === "embeddable" ||
|
return false;
|
||||||
element?.type === "ellipse" ||
|
}
|
||||||
element?.type === "arrow" ||
|
switch (type) {
|
||||||
element?.type === "freedraw" ||
|
case "text":
|
||||||
element?.type === "line"
|
case "diamond":
|
||||||
);
|
case "rectangle":
|
||||||
|
case "embeddable":
|
||||||
|
case "ellipse":
|
||||||
|
case "arrow":
|
||||||
|
case "freedraw":
|
||||||
|
case "line":
|
||||||
|
case "frame":
|
||||||
|
case "image":
|
||||||
|
case "selection": {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
assertNever(type, null);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasBoundTextElement = (
|
export const hasBoundTextElement = (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-13
@@ -123,7 +123,7 @@ describe("adding elements to frames", () => {
|
|||||||
const commonTestCases = async (
|
const commonTestCases = async (
|
||||||
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
||||||
) => {
|
) => {
|
||||||
describe("when frame is in a layer below", async () => {
|
describe.skip("when frame is in a layer below", async () => {
|
||||||
it("should add an element", async () => {
|
it("should add an element", async () => {
|
||||||
h.elements = [frame, rect2];
|
h.elements = [frame, rect2];
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when frame is in a layer above", async () => {
|
describe.skip("when frame is in a layer above", async () => {
|
||||||
it("should add an element", async () => {
|
it("should add an element", async () => {
|
||||||
h.elements = [rect2, frame];
|
h.elements = [rect2, frame];
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when frame is in an inner layer", async () => {
|
describe("when frame is in an inner layer", async () => {
|
||||||
it("should add elements", async () => {
|
it.skip("should add elements", async () => {
|
||||||
h.elements = [rect2, frame, rect3];
|
h.elements = [rect2, frame, rect3];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -223,7 +223,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2, rect3, frame]);
|
expectEqualIds([rect2, rect3, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("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];
|
h.elements = [rect2, rect1, frame, rect4, rect3];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -234,7 +234,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add elements when there are other elements in between and the order is reversed", async () => {
|
it.skip("should add elements when there are other elements in between and the order is reversed", async () => {
|
||||||
h.elements = [rect3, rect4, frame, rect2, rect1];
|
h.elements = [rect3, rect4, frame, rect2, rect1];
|
||||||
|
|
||||||
func(frame, rect2);
|
func(frame, rect2);
|
||||||
@@ -289,7 +289,7 @@ describe("adding elements to frames", () => {
|
|||||||
describe("resizing frame over elements", async () => {
|
describe("resizing frame over elements", async () => {
|
||||||
await commonTestCases(resizeFrameOverElement);
|
await commonTestCases(resizeFrameOverElement);
|
||||||
|
|
||||||
it("resizing over text containers and labelled arrows", async () => {
|
it.skip("resizing over text containers and labelled arrows", async () => {
|
||||||
await resizingTest(
|
await resizingTest(
|
||||||
"rectangle",
|
"rectangle",
|
||||||
["frame", "rectangle", "text"],
|
["frame", "rectangle", "text"],
|
||||||
@@ -339,7 +339,7 @@ describe("adding elements to frames", () => {
|
|||||||
// );
|
// );
|
||||||
});
|
});
|
||||||
|
|
||||||
it("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];
|
h.elements = [frame, arrow, text];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
@@ -359,7 +359,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([arrow, text, frame]);
|
expectEqualIds([arrow, text, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("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];
|
h.elements = [arrow, frame, text];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, arrow);
|
resizeFrameOverElement(frame, arrow);
|
||||||
@@ -371,7 +371,7 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resizing frame over elements but downwards", async () => {
|
describe("resizing frame over elements but downwards", async () => {
|
||||||
it("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];
|
h.elements = [frame, rect1, rect2, rect3, rect4];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
@@ -382,7 +382,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
expectEqualIds([rect2, rect3, frame, rect4, rect1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("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];
|
h.elements = [rect1, rect2, rect3, rect4, frame];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
@@ -393,7 +393,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
expectEqualIds([rect1, rect2, rect3, frame, rect4]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("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];
|
h.elements = [rect1, rect2, frame, rect3, rect4];
|
||||||
|
|
||||||
resizeFrameOverElement(frame, rect4);
|
resizeFrameOverElement(frame, rect4);
|
||||||
@@ -408,7 +408,7 @@ describe("adding elements to frames", () => {
|
|||||||
describe("dragging elements into the frame", async () => {
|
describe("dragging elements into the frame", async () => {
|
||||||
await commonTestCases(dragElementIntoFrame);
|
await commonTestCases(dragElementIntoFrame);
|
||||||
|
|
||||||
it("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];
|
h.elements = [frame, rect2];
|
||||||
|
|
||||||
dragElementIntoFrame(frame, rect2);
|
dragElementIntoFrame(frame, rect2);
|
||||||
@@ -422,7 +422,7 @@ describe("adding elements to frames", () => {
|
|||||||
expectEqualIds([rect2_copy, rect2, frame]);
|
expectEqualIds([rect2_copy, rect2, frame]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("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];
|
h.elements = [frame, rect2];
|
||||||
|
|
||||||
dragElementIntoFrame(frame, rect2);
|
dragElementIntoFrame(frame, rect2);
|
||||||
|
|||||||
+144
-266
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { getCommonBounds, getElementBounds, isTextElement } from "./element";
|
||||||
getCommonBounds,
|
|
||||||
getElementAbsoluteCoords,
|
|
||||||
isTextElement,
|
|
||||||
} from "./element";
|
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawFrameElement,
|
ExcalidrawFrameElement,
|
||||||
@@ -19,10 +15,10 @@ import { mutateElement } from "./element/mutateElement";
|
|||||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { isFrameElement } from "./element";
|
import { isFrameElement } from "./element";
|
||||||
import { moveOneRight } from "./zindex";
|
|
||||||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||||
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||||
import { getElementLineSegments } from "./element/bounds";
|
import { getElementLineSegments } from "./element/bounds";
|
||||||
|
import { doLineSegmentsIntersect } from "./packages/utils";
|
||||||
|
|
||||||
// --------------------------- Frame State ------------------------------------
|
// --------------------------- Frame State ------------------------------------
|
||||||
export const bindElementsToFramesAfterDuplication = (
|
export const bindElementsToFramesAfterDuplication = (
|
||||||
@@ -57,129 +53,21 @@ export const bindElementsToFramesAfterDuplication = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// --------------------------- Frame Geometry ---------------------------------
|
// --------------------------- Frame Geometry ---------------------------------
|
||||||
class Point {
|
export function isElementIntersectingFrame(
|
||||||
x: number;
|
element: ExcalidrawElement,
|
||||||
y: number;
|
frame: ExcalidrawFrameElement,
|
||||||
|
) {
|
||||||
|
const frameLineSegments = getElementLineSegments(frame);
|
||||||
|
|
||||||
constructor(x: number, y: number) {
|
const elementLineSegments = getElementLineSegments(element);
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LineSegment {
|
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
||||||
first: Point;
|
elementLineSegments.some((elementLineSegment) =>
|
||||||
second: Point;
|
doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
constructor(pointA: Point, pointB: Point) {
|
return intersecting;
|
||||||
this.first = pointA;
|
|
||||||
this.second = pointB;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getBoundingBox(): [Point, Point] {
|
|
||||||
return [
|
|
||||||
new Point(
|
|
||||||
Math.min(this.first.x, this.second.x),
|
|
||||||
Math.min(this.first.y, this.second.y),
|
|
||||||
),
|
|
||||||
new Point(
|
|
||||||
Math.max(this.first.x, this.second.x),
|
|
||||||
Math.max(this.first.y, this.second.y),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://martin-thoma.com/how-to-check-if-two-line-segments-intersect/
|
|
||||||
class FrameGeometry {
|
|
||||||
private static EPSILON = 0.000001;
|
|
||||||
|
|
||||||
private static crossProduct(a: Point, b: Point) {
|
|
||||||
return a.x * b.y - b.x * a.y;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static doBoundingBoxesIntersect(
|
|
||||||
a: [Point, Point],
|
|
||||||
b: [Point, Point],
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
a[0].x <= b[1].x &&
|
|
||||||
a[1].x >= b[0].x &&
|
|
||||||
a[0].y <= b[1].y &&
|
|
||||||
a[1].y >= b[0].y
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isPointOnLine(a: LineSegment, b: Point) {
|
|
||||||
const aTmp = new LineSegment(
|
|
||||||
new Point(0, 0),
|
|
||||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
|
||||||
);
|
|
||||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
|
||||||
const r = this.crossProduct(aTmp.second, bTmp);
|
|
||||||
return Math.abs(r) < this.EPSILON;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static isPointRightOfLine(a: LineSegment, b: Point) {
|
|
||||||
const aTmp = new LineSegment(
|
|
||||||
new Point(0, 0),
|
|
||||||
new Point(a.second.x - a.first.x, a.second.y - a.first.y),
|
|
||||||
);
|
|
||||||
const bTmp = new Point(b.x - a.first.x, b.y - a.first.y);
|
|
||||||
return this.crossProduct(aTmp.second, bTmp) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static lineSegmentTouchesOrCrossesLine(
|
|
||||||
a: LineSegment,
|
|
||||||
b: LineSegment,
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
this.isPointOnLine(a, b.first) ||
|
|
||||||
this.isPointOnLine(a, b.second) ||
|
|
||||||
(this.isPointRightOfLine(a, b.first)
|
|
||||||
? !this.isPointRightOfLine(a, b.second)
|
|
||||||
: this.isPointRightOfLine(a, b.second))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static doLineSegmentsIntersect(
|
|
||||||
a: [readonly [number, number], readonly [number, number]],
|
|
||||||
b: [readonly [number, number], readonly [number, number]],
|
|
||||||
) {
|
|
||||||
const aSegment = new LineSegment(
|
|
||||||
new Point(a[0][0], a[0][1]),
|
|
||||||
new Point(a[1][0], a[1][1]),
|
|
||||||
);
|
|
||||||
const bSegment = new LineSegment(
|
|
||||||
new Point(b[0][0], b[0][1]),
|
|
||||||
new Point(b[1][0], b[1][1]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const box1 = aSegment.getBoundingBox();
|
|
||||||
const box2 = bSegment.getBoundingBox();
|
|
||||||
return (
|
|
||||||
this.doBoundingBoxesIntersect(box1, box2) &&
|
|
||||||
this.lineSegmentTouchesOrCrossesLine(aSegment, bSegment) &&
|
|
||||||
this.lineSegmentTouchesOrCrossesLine(bSegment, aSegment)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static isElementIntersectingFrame(
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
frame: ExcalidrawFrameElement,
|
|
||||||
) {
|
|
||||||
const frameLineSegments = getElementLineSegments(frame);
|
|
||||||
|
|
||||||
const elementLineSegments = getElementLineSegments(element);
|
|
||||||
|
|
||||||
const intersecting = frameLineSegments.some((frameLineSegment) =>
|
|
||||||
elementLineSegments.some((elementLineSegment) =>
|
|
||||||
this.doLineSegmentsIntersect(frameLineSegment, elementLineSegment),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return intersecting;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getElementsCompletelyInFrame = (
|
export const getElementsCompletelyInFrame = (
|
||||||
@@ -194,39 +82,27 @@ export const getElementsCompletelyInFrame = (
|
|||||||
element.frameId === frame.id,
|
element.frameId === frame.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
export const isElementContainingFrame = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
element: ExcalidrawElement,
|
|
||||||
frame: ExcalidrawFrameElement,
|
|
||||||
) => {
|
|
||||||
return getElementsWithinSelection(elements, element).some(
|
|
||||||
(e) => e.id === frame.id,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getElementsIntersectingFrame = (
|
export const getElementsIntersectingFrame = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
) =>
|
) => elements.filter((element) => isElementIntersectingFrame(element, frame));
|
||||||
elements.filter((element) =>
|
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const elementsAreInFrameBounds = (
|
export const elementsAreInBounds = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameElement,
|
element: ExcalidrawElement,
|
||||||
|
tolerance = 0,
|
||||||
) => {
|
) => {
|
||||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
|
||||||
getElementAbsoluteCoords(frame);
|
|
||||||
|
|
||||||
const [elementX1, elementY1, elementX2, elementY2] =
|
const [elementX1, elementY1, elementX2, elementY2] =
|
||||||
|
getElementBounds(element);
|
||||||
|
|
||||||
|
const [elementsX1, elementsY1, elementsX2, elementsY2] =
|
||||||
getCommonBounds(elements);
|
getCommonBounds(elements);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
selectionX1 <= elementX1 &&
|
elementX1 <= elementsX1 - tolerance &&
|
||||||
selectionY1 <= elementY1 &&
|
elementY1 <= elementsY1 - tolerance &&
|
||||||
selectionX2 >= elementX2 &&
|
elementX2 >= elementsX2 + tolerance &&
|
||||||
selectionY2 >= elementY2
|
elementY2 >= elementsY2 + tolerance
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,9 +111,12 @@ export const elementOverlapsWithFrame = (
|
|||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
elementsAreInFrameBounds([element], frame) ||
|
// frame contains element
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame) ||
|
elementsAreInBounds([element], frame) ||
|
||||||
isElementContainingFrame([frame], element, frame)
|
// element contains frame
|
||||||
|
(elementsAreInBounds([frame], element) && element.frameId === frame.id) ||
|
||||||
|
// element intersects with frame
|
||||||
|
isElementIntersectingFrame(element, frame)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -248,7 +127,7 @@ export const isCursorInFrame = (
|
|||||||
},
|
},
|
||||||
frame: NonDeleted<ExcalidrawFrameElement>,
|
frame: NonDeleted<ExcalidrawFrameElement>,
|
||||||
) => {
|
) => {
|
||||||
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame);
|
const [fx1, fy1, fx2, fy2] = getElementBounds(frame);
|
||||||
|
|
||||||
return isPointWithinBounds(
|
return isPointWithinBounds(
|
||||||
[fx1, fy1],
|
[fx1, fy1],
|
||||||
@@ -272,8 +151,8 @@ export const groupsAreAtLeastIntersectingTheFrame = (
|
|||||||
|
|
||||||
return !!elementsInGroup.find(
|
return !!elementsInGroup.find(
|
||||||
(element) =>
|
(element) =>
|
||||||
elementsAreInFrameBounds([element], frame) ||
|
elementsAreInBounds([element], frame) ||
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
isElementIntersectingFrame(element, frame),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,8 +172,8 @@ export const groupsAreCompletelyOutOfFrame = (
|
|||||||
return (
|
return (
|
||||||
elementsInGroup.find(
|
elementsInGroup.find(
|
||||||
(element) =>
|
(element) =>
|
||||||
elementsAreInFrameBounds([element], frame) ||
|
elementsAreInBounds([element], frame) ||
|
||||||
FrameGeometry.isElementIntersectingFrame(element, frame),
|
isElementIntersectingFrame(element, frame),
|
||||||
) === undefined
|
) === undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -313,32 +192,66 @@ export const groupByFrames = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
const frameId = isFrameElement(element) ? element.id : element.frameId;
|
const frameId = isFrameElement(element) ? element.id : element.frameId;
|
||||||
if (frameId && !frameElementsMap.has(frameId)) {
|
if (frameId && !frameElementsMap.has(frameId)) {
|
||||||
frameElementsMap.set(frameId, getFrameElements(elements, frameId));
|
frameElementsMap.set(frameId, getFrameChildren(elements, frameId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return frameElementsMap;
|
return frameElementsMap;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFrameElements = (
|
export const getFrameChildren = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
frameId: string,
|
frameId: string,
|
||||||
) => allElements.filter((element) => element.frameId === frameId);
|
) => allElements.filter((element) => element.frameId === frameId);
|
||||||
|
|
||||||
|
export const getFrameElements = (
|
||||||
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
|
): ExcalidrawFrameElement[] => {
|
||||||
|
return allElements.filter((element) =>
|
||||||
|
isFrameElement(element),
|
||||||
|
) as ExcalidrawFrameElement[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns ExcalidrawFrameElements and non-frame-children elements.
|
||||||
|
*
|
||||||
|
* Considers children as root elements if they point to a frame parent
|
||||||
|
* non-existing in the elements set.
|
||||||
|
*
|
||||||
|
* Considers non-frame bound elements (container or arrow labels) as root.
|
||||||
|
*/
|
||||||
|
export const getRootElements = (
|
||||||
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
|
) => {
|
||||||
|
const frameElements = arrayToMap(getFrameElements(allElements));
|
||||||
|
return allElements.filter(
|
||||||
|
(element) =>
|
||||||
|
frameElements.has(element.id) ||
|
||||||
|
!element.frameId ||
|
||||||
|
!frameElements.has(element.frameId),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getElementsInResizingFrame = (
|
export const getElementsInResizingFrame = (
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const prevElementsInFrame = getFrameElements(allElements, frame.id);
|
const prevElementsInFrame = getFrameChildren(allElements, frame.id);
|
||||||
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
|
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
|
||||||
|
|
||||||
const elementsCompletelyInFrame = new Set([
|
const elementsCompletelyInFrame = new Set<ExcalidrawElement>(
|
||||||
...getElementsCompletelyInFrame(allElements, frame),
|
getElementsCompletelyInFrame(allElements, frame),
|
||||||
...prevElementsInFrame.filter((element) =>
|
);
|
||||||
isElementContainingFrame(allElements, element, frame),
|
|
||||||
),
|
for (const element of prevElementsInFrame) {
|
||||||
]);
|
if (!elementsCompletelyInFrame.has(element)) {
|
||||||
|
// element contains the frame
|
||||||
|
if (elementsAreInBounds([frame], element)) {
|
||||||
|
elementsCompletelyInFrame.add(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
|
const elementsNotCompletelyInFrame = prevElementsInFrame.filter(
|
||||||
(element) => !elementsCompletelyInFrame.has(element),
|
(element) => !elementsCompletelyInFrame.has(element),
|
||||||
@@ -354,7 +267,7 @@ export const getElementsInResizingFrame = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const element of elementsNotCompletelyInFrame) {
|
for (const element of elementsNotCompletelyInFrame) {
|
||||||
if (!FrameGeometry.isElementIntersectingFrame(element, frame)) {
|
if (!isElementIntersectingFrame(element, frame)) {
|
||||||
if (element.groupIds.length === 0) {
|
if (element.groupIds.length === 0) {
|
||||||
nextElementsInFrame.delete(element);
|
nextElementsInFrame.delete(element);
|
||||||
}
|
}
|
||||||
@@ -405,7 +318,7 @@ export const getElementsInResizingFrame = (
|
|||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
const elementsInGroup = getElementsInGroup(allElements, id);
|
const elementsInGroup = getElementsInGroup(allElements, id);
|
||||||
|
|
||||||
if (elementsAreInFrameBounds(elementsInGroup, frame)) {
|
if (elementsAreInBounds(elementsInGroup, frame)) {
|
||||||
for (const element of elementsInGroup) {
|
for (const element of elementsInGroup) {
|
||||||
nextElementsInFrame.add(element);
|
nextElementsInFrame.add(element);
|
||||||
}
|
}
|
||||||
@@ -454,7 +367,7 @@ export const getContainingFrame = (
|
|||||||
// --------------------------- Frame Operations -------------------------------
|
// --------------------------- Frame Operations -------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retains (or repairs for target frame) the ordering invriant where children
|
* Retains (or repairs for target frame) the ordering invariant where children
|
||||||
* elements come right before the parent frame:
|
* elements come right before the parent frame:
|
||||||
* [el, el, child, child, frame, el]
|
* [el, el, child, child, frame, el]
|
||||||
*/
|
*/
|
||||||
@@ -463,20 +376,17 @@ export const addElementsToFrame = (
|
|||||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
) => {
|
) => {
|
||||||
const { allElementsIndexMap, currTargetFrameChildrenMap } =
|
const { currTargetFrameChildrenMap } = allElements.reduce(
|
||||||
allElements.reduce(
|
(acc, element, index) => {
|
||||||
(acc, element, index) => {
|
if (element.frameId === frame.id) {
|
||||||
acc.allElementsIndexMap.set(element.id, index);
|
acc.currTargetFrameChildrenMap.set(element.id, true);
|
||||||
if (element.frameId === frame.id) {
|
}
|
||||||
acc.currTargetFrameChildrenMap.set(element.id, true);
|
return acc;
|
||||||
}
|
},
|
||||||
return acc;
|
{
|
||||||
},
|
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
||||||
{
|
},
|
||||||
allElementsIndexMap: new Map<ExcalidrawElement["id"], number>(),
|
);
|
||||||
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||||
|
|
||||||
@@ -502,66 +412,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);
|
|
||||||
});
|
|
||||||
|
|
||||||
// if not found, add all children on top by assigning the lowest index
|
|
||||||
const targetFrameIndex = allElementsIndexMap.get(frame.id) ?? -1;
|
|
||||||
|
|
||||||
const { newChildren_left, newChildren_right } = finalElementsToAdd.reduce(
|
|
||||||
(acc, element) => {
|
|
||||||
// if index not found, add on top of current frame children
|
|
||||||
const elementIndex = allElementsIndexMap.get(element.id) ?? Infinity;
|
|
||||||
if (elementIndex < targetFrameIndex) {
|
|
||||||
acc.newChildren_left.push(element);
|
|
||||||
} else {
|
|
||||||
acc.newChildren_right.push(element);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
newChildren_left: [] as ExcalidrawElement[],
|
|
||||||
newChildren_right: [] as ExcalidrawElement[],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
nextElements.push(
|
|
||||||
...newChildren_left,
|
|
||||||
...currFrameChildren,
|
|
||||||
...newChildren_right,
|
|
||||||
element,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextElements.push(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const element of finalElementsToAdd) {
|
for (const element of finalElementsToAdd) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
@@ -571,8 +421,7 @@ export const addElementsToFrame = (
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return allElements.slice();
|
||||||
return nextElements;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeElementsFromFrame = (
|
export const removeElementsFromFrame = (
|
||||||
@@ -580,20 +429,23 @@ export const removeElementsFromFrame = (
|
|||||||
elementsToRemove: NonDeletedExcalidrawElement[],
|
elementsToRemove: NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const _elementsToRemove: ExcalidrawElement[] = [];
|
const _elementsToRemove = new Map<
|
||||||
|
ExcalidrawElement["id"],
|
||||||
|
ExcalidrawElement
|
||||||
|
>();
|
||||||
|
|
||||||
for (const element of elementsToRemove) {
|
for (const element of elementsToRemove) {
|
||||||
if (element.frameId) {
|
if (element.frameId) {
|
||||||
_elementsToRemove.push(element);
|
_elementsToRemove.set(element.id, element);
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
_elementsToRemove.push(boundTextElement);
|
_elementsToRemove.set(boundTextElement.id, boundTextElement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const element of _elementsToRemove) {
|
for (const [, element] of _elementsToRemove) {
|
||||||
mutateElement(
|
mutateElement(
|
||||||
element,
|
element,
|
||||||
{
|
{
|
||||||
@@ -603,13 +455,7 @@ export const removeElementsFromFrame = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextElements = moveOneRight(
|
return allElements.slice();
|
||||||
allElements,
|
|
||||||
appState,
|
|
||||||
Array.from(_elementsToRemove),
|
|
||||||
);
|
|
||||||
|
|
||||||
return nextElements;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeAllElementsFromFrame = (
|
export const removeAllElementsFromFrame = (
|
||||||
@@ -617,7 +463,7 @@ export const removeAllElementsFromFrame = (
|
|||||||
frame: ExcalidrawFrameElement,
|
frame: ExcalidrawFrameElement,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
const elementsInFrame = getFrameElements(allElements, frame.id);
|
const elementsInFrame = getFrameChildren(allElements, frame.id);
|
||||||
return removeElementsFromFrame(allElements, elementsInFrame, appState);
|
return removeElementsFromFrame(allElements, elementsInFrame, appState);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -660,12 +506,15 @@ export const updateFrameMembershipOfSelectedElements = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
const elementsToRemove = new Set<ExcalidrawElement>();
|
const elementsToRemove = new Set<ExcalidrawElement>();
|
||||||
|
const processedGroupIds = new Map<string, boolean>();
|
||||||
|
|
||||||
elementsToFilter.forEach((element) => {
|
elementsToFilter.forEach((element) => {
|
||||||
if (
|
if (
|
||||||
element.frameId &&
|
element.frameId &&
|
||||||
!isFrameElement(element) &&
|
!isFrameElement(element) &&
|
||||||
!isElementInFrame(element, allElements, appState)
|
!isElementInFrame(element, allElements, appState, {
|
||||||
|
processedGroupIds,
|
||||||
|
})
|
||||||
) {
|
) {
|
||||||
elementsToRemove.add(element);
|
elementsToRemove.add(element);
|
||||||
}
|
}
|
||||||
@@ -727,26 +576,36 @@ export const getTargetFrame = (
|
|||||||
: getContainingFrame(_element);
|
: getContainingFrame(_element);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: this a huge bottleneck for large scenes, optimise
|
|
||||||
// given an element, return if the element is in some frame
|
// given an element, return if the element is in some frame
|
||||||
export const isElementInFrame = (
|
export const isElementInFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
appState: StaticCanvasAppState,
|
appState: StaticCanvasAppState,
|
||||||
|
opts?: {
|
||||||
|
targetFrame?: ExcalidrawFrameElement;
|
||||||
|
processedGroupIds?: Map<string, boolean>;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const frame = getTargetFrame(element, appState);
|
const frame = opts?.targetFrame ?? getTargetFrame(element, appState);
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element) || element
|
? getContainerElement(element) || element
|
||||||
: element;
|
: element;
|
||||||
|
|
||||||
|
const groupsInFrame = (yes: boolean) => {
|
||||||
|
if (opts?.processedGroupIds) {
|
||||||
|
_element.groupIds.forEach((gid) => {
|
||||||
|
opts.processedGroupIds?.set(gid, yes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (frame) {
|
if (frame) {
|
||||||
// Perf improvement:
|
// Perf improvement:
|
||||||
// For an element that's already in a frame, if it's not being dragged
|
// For an element that's already in a frame, if it's not being selected
|
||||||
// then there is no need to refer to geometry (which, yes, is slow) to check if it's in a frame.
|
// and its frame is not being selected, it has to be in its containing frame.
|
||||||
// It has to be in its containing frame.
|
|
||||||
if (
|
if (
|
||||||
!appState.selectedElementIds[element.id] ||
|
!appState.selectedElementIds[element.id] &&
|
||||||
!appState.selectedElementsAreBeingDragged
|
!appState.selectedElementIds[frame.id]
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -755,8 +614,21 @@ export const isElementInFrame = (
|
|||||||
return elementOverlapsWithFrame(_element, frame);
|
return elementOverlapsWithFrame(_element, frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const gid of _element.groupIds) {
|
||||||
|
if (opts?.processedGroupIds?.has(gid)) {
|
||||||
|
return opts.processedGroupIds.get(gid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const allElementsInGroup = new Set(
|
const allElementsInGroup = new Set(
|
||||||
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)),
|
_element.groupIds
|
||||||
|
.filter((gid) => {
|
||||||
|
if (opts?.processedGroupIds) {
|
||||||
|
return !opts.processedGroupIds.has(gid);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.flatMap((gid) => getElementsInGroup(allElements, gid)),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
|
||||||
@@ -777,16 +649,22 @@ export const isElementInFrame = (
|
|||||||
|
|
||||||
for (const elementInGroup of allElementsInGroup) {
|
for (const elementInGroup of allElementsInGroup) {
|
||||||
if (isFrameElement(elementInGroup)) {
|
if (isFrameElement(elementInGroup)) {
|
||||||
|
groupsInFrame(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const elementInGroup of allElementsInGroup) {
|
for (const elementInGroup of allElementsInGroup) {
|
||||||
if (elementOverlapsWithFrame(elementInGroup, frame)) {
|
if (elementOverlapsWithFrame(elementInGroup, frame)) {
|
||||||
|
groupsInFrame(true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_element.groupIds.length > 0) {
|
||||||
|
groupsInFrame(false);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
+9
-4
@@ -232,6 +232,8 @@ export const selectGroupsFromGivenElements = (
|
|||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processedGroupIds = new Set<string>();
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
let groupIds = element.groupIds;
|
let groupIds = element.groupIds;
|
||||||
if (appState.editingGroupId) {
|
if (appState.editingGroupId) {
|
||||||
@@ -242,10 +244,13 @@ export const selectGroupsFromGivenElements = (
|
|||||||
}
|
}
|
||||||
if (groupIds.length > 0) {
|
if (groupIds.length > 0) {
|
||||||
const groupId = groupIds[groupIds.length - 1];
|
const groupId = groupIds[groupIds.length - 1];
|
||||||
nextAppState = {
|
if (!processedGroupIds.has(groupId)) {
|
||||||
...nextAppState,
|
nextAppState = {
|
||||||
...selectGroup(groupId, nextAppState, elements),
|
...nextAppState,
|
||||||
};
|
...selectGroup(groupId, nextAppState, elements),
|
||||||
|
};
|
||||||
|
processedGroupIds.add(groupId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useLayoutEffect } from "react";
|
import { useState, useLayoutEffect } from "react";
|
||||||
import { useDevice, useExcalidrawContainer } from "../components/App";
|
import { useDevice, useExcalidrawContainer } from "../components/App";
|
||||||
import { useUIAppState } from "../context/ui-appState";
|
import { useUIAppState } from "../context/ui-appState";
|
||||||
|
|
||||||
@@ -10,16 +10,17 @@ export const useCreatePortalContainer = (opts?: {
|
|||||||
|
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
const { theme } = useUIAppState();
|
const { theme } = useUIAppState();
|
||||||
const isMobileRef = useRef(device.isMobile);
|
|
||||||
isMobileRef.current = device.isMobile;
|
|
||||||
|
|
||||||
const { container: excalidrawContainer } = useExcalidrawContainer();
|
const { container: excalidrawContainer } = useExcalidrawContainer();
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (div) {
|
if (div) {
|
||||||
div.classList.toggle("excalidraw--mobile", device.isMobile);
|
div.className = "";
|
||||||
|
div.classList.add("excalidraw", ...(opts?.className?.split(/\s+/) || []));
|
||||||
|
div.classList.toggle("excalidraw--mobile", device.editor.isMobile);
|
||||||
|
div.classList.toggle("theme--dark", theme === "dark");
|
||||||
}
|
}
|
||||||
}, [div, device.isMobile]);
|
}, [div, theme, device.editor.isMobile, opts?.className]);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const container = opts?.parentSelector
|
const container = opts?.parentSelector
|
||||||
@@ -32,10 +33,6 @@ export const useCreatePortalContainer = (opts?: {
|
|||||||
|
|
||||||
const div = document.createElement("div");
|
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);
|
container.appendChild(div);
|
||||||
|
|
||||||
setDiv(div);
|
setDiv(div);
|
||||||
@@ -43,7 +40,7 @@ export const useCreatePortalContainer = (opts?: {
|
|||||||
return () => {
|
return () => {
|
||||||
container.removeChild(div);
|
container.removeChild(div);
|
||||||
};
|
};
|
||||||
}, [excalidrawContainer, theme, opts?.className, opts?.parentSelector]);
|
}, [excalidrawContainer, opts?.parentSelector]);
|
||||||
|
|
||||||
return div;
|
return div;
|
||||||
};
|
};
|
||||||
|
|||||||
+129
-80
@@ -50,7 +50,7 @@
|
|||||||
"veryLarge": "كبير جدا",
|
"veryLarge": "كبير جدا",
|
||||||
"solid": "كامل",
|
"solid": "كامل",
|
||||||
"hachure": "خطوط",
|
"hachure": "خطوط",
|
||||||
"zigzag": "",
|
"zigzag": "متعرج",
|
||||||
"crossHatch": "خطوط متقطعة",
|
"crossHatch": "خطوط متقطعة",
|
||||||
"thin": "نحيف",
|
"thin": "نحيف",
|
||||||
"bold": "داكن",
|
"bold": "داكن",
|
||||||
@@ -106,11 +106,15 @@
|
|||||||
"increaseFontSize": "تكبير حجم الخط",
|
"increaseFontSize": "تكبير حجم الخط",
|
||||||
"unbindText": "فك ربط النص",
|
"unbindText": "فك ربط النص",
|
||||||
"bindText": "ربط النص بالحاوية",
|
"bindText": "ربط النص بالحاوية",
|
||||||
"createContainerFromText": "",
|
"createContainerFromText": "نص مغلف في حاوية",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "تعديل الرابط",
|
"edit": "تعديل الرابط",
|
||||||
|
"editEmbed": "تحرير الرابط وإدراجه",
|
||||||
"create": "إنشاء رابط",
|
"create": "إنشاء رابط",
|
||||||
"label": "رابط"
|
"createEmbed": "إنشاء رابط و إدراجه",
|
||||||
|
"label": "رابط",
|
||||||
|
"labelEmbed": "رابط و إدراج",
|
||||||
|
"empty": "لم يتم تعيين رابط"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "تحرير السطر",
|
"edit": "تحرير السطر",
|
||||||
@@ -124,9 +128,9 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "نُشر",
|
"statusPublished": "نُشر",
|
||||||
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
"sidebarLock": "إبقاء الشريط الجانبي مفتوح",
|
||||||
"selectAllElementsInFrame": "",
|
"selectAllElementsInFrame": "تحديد جميع العناصر في الإطار",
|
||||||
"removeAllElementsFromFrame": "",
|
"removeAllElementsFromFrame": "إزالة جميع العناصر من الإطار",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "اختيار اللون من القماش"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "لا توجد عناصر أضيفت بعد...",
|
"noItems": "لا توجد عناصر أضيفت بعد...",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "الوضع المظلم",
|
"darkMode": "الوضع المظلم",
|
||||||
"lightMode": "الوضع المضيء",
|
"lightMode": "الوضع المضيء",
|
||||||
"zenMode": "وضع التأمل",
|
"zenMode": "وضع التأمل",
|
||||||
|
"objectsSnapMode": "التقط إلى العناصر",
|
||||||
"exitZenMode": "إلغاء الوضع الليلى",
|
"exitZenMode": "إلغاء الوضع الليلى",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"clear": "مسح",
|
"clear": "مسح",
|
||||||
"remove": "إزالة",
|
"remove": "إزالة",
|
||||||
|
"embed": "تبديل الإدراج",
|
||||||
"publishLibrary": "انشر",
|
"publishLibrary": "انشر",
|
||||||
"submit": "أرسل",
|
"submit": "أرسل",
|
||||||
"confirm": "تأكيد"
|
"confirm": "تأكيد",
|
||||||
|
"embeddableInteractionButton": "اضغط للتفاعل"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
|
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
|
||||||
@@ -189,23 +196,28 @@
|
|||||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||||
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
||||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
||||||
"collabOfflineWarning": ""
|
"collabOfflineWarning": "لا يوجد اتصال بالانترنت.\nلن يتم حفظ التغييرات التي قمت بها!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
||||||
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
|
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
|
||||||
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
||||||
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "SVG غير صالح.",
|
"invalidSVGString": "SVG غير صالح.",
|
||||||
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
|
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
|
||||||
"importLibraryError": "تعذر تحميل المكتبة",
|
"importLibraryError": "تعذر تحميل المكتبة",
|
||||||
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
|
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
|
||||||
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.",
|
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.",
|
||||||
"brave_measure_text_error": {
|
"brave_measure_text_error": {
|
||||||
"line1": "",
|
"line1": "يبدو أنك تستخدم متصفح Brave مع إعداد <bold>حظر صارم لتتبع البصمة</bold>.",
|
||||||
"line2": "",
|
"line2": "قد يؤدي هذا إلى كسر <bold>عناصر النص</bold> في الرسومات الخاصة بك.",
|
||||||
"line3": "",
|
"line3": "من المستحسن إلغاء تفعيل هذا الإعداد. يمكنك اتباع <link>هذه الخطوات</link> لفعل ذلك.",
|
||||||
"line4": ""
|
"line4": "إذا لم يصلح تعطيل هذا الإعداد طريقة عرض النصوص، الرجاء كتابة <issueLink>بلاغ</issueLink> على حسابنا في GitHub، أو راسلنا على <discordLink>Discord</discordLink>"
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "لا يمكن إضافة العناصر القابلة للتضمين في المكتبة.",
|
||||||
|
"image": "سوف يتم دعم إضافة صور إلى المكتبة قريباً!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -223,9 +235,11 @@
|
|||||||
"penMode": "وضع القلم - امنع اللمس",
|
"penMode": "وضع القلم - امنع اللمس",
|
||||||
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
||||||
"eraser": "ممحاة",
|
"eraser": "ممحاة",
|
||||||
"frame": "",
|
"frame": "أداة الإطار",
|
||||||
"hand": "",
|
"embeddable": "تضمين ويب",
|
||||||
"extraTools": ""
|
"laser": "مؤشر ليزر",
|
||||||
|
"hand": "يد (أداة الإزاحة)",
|
||||||
|
"extraTools": "المزيد من أﻷدوات"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "إجراءات اللوحة",
|
"canvasActions": "إجراءات اللوحة",
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
||||||
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
||||||
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
||||||
|
"embeddable": "اضغط مع السحب لإنشاء موقع ويب مضمّن",
|
||||||
"text_selected": "انقر نقراً مزدوجاً أو اضغط ادخال لتعديل النص",
|
"text_selected": "انقر نقراً مزدوجاً أو اضغط ادخال لتعديل النص",
|
||||||
"text_editing": "اضغط على Esc أو (Ctrl أو Cmd) + Enter لإنهاء التعديل",
|
"text_editing": "اضغط على Esc أو (Ctrl أو Cmd) + Enter لإنهاء التعديل",
|
||||||
"linearElementMulti": "انقر فوق النقطة الأخيرة أو اضغط على Esc أو Enter للإنهاء",
|
"linearElementMulti": "انقر فوق النقطة الأخيرة أو اضغط على Esc أو Enter للإنهاء",
|
||||||
@@ -245,14 +260,15 @@
|
|||||||
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
|
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
|
||||||
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
|
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
|
||||||
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
|
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة (النِّقَاط)، Ctrl/Cmd+D للتكرار، أو اسحب للانتقال",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "اختر نقطة لتعديلها (اضغط على SHIFT لتحديد عدة نِقَاط),\nأو اضغط على ALT و انقر بالفأرة لإضافة نِقَاط جديدة",
|
||||||
"placeImage": "",
|
"placeImage": "انقر لوضع الصورة، أو انقر واسحب لتعيين حجمها يدوياً",
|
||||||
"publishLibrary": "نشر مكتبتك",
|
"publishLibrary": "نشر مكتبتك",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "اضغط على إدخال لإضافة نص",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "اضغط على Ctrl\\Cmd للاختيار العميق، ولمنع السحب",
|
||||||
"eraserRevert": "",
|
"eraserRevert": "اضغط على Alt لاستعادة العناصر المعلَّمة للحذف",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "يمكن على الأرجح تمكين هذه الميزة عن طريق تعيين علم \"dom.events.asyncClipboard.clipboardItem\" إلى \"true\". لتغيير أعلام المتصفح في Firefox، قم بزيارة صفحة \"about:config\".",
|
||||||
|
"disableSnapping": "اضغط على Ctrl أو Cmd لتعطيل الالتقاط"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "تعذر عرض المعاينة",
|
"cannotShowPreview": "تعذر عرض المعاينة",
|
||||||
@@ -260,11 +276,11 @@
|
|||||||
"canvasTooBigTip": "نصيحة: حاول تحريك العناصر البعيدة بشكل أقرب قليلاً."
|
"canvasTooBigTip": "نصيحة: حاول تحريك العناصر البعيدة بشكل أقرب قليلاً."
|
||||||
},
|
},
|
||||||
"errorSplash": {
|
"errorSplash": {
|
||||||
"headingMain": "",
|
"headingMain": "حدث خطأ. حاول <button>تحديث الصفحة</button>.",
|
||||||
"clearCanvasMessage": "إذا لم تعمل إعادة التحميل، حاول مرة أخرى ",
|
"clearCanvasMessage": "إذا لم تعمل إعادة التحميل، حاول مرة أخرى ",
|
||||||
"clearCanvasCaveat": " هذا سيؤدي إلى فقدان العمل ",
|
"clearCanvasCaveat": " هذا سيؤدي إلى فقدان العمل ",
|
||||||
"trackedToSentry": "",
|
"trackedToSentry": "تم تتبع الخطأ في المعرف {{eventId}} على نظامنا.",
|
||||||
"openIssueMessage": "",
|
"openIssueMessage": "حرصنا على عدم إضافة معلومات المشهد في بلاغ الخطأ. في حال كون مشهدك لا يحمل أي معلومات خاصة نرجو المتابعة على <button>نظام تتبع الأخطاء</button>. نرجو إضافة المعلومات أدناه بنسخها ولصقها في محتوى البلاغ على GitHub.",
|
||||||
"sceneContent": "محتوى المشهد:"
|
"sceneContent": "محتوى المشهد:"
|
||||||
},
|
},
|
||||||
"roomDialog": {
|
"roomDialog": {
|
||||||
@@ -294,16 +310,16 @@
|
|||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
"blog": "اقرأ مدونتنا",
|
"blog": "اقرأ مدونتنا",
|
||||||
"click": "انقر",
|
"click": "انقر",
|
||||||
"deepSelect": "",
|
"deepSelect": "تحديد عميق",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "تحديد عميق داخل المربع، ومنع السحب",
|
||||||
"curvedArrow": "سهم مائل",
|
"curvedArrow": "سهم مائل",
|
||||||
"curvedLine": "خط مائل",
|
"curvedLine": "خط مائل",
|
||||||
"documentation": "دليل الاستخدام",
|
"documentation": "دليل الاستخدام",
|
||||||
"doubleClick": "انقر مرتين",
|
"doubleClick": "انقر مرتين",
|
||||||
"drag": "اسحب",
|
"drag": "اسحب",
|
||||||
"editor": "المحرر",
|
"editor": "المحرر",
|
||||||
"editLineArrowPoints": "",
|
"editLineArrowPoints": "تحرير سطر/نقاط سهم",
|
||||||
"editText": "",
|
"editText": "تعديل النص / إضافة تسمية",
|
||||||
"github": "عثرت على مشكلة؟ إرسال",
|
"github": "عثرت على مشكلة؟ إرسال",
|
||||||
"howto": "اتبع التعليمات",
|
"howto": "اتبع التعليمات",
|
||||||
"or": "أو",
|
"or": "أو",
|
||||||
@@ -316,9 +332,9 @@
|
|||||||
"view": "عرض",
|
"view": "عرض",
|
||||||
"zoomToFit": "تكبير للملائمة",
|
"zoomToFit": "تكبير للملائمة",
|
||||||
"zoomToSelection": "تكبير للعنصر المحدد",
|
"zoomToSelection": "تكبير للعنصر المحدد",
|
||||||
"toggleElementLock": "",
|
"toggleElementLock": "إغلاق/فتح المحدد",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "نقل الصفحة أعلى/أسفل",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "نقل الصفحة يسار/يمين"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "مسح اللوحة"
|
"title": "مسح اللوحة"
|
||||||
@@ -336,20 +352,20 @@
|
|||||||
"authorName": "اسمك أو اسم المستخدم",
|
"authorName": "اسمك أو اسم المستخدم",
|
||||||
"libraryName": "اسم مكتبتك",
|
"libraryName": "اسم مكتبتك",
|
||||||
"libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
|
"libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
|
||||||
"githubHandle": "",
|
"githubHandle": "معالج GitHub (اختياري)، حتى تتمكن من تحرير المكتبة عند إرسالها للمراجعة",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "اسم مستخدم تويتر (اختياري)، حتى نعرف من الذي سيتم الإشارة إليه عند الترويج عبر تويتر",
|
||||||
"website": ""
|
"website": "رابط إلى موقعك الشخصي أو في مكان آخر (اختياري)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "مطلوب",
|
"required": "مطلوب",
|
||||||
"website": "أدخل عنوان URL صالح"
|
"website": "أدخل عنوان URL صالح"
|
||||||
},
|
},
|
||||||
"noteDescription": "",
|
"noteDescription": "تقديم مكتبتك لتضمينها في مستودع المكتبة العامة <link></link> لأشخاص آخرين لاستخدامها في رسومهم.",
|
||||||
"noteGuidelines": "",
|
"noteGuidelines": "تحتاج المكتبة إلى الموافقة أولا. يرجى قراءة <link>المعايير</link> قبل تقديمها. سوف تحتاج إلى حساب GitHub للتواصل وإجراء التغييرات عند الطلب، ولكن ليس مطلوبا بشكل صارم.",
|
||||||
"noteLicense": "",
|
"noteLicense": "تقديمك يعني موافقتك على نشر المكتبة المقدمة تحت <link>MIT ترخيص</link>، ما يعني أن لأي أحد الحق في استخدامها دون قيود.",
|
||||||
"noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
|
"noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
|
||||||
"atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء",
|
"atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء",
|
||||||
"republishWarning": ""
|
"republishWarning": "ملاحظة: بعض العناصر المحددة معينة على أنه نشرها أو تقديمها من قبل. يجب عليك فقط إعادة إرسال العناصر عند تحديث مكتبة موجودة أو إرسالها."
|
||||||
},
|
},
|
||||||
"publishSuccessDialog": {
|
"publishSuccessDialog": {
|
||||||
"title": "تم إرسال المكتبة",
|
"title": "تم إرسال المكتبة",
|
||||||
@@ -360,27 +376,27 @@
|
|||||||
"removeItemsFromLib": "إزالة العناصر المحددة من المكتبة"
|
"removeItemsFromLib": "إزالة العناصر المحددة من المكتبة"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "تصدير الصورة",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "الخلفية",
|
||||||
"onlySelected": "",
|
"onlySelected": "المحدد فقط",
|
||||||
"darkMode": "",
|
"darkMode": "الوضع الداكن",
|
||||||
"embedScene": "",
|
"embedScene": "تضمين المشهد",
|
||||||
"scale": "",
|
"scale": "الحجم",
|
||||||
"padding": ""
|
"padding": "الهوامش"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": "سيتم حفظ بيانات المشهد في ملف PNG/SVG المصدّر بحيث يمكن استعادة المشهد منه.\nسيزيد حجم الملف المصدر."
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "تصدير بصيغة PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "تصدير بصيغة SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "نسخ الـ PNG إلى الحافظة"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "نسخ إلى الحافظة"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -411,43 +427,76 @@
|
|||||||
"fileSavedToFilename": "حفظ باسم {filename}",
|
"fileSavedToFilename": "حفظ باسم {filename}",
|
||||||
"canvas": "لوحة الرسم",
|
"canvas": "لوحة الرسم",
|
||||||
"selection": "العنصر المحدد",
|
"selection": "العنصر المحدد",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "استخدم {{shortcut}} للصق كعنصر واحد،\nأو لصق في محرر نص موجود",
|
||||||
|
"unableToEmbed": "تضمين هذا الرابط غير مسموح حاليًا. افتح بلاغاً على GitHub لطلب عنوان Url القائمة البيضاء",
|
||||||
|
"unrecognizedLinkFormat": "الرابط الذي ضمنته لا يتطابق مع التنسيق المتوقع. الرجاء محاولة لصق النص 'المضمن' المُزوَد من موقع المصدر"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "شفاف",
|
"transparent": "شفاف",
|
||||||
"black": "",
|
"black": "أسود",
|
||||||
"white": "",
|
"white": "أبيض",
|
||||||
"red": "",
|
"red": "أحمر",
|
||||||
"pink": "",
|
"pink": "وردي",
|
||||||
"grape": "",
|
"grape": "عنبي",
|
||||||
"violet": "",
|
"violet": "بنفسجي",
|
||||||
"gray": "",
|
"gray": "رمادي",
|
||||||
"blue": "",
|
"blue": "أزرق",
|
||||||
"cyan": "",
|
"cyan": "سماوي",
|
||||||
"teal": "",
|
"teal": "أزرق مخضر",
|
||||||
"green": "",
|
"green": "أخضر",
|
||||||
"yellow": "",
|
"yellow": "أصفر",
|
||||||
"orange": "",
|
"orange": "برتقالي",
|
||||||
"bronze": ""
|
"bronze": "برونزي"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "جميع بياناتك محفوظة محليا في المتصفح الخاص بك.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "هل تريد الذهاب إلى Excalidraw+ بدلاً من ذلك؟",
|
||||||
"menuHint": ""
|
"menuHint": "التصدير والتفضيلات واللغات ..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "التصدير والتفضيلات وغيرها...",
|
||||||
"center_heading": "",
|
"center_heading": "الرسم البياني التصويري. بشكل مبسط.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "اختر أداة و ابدأ الرسم!",
|
||||||
"helpHint": ""
|
"helpHint": "الاختصارات و المساعدة"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "الألوان المخصصة الأكثر استخداما",
|
||||||
"colors": "",
|
"colors": "الألوان",
|
||||||
"shades": "",
|
"shades": "الدرجات",
|
||||||
"hexCode": "",
|
"hexCode": "رمز Hex",
|
||||||
"noShades": ""
|
"noShades": "لا تتوفر درجات لهذا اللون"
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "تصدير كصورة",
|
||||||
|
"button": "تصدير كصورة",
|
||||||
|
"description": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقاً."
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "حفظ الملف للجهاز",
|
||||||
|
"button": "حفظ الملف للجهاز",
|
||||||
|
"description": "تصدير بيانات المشهد إلى ملف يمكنك الاستيراد منه لاحقاً."
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "Excalidraw+",
|
||||||
|
"button": "تصدير إلى Excalidraw+",
|
||||||
|
"description": "حفظ المشهد إلى مساحة العمل +Excalidraw الخاصة بك."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "تحميل من ملف",
|
||||||
|
"button": "تحميل من ملف",
|
||||||
|
"description": "سيتم التحميل من الملف <bold>استبدال المحتوى الموجود</bold>.<br></br>يمكنك النسخ الاحتياطي لرسمك أولاً باستخدام أحد الخيارات أدناه."
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "تحميل من رابط",
|
||||||
|
"button": "استبدال محتواي",
|
||||||
|
"description": "سيتسبب تحميل رسمة خارجية <bold>باستبدال محتواك الموجود حالياً</bold>.<br></br>بإمكانك إجراء النسخ الاحتياطي لرسمتك الحالية باستخدام أحد الخيارات أدناه."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "",
|
"create": "",
|
||||||
"label": ""
|
"createEmbed": "",
|
||||||
|
"label": "",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "",
|
"darkMode": "",
|
||||||
"lightMode": "",
|
"lightMode": "",
|
||||||
"zenMode": "",
|
"zenMode": "",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "",
|
"exitZenMode": "",
|
||||||
"cancel": "",
|
"cancel": "",
|
||||||
"clear": "",
|
"clear": "",
|
||||||
"remove": "",
|
"remove": "",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"submit": "",
|
"submit": "",
|
||||||
"confirm": ""
|
"confirm": "",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "",
|
"clearReset": "",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": "",
|
"importLibraryError": "",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "",
|
"hand": "",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "",
|
"linearElement": "",
|
||||||
"freeDraw": "",
|
"freeDraw": "",
|
||||||
"text": "",
|
"text": "",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "",
|
"text_selected": "",
|
||||||
"text_editing": "",
|
"text_editing": "",
|
||||||
"linearElementMulti": "",
|
"linearElementMulti": "",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": "",
|
"eraserRevert": "",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "",
|
||||||
|
"disableSnapping": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "",
|
"fileSavedToFilename": "",
|
||||||
"canvas": "",
|
"canvas": "",
|
||||||
"selection": "",
|
"selection": "",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "",
|
||||||
|
"unableToEmbed": "",
|
||||||
|
"unrecognizedLinkFormat": ""
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "",
|
"transparent": "",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "",
|
"shades": "",
|
||||||
"hexCode": "",
|
"hexCode": "",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+145
-96
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Постави",
|
"paste": "Постави",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Постави като обикновен текст",
|
||||||
"pasteCharts": "Постави графики",
|
"pasteCharts": "Постави графики",
|
||||||
"selectAll": "Маркирай всичко",
|
"selectAll": "Маркирай всичко",
|
||||||
"multiSelect": "Добави елемент към селекция",
|
"multiSelect": "Добави елемент към селекция",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"veryLarge": "Много голям",
|
"veryLarge": "Много голям",
|
||||||
"solid": "Солиден",
|
"solid": "Солиден",
|
||||||
"hachure": "Хералдика",
|
"hachure": "Хералдика",
|
||||||
"zigzag": "",
|
"zigzag": "Зигзаг",
|
||||||
"crossHatch": "Двойно-пресечено",
|
"crossHatch": "Двойно-пресечено",
|
||||||
"thin": "Тънък",
|
"thin": "Тънък",
|
||||||
"bold": "Ясно очертан",
|
"bold": "Ясно очертан",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"cartoonist": "Карикатурист",
|
"cartoonist": "Карикатурист",
|
||||||
"fileTitle": "Име на файл",
|
"fileTitle": "Име на файл",
|
||||||
"colorPicker": "Избор на цвят",
|
"colorPicker": "Избор на цвят",
|
||||||
"canvasColors": "",
|
"canvasColors": "Използван на платно",
|
||||||
"canvasBackground": "Фон на платно",
|
"canvasBackground": "Фон на платно",
|
||||||
"drawingCanvas": "Платно за рисуване",
|
"drawingCanvas": "Платно за рисуване",
|
||||||
"layers": "Слоеве",
|
"layers": "Слоеве",
|
||||||
@@ -99,37 +99,41 @@
|
|||||||
"share": "Сподели",
|
"share": "Сподели",
|
||||||
"showStroke": "",
|
"showStroke": "",
|
||||||
"showBackground": "",
|
"showBackground": "",
|
||||||
"toggleTheme": "",
|
"toggleTheme": "Включи тема",
|
||||||
"personalLib": "",
|
"personalLib": "Лична Библиотека",
|
||||||
"excalidrawLib": "",
|
"excalidrawLib": "Excalidraw Библиотека",
|
||||||
"decreaseFontSize": "",
|
"decreaseFontSize": "Намали размера на шрифта",
|
||||||
"increaseFontSize": "",
|
"increaseFontSize": "Увеличи размера на шрифта",
|
||||||
"unbindText": "",
|
"unbindText": "",
|
||||||
"bindText": "",
|
"bindText": "",
|
||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "Редактирай линк",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "",
|
"create": "",
|
||||||
"label": ""
|
"createEmbed": "",
|
||||||
|
"label": "Линк",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
"exit": ""
|
"exit": ""
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "Заключи",
|
||||||
"unlock": "",
|
"unlock": "Отключи",
|
||||||
"lockAll": "",
|
"lockAll": "Заключи всички",
|
||||||
"unlockAll": ""
|
"unlockAll": "Отключи всички"
|
||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "Публикувани",
|
||||||
"sidebarLock": "",
|
"sidebarLock": "",
|
||||||
"selectAllElementsInFrame": "",
|
"selectAllElementsInFrame": "",
|
||||||
"removeAllElementsFromFrame": "",
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Избери цвят от платното"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "",
|
"noItems": "Няма добавени неща все още...",
|
||||||
"hint_emptyLibrary": "",
|
"hint_emptyLibrary": "",
|
||||||
"hint_emptyPrivateLibrary": ""
|
"hint_emptyPrivateLibrary": ""
|
||||||
},
|
},
|
||||||
@@ -137,11 +141,11 @@
|
|||||||
"clearReset": "Нулиране на платно",
|
"clearReset": "Нулиране на платно",
|
||||||
"exportJSON": "",
|
"exportJSON": "",
|
||||||
"exportImage": "",
|
"exportImage": "",
|
||||||
"export": "",
|
"export": "Запази на...",
|
||||||
"copyToClipboard": "Копиране в клипборда",
|
"copyToClipboard": "Копиране в клипборда",
|
||||||
"save": "",
|
"save": "Запази към текущ файл",
|
||||||
"saveAs": "Запиши като",
|
"saveAs": "Запиши като",
|
||||||
"load": "",
|
"load": "Отвори",
|
||||||
"getShareableLink": "Получаване на връзка за споделяне",
|
"getShareableLink": "Получаване на връзка за споделяне",
|
||||||
"close": "Затвори",
|
"close": "Затвори",
|
||||||
"selectLanguage": "Избор на език",
|
"selectLanguage": "Избор на език",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Тъмен режим",
|
"darkMode": "Тъмен режим",
|
||||||
"lightMode": "Светъл режим",
|
"lightMode": "Светъл режим",
|
||||||
"zenMode": "Режим Zen",
|
"zenMode": "Режим Zen",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "Спиране на Zen режим",
|
"exitZenMode": "Спиране на Zen режим",
|
||||||
"cancel": "Отмени",
|
"cancel": "Отмени",
|
||||||
"clear": "Изчисти",
|
"clear": "Изчисти",
|
||||||
"remove": "Премахване",
|
"remove": "Премахване",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Публикувай",
|
"publishLibrary": "Публикувай",
|
||||||
"submit": "Изпрати",
|
"submit": "Изпрати",
|
||||||
"confirm": "Потвърждаване"
|
"confirm": "Потвърждаване",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
|
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
|
||||||
@@ -175,37 +182,42 @@
|
|||||||
"couldNotLoadInvalidFile": "Невалиден файл не може да се зареди",
|
"couldNotLoadInvalidFile": "Невалиден файл не може да се зареди",
|
||||||
"importBackendFailed": "Импортирането от бекенд не беше успешно.",
|
"importBackendFailed": "Импортирането от бекенд не беше успешно.",
|
||||||
"cannotExportEmptyCanvas": "Не може да се експортира празно платно.",
|
"cannotExportEmptyCanvas": "Не може да се експортира празно платно.",
|
||||||
"couldNotCopyToClipboard": "",
|
"couldNotCopyToClipboard": "Не можем да копираме в клипбоарда.",
|
||||||
"decryptFailed": "Данните не можаха да се дешифрират.",
|
"decryptFailed": "Данните не можаха да се дешифрират.",
|
||||||
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
|
"uploadedSecurly": "Качването е защитено с криптиране от край до край, което означава, че сървърът Excalidraw и трети страни не могат да четат съдържанието.",
|
||||||
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
"loadSceneOverridePrompt": "Зареждането на външна рисунка ще презапише настоящото ви съдържание. Желаете ли да продължите?",
|
||||||
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
"collabStopOverridePrompt": "Прекратяването на сесията ще презапише предишната, локално запазена, рисунка. Сигурни ли сте?\n\n(Ако искате да продължите с локалната рисунка, просто затворете таба на браузъра.)",
|
||||||
"errorAddingToLibrary": "",
|
"errorAddingToLibrary": "Не можем да заредим от библиотеката",
|
||||||
"errorRemovingFromLibrary": "",
|
"errorRemovingFromLibrary": "Не можем да премахнем елемент от библиотеката",
|
||||||
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
"confirmAddLibrary": "Ще се добавят {{numShapes}} фигура(и) във вашата библиотека. Сигурни ли сте?",
|
||||||
"imageDoesNotContainScene": "",
|
"imageDoesNotContainScene": "",
|
||||||
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
|
||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "Изтрий {{count}} елемент(а) от библиотеката?",
|
||||||
"invalidEncryptionKey": "",
|
"invalidEncryptionKey": "",
|
||||||
"collabOfflineWarning": ""
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "Файлът е твърде голям. Максималния допустим размер е {{maxSize}}.",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
"invalidSVGString": "",
|
"failedToFetchImage": "",
|
||||||
|
"invalidSVGString": "Невалиден SVG.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": "",
|
"importLibraryError": "Не можем да заредим библиотеката",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "",
|
||||||
"collabSaveFailed_sizeExceeded": "",
|
"collabSaveFailed_sizeExceeded": "",
|
||||||
"brave_measure_text_error": {
|
"brave_measure_text_error": {
|
||||||
"line1": "",
|
"line1": "",
|
||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "Силно препоръчваме да изключите тази настройка. Можете да следвате <link>тези стъпки</link> за това как да го направите.",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -222,10 +234,12 @@
|
|||||||
"lock": "Поддържайте избрания инструмент активен след рисуване",
|
"lock": "Поддържайте избрания инструмент активен след рисуване",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "Гума",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "",
|
"hand": "",
|
||||||
"extraTools": ""
|
"extraTools": "Още инструменти"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Действия по платното",
|
"canvasActions": "Действия по платното",
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Кликнете, за да стартирате няколко точки, плъзнете за една линия",
|
"linearElement": "Кликнете, за да стартирате няколко точки, плъзнете за една линия",
|
||||||
"freeDraw": "Натиснете и влачете, пуснете като сте готови",
|
"freeDraw": "Натиснете и влачете, пуснете като сте готови",
|
||||||
"text": "Подсказка: Можете също да добавите текст като натиснете някъде два път с инструмента за селекция",
|
"text": "Подсказка: Можете също да добавите текст като натиснете някъде два път с инструмента за селекция",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "",
|
"text_selected": "",
|
||||||
"text_editing": "",
|
"text_editing": "",
|
||||||
"linearElementMulti": "Кликнете върху последната точка или натиснете Escape или Enter, за да завършите",
|
"linearElementMulti": "Кликнете върху последната точка или натиснете Escape или Enter, за да завършите",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "Натиснете Enter, за да добавите",
|
"bindTextToElement": "Натиснете Enter, за да добавите",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": "",
|
"eraserRevert": "",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "",
|
||||||
|
"disableSnapping": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Невъзможност за показване на preview",
|
"cannotShowPreview": "Невъзможност за показване на preview",
|
||||||
@@ -288,7 +304,7 @@
|
|||||||
"link_details": "",
|
"link_details": "",
|
||||||
"link_button": "",
|
"link_button": "",
|
||||||
"excalidrawplus_description": "",
|
"excalidrawplus_description": "",
|
||||||
"excalidrawplus_button": "",
|
"excalidrawplus_button": "Експорт",
|
||||||
"excalidrawplus_exportError": ""
|
"excalidrawplus_exportError": ""
|
||||||
},
|
},
|
||||||
"helpDialog": {
|
"helpDialog": {
|
||||||
@@ -299,7 +315,7 @@
|
|||||||
"curvedArrow": "Извита стрелка",
|
"curvedArrow": "Извита стрелка",
|
||||||
"curvedLine": "Извита линия",
|
"curvedLine": "Извита линия",
|
||||||
"documentation": "Документация",
|
"documentation": "Документация",
|
||||||
"doubleClick": "",
|
"doubleClick": "двойно-щракване",
|
||||||
"drag": "плъзнете",
|
"drag": "плъзнете",
|
||||||
"editor": "Редактор",
|
"editor": "Редактор",
|
||||||
"editLineArrowPoints": "",
|
"editLineArrowPoints": "",
|
||||||
@@ -308,41 +324,41 @@
|
|||||||
"howto": "Следвайте нашите ръководства",
|
"howto": "Следвайте нашите ръководства",
|
||||||
"or": "или",
|
"or": "или",
|
||||||
"preventBinding": "Спри прилепяне на стрелките",
|
"preventBinding": "Спри прилепяне на стрелките",
|
||||||
"tools": "",
|
"tools": "Инструменти",
|
||||||
"shortcuts": "Клавиши за бърз достъп",
|
"shortcuts": "Клавиши за бърз достъп",
|
||||||
"textFinish": "",
|
"textFinish": "Завърши редактиране (текстов редактор)",
|
||||||
"textNewLine": "",
|
"textNewLine": "Добави нова линия (текстов редактор)",
|
||||||
"title": "Помощ",
|
"title": "Помощ",
|
||||||
"view": "Преглед",
|
"view": "Преглед",
|
||||||
"zoomToFit": "Приближи докато се виждат всички елементи",
|
"zoomToFit": "Приближи докато се виждат всички елементи",
|
||||||
"zoomToSelection": "Приближи селекцията",
|
"zoomToSelection": "Приближи селекцията",
|
||||||
"toggleElementLock": "",
|
"toggleElementLock": "Заключи/Отключи селекция",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Премести страница нагоре/надолу",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Премести страница наляво/надясно"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": ""
|
"title": "Изчисти платното"
|
||||||
},
|
},
|
||||||
"publishDialog": {
|
"publishDialog": {
|
||||||
"title": "",
|
"title": "Публикувай библиотека",
|
||||||
"itemName": "",
|
"itemName": "Име",
|
||||||
"authorName": "",
|
"authorName": "Авторско име",
|
||||||
"githubUsername": "",
|
"githubUsername": "GitHub потребителско име",
|
||||||
"twitterUsername": "",
|
"twitterUsername": "Twitter потребителско име",
|
||||||
"libraryName": "",
|
"libraryName": "Име на библиотеката",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Описание на библиотеката",
|
||||||
"website": "",
|
"website": "Уебсайт",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"authorName": "",
|
"authorName": "Името или потребителското Ви име",
|
||||||
"libraryName": "",
|
"libraryName": "Име на библиотеката Ви",
|
||||||
"libraryDesc": "",
|
"libraryDesc": "Описание на библиотеката ви, за да помогнете на хората да разберат приложенията ѝ",
|
||||||
"githubHandle": "",
|
"githubHandle": "",
|
||||||
"twitterHandle": "",
|
"twitterHandle": "",
|
||||||
"website": ""
|
"website": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"required": "",
|
"required": "Задължително",
|
||||||
"website": ""
|
"website": "Въведете валиден URL адрес"
|
||||||
},
|
},
|
||||||
"noteDescription": "",
|
"noteDescription": "",
|
||||||
"noteGuidelines": "",
|
"noteGuidelines": "",
|
||||||
@@ -356,15 +372,15 @@
|
|||||||
"content": ""
|
"content": ""
|
||||||
},
|
},
|
||||||
"confirmDialog": {
|
"confirmDialog": {
|
||||||
"resetLibrary": "",
|
"resetLibrary": "Нулирай библиотека",
|
||||||
"removeItemsFromLib": ""
|
"removeItemsFromLib": ""
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "Фон",
|
||||||
"onlySelected": "",
|
"onlySelected": "Само избраното",
|
||||||
"darkMode": "",
|
"darkMode": "Тъмен режим",
|
||||||
"embedScene": "",
|
"embedScene": "",
|
||||||
"scale": "",
|
"scale": "",
|
||||||
"padding": ""
|
"padding": ""
|
||||||
@@ -373,14 +389,14 @@
|
|||||||
"embedScene": ""
|
"embedScene": ""
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "Изнасяне в PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "Изнасяне в SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Копирай PNG в клипборда"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Копиране в клипборда"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -403,51 +419,84 @@
|
|||||||
"width": "Широчина"
|
"width": "Широчина"
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
"addedToLibrary": "",
|
"addedToLibrary": "Добавена към библиотеката",
|
||||||
"copyStyles": "Копирани стилове.",
|
"copyStyles": "Копирани стилове.",
|
||||||
"copyToClipboard": "Копирано в клипборда.",
|
"copyToClipboard": "Копирано в клипборда.",
|
||||||
"copyToClipboardAsPng": "",
|
"copyToClipboardAsPng": "Копира {{exportSelection}} в клипборда като PNG\n({{exportColorScheme}})",
|
||||||
"fileSaved": "",
|
"fileSaved": "Файлът е запазен.",
|
||||||
"fileSavedToFilename": "",
|
"fileSavedToFilename": "Запазен към {filename}",
|
||||||
"canvas": "",
|
"canvas": "платно",
|
||||||
"selection": "",
|
"selection": "селекция",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "",
|
||||||
|
"unableToEmbed": "",
|
||||||
|
"unrecognizedLinkFormat": ""
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "",
|
"transparent": "Прозрачен",
|
||||||
"black": "",
|
"black": "Черен",
|
||||||
"white": "",
|
"white": "Бял",
|
||||||
"red": "",
|
"red": "Червен",
|
||||||
"pink": "",
|
"pink": "Розов",
|
||||||
"grape": "",
|
"grape": "Грозде",
|
||||||
"violet": "",
|
"violet": "Виолетово",
|
||||||
"gray": "",
|
"gray": "Сив",
|
||||||
"blue": "",
|
"blue": "Син",
|
||||||
"cyan": "",
|
"cyan": "Синьозелено",
|
||||||
"teal": "",
|
"teal": "Тъмно синьо-зелено",
|
||||||
"green": "",
|
"green": "Зелено",
|
||||||
"yellow": "",
|
"yellow": "Жълто",
|
||||||
"orange": "",
|
"orange": "Оранжево",
|
||||||
"bronze": ""
|
"bronze": "Бронзово"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Всичките Ви данни са запазени локално в браузъра Ви.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "",
|
||||||
"menuHint": ""
|
"menuHint": "Експорт, предпочитания, езици, ..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Експорт, предпочитания, и още...",
|
||||||
"center_heading": "",
|
"center_heading": "Диаграми. Направени. Просто.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Изберете инструмент & Започнете да рисувате!",
|
||||||
"helpHint": ""
|
"helpHint": "Преки пътища & помощ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "Най-често използвани цветове",
|
||||||
"colors": "",
|
"colors": "Цветове",
|
||||||
"shades": "",
|
"shades": "Нюанси",
|
||||||
"hexCode": "",
|
"hexCode": "Шестнадесетичен код",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "Изнеси като изображение",
|
||||||
|
"button": "Изнеси като изображение",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "Запази към диск",
|
||||||
|
"button": "Запази към диск",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "Excalidraw+",
|
||||||
|
"button": "Експортирай към Excalidraw+",
|
||||||
|
"description": "Запази сцената към Excalidraw+ работното място."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "Зареди от файл",
|
||||||
|
"button": "Зареди от файл",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "Зареди от линк",
|
||||||
|
"button": "Замени моето съдържание",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "লিঙ্ক সংশোধন",
|
"edit": "লিঙ্ক সংশোধন",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "লিঙ্ক তৈরী",
|
"create": "লিঙ্ক তৈরী",
|
||||||
"label": "লিঙ্ক নামকরণ"
|
"createEmbed": "",
|
||||||
|
"label": "লিঙ্ক নামকরণ",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "ডার্ক মোড",
|
"darkMode": "ডার্ক মোড",
|
||||||
"lightMode": "লাইট মোড",
|
"lightMode": "লাইট মোড",
|
||||||
"zenMode": "জেন মোড",
|
"zenMode": "জেন মোড",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "জেন মোড বন্ধ করুন",
|
"exitZenMode": "জেন মোড বন্ধ করুন",
|
||||||
"cancel": "বাতিল",
|
"cancel": "বাতিল",
|
||||||
"clear": "সাফ",
|
"clear": "সাফ",
|
||||||
"remove": "বিয়োগ",
|
"remove": "বিয়োগ",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "সংগ্রহ প্রকাশ করুন",
|
"publishLibrary": "সংগ্রহ প্রকাশ করুন",
|
||||||
"submit": "জমা করুন",
|
"submit": "জমা করুন",
|
||||||
"confirm": "নিশ্চিত করুন"
|
"confirm": "নিশ্চিত করুন",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "এটি পুরো ক্যানভাস সাফ করবে। আপনি কি নিশ্চিত?",
|
"clearReset": "এটি পুরো ক্যানভাস সাফ করবে। আপনি কি নিশ্চিত?",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "ছবি সন্নিবেশ করা যায়নি। পরে আবার চেষ্টা করুন...",
|
"imageInsertError": "ছবি সন্নিবেশ করা যায়নি। পরে আবার চেষ্টা করুন...",
|
||||||
"fileTooBig": "ফাইলটি খুব বড়। সর্বাধিক অনুমোদিত আকার হল {{maxSize}}৷",
|
"fileTooBig": "ফাইলটি খুব বড়। সর্বাধিক অনুমোদিত আকার হল {{maxSize}}৷",
|
||||||
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
"svgImageInsertError": "এসভীজী ছবি সন্নিবেশ করা যায়নি। এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
"invalidSVGString": "এসভীজী মার্কআপটি অবৈধ মনে হচ্ছে৷",
|
||||||
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
|
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
|
||||||
"importLibraryError": "সংগ্রহ লোড করা যায়নি",
|
"importLibraryError": "সংগ্রহ লোড করা যায়নি",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
||||||
"eraser": "ঝাড়ন",
|
"eraser": "ঝাড়ন",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "",
|
"hand": "",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
|
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
|
||||||
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
|
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
|
||||||
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
|
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "লেখা সম্পাদনা করতে ডাবল-ক্লিক করুন বা এন্টার টিপুন",
|
"text_selected": "লেখা সম্পাদনা করতে ডাবল-ক্লিক করুন বা এন্টার টিপুন",
|
||||||
"text_editing": "লেখা সম্পাদনা শেষ করতে এসকেপ বা কন্ট্রোল/কম্যান্ড যোগে এন্টার টিপুন",
|
"text_editing": "লেখা সম্পাদনা শেষ করতে এসকেপ বা কন্ট্রোল/কম্যান্ড যোগে এন্টার টিপুন",
|
||||||
"linearElementMulti": "শেষ বিন্দুতে ক্লিক করুন অথবা শেষ করতে এসকেপ বা এন্টার টিপুন",
|
"linearElementMulti": "শেষ বিন্দুতে ক্লিক করুন অথবা শেষ করতে এসকেপ বা এন্টার টিপুন",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
|
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
|
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "",
|
||||||
|
"disableSnapping": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
|
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "",
|
"fileSavedToFilename": "",
|
||||||
"canvas": "",
|
"canvas": "",
|
||||||
"selection": "বাছাই",
|
"selection": "বাছাই",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "",
|
||||||
|
"unableToEmbed": "",
|
||||||
|
"unrecognizedLinkFormat": ""
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "",
|
"transparent": "",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "",
|
"shades": "",
|
||||||
"hexCode": "",
|
"hexCode": "",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edita l'enllaç",
|
"edit": "Edita l'enllaç",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "Crea un enllaç",
|
"create": "Crea un enllaç",
|
||||||
"label": "Enllaç"
|
"createEmbed": "",
|
||||||
|
"label": "Enllaç",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Editar línia",
|
"edit": "Editar línia",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Mode fosc",
|
"darkMode": "Mode fosc",
|
||||||
"lightMode": "Mode clar",
|
"lightMode": "Mode clar",
|
||||||
"zenMode": "Mode zen",
|
"zenMode": "Mode zen",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "Surt de mode zen",
|
"exitZenMode": "Surt de mode zen",
|
||||||
"cancel": "Cancel·la",
|
"cancel": "Cancel·la",
|
||||||
"clear": "Neteja",
|
"clear": "Neteja",
|
||||||
"remove": "Suprimeix",
|
"remove": "Suprimeix",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Publica",
|
"publishLibrary": "Publica",
|
||||||
"submit": "Envia",
|
"submit": "Envia",
|
||||||
"confirm": "Confirma"
|
"confirm": "Confirma",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "S'esborrarà tot el llenç. N'esteu segur?",
|
"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...",
|
"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}}.",
|
"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.",
|
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "SVG no vàlid.",
|
"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.",
|
"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",
|
"importLibraryError": "No s'ha pogut carregar la biblioteca",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||||
"eraser": "Esborrador",
|
"eraser": "Esborrador",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "Mà (eina de desplaçament)",
|
"hand": "Mà (eina de desplaçament)",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
|
"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",
|
"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ó",
|
"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_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ó",
|
"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",
|
"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",
|
"bindTextToElement": "Premeu enter per a afegir-hi text",
|
||||||
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
|
"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",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "S'ha desat a {filename}",
|
"fileSavedToFilename": "S'ha desat a {filename}",
|
||||||
"canvas": "el llenç",
|
"canvas": "el llenç",
|
||||||
"selection": "la selecció",
|
"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": {
|
"colors": {
|
||||||
"transparent": "Transparent",
|
"transparent": "Transparent",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "",
|
"shades": "",
|
||||||
"hexCode": "",
|
"hexCode": "",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "Zabalit text do kontejneru",
|
"createContainerFromText": "Zabalit text do kontejneru",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Upravit odkaz",
|
"edit": "Upravit odkaz",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "Vytvořit odkaz",
|
"create": "Vytvořit odkaz",
|
||||||
"label": "Odkaz"
|
"createEmbed": "",
|
||||||
|
"label": "Odkaz",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Upravit čáru",
|
"edit": "Upravit čáru",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Tmavý režim",
|
"darkMode": "Tmavý režim",
|
||||||
"lightMode": "Světlý režim",
|
"lightMode": "Světlý režim",
|
||||||
"zenMode": "Zen mód",
|
"zenMode": "Zen mód",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "Opustit zen mód",
|
"exitZenMode": "Opustit zen mód",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"clear": "Vyčistit",
|
"clear": "Vyčistit",
|
||||||
"remove": "Odstranit",
|
"remove": "Odstranit",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Zveřejnit",
|
"publishLibrary": "Zveřejnit",
|
||||||
"submit": "Odeslat",
|
"submit": "Odeslat",
|
||||||
"confirm": "Potvrdit"
|
"confirm": "Potvrdit",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Toto vymaže celé plátno. Jste si jisti?",
|
"clearReset": "Toto vymaže celé plátno. Jste si jisti?",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "Nelze vložit obrázek. Zkuste to později...",
|
"imageInsertError": "Nelze vložit obrázek. Zkuste to později...",
|
||||||
"fileTooBig": "Soubor je příliš velký. Maximální povolená velikost je {{maxSize}}.",
|
"fileTooBig": "Soubor je příliš velký. Maximální povolená velikost je {{maxSize}}.",
|
||||||
"svgImageInsertError": "Nelze vložit SVG obrázek. Značení SVG je neplatné.",
|
"svgImageInsertError": "Nelze vložit SVG obrázek. Značení SVG je neplatné.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "Neplatný SVG.",
|
"invalidSVGString": "Neplatný SVG.",
|
||||||
"cannotResolveCollabServer": "Nelze se připojit ke sdílenému serveru. Prosím obnovte stránku a zkuste to znovu.",
|
"cannotResolveCollabServer": "Nelze se připojit ke sdílenému serveru. Prosím obnovte stránku a zkuste to znovu.",
|
||||||
"importLibraryError": "Nelze načíst knihovnu",
|
"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.",
|
"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.",
|
"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>"
|
"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": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
|
"link": "Přidat/aktualizovat odkaz pro vybraný tvar",
|
||||||
"eraser": "Guma",
|
"eraser": "Guma",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "Ruka (nástroj pro posouvání)",
|
"hand": "Ruka (nástroj pro posouvání)",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Kliknutím pro více bodů, táhnutím pro jednu čáru",
|
"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",
|
"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",
|
"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_selected": "Dvojklikem nebo stisknutím klávesy ENTER upravíte text",
|
||||||
"text_editing": "Stiskněte Escape nebo Ctrl/Cmd+ENTER pro dokončení úprav",
|
"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í",
|
"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",
|
"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í",
|
"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í",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Náhled nelze zobrazit",
|
"cannotShowPreview": "Náhled nelze zobrazit",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "Uloženo do {filename}",
|
"fileSavedToFilename": "Uloženo do {filename}",
|
||||||
"canvas": "plátno",
|
"canvas": "plátno",
|
||||||
"selection": "výběr",
|
"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": {
|
"colors": {
|
||||||
"transparent": "Průhledná",
|
"transparent": "Průhledná",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "Stíny",
|
"shades": "Stíny",
|
||||||
"hexCode": "Hex kód",
|
"hexCode": "Hex kód",
|
||||||
"noShades": "Pro tuto barvu nejsou k dispozici žádné odstíny"
|
"noShades": "Pro tuto barvu nejsou k dispozici žádné odstíny"
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-8
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Indsæt",
|
"paste": "Indsæt",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Indsæt som klartekst",
|
||||||
"pasteCharts": "Indsæt diagrammer",
|
"pasteCharts": "Indsæt diagrammer",
|
||||||
"selectAll": "Marker alle",
|
"selectAll": "Marker alle",
|
||||||
"multiSelect": "Tilføj element til markering",
|
"multiSelect": "Tilføj element til markering",
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"veryLarge": "Meget stor",
|
"veryLarge": "Meget stor",
|
||||||
"solid": "Solid",
|
"solid": "Solid",
|
||||||
"hachure": "Skravering",
|
"hachure": "Skravering",
|
||||||
"zigzag": "",
|
"zigzag": "Zigzag",
|
||||||
"crossHatch": "Krydsskravering",
|
"crossHatch": "Krydsskravering",
|
||||||
"thin": "Tynd",
|
"thin": "Tynd",
|
||||||
"bold": "Fed",
|
"bold": "Fed",
|
||||||
@@ -69,8 +69,8 @@
|
|||||||
"layers": "Lag",
|
"layers": "Lag",
|
||||||
"actions": "Handlinger",
|
"actions": "Handlinger",
|
||||||
"language": "Sprog",
|
"language": "Sprog",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Live samarbejde...",
|
||||||
"duplicateSelection": "",
|
"duplicateSelection": "Duplikér",
|
||||||
"untitled": "Unavngivet",
|
"untitled": "Unavngivet",
|
||||||
"name": "Navn",
|
"name": "Navn",
|
||||||
"yourName": "Dit navn",
|
"yourName": "Dit navn",
|
||||||
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "",
|
"create": "",
|
||||||
"label": ""
|
"createEmbed": "",
|
||||||
|
"label": "",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Mørk tilstand",
|
"darkMode": "Mørk tilstand",
|
||||||
"lightMode": "Lys baggrund",
|
"lightMode": "Lys baggrund",
|
||||||
"zenMode": "Zentilstand",
|
"zenMode": "Zentilstand",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "Stop zentilstand",
|
"exitZenMode": "Stop zentilstand",
|
||||||
"cancel": "Annuller",
|
"cancel": "Annuller",
|
||||||
"clear": "Ryd",
|
"clear": "Ryd",
|
||||||
"remove": "Fjern",
|
"remove": "Fjern",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Publicér",
|
"publishLibrary": "Publicér",
|
||||||
"submit": "Gem",
|
"submit": "Gem",
|
||||||
"confirm": "Bekræft"
|
"confirm": "Bekræft",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Dette vil rydde hele lærredet. Er du sikker?",
|
"clearReset": "Dette vil rydde hele lærredet. Er du sikker?",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "",
|
"imageInsertError": "",
|
||||||
"fileTooBig": "",
|
"fileTooBig": "",
|
||||||
"svgImageInsertError": "",
|
"svgImageInsertError": "",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "",
|
"invalidSVGString": "",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "",
|
||||||
"importLibraryError": "",
|
"importLibraryError": "",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "",
|
"eraser": "",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "",
|
"hand": "",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "",
|
"linearElement": "",
|
||||||
"freeDraw": "Klik og træk, slip når du er færdig",
|
"freeDraw": "Klik og træk, slip når du er færdig",
|
||||||
"text": "",
|
"text": "",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "",
|
"text_selected": "",
|
||||||
"text_editing": "",
|
"text_editing": "",
|
||||||
"linearElementMulti": "",
|
"linearElementMulti": "",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": "",
|
"eraserRevert": "",
|
||||||
"firefox_clipboard_write": ""
|
"firefox_clipboard_write": "",
|
||||||
|
"disableSnapping": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "Gemt som {filename}",
|
"fileSavedToFilename": "Gemt som {filename}",
|
||||||
"canvas": "canvas",
|
"canvas": "canvas",
|
||||||
"selection": "markering",
|
"selection": "markering",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "",
|
||||||
|
"unableToEmbed": "",
|
||||||
|
"unrecognizedLinkFormat": ""
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "",
|
"transparent": "",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "",
|
"shades": "",
|
||||||
"hexCode": "",
|
"hexCode": "",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "Text in Container einbetten",
|
"createContainerFromText": "Text in Container einbetten",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Link bearbeiten",
|
"edit": "Link bearbeiten",
|
||||||
|
"editEmbed": "Link bearbeiten & einbetten",
|
||||||
"create": "Link erstellen",
|
"create": "Link erstellen",
|
||||||
"label": "Link"
|
"createEmbed": "Link erstellen & einbetten",
|
||||||
|
"label": "Link",
|
||||||
|
"labelEmbed": "Verlinken & einbetten",
|
||||||
|
"empty": "Kein Link festgelegt"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Linie bearbeiten",
|
"edit": "Linie bearbeiten",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Dunkles Design",
|
"darkMode": "Dunkles Design",
|
||||||
"lightMode": "Helles Design",
|
"lightMode": "Helles Design",
|
||||||
"zenMode": "Zen-Modus",
|
"zenMode": "Zen-Modus",
|
||||||
|
"objectsSnapMode": "Einrasten an Objekten",
|
||||||
"exitZenMode": "Zen-Modus verlassen",
|
"exitZenMode": "Zen-Modus verlassen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"clear": "Löschen",
|
"clear": "Löschen",
|
||||||
"remove": "Entfernen",
|
"remove": "Entfernen",
|
||||||
|
"embed": "Einbettung umschalten",
|
||||||
"publishLibrary": "Veröffentlichen",
|
"publishLibrary": "Veröffentlichen",
|
||||||
"submit": "Absenden",
|
"submit": "Absenden",
|
||||||
"confirm": "Bestätigen"
|
"confirm": "Bestätigen",
|
||||||
|
"embeddableInteractionButton": "Klicken, um zu interagieren"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?",
|
"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...",
|
"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}}.",
|
"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.",
|
"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.",
|
"invalidSVGString": "Ungültige SVG.",
|
||||||
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.",
|
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.",
|
||||||
"importLibraryError": "Bibliothek konnte nicht geladen werden",
|
"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.",
|
"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.",
|
"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>"
|
"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": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
|
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
|
||||||
"eraser": "Radierer",
|
"eraser": "Radierer",
|
||||||
"frame": "Rahmenwerkzeug",
|
"frame": "Rahmenwerkzeug",
|
||||||
|
"embeddable": "Web-Einbettung",
|
||||||
|
"laser": "Laserpointer",
|
||||||
"hand": "Hand (Schwenkwerkzeug)",
|
"hand": "Hand (Schwenkwerkzeug)",
|
||||||
"extraTools": "Weitere Werkzeuge"
|
"extraTools": "Weitere Werkzeuge"
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
|
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
|
||||||
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
|
"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",
|
"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_selected": "Doppelklicken oder Eingabetaste drücken, um Text zu bearbeiten",
|
||||||
"text_editing": "Drücke Escape oder CtrlOrCmd+Eingabetaste, um die Bearbeitung abzuschließen",
|
"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",
|
"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",
|
"bindTextToElement": "Zum Hinzufügen Eingabetaste drücken",
|
||||||
"deepBoxSelect": "Halte CtrlOrCmd gedrückt, um innerhalb der Gruppe auszuwählen, und um Ziehen zu vermeiden",
|
"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",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
|
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "Als {filename} gespeichert",
|
"fileSavedToFilename": "Als {filename} gespeichert",
|
||||||
"canvas": "Zeichenfläche",
|
"canvas": "Zeichenfläche",
|
||||||
"selection": "Auswahl",
|
"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": {
|
"colors": {
|
||||||
"transparent": "Transparent",
|
"transparent": "Transparent",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "Schattierungen",
|
"shades": "Schattierungen",
|
||||||
"hexCode": "Hex-Code",
|
"hexCode": "Hex-Code",
|
||||||
"noShades": "Keine Schattierungen für diese Farbe verfügbar"
|
"noShades": "Keine Schattierungen für diese Farbe verfügbar"
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "Als Bild exportieren",
|
||||||
|
"button": "Als Bild exportieren",
|
||||||
|
"description": "Exportiere die Zeichnungsdaten als ein Bild, von dem Du später importieren kannst."
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "Auf Festplatte speichern",
|
||||||
|
"button": "Auf Festplatte speichern",
|
||||||
|
"description": "Exportiere die Zeichnungsdaten in eine Datei, von der Du später importieren kannst."
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "Excalidraw+",
|
||||||
|
"button": "Export nach Excalidraw+",
|
||||||
|
"description": "Speichere die Szene in deinem Excalidraw+-Arbeitsbereich."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "Aus Datei laden",
|
||||||
|
"button": "Aus Datei laden",
|
||||||
|
"description": "Das Laden aus einer Datei wird <bold>Deinen vorhandenen Inhalt ersetzen</bold>.<br></br>Du kannst Deine Zeichnung zuerst mit einer der folgenden Optionen sichern."
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "Aus Link laden",
|
||||||
|
"button": "Meinen Inhalt ersetzen",
|
||||||
|
"description": "Das Laden einer externen Zeichnung wird <bold>Deinen vorhandenen Inhalt ersetzen</bold>.<br></br>Du kannst Deine Zeichnung zuerst mit einer der folgenden Optionen sichern."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Επεξεργασία συνδέσμου",
|
"edit": "Επεξεργασία συνδέσμου",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "Δημιουργία συνδέσμου",
|
"create": "Δημιουργία συνδέσμου",
|
||||||
"label": "Σύνδεσμος"
|
"createEmbed": "",
|
||||||
|
"label": "Σύνδεσμος",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Επεξεργασία γραμμής",
|
"edit": "Επεξεργασία γραμμής",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Σκοτεινή λειτουργία",
|
"darkMode": "Σκοτεινή λειτουργία",
|
||||||
"lightMode": "Φωτεινή λειτουργία",
|
"lightMode": "Φωτεινή λειτουργία",
|
||||||
"zenMode": "Λειτουργία Zεν",
|
"zenMode": "Λειτουργία Zεν",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "Έξοδος από την λειτουργία Zen",
|
"exitZenMode": "Έξοδος από την λειτουργία Zen",
|
||||||
"cancel": "Ακύρωση",
|
"cancel": "Ακύρωση",
|
||||||
"clear": "Καθαρισμός",
|
"clear": "Καθαρισμός",
|
||||||
"remove": "Κατάργηση",
|
"remove": "Κατάργηση",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Δημοσίευση",
|
"publishLibrary": "Δημοσίευση",
|
||||||
"submit": "Υποβολή",
|
"submit": "Υποβολή",
|
||||||
"confirm": "Επιβεβαίωση"
|
"confirm": "Επιβεβαίωση",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
|
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
|
||||||
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
|
||||||
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
"svgImageInsertError": "Αδυναμία εισαγωγής εικόνας SVG. Η σήμανση της SVG δεν φαίνεται έγκυρη.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "Μη έγκυρο SVG.",
|
"invalidSVGString": "Μη έγκυρο SVG.",
|
||||||
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
|
||||||
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
|
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||||
"eraser": "Γόμα",
|
"eraser": "Γόμα",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "",
|
"hand": "",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
||||||
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
||||||
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "Κάντε διπλό κλικ ή πατήστε ENTER για να επεξεργαστείτε το κείμενο",
|
"text_selected": "Κάντε διπλό κλικ ή πατήστε ENTER για να επεξεργαστείτε το κείμενο",
|
||||||
"text_editing": "Πατήστε Escape ή CtrlOrCmd+ENTER για να ολοκληρώσετε την επεξεργασία",
|
"text_editing": "Πατήστε Escape ή CtrlOrCmd+ENTER για να ολοκληρώσετε την επεξεργασία",
|
||||||
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
"linearElementMulti": "Κάνε κλικ στο τελευταίο σημείο ή πάτησε Escape ή Enter για να τελειώσεις",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
||||||
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
||||||
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
|
"fileSavedToFilename": "Αποθηκεύτηκε στο {filename}",
|
||||||
"canvas": "καμβάς",
|
"canvas": "καμβάς",
|
||||||
"selection": "επιλογή",
|
"selection": "επιλογή",
|
||||||
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου"
|
"pasteAsSingleElement": "Χρησιμοποίησε το {{shortcut}} για να επικολλήσεις ως ένα μόνο στοιχείο,\nή να επικολλήσεις σε έναν υπάρχοντα επεξεργαστή κειμένου",
|
||||||
|
"unableToEmbed": "",
|
||||||
|
"unrecognizedLinkFormat": ""
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "Διαφανές",
|
"transparent": "Διαφανές",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "Αποχρώσεις",
|
"shades": "Αποχρώσεις",
|
||||||
"hexCode": "Κωδικός Hex",
|
"hexCode": "Κωδικός Hex",
|
||||||
"noShades": "Δεν υπάρχουν διαθέσιμες αποχρώσεις για αυτό το χρώμα"
|
"noShades": "Δεν υπάρχουν διαθέσιμες αποχρώσεις για αυτό το χρώμα"
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -203,6 +203,7 @@
|
|||||||
"imageInsertError": "Couldn't insert image. Try again later...",
|
"imageInsertError": "Couldn't insert image. Try again later...",
|
||||||
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
|
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
|
||||||
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
|
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
|
||||||
|
"failedToFetchImage": "Failed to fetch image.",
|
||||||
"invalidSVGString": "Invalid SVG.",
|
"invalidSVGString": "Invalid SVG.",
|
||||||
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
|
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
|
||||||
"importLibraryError": "Couldn't load library",
|
"importLibraryError": "Couldn't load library",
|
||||||
@@ -217,7 +218,10 @@
|
|||||||
"libraryElementTypeError": {
|
"libraryElementTypeError": {
|
||||||
"embeddable": "Embeddable elements cannot be added to the library.",
|
"embeddable": "Embeddable elements cannot be added to the library.",
|
||||||
"image": "Support for adding images to the library coming soon!"
|
"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": {
|
"toolBar": {
|
||||||
"selection": "Selection",
|
"selection": "Selection",
|
||||||
@@ -238,7 +242,8 @@
|
|||||||
"embeddable": "Web Embed",
|
"embeddable": "Web Embed",
|
||||||
"laser": "Laser pointer",
|
"laser": "Laser pointer",
|
||||||
"hand": "Hand (panning tool)",
|
"hand": "Hand (panning tool)",
|
||||||
"extraTools": "More tools"
|
"extraTools": "More tools",
|
||||||
|
"mermaidToExcalidraw": "Mermaid to Excalidraw"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas actions",
|
"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."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-41
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "Envolver el texto en un contenedor",
|
"createContainerFromText": "Envolver el texto en un contenedor",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Editar enlace",
|
"edit": "Editar enlace",
|
||||||
|
"editEmbed": "Editar enlace e incrustar",
|
||||||
"create": "Crear enlace",
|
"create": "Crear enlace",
|
||||||
"label": "Enlace"
|
"createEmbed": "Crear enlace e incrustar",
|
||||||
|
"label": "Enlace",
|
||||||
|
"labelEmbed": "Enlazar e incrustar",
|
||||||
|
"empty": "No se ha establecido un enlace"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Editar línea",
|
"edit": "Editar línea",
|
||||||
@@ -124,9 +128,9 @@
|
|||||||
},
|
},
|
||||||
"statusPublished": "Publicado",
|
"statusPublished": "Publicado",
|
||||||
"sidebarLock": "Mantener barra lateral abierta",
|
"sidebarLock": "Mantener barra lateral abierta",
|
||||||
"selectAllElementsInFrame": "",
|
"selectAllElementsInFrame": "Seleccionar todos los elementos en el marco",
|
||||||
"removeAllElementsFromFrame": "",
|
"removeAllElementsFromFrame": "Eliminar todos los elementos del marco",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "Seleccionar un color del lienzo"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "No hay elementos añadidos todavía...",
|
"noItems": "No hay elementos añadidos todavía...",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Modo oscuro",
|
"darkMode": "Modo oscuro",
|
||||||
"lightMode": "Modo claro",
|
"lightMode": "Modo claro",
|
||||||
"zenMode": "Modo Zen",
|
"zenMode": "Modo Zen",
|
||||||
|
"objectsSnapMode": "Ajustar a los objetos",
|
||||||
"exitZenMode": "Salir del modo Zen",
|
"exitZenMode": "Salir del modo Zen",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"clear": "Borrar",
|
"clear": "Borrar",
|
||||||
"remove": "Eliminar",
|
"remove": "Eliminar",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Publicar",
|
"publishLibrary": "Publicar",
|
||||||
"submit": "Enviar",
|
"submit": "Enviar",
|
||||||
"confirm": "Confirmar"
|
"confirm": "Confirmar",
|
||||||
|
"embeddableInteractionButton": "Pulsa para interactuar"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
|
"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...",
|
"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}}.",
|
"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.",
|
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "SVG no válido.",
|
"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.",
|
"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",
|
"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": "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.",
|
"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": {
|
"brave_measure_text_error": {
|
||||||
"line1": "",
|
"line1": "Parece que estás usando el navegador Brave con el ajuste <bold>Forzar el bloqueo de huellas digitales</bold> habilitado.",
|
||||||
"line2": "",
|
"line2": "Esto podría resultar en errores en los <bold>Elementos de Texto</bold> en tus dibujos.",
|
||||||
"line3": "",
|
"line3": "Recomendamos fuertemente deshabilitar esta configuración. Puedes seguir <link>estos pasos</link> sobre cómo hacerlo.",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,8 +236,10 @@
|
|||||||
"link": "Añadir/Actualizar enlace para una forma seleccionada",
|
"link": "Añadir/Actualizar enlace para una forma seleccionada",
|
||||||
"eraser": "Borrar",
|
"eraser": "Borrar",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "Incrustar Web",
|
||||||
|
"laser": "Puntero láser",
|
||||||
"hand": "Mano (herramienta de panoramización)",
|
"hand": "Mano (herramienta de panoramización)",
|
||||||
"extraTools": ""
|
"extraTools": "Más herramientas"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acciones del lienzo",
|
"canvasActions": "Acciones del lienzo",
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
|
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
|
||||||
"freeDraw": "Haz clic y arrastra, suelta al terminar",
|
"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",
|
"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_selected": "Doble clic o pulse ENTER para editar el texto",
|
||||||
"text_editing": "Pulse Escape o Ctrl/Cmd + ENTER para terminar de editar",
|
"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",
|
"linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "Presione Entrar para agregar",
|
"bindTextToElement": "Presione Entrar para agregar",
|
||||||
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar",
|
"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",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No se puede mostrar la vista previa",
|
"cannotShowPreview": "No se puede mostrar la vista previa",
|
||||||
@@ -360,27 +376,27 @@
|
|||||||
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
|
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
|
||||||
},
|
},
|
||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "Exportar imagen",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "Fondo",
|
||||||
"onlySelected": "",
|
"onlySelected": "Sólo seleccionados",
|
||||||
"darkMode": "",
|
"darkMode": "Modo oscuro",
|
||||||
"embedScene": "",
|
"embedScene": "Incrustar escena",
|
||||||
"scale": "",
|
"scale": "Escalar",
|
||||||
"padding": ""
|
"padding": "Espaciado"
|
||||||
},
|
},
|
||||||
"tooltip": {
|
"tooltip": {
|
||||||
"embedScene": ""
|
"embedScene": ""
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"exportToPng": "",
|
"exportToPng": "Exportar a PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "Exportar a SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Copiar PNG al portapapeles"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"exportToPng": "",
|
"exportToPng": "PNG",
|
||||||
"exportToSvg": "",
|
"exportToSvg": "SVG",
|
||||||
"copyPngToClipboard": ""
|
"copyPngToClipboard": "Copiar al portapapeles"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"encrypted": {
|
"encrypted": {
|
||||||
@@ -411,24 +427,26 @@
|
|||||||
"fileSavedToFilename": "Guardado en {filename}",
|
"fileSavedToFilename": "Guardado en {filename}",
|
||||||
"canvas": "lienzo",
|
"canvas": "lienzo",
|
||||||
"selection": "selección",
|
"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": {
|
"colors": {
|
||||||
"transparent": "Transparente",
|
"transparent": "Transparente",
|
||||||
"black": "",
|
"black": "Negro",
|
||||||
"white": "",
|
"white": "Blanco",
|
||||||
"red": "",
|
"red": "Rojo",
|
||||||
"pink": "",
|
"pink": "Rosa",
|
||||||
"grape": "",
|
"grape": "Uva",
|
||||||
"violet": "",
|
"violet": "Violeta",
|
||||||
"gray": "",
|
"gray": "Gris",
|
||||||
"blue": "",
|
"blue": "Azul",
|
||||||
"cyan": "",
|
"cyan": "Cian",
|
||||||
"teal": "",
|
"teal": "Turquesa",
|
||||||
"green": "",
|
"green": "Verde",
|
||||||
"yellow": "",
|
"yellow": "Amarillo",
|
||||||
"orange": "",
|
"orange": "Naranja",
|
||||||
"bronze": ""
|
"bronze": "Bronce"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -444,10 +462,41 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "Colores personalizados más utilizados",
|
||||||
"colors": "",
|
"colors": "Colores",
|
||||||
"shades": "",
|
"shades": "",
|
||||||
"hexCode": "",
|
"hexCode": "Código Hexadecimal",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "Exportar como imagen",
|
||||||
|
"button": "Exportar como imagen",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "Guardar en el disco",
|
||||||
|
"button": "Guardar en el disco",
|
||||||
|
"description": "Exporta los datos de la escena a un archivo desde el cual podrás importar más tarde."
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "Exportar a Excalidraw+",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "Cargar desde un archivo",
|
||||||
|
"button": "Cargar desde un archivo",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "Cargar desde un enlace",
|
||||||
|
"button": "Reemplazar mi contenido",
|
||||||
|
"description": "Cargar un dibujo externo <bold>reemplazará tu contenido existente</bold>.<br></br>Puedes primero hacer una copia de seguridad de tu dibujo usando una de las opciones de abajo."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "Bilatu testua edukiontzi batean",
|
"createContainerFromText": "Bilatu testua edukiontzi batean",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Editatu esteka",
|
"edit": "Editatu esteka",
|
||||||
|
"editEmbed": "Editatu esteka eta kapsulatu",
|
||||||
"create": "Sortu esteka",
|
"create": "Sortu esteka",
|
||||||
"label": "Esteka"
|
"createEmbed": "Sortu esteka eta kapsulatu",
|
||||||
|
"label": "Esteka",
|
||||||
|
"labelEmbed": "Esteka eta kapsula",
|
||||||
|
"empty": "Ez da estekarik ezarri"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Editatu lerroa",
|
"edit": "Editatu lerroa",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Modu iluna",
|
"darkMode": "Modu iluna",
|
||||||
"lightMode": "Modu argia",
|
"lightMode": "Modu argia",
|
||||||
"zenMode": "Zen modua",
|
"zenMode": "Zen modua",
|
||||||
|
"objectsSnapMode": "Atxiki objektuei",
|
||||||
"exitZenMode": "Irten Zen modutik",
|
"exitZenMode": "Irten Zen modutik",
|
||||||
"cancel": "Utzi",
|
"cancel": "Utzi",
|
||||||
"clear": "Garbitu",
|
"clear": "Garbitu",
|
||||||
"remove": "Kendu",
|
"remove": "Kendu",
|
||||||
|
"embed": "Aldatu kapsulatzea",
|
||||||
"publishLibrary": "Argitaratu",
|
"publishLibrary": "Argitaratu",
|
||||||
"submit": "Bidali",
|
"submit": "Bidali",
|
||||||
"confirm": "Bai"
|
"confirm": "Bai",
|
||||||
|
"embeddableInteractionButton": "Egin klik elkar eragiteko"
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
|
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...",
|
"imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...",
|
||||||
"fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.",
|
"fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.",
|
||||||
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "SVG baliogabea.",
|
"invalidSVGString": "SVG baliogabea.",
|
||||||
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
|
||||||
"importLibraryError": "Ezin izan da liburutegia kargatu",
|
"importLibraryError": "Ezin izan da liburutegia kargatu",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "Honek zure marrazkietako <bold>Testu-elementuak</bold> hautsi ditzake.",
|
"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.",
|
"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"
|
"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": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
||||||
"eraser": "Borragoma",
|
"eraser": "Borragoma",
|
||||||
"frame": "Marko tresna",
|
"frame": "Marko tresna",
|
||||||
|
"embeddable": "Web kapsulatzea",
|
||||||
|
"laser": "Laser punteroa",
|
||||||
"hand": "Eskua (panoratze tresna)",
|
"hand": "Eskua (panoratze tresna)",
|
||||||
"extraTools": "Tresna gehiago"
|
"extraTools": "Tresna gehiago"
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
|
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
|
||||||
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
|
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
|
||||||
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
|
"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_selected": "Egin klik bikoitza edo sakatu SARTU testua editatzeko",
|
||||||
"text_editing": "Sakatu Esc edo Ctrl+SARTU editatzen amaitzeko",
|
"text_editing": "Sakatu Esc edo Ctrl+SARTU editatzen amaitzeko",
|
||||||
"linearElementMulti": "Egin klik azken puntuan edo sakatu Esc edo Sartu amaitzeko",
|
"linearElementMulti": "Egin klik azken puntuan edo sakatu Esc edo Sartu amaitzeko",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
|
||||||
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
|
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
|
||||||
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "{filename}-n gorde da",
|
"fileSavedToFilename": "{filename}-n gorde da",
|
||||||
"canvas": "oihala",
|
"canvas": "oihala",
|
||||||
"selection": "hautapena",
|
"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": {
|
"colors": {
|
||||||
"transparent": "Gardena",
|
"transparent": "Gardena",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "Ñabardurak",
|
"shades": "Ñabardurak",
|
||||||
"hexCode": "Hez kodea",
|
"hexCode": "Hez kodea",
|
||||||
"noShades": "Kolore honetarako ez dago ñabardurarik eskuragarri"
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-24
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "متن را در یک جایگاه بپیچید",
|
"createContainerFromText": "متن را در یک جایگاه بپیچید",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "ویرایش لینک",
|
"edit": "ویرایش لینک",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "ایجاد پیوند",
|
"create": "ایجاد پیوند",
|
||||||
"label": "لینک"
|
"createEmbed": "",
|
||||||
|
"label": "لینک",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "ویرایش لینک",
|
"edit": "ویرایش لینک",
|
||||||
@@ -126,7 +130,7 @@
|
|||||||
"sidebarLock": "باز نگه داشتن سایدبار",
|
"sidebarLock": "باز نگه داشتن سایدبار",
|
||||||
"selectAllElementsInFrame": "",
|
"selectAllElementsInFrame": "",
|
||||||
"removeAllElementsFromFrame": "",
|
"removeAllElementsFromFrame": "",
|
||||||
"eyeDropper": ""
|
"eyeDropper": "انتخاب رنگ از کرباس"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "آیتمی به اینجا اضافه نشده...",
|
"noItems": "آیتمی به اینجا اضافه نشده...",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "حالت تیره",
|
"darkMode": "حالت تیره",
|
||||||
"lightMode": "حالت روشن",
|
"lightMode": "حالت روشن",
|
||||||
"zenMode": "حالت ذن",
|
"zenMode": "حالت ذن",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "خروج از حالت تمرکز",
|
"exitZenMode": "خروج از حالت تمرکز",
|
||||||
"cancel": "لغو",
|
"cancel": "لغو",
|
||||||
"clear": "پاک کردن",
|
"clear": "پاک کردن",
|
||||||
"remove": "پاک کردن",
|
"remove": "پاک کردن",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "انتشار",
|
"publishLibrary": "انتشار",
|
||||||
"submit": "ارسال",
|
"submit": "ارسال",
|
||||||
"confirm": "تایید"
|
"confirm": "تایید",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
|
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
|
"imageInsertError": "عکس ارسال نشد. بعداً دوباره تلاش کنید...",
|
||||||
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
|
"fileTooBig": "فایل خیلی بزرگ است حداکثر اندازه مجاز {{maxSize}}.",
|
||||||
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
"svgImageInsertError": "تصویر SVG وارد نشد. نشانه گذاری SVG نامعتبر به نظر می رسد.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "SVG نادرست.",
|
"invalidSVGString": "SVG نادرست.",
|
||||||
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
|
||||||
"importLibraryError": "دادهها بارگذاری نشدند",
|
"importLibraryError": "دادهها بارگذاری نشدند",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "این می تواند منجر به شکستن <bold>عناصر متن</bold> در نقاشی های شما شود.",
|
"line2": "این می تواند منجر به شکستن <bold>عناصر متن</bold> در نقاشی های شما شود.",
|
||||||
"line3": "اکیداً توصیه می کنیم این تنظیم را غیرفعال کنید. برای نحوه انجام این کار میتوانید <link>این مراحل</link> را دنبال کنید.",
|
"line3": "اکیداً توصیه می کنیم این تنظیم را غیرفعال کنید. برای نحوه انجام این کار میتوانید <link>این مراحل</link> را دنبال کنید.",
|
||||||
"line4": "اگر غیرفعال کردن این تنظیم نمایش عناصر متنی را برطرف نکرد، لطفاً یک <issueLink>مشکل</issueLink> را در GitHub ما باز کنید یا برای ما در <discordLink>Discord</discordLink> بنویسید."
|
"line4": "اگر غیرفعال کردن این تنظیم نمایش عناصر متنی را برطرف نکرد، لطفاً یک <issueLink>مشکل</issueLink> را در GitHub ما باز کنید یا برای ما در <discordLink>Discord</discordLink> بنویسید."
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,8 +236,10 @@
|
|||||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||||
"eraser": "پاک کن",
|
"eraser": "پاک کن",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "دست (ابزار پانینگ)",
|
"hand": "دست (ابزار پانینگ)",
|
||||||
"extraTools": ""
|
"extraTools": "ابزارهای بیشتر"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "عملیات روی بوم",
|
"canvasActions": "عملیات روی بوم",
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
||||||
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
||||||
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
"text_selected": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
|
||||||
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
|
"text_editing": "Escape یا CtrlOrCmd+ENTER را فشار دهید تا ویرایش تمام شود",
|
||||||
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
"linearElementMulti": "روی آخرین نقطه کلیک کنید یا کلید ESC را بزنید یا کلید Enter را بزنید برای اتمام کار",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
||||||
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
||||||
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
|
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
|
||||||
"firefox_clipboard_write": "احتمالاً میتوان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید."
|
"firefox_clipboard_write": "احتمالاً میتوان این ویژگی را با تنظیم پرچم «dom.events.asyncClipboard.clipboardItem» روی «true» فعال کرد. برای تغییر پرچم های مرورگر در فایرفاکس، از صفحه \"about:config\" دیدن کنید.",
|
||||||
|
"disableSnapping": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
||||||
@@ -362,7 +378,7 @@
|
|||||||
"imageExportDialog": {
|
"imageExportDialog": {
|
||||||
"header": "",
|
"header": "",
|
||||||
"label": {
|
"label": {
|
||||||
"withBackground": "",
|
"withBackground": "پس زمینه",
|
||||||
"onlySelected": "",
|
"onlySelected": "",
|
||||||
"darkMode": "",
|
"darkMode": "",
|
||||||
"embedScene": "",
|
"embedScene": "",
|
||||||
@@ -411,24 +427,26 @@
|
|||||||
"fileSavedToFilename": "ذخیره در {filename}",
|
"fileSavedToFilename": "ذخیره در {filename}",
|
||||||
"canvas": "بوم",
|
"canvas": "بوم",
|
||||||
"selection": "انتخاب",
|
"selection": "انتخاب",
|
||||||
"pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید"
|
"pasteAsSingleElement": "از {{shortcut}} برای چسباندن به عنوان یک عنصر استفاده کنید،\nیا در یک ویرایشگر متن موجود جایگذاری کنید",
|
||||||
|
"unableToEmbed": "",
|
||||||
|
"unrecognizedLinkFormat": ""
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"transparent": "شفاف",
|
"transparent": "شفاف",
|
||||||
"black": "",
|
"black": "سیاه",
|
||||||
"white": "",
|
"white": "سفید",
|
||||||
"red": "",
|
"red": "قرمز",
|
||||||
"pink": "",
|
"pink": "صورتی",
|
||||||
"grape": "",
|
"grape": "یاقوتی",
|
||||||
"violet": "",
|
"violet": "بنفش",
|
||||||
"gray": "",
|
"gray": "خاکستری",
|
||||||
"blue": "",
|
"blue": "آبی",
|
||||||
"cyan": "",
|
"cyan": "فیروزهای",
|
||||||
"teal": "",
|
"teal": "سبزآبی",
|
||||||
"green": "",
|
"green": "سبز",
|
||||||
"yellow": "",
|
"yellow": "زرد",
|
||||||
"orange": "",
|
"orange": "نارنجی",
|
||||||
"bronze": ""
|
"bronze": "برنزی"
|
||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -445,9 +463,40 @@
|
|||||||
},
|
},
|
||||||
"colorPicker": {
|
"colorPicker": {
|
||||||
"mostUsedCustomColors": "",
|
"mostUsedCustomColors": "",
|
||||||
"colors": "",
|
"colors": "رنگها",
|
||||||
"shades": "",
|
"shades": "جلوهها",
|
||||||
"hexCode": "",
|
"hexCode": "کدِ هگز",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "ذخیره در دیسک",
|
||||||
|
"button": "ذخیره در دیسک",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "بارگذاری از فایل",
|
||||||
|
"button": "بارگذاری از فایل",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-4
@@ -109,8 +109,12 @@
|
|||||||
"createContainerFromText": "",
|
"createContainerFromText": "",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Muokkaa linkkiä",
|
"edit": "Muokkaa linkkiä",
|
||||||
|
"editEmbed": "",
|
||||||
"create": "Luo linkki",
|
"create": "Luo linkki",
|
||||||
"label": "Linkki"
|
"createEmbed": "",
|
||||||
|
"label": "Linkki",
|
||||||
|
"labelEmbed": "",
|
||||||
|
"empty": ""
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "Muokkaa riviä",
|
"edit": "Muokkaa riviä",
|
||||||
@@ -160,13 +164,16 @@
|
|||||||
"darkMode": "Tumma tila",
|
"darkMode": "Tumma tila",
|
||||||
"lightMode": "Vaalea tila",
|
"lightMode": "Vaalea tila",
|
||||||
"zenMode": "Zen-tila",
|
"zenMode": "Zen-tila",
|
||||||
|
"objectsSnapMode": "",
|
||||||
"exitZenMode": "Poistu zen-tilasta",
|
"exitZenMode": "Poistu zen-tilasta",
|
||||||
"cancel": "Peruuta",
|
"cancel": "Peruuta",
|
||||||
"clear": "Pyyhi",
|
"clear": "Pyyhi",
|
||||||
"remove": "Poista",
|
"remove": "Poista",
|
||||||
|
"embed": "",
|
||||||
"publishLibrary": "Julkaise",
|
"publishLibrary": "Julkaise",
|
||||||
"submit": "Lähetä",
|
"submit": "Lähetä",
|
||||||
"confirm": "Vahvista"
|
"confirm": "Vahvista",
|
||||||
|
"embeddableInteractionButton": ""
|
||||||
},
|
},
|
||||||
"alerts": {
|
"alerts": {
|
||||||
"clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?",
|
"clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?",
|
||||||
@@ -196,6 +203,7 @@
|
|||||||
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
|
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
|
||||||
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
|
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
|
||||||
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
|
||||||
|
"failedToFetchImage": "",
|
||||||
"invalidSVGString": "Virheellinen SVG.",
|
"invalidSVGString": "Virheellinen SVG.",
|
||||||
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
|
||||||
"importLibraryError": "Kokoelman lataaminen epäonnistui",
|
"importLibraryError": "Kokoelman lataaminen epäonnistui",
|
||||||
@@ -206,6 +214,10 @@
|
|||||||
"line2": "",
|
"line2": "",
|
||||||
"line3": "",
|
"line3": "",
|
||||||
"line4": ""
|
"line4": ""
|
||||||
|
},
|
||||||
|
"libraryElementTypeError": {
|
||||||
|
"embeddable": "",
|
||||||
|
"image": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
@@ -224,6 +236,8 @@
|
|||||||
"link": "Lisää/päivitä linkki valitulle muodolle",
|
"link": "Lisää/päivitä linkki valitulle muodolle",
|
||||||
"eraser": "Poistotyökalu",
|
"eraser": "Poistotyökalu",
|
||||||
"frame": "",
|
"frame": "",
|
||||||
|
"embeddable": "",
|
||||||
|
"laser": "",
|
||||||
"hand": "Käsi (panning-työkalu)",
|
"hand": "Käsi (panning-työkalu)",
|
||||||
"extraTools": ""
|
"extraTools": ""
|
||||||
},
|
},
|
||||||
@@ -237,6 +251,7 @@
|
|||||||
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
|
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
|
||||||
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
|
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
|
||||||
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
|
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
|
||||||
|
"embeddable": "",
|
||||||
"text_selected": "Kaksoisnapsauta tai paina ENTER muokataksesi tekstiä",
|
"text_selected": "Kaksoisnapsauta tai paina ENTER muokataksesi tekstiä",
|
||||||
"text_editing": "Paina Escape tai CtrlOrCmd+ENTER lopettaaksesi muokkaamisen",
|
"text_editing": "Paina Escape tai CtrlOrCmd+ENTER lopettaaksesi muokkaamisen",
|
||||||
"linearElementMulti": "Lopeta klikkaamalla viimeistä pistettä, painamalla Escape- tai Enter-näppäintä",
|
"linearElementMulti": "Lopeta klikkaamalla viimeistä pistettä, painamalla Escape- tai Enter-näppäintä",
|
||||||
@@ -252,7 +267,8 @@
|
|||||||
"bindTextToElement": "Lisää tekstiä painamalla enter",
|
"bindTextToElement": "Lisää tekstiä painamalla enter",
|
||||||
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
|
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
|
||||||
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
|
"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": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
||||||
@@ -411,7 +427,9 @@
|
|||||||
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
||||||
"canvas": "piirtoalue",
|
"canvas": "piirtoalue",
|
||||||
"selection": "valinta",
|
"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": {
|
"colors": {
|
||||||
"transparent": "Läpinäkyvä",
|
"transparent": "Läpinäkyvä",
|
||||||
@@ -449,5 +467,36 @@
|
|||||||
"shades": "",
|
"shades": "",
|
||||||
"hexCode": "",
|
"hexCode": "",
|
||||||
"noShades": ""
|
"noShades": ""
|
||||||
|
},
|
||||||
|
"overwriteConfirm": {
|
||||||
|
"action": {
|
||||||
|
"exportToImage": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"saveToDisk": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"excalidrawPlus": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"loadFromFile": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"shareableLink": {
|
||||||
|
"title": "",
|
||||||
|
"button": "",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user