Compare commits
52
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2abc4c2ac1 | ||
|
|
9025ad99fc | ||
|
|
357a1c47f6 | ||
|
|
752c7cf66e | ||
|
|
83780b91d2 | ||
|
|
fefd377408 | ||
|
|
d3d7244993 | ||
|
|
8542c95a7a | ||
|
|
cef6094d4c | ||
|
|
3322f0fa6f | ||
|
|
34a7d48b95 | ||
|
|
5c0b15ce2b | ||
|
|
9f9666110e | ||
|
|
05ffce62ef | ||
|
|
0f06fa3851 | ||
|
|
1ce933d2f5 | ||
|
|
15655acb5a | ||
|
|
b3068a5248 | ||
|
|
d5b264c2d2 | ||
|
|
caa22dd4c3 | ||
|
|
78e5816459 | ||
|
|
0b30a23694 | ||
|
|
bd4424bbe3 | ||
|
|
38fc51b4e3 | ||
|
|
e1dc748aef | ||
|
|
0e95e2b386 | ||
|
|
9659254fd6 | ||
|
|
39b96cb011 | ||
|
|
04a8c22f39 | ||
|
|
e4506be3e8 | ||
|
|
1e816e87bf | ||
|
|
5368ddef74 | ||
|
|
88ff32e9b3 | ||
|
|
0fcbddda8e | ||
|
|
b9ba407f96 | ||
|
|
5acb99777a | ||
|
|
b107c9af2a | ||
|
|
c587b85b4e | ||
|
|
9686141113 | ||
|
|
0d7ee891e0 | ||
|
|
71fb60394a | ||
|
|
c9d18ecab6 | ||
|
|
8c1168ef33 | ||
|
|
c3c45a8c37 | ||
|
|
a8e6028c33 | ||
|
|
11e2f90ca1 | ||
|
|
4db87a0b6a | ||
|
|
4414069617 | ||
|
|
a9c5bdb878 | ||
|
|
5a0334f37f | ||
|
|
d8a4ca6911 | ||
|
|
eb9eeefc63 |
@@ -22,3 +22,8 @@ REACT_APP_DEV_ENABLE_SW=
|
|||||||
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
|
||||||
|
|
||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
|
#Debug flags
|
||||||
|
|
||||||
|
# To enable bounding box for text containers
|
||||||
|
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ FROM node:14-alpine AS build
|
|||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
COPY package.json yarn.lock ./
|
COPY package.json yarn.lock ./
|
||||||
RUN yarn --ignore-optional
|
RUN yarn --ignore-optional --network-timeout 600000
|
||||||
|
|
||||||
ARG NODE_ENV=production
|
ARG NODE_ENV=production
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,121 @@
|
|||||||
<div align="center" style="display:flex;flex-direction:column;"}>
|
<a href="https://excalidraw.com/" target="_blank" rel="noopener">
|
||||||
<a href="https://excalidraw.com">
|
<picture>
|
||||||
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams."/>
|
<source media="(prefers-color-scheme: dark)" alt="Excalidraw" srcset="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover_dark.png" />
|
||||||
</a>
|
<img alt="Excalidraw" src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover.png" />
|
||||||
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br/>Collaborative and end-to-end encrypted.</h3>
|
</picture>
|
||||||
<p>
|
</a>
|
||||||
<a href="https://twitter.com/excalidraw">
|
|
||||||
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
<h4 align="center">
|
||||||
</a>
|
<a href="https://excalidraw.com">Excalidraw Editor</a> |
|
||||||
<a href="https://discord.gg/UexuTaE">
|
<a href="https://blog.excalidraw.com">Blog</a> |
|
||||||
<img alt="Chat with us on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
<a href="https://docs.excalidraw.com">Documentation</a> |
|
||||||
</a>
|
<a href="https://plus.excalidraw.com">Excalidraw+</a>
|
||||||
</p>
|
</h4>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<h2>
|
||||||
|
An open source virtual hand-drawn style whiteboard. </br>
|
||||||
|
Collaborative and end-to-end encrypted. </br>
|
||||||
|
<br />
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Try now
|
<br />
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
|
||||||
|
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
|
||||||
|
</a>
|
||||||
|
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
|
||||||
|
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/UexuTaE">
|
||||||
|
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/excalidraw">
|
||||||
|
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
Visit [excalidraw.com](https://excalidraw.com) to start sketching.
|
<div align="center">
|
||||||
|
<figure>
|
||||||
|
<a href="https://excalidraw.com" target="_blank" rel="noopener">
|
||||||
|
<img src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2Fproduct_showcase.png" alt="Product showcase" />
|
||||||
|
</a>
|
||||||
|
<figcaption>
|
||||||
|
<p align="center">
|
||||||
|
Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
|
||||||
|
</p>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
|
||||||
## Community
|
## Features
|
||||||
|
|
||||||
For latest updates, follow us on [twitter](https://twitter.com/excalidraw). If you need help or want to chat, join us on [Discord](https://discord.gg/UexuTaE). For releases and deep dives, check out our [blog](https://blog.excalidraw.com). Report bugs on [GitHub](https://github.com/excalidraw/excalidraw/issues).
|
The Excalidraw editor (npm package) supports:
|
||||||
|
|
||||||
## Supporting Excalidraw
|
- 💯 Free & open-source.
|
||||||
|
- 🎨 Infinite, canvas-based whiteboard.
|
||||||
|
- ✍️ Hand-drawn like style.
|
||||||
|
- 🌓 Dark mode.
|
||||||
|
- 🏗️ Customizable.
|
||||||
|
- 📷 Image support.
|
||||||
|
- 😀 Shape libraries support.
|
||||||
|
- 👅 Localization (i18n) support.
|
||||||
|
- 🖼️ Export to PNG, SVG & clipboard.
|
||||||
|
- 💾 Open format - export drawings as an `.excalidraw` json file.
|
||||||
|
- ⚒️ Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
|
||||||
|
- ➡️ Arrow-binding & labeled arrows.
|
||||||
|
- 🔙 Undo / Redo.
|
||||||
|
- 🔍 Zoom and panning support.
|
||||||
|
|
||||||
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
|
## Excalidraw.com
|
||||||
|
|
||||||
|
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
|
||||||
|
|
||||||
|
- 📡 PWA support (works offline).
|
||||||
|
- 🤼 Real-time collaboration.
|
||||||
|
- 🔒 End-to-end encryption.
|
||||||
|
- 💾 Local-first support (autosaves to the browser).
|
||||||
|
- 🔗 Shareable links (export to a readonly link you can share with others).
|
||||||
|
|
||||||
|
We'll be adding these features as drop-in plugins for the npm package in the future.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install react react-dom @excalidraw/excalidraw
|
||||||
|
```
|
||||||
|
|
||||||
|
or via yarn
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn add react react-dom @excalidraw/excalidraw
|
||||||
|
```
|
||||||
|
|
||||||
|
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues).
|
||||||
|
- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE).
|
||||||
|
- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating).
|
||||||
|
|
||||||
|
## Integrations
|
||||||
|
|
||||||
|
- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
|
||||||
|
- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw)
|
||||||
|
|
||||||
|
## Who's integrating Excalidraw
|
||||||
|
|
||||||
|
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others
|
||||||
|
|
||||||
|
## Sponsors & support
|
||||||
|
|
||||||
|
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/).
|
||||||
|
|
||||||
|
## Thank you for supporting Excalidraw
|
||||||
|
|
||||||
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
|
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
|
||||||
|
|
||||||
@@ -32,13 +124,3 @@ If you like the project, you can become a sponsor at [Open Collective](https://o
|
|||||||
Last but not least, we're thankful to these companies for offering their services for free:
|
Last but not least, we're thankful to these companies for offering their services for free:
|
||||||
|
|
||||||
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
[](https://vercel.com) [](https://sentry.io) [](https://crowdin.com)
|
||||||
|
|
||||||
## Developers
|
|
||||||
|
|
||||||
You can integrate Excalidraw into your app by installing our [npm component](https://npmjs.com/package/@excalidraw/excalidraw).
|
|
||||||
|
|
||||||
Visit our documentation on [https://docs.excalidraw.com](https://docs.excalidraw.com).
|
|
||||||
|
|
||||||
## Who's integrating Excalidraw
|
|
||||||
|
|
||||||
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/)
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ Parameter `refreshDimensions` indicates whether we should also `recalculate` tex
|
|||||||
**_Signature_**
|
**_Signature_**
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
restoreElements(
|
restore(
|
||||||
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>
|
||||||
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
localAppState: Partial<<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>
|
||||||
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
|
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
|
||||||
|
|||||||
@@ -34,14 +34,16 @@ function App() {
|
|||||||
|
|
||||||
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
|
||||||
|
|
||||||
|
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
|
||||||
|
|
||||||
```jsx showLineNumbers
|
```jsx showLineNumbers
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [Comp, setComp] = useState(null);
|
const [Excalidraw, setExcalidraw] = useState(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
import("@excalidraw/excalidraw").then((comp) => setComp(comp.default));
|
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
|
||||||
}, []);
|
}, []);
|
||||||
return <>{Comp && <Comp />}</>;
|
return <>{Excalidraw && <Excalidraw />}</>;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Pull requests are welcome. For major changes, please [open an issue](https://git
|
|||||||
|
|
||||||
### Option 2 - CodeSandbox
|
### Option 2 - CodeSandbox
|
||||||
|
|
||||||
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
|
1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw
|
||||||
1. Connect your GitHub account
|
1. Connect your GitHub account
|
||||||
1. Go to Git tab on left side
|
1. Go to Git tab on left side
|
||||||
1. Tap on `Fork Sandbox`
|
1. Tap on `Fork Sandbox`
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ const config = {
|
|||||||
docs: {
|
docs: {
|
||||||
sidebarPath: require.resolve("./sidebars.js"),
|
sidebarPath: require.resolve("./sidebars.js"),
|
||||||
// Please change this to your repo.
|
// Please change this to your repo.
|
||||||
editUrl: "https://github.com/excalidraw/docs/tree/master/",
|
editUrl:
|
||||||
|
"https://github.com/excalidraw/excalidraw/tree/master/dev-docs/",
|
||||||
|
showLastUpdateAuthor: true,
|
||||||
|
showLastUpdateTime: true,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
customCss: [
|
customCss: [
|
||||||
@@ -129,6 +132,11 @@ const config = {
|
|||||||
tableOfContents: {
|
tableOfContents: {
|
||||||
maxHeadingLevel: 4,
|
maxHeadingLevel: 4,
|
||||||
},
|
},
|
||||||
|
algolia: {
|
||||||
|
appId: "8FEAOD28DI",
|
||||||
|
apiKey: "4b07cca33ff2d2919bc95ff98f148e9e",
|
||||||
|
indexName: "excalidraw",
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
themes: ["@docusaurus/theme-live-codeblock"],
|
themes: ["@docusaurus/theme-live-codeblock"],
|
||||||
plugins: ["docusaurus-plugin-sass"],
|
plugins: ["docusaurus-plugin-sass"],
|
||||||
|
|||||||
+6
-6
@@ -1785,9 +1785,9 @@
|
|||||||
"@hapi/hoek" "^9.0.0"
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@sideway/formula@^3.0.0":
|
"@sideway/formula@^3.0.0":
|
||||||
version "3.0.0"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
|
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
|
||||||
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
|
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
|
||||||
|
|
||||||
"@sideway/pinpoint@^2.0.0":
|
"@sideway/pinpoint@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
@@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1:
|
|||||||
entities "^4.3.0"
|
entities "^4.3.0"
|
||||||
|
|
||||||
http-cache-semantics@^4.0.0:
|
http-cache-semantics@^4.0.0:
|
||||||
version "4.1.0"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
|
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
|
||||||
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
|
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
|
||||||
|
|
||||||
http-deceiver@^1.2.7:
|
http-deceiver@^1.2.7:
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
|
|||||||
+149
-14
@@ -1,7 +1,8 @@
|
|||||||
import { VERTICAL_ALIGN } from "../constants";
|
import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
import { getNonDeletedElements, isTextElement } from "../element";
|
import { getNonDeletedElements, isTextElement, newElement } from "../element";
|
||||||
import { mutateElement } from "../element/mutateElement";
|
import { mutateElement } from "../element/mutateElement";
|
||||||
import {
|
import {
|
||||||
|
computeContainerDimensionForBoundText,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
measureText,
|
measureText,
|
||||||
redrawTextBoundingBox,
|
redrawTextBoundingBox,
|
||||||
@@ -13,8 +14,11 @@ import {
|
|||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isTextBindableContainer,
|
isTextBindableContainer,
|
||||||
|
isUsingAdaptiveRadius,
|
||||||
} from "../element/typeChecks";
|
} from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawLinearElement,
|
||||||
ExcalidrawTextContainer,
|
ExcalidrawTextContainer,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
@@ -38,7 +42,7 @@ export const actionUnbindText = register({
|
|||||||
selectedElements.forEach((element) => {
|
selectedElements.forEach((element) => {
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
const { width, height, baseline } = measureText(
|
const { width, height } = measureText(
|
||||||
boundTextElement.originalText,
|
boundTextElement.originalText,
|
||||||
getFontString(boundTextElement),
|
getFontString(boundTextElement),
|
||||||
);
|
);
|
||||||
@@ -51,7 +55,6 @@ export const actionUnbindText = register({
|
|||||||
containerId: null,
|
containerId: null,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
baseline,
|
|
||||||
text: boundTextElement.originalText,
|
text: boundTextElement.originalText,
|
||||||
});
|
});
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
@@ -130,19 +133,151 @@ export const actionBindText = register({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
redrawTextBoundingBox(textElement, container);
|
redrawTextBoundingBox(textElement, container);
|
||||||
const updatedElements = elements.slice();
|
|
||||||
const textElementIndex = updatedElements.findIndex(
|
|
||||||
(ele) => ele.id === textElement.id,
|
|
||||||
);
|
|
||||||
updatedElements.splice(textElementIndex, 1);
|
|
||||||
const containerIndex = updatedElements.findIndex(
|
|
||||||
(ele) => ele.id === container.id,
|
|
||||||
);
|
|
||||||
updatedElements.splice(containerIndex + 1, 0, textElement);
|
|
||||||
return {
|
return {
|
||||||
elements: updatedElements,
|
elements: pushTextAboveContainer(elements, container, textElement),
|
||||||
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
appState: { ...appState, selectedElementIds: { [container.id]: true } },
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pushTextAboveContainer = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
) => {
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
const textElementIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === textElement.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(textElementIndex, 1);
|
||||||
|
|
||||||
|
const containerIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === container.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(containerIndex + 1, 0, textElement);
|
||||||
|
return updatedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushContainerBelowText = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
container: ExcalidrawElement,
|
||||||
|
textElement: ExcalidrawTextElement,
|
||||||
|
) => {
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
const containerIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === container.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(containerIndex, 1);
|
||||||
|
|
||||||
|
const textElementIndex = updatedElements.findIndex(
|
||||||
|
(ele) => ele.id === textElement.id,
|
||||||
|
);
|
||||||
|
updatedElements.splice(textElementIndex, 0, container);
|
||||||
|
return updatedElements;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actionCreateContainerFromText = register({
|
||||||
|
name: "createContainerFromText",
|
||||||
|
contextItemLabel: "labels.createContainerFromText",
|
||||||
|
trackEvent: { category: "element" },
|
||||||
|
predicate: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(elements, appState);
|
||||||
|
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
|
||||||
|
},
|
||||||
|
perform: (elements, appState) => {
|
||||||
|
const selectedElements = getSelectedElements(
|
||||||
|
getNonDeletedElements(elements),
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
const updatedElements = elements.slice();
|
||||||
|
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
|
||||||
|
const textElement = selectedElements[0];
|
||||||
|
const container = newElement({
|
||||||
|
type: "rectangle",
|
||||||
|
backgroundColor: appState.currentItemBackgroundColor,
|
||||||
|
boundElements: [
|
||||||
|
...(textElement.boundElements || []),
|
||||||
|
{ id: textElement.id, type: "text" },
|
||||||
|
],
|
||||||
|
angle: textElement.angle,
|
||||||
|
fillStyle: appState.currentItemFillStyle,
|
||||||
|
strokeColor: appState.currentItemStrokeColor,
|
||||||
|
roughness: appState.currentItemRoughness,
|
||||||
|
strokeWidth: appState.currentItemStrokeWidth,
|
||||||
|
strokeStyle: appState.currentItemStrokeStyle,
|
||||||
|
roundness:
|
||||||
|
appState.currentItemRoundness === "round"
|
||||||
|
? {
|
||||||
|
type: isUsingAdaptiveRadius("rectangle")
|
||||||
|
? ROUNDNESS.ADAPTIVE_RADIUS
|
||||||
|
: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
opacity: 100,
|
||||||
|
locked: false,
|
||||||
|
x: textElement.x - BOUND_TEXT_PADDING,
|
||||||
|
y: textElement.y - BOUND_TEXT_PADDING,
|
||||||
|
width: computeContainerDimensionForBoundText(
|
||||||
|
textElement.width,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
height: computeContainerDimensionForBoundText(
|
||||||
|
textElement.height,
|
||||||
|
"rectangle",
|
||||||
|
),
|
||||||
|
groupIds: textElement.groupIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// update bindings
|
||||||
|
if (textElement.boundElements?.length) {
|
||||||
|
const linearElementIds = textElement.boundElements
|
||||||
|
.filter((ele) => ele.type === "arrow")
|
||||||
|
.map((el) => el.id);
|
||||||
|
const linearElements = updatedElements.filter((ele) =>
|
||||||
|
linearElementIds.includes(ele.id),
|
||||||
|
) as ExcalidrawLinearElement[];
|
||||||
|
linearElements.forEach((ele) => {
|
||||||
|
let startBinding = null;
|
||||||
|
let endBinding = null;
|
||||||
|
if (ele.startBinding) {
|
||||||
|
startBinding = { ...ele.startBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
if (ele.endBinding) {
|
||||||
|
endBinding = { ...ele.endBinding, elementId: container.id };
|
||||||
|
}
|
||||||
|
mutateElement(ele, { startBinding, endBinding });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(textElement, {
|
||||||
|
containerId: container.id,
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
boundElements: null,
|
||||||
|
});
|
||||||
|
redrawTextBoundingBox(textElement, container);
|
||||||
|
|
||||||
|
return {
|
||||||
|
elements: pushContainerBelowText(
|
||||||
|
[...elements, container],
|
||||||
|
container,
|
||||||
|
textElement,
|
||||||
|
),
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
|
selectedElementIds: {
|
||||||
|
[container.id]: true,
|
||||||
|
[textElement.id]: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
elements: updatedElements,
|
||||||
|
appState,
|
||||||
|
commitToHistory: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
@@ -154,7 +154,9 @@ export const actionDeleteSelected = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
contextItemLabel: "labels.delete",
|
contextItemLabel: "labels.delete",
|
||||||
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
|
keyTest: (event, appState, elements) =>
|
||||||
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
|
||||||
|
!event[KEYS.CTRL_OR_CMD],
|
||||||
PanelComponent: ({ elements, appState, updateData }) => (
|
PanelComponent: ({ elements, appState, updateData }) => (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ import { AppState } from "../types";
|
|||||||
import { fixBindingsAfterDuplication } from "../element/binding";
|
import { fixBindingsAfterDuplication } from "../element/binding";
|
||||||
import { ActionResult } from "./types";
|
import { ActionResult } from "./types";
|
||||||
import { GRID_SIZE } from "../constants";
|
import { GRID_SIZE } from "../constants";
|
||||||
import { bindTextToShapeAfterDuplication } from "../element/textElement";
|
import {
|
||||||
|
bindTextToShapeAfterDuplication,
|
||||||
|
getBoundTextElement,
|
||||||
|
} from "../element/textElement";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
|
import { normalizeElementOrder } from "../element/sortElements";
|
||||||
import { DuplicateIcon } from "../components/icons";
|
import { DuplicateIcon } from "../components/icons";
|
||||||
|
|
||||||
export const actionDuplicateSelection = register({
|
export const actionDuplicateSelection = register({
|
||||||
@@ -64,6 +68,11 @@ const duplicateElements = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
): Partial<ActionResult> => {
|
): Partial<ActionResult> => {
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// step (1)
|
||||||
|
|
||||||
|
const sortedElements = normalizeElementOrder(elements);
|
||||||
const groupIdMap = new Map();
|
const groupIdMap = new Map();
|
||||||
const newElements: ExcalidrawElement[] = [];
|
const newElements: ExcalidrawElement[] = [];
|
||||||
const oldElements: ExcalidrawElement[] = [];
|
const oldElements: ExcalidrawElement[] = [];
|
||||||
@@ -85,42 +94,112 @@ const duplicateElements = (
|
|||||||
return newElement;
|
return newElement;
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalElements: ExcalidrawElement[] = [];
|
|
||||||
|
|
||||||
let index = 0;
|
|
||||||
const selectedElementIds = arrayToMap(
|
const selectedElementIds = arrayToMap(
|
||||||
getSelectedElements(elements, appState, true),
|
getSelectedElements(sortedElements, appState, true),
|
||||||
);
|
);
|
||||||
while (index < elements.length) {
|
|
||||||
const element = elements[index];
|
// Ids of elements that have already been processed so we don't push them
|
||||||
|
// into the array twice if we end up backtracking when retrieving
|
||||||
|
// discontiguous group of elements (can happen due to a bug, or in edge
|
||||||
|
// cases such as a group containing deleted elements which were not selected).
|
||||||
|
//
|
||||||
|
// This is not enough to prevent duplicates, so we do a second loop afterwards
|
||||||
|
// to remove them.
|
||||||
|
//
|
||||||
|
// For convenience we mark even the newly created ones even though we don't
|
||||||
|
// loop over them.
|
||||||
|
const processedIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
|
|
||||||
|
const markAsProcessed = (elements: ExcalidrawElement[]) => {
|
||||||
|
for (const element of elements) {
|
||||||
|
processedIds.set(element.id, true);
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const elementsWithClones: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
let index = -1;
|
||||||
|
|
||||||
|
while (++index < sortedElements.length) {
|
||||||
|
const element = sortedElements[index];
|
||||||
|
|
||||||
|
if (processedIds.get(element.id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundTextElement = getBoundTextElement(element);
|
||||||
if (selectedElementIds.get(element.id)) {
|
if (selectedElementIds.get(element.id)) {
|
||||||
if (element.groupIds.length) {
|
// if a group or a container/bound-text, duplicate atomically
|
||||||
|
if (element.groupIds.length || boundTextElement) {
|
||||||
const groupId = getSelectedGroupForElement(appState, element);
|
const groupId = getSelectedGroupForElement(appState, element);
|
||||||
// if group selected, duplicate it atomically
|
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
const groupElements = getElementsInGroup(elements, groupId);
|
const groupElements = getElementsInGroup(sortedElements, groupId);
|
||||||
finalElements.push(
|
elementsWithClones.push(
|
||||||
...groupElements,
|
...markAsProcessed([
|
||||||
...groupElements.map((element) =>
|
...groupElements,
|
||||||
|
...groupElements.map((element) =>
|
||||||
|
duplicateAndOffsetElement(element),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (boundTextElement) {
|
||||||
|
elementsWithClones.push(
|
||||||
|
...markAsProcessed([
|
||||||
|
element,
|
||||||
|
boundTextElement,
|
||||||
duplicateAndOffsetElement(element),
|
duplicateAndOffsetElement(element),
|
||||||
),
|
duplicateAndOffsetElement(boundTextElement),
|
||||||
|
]),
|
||||||
);
|
);
|
||||||
index = index + groupElements.length;
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finalElements.push(element, duplicateAndOffsetElement(element));
|
elementsWithClones.push(
|
||||||
|
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
finalElements.push(element);
|
elementsWithClones.push(...markAsProcessed([element]));
|
||||||
}
|
}
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// step (2)
|
||||||
|
|
||||||
|
// second pass to remove duplicates. We loop from the end as it's likelier
|
||||||
|
// that the last elements are in the correct order (contiguous or otherwise).
|
||||||
|
// Thus we need to reverse as the last step (3).
|
||||||
|
|
||||||
|
const finalElementsReversed: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
const finalElementIds = new Map<ExcalidrawElement["id"], true>();
|
||||||
|
index = elementsWithClones.length;
|
||||||
|
|
||||||
|
while (--index >= 0) {
|
||||||
|
const element = elementsWithClones[index];
|
||||||
|
if (!finalElementIds.get(element.id)) {
|
||||||
|
finalElementIds.set(element.id, true);
|
||||||
|
finalElementsReversed.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// step (3)
|
||||||
|
|
||||||
|
const finalElements = finalElementsReversed.reverse();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
bindTextToShapeAfterDuplication(
|
bindTextToShapeAfterDuplication(
|
||||||
finalElements,
|
elementsWithClones,
|
||||||
|
oldElements,
|
||||||
|
oldIdToDuplicatedId,
|
||||||
|
);
|
||||||
|
fixBindingsAfterDuplication(
|
||||||
|
elementsWithClones,
|
||||||
oldElements,
|
oldElements,
|
||||||
oldIdToDuplicatedId,
|
oldIdToDuplicatedId,
|
||||||
);
|
);
|
||||||
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
elements: finalElements,
|
elements: finalElements,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { isDarwin } from "../constants";
|
import { isDarwin } from "../constants";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { SubtypeOf } from "../utility-types";
|
||||||
import { getShortcutKey } from "../utils";
|
import { getShortcutKey } from "../utils";
|
||||||
import { ActionName } from "./types";
|
import { ActionName } from "./types";
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ export type ShortcutName =
|
|||||||
ActionName,
|
ActionName,
|
||||||
| "toggleTheme"
|
| "toggleTheme"
|
||||||
| "loadScene"
|
| "loadScene"
|
||||||
|
| "clearCanvas"
|
||||||
| "cut"
|
| "cut"
|
||||||
| "copy"
|
| "copy"
|
||||||
| "paste"
|
| "paste"
|
||||||
@@ -41,6 +43,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
|||||||
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
toggleTheme: [getShortcutKey("Shift+Alt+D")],
|
||||||
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
saveScene: [getShortcutKey("CtrlOrCmd+S")],
|
||||||
loadScene: [getShortcutKey("CtrlOrCmd+O")],
|
loadScene: [getShortcutKey("CtrlOrCmd+O")],
|
||||||
|
clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")],
|
||||||
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
|
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
|
||||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||||
copy: [getShortcutKey("CtrlOrCmd+C")],
|
copy: [getShortcutKey("CtrlOrCmd+C")],
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
ExcalidrawProps,
|
ExcalidrawProps,
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import { MarkOptional } from "../utility-types";
|
||||||
|
|
||||||
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
|
||||||
|
|
||||||
@@ -113,7 +114,8 @@ export type ActionName =
|
|||||||
| "toggleLock"
|
| "toggleLock"
|
||||||
| "toggleLinearEditor"
|
| "toggleLinearEditor"
|
||||||
| "toggleEraserTool"
|
| "toggleEraserTool"
|
||||||
| "toggleHandTool";
|
| "toggleHandTool"
|
||||||
|
| "createContainerFromText";
|
||||||
|
|
||||||
export type PanelComponentProps = {
|
export type PanelComponentProps = {
|
||||||
elements: readonly ExcalidrawElement[];
|
elements: readonly ExcalidrawElement[];
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ export const getClientColors = (clientId: string, appState: AppState) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getClientInitials = (userName?: string | null) => {
|
export const getClientInitials = (userName?: string | null) => {
|
||||||
if (!userName) {
|
if (!userName?.trim()) {
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
return userName.trim()[0].toUpperCase();
|
return userName.trim()[0].toUpperCase();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { actionClearCanvas } from "../actions";
|
import { actionClearCanvas } from "../actions";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
import { useExcalidrawActionManager } from "./App";
|
import { useExcalidrawActionManager } from "./App";
|
||||||
import ConfirmDialog from "./ConfirmDialog";
|
import ConfirmDialog from "./ConfirmDialog";
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
|
|||||||
export const ActiveConfirmDialog = () => {
|
export const ActiveConfirmDialog = () => {
|
||||||
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
|
||||||
activeConfirmDialogAtom,
|
activeConfirmDialogAtom,
|
||||||
|
jotaiScope,
|
||||||
);
|
);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
|
|||||||
+32
-11
@@ -108,6 +108,7 @@ import {
|
|||||||
textWysiwyg,
|
textWysiwyg,
|
||||||
transformElements,
|
transformElements,
|
||||||
updateTextElement,
|
updateTextElement,
|
||||||
|
redrawTextBoundingBox,
|
||||||
} from "../element";
|
} from "../element";
|
||||||
import {
|
import {
|
||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
@@ -226,6 +227,7 @@ import {
|
|||||||
setEraserCursor,
|
setEraserCursor,
|
||||||
updateActiveTool,
|
updateActiveTool,
|
||||||
getShortcutKey,
|
getShortcutKey,
|
||||||
|
isTransparent,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
@@ -263,6 +265,7 @@ import {
|
|||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getContainerCenter,
|
getContainerCenter,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
|
getContainerElement,
|
||||||
getTextBindableContainerAtPosition,
|
getTextBindableContainerAtPosition,
|
||||||
isValidTextContainer,
|
isValidTextContainer,
|
||||||
} from "../element/textElement";
|
} from "../element/textElement";
|
||||||
@@ -279,6 +282,9 @@ import { shouldShowBoundingBox } from "../element/transformHandles";
|
|||||||
import { Fonts } from "../scene/Fonts";
|
import { Fonts } from "../scene/Fonts";
|
||||||
import { actionPaste } from "../actions/actionClipboard";
|
import { actionPaste } from "../actions/actionClipboard";
|
||||||
import { actionToggleHandTool } from "../actions/actionCanvas";
|
import { actionToggleHandTool } from "../actions/actionCanvas";
|
||||||
|
import { jotaiStore } from "../jotai";
|
||||||
|
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
|
||||||
|
import { actionCreateContainerFromText } from "../actions/actionBoundText";
|
||||||
|
|
||||||
const deviceContextInitialValue = {
|
const deviceContextInitialValue = {
|
||||||
isSmScreen: false,
|
isSmScreen: false,
|
||||||
@@ -834,7 +840,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const scene = restore(initialData, null, null);
|
const scene = restore(initialData, null, null, { repairBindings: true });
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
theme: this.props.theme || scene.appState.theme,
|
theme: this.props.theme || scene.appState.theme,
|
||||||
@@ -1622,6 +1628,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
oldIdToDuplicatedId.set(element.id, newElement.id);
|
oldIdToDuplicatedId.set(element.id, newElement.id);
|
||||||
return newElement;
|
return newElement;
|
||||||
});
|
});
|
||||||
|
|
||||||
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
|
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
|
||||||
const nextElements = [
|
const nextElements = [
|
||||||
...this.scene.getElementsIncludingDeleted(),
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
@@ -1634,6 +1641,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scene.replaceAllElements(nextElements);
|
this.scene.replaceAllElements(nextElements);
|
||||||
|
|
||||||
|
newElements.forEach((newElement) => {
|
||||||
|
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||||
|
const container = getContainerElement(newElement);
|
||||||
|
redrawTextBoundingBox(newElement, container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.history.resumeRecording();
|
this.history.resumeRecording();
|
||||||
|
|
||||||
this.setState(
|
this.setState(
|
||||||
@@ -1952,7 +1967,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Input handling
|
// Input handling
|
||||||
|
|
||||||
private onKeyDown = withBatchedUpdates(
|
private onKeyDown = withBatchedUpdates(
|
||||||
(event: React.KeyboardEvent | KeyboardEvent) => {
|
(event: React.KeyboardEvent | KeyboardEvent) => {
|
||||||
// normalize `event.key` when CapsLock is pressed #2372
|
// normalize `event.key` when CapsLock is pressed #2372
|
||||||
@@ -2194,6 +2208,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
|
||||||
|
) {
|
||||||
|
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -2654,14 +2675,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// case: creating new text not centered to parent element → offset Y
|
|
||||||
// so that the text is centered to cursor position
|
|
||||||
if (!parentCenterPosition) {
|
|
||||||
mutateElement(element, {
|
|
||||||
y: element.y - element.baseline / 2,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -2754,7 +2767,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
sceneY,
|
sceneY,
|
||||||
);
|
);
|
||||||
if (container) {
|
if (container) {
|
||||||
if (isArrowElement(container) || hasBoundTextElement(container)) {
|
if (
|
||||||
|
hasBoundTextElement(container) ||
|
||||||
|
!isTransparent(container.backgroundColor) ||
|
||||||
|
isHittingElementNotConsideringBoundingBox(container, this.state, [
|
||||||
|
sceneX,
|
||||||
|
sceneY,
|
||||||
|
])
|
||||||
|
) {
|
||||||
const midPoint = getContainerCenter(container, this.state);
|
const midPoint = getContainerCenter(container, this.state);
|
||||||
|
|
||||||
sceneX = midPoint.x;
|
sceneX = midPoint.x;
|
||||||
@@ -6218,6 +6238,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionGroup,
|
actionGroup,
|
||||||
actionUnbindText,
|
actionUnbindText,
|
||||||
actionBindText,
|
actionBindText,
|
||||||
|
actionCreateContainerFromText,
|
||||||
actionUngroup,
|
actionUngroup,
|
||||||
CONTEXT_MENU_SEPARATOR,
|
CONTEXT_MENU_SEPARATOR,
|
||||||
actionAddToLibrary,
|
actionAddToLibrary,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import DialogActionButton from "./DialogActionButton";
|
|||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||||
import { useExcalidrawSetAppState } from "./App";
|
import { useExcalidrawSetAppState } from "./App";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
|
||||||
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
interface Props extends Omit<DialogProps, "onCloseRequest"> {
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
@@ -24,7 +25,7 @@ const ConfirmDialog = (props: Props) => {
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { AppState } from "../types";
|
|||||||
import { queryFocusableElements } from "../utils";
|
import { queryFocusableElements } from "../utils";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
|
||||||
|
import { jotaiScope } from "../jotai";
|
||||||
|
|
||||||
export interface DialogProps {
|
export interface DialogProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -72,7 +73,7 @@ export const Dialog = (props: DialogProps) => {
|
|||||||
}, [islandNode, props.autofocus]);
|
}, [islandNode, props.autofocus]);
|
||||||
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
|
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setAppState({ openMenu: null });
|
setAppState({ openMenu: null });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
import { t } from "../i18n";
|
||||||
import { HelpIcon } from "./icons";
|
import { HelpIcon } from "./icons";
|
||||||
|
|
||||||
type HelpButtonProps = {
|
type HelpButtonProps = {
|
||||||
title?: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
onClick?(): void;
|
onClick?(): void;
|
||||||
@@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => (
|
|||||||
className="help-icon"
|
className="help-icon"
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
type="button"
|
type="button"
|
||||||
title={`${props.title} — ?`}
|
title={`${t("helpDialog.title")} — ?`}
|
||||||
aria-label={props.title}
|
aria-label={t("helpDialog.title")}
|
||||||
>
|
>
|
||||||
{HelpIcon}
|
{HelpIcon}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -273,22 +273,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
className="HelpDialog__island--editor"
|
className="HelpDialog__island--editor"
|
||||||
caption={t("helpDialog.editor")}
|
caption={t("helpDialog.editor")}
|
||||||
>
|
>
|
||||||
<Shortcut
|
|
||||||
label={t("labels.selectAll")}
|
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
|
||||||
label={t("labels.multiSelect")}
|
|
||||||
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
|
||||||
label={t("helpDialog.deepSelect")}
|
|
||||||
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
|
||||||
label={t("helpDialog.deepBoxSelect")}
|
|
||||||
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.moveCanvas")}
|
label={t("labels.moveCanvas")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
@@ -297,6 +281,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
]}
|
]}
|
||||||
isOr={true}
|
isOr={true}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("buttons.clearReset")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+Delete")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.delete")}
|
||||||
|
shortcuts={[getShortcutKey("Delete")]}
|
||||||
|
/>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.cut")}
|
label={t("labels.cut")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
|
||||||
@@ -313,6 +305,22 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.pasteAsPlaintext")}
|
label={t("labels.pasteAsPlaintext")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
|
||||||
/>
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.selectAll")}
|
||||||
|
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("labels.multiSelect")}
|
||||||
|
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("helpDialog.deepSelect")}
|
||||||
|
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
|
||||||
|
/>
|
||||||
|
<Shortcut
|
||||||
|
label={t("helpDialog.deepBoxSelect")}
|
||||||
|
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
|
||||||
|
/>
|
||||||
{/* firefox supports clipboard API under a flag, so we'll
|
{/* firefox supports clipboard API under a flag, so we'll
|
||||||
show users what they can do in the error message */}
|
show users what they can do in the error message */}
|
||||||
{(probablySupportsClipboardBlob || isFirefox) && (
|
{(probablySupportsClipboardBlob || isFirefox) && (
|
||||||
@@ -329,10 +337,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
|||||||
label={t("labels.pasteStyles")}
|
label={t("labels.pasteStyles")}
|
||||||
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
|
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
|
||||||
/>
|
/>
|
||||||
<Shortcut
|
|
||||||
label={t("labels.delete")}
|
|
||||||
shortcuts={[getShortcutKey("Delete")]}
|
|
||||||
/>
|
|
||||||
<Shortcut
|
<Shortcut
|
||||||
label={t("labels.sendToBack")}
|
label={t("labels.sendToBack")}
|
||||||
shortcuts={[
|
shortcuts={[
|
||||||
|
|||||||
@@ -124,7 +124,6 @@ const LayerUI = ({
|
|||||||
children,
|
children,
|
||||||
}: LayerUIProps) => {
|
}: LayerUIProps) => {
|
||||||
const device = useDevice();
|
const device = useDevice();
|
||||||
|
|
||||||
const tunnels = useInitializeTunnels();
|
const tunnels = useInitializeTunnels();
|
||||||
|
|
||||||
const renderJSONExportDialog = () => {
|
const renderJSONExportDialog = () => {
|
||||||
@@ -409,6 +408,7 @@ const LayerUI = ({
|
|||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
renderSidebars={renderSidebars}
|
renderSidebars={renderSidebars}
|
||||||
device={device}
|
device={device}
|
||||||
|
renderWelcomeScreen={renderWelcomeScreen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export const LibraryMenuHeader: React.FC<{
|
|||||||
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
|
||||||
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
|
||||||
isLibraryMenuOpenAtom,
|
isLibraryMenuOpenAtom,
|
||||||
|
jotaiScope,
|
||||||
);
|
);
|
||||||
const renderRemoveLibAlert = useCallback(() => {
|
const renderRemoveLibAlert = useCallback(() => {
|
||||||
const content = selectedItems.length
|
const content = selectedItems.length
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type MobileMenuProps = {
|
|||||||
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
|
||||||
renderSidebars: () => JSX.Element | null;
|
renderSidebars: () => JSX.Element | null;
|
||||||
device: Device;
|
device: Device;
|
||||||
|
renderWelcomeScreen: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
@@ -57,12 +58,13 @@ export const MobileMenu = ({
|
|||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
renderSidebars,
|
renderSidebars,
|
||||||
device,
|
device,
|
||||||
|
renderWelcomeScreen,
|
||||||
}: MobileMenuProps) => {
|
}: MobileMenuProps) => {
|
||||||
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
|
||||||
const renderToolbar = () => {
|
const renderToolbar = () => {
|
||||||
return (
|
return (
|
||||||
<FixedSideContainer side="top" className="App-top-bar">
|
<FixedSideContainer side="top" className="App-top-bar">
|
||||||
<welcomeScreenCenterTunnel.Out />
|
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
|
||||||
<Section heading="shapes">
|
<Section heading="shapes">
|
||||||
{(heading: React.ReactNode) => (
|
{(heading: React.ReactNode) => (
|
||||||
<Stack.Col gap={4} align="center">
|
<Stack.Col gap={4} align="center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
useExcalidrawAppState,
|
useExcalidrawAppState,
|
||||||
useExcalidrawSetAppState,
|
useExcalidrawSetAppState,
|
||||||
@@ -31,11 +31,10 @@ import "./DefaultItems.scss";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useSetAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
|
||||||
|
import { jotaiScope } from "../../jotai";
|
||||||
|
|
||||||
export const LoadScene = () => {
|
export const LoadScene = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
if (!actionManager.isActionEnabled(actionLoadScene)) {
|
||||||
@@ -57,9 +56,7 @@ export const LoadScene = () => {
|
|||||||
LoadScene.displayName = "LoadScene";
|
LoadScene.displayName = "LoadScene";
|
||||||
|
|
||||||
export const SaveToActiveFile = () => {
|
export const SaveToActiveFile = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
|
||||||
@@ -80,9 +77,7 @@ SaveToActiveFile.displayName = "SaveToActiveFile";
|
|||||||
|
|
||||||
export const SaveAsImage = () => {
|
export const SaveAsImage = () => {
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
icon={ExportImageIcon}
|
icon={ExportImageIcon}
|
||||||
@@ -98,9 +93,7 @@ export const SaveAsImage = () => {
|
|||||||
SaveAsImage.displayName = "SaveAsImage";
|
SaveAsImage.displayName = "SaveAsImage";
|
||||||
|
|
||||||
export const Help = () => {
|
export const Help = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
|
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
@@ -119,10 +112,12 @@ export const Help = () => {
|
|||||||
Help.displayName = "Help";
|
Help.displayName = "Help";
|
||||||
|
|
||||||
export const ClearCanvas = () => {
|
export const ClearCanvas = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
const setActiveConfirmDialog = useSetAtom(
|
||||||
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
|
activeConfirmDialogAtom,
|
||||||
|
jotaiScope,
|
||||||
|
);
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
if (!actionManager.isActionEnabled(actionClearCanvas)) {
|
||||||
@@ -143,6 +138,7 @@ export const ClearCanvas = () => {
|
|||||||
ClearCanvas.displayName = "ClearCanvas";
|
ClearCanvas.displayName = "ClearCanvas";
|
||||||
|
|
||||||
export const ToggleTheme = () => {
|
export const ToggleTheme = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
@@ -175,6 +171,7 @@ export const ToggleTheme = () => {
|
|||||||
ToggleTheme.displayName = "ToggleTheme";
|
ToggleTheme.displayName = "ToggleTheme";
|
||||||
|
|
||||||
export const ChangeCanvasBackground = () => {
|
export const ChangeCanvasBackground = () => {
|
||||||
|
const { t } = useI18n();
|
||||||
const appState = useExcalidrawAppState();
|
const appState = useExcalidrawAppState();
|
||||||
const actionManager = useExcalidrawActionManager();
|
const actionManager = useExcalidrawActionManager();
|
||||||
|
|
||||||
@@ -195,9 +192,7 @@ export const ChangeCanvasBackground = () => {
|
|||||||
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
|
||||||
|
|
||||||
export const Export = () => {
|
export const Export = () => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
const setAppState = useExcalidrawSetAppState();
|
const setAppState = useExcalidrawSetAppState();
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -248,9 +243,7 @@ export const LiveCollaborationTrigger = ({
|
|||||||
onSelect: () => void;
|
onSelect: () => void;
|
||||||
isCollaborating: boolean;
|
isCollaborating: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
// FIXME Hack until we tie "t" to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
data-testid="collab-button"
|
data-testid="collab-button"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { actionLoadScene, actionShortcuts } from "../../actions";
|
import { actionLoadScene, actionShortcuts } from "../../actions";
|
||||||
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
|
||||||
import { t } from "../../i18n";
|
import { t, useI18n } from "../../i18n";
|
||||||
import {
|
import {
|
||||||
useDevice,
|
useDevice,
|
||||||
useExcalidrawActionManager,
|
useExcalidrawActionManager,
|
||||||
@@ -172,10 +172,7 @@ const MenuItemLiveCollaborationTrigger = ({
|
|||||||
}: {
|
}: {
|
||||||
onSelect: () => any;
|
onSelect: () => any;
|
||||||
}) => {
|
}) => {
|
||||||
// FIXME when we tie t() to lang state
|
const { t } = useI18n();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const appState = useExcalidrawAppState();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
|
||||||
{t("labels.liveCollaboration")}
|
{t("labels.liveCollaboration")}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const isFirefox =
|
|||||||
"netscape" in window &&
|
"netscape" in window &&
|
||||||
navigator.userAgent.indexOf("rv:") > 1 &&
|
navigator.userAgent.indexOf("rv:") > 1 &&
|
||||||
navigator.userAgent.indexOf("Gecko") > 1;
|
navigator.userAgent.indexOf("Gecko") > 1;
|
||||||
|
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
|
||||||
|
export const isSafari =
|
||||||
|
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
||||||
|
|
||||||
export const APP_NAME = "Excalidraw";
|
export const APP_NAME = "Excalidraw";
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -530,6 +530,7 @@
|
|||||||
// (doesn't work in Firefox)
|
// (doesn't work in Firefox)
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@@ -567,8 +568,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.App-toolbar--mobile {
|
.App-toolbar--mobile {
|
||||||
overflow-x: hidden;
|
overflow-x: auto;
|
||||||
max-width: 100vw;
|
max-width: 90vw;
|
||||||
|
|
||||||
.ToolIcon__keybinding {
|
.ToolIcon__keybinding {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -95,6 +95,9 @@
|
|||||||
--color-gray-90: #1e1e1e;
|
--color-gray-90: #1e1e1e;
|
||||||
--color-gray-100: #121212;
|
--color-gray-100: #121212;
|
||||||
|
|
||||||
|
--color-warning: #fceeca;
|
||||||
|
--color-text-warning: var(--text-primary-color);
|
||||||
|
|
||||||
--color-danger: #db6965;
|
--color-danger: #db6965;
|
||||||
--color-promo: #e70078;
|
--color-promo: #e70078;
|
||||||
|
|
||||||
@@ -163,6 +166,8 @@
|
|||||||
--color-primary-darkest: #beb9ff;
|
--color-primary-darkest: #beb9ff;
|
||||||
--color-primary-light: #4f4d6f;
|
--color-primary-light: #4f4d6f;
|
||||||
|
|
||||||
|
--color-text-warning: var(--color-gray-80);
|
||||||
|
|
||||||
--color-danger: #ffa8a5;
|
--color-danger: #ffa8a5;
|
||||||
--color-promo: #d297ff;
|
--color-promo: #d297ff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CanvasError } from "../errors";
|
|||||||
import { t } from "../i18n";
|
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 { bytesToHexString } from "../utils";
|
import { bytesToHexString } from "../utils";
|
||||||
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
||||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||||
@@ -156,6 +157,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
|
{ repairBindings: true },
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
|||||||
+9
-4
@@ -34,6 +34,7 @@ import { bumpVersion } from "../element/mutateElement";
|
|||||||
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
|
||||||
import { arrayToMap } from "../utils";
|
import { arrayToMap } from "../utils";
|
||||||
import oc from "open-color";
|
import oc from "open-color";
|
||||||
|
import { MarkOptional, Mutable } from "../utility-types";
|
||||||
|
|
||||||
type RestoredAppState = Omit<
|
type RestoredAppState = Omit<
|
||||||
AppState,
|
AppState,
|
||||||
@@ -171,7 +172,6 @@ const restoreElement = (
|
|||||||
fontSize,
|
fontSize,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
text: element.text ?? "",
|
text: element.text ?? "",
|
||||||
baseline: element.baseline,
|
|
||||||
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
|
||||||
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
|
||||||
containerId: element.containerId ?? null,
|
containerId: element.containerId ?? null,
|
||||||
@@ -339,7 +339,7 @@ export const restoreElements = (
|
|||||||
elements: ImportedDataState["elements"],
|
elements: ImportedDataState["elements"],
|
||||||
/** NOTE doesn't serve for reconciliation */
|
/** NOTE doesn't serve for reconciliation */
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
refreshDimensions = false,
|
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
|
||||||
): ExcalidrawElement[] => {
|
): ExcalidrawElement[] => {
|
||||||
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
const localElementsMap = localElements ? arrayToMap(localElements) : null;
|
||||||
const restoredElements = (elements || []).reduce((elements, element) => {
|
const restoredElements = (elements || []).reduce((elements, element) => {
|
||||||
@@ -348,7 +348,7 @@ export const restoreElements = (
|
|||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||||
element,
|
element,
|
||||||
refreshDimensions,
|
opts?.refreshDimensions,
|
||||||
);
|
);
|
||||||
if (migratedElement) {
|
if (migratedElement) {
|
||||||
const localElement = localElementsMap?.get(element.id);
|
const localElement = localElementsMap?.get(element.id);
|
||||||
@@ -361,6 +361,10 @@ export const restoreElements = (
|
|||||||
return elements;
|
return elements;
|
||||||
}, [] as ExcalidrawElement[]);
|
}, [] as ExcalidrawElement[]);
|
||||||
|
|
||||||
|
if (!opts?.repairBindings) {
|
||||||
|
return restoredElements;
|
||||||
|
}
|
||||||
|
|
||||||
// repair binding. Mutates elements.
|
// repair binding. Mutates elements.
|
||||||
const restoredElementsMap = arrayToMap(restoredElements);
|
const restoredElementsMap = arrayToMap(restoredElements);
|
||||||
for (const element of restoredElements) {
|
for (const element of restoredElements) {
|
||||||
@@ -497,9 +501,10 @@ export const restore = (
|
|||||||
*/
|
*/
|
||||||
localAppState: Partial<AppState> | null | undefined,
|
localAppState: Partial<AppState> | null | undefined,
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
|
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
|
||||||
): RestoredDataState => {
|
): RestoredDataState => {
|
||||||
return {
|
return {
|
||||||
elements: restoreElements(data?.elements, localElements),
|
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
||||||
appState: restoreAppState(data?.appState, localAppState || null),
|
appState: restoreAppState(data?.appState, localAppState || null),
|
||||||
files: data?.files || {},
|
files: data?.files || {},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
import { rescalePoints } from "../points";
|
import { rescalePoints } from "../points";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
// 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 [number, number, number, number];
|
export type Bounds = readonly [number, number, number, number];
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { isTextElement } from ".";
|
|||||||
import { isTransparent } from "../utils";
|
import { isTransparent } from "../utils";
|
||||||
import { shouldShowBoundingBox } from "./transformHandles";
|
import { shouldShowBoundingBox } from "./transformHandles";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
const isElementDraggableFromInside = (
|
const isElementDraggableFromInside = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { shouldRotateWithDiscreteAngle } from "../keys";
|
|||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
import { getShapeForElement } from "../renderer/renderElement";
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { getSizeFromPoints } from "../points";
|
|||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
|
import { Mutable } from "../utility-types";
|
||||||
|
|
||||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
|
|||||||
@@ -22,16 +22,17 @@ import { getElementAbsoluteCoords } from ".";
|
|||||||
import { adjustXYWithRotation } from "../math";
|
import { adjustXYWithRotation } from "../math";
|
||||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||||
import {
|
import {
|
||||||
getBoundTextElement,
|
|
||||||
getBoundTextElementOffset,
|
getBoundTextElementOffset,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
measureText,
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
getMaxContainerWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
|
import { VERTICAL_ALIGN } from "../constants";
|
||||||
import { isArrowElement } from "./typeChecks";
|
import { isArrowElement } from "./typeChecks";
|
||||||
|
import { MarkOptional, Merge, Mutable } from "../utility-types";
|
||||||
|
|
||||||
type ElementConstructorOpts = MarkOptional<
|
type ElementConstructorOpts = MarkOptional<
|
||||||
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
|
||||||
@@ -153,7 +154,6 @@ export const newTextElement = (
|
|||||||
y: opts.y - offsets.y,
|
y: opts.y - offsets.y,
|
||||||
width: metrics.width,
|
width: metrics.width,
|
||||||
height: metrics.height,
|
height: metrics.height,
|
||||||
baseline: metrics.baseline,
|
|
||||||
containerId: opts.containerId || null,
|
containerId: opts.containerId || null,
|
||||||
originalText: text,
|
originalText: text,
|
||||||
},
|
},
|
||||||
@@ -170,18 +170,13 @@ const getAdjustedDimensions = (
|
|||||||
y: number;
|
y: number;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
baseline: number;
|
|
||||||
} => {
|
} => {
|
||||||
let maxWidth = null;
|
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
if (container) {
|
|
||||||
maxWidth = getMaxContainerWidth(container);
|
const { width: nextWidth, height: nextHeight } = measureText(
|
||||||
}
|
nextText,
|
||||||
const {
|
getFontString(element),
|
||||||
width: nextWidth,
|
);
|
||||||
height: nextHeight,
|
|
||||||
baseline: nextBaseline,
|
|
||||||
} = measureText(nextText, getFontString(element), maxWidth);
|
|
||||||
const { textAlign, verticalAlign } = element;
|
const { textAlign, verticalAlign } = element;
|
||||||
let x: number;
|
let x: number;
|
||||||
let y: number;
|
let y: number;
|
||||||
@@ -190,11 +185,7 @@ const getAdjustedDimensions = (
|
|||||||
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
|
||||||
!element.containerId
|
!element.containerId
|
||||||
) {
|
) {
|
||||||
const prevMetrics = measureText(
|
const prevMetrics = measureText(element.text, getFontString(element));
|
||||||
element.text,
|
|
||||||
getFontString(element),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
const offsets = getTextElementPositionOffsets(element, {
|
const offsets = getTextElementPositionOffsets(element, {
|
||||||
width: nextWidth - prevMetrics.width,
|
width: nextWidth - prevMetrics.width,
|
||||||
height: nextHeight - prevMetrics.height,
|
height: nextHeight - prevMetrics.height,
|
||||||
@@ -258,7 +249,6 @@ const getAdjustedDimensions = (
|
|||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
x: Number.isFinite(x) ? x : element.x,
|
x: Number.isFinite(x) ? x : element.x,
|
||||||
y: Number.isFinite(y) ? y : element.y,
|
y: Number.isFinite(y) ? y : element.y,
|
||||||
baseline: nextBaseline,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -278,38 +268,6 @@ export const refreshTextDimensions = (
|
|||||||
return { text, ...dimensions };
|
return { text, ...dimensions };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
|
||||||
const width = getContainerDims(container).width;
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
if (containerWidth <= 0) {
|
|
||||||
const boundText = getBoundTextElement(container);
|
|
||||||
if (boundText) {
|
|
||||||
return boundText.width;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
|
||||||
return containerWidth;
|
|
||||||
}
|
|
||||||
return width - BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
|
||||||
const height = getContainerDims(container).height;
|
|
||||||
if (isArrowElement(container)) {
|
|
||||||
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
if (containerHeight <= 0) {
|
|
||||||
const boundText = getBoundTextElement(container);
|
|
||||||
if (boundText) {
|
|
||||||
return boundText.height;
|
|
||||||
}
|
|
||||||
return BOUND_TEXT_PADDING * 8 * 2;
|
|
||||||
}
|
|
||||||
return height;
|
|
||||||
}
|
|
||||||
return height - BOUND_TEXT_PADDING * 2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateTextElement = (
|
export const updateTextElement = (
|
||||||
textElement: ExcalidrawTextElement,
|
textElement: ExcalidrawTextElement,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,12 +43,10 @@ import {
|
|||||||
getApproxMinLineWidth,
|
getApproxMinLineWidth,
|
||||||
getBoundTextElement,
|
getBoundTextElement,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getBoundTextElementOffset,
|
|
||||||
getContainerElement,
|
|
||||||
handleBindTextResize,
|
handleBindTextResize,
|
||||||
measureText,
|
getMaxContainerWidth,
|
||||||
|
getContainerElement,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import { getMaxContainerWidth } from "./newElement";
|
|
||||||
|
|
||||||
export const normalizeAngle = (angle: number): number => {
|
export const normalizeAngle = (angle: number): number => {
|
||||||
if (angle >= 2 * Math.PI) {
|
if (angle >= 2 * Math.PI) {
|
||||||
@@ -192,11 +190,10 @@ const rescalePointsInElement = (
|
|||||||
|
|
||||||
const MIN_FONT_SIZE = 1;
|
const MIN_FONT_SIZE = 1;
|
||||||
|
|
||||||
const measureFontSizeFromWH = (
|
const measureFontSizeFromWidth = (
|
||||||
element: NonDeleted<ExcalidrawTextElement>,
|
element: NonDeleted<ExcalidrawTextElement>,
|
||||||
nextWidth: number,
|
nextWidth: number,
|
||||||
nextHeight: number,
|
): number | null => {
|
||||||
): { size: number; baseline: number } | null => {
|
|
||||||
// We only use width to scale font on resize
|
// We only use width to scale font on resize
|
||||||
let width = element.width;
|
let width = element.width;
|
||||||
|
|
||||||
@@ -211,15 +208,8 @@ const measureFontSizeFromWH = (
|
|||||||
if (nextFontSize < MIN_FONT_SIZE) {
|
if (nextFontSize < MIN_FONT_SIZE) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const metrics = measureText(
|
|
||||||
element.text,
|
return nextFontSize;
|
||||||
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
|
|
||||||
element.containerId ? width : null,
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
size: nextFontSize,
|
|
||||||
baseline: metrics.baseline + (nextHeight - metrics.height),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSidesForTransformHandle = (
|
const getSidesForTransformHandle = (
|
||||||
@@ -290,8 +280,8 @@ const resizeSingleTextElement = (
|
|||||||
if (scale > 0) {
|
if (scale > 0) {
|
||||||
const nextWidth = element.width * scale;
|
const nextWidth = element.width * scale;
|
||||||
const nextHeight = element.height * scale;
|
const nextHeight = element.height * scale;
|
||||||
const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
|
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
|
||||||
if (nextFont === null) {
|
if (nextFontSize === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
|
||||||
@@ -315,10 +305,9 @@ const resizeSingleTextElement = (
|
|||||||
deltaY2,
|
deltaY2,
|
||||||
);
|
);
|
||||||
mutateElement(element, {
|
mutateElement(element, {
|
||||||
fontSize: nextFont.size,
|
fontSize: nextFontSize,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
baseline: nextFont.baseline,
|
|
||||||
x: nextElementX,
|
x: nextElementX,
|
||||||
y: nextElementY,
|
y: nextElementY,
|
||||||
});
|
});
|
||||||
@@ -371,7 +360,7 @@ export const resizeSingleElement = (
|
|||||||
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
|
||||||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||||
|
|
||||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
let boundTextFont: { fontSize?: number } = {};
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
|
|
||||||
if (transformHandleDirection.includes("e")) {
|
if (transformHandleDirection.includes("e")) {
|
||||||
@@ -423,30 +412,13 @@ export const resizeSingleElement = (
|
|||||||
if (stateOfBoundTextElementAtResize) {
|
if (stateOfBoundTextElementAtResize) {
|
||||||
boundTextFont = {
|
boundTextFont = {
|
||||||
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
fontSize: stateOfBoundTextElementAtResize.fontSize,
|
||||||
baseline: stateOfBoundTextElementAtResize.baseline,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (shouldMaintainAspectRatio) {
|
|
||||||
const boundTextElementPadding =
|
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
||||||
getBoundTextElementOffset(boundTextElement);
|
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
||||||
const nextFont = measureFontSizeFromWH(
|
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
||||||
boundTextElement,
|
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
||||||
eleNewWidth - boundTextElementPadding * 2,
|
|
||||||
eleNewHeight - boundTextElementPadding * 2,
|
|
||||||
);
|
|
||||||
if (nextFont === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
boundTextFont = {
|
|
||||||
fontSize: nextFont.size,
|
|
||||||
baseline: nextFont.baseline,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
|
|
||||||
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
|
|
||||||
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
|
|
||||||
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
|
||||||
@@ -579,7 +551,11 @@ export const resizeSingleElement = (
|
|||||||
if (boundTextElement && boundTextFont) {
|
if (boundTextElement && boundTextFont) {
|
||||||
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
|
||||||
}
|
}
|
||||||
handleBindTextResize(element, transformHandleDirection);
|
handleBindTextResize(
|
||||||
|
element,
|
||||||
|
transformHandleDirection,
|
||||||
|
shouldMaintainAspectRatio,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -683,7 +659,6 @@ const resizeMultipleElements = (
|
|||||||
y: number;
|
y: number;
|
||||||
points?: Point[];
|
points?: Point[];
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
baseline?: number;
|
|
||||||
} = {
|
} = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@@ -692,31 +667,34 @@ const resizeMultipleElements = (
|
|||||||
...rescaledPoints,
|
...rescaledPoints,
|
||||||
};
|
};
|
||||||
|
|
||||||
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
|
let boundTextUpdates: { fontSize: number } | null = null;
|
||||||
|
|
||||||
const boundTextElement = getBoundTextElement(element.latest);
|
const boundTextElement = getBoundTextElement(element.latest);
|
||||||
|
|
||||||
if (boundTextElement || isTextElement(element.orig)) {
|
if (boundTextElement || isTextElement(element.orig)) {
|
||||||
const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2;
|
const updatedElement = {
|
||||||
const textMeasurements = measureFontSizeFromWH(
|
...element.latest,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
const fontSize = measureFontSizeFromWidth(
|
||||||
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
boundTextElement ?? (element.orig as ExcalidrawTextElement),
|
||||||
width - optionalPadding,
|
boundTextElement
|
||||||
height - optionalPadding,
|
? getMaxContainerWidth(updatedElement)
|
||||||
|
: updatedElement.width,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!textMeasurements) {
|
if (!fontSize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element.orig)) {
|
if (isTextElement(element.orig)) {
|
||||||
update.fontSize = textMeasurements.size;
|
update.fontSize = fontSize;
|
||||||
update.baseline = textMeasurements.baseline;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundTextElement) {
|
if (boundTextElement) {
|
||||||
boundTextUpdates = {
|
boundTextUpdates = {
|
||||||
fontSize: textMeasurements.size,
|
fontSize,
|
||||||
baseline: textMeasurements.baseline,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { mutateElement } from "./mutateElement";
|
||||||
|
import { normalizeElementOrder } from "./sortElements";
|
||||||
|
import { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
const assertOrder = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
expectedOrder: string[],
|
||||||
|
) => {
|
||||||
|
const actualOrder = elements.map((element) => element.id);
|
||||||
|
expect(actualOrder).toEqual(expectedOrder);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("normalizeElementsOrder", () => {
|
||||||
|
it("sort bound-text elements", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
id: "container",
|
||||||
|
type: "rectangle",
|
||||||
|
});
|
||||||
|
const boundText = API.createElement({
|
||||||
|
id: "boundText",
|
||||||
|
type: "text",
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
const otherElement = API.createElement({
|
||||||
|
id: "otherElement",
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
const otherElement2 = API.createElement({
|
||||||
|
id: "otherElement2",
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
assertOrder(normalizeElementOrder([container, boundText]), [
|
||||||
|
"container",
|
||||||
|
"boundText",
|
||||||
|
]);
|
||||||
|
assertOrder(normalizeElementOrder([boundText, container]), [
|
||||||
|
"container",
|
||||||
|
"boundText",
|
||||||
|
]);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
boundText,
|
||||||
|
container,
|
||||||
|
otherElement,
|
||||||
|
otherElement2,
|
||||||
|
]),
|
||||||
|
["container", "boundText", "otherElement", "otherElement2"],
|
||||||
|
);
|
||||||
|
assertOrder(normalizeElementOrder([container, otherElement, boundText]), [
|
||||||
|
"container",
|
||||||
|
"boundText",
|
||||||
|
"otherElement",
|
||||||
|
]);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
container,
|
||||||
|
otherElement,
|
||||||
|
otherElement2,
|
||||||
|
boundText,
|
||||||
|
]),
|
||||||
|
["container", "boundText", "otherElement", "otherElement2"],
|
||||||
|
);
|
||||||
|
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
boundText,
|
||||||
|
otherElement,
|
||||||
|
container,
|
||||||
|
otherElement2,
|
||||||
|
]),
|
||||||
|
["otherElement", "container", "boundText", "otherElement2"],
|
||||||
|
);
|
||||||
|
|
||||||
|
// noop
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
otherElement,
|
||||||
|
container,
|
||||||
|
boundText,
|
||||||
|
otherElement2,
|
||||||
|
]),
|
||||||
|
["otherElement", "container", "boundText", "otherElement2"],
|
||||||
|
);
|
||||||
|
|
||||||
|
// text has existing containerId, but container doesn't list is
|
||||||
|
// as a boundElement
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "boundText",
|
||||||
|
type: "text",
|
||||||
|
containerId: "container",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "container",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["boundText", "container"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "boundText",
|
||||||
|
type: "text",
|
||||||
|
containerId: "container",
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["boundText"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "container",
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["container"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "container",
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ id: "x", type: "text" }],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["container"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "arrow",
|
||||||
|
type: "arrow",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "container",
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ id: "arrow", type: "arrow" }],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["arrow", "container"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("normalize group order", () => {
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect3",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect4",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect5",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect6",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect7",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["A_rect1", "A_rect4", "A_rect5", "A_rect7", "rect2", "rect3", "rect6"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "B_rect3",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect4",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "B_rect5",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect6",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect7",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["A_rect1", "A_rect4", "A_rect7", "rect2", "B_rect3", "B_rect5", "rect6"],
|
||||||
|
);
|
||||||
|
// nested groups
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "BA_rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B", "A"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["A_rect1", "BA_rect2"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "BA_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["BA_rect1", "A_rect2"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "BA_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "CBA_rect3",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["C", "B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect4",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect5",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "BA_rect5",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "BA_rect6",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "CBA_rect7",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["C", "B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "X_rect8",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["X"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "rect9",
|
||||||
|
type: "rectangle",
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "YX_rect10",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["Y", "X"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "X_rect11",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["X"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
[
|
||||||
|
"BA_rect1",
|
||||||
|
"BA_rect5",
|
||||||
|
"BA_rect6",
|
||||||
|
"A_rect2",
|
||||||
|
"A_rect5",
|
||||||
|
"CBA_rect3",
|
||||||
|
"CBA_rect7",
|
||||||
|
"rect4",
|
||||||
|
"X_rect8",
|
||||||
|
"X_rect11",
|
||||||
|
"YX_rect10",
|
||||||
|
"rect9",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
it.skip("normalize boundElements array", () => {
|
||||||
|
const container = API.createElement({
|
||||||
|
id: "container",
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [],
|
||||||
|
});
|
||||||
|
const boundText = API.createElement({
|
||||||
|
id: "boundText",
|
||||||
|
type: "text",
|
||||||
|
containerId: container.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [
|
||||||
|
{ type: "text", id: boundText.id },
|
||||||
|
{ type: "text", id: "xxx" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(normalizeElementOrder([container, boundText])).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: container.id,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({ id: boundText.id }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// should take around <100ms for 10K iterations (@dwelle's PC 22-05-25)
|
||||||
|
it.skip("normalizeElementsOrder() perf", () => {
|
||||||
|
const makeElements = (iterations: number) => {
|
||||||
|
const elements: ExcalidrawElement[] = [];
|
||||||
|
while (iterations--) {
|
||||||
|
const container = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [],
|
||||||
|
groupIds: ["B", "A"],
|
||||||
|
});
|
||||||
|
const boundText = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
containerId: container.id,
|
||||||
|
groupIds: ["A"],
|
||||||
|
});
|
||||||
|
const otherElement = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [],
|
||||||
|
groupIds: ["C", "A"],
|
||||||
|
});
|
||||||
|
mutateElement(container, {
|
||||||
|
boundElements: [{ type: "text", id: boundText.id }],
|
||||||
|
});
|
||||||
|
|
||||||
|
elements.push(boundText, otherElement, container);
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
};
|
||||||
|
|
||||||
|
const elements = makeElements(10000);
|
||||||
|
const t0 = Date.now();
|
||||||
|
normalizeElementOrder(elements);
|
||||||
|
console.info(`${Date.now() - t0}ms`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { arrayToMapWithIndex } from "../utils";
|
||||||
|
import { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
|
const normalizeGroupElementOrder = (elements: readonly ExcalidrawElement[]) => {
|
||||||
|
const origElements: ExcalidrawElement[] = elements.slice();
|
||||||
|
const sortedElements = new Set<ExcalidrawElement>();
|
||||||
|
|
||||||
|
const orderInnerGroups = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
): ExcalidrawElement[] => {
|
||||||
|
const firstGroupSig = elements[0]?.groupIds?.join("");
|
||||||
|
const aGroup: ExcalidrawElement[] = [elements[0]];
|
||||||
|
const bGroup: ExcalidrawElement[] = [];
|
||||||
|
for (const element of elements.slice(1)) {
|
||||||
|
if (element.groupIds?.join("") === firstGroupSig) {
|
||||||
|
aGroup.push(element);
|
||||||
|
} else {
|
||||||
|
bGroup.push(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bGroup.length ? [...aGroup, ...orderInnerGroups(bGroup)] : aGroup;
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupHandledElements = new Map<string, true>();
|
||||||
|
|
||||||
|
origElements.forEach((element, idx) => {
|
||||||
|
if (groupHandledElements.has(element.id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (element.groupIds?.length) {
|
||||||
|
const topGroup = element.groupIds[element.groupIds.length - 1];
|
||||||
|
const groupElements = origElements.slice(idx).filter((element) => {
|
||||||
|
const ret = element?.groupIds?.some((id) => id === topGroup);
|
||||||
|
if (ret) {
|
||||||
|
groupHandledElements.set(element!.id, true);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const elem of orderInnerGroups(groupElements)) {
|
||||||
|
sortedElements.add(elem);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sortedElements.add(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if there's a bug which resulted in losing some of the elements, return
|
||||||
|
// original instead as that's better than losing data
|
||||||
|
if (sortedElements.size !== elements.length) {
|
||||||
|
console.error("normalizeGroupElementOrder: lost some elements... bailing!");
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...sortedElements];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In theory, when we have text elements bound to a container, they
|
||||||
|
* should be right after the container element in the elements array.
|
||||||
|
* However, this is not guaranteed due to old and potential future bugs.
|
||||||
|
*
|
||||||
|
* This function sorts containers and their bound texts together. It prefers
|
||||||
|
* original z-index of container (i.e. it moves bound text elements after
|
||||||
|
* containers).
|
||||||
|
*/
|
||||||
|
const normalizeBoundElementsOrder = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const elementsMap = arrayToMapWithIndex(elements);
|
||||||
|
|
||||||
|
const origElements: (ExcalidrawElement | null)[] = elements.slice();
|
||||||
|
const sortedElements = new Set<ExcalidrawElement>();
|
||||||
|
|
||||||
|
origElements.forEach((element, idx) => {
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (element.boundElements?.length) {
|
||||||
|
sortedElements.add(element);
|
||||||
|
origElements[idx] = null;
|
||||||
|
element.boundElements.forEach((boundElement) => {
|
||||||
|
const child = elementsMap.get(boundElement.id);
|
||||||
|
if (child && boundElement.type === "text") {
|
||||||
|
sortedElements.add(child[0]);
|
||||||
|
origElements[child[1]] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (element.type === "text" && element.containerId) {
|
||||||
|
const parent = elementsMap.get(element.containerId);
|
||||||
|
if (!parent?.[0].boundElements?.find((x) => x.id === element.id)) {
|
||||||
|
sortedElements.add(element);
|
||||||
|
origElements[idx] = null;
|
||||||
|
|
||||||
|
// if element has a container and container lists it, skip this element
|
||||||
|
// as it'll be taken care of by the container
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sortedElements.add(element);
|
||||||
|
origElements[idx] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// if there's a bug which resulted in losing some of the elements, return
|
||||||
|
// original instead as that's better than losing data
|
||||||
|
if (sortedElements.size !== elements.length) {
|
||||||
|
console.error(
|
||||||
|
"normalizeBoundElementsOrder: lost some elements... bailing!",
|
||||||
|
);
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...sortedElements];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeElementOrder = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
// console.time();
|
||||||
|
const ret = normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
|
||||||
|
// console.timeEnd();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
+162
-31
@@ -1,5 +1,13 @@
|
|||||||
import { BOUND_TEXT_PADDING } from "../constants";
|
import { BOUND_TEXT_PADDING } from "../constants";
|
||||||
import { measureText, wrapText } from "./textElement";
|
import { API } from "../tests/helpers/api";
|
||||||
|
import {
|
||||||
|
computeContainerDimensionForBoundText,
|
||||||
|
computeBoundTextElementCoords,
|
||||||
|
getMaxContainerWidth,
|
||||||
|
getMaxContainerHeight,
|
||||||
|
wrapText,
|
||||||
|
computeContainerCoords,
|
||||||
|
} from "./textElement";
|
||||||
import { FontString } from "./types";
|
import { FontString } from "./types";
|
||||||
|
|
||||||
describe("Test wrapText", () => {
|
describe("Test wrapText", () => {
|
||||||
@@ -28,10 +36,11 @@ describe("Test wrapText", () => {
|
|||||||
|
|
||||||
describe("When text doesn't contain new lines", () => {
|
describe("When text doesn't contain new lines", () => {
|
||||||
const text = "Hello whats up";
|
const text = "Hello whats up";
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 90,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello
|
||||||
whats
|
whats
|
||||||
up`,
|
up`,
|
||||||
@@ -55,7 +64,7 @@ p`,
|
|||||||
{
|
{
|
||||||
desc: "break words as per the width",
|
desc: "break words as per the width",
|
||||||
|
|
||||||
width: 150,
|
width: 140,
|
||||||
res: `Hello whats
|
res: `Hello whats
|
||||||
up`,
|
up`,
|
||||||
},
|
},
|
||||||
@@ -65,6 +74,13 @@ up`,
|
|||||||
width: 250,
|
width: 250,
|
||||||
res: "Hello whats up",
|
res: "Hello whats up",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "should push the word if its equal to max width",
|
||||||
|
width: 60,
|
||||||
|
res: `Hello
|
||||||
|
whats
|
||||||
|
up`,
|
||||||
|
},
|
||||||
].forEach((data) => {
|
].forEach((data) => {
|
||||||
it(`should ${data.desc}`, () => {
|
it(`should ${data.desc}`, () => {
|
||||||
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
|
||||||
@@ -72,13 +88,14 @@ up`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("When text contain new lines", () => {
|
describe("When text contain new lines", () => {
|
||||||
const text = `Hello
|
const text = `Hello
|
||||||
whats up`;
|
whats up`;
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
desc: "break all words when width of each word is less than container width",
|
desc: "break all words when width of each word is less than container width",
|
||||||
width: 90,
|
width: 80,
|
||||||
res: `Hello
|
res: `Hello
|
||||||
whats
|
whats
|
||||||
up`,
|
up`,
|
||||||
@@ -161,36 +178,150 @@ break it now`,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test measureText", () => {
|
describe("Test computeBoundTextElementCoords", () => {
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
const params = { width: 200, height: 100, x: 10, y: 20 };
|
||||||
const text = "Hello World";
|
|
||||||
|
|
||||||
it("should add correct attributes when maxWidth is passed", () => {
|
it("should compute coords correctly when ellipse", () => {
|
||||||
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
|
const element = API.createElement({
|
||||||
const res = measureText(text, font, maxWidth);
|
type: "ellipse",
|
||||||
|
...params,
|
||||||
expect(res.container).toMatchInlineSnapshot(`
|
});
|
||||||
<div
|
expect(computeBoundTextElementCoords(element)).toEqual({
|
||||||
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; max-width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
|
x: 44.2893218813452455,
|
||||||
>
|
y: 39.64466094067262,
|
||||||
<span
|
});
|
||||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add correct attributes when maxWidth is not passed", () => {
|
it("should compute coords correctly when rectangle", () => {
|
||||||
const res = measureText(text, font);
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeBoundTextElementCoords(element)).toEqual({
|
||||||
|
x: 15,
|
||||||
|
y: 25,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(res.container).toMatchInlineSnapshot(`
|
it("should compute coords correctly when diamond", () => {
|
||||||
<div
|
const element = API.createElement({
|
||||||
style="position: absolute; white-space: pre; font: Emoji 20px 20px; min-height: 1em;"
|
type: "diamond",
|
||||||
>
|
...params,
|
||||||
<span
|
});
|
||||||
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
|
expect(computeBoundTextElementCoords(element)).toEqual({
|
||||||
/>
|
x: 65,
|
||||||
</div>
|
y: 50,
|
||||||
`);
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test computeContainerCoords", () => {
|
||||||
|
const boundTextElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
width: 200,
|
||||||
|
height: 100,
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
});
|
||||||
|
it("should compute coords correctly when ellipse", () => {
|
||||||
|
expect(computeContainerCoords(boundTextElement, "ellipse")).toEqual({
|
||||||
|
x: -24.289321881345245,
|
||||||
|
y: 0.3553390593273775,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute coords correctly when rectangle", () => {
|
||||||
|
expect(computeContainerCoords(boundTextElement, "rectangle")).toEqual({
|
||||||
|
x: 5,
|
||||||
|
y: 15,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute coords correctly when diamond", () => {
|
||||||
|
expect(computeContainerCoords(boundTextElement, "diamond")).toEqual({
|
||||||
|
x: -45,
|
||||||
|
y: -10,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test computeContainerDimensionForBoundText", () => {
|
||||||
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should compute container height correctly for rectangle", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
|
160,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute container height correctly for ellipse", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "ellipse",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
|
226,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should compute container height correctly for diamond", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "diamond",
|
||||||
|
...params,
|
||||||
|
});
|
||||||
|
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
|
||||||
|
320,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getMaxContainerWidth", () => {
|
||||||
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return max width when container is rectangle", () => {
|
||||||
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(168);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max width when container is ellipse", () => {
|
||||||
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(116);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max width when container is diamond", () => {
|
||||||
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
|
expect(getMaxContainerWidth(container)).toBe(79);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Test getMaxContainerHeight", () => {
|
||||||
|
const params = {
|
||||||
|
width: 178,
|
||||||
|
height: 194,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should return max height when container is rectangle", () => {
|
||||||
|
const container = API.createElement({ type: "rectangle", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(184);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is ellipse", () => {
|
||||||
|
const container = API.createElement({ type: "ellipse", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(127);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return max height when container is diamond", () => {
|
||||||
|
const container = API.createElement({ type: "diamond", ...params });
|
||||||
|
expect(getMaxContainerHeight(container)).toBe(87);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+300
-152
@@ -12,12 +12,7 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
|||||||
import { MaybeTransformHandleType } from "./transformHandles";
|
import { MaybeTransformHandleType } from "./transformHandles";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||||
import {
|
|
||||||
isBoundToContainer,
|
|
||||||
isImageElement,
|
|
||||||
isArrowElement,
|
|
||||||
} from "./typeChecks";
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { isTextBindableContainer } from "./typeChecks";
|
import { isTextBindableContainer } from "./typeChecks";
|
||||||
@@ -28,6 +23,7 @@ import {
|
|||||||
resetOriginalContainerCache,
|
resetOriginalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./textWysiwyg";
|
} from "./textWysiwyg";
|
||||||
|
import { ExtractSetType } from "../utility-types";
|
||||||
|
|
||||||
export const normalizeText = (text: string) => {
|
export const normalizeText = (text: string) => {
|
||||||
return (
|
return (
|
||||||
@@ -44,68 +40,66 @@ export const redrawTextBoundingBox = (
|
|||||||
container: ExcalidrawElement | null,
|
container: ExcalidrawElement | null,
|
||||||
) => {
|
) => {
|
||||||
let maxWidth = undefined;
|
let maxWidth = undefined;
|
||||||
let text = textElement.text;
|
|
||||||
|
const boundTextUpdates = {
|
||||||
|
x: textElement.x,
|
||||||
|
y: textElement.y,
|
||||||
|
text: textElement.text,
|
||||||
|
width: textElement.width,
|
||||||
|
height: textElement.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
boundTextUpdates.text = textElement.text;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
maxWidth = getMaxContainerWidth(container);
|
maxWidth = getMaxContainerWidth(container);
|
||||||
text = wrapText(
|
boundTextUpdates.text = wrapText(
|
||||||
textElement.originalText,
|
textElement.originalText,
|
||||||
getFontString(textElement),
|
getFontString(textElement),
|
||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const metrics = measureText(text, getFontString(textElement), maxWidth);
|
const metrics = measureText(
|
||||||
let coordY = textElement.y;
|
boundTextUpdates.text,
|
||||||
let coordX = textElement.x;
|
getFontString(textElement),
|
||||||
// Resize container and vertically center align the text
|
);
|
||||||
|
|
||||||
|
boundTextUpdates.width = metrics.width;
|
||||||
|
boundTextUpdates.height = metrics.height;
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
if (!isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
const containerDims = getContainerDims(container);
|
|
||||||
let nextHeight = containerDims.height;
|
|
||||||
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
|
||||||
coordY = container.y;
|
|
||||||
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
|
||||||
coordY =
|
|
||||||
container.y +
|
|
||||||
containerDims.height -
|
|
||||||
metrics.height -
|
|
||||||
BOUND_TEXT_PADDING;
|
|
||||||
} else {
|
|
||||||
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
|
|
||||||
if (metrics.height > getMaxContainerHeight(container)) {
|
|
||||||
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
|
|
||||||
coordY = container.y + nextHeight / 2 - metrics.height / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
|
|
||||||
coordX = container.x + BOUND_TEXT_PADDING;
|
|
||||||
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
|
|
||||||
coordX =
|
|
||||||
container.x +
|
|
||||||
containerDims.width -
|
|
||||||
metrics.width -
|
|
||||||
BOUND_TEXT_PADDING;
|
|
||||||
} else {
|
|
||||||
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
|
|
||||||
}
|
|
||||||
updateOriginalContainerCache(container.id, nextHeight);
|
|
||||||
mutateElement(container, { height: nextHeight });
|
|
||||||
} else {
|
|
||||||
const centerX = textElement.x + textElement.width / 2;
|
const centerX = textElement.x + textElement.width / 2;
|
||||||
const centerY = textElement.y + textElement.height / 2;
|
const centerY = textElement.y + textElement.height / 2;
|
||||||
const diffWidth = metrics.width - textElement.width;
|
const diffWidth = metrics.width - textElement.width;
|
||||||
const diffHeight = metrics.height - textElement.height;
|
const diffHeight = metrics.height - textElement.height;
|
||||||
coordY = centerY - (textElement.height + diffHeight) / 2;
|
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
|
||||||
coordX = centerX - (textElement.width + diffWidth) / 2;
|
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
|
||||||
|
} else {
|
||||||
|
const containerDims = getContainerDims(container);
|
||||||
|
let maxContainerHeight = getMaxContainerHeight(container);
|
||||||
|
|
||||||
|
let nextHeight = containerDims.height;
|
||||||
|
if (metrics.height > maxContainerHeight) {
|
||||||
|
nextHeight = computeContainerDimensionForBoundText(
|
||||||
|
metrics.height,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
mutateElement(container, { height: nextHeight });
|
||||||
|
maxContainerHeight = getMaxContainerHeight(container);
|
||||||
|
updateOriginalContainerCache(container.id, nextHeight);
|
||||||
|
}
|
||||||
|
const updatedTextElement = {
|
||||||
|
...textElement,
|
||||||
|
...boundTextUpdates,
|
||||||
|
} as ExcalidrawTextElementWithContainer;
|
||||||
|
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
|
||||||
|
boundTextUpdates.x = x;
|
||||||
|
boundTextUpdates.y = y;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mutateElement(textElement, {
|
|
||||||
width: metrics.width,
|
mutateElement(textElement, boundTextUpdates);
|
||||||
height: metrics.height,
|
|
||||||
baseline: metrics.baseline,
|
|
||||||
y: coordY,
|
|
||||||
x: coordX,
|
|
||||||
text,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bindTextToShapeAfterDuplication = (
|
export const bindTextToShapeAfterDuplication = (
|
||||||
@@ -127,10 +121,16 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
const newContainer = sceneElementMap.get(newElementId);
|
const newContainer = sceneElementMap.get(newElementId);
|
||||||
if (newContainer) {
|
if (newContainer) {
|
||||||
mutateElement(newContainer, {
|
mutateElement(newContainer, {
|
||||||
boundElements: (newContainer.boundElements || []).concat({
|
boundElements: (element.boundElements || [])
|
||||||
type: "text",
|
.filter(
|
||||||
id: newTextElementId,
|
(boundElement) =>
|
||||||
}),
|
boundElement.id !== newTextElementId &&
|
||||||
|
boundElement.id !== boundTextElementId,
|
||||||
|
)
|
||||||
|
.concat({
|
||||||
|
type: "text",
|
||||||
|
id: newTextElementId,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const newTextElement = sceneElementMap.get(newTextElementId);
|
const newTextElement = sceneElementMap.get(newTextElementId);
|
||||||
@@ -147,6 +147,7 @@ export const bindTextToShapeAfterDuplication = (
|
|||||||
export const handleBindTextResize = (
|
export const handleBindTextResize = (
|
||||||
container: NonDeletedExcalidrawElement,
|
container: NonDeletedExcalidrawElement,
|
||||||
transformHandleType: MaybeTransformHandleType,
|
transformHandleType: MaybeTransformHandleType,
|
||||||
|
shouldMaintainAspectRatio = false,
|
||||||
) => {
|
) => {
|
||||||
const boundTextElementId = getBoundTextElementId(container);
|
const boundTextElementId = getBoundTextElementId(container);
|
||||||
if (!boundTextElementId) {
|
if (!boundTextElementId) {
|
||||||
@@ -171,7 +172,6 @@ export const handleBindTextResize = (
|
|||||||
const maxWidth = getMaxContainerWidth(container);
|
const maxWidth = getMaxContainerWidth(container);
|
||||||
const maxHeight = getMaxContainerHeight(container);
|
const maxHeight = getMaxContainerHeight(container);
|
||||||
let containerHeight = containerDims.height;
|
let containerHeight = containerDims.height;
|
||||||
let nextBaseLine = textElement.baseline;
|
|
||||||
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
if (transformHandleType !== "n" && transformHandleType !== "s") {
|
||||||
if (text) {
|
if (text) {
|
||||||
text = wrapText(
|
text = wrapText(
|
||||||
@@ -180,18 +180,17 @@ export const handleBindTextResize = (
|
|||||||
maxWidth,
|
maxWidth,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const dimensions = measureText(
|
const dimensions = measureText(text, getFontString(textElement));
|
||||||
text,
|
|
||||||
getFontString(textElement),
|
|
||||||
maxWidth,
|
|
||||||
);
|
|
||||||
nextHeight = dimensions.height;
|
nextHeight = dimensions.height;
|
||||||
nextWidth = dimensions.width;
|
nextWidth = dimensions.width;
|
||||||
nextBaseLine = dimensions.baseline;
|
|
||||||
}
|
}
|
||||||
// increase height in case text element height exceeds
|
// increase height in case text element height exceeds
|
||||||
if (nextHeight > maxHeight) {
|
if (!shouldMaintainAspectRatio && nextHeight > maxHeight) {
|
||||||
containerHeight = nextHeight + getBoundTextElementOffset(textElement) * 2;
|
containerHeight = computeContainerDimensionForBoundText(
|
||||||
|
nextHeight,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
|
||||||
const diff = containerHeight - containerDims.height;
|
const diff = containerHeight - containerDims.height;
|
||||||
// fix the y coord when resizing from ne/nw/n
|
// fix the y coord when resizing from ne/nw/n
|
||||||
const updatedY =
|
const updatedY =
|
||||||
@@ -207,98 +206,115 @@ export const handleBindTextResize = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldMaintainAspectRatio &&
|
||||||
|
(nextHeight > maxHeight || nextWidth > maxWidth)
|
||||||
|
) {
|
||||||
|
let height = containerDims.height;
|
||||||
|
let width = containerDims.width;
|
||||||
|
let x = container.x;
|
||||||
|
let y = container.y;
|
||||||
|
|
||||||
|
if (nextHeight > maxHeight) {
|
||||||
|
height = computeContainerDimensionForBoundText(
|
||||||
|
nextHeight,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (nextWidth > maxWidth) {
|
||||||
|
width = computeContainerDimensionForBoundText(
|
||||||
|
nextWidth,
|
||||||
|
container.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const diffX = width - containerDims.width;
|
||||||
|
const diffY = height - containerDims.height;
|
||||||
|
|
||||||
|
if (transformHandleType === "n") {
|
||||||
|
y = container.y - diffY;
|
||||||
|
} else if (
|
||||||
|
transformHandleType === "e" ||
|
||||||
|
transformHandleType === "w" ||
|
||||||
|
transformHandleType === "ne" ||
|
||||||
|
transformHandleType === "nw"
|
||||||
|
) {
|
||||||
|
y = container.y - diffY / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transformHandleType === "s" || transformHandleType === "n") {
|
||||||
|
x = container.x - diffX / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
mutateElement(container, {
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mutateElement(textElement, {
|
mutateElement(textElement, {
|
||||||
text,
|
text,
|
||||||
width: nextWidth,
|
width: nextWidth,
|
||||||
height: nextHeight,
|
height: nextHeight,
|
||||||
|
|
||||||
baseline: nextBaseLine,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
updateBoundTextPosition(
|
mutateElement(
|
||||||
container,
|
textElement,
|
||||||
textElement as ExcalidrawTextElementWithContainer,
|
computeBoundTextPosition(
|
||||||
|
container,
|
||||||
|
textElement as ExcalidrawTextElementWithContainer,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateBoundTextPosition = (
|
const computeBoundTextPosition = (
|
||||||
container: ExcalidrawElement,
|
container: ExcalidrawElement,
|
||||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||||
) => {
|
) => {
|
||||||
const containerDims = getContainerDims(container);
|
const containerCoords = computeBoundTextElementCoords(container);
|
||||||
const boundTextElementPadding = getBoundTextElementOffset(boundTextElement);
|
const maxContainerHeight = getMaxContainerHeight(container);
|
||||||
|
const maxContainerWidth = getMaxContainerWidth(container);
|
||||||
|
|
||||||
|
let x;
|
||||||
let y;
|
let y;
|
||||||
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
|
||||||
y = container.y + boundTextElementPadding;
|
y = containerCoords.y;
|
||||||
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
y =
|
y = containerCoords.y + (maxContainerHeight - boundTextElement.height);
|
||||||
container.y +
|
|
||||||
containerDims.height -
|
|
||||||
boundTextElement.height -
|
|
||||||
boundTextElementPadding;
|
|
||||||
} else {
|
} else {
|
||||||
y = container.y + containerDims.height / 2 - boundTextElement.height / 2;
|
y =
|
||||||
|
containerCoords.y +
|
||||||
|
(maxContainerHeight / 2 - boundTextElement.height / 2);
|
||||||
}
|
}
|
||||||
const x =
|
if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) {
|
||||||
boundTextElement.textAlign === TEXT_ALIGN.LEFT
|
x = containerCoords.x;
|
||||||
? container.x + boundTextElementPadding
|
} else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) {
|
||||||
: boundTextElement.textAlign === TEXT_ALIGN.RIGHT
|
x = containerCoords.x + (maxContainerWidth - boundTextElement.width);
|
||||||
? container.x +
|
} else {
|
||||||
containerDims.width -
|
x =
|
||||||
boundTextElement.width -
|
containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2);
|
||||||
boundTextElementPadding
|
}
|
||||||
: container.x + containerDims.width / 2 - boundTextElement.width / 2;
|
return { x, y };
|
||||||
|
|
||||||
mutateElement(boundTextElement, { x, y });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
|
||||||
export const measureText = (
|
|
||||||
text: string,
|
export const measureText = (text: string, font: FontString) => {
|
||||||
font: FontString,
|
|
||||||
maxWidth?: number | null,
|
|
||||||
) => {
|
|
||||||
text = text
|
text = text
|
||||||
.split("\n")
|
.split("\n")
|
||||||
// replace empty lines with single space because leading/trailing empty
|
// replace empty lines with single space because leading/trailing empty
|
||||||
// lines would be stripped from computation
|
// lines would be stripped from computation
|
||||||
.map((x) => x || " ")
|
.map((x) => x || " ")
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const container = document.createElement("div");
|
|
||||||
container.style.position = "absolute";
|
|
||||||
container.style.whiteSpace = "pre";
|
|
||||||
container.style.font = font;
|
|
||||||
container.style.minHeight = "1em";
|
|
||||||
|
|
||||||
if (maxWidth) {
|
const height = getTextHeight(text, font);
|
||||||
const lineHeight = getApproxLineHeight(font);
|
const width = getTextWidth(text, font);
|
||||||
// since we are adding a span of width 1px later
|
|
||||||
container.style.maxWidth = `${maxWidth + 1}px`;
|
|
||||||
container.style.overflow = "hidden";
|
|
||||||
container.style.wordBreak = "break-word";
|
|
||||||
container.style.lineHeight = `${String(lineHeight)}px`;
|
|
||||||
container.style.whiteSpace = "pre-wrap";
|
|
||||||
}
|
|
||||||
document.body.appendChild(container);
|
|
||||||
container.innerText = text;
|
|
||||||
|
|
||||||
const span = document.createElement("span");
|
return { width, height };
|
||||||
span.style.display = "inline-block";
|
|
||||||
span.style.overflow = "hidden";
|
|
||||||
span.style.width = "1px";
|
|
||||||
span.style.height = "1px";
|
|
||||||
container.appendChild(span);
|
|
||||||
// Baseline is important for positioning text on canvas
|
|
||||||
const baseline = span.offsetTop + span.offsetHeight;
|
|
||||||
// since we are adding a span of width 1px
|
|
||||||
const width = container.offsetWidth + 1;
|
|
||||||
const height = container.offsetHeight;
|
|
||||||
document.body.removeChild(container);
|
|
||||||
if (isTestEnv()) {
|
|
||||||
return { width, height, baseline, container };
|
|
||||||
}
|
|
||||||
return { width, height, baseline };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
|
||||||
@@ -308,45 +324,51 @@ export const getApproxLineHeight = (font: FontString) => {
|
|||||||
if (cacheApproxLineHeight[font]) {
|
if (cacheApproxLineHeight[font]) {
|
||||||
return cacheApproxLineHeight[font];
|
return cacheApproxLineHeight[font];
|
||||||
}
|
}
|
||||||
cacheApproxLineHeight[font] = measureText(DUMMY_TEXT, font, null).height;
|
const fontSize = parseInt(font);
|
||||||
|
|
||||||
|
// Calculate line height relative to font size
|
||||||
|
cacheApproxLineHeight[font] = fontSize * 1.2;
|
||||||
return cacheApproxLineHeight[font];
|
return cacheApproxLineHeight[font];
|
||||||
};
|
};
|
||||||
|
|
||||||
let canvas: HTMLCanvasElement | undefined;
|
let canvas: HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
const getLineWidth = (text: string, font: FontString) => {
|
const getLineWidth = (text: string, font: FontString) => {
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
canvas = document.createElement("canvas");
|
canvas = document.createElement("canvas");
|
||||||
}
|
}
|
||||||
const canvas2dContext = canvas.getContext("2d")!;
|
const canvas2dContext = canvas.getContext("2d")!;
|
||||||
canvas2dContext.font = font;
|
canvas2dContext.font = font;
|
||||||
|
const width = canvas2dContext.measureText(text).width;
|
||||||
|
|
||||||
const metrics = canvas2dContext.measureText(text);
|
|
||||||
// since in test env the canvas measureText algo
|
// since in test env the canvas measureText algo
|
||||||
// doesn't measure text and instead just returns number of
|
// doesn't measure text and instead just returns number of
|
||||||
// characters hence we assume that each letteris 10px
|
// characters hence we assume that each letteris 10px
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return metrics.width * 10;
|
return width * 10;
|
||||||
}
|
}
|
||||||
// Since measureText behaves differently in different browsers
|
return width;
|
||||||
// OS so considering a adjustment factor of 0.2
|
|
||||||
const adjustmentFactor = 0.2;
|
|
||||||
|
|
||||||
return metrics.width + adjustmentFactor;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTextWidth = (text: string, font: FontString) => {
|
export const getTextWidth = (text: string, font: FontString) => {
|
||||||
const lines = text.split("\n");
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
let width = 0;
|
let width = 0;
|
||||||
lines.forEach((line) => {
|
lines.forEach((line) => {
|
||||||
width = Math.max(width, getLineWidth(line, font));
|
width = Math.max(width, getLineWidth(line, font));
|
||||||
});
|
});
|
||||||
return width;
|
return width;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getTextHeight = (text: string, font: FontString) => {
|
||||||
|
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
||||||
|
const lineHeight = getApproxLineHeight(font);
|
||||||
|
return lineHeight * lines.length;
|
||||||
|
};
|
||||||
|
|
||||||
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
||||||
const lines: Array<string> = [];
|
const lines: Array<string> = [];
|
||||||
const originalLines = text.split("\n");
|
const originalLines = text.split("\n");
|
||||||
const spaceWidth = getLineWidth(" ", font);
|
const spaceWidth = getLineWidth(" ", font);
|
||||||
|
|
||||||
const push = (str: string) => {
|
const push = (str: string) => {
|
||||||
if (str.trim()) {
|
if (str.trim()) {
|
||||||
lines.push(str);
|
lines.push(str);
|
||||||
@@ -363,16 +385,23 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
let currentLineWidthTillNow = 0;
|
let currentLineWidthTillNow = 0;
|
||||||
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
|
||||||
while (index < words.length) {
|
while (index < words.length) {
|
||||||
const currentWordWidth = getLineWidth(words[index], font);
|
const currentWordWidth = getLineWidth(words[index], font);
|
||||||
|
// This will only happen when single word takes entire width
|
||||||
|
if (currentWordWidth === maxWidth) {
|
||||||
|
push(words[index]);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
|
||||||
// Start breaking longer words exceeding max width
|
// Start breaking longer words exceeding max width
|
||||||
if (currentWordWidth >= maxWidth) {
|
else if (currentWordWidth > maxWidth) {
|
||||||
// push current line since the current word exceeds the max width
|
// push current line since the current word exceeds the max width
|
||||||
// so will be appended in next line
|
// so will be appended in next line
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
|
|
||||||
while (words[index].length > 0) {
|
while (words[index].length > 0) {
|
||||||
const currentChar = String.fromCodePoint(
|
const currentChar = String.fromCodePoint(
|
||||||
words[index].codePointAt(0)!,
|
words[index].codePointAt(0)!,
|
||||||
@@ -413,7 +442,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
|
|||||||
const word = words[index];
|
const word = words[index];
|
||||||
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
|
||||||
|
|
||||||
if (currentLineWidthTillNow >= maxWidth) {
|
if (currentLineWidthTillNow > maxWidth) {
|
||||||
push(currentLine);
|
push(currentLine);
|
||||||
currentLineWidthTillNow = 0;
|
currentLineWidthTillNow = 0;
|
||||||
currentLine = "";
|
currentLine = "";
|
||||||
@@ -473,9 +502,9 @@ export const charWidth = (() => {
|
|||||||
getCache,
|
getCache,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export const getApproxMinLineWidth = (font: FontString) => {
|
export const getApproxMinLineWidth = (font: FontString) => {
|
||||||
const maxCharWidth = getMaxCharWidth(font);
|
const maxCharWidth = getMaxCharWidth(font);
|
||||||
|
|
||||||
if (maxCharWidth === 0) {
|
if (maxCharWidth === 0) {
|
||||||
return (
|
return (
|
||||||
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
|
||||||
@@ -615,6 +644,49 @@ export const getContainerCenter = (
|
|||||||
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const computeBoundTextElementCoords = (
|
||||||
|
container: NonDeletedExcalidrawElement,
|
||||||
|
) => {
|
||||||
|
let offsetX = BOUND_TEXT_PADDING;
|
||||||
|
let offsetY = BOUND_TEXT_PADDING;
|
||||||
|
|
||||||
|
if (container.type === "ellipse") {
|
||||||
|
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
|
offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2);
|
||||||
|
offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2);
|
||||||
|
}
|
||||||
|
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
|
if (container.type === "diamond") {
|
||||||
|
offsetX += container.width / 4;
|
||||||
|
offsetY += container.height / 4;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: container.x + offsetX,
|
||||||
|
y: container.y + offsetY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const computeContainerCoords = (
|
||||||
|
boundTextElement: ExcalidrawTextElement,
|
||||||
|
containerType: string,
|
||||||
|
) => {
|
||||||
|
let offsetX = BOUND_TEXT_PADDING;
|
||||||
|
let offsetY = BOUND_TEXT_PADDING;
|
||||||
|
|
||||||
|
if (containerType === "ellipse") {
|
||||||
|
offsetX += (boundTextElement.width / 2) * (1 - Math.sqrt(2) / 2);
|
||||||
|
offsetY += (boundTextElement.height / 2) * (1 - Math.sqrt(2) / 2);
|
||||||
|
}
|
||||||
|
if (containerType === "diamond") {
|
||||||
|
offsetX += boundTextElement.width / 4;
|
||||||
|
offsetY += boundTextElement.height / 4;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: boundTextElement.x - offsetX,
|
||||||
|
y: boundTextElement.y - offsetY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||||
const container = getContainerElement(textElement);
|
const container = getContainerElement(textElement);
|
||||||
if (!container || isArrowElement(container)) {
|
if (!container || isArrowElement(container)) {
|
||||||
@@ -627,12 +699,13 @@ export const getBoundTextElementOffset = (
|
|||||||
boundTextElement: ExcalidrawTextElement | null,
|
boundTextElement: ExcalidrawTextElement | null,
|
||||||
) => {
|
) => {
|
||||||
const container = getContainerElement(boundTextElement);
|
const container = getContainerElement(boundTextElement);
|
||||||
if (!container) {
|
if (!container || !boundTextElement) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (isArrowElement(container)) {
|
if (isArrowElement(container)) {
|
||||||
return BOUND_TEXT_PADDING * 8;
|
return BOUND_TEXT_PADDING * 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
return BOUND_TEXT_PADDING;
|
return BOUND_TEXT_PADDING;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -708,12 +781,87 @@ export const getTextBindableContainerAtPosition = (
|
|||||||
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
return isTextBindableContainer(hitElement, false) ? hitElement : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidTextContainer = (element: ExcalidrawElement) => {
|
const VALID_CONTAINER_TYPES = new Set([
|
||||||
return (
|
"rectangle",
|
||||||
element.type === "rectangle" ||
|
"ellipse",
|
||||||
element.type === "ellipse" ||
|
"diamond",
|
||||||
element.type === "diamond" ||
|
"image",
|
||||||
isImageElement(element) ||
|
"arrow",
|
||||||
isArrowElement(element)
|
]);
|
||||||
);
|
|
||||||
|
export const isValidTextContainer = (element: ExcalidrawElement) =>
|
||||||
|
VALID_CONTAINER_TYPES.has(element.type);
|
||||||
|
|
||||||
|
export const computeContainerDimensionForBoundText = (
|
||||||
|
dimension: number,
|
||||||
|
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
|
||||||
|
) => {
|
||||||
|
dimension = Math.ceil(dimension);
|
||||||
|
const padding = BOUND_TEXT_PADDING * 2;
|
||||||
|
|
||||||
|
if (containerType === "ellipse") {
|
||||||
|
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
|
||||||
|
}
|
||||||
|
if (containerType === "arrow") {
|
||||||
|
return dimension + padding * 8;
|
||||||
|
}
|
||||||
|
if (containerType === "diamond") {
|
||||||
|
return 2 * (dimension + padding);
|
||||||
|
}
|
||||||
|
return dimension + padding;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
|
||||||
|
const width = getContainerDims(container).width;
|
||||||
|
if (isArrowElement(container)) {
|
||||||
|
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
if (containerWidth <= 0) {
|
||||||
|
const boundText = getBoundTextElement(container);
|
||||||
|
if (boundText) {
|
||||||
|
return boundText.width;
|
||||||
|
}
|
||||||
|
return BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
}
|
||||||
|
return containerWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container.type === "ellipse") {
|
||||||
|
// The width of the largest rectangle inscribed inside an ellipse is
|
||||||
|
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
|
||||||
|
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
|
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
if (container.type === "diamond") {
|
||||||
|
// The width of the largest rectangle inscribed inside a rhombus is
|
||||||
|
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
|
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
return width - BOUND_TEXT_PADDING * 2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
|
||||||
|
const height = getContainerDims(container).height;
|
||||||
|
if (isArrowElement(container)) {
|
||||||
|
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
if (containerHeight <= 0) {
|
||||||
|
const boundText = getBoundTextElement(container);
|
||||||
|
if (boundText) {
|
||||||
|
return boundText.height;
|
||||||
|
}
|
||||||
|
return BOUND_TEXT_PADDING * 8 * 2;
|
||||||
|
}
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
if (container.type === "ellipse") {
|
||||||
|
// The height of the largest rectangle inscribed inside an ellipse is
|
||||||
|
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
|
||||||
|
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
|
||||||
|
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
if (container.type === "diamond") {
|
||||||
|
// The height of the largest rectangle inscribed inside a rhombus is
|
||||||
|
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
|
||||||
|
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
|
||||||
|
}
|
||||||
|
return height - BOUND_TEXT_PADDING * 2;
|
||||||
};
|
};
|
||||||
|
|||||||
+384
-168
@@ -3,19 +3,23 @@ import ExcalidrawApp from "../excalidraw-app";
|
|||||||
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
import { GlobalTestState, render, screen } from "../tests/test-utils";
|
||||||
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
|
||||||
import { CODES, KEYS } from "../keys";
|
import { CODES, KEYS } from "../keys";
|
||||||
import { fireEvent } from "../tests/test-utils";
|
import {
|
||||||
|
fireEvent,
|
||||||
|
mockBoundingClientRect,
|
||||||
|
restoreOriginalGetBoundingClientRect,
|
||||||
|
} from "../tests/test-utils";
|
||||||
import { queryByText } from "@testing-library/react";
|
import { queryByText } from "@testing-library/react";
|
||||||
|
|
||||||
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
|
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
ExcalidrawTextElementWithContainer,
|
ExcalidrawTextElementWithContainer,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import * as textElementUtils from "./textElement";
|
|
||||||
import { API } from "../tests/helpers/api";
|
import { API } from "../tests/helpers/api";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { resize } from "../tests/utils";
|
import { resize } from "../tests/utils";
|
||||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||||
|
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
@@ -222,11 +226,19 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
describe("Test container-unbound text", () => {
|
describe("Test container-unbound text", () => {
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
const dimensions = { height: 400, width: 800 };
|
||||||
|
|
||||||
let textarea: HTMLTextAreaElement;
|
let textarea: HTMLTextAreaElement;
|
||||||
let textElement: ExcalidrawTextElement;
|
let textElement: ExcalidrawTextElement;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
mockBoundingClientRect(dimensions);
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
|
//@ts-ignore
|
||||||
|
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
|
||||||
|
|
||||||
textElement = UI.createElement("text");
|
textElement = UI.createElement("text");
|
||||||
|
|
||||||
@@ -236,6 +248,10 @@ describe("textWysiwyg", () => {
|
|||||||
)!;
|
)!;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
restoreOriginalGetBoundingClientRect();
|
||||||
|
});
|
||||||
|
|
||||||
it("should add a tab at the start of the first line", () => {
|
it("should add a tab at the start of the first line", () => {
|
||||||
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
|
||||||
textarea.value = "Line#1\nLine#2";
|
textarea.value = "Line#1\nLine#2";
|
||||||
@@ -434,23 +450,33 @@ describe("textWysiwyg", () => {
|
|||||||
);
|
);
|
||||||
expect(h.state.zoom.value).toBe(1);
|
expect(h.state.zoom.value).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("text should never go beyond max width", async () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
mouse.clickAt(750, 300);
|
||||||
|
|
||||||
|
textarea = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
)!;
|
||||||
|
fireEvent.change(textarea, {
|
||||||
|
target: {
|
||||||
|
value:
|
||||||
|
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
textarea.dispatchEvent(new Event("input"));
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
textarea.blur();
|
||||||
|
expect(textarea.style.width).toBe("792px");
|
||||||
|
expect(h.elements[0].width).toBe(1000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Test container-bound text", () => {
|
describe("Test container-bound text", () => {
|
||||||
let rectangle: any;
|
let rectangle: any;
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const DUMMY_HEIGHT = 240;
|
|
||||||
const DUMMY_WIDTH = 160;
|
|
||||||
const APPROX_LINE_HEIGHT = 25;
|
|
||||||
const INITIAL_WIDTH = 10;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest
|
|
||||||
.spyOn(textElementUtils, "getApproxLineHeight")
|
|
||||||
.mockReturnValue(APPROX_LINE_HEIGHT);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await render(<ExcalidrawApp />);
|
await render(<ExcalidrawApp />);
|
||||||
h.elements = [];
|
h.elements = [];
|
||||||
@@ -463,14 +489,21 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bind text to container when double clicked on center of filled container", async () => {
|
it("should bind text to container when double clicked inside filled container", async () => {
|
||||||
|
const rectangle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
backgroundColor: "red",
|
||||||
|
});
|
||||||
|
h.elements = [rectangle];
|
||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
expect(h.elements[0].id).toBe(rectangle.id);
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
|
|
||||||
mouse.doubleClickAt(
|
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||||
rectangle.x + rectangle.width / 2,
|
|
||||||
rectangle.y + rectangle.height / 2,
|
|
||||||
);
|
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
@@ -504,24 +537,37 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
h.elements = [rectangle];
|
h.elements = [rectangle];
|
||||||
|
|
||||||
|
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(null);
|
||||||
|
mouse.down();
|
||||||
|
let editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
mouse.doubleClickAt(
|
mouse.doubleClickAt(
|
||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(3);
|
||||||
|
|
||||||
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
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);
|
||||||
|
|
||||||
mouse.down();
|
mouse.down();
|
||||||
const editor = document.querySelector(
|
editor = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
expect(rectangle.boundElements).toStrictEqual([
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
{ id: text.id, type: "text" },
|
{ id: text.id, type: "text" },
|
||||||
]);
|
]);
|
||||||
@@ -551,6 +597,43 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should bind text to container when double clicked on container stroke", async () => {
|
||||||
|
const rectangle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 90,
|
||||||
|
height: 75,
|
||||||
|
strokeWidth: 4,
|
||||||
|
});
|
||||||
|
h.elements = [rectangle];
|
||||||
|
|
||||||
|
expect(h.elements.length).toBe(1);
|
||||||
|
expect(h.elements[0].id).toBe(rectangle.id);
|
||||||
|
|
||||||
|
mouse.doubleClickAt(rectangle.x + 2, rectangle.y + 2);
|
||||||
|
expect(h.elements.length).toBe(2);
|
||||||
|
|
||||||
|
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
|
expect(text.type).toBe("text");
|
||||||
|
expect(text.containerId).toBe(rectangle.id);
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
mouse.down();
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, { target: { value: "Hello World!" } });
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
editor.blur();
|
||||||
|
expect(rectangle.boundElements).toStrictEqual([
|
||||||
|
{ id: text.id, type: "text" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("shouldn't bind to non-text-bindable containers", async () => {
|
it("shouldn't bind to non-text-bindable containers", async () => {
|
||||||
const freedraw = API.createElement({
|
const freedraw = API.createElement({
|
||||||
type: "freedraw",
|
type: "freedraw",
|
||||||
@@ -586,11 +669,11 @@ describe("textWysiwyg", () => {
|
|||||||
["freedraw", "line"].forEach((type: any) => {
|
["freedraw", "line"].forEach((type: any) => {
|
||||||
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
|
||||||
h.elements = [];
|
h.elements = [];
|
||||||
const elemnet = UI.createElement(type, {
|
const element = UI.createElement(type, {
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 50,
|
height: 50,
|
||||||
});
|
});
|
||||||
API.setSelectedElements([elemnet]);
|
API.setSelectedElements([element]);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
});
|
});
|
||||||
@@ -675,39 +758,6 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should wrap text and vertcially center align once text submitted", async () => {
|
it("should wrap text and vertcially center align once text submitted", async () => {
|
||||||
jest
|
|
||||||
.spyOn(textElementUtils, "measureText")
|
|
||||||
.mockImplementation((text, font, maxWidth) => {
|
|
||||||
let width = INITIAL_WIDTH;
|
|
||||||
let height = APPROX_LINE_HEIGHT;
|
|
||||||
let baseline = 10;
|
|
||||||
if (!text) {
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
baseline = 30;
|
|
||||||
width = DUMMY_WIDTH;
|
|
||||||
if (text === "Hello \nWorld!") {
|
|
||||||
height = APPROX_LINE_HEIGHT * 2;
|
|
||||||
}
|
|
||||||
if (maxWidth) {
|
|
||||||
width = maxWidth;
|
|
||||||
// To capture cases where maxWidth passed is initial width
|
|
||||||
// due to which the text is not wrapped correctly
|
|
||||||
if (maxWidth === INITIAL_WIDTH) {
|
|
||||||
height = DUMMY_HEIGHT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(h.elements.length).toBe(1);
|
expect(h.elements.length).toBe(1);
|
||||||
|
|
||||||
Keyboard.keyDown(KEYS.ENTER);
|
Keyboard.keyDown(KEYS.ENTER);
|
||||||
@@ -716,11 +766,6 @@ describe("textWysiwyg", () => {
|
|||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
// mock scroll height
|
|
||||||
jest
|
|
||||||
.spyOn(editor, "scrollHeight", "get")
|
|
||||||
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
|
|
||||||
|
|
||||||
fireEvent.change(editor, {
|
fireEvent.change(editor, {
|
||||||
target: {
|
target: {
|
||||||
value: "Hello World!",
|
value: "Hello World!",
|
||||||
@@ -735,11 +780,11 @@ describe("textWysiwyg", () => {
|
|||||||
expect(text.text).toBe("Hello \nWorld!");
|
expect(text.text).toBe("Hello \nWorld!");
|
||||||
expect(text.originalText).toBe("Hello World!");
|
expect(text.originalText).toBe("Hello World!");
|
||||||
expect(text.y).toBe(
|
expect(text.y).toBe(
|
||||||
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
);
|
);
|
||||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
expect(text.x).toBe(25);
|
||||||
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
|
expect(text.height).toBe(48);
|
||||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
expect(text.width).toBe(60);
|
||||||
|
|
||||||
// Edit and text by removing second line and it should
|
// Edit and text by removing second line and it should
|
||||||
// still vertically align correctly
|
// still vertically align correctly
|
||||||
@@ -756,11 +801,6 @@ describe("textWysiwyg", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// mock scroll height
|
|
||||||
jest
|
|
||||||
.spyOn(editor, "scrollHeight", "get")
|
|
||||||
.mockImplementation(() => APPROX_LINE_HEIGHT);
|
|
||||||
editor.style.height = "25px";
|
|
||||||
editor.dispatchEvent(new Event("input"));
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
@@ -770,12 +810,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
expect(text.text).toBe("Hello");
|
expect(text.text).toBe("Hello");
|
||||||
expect(text.originalText).toBe("Hello");
|
expect(text.originalText).toBe("Hello");
|
||||||
|
expect(text.height).toBe(24);
|
||||||
|
expect(text.width).toBe(50);
|
||||||
expect(text.y).toBe(
|
expect(text.y).toBe(
|
||||||
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
|
rectangle.y + h.elements[0].height / 2 - text.height / 2,
|
||||||
);
|
);
|
||||||
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
|
expect(text.x).toBe(30);
|
||||||
expect(text.height).toBe(APPROX_LINE_HEIGHT);
|
|
||||||
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should unbind bound text when unbind action from context menu is triggered", async () => {
|
it("should unbind bound text when unbind action from context menu is triggered", async () => {
|
||||||
@@ -862,8 +902,8 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
109.5,
|
85,
|
||||||
17,
|
5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -877,6 +917,8 @@ describe("textWysiwyg", () => {
|
|||||||
editor.select();
|
editor.select();
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
@@ -887,7 +929,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
90,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@@ -910,7 +952,7 @@ describe("textWysiwyg", () => {
|
|||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
424,
|
375,
|
||||||
-539,
|
-539,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
@@ -947,26 +989,136 @@ describe("textWysiwyg", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should scale font size correctly when resizing using shift", async () => {
|
describe.only("when using shift resize", () => {
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
it("should wrap text correctly when resizing using shift from 'ne' handle", async () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
const editor = document.querySelector(
|
const editor = document.querySelector(
|
||||||
".excalidraw-textEditorContainer > textarea",
|
".excalidraw-textEditorContainer > textarea",
|
||||||
) as HTMLTextAreaElement;
|
) as HTMLTextAreaElement;
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
fireEvent.change(editor, { target: { value: "Hello" } });
|
fireEvent.change(editor, {
|
||||||
editor.blur();
|
target: { value: "Excalidraw is an opensource virtual whiteboard" },
|
||||||
const textElement = h.elements[1] as ExcalidrawTextElement;
|
});
|
||||||
expect(rectangle.width).toBe(90);
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(75);
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
expect(textElement.fontSize).toBe(20);
|
expect(rectangle.width).toBe(90);
|
||||||
|
expect(rectangle.height).toBe(202);
|
||||||
|
expect(textElement.fontSize).toBe(20);
|
||||||
|
expect(textElement.text).toBe(
|
||||||
|
`Excalid
|
||||||
|
raw is
|
||||||
|
an
|
||||||
|
opensou
|
||||||
|
rce
|
||||||
|
virtual
|
||||||
|
whitebo
|
||||||
|
ard`,
|
||||||
|
);
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 50], {
|
||||||
shift: true,
|
shift: true,
|
||||||
|
});
|
||||||
|
expect(rectangle.width).toBe(200);
|
||||||
|
expect(rectangle.height).toBe(449);
|
||||||
|
expect(textElement.fontSize).toBe(20);
|
||||||
|
expect(textElement.text).toBe(
|
||||||
|
`Excalidraw is an
|
||||||
|
opensource virtual
|
||||||
|
whiteboard`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wrap text correctly when resizing using shift vertically using 'n' handle", async () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: { value: "Excalidraw is an opensource virtual whiteboard" },
|
||||||
|
});
|
||||||
|
editor.blur();
|
||||||
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
|
expect(rectangle.width).toBe(90);
|
||||||
|
expect(rectangle.height).toBe(202);
|
||||||
|
expect(textElement.fontSize).toBe(20);
|
||||||
|
expect(textElement.text).toBe(
|
||||||
|
`Excalid
|
||||||
|
raw is
|
||||||
|
an
|
||||||
|
opensou
|
||||||
|
rce
|
||||||
|
virtual
|
||||||
|
whitebo
|
||||||
|
ard`,
|
||||||
|
);
|
||||||
|
|
||||||
|
resize(rectangle, "n", [rectangle.x + 30, rectangle.y - 50], {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
|
expect(rectangle.width).toBe(104);
|
||||||
|
expect(rectangle.height).toBe(232);
|
||||||
|
expect(textElement.fontSize).toBe(20);
|
||||||
|
expect(textElement.text).toBe(
|
||||||
|
`Excalid
|
||||||
|
raw is
|
||||||
|
an
|
||||||
|
opensou
|
||||||
|
rce
|
||||||
|
virtual
|
||||||
|
whitebo
|
||||||
|
ard`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wrap text correctly when resizing using shift horizontally and text overflows", async () => {
|
||||||
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
|
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: { value: "Excalidraw is an opensource virtual whiteboard" },
|
||||||
|
});
|
||||||
|
editor.blur();
|
||||||
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
|
expect(rectangle.width).toBe(90);
|
||||||
|
expect(rectangle.height).toBe(202);
|
||||||
|
expect(rectangle.y).toBe(20);
|
||||||
|
expect(textElement.fontSize).toBe(20);
|
||||||
|
expect(textElement.text).toBe(
|
||||||
|
`Excalid
|
||||||
|
raw is
|
||||||
|
an
|
||||||
|
opensou
|
||||||
|
rce
|
||||||
|
virtual
|
||||||
|
whitebo
|
||||||
|
ard`,
|
||||||
|
);
|
||||||
|
|
||||||
|
resize(rectangle, "e", [rectangle.x - 30, rectangle.y + 30], {
|
||||||
|
shift: true,
|
||||||
|
});
|
||||||
|
expect(rectangle.width).toBe(70);
|
||||||
|
expect(rectangle.height).toBe(226);
|
||||||
|
expect(rectangle.y).toBe(8);
|
||||||
|
expect(textElement.fontSize).toBe(20);
|
||||||
|
expect(textElement.text).toBe(
|
||||||
|
`Excal
|
||||||
|
idraw
|
||||||
|
is an
|
||||||
|
opens
|
||||||
|
ource
|
||||||
|
virtu
|
||||||
|
al
|
||||||
|
white
|
||||||
|
board`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
expect(rectangle.width).toBe(200);
|
|
||||||
expect(rectangle.height).toBe(166.66666666666669);
|
|
||||||
expect(textElement.fontSize).toBe(47.5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
it("should bind text correctly when container duplicated with alt-drag", async () => {
|
||||||
@@ -1025,9 +1177,9 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
|
||||||
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
mouse.up(rectangle.x + 100, rectangle.y + 50);
|
||||||
expect(rectangle.x).toBe(80);
|
expect(rectangle.x).toBe(80);
|
||||||
expect(rectangle.y).toBe(85);
|
expect(rectangle.y).toBe(-35);
|
||||||
expect(text.x).toBe(89.5);
|
expect(text.x).toBe(85);
|
||||||
expect(text.y).toBe(90);
|
expect(text.y).toBe(-30);
|
||||||
|
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
Keyboard.keyPress(KEYS.Z);
|
Keyboard.keyPress(KEYS.Z);
|
||||||
@@ -1057,29 +1209,6 @@ describe("textWysiwyg", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||||
jest
|
|
||||||
.spyOn(textElementUtils, "measureText")
|
|
||||||
.mockImplementation((text, font, maxWidth) => {
|
|
||||||
let width = INITIAL_WIDTH;
|
|
||||||
let height = APPROX_LINE_HEIGHT;
|
|
||||||
let baseline = 10;
|
|
||||||
if (!text) {
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
baseline = 30;
|
|
||||||
width = DUMMY_WIDTH;
|
|
||||||
height = APPROX_LINE_HEIGHT * 5;
|
|
||||||
|
|
||||||
return {
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
baseline,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const originalRectHeight = rectangle.height;
|
const originalRectHeight = rectangle.height;
|
||||||
expect(rectangle.height).toBe(originalRectHeight);
|
expect(rectangle.height).toBe(originalRectHeight);
|
||||||
|
|
||||||
@@ -1093,7 +1222,7 @@ describe("textWysiwyg", () => {
|
|||||||
target: { value: "Online whiteboard collaboration made easy" },
|
target: { value: "Online whiteboard collaboration made easy" },
|
||||||
});
|
});
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(135);
|
expect(rectangle.height).toBe(178);
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
@@ -1119,7 +1248,7 @@ describe("textWysiwyg", () => {
|
|||||||
editor.blur();
|
editor.blur();
|
||||||
|
|
||||||
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
|
||||||
expect(rectangle.height).toBe(215);
|
expect(rectangle.height).toBe(156);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
|
||||||
|
|
||||||
mouse.select(rectangle);
|
mouse.select(rectangle);
|
||||||
@@ -1131,13 +1260,12 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 0));
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
editor.blur();
|
editor.blur();
|
||||||
expect(rectangle.height).toBe(215);
|
expect(rectangle.height).toBe(156);
|
||||||
// cache updated again
|
// cache updated again
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(215);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
|
||||||
});
|
});
|
||||||
|
|
||||||
//@todo fix this test later once measureText is mocked correctly
|
it("should reset the container height cache when font properties updated", async () => {
|
||||||
it.skip("should reset the container height cache when font properties updated", async () => {
|
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
||||||
|
|
||||||
@@ -1163,7 +1291,9 @@ describe("textWysiwyg", () => {
|
|||||||
expect(
|
expect(
|
||||||
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
|
||||||
).toEqual(36);
|
).toEqual(36);
|
||||||
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
|
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(
|
||||||
|
96.39999999999999,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("should align correctly", () => {
|
describe("should align correctly", () => {
|
||||||
@@ -1191,7 +1321,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
20,
|
25,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -1201,8 +1331,8 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
94.5,
|
30,
|
||||||
20,
|
25,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
@@ -1212,22 +1342,22 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align top"));
|
fireEvent.click(screen.getByTitle("Align top"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
174,
|
45,
|
||||||
20,
|
25,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when center left", async () => {
|
it("when center left", async () => {
|
||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
fireEvent.click(screen.getByTitle("Left"));
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
25,
|
45.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when center center", async () => {
|
it("when center center", async () => {
|
||||||
@@ -1235,11 +1365,11 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
-25,
|
30,
|
||||||
25,
|
45.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when center right", async () => {
|
it("when center right", async () => {
|
||||||
@@ -1247,11 +1377,11 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Center vertically"));
|
fireEvent.click(screen.getByTitle("Center vertically"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
174,
|
45,
|
||||||
25,
|
45.5,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when bottom left", async () => {
|
it("when bottom left", async () => {
|
||||||
@@ -1259,34 +1389,120 @@ describe("textWysiwyg", () => {
|
|||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
|
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
15,
|
15,
|
||||||
25,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when bottom center", async () => {
|
it("when bottom center", async () => {
|
||||||
fireEvent.click(screen.getByTitle("Center"));
|
fireEvent.click(screen.getByTitle("Center"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
94.5,
|
30,
|
||||||
25,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("when bottom right", async () => {
|
it("when bottom right", async () => {
|
||||||
fireEvent.click(screen.getByTitle("Right"));
|
fireEvent.click(screen.getByTitle("Right"));
|
||||||
fireEvent.click(screen.getByTitle("Align bottom"));
|
fireEvent.click(screen.getByTitle("Align bottom"));
|
||||||
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
174,
|
45,
|
||||||
25,
|
66,
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
|
||||||
|
UI.clickTool("text");
|
||||||
|
mouse.clickAt(20, 30);
|
||||||
|
const editor = document.querySelector(
|
||||||
|
".excalidraw-textEditorContainer > textarea",
|
||||||
|
) as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
fireEvent.change(editor, {
|
||||||
|
target: {
|
||||||
|
value: "Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.dispatchEvent(new Event("input"));
|
||||||
|
await new Promise((cb) => setTimeout(cb, 0));
|
||||||
|
|
||||||
|
editor.select();
|
||||||
|
fireEvent.click(screen.getByTitle("Left"));
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
|
||||||
|
editor.blur();
|
||||||
|
|
||||||
|
const textElement = h.elements[1] as ExcalidrawTextElement;
|
||||||
|
expect(textElement.width).toBe(600);
|
||||||
|
expect(textElement.height).toBe(24);
|
||||||
|
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
|
||||||
|
expect((textElement as ExcalidrawTextElement).text).toBe(
|
||||||
|
"Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
|
);
|
||||||
|
|
||||||
|
API.setSelectedElements([textElement]);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||||
|
button: 2,
|
||||||
|
clientX: 20,
|
||||||
|
clientY: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
const contextMenu = document.querySelector(".context-menu");
|
||||||
|
fireEvent.click(
|
||||||
|
queryByText(contextMenu as HTMLElement, "Wrap text in a container")!,
|
||||||
|
);
|
||||||
|
expect(h.elements.length).toBe(3);
|
||||||
|
|
||||||
|
expect(h.elements[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
angle: 0,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
boundElements: [
|
||||||
|
{
|
||||||
|
id: h.elements[2].id,
|
||||||
|
type: "text",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
fillStyle: "hachure",
|
||||||
|
groupIds: [],
|
||||||
|
height: 34,
|
||||||
|
isDeleted: false,
|
||||||
|
link: null,
|
||||||
|
locked: false,
|
||||||
|
opacity: 100,
|
||||||
|
roughness: 1,
|
||||||
|
roundness: {
|
||||||
|
type: 3,
|
||||||
|
},
|
||||||
|
strokeColor: "#000000",
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeWidth: 1,
|
||||||
|
type: "rectangle",
|
||||||
|
updated: 1,
|
||||||
|
version: 1,
|
||||||
|
width: 610,
|
||||||
|
x: 15,
|
||||||
|
y: 25,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(h.elements[2] as ExcalidrawTextElement).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
text: "Excalidraw is an opensource virtual collaborative whiteboard",
|
||||||
|
verticalAlign: VERTICAL_ALIGN.MIDDLE,
|
||||||
|
textAlign: TEXT_ALIGN.LEFT,
|
||||||
|
boundElements: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+24
-56
@@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, VERTICAL_ALIGN } from "../constants";
|
import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@@ -24,13 +24,17 @@ import { mutateElement } from "./mutateElement";
|
|||||||
import {
|
import {
|
||||||
getApproxLineHeight,
|
getApproxLineHeight,
|
||||||
getBoundTextElementId,
|
getBoundTextElementId,
|
||||||
getBoundTextElementOffset,
|
computeBoundTextElementCoords,
|
||||||
getContainerDims,
|
getContainerDims,
|
||||||
getContainerElement,
|
getContainerElement,
|
||||||
getTextElementAngle,
|
getTextElementAngle,
|
||||||
getTextWidth,
|
getTextWidth,
|
||||||
|
measureText,
|
||||||
normalizeText,
|
normalizeText,
|
||||||
|
redrawTextBoundingBox,
|
||||||
wrapText,
|
wrapText,
|
||||||
|
getMaxContainerHeight,
|
||||||
|
getMaxContainerWidth,
|
||||||
} from "./textElement";
|
} from "./textElement";
|
||||||
import {
|
import {
|
||||||
actionDecreaseFontSize,
|
actionDecreaseFontSize,
|
||||||
@@ -38,7 +42,6 @@ import {
|
|||||||
} from "../actions/actionProperties";
|
} from "../actions/actionProperties";
|
||||||
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
||||||
import App from "../components/App";
|
import App from "../components/App";
|
||||||
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { parseClipboard } from "../clipboard";
|
import { parseClipboard } from "../clipboard";
|
||||||
|
|
||||||
@@ -157,7 +160,7 @@ export const textWysiwyg = ({
|
|||||||
let maxWidth = updatedTextElement.width;
|
let maxWidth = updatedTextElement.width;
|
||||||
|
|
||||||
let maxHeight = updatedTextElement.height;
|
let maxHeight = updatedTextElement.height;
|
||||||
const width = updatedTextElement.width;
|
let textElementWidth = updatedTextElement.width;
|
||||||
// Set to element height by default since that's
|
// Set to element height by default since that's
|
||||||
// what is going to be used for unbounded text
|
// what is going to be used for unbounded text
|
||||||
let textElementHeight = updatedTextElement.height;
|
let textElementHeight = updatedTextElement.height;
|
||||||
@@ -230,19 +233,17 @@ export const textWysiwyg = ({
|
|||||||
// Start pushing text upward until a diff of 30px (padding)
|
// Start pushing text upward until a diff of 30px (padding)
|
||||||
// is reached
|
// is reached
|
||||||
else {
|
else {
|
||||||
|
const containerCoords = computeBoundTextElementCoords(container);
|
||||||
|
|
||||||
// vertically center align the text
|
// vertically center align the text
|
||||||
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
|
||||||
if (!isArrowElement(container)) {
|
if (!isArrowElement(container)) {
|
||||||
coordY =
|
coordY =
|
||||||
container.y + containerDims.height / 2 - textElementHeight / 2;
|
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
|
||||||
coordY =
|
coordY = containerCoords.y + (maxHeight - textElementHeight);
|
||||||
container.y +
|
|
||||||
containerDims.height -
|
|
||||||
textElementHeight -
|
|
||||||
getBoundTextElementOffset(updatedTextElement);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -271,8 +272,11 @@ export const textWysiwyg = ({
|
|||||||
: updatedTextElement.height / lines.length;
|
: updatedTextElement.height / lines.length;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
|
||||||
|
textElementWidth = Math.min(textElementWidth, maxWidth);
|
||||||
|
} else if (isFirefox || isSafari) {
|
||||||
|
// As firefox, Safari needs little higher dimensions on DOM
|
||||||
|
textElementWidth += 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure text editor height doesn't go beyond viewport
|
// Make sure text editor height doesn't go beyond viewport
|
||||||
const editorMaxHeight =
|
const editorMaxHeight =
|
||||||
(appState.height - viewportY) / appState.zoom.value;
|
(appState.height - viewportY) / appState.zoom.value;
|
||||||
@@ -280,12 +284,12 @@ export const textWysiwyg = ({
|
|||||||
font: getFontString(updatedTextElement),
|
font: getFontString(updatedTextElement),
|
||||||
// must be defined *after* font ¯\_(ツ)_/¯
|
// must be defined *after* font ¯\_(ツ)_/¯
|
||||||
lineHeight: `${lineHeight}px`,
|
lineHeight: `${lineHeight}px`,
|
||||||
width: `${Math.min(width, maxWidth)}px`,
|
width: `${textElementWidth}px`,
|
||||||
height: `${textElementHeight}px`,
|
height: `${textElementHeight}px`,
|
||||||
left: `${viewportX}px`,
|
left: `${viewportX}px`,
|
||||||
top: `${viewportY}px`,
|
top: `${viewportY}px`,
|
||||||
transform: getTransform(
|
transform: getTransform(
|
||||||
width,
|
textElementWidth,
|
||||||
textElementHeight,
|
textElementHeight,
|
||||||
getTextElementAngle(updatedTextElement),
|
getTextElementAngle(updatedTextElement),
|
||||||
appState,
|
appState,
|
||||||
@@ -378,55 +382,16 @@ export const textWysiwyg = ({
|
|||||||
id,
|
id,
|
||||||
) as ExcalidrawTextElement;
|
) as ExcalidrawTextElement;
|
||||||
const font = getFontString(updatedTextElement);
|
const font = getFontString(updatedTextElement);
|
||||||
// using scrollHeight here since we need to calculate
|
if (isBoundToContainer(element)) {
|
||||||
// number of lines so cannot use editable.style.height
|
|
||||||
// as that gets updated below
|
|
||||||
// Rounding here so that the lines calculated is more accurate in all browsers.
|
|
||||||
// The scrollHeight and approxLineHeight differs in diff browsers
|
|
||||||
// eg it gives 1.05 in firefox for handewritten small font due to which
|
|
||||||
// height gets updated as lines > 1 and leads to jumping text for first line in bound container
|
|
||||||
// hence rounding here to avoid that
|
|
||||||
const lines = Math.round(
|
|
||||||
editable.scrollHeight / getApproxLineHeight(font),
|
|
||||||
);
|
|
||||||
// auto increase height only when lines > 1 so its
|
|
||||||
// measured correctly and vertically aligns for
|
|
||||||
// first line as well as setting height to "auto"
|
|
||||||
// doubles the height as soon as user starts typing
|
|
||||||
if (isBoundToContainer(element) && lines > 1) {
|
|
||||||
const container = getContainerElement(element);
|
const container = getContainerElement(element);
|
||||||
|
|
||||||
let height = "auto";
|
|
||||||
editable.style.height = "0px";
|
|
||||||
let heightSet = false;
|
|
||||||
if (lines === 2) {
|
|
||||||
const actualLineCount = wrapText(
|
|
||||||
editable.value,
|
|
||||||
font,
|
|
||||||
getMaxContainerWidth(container!),
|
|
||||||
).split("\n").length;
|
|
||||||
// This is browser behaviour when setting height to "auto"
|
|
||||||
// It sets the height needed for 2 lines even if actual
|
|
||||||
// line count is 1 as mentioned above as well
|
|
||||||
// hence reducing the height by half if actual line count is 1
|
|
||||||
// so single line aligns vertically when deleting
|
|
||||||
if (actualLineCount === 1) {
|
|
||||||
height = `${editable.scrollHeight / 2}px`;
|
|
||||||
editable.style.height = height;
|
|
||||||
heightSet = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const wrappedText = wrapText(
|
const wrappedText = wrapText(
|
||||||
normalizeText(editable.value),
|
normalizeText(editable.value),
|
||||||
font,
|
font,
|
||||||
getMaxContainerWidth(container!),
|
getMaxContainerWidth(container!),
|
||||||
);
|
);
|
||||||
const width = getTextWidth(wrappedText, font);
|
const { width, height } = measureText(wrappedText, font);
|
||||||
editable.style.width = `${width}px`;
|
editable.style.width = `${width}px`;
|
||||||
|
editable.style.height = `${height}px`;
|
||||||
if (!heightSet) {
|
|
||||||
editable.style.height = `${editable.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
onChange(normalizeText(editable.value));
|
onChange(normalizeText(editable.value));
|
||||||
};
|
};
|
||||||
@@ -463,7 +428,9 @@ export const textWysiwyg = ({
|
|||||||
event.code === CODES.BRACKET_RIGHT))
|
event.code === CODES.BRACKET_RIGHT))
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
if (event.isComposing) {
|
||||||
|
return;
|
||||||
|
} else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
||||||
outdent();
|
outdent();
|
||||||
} else {
|
} else {
|
||||||
indent();
|
indent();
|
||||||
@@ -612,6 +579,7 @@ export const textWysiwyg = ({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
redrawTextBoundingBox(updateElement, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit({
|
onSubmit({
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { API } from "../tests/helpers/api";
|
||||||
|
import { hasBoundTextElement } from "./typeChecks";
|
||||||
|
|
||||||
|
describe("Test TypeChecks", () => {
|
||||||
|
describe("Test hasBoundTextElement", () => {
|
||||||
|
it("should return true for text bindable containers with bound text", () => {
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "ellipse",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "arrow",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "image",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for text bindable containers without bound text", () => {
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "freedraw",
|
||||||
|
boundElements: [{ type: "arrow", id: "arrow-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for non text bindable containers", () => {
|
||||||
|
expect(
|
||||||
|
hasBoundTextElement(
|
||||||
|
API.createElement({
|
||||||
|
type: "freedraw",
|
||||||
|
boundElements: [{ type: "text", id: "text-id" }],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ROUNDNESS } from "../constants";
|
import { ROUNDNESS } from "../constants";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
|
import { MarkNonNullable } from "../utility-types";
|
||||||
import {
|
import {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawTextElement,
|
ExcalidrawTextElement,
|
||||||
@@ -137,9 +138,9 @@ export const isExcalidrawElement = (element: any): boolean => {
|
|||||||
|
|
||||||
export const hasBoundTextElement = (
|
export const hasBoundTextElement = (
|
||||||
element: ExcalidrawElement | null,
|
element: ExcalidrawElement | null,
|
||||||
): element is ExcalidrawBindableElement => {
|
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
|
||||||
return (
|
return (
|
||||||
isBindableElement(element) &&
|
isTextBindableContainer(element) &&
|
||||||
!!element.boundElements?.some(({ type }) => type === "text")
|
!!element.boundElements?.some(({ type }) => type === "text")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
THEME,
|
THEME,
|
||||||
VERTICAL_ALIGN,
|
VERTICAL_ALIGN,
|
||||||
} from "../constants";
|
} from "../constants";
|
||||||
|
import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||||
|
|
||||||
export type ChartType = "bar" | "line";
|
export type ChartType = "bar" | "line";
|
||||||
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
export type FillStyle = "hachure" | "cross-hatch" | "solid";
|
||||||
@@ -130,7 +131,6 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
|
|||||||
fontSize: number;
|
fontSize: number;
|
||||||
fontFamily: FontFamilyValues;
|
fontFamily: FontFamilyValues;
|
||||||
text: string;
|
text: string;
|
||||||
baseline: number;
|
|
||||||
textAlign: TextAlign;
|
textAlign: TextAlign;
|
||||||
verticalAlign: VerticalAlign;
|
verticalAlign: VerticalAlign;
|
||||||
containerId: ExcalidrawGenericElement["id"] | null;
|
containerId: ExcalidrawGenericElement["id"] | null;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { unstable_createStore } from "jotai";
|
||||||
|
|
||||||
|
export const appJotaiStore = unstable_createStore();
|
||||||
@@ -70,11 +70,12 @@ import { decryptData } from "../../data/encryption";
|
|||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
import { LocalData } from "../data/LocalData";
|
import { LocalData } from "../data/LocalData";
|
||||||
import { atom, useAtom } from "jotai";
|
import { atom, useAtom } from "jotai";
|
||||||
import { jotaiStore } from "../../jotai";
|
import { appJotaiStore } from "../app-jotai";
|
||||||
|
|
||||||
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
export const collabAPIAtom = atom<CollabAPI | null>(null);
|
||||||
export const collabDialogShownAtom = atom(false);
|
export const collabDialogShownAtom = atom(false);
|
||||||
export const isCollaboratingAtom = atom(false);
|
export const isCollaboratingAtom = atom(false);
|
||||||
|
export const isOfflineAtom = atom(false);
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
@@ -152,6 +153,8 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||||
|
window.addEventListener("online", this.onOfflineStatusToggle);
|
||||||
|
window.addEventListener("offline", this.onOfflineStatusToggle);
|
||||||
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
window.addEventListener(EVENT.UNLOAD, this.onUnload);
|
||||||
|
|
||||||
const collabAPI: CollabAPI = {
|
const collabAPI: CollabAPI = {
|
||||||
@@ -164,7 +167,8 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
setUsername: this.setUsername,
|
setUsername: this.setUsername,
|
||||||
};
|
};
|
||||||
|
|
||||||
jotaiStore.set(collabAPIAtom, collabAPI);
|
appJotaiStore.set(collabAPIAtom, collabAPI);
|
||||||
|
this.onOfflineStatusToggle();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV === ENV.TEST ||
|
process.env.NODE_ENV === ENV.TEST ||
|
||||||
@@ -180,7 +184,13 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOfflineStatusToggle = () => {
|
||||||
|
appJotaiStore.set(isOfflineAtom, !window.navigator.onLine);
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
window.removeEventListener("online", this.onOfflineStatusToggle);
|
||||||
|
window.removeEventListener("offline", this.onOfflineStatusToggle);
|
||||||
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
|
||||||
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
|
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
|
||||||
window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
|
window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
|
||||||
@@ -198,10 +208,10 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
|
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
|
||||||
|
|
||||||
private setIsCollaborating = (isCollaborating: boolean) => {
|
private setIsCollaborating = (isCollaborating: boolean) => {
|
||||||
jotaiStore.set(isCollaboratingAtom, isCollaborating);
|
appJotaiStore.set(isCollaboratingAtom, isCollaborating);
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUnload = () => {
|
private onUnload = () => {
|
||||||
@@ -600,7 +610,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
const localElements = this.getSceneElementsIncludingDeleted();
|
const localElements = this.getSceneElementsIncludingDeleted();
|
||||||
const appState = this.excalidrawAPI.getAppState();
|
const appState = this.excalidrawAPI.getAppState();
|
||||||
|
|
||||||
remoteElements = restoreElements(remoteElements, null, false);
|
remoteElements = restoreElements(remoteElements, null);
|
||||||
|
|
||||||
const reconciledElements = _reconcileElements(
|
const reconciledElements = _reconcileElements(
|
||||||
localElements,
|
localElements,
|
||||||
@@ -794,7 +804,7 @@ class Collab extends PureComponent<Props, CollabState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
handleClose = () => {
|
handleClose = () => {
|
||||||
jotaiStore.set(collabDialogShownAtom, false);
|
appJotaiStore.set(collabDialogShownAtom, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
setUsername = (username: string) => {
|
setUsername = (username: string) => {
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ import {
|
|||||||
shareWindows,
|
shareWindows,
|
||||||
} from "../../components/icons";
|
} from "../../components/icons";
|
||||||
import { ToolButton } from "../../components/ToolButton";
|
import { ToolButton } from "../../components/ToolButton";
|
||||||
import { t } from "../../i18n";
|
|
||||||
import "./RoomDialog.scss";
|
import "./RoomDialog.scss";
|
||||||
import Stack from "../../components/Stack";
|
import Stack from "../../components/Stack";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
import { trackEvent } from "../../analytics";
|
import { trackEvent } from "../../analytics";
|
||||||
import { getFrame } from "../../utils";
|
import { getFrame } from "../../utils";
|
||||||
import DialogActionButton from "../../components/DialogActionButton";
|
import DialogActionButton from "../../components/DialogActionButton";
|
||||||
|
import { useI18n } from "../../i18n";
|
||||||
|
|
||||||
const getShareIcon = () => {
|
const getShareIcon = () => {
|
||||||
const navigator = window.navigator as any;
|
const navigator = window.navigator as any;
|
||||||
@@ -51,6 +51,7 @@ const RoomDialog = ({
|
|||||||
setErrorMessage: (message: string) => void;
|
setErrorMessage: (message: string) => void;
|
||||||
theme: AppState["theme"];
|
theme: AppState["theme"];
|
||||||
}) => {
|
}) => {
|
||||||
|
const { t } = useI18n();
|
||||||
const roomLinkInput = useRef<HTMLInputElement>(null);
|
const roomLinkInput = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const copyRoomLink = async () => {
|
const copyRoomLink = async () => {
|
||||||
@@ -144,7 +145,7 @@ const RoomDialog = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
value={username || ""}
|
value={username.trim() || ""}
|
||||||
className="RoomDialog-username TextInput"
|
className="RoomDialog-username TextInput"
|
||||||
onChange={(event) => onUsernameChange(event.target.value)}
|
onChange={(event) => onUsernameChange(event.target.value)}
|
||||||
onKeyPress={(event) => event.key === "Enter" && handleClose()}
|
onKeyPress={(event) => event.key === "Enter" && handleClose()}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { PRECEDING_ELEMENT_KEY } from "../../constants";
|
import { PRECEDING_ELEMENT_KEY } from "../../constants";
|
||||||
import { ExcalidrawElement } from "../../element/types";
|
import { ExcalidrawElement } from "../../element/types";
|
||||||
import { AppState } from "../../types";
|
import { AppState } from "../../types";
|
||||||
|
import { arrayToMapWithIndex } from "../../utils";
|
||||||
|
|
||||||
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
export type ReconciledElements = readonly ExcalidrawElement[] & {
|
||||||
_brand: "reconciledElements";
|
_brand: "reconciledElements";
|
||||||
@@ -33,30 +34,13 @@ const shouldDiscardRemoteElement = (
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getElementsMapWithIndex = <T extends ExcalidrawElement>(
|
|
||||||
elements: readonly T[],
|
|
||||||
) =>
|
|
||||||
elements.reduce(
|
|
||||||
(
|
|
||||||
acc: {
|
|
||||||
[key: string]: [element: T, index: number] | undefined;
|
|
||||||
},
|
|
||||||
element: T,
|
|
||||||
idx,
|
|
||||||
) => {
|
|
||||||
acc[element.id] = [element, idx];
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const reconcileElements = (
|
export const reconcileElements = (
|
||||||
localElements: readonly ExcalidrawElement[],
|
localElements: readonly ExcalidrawElement[],
|
||||||
remoteElements: readonly BroadcastedExcalidrawElement[],
|
remoteElements: readonly BroadcastedExcalidrawElement[],
|
||||||
localAppState: AppState,
|
localAppState: AppState,
|
||||||
): ReconciledElements => {
|
): ReconciledElements => {
|
||||||
const localElementsData =
|
const localElementsData =
|
||||||
getElementsMapWithIndex<ExcalidrawElement>(localElements);
|
arrayToMapWithIndex<ExcalidrawElement>(localElements);
|
||||||
|
|
||||||
const reconciledElements: ExcalidrawElement[] = localElements.slice();
|
const reconciledElements: ExcalidrawElement[] = localElements.slice();
|
||||||
|
|
||||||
@@ -69,7 +53,7 @@ export const reconcileElements = (
|
|||||||
for (const remoteElement of remoteElements) {
|
for (const remoteElement of remoteElements) {
|
||||||
remoteElementIdx++;
|
remoteElementIdx++;
|
||||||
|
|
||||||
const local = localElementsData[remoteElement.id];
|
const local = localElementsData.get(remoteElement.id);
|
||||||
|
|
||||||
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
|
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
|
||||||
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
|
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
|
||||||
@@ -105,21 +89,21 @@ export const reconcileElements = (
|
|||||||
offset++;
|
offset++;
|
||||||
if (cursor === 0) {
|
if (cursor === 0) {
|
||||||
reconciledElements.unshift(remoteElement);
|
reconciledElements.unshift(remoteElement);
|
||||||
localElementsData[remoteElement.id] = [
|
localElementsData.set(remoteElement.id, [
|
||||||
remoteElement,
|
remoteElement,
|
||||||
cursor - offset,
|
cursor - offset,
|
||||||
];
|
]);
|
||||||
} else {
|
} else {
|
||||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
||||||
localElementsData[remoteElement.id] = [
|
localElementsData.set(remoteElement.id, [
|
||||||
remoteElement,
|
remoteElement,
|
||||||
cursor + 1 - offset,
|
cursor + 1 - offset,
|
||||||
];
|
]);
|
||||||
cursor++;
|
cursor++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let idx = localElementsData[parent]
|
let idx = localElementsData.has(parent)
|
||||||
? localElementsData[parent]![1]
|
? localElementsData.get(parent)![1]
|
||||||
: null;
|
: null;
|
||||||
if (idx != null) {
|
if (idx != null) {
|
||||||
idx += offset;
|
idx += offset;
|
||||||
@@ -127,38 +111,38 @@ export const reconcileElements = (
|
|||||||
if (idx != null && idx >= cursor) {
|
if (idx != null && idx >= cursor) {
|
||||||
reconciledElements.splice(idx + 1, 0, remoteElement);
|
reconciledElements.splice(idx + 1, 0, remoteElement);
|
||||||
offset++;
|
offset++;
|
||||||
localElementsData[remoteElement.id] = [
|
localElementsData.set(remoteElement.id, [
|
||||||
remoteElement,
|
remoteElement,
|
||||||
idx + 1 - offset,
|
idx + 1 - offset,
|
||||||
];
|
]);
|
||||||
cursor = idx + 1;
|
cursor = idx + 1;
|
||||||
} else if (idx != null) {
|
} else if (idx != null) {
|
||||||
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
reconciledElements.splice(cursor + 1, 0, remoteElement);
|
||||||
offset++;
|
offset++;
|
||||||
localElementsData[remoteElement.id] = [
|
localElementsData.set(remoteElement.id, [
|
||||||
remoteElement,
|
remoteElement,
|
||||||
cursor + 1 - offset,
|
cursor + 1 - offset,
|
||||||
];
|
]);
|
||||||
cursor++;
|
cursor++;
|
||||||
} else {
|
} else {
|
||||||
reconciledElements.push(remoteElement);
|
reconciledElements.push(remoteElement);
|
||||||
localElementsData[remoteElement.id] = [
|
localElementsData.set(remoteElement.id, [
|
||||||
remoteElement,
|
remoteElement,
|
||||||
reconciledElements.length - 1 - offset,
|
reconciledElements.length - 1 - offset,
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// no parent z-index information, local element exists → replace in place
|
// no parent z-index information, local element exists → replace in place
|
||||||
} else if (local) {
|
} else if (local) {
|
||||||
reconciledElements[local[1]] = remoteElement;
|
reconciledElements[local[1]] = remoteElement;
|
||||||
localElementsData[remoteElement.id] = [remoteElement, local[1]];
|
localElementsData.set(remoteElement.id, [remoteElement, local[1]]);
|
||||||
// otherwise push to the end
|
// otherwise push to the end
|
||||||
} else {
|
} else {
|
||||||
reconciledElements.push(remoteElement);
|
reconciledElements.push(remoteElement);
|
||||||
localElementsData[remoteElement.id] = [
|
localElementsData.set(remoteElement.id, [
|
||||||
remoteElement,
|
remoteElement,
|
||||||
reconciledElements.length - 1 - offset,
|
reconciledElements.length - 1 - offset,
|
||||||
];
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { PlusPromoIcon } from "../../components/icons";
|
import { PlusPromoIcon } from "../../components/icons";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
import { WelcomeScreen } from "../../packages/excalidraw/index";
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
export const AppWelcomeScreen: React.FC<{
|
export const AppWelcomeScreen: React.FC<{
|
||||||
setCollabDialogShown: (toggle: boolean) => any;
|
setCollabDialogShown: (toggle: boolean) => any;
|
||||||
}> = React.memo((props) => {
|
}> = React.memo((props) => {
|
||||||
|
const { t } = useI18n();
|
||||||
let headingContent;
|
let headingContent;
|
||||||
|
|
||||||
if (isExcalidrawPlusSignedUser) {
|
if (isExcalidrawPlusSignedUser) {
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { shield } from "../../components/icons";
|
import { shield } from "../../components/icons";
|
||||||
import { Tooltip } from "../../components/Tooltip";
|
import { Tooltip } from "../../components/Tooltip";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
|
|
||||||
export const EncryptedIcon = () => (
|
export const EncryptedIcon = () => {
|
||||||
<a
|
const { t } = useI18n();
|
||||||
className="encrypted-icon tooltip"
|
|
||||||
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
return (
|
||||||
target="_blank"
|
<a
|
||||||
rel="noopener noreferrer"
|
className="encrypted-icon tooltip"
|
||||||
aria-label={t("encrypted.link")}
|
href="https://blog.excalidraw.com/end-to-end-encryption/"
|
||||||
>
|
target="_blank"
|
||||||
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
rel="noopener noreferrer"
|
||||||
{shield}
|
aria-label={t("encrypted.link")}
|
||||||
</Tooltip>
|
>
|
||||||
</a>
|
<Tooltip label={t("encrypted.tooltip")} long={true}>
|
||||||
);
|
{shield}
|
||||||
|
</Tooltip>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
|
|||||||
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { t } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import { excalidrawPlusIcon } from "./icons";
|
import { excalidrawPlusIcon } from "./icons";
|
||||||
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
import { encryptData, generateEncryptionKey } from "../../data/encryption";
|
||||||
import { isInitializedImageElement } from "../../element/typeChecks";
|
import { isInitializedImageElement } from "../../element/typeChecks";
|
||||||
@@ -79,6 +79,7 @@ export const ExportToExcalidrawPlus: React.FC<{
|
|||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
onError: (error: Error) => void;
|
onError: (error: Error) => void;
|
||||||
}> = ({ elements, appState, files, onError }) => {
|
}> = ({ elements, appState, files, onError }) => {
|
||||||
|
const { t } = useI18n();
|
||||||
return (
|
return (
|
||||||
<Card color="primary">
|
<Card color="primary">
|
||||||
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
<div className="Card-icon">{excalidrawPlusIcon}</div>
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
import { useAtom } from "jotai";
|
import { useSetAtom } from "jotai";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { langCodeAtom } from "..";
|
import { appLangCodeAtom } from "..";
|
||||||
import * as i18n from "../../i18n";
|
import { defaultLang, useI18n } from "../../i18n";
|
||||||
import { languages } from "../../i18n";
|
import { languages } from "../../i18n";
|
||||||
|
|
||||||
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
|
||||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
const { t, langCode } = useI18n();
|
||||||
|
const setLangCode = useSetAtom(appLangCodeAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
className="dropdown-select dropdown-select__language"
|
className="dropdown-select dropdown-select__language"
|
||||||
onChange={({ target }) => setLangCode(target.value)}
|
onChange={({ target }) => setLangCode(target.value)}
|
||||||
value={langCode}
|
value={langCode}
|
||||||
aria-label={i18n.t("buttons.selectLanguage")}
|
aria-label={t("buttons.selectLanguage")}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<option key={i18n.defaultLang.code} value={i18n.defaultLang.code}>
|
<option key={defaultLang.code} value={defaultLang.code}>
|
||||||
{i18n.defaultLang.label}
|
{defaultLang.label}
|
||||||
</option>
|
</option>
|
||||||
{languages.map((lang) => (
|
{languages.map((lang) => (
|
||||||
<option key={lang.code} value={lang.code}>
|
<option key={lang.code} value={lang.code}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { encryptData, decryptData } from "../../data/encryption";
|
|||||||
import { MIME_TYPES } from "../../constants";
|
import { MIME_TYPES } from "../../constants";
|
||||||
import { reconcileElements } from "../collab/reconciliation";
|
import { reconcileElements } from "../collab/reconciliation";
|
||||||
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
import { getSyncableElements, SyncableExcalidrawElement } from ".";
|
||||||
|
import { ResolutionType } from "../../utility-types";
|
||||||
|
|
||||||
// private
|
// private
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -263,9 +263,12 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
|
{ repairBindings: true },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null);
|
data = restore(localDataState || null, null, null, {
|
||||||
|
repairBindings: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -45,6 +45,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collab-offline-warning {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 6.5rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.5;
|
||||||
|
border-radius: var(--border-radius-md);
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
color: var(--color-text-warning);
|
||||||
|
z-index: 6;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.excalidraw-app.is-collaborating {
|
.excalidraw-app.is-collaborating {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import Collab, {
|
|||||||
collabAPIAtom,
|
collabAPIAtom,
|
||||||
collabDialogShownAtom,
|
collabDialogShownAtom,
|
||||||
isCollaboratingAtom,
|
isCollaboratingAtom,
|
||||||
|
isOfflineAtom,
|
||||||
} from "./collab/Collab";
|
} from "./collab/Collab";
|
||||||
import {
|
import {
|
||||||
exportToBackend,
|
exportToBackend,
|
||||||
@@ -66,10 +67,7 @@ import {
|
|||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
|
|
||||||
import "./index.scss";
|
|
||||||
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
||||||
|
|
||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../element/typeChecks";
|
import { isInitializedImageElement } from "../element/typeChecks";
|
||||||
@@ -77,13 +75,17 @@ import { loadFilesFromFirebase } from "./data/firebase";
|
|||||||
import { LocalData } from "./data/LocalData";
|
import { LocalData } from "./data/LocalData";
|
||||||
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { atom, Provider, useAtom } from "jotai";
|
|
||||||
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
|
|
||||||
import { reconcileElements } from "./collab/reconciliation";
|
import { reconcileElements } from "./collab/reconciliation";
|
||||||
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
|
||||||
import { AppMainMenu } from "./components/AppMainMenu";
|
import { AppMainMenu } from "./components/AppMainMenu";
|
||||||
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
|
||||||
import { AppFooter } from "./components/AppFooter";
|
import { AppFooter } from "./components/AppFooter";
|
||||||
|
import { atom, Provider, useAtom, useAtomValue } from "jotai";
|
||||||
|
import { useAtomWithInitialValue } from "../jotai";
|
||||||
|
import { appJotaiStore } from "./app-jotai";
|
||||||
|
|
||||||
|
import "./index.scss";
|
||||||
|
import { ResolutionType } from "../utility-types";
|
||||||
|
|
||||||
polyfill();
|
polyfill();
|
||||||
|
|
||||||
@@ -226,15 +228,15 @@ const initializeScene = async (opts: {
|
|||||||
return { scene: null, isExternalScene: false };
|
return { scene: null, isExternalScene: false };
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentLangCode = languageDetector.detect() || defaultLang.code;
|
const detectedLangCode = languageDetector.detect() || defaultLang.code;
|
||||||
|
export const appLangCodeAtom = atom(
|
||||||
export const langCodeAtom = atom(
|
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
|
||||||
Array.isArray(currentLangCode) ? currentLangCode[0] : currentLangCode,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const ExcalidrawWrapper = () => {
|
const ExcalidrawWrapper = () => {
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [langCode, setLangCode] = useAtom(langCodeAtom);
|
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
|
||||||
|
|
||||||
// initial state
|
// initial state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -362,7 +364,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
if (data.scene) {
|
if (data.scene) {
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
...data.scene,
|
...data.scene,
|
||||||
...restore(data.scene, null, null),
|
...restore(data.scene, null, null, { repairBindings: true }),
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -599,6 +601,8 @@ const ExcalidrawWrapper = () => {
|
|||||||
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isOffline = useAtomValue(isOfflineAtom);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ height: "100%" }}
|
style={{ height: "100%" }}
|
||||||
@@ -661,6 +665,11 @@ const ExcalidrawWrapper = () => {
|
|||||||
/>
|
/>
|
||||||
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
|
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
|
||||||
<AppFooter />
|
<AppFooter />
|
||||||
|
{isCollaborating && isOffline && (
|
||||||
|
<div className="collab-offline-warning">
|
||||||
|
{t("alerts.collabOfflineWarning")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Excalidraw>
|
</Excalidraw>
|
||||||
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
@@ -676,7 +685,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
const ExcalidrawApp = () => {
|
const ExcalidrawApp = () => {
|
||||||
return (
|
return (
|
||||||
<TopErrorBoundary>
|
<TopErrorBoundary>
|
||||||
<Provider unstable_createStore={() => jotaiStore}>
|
<Provider unstable_createStore={() => appJotaiStore}>
|
||||||
<ExcalidrawWrapper />
|
<ExcalidrawWrapper />
|
||||||
</Provider>
|
</Provider>
|
||||||
</TopErrorBoundary>
|
</TopErrorBoundary>
|
||||||
|
|||||||
Vendored
-47
@@ -50,36 +50,6 @@ interface Clipboard extends EventTarget {
|
|||||||
write(data: any[]): Promise<void>;
|
write(data: any[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutable<T> = {
|
|
||||||
-readonly [P in keyof T]: T[P];
|
|
||||||
};
|
|
||||||
|
|
||||||
type ValueOf<T> = T[keyof T];
|
|
||||||
|
|
||||||
type Merge<M, N> = Omit<M, keyof N> & N;
|
|
||||||
|
|
||||||
/** utility type to assert that the second type is a subtype of the first type.
|
|
||||||
* Returns the subtype. */
|
|
||||||
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
|
|
||||||
|
|
||||||
type ResolutionType<T extends (...args: any) => any> = T extends (
|
|
||||||
...args: any
|
|
||||||
) => Promise<infer R>
|
|
||||||
? R
|
|
||||||
: any;
|
|
||||||
|
|
||||||
// https://github.com/krzkaczor/ts-essentials
|
|
||||||
type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
||||||
|
|
||||||
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
|
|
||||||
Required<Pick<T, RK>>;
|
|
||||||
|
|
||||||
type MarkNonNullable<T, K extends keyof T> = {
|
|
||||||
[P in K]-?: P extends K ? NonNullable<T[P]> : T[P];
|
|
||||||
} & { [P in keyof T]: T[P] };
|
|
||||||
|
|
||||||
type NonOptional<T> = Exclude<T, undefined>;
|
|
||||||
|
|
||||||
// PNG encoding/decoding
|
// PNG encoding/decoding
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
|
||||||
@@ -101,23 +71,6 @@ declare module "png-chunks-extract" {
|
|||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
// type getter for interface's callable type
|
|
||||||
// src: https://stackoverflow.com/a/58658851/927631
|
|
||||||
// -----------------------------------------------------------------------------
|
|
||||||
type SignatureType<T> = T extends (...args: infer R) => any ? R : never;
|
|
||||||
type CallableType<T extends (...args: any[]) => any> = (
|
|
||||||
...args: SignatureType<T>
|
|
||||||
) => ReturnType<T>;
|
|
||||||
// --------------------------------------------------------------------------—
|
|
||||||
|
|
||||||
// Type for React.forwardRef --- supply only the first generic argument T
|
|
||||||
type ForwardRef<T, P = any> = Parameters<
|
|
||||||
CallableType<React.ForwardRefRenderFunction<T, P>>
|
|
||||||
>[1];
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------------—
|
|
||||||
|
|
||||||
interface Blob {
|
interface Blob {
|
||||||
handle?: import("browser-fs-acces").FileSystemHandle;
|
handle?: import("browser-fs-acces").FileSystemHandle;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { AppState } from "./types";
|
|||||||
import { ExcalidrawElement } from "./element/types";
|
import { ExcalidrawElement } from "./element/types";
|
||||||
import { isLinearElement } from "./element/typeChecks";
|
import { isLinearElement } from "./element/typeChecks";
|
||||||
import { deepCopyElement } from "./element/newElement";
|
import { deepCopyElement } from "./element/newElement";
|
||||||
|
import { Mutable } from "./utility-types";
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
|
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
|
||||||
|
|||||||
+16
@@ -1,6 +1,8 @@
|
|||||||
import fallbackLangData from "./locales/en.json";
|
import fallbackLangData from "./locales/en.json";
|
||||||
import percentages from "./locales/percentages.json";
|
import percentages from "./locales/percentages.json";
|
||||||
import { ENV } from "./constants";
|
import { ENV } from "./constants";
|
||||||
|
import { jotaiScope, jotaiStore } from "./jotai";
|
||||||
|
import { atom, useAtomValue } from "jotai";
|
||||||
|
|
||||||
const COMPLETION_THRESHOLD = 85;
|
const COMPLETION_THRESHOLD = 85;
|
||||||
|
|
||||||
@@ -99,6 +101,8 @@ export const setLanguage = async (lang: Language) => {
|
|||||||
currentLangData = fallbackLangData;
|
currentLangData = fallbackLangData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jotaiStore.set(editorLangCodeAtom, lang.code);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLanguage = () => currentLang;
|
export const getLanguage = () => currentLang;
|
||||||
@@ -143,3 +147,15 @@ export const t = (
|
|||||||
}
|
}
|
||||||
return translation;
|
return translation;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @private atom used solely to rerender components using `useI18n` hook */
|
||||||
|
const editorLangCodeAtom = atom(defaultLang.code);
|
||||||
|
|
||||||
|
// Should be used in components that fall under these cases:
|
||||||
|
// - component is rendered as an <Excalidraw> child
|
||||||
|
// - component is rendered internally by <Excalidraw>, but the component
|
||||||
|
// is memoized w/o being updated on `langCode`, `AppState`, or `UIAppState`
|
||||||
|
export const useI18n = () => {
|
||||||
|
const langCode = useAtomValue(editorLangCodeAtom, jotaiScope);
|
||||||
|
return { t, langCode };
|
||||||
|
};
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
|
import { PrimitiveAtom, unstable_createStore, useAtom } from "jotai";
|
||||||
import { useLayoutEffect } from "react";
|
import { useLayoutEffect } from "react";
|
||||||
|
|
||||||
export const jotaiScope = Symbol();
|
export const jotaiScope = Symbol();
|
||||||
@@ -6,7 +6,7 @@ export const jotaiStore = unstable_createStore();
|
|||||||
|
|
||||||
export const useAtomWithInitialValue = <
|
export const useAtomWithInitialValue = <
|
||||||
T extends unknown,
|
T extends unknown,
|
||||||
A extends WritableAtom<T, T>,
|
A extends PrimitiveAtom<T>,
|
||||||
>(
|
>(
|
||||||
atom: A,
|
atom: A,
|
||||||
initialValue: T | (() => T),
|
initialValue: T | (() => T),
|
||||||
|
|||||||
+29
-26
@@ -109,35 +109,35 @@
|
|||||||
"decreaseFontSize": "تصغير حجم الخط",
|
"decreaseFontSize": "تصغير حجم الخط",
|
||||||
"increaseFontSize": "تكبير حجم الخط",
|
"increaseFontSize": "تكبير حجم الخط",
|
||||||
"unbindText": "فك ربط النص",
|
"unbindText": "فك ربط النص",
|
||||||
"bindText": "",
|
"bindText": "ربط النص بالحاوية",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "تعديل الرابط",
|
"edit": "تعديل الرابط",
|
||||||
"create": "إنشاء رابط",
|
"create": "إنشاء رابط",
|
||||||
"label": "رابط"
|
"label": "رابط"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "تحرير السطر",
|
||||||
"exit": ""
|
"exit": "الخروج من المُحرر"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "قفل",
|
||||||
"unlock": "",
|
"unlock": "فتح",
|
||||||
"lockAll": "",
|
"lockAll": "قفل الكل",
|
||||||
"unlockAll": ""
|
"unlockAll": "فتح الكل"
|
||||||
},
|
},
|
||||||
"statusPublished": "",
|
"statusPublished": "نُشر",
|
||||||
"sidebarLock": ""
|
"sidebarLock": "إبقاء الشريط الجانبي مفتوح"
|
||||||
},
|
},
|
||||||
"library": {
|
"library": {
|
||||||
"noItems": "",
|
"noItems": "لا توجد عناصر أضيفت بعد...",
|
||||||
"hint_emptyLibrary": "",
|
"hint_emptyLibrary": "حدد عنصر على القماش لإضافته هنا، أو تثبيت مكتبة من المستودع العام أدناه.",
|
||||||
"hint_emptyPrivateLibrary": ""
|
"hint_emptyPrivateLibrary": "حدد عنصر على القماش لإضافته هنا."
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "إعادة تعيين اللوحة",
|
"clearReset": "إعادة تعيين اللوحة",
|
||||||
"exportJSON": "صدر الملف",
|
"exportJSON": "صدر الملف",
|
||||||
"exportImage": "",
|
"exportImage": "تصدير الصورة...",
|
||||||
"export": "",
|
"export": "حفظ إلى...",
|
||||||
"exportToPng": "تصدير بصيغة PNG",
|
"exportToPng": "تصدير بصيغة PNG",
|
||||||
"exportToSvg": "تصدير بصيغة SVG",
|
"exportToSvg": "تصدير بصيغة SVG",
|
||||||
"copyToClipboard": "نسخ إلى الحافظة",
|
"copyToClipboard": "نسخ إلى الحافظة",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"couldNotLoadInvalidFile": "تعذر التحميل، الملف غير صالح",
|
"couldNotLoadInvalidFile": "تعذر التحميل، الملف غير صالح",
|
||||||
"importBackendFailed": "فشل الاستيراد من الخادوم.",
|
"importBackendFailed": "فشل الاستيراد من الخادوم.",
|
||||||
"cannotExportEmptyCanvas": "لا يمكن تصدير لوحة فارغة.",
|
"cannotExportEmptyCanvas": "لا يمكن تصدير لوحة فارغة.",
|
||||||
"couldNotCopyToClipboard": "",
|
"couldNotCopyToClipboard": "تعذر النسخ إلى الحافظة.",
|
||||||
"decryptFailed": "تعذر فك تشفير البيانات.",
|
"decryptFailed": "تعذر فك تشفير البيانات.",
|
||||||
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
|
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
|
||||||
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
|
||||||
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
|
||||||
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
|
||||||
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
|
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
"unsupportedFileType": "نوع الملف غير مدعوم.",
|
||||||
@@ -200,10 +201,10 @@
|
|||||||
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
|
||||||
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
|
||||||
"invalidSVGString": "SVG غير صالح.",
|
"invalidSVGString": "SVG غير صالح.",
|
||||||
"cannotResolveCollabServer": "",
|
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
|
||||||
"importLibraryError": "",
|
"importLibraryError": "تعذر تحميل المكتبة",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
|
||||||
"collabSaveFailed_sizeExceeded": ""
|
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "تحديد",
|
"selection": "تحديد",
|
||||||
@@ -217,9 +218,10 @@
|
|||||||
"text": "نص",
|
"text": "نص",
|
||||||
"library": "مكتبة",
|
"library": "مكتبة",
|
||||||
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم",
|
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم",
|
||||||
"penMode": "",
|
"penMode": "وضع القلم - امنع اللمس",
|
||||||
"link": "",
|
"link": "إضافة/تحديث الرابط للشكل المحدد",
|
||||||
"eraser": "ممحاة"
|
"eraser": "ممحاة",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "إجراءات اللوحة",
|
"canvasActions": "إجراءات اللوحة",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "الأشكال"
|
"shapes": "الأشكال"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "لتحريك لوحة الرسم ، استمر في الضغط على عجلة الماوس أو مفتاح المسافة أثناء السحب",
|
"canvasPanning": "لتحريك القماش، اضغط على عجلة الفأرة أو مفتاح المسافة أثناء السحب، أو استخدم أداة اليد",
|
||||||
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
|
||||||
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
|
||||||
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
|
||||||
@@ -238,14 +240,15 @@
|
|||||||
"resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز",
|
"resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز",
|
||||||
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
|
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
|
||||||
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
|
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
|
||||||
"lineEditor_pointSelected": "",
|
"lineEditor_pointSelected": "",
|
||||||
"lineEditor_nothingSelected": "",
|
"lineEditor_nothingSelected": "",
|
||||||
"placeImage": "",
|
"placeImage": "",
|
||||||
"publishLibrary": "نشر مكتبتك",
|
"publishLibrary": "نشر مكتبتك",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "تعذر عرض المعاينة",
|
"cannotShowPreview": "تعذر عرض المعاينة",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
"unsupportedFileType": "Този файлов формат не се поддържа.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Поддържайте избрания инструмент активен след рисуване",
|
"lock": "Поддържайте избрания инструмент активен след рисуване",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": ""
|
"eraser": "",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Действия по платното",
|
"canvasActions": "Действия по платното",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "Натиснете Enter, за да добавите",
|
"bindTextToElement": "Натиснете Enter, за да добавите",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Невъзможност за показване на preview",
|
"cannotShowPreview": "Невъзможност за показване на preview",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷",
|
"invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷",
|
||||||
"resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?",
|
"resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?",
|
||||||
"removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?",
|
"removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?",
|
||||||
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।"
|
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "অসমর্থিত ফাইল।",
|
"unsupportedFileType": "অসমর্থিত ফাইল।",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "আঁকার পরে নির্বাচিত টুল সক্রিয় রাখুন",
|
"lock": "আঁকার পরে নির্বাচিত টুল সক্রিয় রাখুন",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
|
||||||
"eraser": "ঝাড়ন"
|
"eraser": "ঝাড়ন",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "ক্যানভাস কার্যকলাপ",
|
"canvasActions": "ক্যানভাস কার্যকলাপ",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "আকার(গুলি)"
|
"shapes": "আকার(গুলি)"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "ক্যানভাস সরানোর জন্য মাউস হুইল বা স্পেসবার ধরে টানুন",
|
"canvasPanning": "",
|
||||||
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
|
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
|
||||||
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
|
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
|
||||||
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
|
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "আপনার নিজস্ব সংগ্রহ প্রকাশ করুন",
|
"publishLibrary": "আপনার নিজস্ব সংগ্রহ প্রকাশ করুন",
|
||||||
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
|
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন"
|
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
|
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
|
||||||
|
|||||||
+29
-26
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Enganxa",
|
"paste": "Enganxa",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Enganxar com a text pla",
|
||||||
"pasteCharts": "Enganxa els diagrames",
|
"pasteCharts": "Enganxa els diagrames",
|
||||||
"selectAll": "Selecciona-ho tot",
|
"selectAll": "Selecciona-ho tot",
|
||||||
"multiSelect": "Afegeix un element a la selecció",
|
"multiSelect": "Afegeix un element a la selecció",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"layers": "Capes",
|
"layers": "Capes",
|
||||||
"actions": "Accions",
|
"actions": "Accions",
|
||||||
"language": "Llengua",
|
"language": "Llengua",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Col·laboració en directe...",
|
||||||
"duplicateSelection": "Duplica",
|
"duplicateSelection": "Duplica",
|
||||||
"untitled": "Sense títol",
|
"untitled": "Sense títol",
|
||||||
"name": "Nom",
|
"name": "Nom",
|
||||||
@@ -116,8 +116,8 @@
|
|||||||
"label": "Enllaç"
|
"label": "Enllaç"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "Editar línia",
|
||||||
"exit": ""
|
"exit": "Sortir de l'editor de línia"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "Bloca",
|
"lock": "Bloca",
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Neteja el llenç",
|
"clearReset": "Neteja el llenç",
|
||||||
"exportJSON": "Exporta a un fitxer",
|
"exportJSON": "Exporta a un fitxer",
|
||||||
"exportImage": "",
|
"exportImage": "Exporta la imatge...",
|
||||||
"export": "",
|
"export": "Guardar a...",
|
||||||
"exportToPng": "Exporta a PNG",
|
"exportToPng": "Exporta a PNG",
|
||||||
"exportToSvg": "Exporta a SNG",
|
"exportToSvg": "Exporta a SNG",
|
||||||
"copyToClipboard": "Copia al porta-retalls",
|
"copyToClipboard": "Copia al porta-retalls",
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"scale": "Escala",
|
"scale": "Escala",
|
||||||
"save": "Desa al fitxer actual",
|
"save": "Desa al fitxer actual",
|
||||||
"saveAs": "Anomena i desa",
|
"saveAs": "Anomena i desa",
|
||||||
"load": "",
|
"load": "Obrir",
|
||||||
"getShareableLink": "Obté l'enllaç per a compartir",
|
"getShareableLink": "Obté l'enllaç per a compartir",
|
||||||
"close": "Tanca",
|
"close": "Tanca",
|
||||||
"selectLanguage": "Trieu la llengua",
|
"selectLanguage": "Trieu la llengua",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
|
||||||
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
|
||||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
|
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
|
||||||
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
|
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
|
||||||
|
"collabOfflineWarning": "Sense connexió a internet disponible.\nEls vostres canvis no seran guardats!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipus de fitxer no suportat.",
|
"unsupportedFileType": "Tipus de fitxer no suportat.",
|
||||||
@@ -202,8 +203,8 @@
|
|||||||
"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",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.",
|
||||||
"collabSaveFailed_sizeExceeded": ""
|
"collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Selecció",
|
"selection": "Selecció",
|
||||||
@@ -217,9 +218,10 @@
|
|||||||
"text": "Text",
|
"text": "Text",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
|
||||||
"penMode": "",
|
"penMode": "Mode de llapis - evita tocar",
|
||||||
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
|
||||||
"eraser": "Esborrador"
|
"eraser": "Esborrador",
|
||||||
|
"hand": "Mà (eina de desplaçament)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del llenç",
|
"canvasActions": "Accions del llenç",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formes"
|
"shapes": "Formes"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Per a moure el llenç, mantingueu premuda la roda del ratolí o la tecla espai mentre l'arrossegueu",
|
"canvasPanning": "Per moure el llenç, manteniu premuda la roda del ratolí o la barra espaiadora mentre arrossegueu o utilitzeu l'eina manual",
|
||||||
"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ó",
|
||||||
@@ -238,14 +240,15 @@
|
|||||||
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
|
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
|
||||||
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
|
||||||
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Mantingueu premut Ctrl o Cmd i feu doble clic o premeu Ctrl o Cmd + Retorn per editar els punts",
|
||||||
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
|
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
|
||||||
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
|
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
|
||||||
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
|
||||||
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
|
||||||
"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\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No es pot mostrar la previsualització",
|
"cannotShowPreview": "No es pot mostrar la previsualització",
|
||||||
@@ -293,7 +296,7 @@
|
|||||||
"blog": "Llegiu el nostre blog",
|
"blog": "Llegiu el nostre blog",
|
||||||
"click": "clic",
|
"click": "clic",
|
||||||
"deepSelect": "Selecció profunda",
|
"deepSelect": "Selecció profunda",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "Seleccioneu profundament dins del quadre i eviteu arrossegar",
|
||||||
"curvedArrow": "Fletxa corba",
|
"curvedArrow": "Fletxa corba",
|
||||||
"curvedLine": "Línia corba",
|
"curvedLine": "Línia corba",
|
||||||
"documentation": "Documentació",
|
"documentation": "Documentació",
|
||||||
@@ -314,8 +317,8 @@
|
|||||||
"zoomToFit": "Zoom per veure tots els elements",
|
"zoomToFit": "Zoom per veure tots els elements",
|
||||||
"zoomToSelection": "Zoom per veure la selecció",
|
"zoomToSelection": "Zoom per veure la selecció",
|
||||||
"toggleElementLock": "Blocar/desblocar la selecció",
|
"toggleElementLock": "Blocar/desblocar la selecció",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Mou la pàgina cap amunt/a baix",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Mou la pàgina cap a l'esquerra/dreta"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Neteja el llenç"
|
"title": "Neteja el llenç"
|
||||||
@@ -397,7 +400,7 @@
|
|||||||
"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": ""
|
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Blanc",
|
"ffffff": "Blanc",
|
||||||
@@ -448,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Totes les vostres dades es guarden localment al vostre navegador.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Vols anar a Excalidraw+ en comptes?",
|
||||||
"menuHint": ""
|
"menuHint": "Exportar, preferències, llenguatges..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Exportar, preferències i més...",
|
||||||
"center_heading": "",
|
"center_heading": "Diagrames. Fer. Simple.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Selecciona una eina i comença a dibuixar!",
|
||||||
"helpHint": ""
|
"helpHint": "Dreceres i ajuda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "",
|
"lock": "",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "Guma"
|
"eraser": "Guma",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "",
|
"lock": "",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": ""
|
"eraser": "",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
|
||||||
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
|
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
|
||||||
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
|
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
|
||||||
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
|
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
|
||||||
|
"collabOfflineWarning": "Keine Internetverbindung verfügbar.\nDeine Änderungen werden nicht gespeichert!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nicht unterstützter Dateityp.",
|
"unsupportedFileType": "Nicht unterstützter Dateityp.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen",
|
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen",
|
||||||
"penMode": "Stift-Modus - Berührung verhindern",
|
"penMode": "Stift-Modus - Berührung verhindern",
|
||||||
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
|
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
|
||||||
"eraser": "Radierer"
|
"eraser": "Radierer",
|
||||||
|
"hand": "Hand (Schwenkwerkzeug)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Aktionen für Zeichenfläche",
|
"canvasActions": "Aktionen für Zeichenfläche",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formen"
|
"shapes": "Formen"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens",
|
"canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens, oder verwende das Hand-Werkzeug",
|
||||||
"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",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Veröffentliche deine eigene Bibliothek",
|
"publishLibrary": "Veröffentliche deine eigene Bibliothek",
|
||||||
"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\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
|
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
|
||||||
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
|
||||||
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
|
||||||
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
|
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
|
||||||
|
"collabOfflineWarning": "Δεν υπάρχει διαθέσιμη σύνδεση στο internet.\nΟι αλλαγές σας δεν θα αποθηκευτούν!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
|
||||||
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
|
||||||
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
|
||||||
"eraser": "Γόμα"
|
"eraser": "Γόμα",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ενέργειες καμβά",
|
"canvasActions": "Ενέργειες καμβά",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Σχήματα"
|
"shapes": "Σχήματα"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Για να μετακινήσετε καμβά, κρατήστε πατημένο τον τροχό του ποντικιού ή το πλήκτρο διαστήματος ενώ σύρετε",
|
"canvasPanning": "",
|
||||||
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
|
||||||
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
|
||||||
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
|
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
|
||||||
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
|
||||||
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
|
||||||
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή"
|
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή",
|
||||||
|
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
|
||||||
|
|||||||
+3
-1
@@ -110,6 +110,7 @@
|
|||||||
"increaseFontSize": "Increase font size",
|
"increaseFontSize": "Increase font size",
|
||||||
"unbindText": "Unbind text",
|
"unbindText": "Unbind text",
|
||||||
"bindText": "Bind text to the container",
|
"bindText": "Bind text to the container",
|
||||||
|
"createContainerFromText": "Wrap text in a container",
|
||||||
"link": {
|
"link": {
|
||||||
"edit": "Edit link",
|
"edit": "Edit link",
|
||||||
"create": "Create link",
|
"create": "Create link",
|
||||||
@@ -193,7 +194,8 @@
|
|||||||
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
|
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
|
||||||
"resetLibrary": "This will clear your library. Are you sure?",
|
"resetLibrary": "This will clear your library. Are you sure?",
|
||||||
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
|
||||||
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
|
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
|
||||||
|
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Unsupported file type.",
|
"unsupportedFileType": "Unsupported file type.",
|
||||||
|
|||||||
+14
-11
@@ -103,7 +103,7 @@
|
|||||||
"share": "Compartir",
|
"share": "Compartir",
|
||||||
"showStroke": "Mostrar selector de color de trazo",
|
"showStroke": "Mostrar selector de color de trazo",
|
||||||
"showBackground": "Mostrar el selector de color de fondo",
|
"showBackground": "Mostrar el selector de color de fondo",
|
||||||
"toggleTheme": "Alternar tema",
|
"toggleTheme": "Cambiar tema",
|
||||||
"personalLib": "Biblioteca personal",
|
"personalLib": "Biblioteca personal",
|
||||||
"excalidrawLib": "Biblioteca Excalidraw",
|
"excalidrawLib": "Biblioteca Excalidraw",
|
||||||
"decreaseFontSize": "Disminuir tamaño de letra",
|
"decreaseFontSize": "Disminuir tamaño de letra",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
|
||||||
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
|
||||||
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
|
||||||
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
|
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
|
||||||
|
"collabOfflineWarning": "No hay conexión a internet disponible.\n¡No se guardarán los cambios!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de archivo no admitido.",
|
"unsupportedFileType": "Tipo de archivo no admitido.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
|
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
|
||||||
"penMode": "Modo Lápiz - previene toque",
|
"penMode": "Modo Lápiz - previene toque",
|
||||||
"link": "Añadir/Actualizar enlace para una forma seleccionada",
|
"link": "Añadir/Actualizar enlace para una forma seleccionada",
|
||||||
"eraser": "Borrar"
|
"eraser": "Borrar",
|
||||||
|
"hand": "Mano (herramienta de panoramización)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acciones del lienzo",
|
"canvasActions": "Acciones del lienzo",
|
||||||
@@ -227,12 +229,12 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra de espacio mientras arrastra",
|
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra espaciadora mientras arrastra o utilice la herramienta de mano",
|
||||||
"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",
|
||||||
"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 CtrlOrCmd+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",
|
||||||
"lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT",
|
"lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT",
|
||||||
"resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro",
|
"resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publica tu propia biblioteca",
|
"publishLibrary": "Publica tu propia biblioteca",
|
||||||
"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\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "No se puede mostrar la vista previa",
|
"cannotShowPreview": "No se puede mostrar la vista previa",
|
||||||
@@ -312,7 +315,7 @@
|
|||||||
"title": "Ayuda",
|
"title": "Ayuda",
|
||||||
"view": "Vista",
|
"view": "Vista",
|
||||||
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
|
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
|
||||||
"zoomToSelection": "Zoom a la selección",
|
"zoomToSelection": "Ampliar selección",
|
||||||
"toggleElementLock": "Bloquear/desbloquear selección",
|
"toggleElementLock": "Bloquear/desbloquear selección",
|
||||||
"movePageUpDown": "Mover página hacia arriba/abajo",
|
"movePageUpDown": "Mover página hacia arriba/abajo",
|
||||||
"movePageLeftRight": "Mover página hacia la izquierda/derecha"
|
"movePageLeftRight": "Mover página hacia la izquierda/derecha"
|
||||||
@@ -324,9 +327,9 @@
|
|||||||
"title": "Publicar biblioteca",
|
"title": "Publicar biblioteca",
|
||||||
"itemName": "Nombre del artículo",
|
"itemName": "Nombre del artículo",
|
||||||
"authorName": "Nombre del autor",
|
"authorName": "Nombre del autor",
|
||||||
"githubUsername": "Nombre de usuario de Github",
|
"githubUsername": "Nombre de usuario de GitHub",
|
||||||
"twitterUsername": "Nombre de usuario de Twitter",
|
"twitterUsername": "Nombre de usuario de Twitter",
|
||||||
"libraryName": "Nombre de la librería",
|
"libraryName": "Nombre de la biblioteca",
|
||||||
"libraryDesc": "Descripción de la biblioteca",
|
"libraryDesc": "Descripción de la biblioteca",
|
||||||
"website": "Sitio Web",
|
"website": "Sitio Web",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
@@ -334,7 +337,7 @@
|
|||||||
"libraryName": "Nombre de tu biblioteca",
|
"libraryName": "Nombre de tu biblioteca",
|
||||||
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
|
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
|
||||||
"githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión",
|
"githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión",
|
||||||
"twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
|
"twitterHandle": "Nombre de usuario de Twitter (opcional), así sabemos a quién acreditar cuando se promociona en Twitter",
|
||||||
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
|
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -456,7 +459,7 @@
|
|||||||
"menuHint": "Exportar, preferencias y más...",
|
"menuHint": "Exportar, preferencias y más...",
|
||||||
"center_heading": "Diagramas. Hecho. Simplemente.",
|
"center_heading": "Diagramas. Hecho. Simplemente.",
|
||||||
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
|
||||||
"helpHint": "Atajos & ayuda"
|
"helpHint": "Atajos y ayuda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
|
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
|
||||||
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
|
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
|
||||||
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
|
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
|
||||||
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
|
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
|
||||||
|
"collabOfflineWarning": "Ez dago Interneteko konexiorik.\nZure aldaketak ez dira gordeko!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
|
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
|
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
|
||||||
"penMode": "Luma modua - ukipena saihestu",
|
"penMode": "Luma modua - ukipena saihestu",
|
||||||
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
|
||||||
"eraser": "Borragoma"
|
"eraser": "Borragoma",
|
||||||
|
"hand": "Eskua (panoratze tresna)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvas ekintzak",
|
"canvasActions": "Canvas ekintzak",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formak"
|
"shapes": "Formak"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Oihala mugitzeko, sakatu saguaren gurpila edo zuriune-barra arrastatzean",
|
"canvasPanning": "Oihala mugitzeko, eutsi saguaren gurpila edo zuriune-barra arrastatzean, edo erabili esku tresna",
|
||||||
"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",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Argitaratu zure liburutegia",
|
"publishLibrary": "Argitaratu zure liburutegia",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
"cannotShowPreview": "Ezin da oihala aurreikusi",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
|
||||||
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
|
||||||
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
|
||||||
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
|
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
|
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
|
||||||
"penMode": "حالت قلم - جلوگیری از تماس",
|
"penMode": "حالت قلم - جلوگیری از تماس",
|
||||||
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
"link": "افزودن/بهروزرسانی پیوند برای شکل انتخابی",
|
||||||
"eraser": "پاک کن"
|
"eraser": "پاک کن",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "عملیات روی بوم",
|
"canvasActions": "عملیات روی بوم",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "شکلها"
|
"shapes": "شکلها"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
|
"canvasPanning": "",
|
||||||
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
|
||||||
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
|
||||||
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "کتابخانه خود را منتشر کنید",
|
"publishLibrary": "کتابخانه خود را منتشر کنید",
|
||||||
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
"bindTextToElement": "برای افزودن اینتر را بزنید",
|
||||||
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
|
||||||
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
|
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
|
||||||
|
|||||||
+34
-31
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"labels": {
|
"labels": {
|
||||||
"paste": "Liitä",
|
"paste": "Liitä",
|
||||||
"pasteAsPlaintext": "",
|
"pasteAsPlaintext": "Liitä pelkkänä tekstinä",
|
||||||
"pasteCharts": "Liitä kaaviot",
|
"pasteCharts": "Liitä kaaviot",
|
||||||
"selectAll": "Valitse kaikki",
|
"selectAll": "Valitse kaikki",
|
||||||
"multiSelect": "Lisää kohde valintaan",
|
"multiSelect": "Lisää kohde valintaan",
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
"layers": "Tasot",
|
"layers": "Tasot",
|
||||||
"actions": "Toiminnot",
|
"actions": "Toiminnot",
|
||||||
"language": "Kieli",
|
"language": "Kieli",
|
||||||
"liveCollaboration": "",
|
"liveCollaboration": "Live Yhteistyö...",
|
||||||
"duplicateSelection": "Monista",
|
"duplicateSelection": "Monista",
|
||||||
"untitled": "Nimetön",
|
"untitled": "Nimetön",
|
||||||
"name": "Nimi",
|
"name": "Nimi",
|
||||||
@@ -116,14 +116,14 @@
|
|||||||
"label": "Linkki"
|
"label": "Linkki"
|
||||||
},
|
},
|
||||||
"lineEditor": {
|
"lineEditor": {
|
||||||
"edit": "",
|
"edit": "Muokkaa riviä",
|
||||||
"exit": ""
|
"exit": "Poistu rivieditorista"
|
||||||
},
|
},
|
||||||
"elementLock": {
|
"elementLock": {
|
||||||
"lock": "",
|
"lock": "Lukitse",
|
||||||
"unlock": "",
|
"unlock": "Poista lukitus",
|
||||||
"lockAll": "",
|
"lockAll": "Lukitse kaikki",
|
||||||
"unlockAll": ""
|
"unlockAll": "Poista lukitus kaikista"
|
||||||
},
|
},
|
||||||
"statusPublished": "Julkaistu",
|
"statusPublished": "Julkaistu",
|
||||||
"sidebarLock": "Pidä sivupalkki avoinna"
|
"sidebarLock": "Pidä sivupalkki avoinna"
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
"buttons": {
|
"buttons": {
|
||||||
"clearReset": "Tyhjennä piirtoalue",
|
"clearReset": "Tyhjennä piirtoalue",
|
||||||
"exportJSON": "Vie tiedostoon",
|
"exportJSON": "Vie tiedostoon",
|
||||||
"exportImage": "",
|
"exportImage": "Vie kuva...",
|
||||||
"export": "",
|
"export": "Tallenna nimellä...",
|
||||||
"exportToPng": "Vie PNG-tiedostona",
|
"exportToPng": "Vie PNG-tiedostona",
|
||||||
"exportToSvg": "Vie SVG-tiedostona",
|
"exportToSvg": "Vie SVG-tiedostona",
|
||||||
"copyToClipboard": "Kopioi leikepöydälle",
|
"copyToClipboard": "Kopioi leikepöydälle",
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
"scale": "Koko",
|
"scale": "Koko",
|
||||||
"save": "Tallenna nykyiseen tiedostoon",
|
"save": "Tallenna nykyiseen tiedostoon",
|
||||||
"saveAs": "Tallenna nimellä",
|
"saveAs": "Tallenna nimellä",
|
||||||
"load": "",
|
"load": "Avaa",
|
||||||
"getShareableLink": "Hae jaettava linkki",
|
"getShareableLink": "Hae jaettava linkki",
|
||||||
"close": "Sulje",
|
"close": "Sulje",
|
||||||
"selectLanguage": "Valitse kieli",
|
"selectLanguage": "Valitse kieli",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
|
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
|
||||||
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
|
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
|
||||||
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
|
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
|
||||||
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
|
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
|
||||||
|
"collabOfflineWarning": "Internet-yhteyttä ei ole saatavilla.\nMuutoksiasi ei tallenneta!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
|
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
|
||||||
@@ -201,9 +202,9 @@
|
|||||||
"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ä.",
|
||||||
"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": "",
|
"importLibraryError": "Kokoelman lataaminen epäonnistui",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
|
||||||
"collabSaveFailed_sizeExceeded": ""
|
"collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Valinta",
|
"selection": "Valinta",
|
||||||
@@ -217,9 +218,10 @@
|
|||||||
"text": "Teksti",
|
"text": "Teksti",
|
||||||
"library": "Kirjasto",
|
"library": "Kirjasto",
|
||||||
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
|
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
|
||||||
"penMode": "",
|
"penMode": "Kynätila - estä kosketus",
|
||||||
"link": "Lisää/päivitä linkki valitulle muodolle",
|
"link": "Lisää/päivitä linkki valitulle muodolle",
|
||||||
"eraser": "Poistotyökalu"
|
"eraser": "Poistotyökalu",
|
||||||
|
"hand": "Käsi (panning-työkalu)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Piirtoalueen toiminnot",
|
"canvasActions": "Piirtoalueen toiminnot",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Muodot"
|
"shapes": "Muodot"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Liikuttaaksesi piirtoaluetta, raahaa hiiren vieritysrulla tai välilyöntinäppäin alaspainettuna",
|
"canvasPanning": "Piirtoalueen liikuttamiseksi pidä hiiren pyörää tai välilyöntiä pohjassa tai käytä käsityökalua",
|
||||||
"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",
|
||||||
@@ -238,14 +240,15 @@
|
|||||||
"resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen",
|
"resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen",
|
||||||
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
|
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
|
||||||
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
|
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Pidä CtrlOrCmd pohjassa ja kaksoisnapsauta tai paina CtrlOrCmd + Enter muokataksesi pisteitä",
|
||||||
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
|
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
|
||||||
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
|
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
|
||||||
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
|
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
|
||||||
"publishLibrary": "Julkaise oma kirjasto",
|
"publishLibrary": "Julkaise oma kirjasto",
|
||||||
"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."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
"cannotShowPreview": "Esikatselua ei voitu näyttää",
|
||||||
@@ -313,9 +316,9 @@
|
|||||||
"view": "Näkymä",
|
"view": "Näkymä",
|
||||||
"zoomToFit": "Näytä kaikki elementit",
|
"zoomToFit": "Näytä kaikki elementit",
|
||||||
"zoomToSelection": "Näytä valinta",
|
"zoomToSelection": "Näytä valinta",
|
||||||
"toggleElementLock": "",
|
"toggleElementLock": "Lukitse / poista lukitus valinta",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Siirrä sivua ylös/alas",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Siirrä sivua vasemmalle/oikealle"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Pyyhi piirtoalue"
|
"title": "Pyyhi piirtoalue"
|
||||||
@@ -397,7 +400,7 @@
|
|||||||
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
|
||||||
"canvas": "piirtoalue",
|
"canvas": "piirtoalue",
|
||||||
"selection": "valinta",
|
"selection": "valinta",
|
||||||
"pasteAsSingleElement": ""
|
"pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin"
|
||||||
},
|
},
|
||||||
"colors": {
|
"colors": {
|
||||||
"ffffff": "Valkoinen",
|
"ffffff": "Valkoinen",
|
||||||
@@ -448,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Kaikki tietosi on tallennettu paikallisesti selaimellesi.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Haluatko sen sijaan mennä Excalidraw+:aan?",
|
||||||
"menuHint": ""
|
"menuHint": "Vie, asetukset, kielet, ..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Vie, asetukset ja lisää...",
|
||||||
"center_heading": "",
|
"center_heading": "Kaaviot. Tehty. Yksinkertaiseksi.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Valitse työkalu ja aloita piirtäminen!",
|
||||||
"helpHint": ""
|
"helpHint": "Pikanäppäimet & ohje"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"exportEmbedScene": "Intégrer la scène",
|
"exportEmbedScene": "Intégrer la scène",
|
||||||
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
|
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
|
||||||
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
|
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
|
||||||
"handDrawn": "Manuscrit",
|
"handDrawn": "À main levée",
|
||||||
"normal": "Normale",
|
"normal": "Normale",
|
||||||
"code": "Code",
|
"code": "Code",
|
||||||
"small": "Petite",
|
"small": "Petite",
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
|
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
|
||||||
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
|
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
|
||||||
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
|
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
|
||||||
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
|
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
|
||||||
|
"collabOfflineWarning": "Aucune connexion internet disponible.\nVos modifications ne seront pas enregistrées !"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Type de fichier non supporté.",
|
"unsupportedFileType": "Type de fichier non supporté.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Garder l'outil sélectionné actif après le dessin",
|
"lock": "Garder l'outil sélectionné actif après le dessin",
|
||||||
"penMode": "Mode stylo - évite le toucher",
|
"penMode": "Mode stylo - évite le toucher",
|
||||||
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
|
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
|
||||||
"eraser": "Gomme"
|
"eraser": "Gomme",
|
||||||
|
"hand": "Mains (outil de déplacement de la vue)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Actions du canevas",
|
"canvasActions": "Actions du canevas",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formes"
|
"shapes": "Formes"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser",
|
"canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser, ou utiliser l'outil main.",
|
||||||
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
|
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
|
||||||
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
|
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
|
||||||
"text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
|
"text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publier votre propre bibliothèque",
|
"publishLibrary": "Publier votre propre bibliothèque",
|
||||||
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
|
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
|
||||||
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
|
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
|
||||||
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression"
|
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression",
|
||||||
|
"firefox_clipboard_write": "Cette fonctionnalité devrait pouvoir être activée en définissant l'option \"dom.events.asyncClipboard.clipboard.clipboardItem\" à \"true\". Pour modifier les paramètres du navigateur dans Firefox, visitez la page \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Impossible d’afficher l’aperçu",
|
"cannotShowPreview": "Impossible d’afficher l’aperçu",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.",
|
"invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.",
|
||||||
"resetLibrary": "Isto limpará a súa biblioteca. Está seguro?",
|
"resetLibrary": "Isto limpará a súa biblioteca. Está seguro?",
|
||||||
"removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?",
|
"removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?",
|
||||||
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada."
|
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de ficheiro non soportado.",
|
"unsupportedFileType": "Tipo de ficheiro non soportado.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
|
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
|
||||||
"penMode": "Modo lapis - evitar o contacto",
|
"penMode": "Modo lapis - evitar o contacto",
|
||||||
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
|
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
|
||||||
"eraser": "Goma de borrar"
|
"eraser": "Goma de borrar",
|
||||||
|
"hand": "Man (ferramenta de desprazamento)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accións do lenzo",
|
"canvasActions": "Accións do lenzo",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Para mover o lenzo, manteña a roda do rato ou a barra de espazo mentres arrastra",
|
"canvasPanning": "Para mover o lenzo, manteña pulsada a roda do rato ou a barra de espazo mentres arrastra, ou utilice a ferramenta da man",
|
||||||
"linearElement": "Faga clic para iniciar varios puntos, arrastre para unha sola liña",
|
"linearElement": "Faga clic para iniciar varios puntos, arrastre para unha sola liña",
|
||||||
"freeDraw": "Fai clic e arrastra, solta cando acabes",
|
"freeDraw": "Fai clic e arrastra, solta cando acabes",
|
||||||
"text": "Consello: tamén podes engadir texto facendo dobre-clic en calquera lugar coa ferramenta de selección",
|
"text": "Consello: tamén podes engadir texto facendo dobre-clic en calquera lugar coa ferramenta de selección",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publica a túa propia biblioteca",
|
"publishLibrary": "Publica a túa propia biblioteca",
|
||||||
"bindTextToElement": "Prema a tecla enter para engadir texto",
|
"bindTextToElement": "Prema a tecla enter para engadir texto",
|
||||||
"deepBoxSelect": "Manteña pulsado CtrlOrCmd para seleccionar en profundidade e evitar o arrastre",
|
"deepBoxSelect": "Manteña pulsado CtrlOrCmd para seleccionar en profundidade e evitar o arrastre",
|
||||||
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación"
|
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación",
|
||||||
|
"firefox_clipboard_write": "Esta función pódese activar establecendo a opción \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar as opcións do navegador en Firefox, visita a páxina \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Non se pode mostrar a vista previa",
|
"cannotShowPreview": "Non se pode mostrar a vista previa",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
|
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
|
||||||
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
|
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
|
||||||
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
|
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
|
||||||
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
|
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
|
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
|
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "הוספה/עדכון של קישור עבור הצורה הנבחרת",
|
"link": "הוספה/עדכון של קישור עבור הצורה הנבחרת",
|
||||||
"eraser": "מחק"
|
"eraser": "מחק",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "פעולות הלוח",
|
"canvasActions": "פעולות הלוח",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "צורות"
|
"shapes": "צורות"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "כדי להזיז את הקנבס לחצו על גלגל העכבר או על מקש הרווח תוך כדי גרירה",
|
"canvasPanning": "",
|
||||||
"linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד",
|
"linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד",
|
||||||
"freeDraw": "לחץ וגרור, שחרר כשסיימת",
|
"freeDraw": "לחץ וגרור, שחרר כשסיימת",
|
||||||
"text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה",
|
"text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "פירסום ספריה אישית",
|
"publishLibrary": "פירסום ספריה אישית",
|
||||||
"bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט",
|
"bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",
|
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",
|
||||||
|
|||||||
+14
-11
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।",
|
"invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।",
|
||||||
"resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?",
|
"resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?",
|
||||||
"removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?",
|
"removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?",
|
||||||
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं"
|
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं",
|
||||||
|
"collabOfflineWarning": "कोई इंटरनेट कनेक्शन उपलब्ध नहीं है।\nआपके बदलाव सहेजे नहीं जाएंगे!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "असमर्थित फाइल प्रकार",
|
"unsupportedFileType": "असमर्थित फाइल प्रकार",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें",
|
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें",
|
||||||
"penMode": "पेन का मोड - स्पर्श टाले",
|
"penMode": "पेन का मोड - स्पर्श टाले",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "रबड़"
|
"eraser": "रबड़",
|
||||||
|
"hand": "हाथ ( खिसकाने का औज़ार)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "कैनवास क्रिया",
|
"canvasActions": "कैनवास क्रिया",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "आकृतियाँ"
|
"shapes": "आकृतियाँ"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "",
|
"canvasPanning": "कैनवास को सरकाने के लिए, ड्रैग करते समय माउस व्हील को पकड़े रखे या स्पेसबार को दबाए रखे, अथवा हाथ वाले औज़ार का उपयोग करें",
|
||||||
"linearElement": "कई बिंदुओं को शुरू करने के लिए क्लिक करें, सिंगल लाइन के लिए खींचें",
|
"linearElement": "कई बिंदुओं को शुरू करने के लिए क्लिक करें, सिंगल लाइन के लिए खींचें",
|
||||||
"freeDraw": "क्लिक करें और खींचें। समाप्त करने के लिए, छोड़ो",
|
"freeDraw": "क्लिक करें और खींचें। समाप्त करने के लिए, छोड़ो",
|
||||||
"text": "आप चयन टूल से कहीं भी डबल-क्लिक करके टेक्स्ट जोड़ सकते हैं",
|
"text": "आप चयन टूल से कहीं भी डबल-क्लिक करके टेक्स्ट जोड़ सकते हैं",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए"
|
"eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए",
|
||||||
|
"firefox_clipboard_write": "\"dom.events.asyncClipboard.clipboardItem\" फ़्लैग को \"true\" पर सेट करके इस सुविधा को संभवतः सक्षम किया जा सकता है। Firefox में ब्राउज़र फ़्लैग बदलने के लिए, \"about:config\" पृष्ठ पर जाएँ।"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
|
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
|
||||||
@@ -448,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"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": "शॉर्ट्कट और सहाय्य"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
|
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
|
||||||
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
|
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
|
||||||
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
|
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
|
||||||
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
|
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nem támogatott fájltípus.",
|
"unsupportedFileType": "Nem támogatott fájltípus.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
|
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
|
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
|
||||||
"eraser": ""
|
"eraser": "",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Vászon műveletek",
|
"canvasActions": "Vászon műveletek",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Alakzatok"
|
"shapes": "Alakzatok"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "A vászon mozgatásához tartsd lenyomva az egér görgőjét vagy a szóköz billentyűt húzás közben",
|
"canvasPanning": "",
|
||||||
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
|
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
|
||||||
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
|
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
|
||||||
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
|
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Tedd közzé saját könyvtáradat",
|
"publishLibrary": "Tedd közzé saját könyvtáradat",
|
||||||
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
|
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
|
||||||
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához",
|
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Előnézet nem jeleníthető meg",
|
"cannotShowPreview": "Előnézet nem jeleníthető meg",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
|
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
|
||||||
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
|
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
|
||||||
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
|
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
|
||||||
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
|
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipe file tidak didukung.",
|
"unsupportedFileType": "Tipe file tidak didukung.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Biarkan alat yang dipilih aktif setelah menggambar",
|
"lock": "Biarkan alat yang dipilih aktif setelah menggambar",
|
||||||
"penMode": "Mode pena - mencegah sentuhan",
|
"penMode": "Mode pena - mencegah sentuhan",
|
||||||
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
|
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih",
|
||||||
"eraser": "Penghapus"
|
"eraser": "Penghapus",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Opsi Kanvas",
|
"canvasActions": "Opsi Kanvas",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Bentuk"
|
"shapes": "Bentuk"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Untuk memindahkan kanvas, tekan roda mouse atau spasi ketika menarik",
|
"canvasPanning": "",
|
||||||
"linearElement": "Klik untuk memulai banyak poin, seret untuk satu baris",
|
"linearElement": "Klik untuk memulai banyak poin, seret untuk satu baris",
|
||||||
"freeDraw": "Klik dan seret, lepaskan jika Anda selesai",
|
"freeDraw": "Klik dan seret, lepaskan jika Anda selesai",
|
||||||
"text": "Tip: Anda juga dapat menambahkan teks dengan klik ganda di mana saja dengan alat pemilihan",
|
"text": "Tip: Anda juga dapat menambahkan teks dengan klik ganda di mana saja dengan alat pemilihan",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Terbitkan pustaka Anda",
|
"publishLibrary": "Terbitkan pustaka Anda",
|
||||||
"bindTextToElement": "Tekan enter untuk tambahkan teks",
|
"bindTextToElement": "Tekan enter untuk tambahkan teks",
|
||||||
"deepBoxSelect": "Tekan Ctrl atau Cmd untuk memilih yang di dalam, dan mencegah penggeseran",
|
"deepBoxSelect": "Tekan Ctrl atau Cmd untuk memilih yang di dalam, dan mencegah penggeseran",
|
||||||
"eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus"
|
"eraserRevert": "Tahan Alt untuk mengembalikan elemen yang ditandai untuk dihapus",
|
||||||
|
"firefox_clipboard_write": "Fitur ini dapat diaktifkan melalui pengaturan flag \"dom.events.asyncClipboard.clipboardItem\" ke \"true\". Untuk mengganti flag di Firefox, pergi ke laman \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Tidak dapat menampilkan pratinjau",
|
"cannotShowPreview": "Tidak dapat menampilkan pratinjau",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
|
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
|
||||||
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
|
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
|
||||||
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
|
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
|
||||||
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
|
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata.",
|
||||||
|
"collabOfflineWarning": "Nessuna connessione internet disponibile.\nLe tue modifiche non verranno salvate!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo di file non supportato.",
|
"unsupportedFileType": "Tipo di file non supportato.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato",
|
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato",
|
||||||
"penMode": "Modalità penna - previene il tocco",
|
"penMode": "Modalità penna - previene il tocco",
|
||||||
"link": "Aggiungi/ aggiorna il link per una forma selezionata",
|
"link": "Aggiungi/ aggiorna il link per una forma selezionata",
|
||||||
"eraser": "Gomma"
|
"eraser": "Gomma",
|
||||||
|
"hand": "Mano (strumento di panoramica)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Azioni sulla Tela",
|
"canvasActions": "Azioni sulla Tela",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Forme"
|
"shapes": "Forme"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Per spostare la tela, tieni premuta la rotella del mouse o la barra spaziatrice mentre la trascini",
|
"canvasPanning": "Per spostare la tela, tieni premuta la rotellina del mouse o la barra spaziatrice mentre trascini oppure usa lo strumento mano",
|
||||||
"linearElement": "Clicca per iniziare una linea in più punti, trascina per singola linea",
|
"linearElement": "Clicca per iniziare una linea in più punti, trascina per singola linea",
|
||||||
"freeDraw": "Clicca e trascina, rilascia quando avrai finito",
|
"freeDraw": "Clicca e trascina, rilascia quando avrai finito",
|
||||||
"text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione",
|
"text": "Suggerimento: puoi anche aggiungere del testo facendo doppio clic ovunque con lo strumento di selezione",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Pubblica la tua libreria",
|
"publishLibrary": "Pubblica la tua libreria",
|
||||||
"bindTextToElement": "Premi invio per aggiungere il testo",
|
"bindTextToElement": "Premi invio per aggiungere il testo",
|
||||||
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento",
|
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento",
|
||||||
"eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione"
|
"eraserRevert": "Tieni premuto Alt per ripristinare gli elementi contrassegnati per l'eliminazione",
|
||||||
|
"firefox_clipboard_write": "Questa funzione può essere abilitata impostando il flag \"dom.events.asyncClipboard.clipboardItem\" su \"true\". Per modificare i flag del browser in Firefox, visitare la pagina \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Impossibile visualizzare l'anteprima",
|
"cannotShowPreview": "Impossibile visualizzare l'anteprima",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
|
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
|
||||||
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
|
"resetLibrary": "ライブラリを消去します。本当によろしいですか?",
|
||||||
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
|
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
|
||||||
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
|
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。",
|
||||||
|
"collabOfflineWarning": "インターネットに接続されていません。\n変更は保存されません!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "サポートされていないファイル形式です。",
|
"unsupportedFileType": "サポートされていないファイル形式です。",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "描画後も使用中のツールを選択したままにする",
|
"lock": "描画後も使用中のツールを選択したままにする",
|
||||||
"penMode": "ペンモード - タッチ防止",
|
"penMode": "ペンモード - タッチ防止",
|
||||||
"link": "選択した図形のリンクを追加/更新",
|
"link": "選択した図形のリンクを追加/更新",
|
||||||
"eraser": "消しゴム"
|
"eraser": "消しゴム",
|
||||||
|
"hand": "手 (パンニングツール)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "キャンバス操作",
|
"canvasActions": "キャンバス操作",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "図形"
|
"shapes": "図形"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグします",
|
"canvasPanning": "キャンバスを移動するには、マウスホイールまたはスペースバーを押しながらドラッグするか、手ツールを使用します",
|
||||||
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
|
"linearElement": "クリックすると複数の頂点からなる曲線を開始、ドラッグすると直線",
|
||||||
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
"freeDraw": "クリックしてドラッグします。離すと終了します",
|
||||||
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
"text": "ヒント: 選択ツールを使用して任意の場所をダブルクリックしてテキストを追加することもできます",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "自分のライブラリを公開",
|
"publishLibrary": "自分のライブラリを公開",
|
||||||
"bindTextToElement": "Enterを押してテキストを追加",
|
"bindTextToElement": "Enterを押してテキストを追加",
|
||||||
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行います",
|
"deepBoxSelect": "CtrlOrCmd を押し続けることでドラッグを抑止し、深い選択を行います",
|
||||||
"eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す"
|
"eraserRevert": "Alt を押し続けることで削除マークされた要素を元に戻す",
|
||||||
|
"firefox_clipboard_write": "この機能は、\"dom.events.asyncClipboard.clipboardItem\" フラグを \"true\" に設定することで有効になる可能性があります。Firefox でブラウザーの設定を変更するには、\"about:config\" ページを参照してください。"
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "プレビューを表示できません",
|
"cannotShowPreview": "プレビューを表示できません",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
|
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
|
||||||
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
|
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
|
||||||
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
|
"removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
|
||||||
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
|
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
|
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
|
||||||
@@ -202,8 +203,8 @@
|
|||||||
"invalidSVGString": "SVG armeɣtu.",
|
"invalidSVGString": "SVG armeɣtu.",
|
||||||
"cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.",
|
"cannotResolveCollabServer": "Ulamek tuqqna s aqeddac n umyalel. Ma ulac uɣilif ales asali n usebter sakin eɛreḍ tikkelt-nniḍen.",
|
||||||
"importLibraryError": "Ur d-ssalay ara tamkarḍit",
|
"importLibraryError": "Ur d-ssalay ara tamkarḍit",
|
||||||
"collabSaveFailed": "",
|
"collabSaveFailed": "Ulamek asekles deg uzadur n yisefka deg ugilal. Ma ikemmel wugur, isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem.",
|
||||||
"collabSaveFailed_sizeExceeded": ""
|
"collabSaveFailed_sizeExceeded": "Ulamek asekles deg uzadur n yisefka deg ugilal, taɣzut n usuneɣ tettban-d temqer aṭas. Isefk ad teskelseḍ afaylu s wudem adigan akken ad tetḥeqqeḍ ur tesruḥuyeḍ ara amahil-inek•inem."
|
||||||
},
|
},
|
||||||
"toolBar": {
|
"toolBar": {
|
||||||
"selection": "Tafrayt",
|
"selection": "Tafrayt",
|
||||||
@@ -217,9 +218,10 @@
|
|||||||
"text": "Aḍris",
|
"text": "Aḍris",
|
||||||
"library": "Tamkarḍit",
|
"library": "Tamkarḍit",
|
||||||
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ",
|
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ",
|
||||||
"penMode": "",
|
"penMode": "Askar n yimru - gdel tanalit",
|
||||||
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen",
|
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen",
|
||||||
"eraser": "Sfeḍ"
|
"eraser": "Sfeḍ",
|
||||||
|
"hand": "Afus (afecku n usmutti n tmuɣli)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Tigawin n teɣzut n usuneɣ",
|
"canvasActions": "Tigawin n teɣzut n usuneɣ",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Talɣiwin"
|
"shapes": "Talɣiwin"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Akken ad tesmuttiḍ taɣzut n usuneɣ, ṭṭef ṛṛuda n umumed, neɣ afeggag n tallunt mi ara tzuɣreḍ",
|
"canvasPanning": "",
|
||||||
"linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig",
|
"linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig",
|
||||||
"freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ",
|
"freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ",
|
||||||
"text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt",
|
"text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Siẓreg tamkarḍit-inek•inem",
|
"publishLibrary": "Siẓreg tamkarḍit-inek•inem",
|
||||||
"bindTextToElement": "Ssed ɣef kcem akken ad ternuḍ aḍris",
|
"bindTextToElement": "Ssed ɣef kcem akken ad ternuḍ aḍris",
|
||||||
"deepBoxSelect": "Ṭṭef CtrlOrCmd akken ad tferneḍ s telqey, yerna ad trewleḍ i uzuɣer",
|
"deepBoxSelect": "Ṭṭef CtrlOrCmd akken ad tferneḍ s telqey, yerna ad trewleḍ i uzuɣer",
|
||||||
"eraserRevert": "Ssed Alt akken ad tsefsxeḍ iferdisen yettwacerḍen i tukksa"
|
"eraserRevert": "Ssed Alt akken ad tsefsxeḍ iferdisen yettwacerḍen i tukksa",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Ulamek abeqqeḍ n teskant",
|
"cannotShowPreview": "Ulamek abeqqeḍ n teskant",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "",
|
"lock": "",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": ""
|
"eraser": "",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "",
|
"canvasActions": "",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
|
"invalidSceneUrl": "제공된 URL에서 화면을 가져오는데 실패했습니다. 주소가 잘못되거나, 유효한 Excalidraw JSON 데이터를 포함하고 있지 않은 것일 수 있습니다.",
|
||||||
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
|
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
|
||||||
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
|
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
|
||||||
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다."
|
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
|
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "선택된 도구 유지하기",
|
"lock": "선택된 도구 유지하기",
|
||||||
"penMode": "펜 모드 - 터치 방지",
|
"penMode": "펜 모드 - 터치 방지",
|
||||||
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
"link": "선택한 도형에 대해서 링크를 추가/업데이트",
|
||||||
"eraser": "지우개"
|
"eraser": "지우개",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "캔버스 동작",
|
"canvasActions": "캔버스 동작",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "모양"
|
"shapes": "모양"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "캔버스를 옮기려면 마우스 휠이나 스페이스바를 누르고 드래그하기",
|
"canvasPanning": "",
|
||||||
"linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.",
|
"linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.",
|
||||||
"freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.",
|
"freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.",
|
||||||
"text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.",
|
"text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "당신만의 라이브러리를 게시하기",
|
"publishLibrary": "당신만의 라이브러리를 게시하기",
|
||||||
"bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기",
|
"bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기",
|
||||||
"deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기",
|
"deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기",
|
||||||
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기"
|
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "미리보기를 볼 수 없습니다",
|
"cannotShowPreview": "미리보기를 볼 수 없습니다",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "ناتوانێت دیمەنەکە هاوردە بکات لە URL ی دابینکراو. یان نادروستە، یان داتای \"ئێکسکالیدراو\" JSON ی دروستی تێدا نییە.",
|
"invalidSceneUrl": "ناتوانێت دیمەنەکە هاوردە بکات لە URL ی دابینکراو. یان نادروستە، یان داتای \"ئێکسکالیدراو\" JSON ی دروستی تێدا نییە.",
|
||||||
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
|
"resetLibrary": "ئەمە کتێبخانەکەت خاوێن دەکاتەوە. ئایا دڵنیایت?",
|
||||||
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
|
"removeItemsFromsLibrary": "سڕینەوەی {{count}} ئایتم(ەکان) لە کتێبخانە؟",
|
||||||
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە."
|
"invalidEncryptionKey": "کلیلی رەمزاندن دەبێت لە 22 پیت بێت. هاوکاری ڕاستەوخۆ لە کارخراوە.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
|
"unsupportedFileType": "جۆری فایلی پشتگیری نەکراو.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "ئامێرە دیاریکراوەکان چالاک بهێڵەوە دوای وێنەکێشان",
|
"lock": "ئامێرە دیاریکراوەکان چالاک بهێڵەوە دوای وێنەکێشان",
|
||||||
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
|
"penMode": "شێوازی قەڵەم - دەست لێدان ڕابگرە",
|
||||||
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
|
"link": "زیادکردن/ نوێکردنەوەی لینک بۆ شێوەی دیاریکراو",
|
||||||
"eraser": "سڕەر"
|
"eraser": "سڕەر",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "کردارەکانی تابلۆ",
|
"canvasActions": "کردارەکانی تابلۆ",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "شێوەکان"
|
"shapes": "شێوەکان"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "بۆ جوڵاندنی تابلۆ، لە کاتی ڕاکێشاندا ویلی ماوس یان شریتی بۆشایی دابگرە",
|
"canvasPanning": "",
|
||||||
"linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ",
|
"linearElement": "کرتە بکە بۆ دەستپێکردنی چەند خاڵێک، ڕایبکێشە بۆ یەک هێڵ",
|
||||||
"freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە",
|
"freeDraw": "کرتە بکە و ڕایبکێشە، کاتێک تەواو بوویت دەست هەڵگرە",
|
||||||
"text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن",
|
"text": "زانیاری: هەروەها دەتوانیت دەق زیادبکەیت بە دوو کرتەکردن لە هەر شوێنێک لەگەڵ ئامڕازی دەستنیشانکردن",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "کتێبخانەی تایبەت بە خۆت بڵاوبکەرەوە",
|
"publishLibrary": "کتێبخانەی تایبەت بە خۆت بڵاوبکەرەوە",
|
||||||
"bindTextToElement": "بۆ زیادکردنی دەق enter بکە",
|
"bindTextToElement": "بۆ زیادکردنی دەق enter بکە",
|
||||||
"deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان",
|
"deepBoxSelect": "CtrlOrCmd ڕابگرە بۆ هەڵبژاردنی قووڵ، و بۆ ڕێگریکردن لە ڕاکێشان",
|
||||||
"eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە"
|
"eraserRevert": "بۆ گەڕاندنەوەی ئەو توخمانەی کە بۆ سڕینەوە نیشانە کراون، Alt ڕابگرە",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت",
|
"cannotShowPreview": "ناتوانرێ پێشبینین پیشان بدرێت",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nepavyko suimportuoti scenos iš pateiktos nuorodos (URL). Ji arba blogai suformatuota, arba savyje neturi teisingų Excalidraw JSON duomenų.",
|
"invalidSceneUrl": "Nepavyko suimportuoti scenos iš pateiktos nuorodos (URL). Ji arba blogai suformatuota, arba savyje neturi teisingų Excalidraw JSON duomenų.",
|
||||||
"resetLibrary": "Tai išvalys tavo biblioteką. Ar tikrai to nori?",
|
"resetLibrary": "Tai išvalys tavo biblioteką. Ar tikrai to nori?",
|
||||||
"removeItemsFromsLibrary": "Ištrinti {{count}} elementą/-us iš bibliotekos?",
|
"removeItemsFromsLibrary": "Ištrinti {{count}} elementą/-us iš bibliotekos?",
|
||||||
"invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas."
|
"invalidEncryptionKey": "Šifravimo raktas turi būti iš 22 simbolių. Redagavimas gyvai yra išjungtas.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nepalaikomas failo tipas.",
|
"unsupportedFileType": "Nepalaikomas failo tipas.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Baigus piešti, išlaikyti pasirinktą įrankį",
|
"lock": "Baigus piešti, išlaikyti pasirinktą įrankį",
|
||||||
"penMode": "Rašyklio režimas - neleisti prisilietimų",
|
"penMode": "Rašyklio režimas - neleisti prisilietimų",
|
||||||
"link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą",
|
"link": "Pridėti / Atnaujinti pasirinktos figūros nuorodą",
|
||||||
"eraser": "Trintukas"
|
"eraser": "Trintukas",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Veiksmai su drobe",
|
"canvasActions": "Veiksmai su drobe",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Figūros"
|
"shapes": "Figūros"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Norint judinti drobę, judink pelę kartu įspaudus pelės ratuką arba tarpo klavišą",
|
"canvasPanning": "",
|
||||||
"linearElement": "Paspaudimai sukurs papildomus taškus, nepertraukiamas tempimas sukurs liniją",
|
"linearElement": "Paspaudimai sukurs papildomus taškus, nepertraukiamas tempimas sukurs liniją",
|
||||||
"freeDraw": "Spausk ir tempk, paleisk kai norėsi pabaigti",
|
"freeDraw": "Spausk ir tempk, paleisk kai norėsi pabaigti",
|
||||||
"text": "Užuomina: tekstą taip pat galima pridėti bet kur su dvigubu pelės paspaudimu, kol parinkas žymėjimo įrankis",
|
"text": "Užuomina: tekstą taip pat galima pridėti bet kur su dvigubu pelės paspaudimu, kol parinkas žymėjimo įrankis",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "",
|
"cannotShowPreview": "",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
|
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
|
||||||
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
|
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
|
||||||
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
|
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
|
||||||
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
|
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Neatbalstīts datnes veids.",
|
"unsupportedFileType": "Neatbalstīts datnes veids.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Paturēt izvēlēto rīku pēc darbības",
|
"lock": "Paturēt izvēlēto rīku pēc darbības",
|
||||||
"penMode": "Pildspalvas režīms – novērst pieskaršanos",
|
"penMode": "Pildspalvas režīms – novērst pieskaršanos",
|
||||||
"link": "Pievienot/rediģēt atlasītās figūras saiti",
|
"link": "Pievienot/rediģēt atlasītās figūras saiti",
|
||||||
"eraser": "Dzēšgumija"
|
"eraser": "Dzēšgumija",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Tāfeles darbības",
|
"canvasActions": "Tāfeles darbības",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Lai bīdītu tāfeli, turiet nospiestu ritināšanas vai atstarpes taustiņu, velkot ar peli",
|
"canvasPanning": "",
|
||||||
"linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju",
|
"linearElement": "Klikšķiniet, lai sāktu zīmēt vairākus punktus; velciet, lai zīmētu līniju",
|
||||||
"freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat",
|
"freeDraw": "Spiediet un velciet; atlaidiet, kad pabeidzat",
|
||||||
"text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku",
|
"text": "Ieteikums: lai pievienotu tekstu, varat arī jebkur dubultklikšķināt ar atlases rīku",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publicēt savu bibliotēku",
|
"publishLibrary": "Publicēt savu bibliotēku",
|
||||||
"bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu",
|
"bindTextToElement": "Spiediet ievades taustiņu, lai pievienotu tekstu",
|
||||||
"deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu",
|
"deepBoxSelect": "Turient nospiestu Ctrl vai Cmd, lai atlasītu dziļumā un lai nepieļautu objektu pavilkšanu",
|
||||||
"eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases"
|
"eraserRevert": "Turiet Alt, lai noņemtu elementus no dzēsšanas atlases",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Nevar rādīt priekšskatījumu",
|
"cannotShowPreview": "Nevar rādīt priekšskatījumu",
|
||||||
|
|||||||
+14
-11
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
|
"invalidSceneUrl": "दिलेल्या यू-आर-एल पासून दृश्य आणू शकलो नाही. तो एकतर बरोबार नाही आहे किंवा त्यात वैध एक्सकेलीड्रॉ जेसन डेटा नाही.",
|
||||||
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
|
"resetLibrary": "पटल स्वच्छ होणार, तुम्हाला खात्री आहे का?",
|
||||||
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
|
"removeItemsFromsLibrary": "संग्रहातून {{count}} तत्व (एक किव्हा अनेक) काढू?",
|
||||||
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे."
|
"invalidEncryptionKey": "कूटबद्धन कुंजी 22 अक्षरांची असणे आवश्यक आहे. थेट सहयोग अक्षम केले आहे.",
|
||||||
|
"collabOfflineWarning": "इंटरनेट कनेक्शन उपलब्ध नाही.\nतुमचे बदल जतन केले जाणार नाहीत!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "असमर्थित फाइल प्रकार.",
|
"unsupportedFileType": "असमर्थित फाइल प्रकार.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "निवडलेले यंत्र चित्रकरण झाल्या नंतर ही सक्रिय ठेवा",
|
"lock": "निवडलेले यंत्र चित्रकरण झाल्या नंतर ही सक्रिय ठेवा",
|
||||||
"penMode": "पेन चा मोड - स्पर्श टाळा",
|
"penMode": "पेन चा मोड - स्पर्श टाळा",
|
||||||
"link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा",
|
"link": "निवडलेल्या आकारासाठी दुवा जोडा/बदल करा",
|
||||||
"eraser": "खोड रबर"
|
"eraser": "खोड रबर",
|
||||||
|
"hand": "हात ( सरकवण्या चे उपकरण)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "पटल क्रिया",
|
"canvasActions": "पटल क्रिया",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "आकार"
|
"shapes": "आकार"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "पटल हलवण्यासाठी, ड्रैग करताना माउस वील ला पकड़ा किव्हा स्पेसबार दाबून ठेवा",
|
"canvasPanning": "कॅनव्हास सरकवण्या साठी, ड्रॅग करताना माउस व्हील धरा किंवा स्पेसबार दाबून ठेवा अथवा हात वालं उपकरण वापरा",
|
||||||
"linearElement": "अनेक बिंदु साठी क्लिक करा, रेघे साठी ड्रैग करा",
|
"linearElement": "अनेक बिंदु साठी क्लिक करा, रेघे साठी ड्रैग करा",
|
||||||
"freeDraw": "क्लिक आणि ड्रैग करा, झालं तेव्हा सोडा",
|
"freeDraw": "क्लिक आणि ड्रैग करा, झालं तेव्हा सोडा",
|
||||||
"text": "टीप: तुम्हीं निवड यंत्रानी कोठेही दुहेरी क्लिक करून टेक्स्ट जोडू शकता",
|
"text": "टीप: तुम्हीं निवड यंत्रानी कोठेही दुहेरी क्लिक करून टेक्स्ट जोडू शकता",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "आपला खाजगी संग्रह प्रकाशित करा",
|
"publishLibrary": "आपला खाजगी संग्रह प्रकाशित करा",
|
||||||
"bindTextToElement": "मजकूर जोडण्यासाठी एंटर की दाबा",
|
"bindTextToElement": "मजकूर जोडण्यासाठी एंटर की दाबा",
|
||||||
"deepBoxSelect": "खोल निवड ह्या साठी कंट्रोल किव्हा कमांड दाबून ठेवा, आणि बाहेर खेचणे वाचवण्या साठी पण",
|
"deepBoxSelect": "खोल निवड ह्या साठी कंट्रोल किव्हा कमांड दाबून ठेवा, आणि बाहेर खेचणे वाचवण्या साठी पण",
|
||||||
"eraserRevert": "खोडण्या साठी घेतलेल्या वस्तु ना घेण्या साठी Alt दाबून ठेवावे"
|
"eraserRevert": "खोडण्या साठी घेतलेल्या वस्तु ना घेण्या साठी Alt दाबून ठेवावे",
|
||||||
|
"firefox_clipboard_write": "हे वैशिष्ट्य \"dom.events.asyncClipboard.clipboardItem\" फ्लॅग \"सत्य\" वर सेट करून शक्यतो सक्षम केले जाऊ शकते. Firefox मध्ये ब्राउझर फ्लॅग बदलण्यासाठी, \"about:config\" पृष्ठावर जा."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "पूर्वावलोकन दाखवू शकत नाही",
|
"cannotShowPreview": "पूर्वावलोकन दाखवू शकत नाही",
|
||||||
@@ -448,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"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": "शॉर्टकट आणि सहाय्य"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "",
|
"invalidSceneUrl": "",
|
||||||
"resetLibrary": "",
|
"resetLibrary": "",
|
||||||
"removeItemsFromsLibrary": "",
|
"removeItemsFromsLibrary": "",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "ရွေးချယ်ထားသောကိရိယာကိုသာဆက်သုံး",
|
"lock": "ရွေးချယ်ထားသောကိရိယာကိုသာဆက်သုံး",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": ""
|
"eraser": "",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "ကားချပ်လုပ်ဆောင်ချက်",
|
"canvasActions": "ကားချပ်လုပ်ဆောင်ချက်",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "",
|
"publishLibrary": "",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "နမူနာမပြသနိုင်ပါ",
|
"cannotShowPreview": "နမူနာမပြသနိုင်ပါ",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
|
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
|
||||||
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
|
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
|
||||||
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
|
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
|
||||||
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
|
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert.",
|
||||||
|
"collabOfflineWarning": "Ingen Internett-tilkobling tilgjengelig.\nEndringer dine vil ikke bli lagret!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Filtypen støttes ikke.",
|
"unsupportedFileType": "Filtypen støttes ikke.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Behold merket verktøy som aktivt",
|
"lock": "Behold merket verktøy som aktivt",
|
||||||
"penMode": "Pennemodus - forhindre berøring",
|
"penMode": "Pennemodus - forhindre berøring",
|
||||||
"link": "Legg til / oppdater link for en valgt figur",
|
"link": "Legg til / oppdater link for en valgt figur",
|
||||||
"eraser": "Viskelær"
|
"eraser": "Viskelær",
|
||||||
|
"hand": "Hånd (panoreringsverktøy)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Handlinger: lerret",
|
"canvasActions": "Handlinger: lerret",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Former"
|
"shapes": "Former"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "For å flytte lerretet, hold musehjulet eller mellomromstasten mens du drar",
|
"canvasPanning": "For å flytte lerretet, hold musehjulet eller mellomromstasten mens du drar, eller bruk hånd-verktøyet",
|
||||||
"linearElement": "Klikk for å starte linje med flere punkter, eller dra for en enkel linje",
|
"linearElement": "Klikk for å starte linje med flere punkter, eller dra for en enkel linje",
|
||||||
"freeDraw": "Klikk og dra, slipp når du er ferdig",
|
"freeDraw": "Klikk og dra, slipp når du er ferdig",
|
||||||
"text": "Tips: du kan også legge til tekst ved å dobbeltklikke hvor som helst med utvalgsverktøyet",
|
"text": "Tips: du kan også legge til tekst ved å dobbeltklikke hvor som helst med utvalgsverktøyet",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publiser ditt eget bibliotek",
|
"publishLibrary": "Publiser ditt eget bibliotek",
|
||||||
"bindTextToElement": "Trykk Enter for å legge til tekst",
|
"bindTextToElement": "Trykk Enter for å legge til tekst",
|
||||||
"deepBoxSelect": "Hold CTRL/CMD for å markere dypt og forhindre flytting",
|
"deepBoxSelect": "Hold CTRL/CMD for å markere dypt og forhindre flytting",
|
||||||
"eraserRevert": "Hold Alt for å reversere elementene merket for sletting"
|
"eraserRevert": "Hold Alt for å reversere elementene merket for sletting",
|
||||||
|
"firefox_clipboard_write": "Denne funksjonen kan sannsynligvis aktiveres ved å sette \"dom.events.asyncClipboard.clipboardItem\" flagget til \"true\". For å endre nettleserens flagg i Firefox, besøk \"about:config\"-siden."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Kan ikke vise forhåndsvisning",
|
"cannotShowPreview": "Kan ikke vise forhåndsvisning",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
|
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
|
||||||
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
|
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
|
||||||
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
|
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
|
||||||
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
|
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Niet-ondersteund bestandstype.",
|
"unsupportedFileType": "Niet-ondersteund bestandstype.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Geselecteerde tool actief houden na tekenen",
|
"lock": "Geselecteerde tool actief houden na tekenen",
|
||||||
"penMode": "Pen modus - Blokkeer aanraken",
|
"penMode": "Pen modus - Blokkeer aanraken",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "Gum"
|
"eraser": "Gum",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Canvasacties",
|
"canvasActions": "Canvasacties",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Vormen"
|
"shapes": "Vormen"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Om canvas te verplaatsen, houd muiswiel of spatiebalk ingedrukt tijdens slepen",
|
"canvasPanning": "",
|
||||||
"linearElement": "Klik om meerdere punten te starten, sleep voor één lijn",
|
"linearElement": "Klik om meerdere punten te starten, sleep voor één lijn",
|
||||||
"freeDraw": "Klik en sleep, laat los als je klaar bent",
|
"freeDraw": "Klik en sleep, laat los als je klaar bent",
|
||||||
"text": "Tip: je kunt tekst toevoegen door ergens dubbel te klikken met de selectietool",
|
"text": "Tip: je kunt tekst toevoegen door ergens dubbel te klikken met de selectietool",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publiceer je eigen bibliotheek",
|
"publishLibrary": "Publiceer je eigen bibliotheek",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Kan voorbeeld niet tonen",
|
"cannotShowPreview": "Kan voorbeeld niet tonen",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
|
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
|
||||||
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
|
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
|
||||||
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
|
"removeItemsFromsLibrary": "Slette {{count}} element frå biblioteket?",
|
||||||
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
|
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Filtypen er ikkje støtta.",
|
"unsupportedFileType": "Filtypen er ikkje støtta.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Hald fram med valt verktøy",
|
"lock": "Hald fram med valt verktøy",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "Legg til/ oppdater lenke til valt figur",
|
"link": "Legg til/ oppdater lenke til valt figur",
|
||||||
"eraser": "Viskelêr"
|
"eraser": "Viskelêr",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Handlingar: lerret",
|
"canvasActions": "Handlingar: lerret",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formar"
|
"shapes": "Formar"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "For å flytte lerretet, hald inne musehjulet eller mellomromstasten medan du dreg",
|
"canvasPanning": "",
|
||||||
"linearElement": "Klikk for å starte linje med fleire punkt, eller drag for ei enkel linje",
|
"linearElement": "Klikk for å starte linje med fleire punkt, eller drag for ei enkel linje",
|
||||||
"freeDraw": "Klikk og drag, slepp når du er ferdig",
|
"freeDraw": "Klikk og drag, slepp når du er ferdig",
|
||||||
"text": "Tips: du kan òg leggje til tekst ved å dobbeltklikke kor som helst med utvalgsverktyet",
|
"text": "Tips: du kan òg leggje til tekst ved å dobbeltklikke kor som helst med utvalgsverktyet",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publiser ditt eige bibliotek",
|
"publishLibrary": "Publiser ditt eige bibliotek",
|
||||||
"bindTextToElement": "Trykk på enter for å legge til tekst",
|
"bindTextToElement": "Trykk på enter for å legge til tekst",
|
||||||
"deepBoxSelect": "Hald inne Ctrl / Cmd for å velje djupt, og forhindre flytting",
|
"deepBoxSelect": "Hald inne Ctrl / Cmd for å velje djupt, og forhindre flytting",
|
||||||
"eraserRevert": "Hald inne Alt for å reversere markering av element for sletting"
|
"eraserRevert": "Hald inne Alt for å reversere markering av element for sletting",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Kan ikkje vise førehandsvising",
|
"cannotShowPreview": "Kan ikkje vise førehandsvising",
|
||||||
|
|||||||
+14
-11
@@ -38,8 +38,8 @@
|
|||||||
"arrowhead_bar": "Barra",
|
"arrowhead_bar": "Barra",
|
||||||
"arrowhead_dot": "Ponch",
|
"arrowhead_dot": "Ponch",
|
||||||
"arrowhead_triangle": "Triangle",
|
"arrowhead_triangle": "Triangle",
|
||||||
"fontSize": "Talha poliça",
|
"fontSize": "Talha polissa",
|
||||||
"fontFamily": "Familha de poliça",
|
"fontFamily": "Familha de polissa",
|
||||||
"onlySelected": "Seleccion sonque",
|
"onlySelected": "Seleccion sonque",
|
||||||
"withBackground": "Rèireplan",
|
"withBackground": "Rèireplan",
|
||||||
"exportEmbedScene": "Scèna embarcada",
|
"exportEmbedScene": "Scèna embarcada",
|
||||||
@@ -106,8 +106,8 @@
|
|||||||
"toggleTheme": "Alternar tèma",
|
"toggleTheme": "Alternar tèma",
|
||||||
"personalLib": "Bibliotèca personala",
|
"personalLib": "Bibliotèca personala",
|
||||||
"excalidrawLib": "Bibliotèca Excalidraw",
|
"excalidrawLib": "Bibliotèca Excalidraw",
|
||||||
"decreaseFontSize": "Reduire talha poliça",
|
"decreaseFontSize": "Reduire talha polissa",
|
||||||
"increaseFontSize": "Aumentar talha poliça",
|
"increaseFontSize": "Aumentar talha polissa",
|
||||||
"unbindText": "Dessociar lo tèxte",
|
"unbindText": "Dessociar lo tèxte",
|
||||||
"bindText": "Ligar lo tèxt al contenidor",
|
"bindText": "Ligar lo tèxt al contenidor",
|
||||||
"link": {
|
"link": {
|
||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de l’URL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
|
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de l’URL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
|
||||||
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament ?",
|
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament ?",
|
||||||
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca ?",
|
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca ?",
|
||||||
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada."
|
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada.",
|
||||||
|
"collabOfflineWarning": "Cap de connexion pas disponibla.\nVòstras modificacions seràn pas salvadas !"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",
|
"unsupportedFileType": "Tipe de fichièr pas pres en carga.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Mantenir activa l’aisina aprèp dessenhar",
|
"lock": "Mantenir activa l’aisina aprèp dessenhar",
|
||||||
"penMode": "Mòde estilo - empachar lo contact",
|
"penMode": "Mòde estilo - empachar lo contact",
|
||||||
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
|
"link": "Apondre/Actualizar lo ligam per una fòrma seleccionada",
|
||||||
"eraser": "Goma"
|
"eraser": "Goma",
|
||||||
|
"hand": "Man (aisina de desplaçament de la vista)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Accions del canabàs",
|
"canvasActions": "Accions del canabàs",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Per desplaçar los canabasses, tenètz la rodeta de la mirga o la barra d’espaci pendent lo desplaçament",
|
"canvasPanning": "",
|
||||||
"linearElement": "Clicatz per començar mantun punt, lisatz per una sola linha",
|
"linearElement": "Clicatz per començar mantun punt, lisatz per una sola linha",
|
||||||
"freeDraw": "Clicatz e lisatz, relargatz un còp acabat",
|
"freeDraw": "Clicatz e lisatz, relargatz un còp acabat",
|
||||||
"text": "Astúcia : podètz tanben apondre de tèxt en doble clicant ont que siá amb l’aisina de seleccion",
|
"text": "Astúcia : podètz tanben apondre de tèxt en doble clicant ont que siá amb l’aisina de seleccion",
|
||||||
@@ -238,14 +240,15 @@
|
|||||||
"resize": "Podètz servar las proporcions en mantenent la tòca MAJ pendent lo redimensionament,\nmantenètz la tòca ALT per redimensionar a partir del centre",
|
"resize": "Podètz servar las proporcions en mantenent la tòca MAJ pendent lo redimensionament,\nmantenètz la tòca ALT per redimensionar a partir del centre",
|
||||||
"resizeImage": "Podètz retalhar liurament en quichant CTRL,\nquichatz ALT per retalhar a partir del centre",
|
"resizeImage": "Podètz retalhar liurament en quichant CTRL,\nquichatz ALT per retalhar a partir del centre",
|
||||||
"rotate": "Podètz restrénger los angles en mantenent MAJ pendent la rotacion",
|
"rotate": "Podètz restrénger los angles en mantenent MAJ pendent la rotacion",
|
||||||
"lineEditor_info": "",
|
"lineEditor_info": "Tenètz quichat Ctrl o Cmd e doble clic o quichatz Ctrl o Cmd + Entrada per modificar los ponches",
|
||||||
"lineEditor_pointSelected": "Quichar Suprimir per tirar lo(s) punt(s),\nCtrlOCmd+D per duplicar, o lisatz per desplaçar",
|
"lineEditor_pointSelected": "Quichar Suprimir per tirar lo(s) punt(s),\nCtrlOCmd+D per duplicar, o lisatz per desplaçar",
|
||||||
"lineEditor_nothingSelected": "Seleccionar un punt d’editar (manténer Maj. per ne seleccionar mantun),\no manténer Alt e clicar per n’apondre de novèls",
|
"lineEditor_nothingSelected": "Seleccionar un punt d’editar (manténer Maj. per ne seleccionar mantun),\no manténer Alt e clicar per n’apondre de novèls",
|
||||||
"placeImage": "Clicatz per plaçar l’imatge, o clicatz e lisatz per definir sa talha manualament",
|
"placeImage": "Clicatz per plaçar l’imatge, o clicatz e lisatz per definir sa talha manualament",
|
||||||
"publishLibrary": "Publicar vòstra pròpria bibliotèca",
|
"publishLibrary": "Publicar vòstra pròpria bibliotèca",
|
||||||
"bindTextToElement": "Quichatz Entrada per apondre de tèxte",
|
"bindTextToElement": "Quichatz Entrada per apondre de tèxte",
|
||||||
"deepBoxSelect": "Gardar CtrlOCmd per una seleccion gropada e empachar lo desplaçament",
|
"deepBoxSelect": "Gardar CtrlOCmd per una seleccion gropada e empachar lo desplaçament",
|
||||||
"eraserRevert": "Tenètz quichat Alt per anullar los elements marcats per supression"
|
"eraserRevert": "Tenètz quichat Alt per anullar los elements marcats per supression",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Afichatge impossible de l’apercebut",
|
"cannotShowPreview": "Afichatge impossible de l’apercebut",
|
||||||
@@ -314,8 +317,8 @@
|
|||||||
"zoomToFit": "Zoomar per veire totes los elements",
|
"zoomToFit": "Zoomar per veire totes los elements",
|
||||||
"zoomToSelection": "Zoomar la seleccion",
|
"zoomToSelection": "Zoomar la seleccion",
|
||||||
"toggleElementLock": "Verrolhar/Desverrolhar la seleccion",
|
"toggleElementLock": "Verrolhar/Desverrolhar la seleccion",
|
||||||
"movePageUpDown": "",
|
"movePageUpDown": "Desplaçar la pagina ennaut/enbàs",
|
||||||
"movePageLeftRight": ""
|
"movePageLeftRight": "Desplaçar la pagina a esquèrra/drecha"
|
||||||
},
|
},
|
||||||
"clearCanvasDialog": {
|
"clearCanvasDialog": {
|
||||||
"title": "Escafar canabàs"
|
"title": "Escafar canabàs"
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
|
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
|
||||||
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
|
||||||
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
|
"removeItemsFromsLibrary": "ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚੋਂ {{count}} ਚੀਜ਼(-ਜ਼ਾਂ) ਮਿਟਾਉਣੀਆਂ ਹਨ?",
|
||||||
"invalidEncryptionKey": ""
|
"invalidEncryptionKey": "",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "",
|
"unsupportedFileType": "",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "ਡਰਾਇੰਗ ਤੋਂ ਬਾਅਦ ਵੀ ਚੁਣੇ ਹੋਏ ਸੰਦ ਨੂੰ ਸਰਗਰਮ ਰੱਖੋ ",
|
"lock": "ਡਰਾਇੰਗ ਤੋਂ ਬਾਅਦ ਵੀ ਚੁਣੇ ਹੋਏ ਸੰਦ ਨੂੰ ਸਰਗਰਮ ਰੱਖੋ ",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "ਰਬੜ"
|
"eraser": "ਰਬੜ",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ",
|
"canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "ਆਪਣੀ ਲਾਇਬ੍ਰੇਰੀ ਪ੍ਰਕਾਸ਼ਿਤ ਕਰੋ",
|
"publishLibrary": "ਆਪਣੀ ਲਾਇਬ੍ਰੇਰੀ ਪ੍ਰਕਾਸ਼ਿਤ ਕਰੋ",
|
||||||
"bindTextToElement": "ਪਾਠ ਜੋੜਨ ਲਈ ਐੰਟਰ ਦਬਾਓ",
|
"bindTextToElement": "ਪਾਠ ਜੋੜਨ ਲਈ ਐੰਟਰ ਦਬਾਓ",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "ਝਲਕ ਨਹੀਂ ਦਿਖਾ ਸਕਦੇ",
|
"cannotShowPreview": "ਝਲਕ ਨਹੀਂ ਦਿਖਾ ਸਕਦੇ",
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
{
|
{
|
||||||
"ar-SA": 87,
|
"ar-SA": 92,
|
||||||
"bg-BG": 55,
|
"bg-BG": 54,
|
||||||
"bn-BD": 60,
|
"bn-BD": 59,
|
||||||
"ca-ES": 94,
|
"ca-ES": 100,
|
||||||
"cs-CZ": 75,
|
"cs-CZ": 74,
|
||||||
"da-DK": 33,
|
"da-DK": 32,
|
||||||
"de-DE": 100,
|
"de-DE": 100,
|
||||||
"el-GR": 100,
|
"el-GR": 99,
|
||||||
"en": 100,
|
"en": 100,
|
||||||
"es-ES": 100,
|
"es-ES": 100,
|
||||||
"eu-ES": 100,
|
"eu-ES": 100,
|
||||||
"fa-IR": 96,
|
"fa-IR": 95,
|
||||||
"fi-FI": 93,
|
"fi-FI": 100,
|
||||||
"fr-FR": 100,
|
"fr-FR": 100,
|
||||||
"gl-ES": 100,
|
"gl-ES": 99,
|
||||||
"he-IL": 90,
|
"he-IL": 89,
|
||||||
"hi-IN": 69,
|
"hi-IN": 71,
|
||||||
"hu-HU": 89,
|
"hu-HU": 88,
|
||||||
"id-ID": 100,
|
"id-ID": 99,
|
||||||
"it-IT": 100,
|
"it-IT": 100,
|
||||||
"ja-JP": 100,
|
"ja-JP": 100,
|
||||||
"kab-KAB": 93,
|
"kab-KAB": 93,
|
||||||
"kk-KZ": 21,
|
"kk-KZ": 20,
|
||||||
"ko-KR": 99,
|
"ko-KR": 98,
|
||||||
"ku-TR": 96,
|
"ku-TR": 95,
|
||||||
"lt-LT": 64,
|
"lt-LT": 63,
|
||||||
"lv-LV": 98,
|
"lv-LV": 97,
|
||||||
"mr-IN": 98,
|
"mr-IN": 100,
|
||||||
"my-MM": 41,
|
"my-MM": 41,
|
||||||
"nb-NO": 100,
|
"nb-NO": 100,
|
||||||
"nl-NL": 91,
|
"nl-NL": 90,
|
||||||
"nn-NO": 90,
|
"nn-NO": 89,
|
||||||
"oc-FR": 98,
|
"oc-FR": 98,
|
||||||
"pa-IN": 83,
|
"pa-IN": 82,
|
||||||
"pl-PL": 85,
|
"pl-PL": 84,
|
||||||
"pt-BR": 98,
|
"pt-BR": 100,
|
||||||
"pt-PT": 100,
|
"pt-PT": 100,
|
||||||
"ro-RO": 100,
|
"ro-RO": 100,
|
||||||
"ru-RU": 98,
|
"ru-RU": 100,
|
||||||
"si-LK": 8,
|
"si-LK": 8,
|
||||||
"sk-SK": 100,
|
"sk-SK": 100,
|
||||||
"sl-SI": 100,
|
"sl-SI": 100,
|
||||||
"sv-SE": 96,
|
"sv-SE": 100,
|
||||||
"ta-IN": 93,
|
"ta-IN": 94,
|
||||||
"tr-TR": 98,
|
"tr-TR": 97,
|
||||||
"uk-UA": 96,
|
"uk-UA": 96,
|
||||||
"vi-VN": 20,
|
"vi-VN": 20,
|
||||||
"zh-CN": 100,
|
"zh-CN": 100,
|
||||||
"zh-HK": 26,
|
"zh-HK": 25,
|
||||||
"zh-TW": 100
|
"zh-TW": 100
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
|
"invalidSceneUrl": "Nie udało się zaimportować sceny z podanego adresu URL. Jest ona wadliwa lub nie zawiera poprawnych danych Excalidraw w formacie JSON.",
|
||||||
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
|
"resetLibrary": "To wyczyści twoją bibliotekę. Jesteś pewien?",
|
||||||
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
|
"removeItemsFromsLibrary": "Usunąć {{count}} element(ów) z biblioteki?",
|
||||||
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona."
|
"invalidEncryptionKey": "Klucz szyfrowania musi składać się z 22 znaków. Współpraca na żywo jest wyłączona.",
|
||||||
|
"collabOfflineWarning": ""
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Nieobsługiwany typ pliku.",
|
"unsupportedFileType": "Nieobsługiwany typ pliku.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Zablokuj wybrane narzędzie",
|
"lock": "Zablokuj wybrane narzędzie",
|
||||||
"penMode": "",
|
"penMode": "",
|
||||||
"link": "",
|
"link": "",
|
||||||
"eraser": "Gumka"
|
"eraser": "Gumka",
|
||||||
|
"hand": ""
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Narzędzia",
|
"canvasActions": "Narzędzia",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Kształty"
|
"shapes": "Kształty"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Aby przesunąć płótno, przytrzymaj kółko myszy lub spację podczas przeciągania",
|
"canvasPanning": "",
|
||||||
"linearElement": "Naciśnij, aby zrobić punkt, przeciągnij, aby narysować linię",
|
"linearElement": "Naciśnij, aby zrobić punkt, przeciągnij, aby narysować linię",
|
||||||
"freeDraw": "Naciśnij i przeciągnij by rysować, puść kiedy skończysz",
|
"freeDraw": "Naciśnij i przeciągnij by rysować, puść kiedy skończysz",
|
||||||
"text": "Wskazówka: możesz również dodać tekst klikając dwukrotnie gdziekolwiek za pomocą narzędzia zaznaczania",
|
"text": "Wskazówka: możesz również dodać tekst klikając dwukrotnie gdziekolwiek za pomocą narzędzia zaznaczania",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Opublikuj własną bibliotekę",
|
"publishLibrary": "Opublikuj własną bibliotekę",
|
||||||
"bindTextToElement": "",
|
"bindTextToElement": "",
|
||||||
"deepBoxSelect": "",
|
"deepBoxSelect": "",
|
||||||
"eraserRevert": ""
|
"eraserRevert": "",
|
||||||
|
"firefox_clipboard_write": ""
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Nie można wyświetlić podglądu",
|
"cannotShowPreview": "Nie można wyświetlić podglądu",
|
||||||
|
|||||||
+14
-11
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
|
"invalidSceneUrl": "Não foi possível importar a cena da URL fornecida. Ela está incompleta ou não contém dados JSON válidos do Excalidraw.",
|
||||||
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
|
"resetLibrary": "Isto limpará a sua biblioteca. Você tem certeza?",
|
||||||
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
|
"removeItemsFromsLibrary": "Excluir {{count}} item(ns) da biblioteca?",
|
||||||
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada."
|
"invalidEncryptionKey": "A chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desabilitada.",
|
||||||
|
"collabOfflineWarning": "Sem conexão com a internet disponível.\nSuas alterações não serão salvas!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de arquivo não suportado.",
|
"unsupportedFileType": "Tipo de arquivo não suportado.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Manter ativa a ferramenta selecionada após desenhar",
|
"lock": "Manter ativa a ferramenta selecionada após desenhar",
|
||||||
"penMode": "Modo caneta — impede o toque",
|
"penMode": "Modo caneta — impede o toque",
|
||||||
"link": "Adicionar/Atualizar link para uma forma selecionada",
|
"link": "Adicionar/Atualizar link para uma forma selecionada",
|
||||||
"eraser": "Borracha"
|
"eraser": "Borracha",
|
||||||
|
"hand": "Mão (ferramenta de rolagem)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ações da tela",
|
"canvasActions": "Ações da tela",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta",
|
"canvasPanning": "Para mover a tela, segure a roda do mouse ou a barra de espaço enquanto arrasta ou use a ferramenta de mão",
|
||||||
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
||||||
"freeDraw": "Toque e arraste, solte quando terminar",
|
"freeDraw": "Toque e arraste, solte quando terminar",
|
||||||
"text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
"text": "Dica: você também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publicar sua própria biblioteca",
|
"publishLibrary": "Publicar sua própria biblioteca",
|
||||||
"bindTextToElement": "Pressione Enter para adicionar o texto",
|
"bindTextToElement": "Pressione Enter para adicionar o texto",
|
||||||
"deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar",
|
"deepBoxSelect": "Segure Ctrl/Cmd para seleção profunda e para evitar arrastar",
|
||||||
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão"
|
"eraserRevert": "Segure a tecla Alt para inverter os elementos marcados para exclusão",
|
||||||
|
"firefox_clipboard_write": "Esse recurso pode ser ativado configurando a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Não é possível mostrar pré-visualização",
|
"cannotShowPreview": "Não é possível mostrar pré-visualização",
|
||||||
@@ -448,15 +451,15 @@
|
|||||||
},
|
},
|
||||||
"welcomeScreen": {
|
"welcomeScreen": {
|
||||||
"app": {
|
"app": {
|
||||||
"center_heading": "",
|
"center_heading": "Todos os dados são salvos localmente no seu navegador.",
|
||||||
"center_heading_plus": "",
|
"center_heading_plus": "Você queria ir para o Excalidraw+ em vez disso?",
|
||||||
"menuHint": ""
|
"menuHint": "Exportar, preferências, idiomas..."
|
||||||
},
|
},
|
||||||
"defaults": {
|
"defaults": {
|
||||||
"menuHint": "",
|
"menuHint": "Exportar, preferências e mais...",
|
||||||
"center_heading": "",
|
"center_heading": "Diagramas, Feito. Simples.",
|
||||||
"toolbarHint": "",
|
"toolbarHint": "Escolha uma ferramenta e comece a desenhar!",
|
||||||
"helpHint": ""
|
"helpHint": "Atalhos e ajuda"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
|
"invalidSceneUrl": "Não foi possível importar a cena a partir do URL fornecido. Ou está mal formado ou não contém dados JSON do Excalidraw válidos.",
|
||||||
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
|
"resetLibrary": "Isto irá limpar a sua biblioteca. Tem a certeza?",
|
||||||
"removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?",
|
"removeItemsFromsLibrary": "Apagar {{count}} item(ns) da biblioteca?",
|
||||||
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada."
|
"invalidEncryptionKey": "Chave de encriptação deve ter 22 caracteres. A colaboração ao vivo está desativada.",
|
||||||
|
"collabOfflineWarning": "Sem ligação à internet disponível.\nAs suas alterações não serão salvas!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tipo de ficheiro não suportado.",
|
"unsupportedFileType": "Tipo de ficheiro não suportado.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Manter a ferramenta selecionada ativa após desenhar",
|
"lock": "Manter a ferramenta selecionada ativa após desenhar",
|
||||||
"penMode": "Modo caneta - impedir toque",
|
"penMode": "Modo caneta - impedir toque",
|
||||||
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
|
"link": "Acrescentar/ Adicionar ligação para uma forma seleccionada",
|
||||||
"eraser": "Borracha"
|
"eraser": "Borracha",
|
||||||
|
"hand": "Mão (ferramenta de movimento da tela)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Ações da área de desenho",
|
"canvasActions": "Ações da área de desenho",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Formas"
|
"shapes": "Formas"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta",
|
"canvasPanning": "Para mover a tela, carregue na roda do rato ou na barra de espaço enquanto arrasta, ou use a ferramenta da mão",
|
||||||
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
"linearElement": "Clique para iniciar vários pontos, arraste para uma única linha",
|
||||||
"freeDraw": "Clique e arraste, large quando terminar",
|
"freeDraw": "Clique e arraste, large quando terminar",
|
||||||
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
"text": "Dica: também pode adicionar texto clicando duas vezes em qualquer lugar com a ferramenta de seleção",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publique a sua própria biblioteca",
|
"publishLibrary": "Publique a sua própria biblioteca",
|
||||||
"bindTextToElement": "Carregue Enter para acrescentar texto",
|
"bindTextToElement": "Carregue Enter para acrescentar texto",
|
||||||
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
|
"deepBoxSelect": "Mantenha a tecla CtrlOrCmd carregada para selecção profunda, impedindo o arrastamento",
|
||||||
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados"
|
"eraserRevert": "Carregue também em Alt para reverter os elementos marcados para serem apagados",
|
||||||
|
"firefox_clipboard_write": "Esta função pode provavelmente ser ativada definindo a opção \"dom.events.asyncClipboard.clipboardItem\" como \"true\". Para alterar os sinalizadores do navegador no Firefox, visite a página \"about:config\"."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",
|
"cannotShowPreview": "Não é possível mostrar uma pré-visualização",
|
||||||
|
|||||||
@@ -192,7 +192,8 @@
|
|||||||
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
|
"invalidSceneUrl": "Scena nu a putut fi importată din URL-ul furnizat. Este fie incorect formată, fie nu conține date JSON Excalidraw valide.",
|
||||||
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
|
"resetLibrary": "Această opțiune va elimina conținutul din bibliotecă. Confirmi?",
|
||||||
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
|
"removeItemsFromsLibrary": "Ștergi {{count}} element(e) din bibliotecă?",
|
||||||
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată."
|
"invalidEncryptionKey": "Cheia de criptare trebuie să aibă 22 de caractere. Colaborarea în direct este dezactivată.",
|
||||||
|
"collabOfflineWarning": "Nu este disponibilă nicio conexiune la internet.\nModificările nu vor fi salvate!"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"unsupportedFileType": "Tip de fișier neacceptat.",
|
"unsupportedFileType": "Tip de fișier neacceptat.",
|
||||||
@@ -219,7 +220,8 @@
|
|||||||
"lock": "Menține activ instrumentul selectat după desenare",
|
"lock": "Menține activ instrumentul selectat după desenare",
|
||||||
"penMode": "Mod stilou – împiedică atingerea",
|
"penMode": "Mod stilou – împiedică atingerea",
|
||||||
"link": "Adăugare/actualizare URL pentru forma selectată",
|
"link": "Adăugare/actualizare URL pentru forma selectată",
|
||||||
"eraser": "Radieră"
|
"eraser": "Radieră",
|
||||||
|
"hand": "Mână (instrument de panoramare)"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
"canvasActions": "Acțiuni pentru pânză",
|
"canvasActions": "Acțiuni pentru pânză",
|
||||||
@@ -227,7 +229,7 @@
|
|||||||
"shapes": "Forme"
|
"shapes": "Forme"
|
||||||
},
|
},
|
||||||
"hints": {
|
"hints": {
|
||||||
"canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu în timpul glisării",
|
"canvasPanning": "Pentru a muta pânză, ține apăsată rotița mausului sau bara de spațiu sau folosește instrumentul în formă de mână",
|
||||||
"linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie",
|
"linearElement": "Dă clic pentru a crea mai multe puncte, glisează pentru a forma o singură linie",
|
||||||
"freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat",
|
"freeDraw": "Dă clic pe pânză și glisează cursorul, apoi eliberează-l când ai terminat",
|
||||||
"text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție",
|
"text": "Sfat: poți adăuga text și dând dublu clic oriunde cu instrumentul de selecție",
|
||||||
@@ -245,7 +247,8 @@
|
|||||||
"publishLibrary": "Publică propria bibliotecă",
|
"publishLibrary": "Publică propria bibliotecă",
|
||||||
"bindTextToElement": "Apasă tasta Enter pentru a adăuga text",
|
"bindTextToElement": "Apasă tasta Enter pentru a adăuga text",
|
||||||
"deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea",
|
"deepBoxSelect": "Ține apăsată tasta Ctrl sau Cmd pentru a efectua selectarea de adâncime și pentru a preveni glisarea",
|
||||||
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere"
|
"eraserRevert": "Ține apăsată tasta Alt pentru a anula elementele marcate pentru ștergere",
|
||||||
|
"firefox_clipboard_write": "Această caracteristică poate fi probabil activată prin setarea preferinței „dom.events.asyncClipboard.clipboardItem” ca „true”. Pentru a schimba preferințele navigatorului în Firefox, accesează pagina „about:config”."
|
||||||
},
|
},
|
||||||
"canvasError": {
|
"canvasError": {
|
||||||
"cannotShowPreview": "Nu se poate afișa previzualizarea",
|
"cannotShowPreview": "Nu se poate afișa previzualizarea",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user