Compare commits

...
Author SHA1 Message Date
Aakansha DoshiandGitHub c9c79646c5 docs: release @excalidraw/excalidraw@0.15.0 🎉 (#6481) 2023-04-18 20:48:30 +05:30
Aakansha DoshiandGitHub 979312f779 fix: center align text when wrapped in container via context menu (#6480)
* rename action to wrapTextInContainer

* fix: center align text when wrapped in container via context menu

* revert translation key

* fix tests
2023-04-18 19:44:14 +05:30
David LuzarandGitHub 4d0d844e39 feat: constrain export dialog preview size (#6475) 2023-04-18 15:27:51 +02:00
Aakansha DoshiandGitHub 801412bf6b fix: restore original container height when unbinding text which was binded via context menu (#6444)
* fix: restore original container height when unbinding text which was binded via context menu

* remove flag

* comment
2023-04-18 18:50:25 +05:30
Aakansha DoshiandGitHub 21726e22cc fix: mark more props as optional for element (#6448)
* fix: mark more props as optional for element

* fix
2023-04-18 18:42:48 +05:30
David LuzarandGitHub c3e8ddaf58 fix: improperly cache-busting on canvas scale instead of zoom (#6473) 2023-04-17 11:41:27 +02:00
David LuzarandGitHub f640ddc2aa fix: incorrectly duplicating items on paste/library insert (#6467
* fix: incorrectly duplicating items on paste/library insert

* fix: deduplicate element ids on restore

* tests
2023-04-16 17:22:16 +02:00
Excalidraw BotandGitHub e7e54814e7 chore: Update translations from Crowdin (#6290)
* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Slovenian)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Catalan)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Portuguese)

* New translations en.json (Slovak)

* New translations en.json (Chinese Simplified)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (French)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Russian)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Bengali)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Greek)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* Auto commit: Calculate translation coverage

* New translations en.json (Hebrew)

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Thai)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Polish)

* Auto commit: Calculate translation coverage

* New translations en.json (Korean)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* New translations en.json (Basque)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (Polish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Korean)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Punjabi)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Thai)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Burmese)

* New translations en.json (Chinese Traditional, Hong Kong)

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovenian)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage
2023-04-16 17:12:37 +02:00
David LuzarandGitHub e9064a4a87 fix: library ids cross-contamination on multiple insert (#6466) 2023-04-16 17:09:51 +02:00
David LuzarandGitHub 034113772d fix: color picker keyboard handling not working (#6464) 2023-04-16 15:33:30 +02:00
zsviczianandGitHub d34cd3072f fix: abort freedraw line if second touch is detected (#6440) 2023-04-16 15:33:16 +02:00
David LuzarandGitHub e31230f78c refactor: inline SingleLibraryItem into PublishLibrary (#6462
refactor: inline `SingleLibraryItem` into `PublishLibrary` to reduce api surface area
2023-04-16 11:57:13 +02:00
David LuzarandGitHub 399c92d882 fix: utils leaking Scene state (#6461
* fix: utils leaking Scene state

* remove debug

* doc

* add tests for group duplicating

* tweaks
2023-04-16 11:56:25 +02:00
b0b23353cf fix: split "Edit selected shape" shortcut (#6457)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-14 21:34:26 +00:00
BearandGitHub 6164b5273c fix: center align text when bind to container via context menu (#6451) 2023-04-14 15:22:39 +02:00
Aakansha DoshiandGitHub ca3be2c678 fix: exporting labelled arrows via export utils (#6443)
* fix: exporting labelled arrows via export utils

* add comments

* lint

* update changelog

* fix lint

* initialize scene in the utils so it can be availabe in the helper functions

* fix library rendering

* add comments
2023-04-13 17:19:46 +05:30
Aakansha DoshiandGitHub 13b27afe0f fix: update coords when text unbinded from its container (#6445)
* fix: update coords when text unbinded from its container

* Add specs
2023-04-13 11:45:58 +05:30
David LuzarandGitHub 372743f59f fix: autoredirect to plus in prod only (#6446) 2023-04-12 10:57:00 +02:00
fc601347cf fix: fixing popover overflow on small screen (#6433)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-11 23:23:36 +02:00
David LuzarandGitHub e4d8ba226f feat: zigzag fill easter egg (#6439) 2023-04-10 15:38:50 +02:00
ec215362a1 fix: introduce baseline to fix the layout shift when switching to text editor (#6397)
* fix: introduce baseline to fix the layout shift when switching to text editor

* uncomment

* change offset to 8pixels

* [debug]

* introduce DOM baseline in canvas rendering instead

* introduce baseline in element making it backward compat

* fix

* lint

* fix

* update baseline when resizing text element

* fix safari backward compat

* fix for safari

* lint

* reduce safari LS

* floor line height and height when dom height increases than canvas height

* Revert "floor line height and height when dom height increases than canvas height"

This reverts commit 8de6516823.

* cleanup

* use DOM height only for safari to fix LS

* Revert "use DOM height only for safari to fix LS"

This reverts commit d75889238d.

* fix lint and test

* fix

* calculate line height by rounding off instead of DOM

* cleanup

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-10 18:52:46 +05:30
Aakansha DoshiandGitHub 0b8fc4f4b6 fix: don't refresh dimensions for deleted text elements (#6438) 2023-04-10 16:31:58 +05:30
Manuel BrandstetterandGitHub c170403b13 docs: fix closing tag in readme (#6424
Update README.md
2023-04-07 17:43:50 +02:00
705ac9c1ab fix: element vanishes when zoomed in (#6417)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-04-07 15:30:44 +00:00
68692b9d4c feat: add container to multiple text elements (#6428)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-04-07 13:50:36 +00:00
Aakansha DoshiandGitHub d61b3cf83d fix: don't jump text to end when out of viewport in safari (#6416) 2023-04-05 12:17:18 +05:30
d2b8f4d2f8 fix: getDefaultLineHeight should return default font family line height for unknown font (#6399)
* fix(getDefaultLineHeight): make getDefaultLineHeight always has a default value

* test: add getDefaultLineHeight test case when using unknown font

* test: add getDefaultLineHeight test case when using unknown font

* Revert "test: add getDefaultLineHeight test case when using unknown font"

This reverts commit d41da5493b6edab9e599a13a23c387d38345bf03.

* test: add getDefaultLineHeight test case when using unknown font

* newline

* newline

* tweaks

* trigger action

* trigger action

* fix

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-03-30 00:46:23 +05:30
Aakansha DoshiandGitHub f8e65bb77e fix: Revert use ideographic textBaseline to improve layout shift when editing text" (#6400)
Revert "fix: use `ideographic` textBaseline to improve layout shift when editing text (#6384)"

This reverts commit 9e52c30ce8.
2023-03-29 18:53:03 +05:30
3030e96d62 feat: starting migration from GA to Matomo for better privacy (#6398)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-29 11:13:06 +02:00
44453b725d fix: call stack size exceeded when paste large text (#6373) (#6396)
* fix: call stack size exceeded when paste large text (#6373)

* fix: add test case for paste multi-line text

* fix

* tweak

* add missing assertion

* add comments

* lint

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-03-29 13:45:30 +05:30
25bb6738ea feat: Add fitToContent and animate to scrollToContent (#6319)
Co-authored-by: Brice Leroy <brice@brigalabs.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-25 23:26:58 +01:00
David LuzarandGitHub 9e52c30ce8 fix: use ideographic textBaseline to improve layout shift when editing text (#6384) 2023-03-22 18:33:37 +01:00
83383977f5 feat: add line height attribute to text element (#6360)
* feat: add line height attribute to text element

* lint

* update line height when redrawing text bounding box

* fix tests

* retain line height when pasting styles

* fix test

* create a util for calculating ling height using old algo

* update line height when resizing multiple text elements

* make line height backward compatible

* udpate line height for older element when font size updated

* remove logs

* Add specs

* lint

* review fixes

* simplify by changing `lineHeight` from px to unitless

* make param non-optional

* update comment

* fix: jumping text due to font size being calculated incorrectly

* update line height when font family is updated

* lint

* Add spec

* more specs

* rename to getDefaultLineHeight

* fix getting lineHeight for potentially undefined fontFamily

* reduce duplication

* fix fallback

* refactor and comment tweaks

* fix

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-22 11:32:38 +05:30
David LuzarandGitHub ac4c8b3ca7 fix: chrome crashing when embedding scene on chrome arm (#6383) 2023-03-21 18:48:49 +01:00
5c8941467d fix: division by zero in findFocusPointForEllipse leads to infinite loop in wrapText freezing Excalidraw (#6377)
* Update collision.ts

* Update textElement.ts

* Update textElement.ts

* tweak

* fix

* remove unnecessary `Math.sign`

* change check and add doc

* Add a case for negative max width and specs

* fix

---------

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-03-20 17:50:09 +05:30
0726911fa6 fix: containerizing text incorrectly updates arrow bindings (#6369)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-03-18 15:00:28 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7e330c8ee1 build(deps-dev): bump webpack from 5.73.0 to 5.76.0 in /src/packages/utils (#6354)
build(deps-dev): bump webpack in /src/packages/utils

Bumps [webpack](https://github.com/webpack/webpack) from 5.73.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.73.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:04:13 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7d21747644 build(deps-dev): bump webpack from 5.73.0 to 5.76.0 in /src/packages/excalidraw (#6355)
build(deps-dev): bump webpack in /src/packages/excalidraw

Bumps [webpack](https://github.com/webpack/webpack) from 5.73.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.73.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:04:01 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e718136aea build(deps): bump webpack from 5.74.0 to 5.76.1 in /dev-docs (#6356)
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 11:03:40 +05:30
Salah Eddine DaciandGitHub fe83e2922d build: move TS and types to devDependencies (#6346) 2023-03-14 12:06:57 +00:00
David LuzarandGitHub 20edddcd4e fix: ensure export preview is centered (#6337) 2023-03-14 13:03:55 +01:00
Aakansha DoshiandGitHub f6e8be399e fix: hide text align for labelled arrows (#6339)
* fix: hide text align for labelled arrows

* lintttt

* since we fetch seledcted Elements including the bound text hence this block can be removed

* fix
2023-03-14 17:21:46 +05:30
Aakansha DoshiandGitHub ab49cad6b1 perf: break early if the line width <= max width of the container (#6347)
* fix: break early if the line width <= max width of the container

* Remove dead code

* remove dead code

* lint

* remove
2023-03-14 17:18:16 +05:30
Aakansha DoshiandGitHub 6aeb18b784 fix: refresh dimensions when elements loaded from shareable link and blob (#6333)
* fix: refresh dimensions when elements loaded from shareable link

* refresh text dimensions when loading from file

* remove log
2023-03-14 17:08:23 +05:30
023313e92f fix: show error message when measureText API breaks in brave (#6336)
* fix: show error message when measureText API breaks in brave

* Add docs

* Add assets

* tweak message

* fix

* tweak message

* add translations

* lint

* fix

* fix

* lint

* lint

* lint please work now

* tweak doc

* fix

* split error component to new file

* add specs

* tweaks

Co-authored-by: David Luzar <luzar.david@gmail.com>

* wrap in div with a width of 30rem

* fix spec

* fix

* Fix typo

---------

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: Daniel J. Geiger <1852529+DanielJGeiger@users.noreply.github.com>
2023-03-13 19:46:09 +05:30
David LuzarandGitHub 1eee488dab feat: add thai lang support (#6314) 2023-03-11 22:15:52 +01:00
Aakansha DoshiandGitHub dd4c333925 fix: add an offset of 0.5px for text editor in containers (#6328)
* fix: add an offset of 0.5px for text editor in containers

* fix specs and lint
2023-03-09 13:07:36 +05:30
132 changed files with 5034 additions and 1449 deletions
+5
View File
@@ -23,6 +23,11 @@ REACT_APP_DEV_DISABLE_LIVE_RELOAD=
FAST_REFRESH=false FAST_REFRESH=false
# MATOMO
REACT_APP_MATOMO_URL=
REACT_APP_CDN_MATOMO_TRACKER_URL=
REACT_APP_MATOMO_SITE_ID=
#Debug flags #Debug flags
# To enable bounding box for text containers # To enable bounding box for text containers
+7
View File
@@ -12,6 +12,13 @@ REACT_APP_WS_SERVER_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}' REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
# production-only vars # production-only vars
# GOOGLE ANALYTICS
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13 REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
# MATOMO
REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/
REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js
REACT_APP_MATOMO_SITE_ID=1
REACT_APP_PLUS_APP=https://app.excalidraw.com REACT_APP_PLUS_APP=https://app.excalidraw.com
+1
View File
@@ -25,3 +25,4 @@ src/packages/excalidraw/types
src/packages/excalidraw/example/public/bundle.js src/packages/excalidraw/example/public/bundle.js
src/packages/excalidraw/example/public/excalidraw-assets-dev src/packages/excalidraw/example/public/excalidraw-assets-dev
src/packages/excalidraw/example/public/excalidraw.development.js src/packages/excalidraw/example/public/excalidraw.development.js
coverage
+1 -1
View File
@@ -17,7 +17,7 @@
An open source virtual hand-drawn style whiteboard. </br> An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br> Collaborative and end-to-end encrypted. </br>
<br /> <br />
</h3> </h2>
</div> </div>
<br /> <br />
@@ -1,6 +1,19 @@
# ref # ref
<pre> <pre>
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">createRef</a> &#124; <a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a> &#124; <a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">callbackRef</a> &#124; <br/>&#123; current: &#123; readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">resolvablePromise</a> } } <a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">
createRef
</a>{" "}
&#124;{" "}
<a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a>{" "}
&#124;{" "}
<a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">
callbackRef
</a>{" "}
&#124; <br />
&#123; current: &#123; readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
resolvablePromise
</a> } }
</pre> </pre>
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs: You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
@@ -139,7 +152,9 @@ function App() {
return ( return (
<div style={{ height: "500px" }}> <div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the scene</p> <p style={{ fontSize: "16px" }}> Click to update the scene</p>
<button className="custom-button" onClick={updateScene}>Update Scene</button> <button className="custom-button" onClick={updateScene}>
Update Scene
</button>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} /> <Excalidraw ref={(api) => setExcalidrawAPI(api)} />
</div> </div>
); );
@@ -187,7 +202,8 @@ function App() {
return ( return (
<div style={{ height: "500px" }}> <div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the library items</p> <p style={{ fontSize: "16px" }}> Click to update the library items</p>
<button className="custom-button" <button
className="custom-button"
onClick={() => { onClick={() => {
const libraryItems = [ const libraryItems = [
{ {
@@ -205,10 +221,8 @@ function App() {
]; ];
excalidrawAPI.updateLibrary({ excalidrawAPI.updateLibrary({
libraryItems, libraryItems,
openLibraryMenu: true openLibraryMenu: true,
}); });
}} }}
> >
Update Library Update Library
@@ -250,7 +264,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
<pre> <pre>
() =>{" "} () =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement[] ExcalidrawElement[]
</a> </a>
</pre> </pre>
@@ -261,7 +275,7 @@ Returns all the elements including the deleted in the scene.
<pre> <pre>
() => NonDeleted&#60; () => NonDeleted&#60;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement ExcalidrawElement
</a> </a>
[]&#62; []&#62;
@@ -293,18 +307,31 @@ This is the history API. history.clear() will clear the history.
## scrollToContent ## scrollToContent
<pre> <pre>
(target?:{" "} (<br />
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> {" "}
target?:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement ExcalidrawElement
</a>{" "} </a>{" "}
&#124;{" "} &#124;{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
ExcalidrawElement ExcalidrawElement
</a> </a>
[]) => void [],
<br />
{" "}opts?: &#123; fitToContent?: boolean; animate?: boolean; duration?: number
&#125;
<br />) => void
</pre> </pre>
Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
| Attribute | type | default | Description |
| --- | --- | --- | --- |
| target | <code>ExcalidrawElement &#124; ExcalidrawElement[]</code> | All scene elements | The element(s) to scroll to. |
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. |
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
## refresh ## refresh
@@ -323,7 +350,7 @@ For any other cases if the position of excalidraw is updated (example due to scr
This API can be used to show the toast with custom message. This API can be used to show the toast with custom message.
```tsx ```tsx
({ message: string, closable?:boolean,duration?:number ({ message: string, closable?:boolean,duration?:number
} | null) => void } | null) => void
``` ```
@@ -358,15 +385,18 @@ This API can be used to get the files present in the scene. It may contain files
This API has the below signature. It sets the `tool` passed in param as the active tool. This API has the below signature. It sets the `tool` passed in param as the active tool.
<pre> <pre>
(tool: <br/> &#123; type: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">SHAPES</a>[number]["value"]&#124; "eraser" &#125; &#124;<br/> &#123; type: "custom"; customType: string &#125;) => void (tool: <br /> &#123; type:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">
SHAPES
</a>
[number]["value"]&#124; "eraser" &#125; &#124;
<br /> &#123; type: "custom"; customType: string &#125;) => void
</pre> </pre>
## setCursor ## setCursor
This API can be used to customise the mouse cursor on the canvas and has the below signature. This API can be used to customise the mouse cursor on the canvas and has the below signature. It sets the mouse cursor to the cursor passed in param.
It sets the mouse cursor to the cursor passed in param.
```tsx ```tsx
(cursor: string) => void (cursor: string) => void
@@ -4,6 +4,34 @@
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same. No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
### Turning off Aggressive Anti-Fingerprinting in Brave browser
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
We strongly recommend turning it off. You can follow the steps below on how to do so.
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
![Shield button](../../assets/brave-shield.png)
<div style={{width:'30rem'}}>
2. Once opened, look for **Aggressively Block Fingerprinting**
![Aggresive block fingerprinting](../../assets/aggressive-block-fingerprint.png)
3. Switch to **Block Fingerprinting**
![Block filtering](../../assets/block-fingerprint.png)
4. Thats all. All text elements should be fixed now 🎉
</div>
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
## Need help? ## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+3 -3
View File
@@ -7542,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.73.0: webpack@^5.73.0:
version "5.74.0" version "5.76.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
dependencies: dependencies:
"@types/eslint-scope" "^3.7.3" "@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51" "@types/estree" "^0.0.51"
+7 -7
View File
@@ -25,11 +25,6 @@
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5", "@testing-library/react": "12.1.5",
"@tldraw/vec": "1.7.1", "@tldraw/vec": "1.7.1",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1", "browser-fs-access": "0.29.1",
"clsx": "1.1.1", "clsx": "1.1.1",
"cross-env": "7.0.3", "cross-env": "7.0.3",
@@ -57,7 +52,6 @@
"sass": "1.51.0", "sass": "1.51.0",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"tunnel-rat": "0.1.0", "tunnel-rat": "0.1.0",
"typescript": "4.9.4",
"workbox-background-sync": "^6.5.4", "workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4", "workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4", "workbox-cacheable-response": "^6.5.4",
@@ -75,9 +69,14 @@
"@excalidraw/eslint-config": "1.0.0", "@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2", "@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0", "@types/chai": "4.3.0",
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7", "@types/lodash.throttle": "4.1.7",
"@types/pako": "1.0.3", "@types/pako": "1.0.3",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "0.1.7", "@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36",
"chai": "4.3.6", "chai": "4.3.6",
"dotenv": "16.0.1", "dotenv": "16.0.1",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
@@ -88,7 +87,8 @@
"lint-staged": "12.3.7", "lint-staged": "12.3.7",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"prettier": "2.6.2", "prettier": "2.6.2",
"rewire": "6.0.0" "rewire": "6.0.0",
"typescript": "4.9.4"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
+33 -2
View File
@@ -79,6 +79,7 @@
</style> </style>
<!-------------------------------------------------------------------------> <!------------------------------------------------------------------------->
<% if (process.env.NODE_ENV === "production") { %>
<script> <script>
// Redirect Excalidraw+ users which have auto-redirect enabled. // Redirect Excalidraw+ users which have auto-redirect enabled.
// //
@@ -97,6 +98,7 @@
window.location.href = "https://app.excalidraw.com"; window.location.href = "https://app.excalidraw.com";
} }
</script> </script>
<% } %>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
@@ -146,8 +148,10 @@
// setting this so that libraries installation reuses this window tab. // setting this so that libraries installation reuses this window tab.
window.name = "_excalidraw"; window.name = "_excalidraw";
</script> </script>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true' && <% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<!-- LEGACY GOOGLE ANALYTICS -->
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<script <script
async async
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%" src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
@@ -160,6 +164,33 @@
gtag("js", new Date()); gtag("js", new Date());
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%"); gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
</script> </script>
<% } %>
<!-- end LEGACY GOOGLE ANALYTICS -->
<!-- Matomo -->
<% if (process.env.REACT_APP_MATOMO_URL &&
process.env.REACT_APP_MATOMO_SITE_ID &&
process.env.REACT_APP_CDN_MATOMO_TRACKER_URL) { %>
<script>
var _paq = (window._paq = window._paq || []);
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
var u = "%REACT_APP_MATOMO_URL%";
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", "%REACT_APP_MATOMO_SITE_ID%"]);
var d = document,
g = d.createElement("script"),
s = d.getElementsByTagName("script")[0];
g.async = true;
g.src = "%REACT_APP_CDN_MATOMO_TRACKER_URL%";
s.parentNode.insertBefore(g, s);
})();
</script>
<% } %>
<!-- end Matomo analytics -->
<% } %> <% } %>
<!-- FIXME: remove this when we update CRA (fix SW caching) --> <!-- FIXME: remove this when we update CRA (fix SW caching) -->
+6
View File
@@ -2,6 +2,9 @@ const fs = require("fs");
const THRESSHOLD = 85; const THRESSHOLD = 85;
// we're using BCP 47 language tags as keys
// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
const crowdinMap = { const crowdinMap = {
"ar-SA": "en-ar", "ar-SA": "en-ar",
"bg-BG": "en-bg", "bg-BG": "en-bg",
@@ -52,6 +55,7 @@ const crowdinMap = {
"kk-KZ": "en-kk", "kk-KZ": "en-kk",
"vi-VN": "en-vi", "vi-VN": "en-vi",
"mr-IN": "en-mr", "mr-IN": "en-mr",
"th-TH": "en-th",
}; };
const flags = { const flags = {
@@ -104,6 +108,7 @@ const flags = {
"eu-ES": "🇪🇦", "eu-ES": "🇪🇦",
"vi-VN": "🇻🇳", "vi-VN": "🇻🇳",
"mr-IN": "🇮🇳", "mr-IN": "🇮🇳",
"th-TH": "🇹🇭",
}; };
const languages = { const languages = {
@@ -156,6 +161,7 @@ const languages = {
"zh-TW": "繁體中文", "zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt", "vi-VN": "Tiếng Việt",
"mr-IN": "मराठी", "mr-IN": "मराठी",
"th-TH": "ภาษาไทย",
}; };
const percentages = fs.readFileSync( const percentages = fs.readFileSync(
+113 -83
View File
@@ -1,7 +1,13 @@
import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import {
BOUND_TEXT_PADDING,
ROUNDNESS,
VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants";
import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { getNonDeletedElements, isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { import {
computeBoundTextPosition,
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
getBoundTextElement, getBoundTextElement,
measureText, measureText,
@@ -10,6 +16,7 @@ import {
import { import {
getOriginalContainerHeightFromCache, getOriginalContainerHeightFromCache,
resetOriginalContainerCache, resetOriginalContainerCache,
updateOriginalContainerCache,
} from "../element/textWysiwyg"; } from "../element/textWysiwyg";
import { import {
hasBoundTextElement, hasBoundTextElement,
@@ -23,6 +30,7 @@ import {
ExcalidrawTextElement, ExcalidrawTextElement,
} from "../element/types"; } from "../element/types";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
import { register } from "./register"; import { register } from "./register";
@@ -32,6 +40,7 @@ export const actionUnbindText = register({
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
return selectedElements.some((element) => hasBoundTextElement(element)); return selectedElements.some((element) => hasBoundTextElement(element));
}, },
perform: (elements, appState) => { perform: (elements, appState) => {
@@ -42,20 +51,24 @@ 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 } = measureText( const { width, height, baseline } = measureText(
boundTextElement.originalText, boundTextElement.originalText,
getFontString(boundTextElement), getFontString(boundTextElement),
boundTextElement.lineHeight,
); );
const originalContainerHeight = getOriginalContainerHeightFromCache( const originalContainerHeight = getOriginalContainerHeightFromCache(
element.id, element.id,
); );
resetOriginalContainerCache(element.id); resetOriginalContainerCache(element.id);
const { x, y } = computeBoundTextPosition(element, boundTextElement);
mutateElement(boundTextElement as ExcalidrawTextElement, { mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null, containerId: null,
width, width,
height, height,
baseline,
text: boundTextElement.originalText, text: boundTextElement.originalText,
x,
y,
}); });
mutateElement(element, { mutateElement(element, {
boundElements: element.boundElements?.filter( boundElements: element.boundElements?.filter(
@@ -125,6 +138,7 @@ export const actionBindText = register({
mutateElement(textElement, { mutateElement(textElement, {
containerId: container.id, containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER,
}); });
mutateElement(container, { mutateElement(container, {
boundElements: (container.boundElements || []).concat({ boundElements: (container.boundElements || []).concat({
@@ -132,7 +146,11 @@ export const actionBindText = register({
id: textElement.id, id: textElement.id,
}), }),
}); });
const originalContainerHeight = container.height;
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(textElement, container);
// overwritting the cache with original container height so
// it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight);
return { return {
elements: pushTextAboveContainer(elements, container, textElement), elements: pushTextAboveContainer(elements, container, textElement),
@@ -178,105 +196,117 @@ const pushContainerBelowText = (
return updatedElements; return updatedElements;
}; };
export const actionCreateContainerFromText = register({ export const actionWrapTextInContainer = register({
name: "createContainerFromText", name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText", contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1 && isTextElement(selectedElements[0]); const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
}, },
perform: (elements, appState) => { perform: (elements, appState) => {
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
); );
const updatedElements = elements.slice(); let updatedElements: readonly ExcalidrawElement[] = elements.slice();
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) { const containerIds: AppState["selectedElementIds"] = {};
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 for (const textElement of selectedElements) {
if (textElement.boundElements?.length) { if (isTextElement(textElement)) {
const linearElementIds = textElement.boundElements const container = newElement({
.filter((ele) => ele.type === "arrow") type: "rectangle",
.map((el) => el.id); backgroundColor: appState.currentItemBackgroundColor,
const linearElements = updatedElements.filter((ele) => boundElements: [
linearElementIds.includes(ele.id), ...(textElement.boundElements || []),
) as ExcalidrawLinearElement[]; { id: textElement.id, type: "text" },
linearElements.forEach((ele) => { ],
let startBinding = null; angle: textElement.angle,
let endBinding = null; fillStyle: appState.currentItemFillStyle,
if (ele.startBinding) { strokeColor: appState.currentItemStrokeColor,
startBinding = { ...ele.startBinding, elementId: container.id }; roughness: appState.currentItemRoughness,
} strokeWidth: appState.currentItemStrokeWidth,
if (ele.endBinding) { strokeStyle: appState.currentItemStrokeStyle,
endBinding = { ...ele.endBinding, elementId: container.id }; roundness:
} appState.currentItemRoundness === "round"
mutateElement(ele, { startBinding, endBinding }); ? {
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,
}); });
}
mutateElement(textElement, { // update bindings
containerId: container.id, if (textElement.boundElements?.length) {
verticalAlign: VERTICAL_ALIGN.MIDDLE, const linearElementIds = textElement.boundElements
boundElements: null, .filter((ele) => ele.type === "arrow")
}); .map((el) => el.id);
redrawTextBoundingBox(textElement, container); const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
return { if (startBinding?.elementId === textElement.id) {
elements: pushContainerBelowText( startBinding = {
[...elements, container], ...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding }, false);
}
});
}
mutateElement(
textElement,
{
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
},
false,
);
redrawTextBoundingBox(textElement, container);
updatedElements = pushContainerBelowText(
[...updatedElements, container],
container, container,
textElement, textElement,
), );
appState: { containerIds[container.id] = true;
...appState, }
selectedElementIds: {
[container.id]: true,
[textElement.id]: false,
},
},
commitToHistory: true,
};
} }
return { return {
elements: updatedElements, elements: updatedElements,
appState, appState: {
...appState,
selectedElementIds: containerIds,
},
commitToHistory: true, commitToHistory: true,
}; };
}, },
+1 -1
View File
@@ -226,7 +226,7 @@ const zoomValueToFitBoundsOnViewport = (
return clampedZoomValueToFitElements as NormalizedZoomValue; return clampedZoomValueToFitElements as NormalizedZoomValue;
}; };
const zoomToFitElements = ( export const zoomToFitElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
zoomToSelection: boolean, zoomToSelection: boolean,
+62 -35
View File
@@ -1,4 +1,5 @@
import { AppState } from "../../src/types"; import { AppState } from "../../src/types";
import { trackEvent } from "../analytics";
import { ButtonIconSelect } from "../components/ButtonIconSelect"; import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { IconPicker } from "../components/IconPicker"; import { IconPicker } from "../components/IconPicker";
@@ -37,6 +38,7 @@ import {
TextAlignLeftIcon, TextAlignLeftIcon,
TextAlignCenterIcon, TextAlignCenterIcon,
TextAlignRightIcon, TextAlignRightIcon,
FillZigZagIcon,
} from "../components/icons"; } from "../components/icons";
import { import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
@@ -54,6 +56,7 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
import { import {
getBoundTextElement, getBoundTextElement,
getContainerElement, getContainerElement,
getDefaultLineHeight,
} from "../element/textElement"; } from "../element/textElement";
import { import {
isBoundToContainer, isBoundToContainer,
@@ -293,7 +296,12 @@ export const actionChangeBackgroundColor = register({
export const actionChangeFillStyle = register({ export const actionChangeFillStyle = register({
name: "changeFillStyle", name: "changeFillStyle",
trackEvent: false, trackEvent: false,
perform: (elements, appState, value) => { perform: (elements, appState, value, app) => {
trackEvent(
"element",
"changeFillStyle",
`${value} (${app.device.isMobile ? "mobile" : "desktop"})`,
);
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(elements, appState, (el) =>
newElementWith(el, { newElementWith(el, {
@@ -304,40 +312,55 @@ export const actionChangeFillStyle = register({
commitToHistory: true, commitToHistory: true,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => {
<fieldset> const selectedElements = getSelectedElements(elements, appState);
<legend>{t("labels.fill")}</legend> const allElementsZigZag = selectedElements.every(
<ButtonIconSelect (el) => el.fillStyle === "zigzag",
options={[ );
{
value: "hachure", return (
text: t("labels.hachure"), <fieldset>
icon: FillHachureIcon, <legend>{t("labels.fill")}</legend>
}, <ButtonIconSelect
{ type="button"
value: "cross-hatch", options={[
text: t("labels.crossHatch"), {
icon: FillCrossHatchIcon, value: "hachure",
}, text: t("labels.hachure"),
{ icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
value: "solid", active: allElementsZigZag ? true : undefined,
text: t("labels.solid"), },
icon: FillSolidIcon, {
}, value: "cross-hatch",
]} text: t("labels.crossHatch"),
group="fill" icon: FillCrossHatchIcon,
value={getFormValue( },
elements, {
appState, value: "solid",
(element) => element.fillStyle, text: t("labels.solid"),
appState.currentItemFillStyle, icon: FillSolidIcon,
)} },
onChange={(value) => { ]}
updateData(value); value={getFormValue(
}} elements,
/> appState,
</fieldset> (element) => element.fillStyle,
), appState.currentItemFillStyle,
)}
onClick={(value, event) => {
const nextValue =
event.altKey &&
value === "hachure" &&
selectedElements.every((el) => el.fillStyle === "hachure")
? "zigzag"
: value;
updateData(nextValue);
}}
/>
</fieldset>
);
},
}); });
export const actionChangeStrokeWidth = register({ export const actionChangeStrokeWidth = register({
@@ -637,6 +660,7 @@ export const actionChangeFontFamily = register({
oldElement, oldElement,
{ {
fontFamily: value, fontFamily: value,
lineHeight: getDefaultLineHeight(value),
}, },
); );
redrawTextBoundingBox(newElement, getContainerElement(oldElement)); redrawTextBoundingBox(newElement, getContainerElement(oldElement));
@@ -745,16 +769,19 @@ export const actionChangeTextAlign = register({
value: "left", value: "left",
text: t("labels.left"), text: t("labels.left"),
icon: TextAlignLeftIcon, icon: TextAlignLeftIcon,
testId: "align-left",
}, },
{ {
value: "center", value: "center",
text: t("labels.center"), text: t("labels.center"),
icon: TextAlignCenterIcon, icon: TextAlignCenterIcon,
testId: "align-horizontal-center",
}, },
{ {
value: "right", value: "right",
text: t("labels.right"), text: t("labels.right"),
icon: TextAlignRightIcon, icon: TextAlignRightIcon,
testId: "align-right",
}, },
]} ]}
value={getFormValue( value={getFormValue(
+13 -4
View File
@@ -12,7 +12,10 @@ import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
} from "../constants"; } from "../constants";
import { getBoundTextElement } from "../element/textElement"; import {
getBoundTextElement,
getDefaultLineHeight,
} from "../element/textElement";
import { import {
hasBoundTextElement, hasBoundTextElement,
canApplyRoundnessTypeToElement, canApplyRoundnessTypeToElement,
@@ -92,12 +95,18 @@ export const actionPasteStyles = register({
}); });
if (isTextElement(newElement)) { if (isTextElement(newElement)) {
const fontSize =
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
const fontFamily =
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
newElement = newElementWith(newElement, { newElement = newElementWith(newElement, {
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE, fontSize,
fontFamily: fontFamily,
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
textAlign: textAlign:
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN, elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
lineHeight:
elementStylesToCopyFrom.lineHeight ||
getDefaultLineHeight(fontFamily),
}); });
let container = null; let container = null;
if (newElement.containerId) { if (newElement.containerId) {
+1 -1
View File
@@ -115,7 +115,7 @@ export type ActionName =
| "toggleLinearEditor" | "toggleLinearEditor"
| "toggleEraserTool" | "toggleEraserTool"
| "toggleHandTool" | "toggleHandTool"
| "createContainerFromText"; | "wrapTextInContainer";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
+30 -22
View File
@@ -1,22 +1,30 @@
export const trackEvent = export const trackEvent = (
typeof process !== "undefined" && category: string,
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID && action: string,
typeof window !== "undefined" && label?: string,
window.gtag value?: number,
? (category: string, action: string, label?: string, value?: number) => { ) => {
try { try {
window.gtag("event", action, { // Uncomment the next line to track locally
event_category: category, // console.log("Track Event", { category, action, label, value });
event_label: label,
value, if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
}); return;
} catch (error) { }
console.error("error logging to ga", error);
} if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) {
} window.gtag("event", action, {
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID event_category: category,
? (category: string, action: string, label?: string, value?: number) => {} event_label: label,
: (category: string, action: string, label?: string, value?: number) => { value,
// Uncomment the next line to track locally });
// console.log("Track Event", { category, action, label, value }); }
};
// MATOMO event tracking _paq must be same as the one in index.html
if (window._paq) {
window._paq.push(["trackEvent", category, action, label, value]);
}
} catch (error) {
console.error("error during analytics", error);
}
};
+9 -8
View File
@@ -1,5 +1,6 @@
import oc from "open-color"; import oc from "open-color";
import { import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
@@ -23,18 +24,18 @@ export const getDefaultAppState = (): Omit<
theme: THEME.LIGHT, theme: THEME.LIGHT,
collaborators: new Map(), collaborators: new Map(),
currentChartType: "bar", currentChartType: "bar",
currentItemBackgroundColor: "transparent", currentItemBackgroundColor: DEFAULT_ELEMENT_PROPS.backgroundColor,
currentItemEndArrowhead: "arrow", currentItemEndArrowhead: "arrow",
currentItemFillStyle: "hachure", currentItemFillStyle: DEFAULT_ELEMENT_PROPS.fillStyle,
currentItemFontFamily: DEFAULT_FONT_FAMILY, currentItemFontFamily: DEFAULT_FONT_FAMILY,
currentItemFontSize: DEFAULT_FONT_SIZE, currentItemFontSize: DEFAULT_FONT_SIZE,
currentItemOpacity: 100, currentItemOpacity: DEFAULT_ELEMENT_PROPS.opacity,
currentItemRoughness: 1, currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness,
currentItemStartArrowhead: null, currentItemStartArrowhead: null,
currentItemStrokeColor: oc.black, currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor,
currentItemRoundness: "round", currentItemRoundness: "round",
currentItemStrokeStyle: "solid", currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle,
currentItemStrokeWidth: 1, currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth,
currentItemTextAlign: DEFAULT_TEXT_ALIGN, currentItemTextAlign: DEFAULT_TEXT_ALIGN,
cursorButton: "up", cursorButton: "up",
draggingElement: null, draggingElement: null,
@@ -44,7 +45,7 @@ export const getDefaultAppState = (): Omit<
activeTool: { activeTool: {
type: "selection", type: "selection",
customType: null, customType: null,
locked: false, locked: DEFAULT_ELEMENT_PROPS.locked,
lastActiveTool: null, lastActiveTool: null,
}, },
penMode: false, penMode: false,
+1 -17
View File
@@ -1,10 +1,5 @@
import colors from "./colors"; import colors from "./colors";
import { import { DEFAULT_FONT_SIZE, ENV } from "./constants";
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
ENV,
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element"; import { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types"; import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random"; import { randomId } from "./random";
@@ -166,17 +161,7 @@ const bgColors = colors.elementBackground.slice(
// Put all the common properties here so when the whole chart is selected // Put all the common properties here so when the whole chart is selected
// the properties dialog shows the correct selected values // the properties dialog shows the correct selected values
const commonProps = { const commonProps = {
fillStyle: "hachure",
fontFamily: DEFAULT_FONT_FAMILY,
fontSize: DEFAULT_FONT_SIZE,
opacity: 100,
roughness: 1,
strokeColor: colors.elementStroke[0], strokeColor: colors.elementStroke[0],
roundness: null,
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
locked: false,
} as const; } as const;
const getChartDimentions = (spreadsheet: Spreadsheet) => { const getChartDimentions = (spreadsheet: Spreadsheet) => {
@@ -323,7 +308,6 @@ const chartBaseElements = (
x: x + chartWidth / 2, x: x + chartWidth / 2,
y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE, y: y - BAR_HEIGHT - BAR_GAP * 2 - DEFAULT_FONT_SIZE,
roundness: null, roundness: null,
strokeStyle: "solid",
textAlign: "center", textAlign: "center",
}) })
: null; : null;
+6 -2
View File
@@ -30,7 +30,10 @@ import clsx from "clsx";
import { actionToggleZenMode } from "../actions"; import { actionToggleZenMode } from "../actions";
import "./Actions.scss"; import "./Actions.scss";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import { shouldAllowVerticalAlign } from "../element/textElement"; import {
shouldAllowVerticalAlign,
suppportsHorizontalAlign,
} from "../element/textElement";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
@@ -122,7 +125,8 @@ export const SelectedShapeActions = ({
{renderAction("changeFontFamily")} {renderAction("changeFontFamily")}
{renderAction("changeTextAlign")} {suppportsHorizontalAlign(targetElements) &&
renderAction("changeTextAlign")}
</> </>
)} )}
+45
View File
@@ -0,0 +1,45 @@
import ReactDOM from "react-dom";
import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random";
import { render, queryByTestId } from "../tests/test-utils";
import ExcalidrawApp from "../excalidraw-app";
const renderScene = jest.spyOn(Renderer, "renderScene");
describe("Test <App/>", () => {
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
localStorage.clear();
renderScene.mockClear();
reseed(7);
});
it("should show error modal when using brave and measureText API is not working", async () => {
(global.navigator as any).brave = {
isBrave: {
name: "isBrave",
},
};
const originalContext = global.HTMLCanvasElement.prototype.getContext("2d");
//@ts-ignore
global.HTMLCanvasElement.prototype.getContext = (contextId) => {
return {
...originalContext,
measureText: () => ({
width: 0,
}),
};
};
await render(<ExcalidrawApp />);
expect(
queryByTestId(
document.querySelector(".excalidraw-modal-container")!,
"brave-measure-text-error",
),
).toMatchSnapshot();
});
});
+188 -62
View File
@@ -62,6 +62,7 @@ import {
GRID_SIZE, GRID_SIZE,
IMAGE_RENDER_TIMEOUT, IMAGE_RENDER_TIMEOUT,
isAndroid, isAndroid,
isBrave,
LINE_CONFIRM_THRESHOLD, LINE_CONFIRM_THRESHOLD,
MAX_ALLOWED_FILE_BYTES, MAX_ALLOWED_FILE_BYTES,
MIME_TYPES, MIME_TYPES,
@@ -126,7 +127,11 @@ import {
} from "../element/binding"; } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { mutateElement, newElementWith } from "../element/mutateElement";
import { deepCopyElement, newFreeDrawElement } from "../element/newElement"; import {
deepCopyElement,
duplicateElements,
newFreeDrawElement,
} from "../element/newElement";
import { import {
hasBoundTextElement, hasBoundTextElement,
isArrowElement, isArrowElement,
@@ -228,6 +233,7 @@ import {
updateActiveTool, updateActiveTool,
getShortcutKey, getShortcutKey,
isTransparent, isTransparent,
easeToValuesRAF,
} from "../utils"; } from "../utils";
import { import {
ContextMenu, ContextMenu,
@@ -259,14 +265,16 @@ import throttle from "lodash.throttle";
import { fileOpen, FileSystemHandle } from "../data/filesystem"; import { fileOpen, FileSystemHandle } from "../data/filesystem";
import { import {
bindTextToShapeAfterDuplication, bindTextToShapeAfterDuplication,
getApproxLineHeight,
getApproxMinLineHeight, getApproxMinLineHeight,
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElement, getBoundTextElement,
getContainerCenter, getContainerCenter,
getContainerDims, getContainerDims,
getContainerElement, getContainerElement,
getDefaultLineHeight,
getLineHeightInPx,
getTextBindableContainerAtPosition, getTextBindableContainerAtPosition,
isMeasureTextSupported,
isValidTextContainer, isValidTextContainer,
} from "../element/textElement"; } from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision"; import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
@@ -281,10 +289,14 @@ import {
import { shouldShowBoundingBox } from "../element/transformHandles"; 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,
zoomToFitElements,
} from "../actions/actionCanvas";
import { jotaiStore } from "../jotai"; import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionCreateContainerFromText } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
const deviceContextInitialValue = { const deviceContextInitialValue = {
isSmScreen: false, isSmScreen: false,
@@ -429,7 +441,6 @@ class App extends React.Component<AppProps, AppState> {
}; };
this.id = nanoid(); this.id = nanoid();
this.library = new Library(this); this.library = new Library(this);
if (excalidrawRef) { if (excalidrawRef) {
const readyPromise = const readyPromise =
@@ -711,6 +722,8 @@ class App extends React.Component<AppProps, AppState> {
const theme = const theme =
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT; actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
let name = actionResult?.appState?.name ?? this.state.name; let name = actionResult?.appState?.name ?? this.state.name;
const errorMessage =
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
if (typeof this.props.viewModeEnabled !== "undefined") { if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled; viewModeEnabled = this.props.viewModeEnabled;
} }
@@ -726,7 +739,6 @@ class App extends React.Component<AppProps, AppState> {
if (typeof this.props.name !== "undefined") { if (typeof this.props.name !== "undefined") {
name = this.props.name; name = this.props.name;
} }
this.setState( this.setState(
(state) => { (state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into // using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -744,6 +756,7 @@ class App extends React.Component<AppProps, AppState> {
gridSize, gridSize,
theme, theme,
name, name,
errorMessage,
}); });
}, },
() => { () => {
@@ -872,7 +885,6 @@ class App extends React.Component<AppProps, AppState> {
), ),
}; };
} }
// FontFaceSet loadingdone event we listen on may not always fire // FontFaceSet loadingdone event we listen on may not always fire
// (looking at you Safari), so on init we manually load fonts for current // (looking at you Safari), so on init we manually load fonts for current
// text elements on canvas, and rerender them once done. This also // text elements on canvas, and rerender them once done. This also
@@ -1000,6 +1012,13 @@ class App extends React.Component<AppProps, AppState> {
} else { } else {
this.updateDOMRect(this.initializeScene); this.updateDOMRect(this.initializeScene);
} }
// note that this check seems to always pass in localhost
if (isBrave() && !isMeasureTextSupported()) {
this.setState({
errorMessage: <BraveMeasureTextError />,
});
}
} }
public componentWillUnmount() { public componentWillUnmount() {
@@ -1610,35 +1629,22 @@ class App extends React.Component<AppProps, AppState> {
const dx = x - elementsCenterX; const dx = x - elementsCenterX;
const dy = y - elementsCenterY; const dy = y - elementsCenterY;
const groupIdMap = new Map();
const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize); const [gridX, gridY] = getGridPoint(dx, dy, this.state.gridSize);
const oldIdToDuplicatedId = new Map(); const newElements = duplicateElements(
const newElements = elements.map((element) => { elements.map((element) => {
const newElement = duplicateElement( return newElementWith(element, {
this.state.editingGroupId,
groupIdMap,
element,
{
x: element.x + gridX - minX, x: element.x + gridX - minX,
y: element.y + gridY - minY, y: element.y + gridY - minY,
}, });
); }),
oldIdToDuplicatedId.set(element.id, newElement.id); );
return newElement;
});
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
const nextElements = [ const nextElements = [
...this.scene.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
...newElements, ...newElements,
]; ];
fixBindingsAfterDuplication(nextElements, elements, oldIdToDuplicatedId);
if (opts.files) {
this.files = { ...this.files, ...opts.files };
}
this.scene.replaceAllElements(nextElements); this.scene.replaceAllElements(nextElements);
@@ -1649,6 +1655,10 @@ class App extends React.Component<AppProps, AppState> {
} }
}); });
if (opts.files) {
this.files = { ...this.files, ...opts.files };
}
this.history.resumeRecording(); this.history.resumeRecording();
this.setState( this.setState(
@@ -1721,12 +1731,14 @@ class App extends React.Component<AppProps, AppState> {
(acc: ExcalidrawTextElement[], line, idx) => { (acc: ExcalidrawTextElement[], line, idx) => {
const text = line.trim(); const text = line.trim();
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
if (text.length) { if (text.length) {
const element = newTextElement({ const element = newTextElement({
...textElementProps, ...textElementProps,
x, x,
y: currentY, y: currentY,
text, text,
lineHeight,
}); });
acc.push(element); acc.push(element);
currentY += element.height + LINE_GAP; currentY += element.height + LINE_GAP;
@@ -1735,14 +1747,9 @@ class App extends React.Component<AppProps, AppState> {
// add paragraph only if previous line was not empty, IOW don't add // add paragraph only if previous line was not empty, IOW don't add
// more than one empty line // more than one empty line
if (prevLine) { if (prevLine) {
const defaultLineHeight = getApproxLineHeight( currentY +=
getFontString({ getLineHeightInPx(textElementProps.fontSize, lineHeight) +
fontSize: textElementProps.fontSize, LINE_GAP;
fontFamily: textElementProps.fontFamily,
}),
);
currentY += defaultLineHeight + LINE_GAP;
} }
} }
@@ -1835,18 +1842,89 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.executeAction(actionToggleHandTool); this.actionManager.executeAction(actionToggleHandTool);
}; };
/**
* Zooms on canvas viewport center
*/
zoomCanvas = (
/** decimal fraction between 0.1 (10% zoom) and 30 (3000% zoom) */
value: number,
) => {
this.setState({
...getStateForZoom(
{
viewportX: this.state.width / 2 + this.state.offsetLeft,
viewportY: this.state.height / 2 + this.state.offsetTop,
nextZoom: getNormalizedZoom(value),
},
this.state,
),
});
};
private cancelInProgresAnimation: (() => void) | null = null;
scrollToContent = ( scrollToContent = (
target: target:
| ExcalidrawElement | ExcalidrawElement
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(), | readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
opts?: { fitToContent?: boolean; animate?: boolean; duration?: number },
) => { ) => {
this.setState({ this.cancelInProgresAnimation?.();
...calculateScrollCenter(
Array.isArray(target) ? target : [target], // convert provided target into ExcalidrawElement[] if necessary
this.state, const targets = Array.isArray(target) ? target : [target];
this.canvas,
), let zoom = this.state.zoom;
}); let scrollX = this.state.scrollX;
let scrollY = this.state.scrollY;
if (opts?.fitToContent) {
// compute an appropriate viewport location (scroll X, Y) and zoom level
// that fit the target elements on the scene
const { appState } = zoomToFitElements(targets, this.state, false);
zoom = appState.zoom;
scrollX = appState.scrollX;
scrollY = appState.scrollY;
} else {
// compute only the viewport location, without any zoom adjustment
const scroll = calculateScrollCenter(targets, this.state, this.canvas);
scrollX = scroll.scrollX;
scrollY = scroll.scrollY;
}
// when animating, we use RequestAnimationFrame to prevent the animation
// from slowing down other processes
if (opts?.animate) {
const origScrollX = this.state.scrollX;
const origScrollY = this.state.scrollY;
// zoom animation could become problematic on scenes with large number
// of elements, setting it to its final value to improve user experience.
//
// using zoomCanvas() to zoom on current viewport center
this.zoomCanvas(zoom.value);
const cancel = easeToValuesRAF(
[origScrollX, origScrollY],
[scrollX, scrollY],
(scrollX, scrollY) => this.setState({ scrollX, scrollY }),
{ duration: opts?.duration ?? 500 },
);
this.cancelInProgresAnimation = () => {
cancel();
this.cancelInProgresAnimation = null;
};
} else {
this.setState({ scrollX, scrollY, zoom });
}
};
/** use when changing scrollX/scrollY/zoom based on user interaction */
private translateCanvas: React.Component<any, AppState>["setState"] = (
state,
) => {
this.cancelInProgresAnimation?.();
this.setState(state);
}; };
setToast = ( setToast = (
@@ -2047,9 +2125,13 @@ class App extends React.Component<AppProps, AppState> {
offset = -offset; offset = -offset;
} }
if (event.shiftKey) { if (event.shiftKey) {
this.setState((state) => ({ scrollX: state.scrollX + offset })); this.translateCanvas((state) => ({
scrollX: state.scrollX + offset,
}));
} else { } else {
this.setState((state) => ({ scrollY: state.scrollY + offset })); this.translateCanvas((state) => ({
scrollY: state.scrollY + offset,
}));
} }
} }
@@ -2597,6 +2679,13 @@ class App extends React.Component<AppProps, AppState> {
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY); existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
} }
const fontFamily =
existingTextElement?.fontFamily || this.state.currentItemFontFamily;
const lineHeight =
existingTextElement?.lineHeight || getDefaultLineHeight(fontFamily);
const fontSize = this.state.currentItemFontSize;
if ( if (
!existingTextElement && !existingTextElement &&
shouldBindToContainer && shouldBindToContainer &&
@@ -2604,11 +2693,14 @@ class App extends React.Component<AppProps, AppState> {
!isArrowElement(container) !isArrowElement(container)
) { ) {
const fontString = { const fontString = {
fontSize: this.state.currentItemFontSize, fontSize,
fontFamily: this.state.currentItemFontFamily, fontFamily,
}; };
const minWidth = getApproxMinLineWidth(getFontString(fontString)); const minWidth = getApproxMinLineWidth(
const minHeight = getApproxMinLineHeight(getFontString(fontString)); getFontString(fontString),
lineHeight,
);
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const containerDims = getContainerDims(container); const containerDims = getContainerDims(container);
const newHeight = Math.max(containerDims.height, minHeight); const newHeight = Math.max(containerDims.height, minHeight);
const newWidth = Math.max(containerDims.width, minWidth); const newWidth = Math.max(containerDims.width, minWidth);
@@ -2640,10 +2732,9 @@ class App extends React.Component<AppProps, AppState> {
strokeStyle: this.state.currentItemStrokeStyle, strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness, roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity, opacity: this.state.currentItemOpacity,
roundness: null,
text: "", text: "",
fontSize: this.state.currentItemFontSize, fontSize,
fontFamily: this.state.currentItemFontFamily, fontFamily,
textAlign: parentCenterPosition textAlign: parentCenterPosition
? "center" ? "center"
: this.state.currentItemTextAlign, : this.state.currentItemTextAlign,
@@ -2652,7 +2743,7 @@ class App extends React.Component<AppProps, AppState> {
: DEFAULT_VERTICAL_ALIGN, : DEFAULT_VERTICAL_ALIGN,
containerId: shouldBindToContainer ? container?.id : undefined, containerId: shouldBindToContainer ? container?.id : undefined,
groupIds: container?.groupIds ?? [], groupIds: container?.groupIds ?? [],
locked: false, lineHeight,
}); });
if (!existingTextElement && shouldBindToContainer && container) { if (!existingTextElement && shouldBindToContainer && container) {
@@ -2919,12 +3010,12 @@ class App extends React.Component<AppProps, AppState> {
state, state,
); );
return { this.translateCanvas({
zoom: zoomState.zoom, zoom: zoomState.zoom,
scrollX: zoomState.scrollX + deltaX / nextZoom, scrollX: zoomState.scrollX + deltaX / nextZoom,
scrollY: zoomState.scrollY + deltaY / nextZoom, scrollY: zoomState.scrollY + deltaY / nextZoom,
shouldCacheIgnoreZoom: true, shouldCacheIgnoreZoom: true,
}; });
}); });
this.resetShouldCacheIgnoreZoomDebounced(); this.resetShouldCacheIgnoreZoomDebounced();
} else { } else {
@@ -3402,6 +3493,43 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ contextMenu: null }); this.setState({ contextMenu: null });
} }
this.updateGestureOnPointerDown(event);
// if dragging element is freedraw and another pointerdown event occurs
// a second finger is on the screen
// discard the freedraw element if it is very short because it is likely
// just a spike, otherwise finalize the freedraw element when the second
// finger is lifted
if (
event.pointerType === "touch" &&
this.state.draggingElement &&
this.state.draggingElement.type === "freedraw"
) {
const element = this.state.draggingElement as ExcalidrawFreeDrawElement;
this.updateScene({
...(element.points.length < 10
? {
elements: this.scene
.getElementsIncludingDeleted()
.filter((el) => el.id !== element.id),
}
: {}),
appState: {
draggingElement: null,
editingElement: null,
startBoundElement: null,
suggestedBindings: [],
selectedElementIds: Object.keys(this.state.selectedElementIds)
.filter((key) => key !== element.id)
.reduce((obj: { [id: string]: boolean }, key) => {
obj[key] = this.state.selectedElementIds[key];
return obj;
}, {}),
},
});
return;
}
// remove any active selection when we start to interact with canvas // remove any active selection when we start to interact with canvas
// (mainly, we care about removing selection outside the component which // (mainly, we care about removing selection outside the component which
// would prevent our copy handling otherwise) // would prevent our copy handling otherwise)
@@ -3441,8 +3569,6 @@ class App extends React.Component<AppProps, AppState> {
}); });
this.savePointer(event.clientX, event.clientY, "down"); this.savePointer(event.clientX, event.clientY, "down");
this.updateGestureOnPointerDown(event);
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) { if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
return; return;
} }
@@ -3700,7 +3826,7 @@ class App extends React.Component<AppProps, AppState> {
window.addEventListener(EVENT.POINTER_UP, enableNextPaste); window.addEventListener(EVENT.POINTER_UP, enableNextPaste);
} }
this.setState({ this.translateCanvas({
scrollX: this.state.scrollX - deltaX / this.state.zoom.value, scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
scrollY: this.state.scrollY - deltaY / this.state.zoom.value, scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
}); });
@@ -4846,7 +4972,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.scrollbars.isOverHorizontal) { if (pointerDownState.scrollbars.isOverHorizontal) {
const x = event.clientX; const x = event.clientX;
const dx = x - pointerDownState.lastCoords.x; const dx = x - pointerDownState.lastCoords.x;
this.setState({ this.translateCanvas({
scrollX: this.state.scrollX - dx / this.state.zoom.value, scrollX: this.state.scrollX - dx / this.state.zoom.value,
}); });
pointerDownState.lastCoords.x = x; pointerDownState.lastCoords.x = x;
@@ -4856,7 +4982,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.scrollbars.isOverVertical) { if (pointerDownState.scrollbars.isOverVertical) {
const y = event.clientY; const y = event.clientY;
const dy = y - pointerDownState.lastCoords.y; const dy = y - pointerDownState.lastCoords.y;
this.setState({ this.translateCanvas({
scrollY: this.state.scrollY - dy / this.state.zoom.value, scrollY: this.state.scrollY - dy / this.state.zoom.value,
}); });
pointerDownState.lastCoords.y = y; pointerDownState.lastCoords.y = y;
@@ -6238,7 +6364,7 @@ class App extends React.Component<AppProps, AppState> {
actionGroup, actionGroup,
actionUnbindText, actionUnbindText,
actionBindText, actionBindText,
actionCreateContainerFromText, actionWrapTextInContainer,
actionUngroup, actionUngroup,
CONTEXT_MENU_SEPARATOR, CONTEXT_MENU_SEPARATOR,
actionAddToLibrary, actionAddToLibrary,
@@ -6285,7 +6411,7 @@ class App extends React.Component<AppProps, AppState> {
// reduced amplification for small deltas (small movements on a trackpad) // reduced amplification for small deltas (small movements on a trackpad)
Math.min(1, absDelta / 20); Math.min(1, absDelta / 20);
this.setState((state) => ({ this.translateCanvas((state) => ({
...getStateForZoom( ...getStateForZoom(
{ {
viewportX: cursorX, viewportX: cursorX,
@@ -6302,14 +6428,14 @@ class App extends React.Component<AppProps, AppState> {
// scroll horizontally when shift pressed // scroll horizontally when shift pressed
if (event.shiftKey) { if (event.shiftKey) {
this.setState(({ zoom, scrollX }) => ({ this.translateCanvas(({ zoom, scrollX }) => ({
// on Mac, shift+wheel tends to result in deltaX // on Mac, shift+wheel tends to result in deltaX
scrollX: scrollX - (deltaY || deltaX) / zoom.value, scrollX: scrollX - (deltaY || deltaX) / zoom.value,
})); }));
return; return;
} }
this.setState(({ zoom, scrollX, scrollY }) => ({ this.translateCanvas(({ zoom, scrollX, scrollY }) => ({
scrollX: scrollX - deltaX / zoom.value, scrollX: scrollX - deltaX / zoom.value,
scrollY: scrollY - deltaY / zoom.value, scrollY: scrollY - deltaY / zoom.value,
})); }));
+42
View File
@@ -0,0 +1,42 @@
import { t } from "../i18n";
const BraveMeasureTextError = () => {
return (
<div data-testid="brave-measure-text-error">
<p>
{t("errors.brave_measure_text_error.start")} &nbsp;
<span style={{ fontWeight: 600 }}>
{t("errors.brave_measure_text_error.aggressive_block_fingerprint")}
</span>{" "}
{t("errors.brave_measure_text_error.setting_enabled")}.
<br />
<br />
{t("errors.brave_measure_text_error.break")}{" "}
<span style={{ fontWeight: 600 }}>
{t("errors.brave_measure_text_error.text_elements")}
</span>{" "}
{t("errors.brave_measure_text_error.in_your_drawings")}.
</p>
<p>
{t("errors.brave_measure_text_error.strongly_recommend")}{" "}
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
{" "}
{t("errors.brave_measure_text_error.steps")}
</a>{" "}
{t("errors.brave_measure_text_error.how")}.
</p>
<p>
{t("errors.brave_measure_text_error.disable_setting")}{" "}
<a href="https://github.com/excalidraw/excalidraw/issues/new">
{t("errors.brave_measure_text_error.issue")}
</a>{" "}
{t("errors.brave_measure_text_error.write")}{" "}
<a href="https://discord.gg/UexuTaE">
{t("errors.brave_measure_text_error.discord")}
</a>
.
</p>
</div>
);
};
export default BraveMeasureTextError;
+52 -26
View File
@@ -1,33 +1,59 @@
import clsx from "clsx"; import clsx from "clsx";
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect /> // TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
export const ButtonIconSelect = <T extends Object>({ export const ButtonIconSelect = <T extends Object>(
options, props: {
value, options: {
onChange, value: T;
group, text: string;
}: { icon: JSX.Element;
options: { value: T; text: string; icon: JSX.Element; testId?: string }[]; testId?: string;
value: T | null; /** if not supplied, defaults to value identity check */
onChange: (value: T) => void; active?: boolean;
group: string; }[];
}) => ( value: T | null;
type?: "radio" | "button";
} & (
| { type?: "radio"; group: string; onChange: (value: T) => void }
| {
type: "button";
onClick: (
value: T,
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
) => void;
}
),
) => (
<div className="buttonList buttonListIcon"> <div className="buttonList buttonListIcon">
{options.map((option) => ( {props.options.map((option) =>
<label props.type === "button" ? (
key={option.text} <button
className={clsx({ active: value === option.value })} key={option.text}
title={option.text} onClick={(event) => props.onClick(option.value, event)}
> className={clsx({
<input active: option.active ?? props.value === option.value,
type="radio" })}
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value}
data-testid={option.testId} data-testid={option.testId}
/> title={option.text}
{option.icon} >
</label> {option.icon}
))} </button>
) : (
<label
key={option.text}
className={clsx({ active: props.value === option.value })}
title={option.text}
>
<input
type="radio"
name={props.group}
onChange={() => props.onChange(option.value)}
checked={props.value === option.value}
data-testid={option.testId}
/>
{option.icon}
</label>
),
)}
</div> </div>
); );
+4 -4
View File
@@ -5,13 +5,13 @@ import { Dialog } from "./Dialog";
import { useExcalidrawContainer } from "./App"; import { useExcalidrawContainer } from "./App";
export const ErrorDialog = ({ export const ErrorDialog = ({
message, children,
onClose, onClose,
}: { }: {
message: string; children?: React.ReactNode;
onClose?: () => void; onClose?: () => void;
}) => { }) => {
const [modalIsShown, setModalIsShown] = useState(!!message); const [modalIsShown, setModalIsShown] = useState(!!children);
const { container: excalidrawContainer } = useExcalidrawContainer(); const { container: excalidrawContainer } = useExcalidrawContainer();
const handleClose = React.useCallback(() => { const handleClose = React.useCallback(() => {
@@ -32,7 +32,7 @@ export const ErrorDialog = ({
onCloseRequest={handleClose} onCloseRequest={handleClose}
title={t("errorDialog.title")} title={t("errorDialog.title")}
> >
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div> <div style={{ whiteSpace: "pre-wrap" }}>{children}</div>
</Dialog> </Dialog>
)} )}
</> </>
+4
View File
@@ -9,6 +9,10 @@
text-align: center; text-align: center;
padding: var(--preview-padding); padding: var(--preview-padding);
margin-bottom: calc(var(--space-factor) * 3); margin-bottom: calc(var(--space-factor) * 3);
display: flex;
justify-content: center;
align-items: center;
} }
.ExportDialog__preview canvas { .ExportDialog__preview canvas {
+6 -5
View File
@@ -165,11 +165,12 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={[KEYS.E, KEYS["0"]]} shortcuts={[KEYS.E, KEYS["0"]]}
/> />
<Shortcut <Shortcut
label={t("helpDialog.editSelectedShape")} label={t("helpDialog.editLineArrowPoints")}
shortcuts={[ shortcuts={[getShortcutKey("CtrlOrCmd+Enter")]}
getShortcutKey("CtrlOrCmd+Enter"), />
getShortcutKey(`CtrlOrCmd + ${t("helpDialog.doubleClick")}`), <Shortcut
]} label={t("helpDialog.editText")}
shortcuts={[getShortcutKey("Enter")]}
/> />
<Shortcut <Shortcut
label={t("helpDialog.textNewLine")} label={t("helpDialog.textNewLine")}
+11 -13
View File
@@ -4,7 +4,6 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types"; import { AppState, BinaryFiles } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { clipboard } from "./icons"; import { clipboard } from "./icons";
@@ -15,6 +14,7 @@ import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants"; import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
import { nativeFileSystemSupported } from "../data/filesystem"; import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { exportToCanvas } from "../packages/utils";
const supportsContextFilters = const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!; "filter" in document.createElement("canvas").getContext("2d")!;
@@ -83,7 +83,6 @@ const ImageExportModal = ({
const someElementIsSelected = isSomeElementSelected(elements, appState); const someElementIsSelected = isSomeElementSelected(elements, appState);
const [exportSelected, setExportSelected] = useState(someElementIsSelected); const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
const { exportBackground, viewBackgroundColor } = appState;
const [renderError, setRenderError] = useState<Error | null>(null); const [renderError, setRenderError] = useState<Error | null>(null);
const exportedElements = exportSelected const exportedElements = exportSelected
@@ -99,10 +98,16 @@ const ImageExportModal = ({
if (!previewNode) { if (!previewNode) {
return; return;
} }
exportToCanvas(exportedElements, appState, files, { const maxWidth = previewNode.offsetWidth;
exportBackground, if (!maxWidth) {
viewBackgroundColor, return;
}
exportToCanvas({
elements: exportedElements,
appState,
files,
exportPadding, exportPadding,
maxWidthOrHeight: maxWidth,
}) })
.then((canvas) => { .then((canvas) => {
setRenderError(null); setRenderError(null);
@@ -116,14 +121,7 @@ const ImageExportModal = ({
console.error(error); console.error(error);
setRenderError(error); setRenderError(error);
}); });
}, [ }, [appState, files, exportedElements, exportPadding]);
appState,
files,
exportedElements,
exportBackground,
exportPadding,
viewBackgroundColor,
]);
return ( return (
<div className="ExportDialog"> <div className="ExportDialog">
+3 -4
View File
@@ -364,10 +364,9 @@ const LayerUI = ({
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && ( {appState.errorMessage && (
<ErrorDialog <ErrorDialog onClose={() => setAppState({ errorMessage: null })}>
message={appState.errorMessage} {appState.errorMessage}
onClose={() => setAppState({ errorMessage: null })} </ErrorDialog>
/>
)} )}
{appState.openDialog === "help" && ( {appState.openDialog === "help" && (
<HelpDialog <HelpDialog
+9 -1
View File
@@ -12,6 +12,7 @@ import { MIME_TYPES } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton"; import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import clsx from "clsx"; import clsx from "clsx";
import { duplicateElements } from "../element/newElement";
const CELLS_PER_ROW = 4; const CELLS_PER_ROW = 4;
@@ -96,7 +97,14 @@ const LibraryMenuItems = ({
} else { } else {
targetElements = libraryItems.filter((item) => item.id === id); targetElements = libraryItems.filter((item) => item.id === id);
} }
return targetElements; return targetElements.map((item) => {
return {
...item,
// duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465
elements: duplicateElements(item.elements),
};
});
}; };
const createLibraryItemCompo = (params: { const createLibraryItemCompo = (params: {
+5 -5
View File
@@ -2,7 +2,7 @@ import clsx from "clsx";
import oc from "open-color"; import oc from "open-color";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useDevice } from "../components/App"; import { useDevice } from "../components/App";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../packages/utils";
import { LibraryItem } from "../types"; import { LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
@@ -36,14 +36,14 @@ export const LibraryUnit = ({
if (!elements) { if (!elements) {
return; return;
} }
const svg = await exportToSvg( const svg = await exportToSvg({
elements, elements,
{ appState: {
exportBackground: false, exportBackground: false,
viewBackgroundColor: oc.white, viewBackgroundColor: oc.white,
}, },
null, files: null,
); });
svg.querySelector(".style-fonts")?.remove(); svg.querySelector(".style-fonts")?.remove();
node.innerHTML = svg.outerHTML; node.innerHTML = svg.outerHTML;
})(); })();
+1
View File
@@ -3,5 +3,6 @@
position: absolute; position: absolute;
z-index: 10; z-index: 10;
padding: 5px 0 5px; padding: 5px 0 5px;
outline: none;
} }
} }
+66 -26
View File
@@ -29,13 +29,21 @@ export const Popover = ({
}: Props) => { }: Props) => {
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);
const container = popoverRef.current;
useEffect(() => { useEffect(() => {
const container = popoverRef.current;
if (!container) { if (!container) {
return; return;
} }
// focus popover only if the caller didn't focus on something else nested
// within the popover, which should take precedence. Fixes cases
// like color picker listening to keydown events on containers nested
// in the popover.
if (!container.contains(document.activeElement)) {
container.focus();
}
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.TAB) { if (event.key === KEYS.TAB) {
const focusableElements = queryFocusableElements(container); const focusableElements = queryFocusableElements(container);
@@ -44,15 +52,23 @@ export const Popover = ({
(element) => element === activeElement, (element) => element === activeElement,
); );
if (currentIndex === 0 && event.shiftKey) { if (activeElement === container) {
focusableElements[focusableElements.length - 1].focus(); if (event.shiftKey) {
focusableElements[focusableElements.length - 1]?.focus();
} else {
focusableElements[0].focus();
}
event.preventDefault();
event.stopImmediatePropagation();
} else if (currentIndex === 0 && event.shiftKey) {
focusableElements[focusableElements.length - 1]?.focus();
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} else if ( } else if (
currentIndex === focusableElements.length - 1 && currentIndex === focusableElements.length - 1 &&
!event.shiftKey !event.shiftKey
) { ) {
focusableElements[0].focus(); focusableElements[0]?.focus();
event.preventDefault(); event.preventDefault();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
} }
@@ -62,35 +78,59 @@ export const Popover = ({
container.addEventListener("keydown", handleKeyDown); container.addEventListener("keydown", handleKeyDown);
return () => container.removeEventListener("keydown", handleKeyDown); return () => container.removeEventListener("keydown", handleKeyDown);
}, [container]); }, []);
const lastInitializedPosRef = useRef<{ top: number; left: number } | null>(
null,
);
// ensure the popover doesn't overflow the viewport // ensure the popover doesn't overflow the viewport
useLayoutEffect(() => { useLayoutEffect(() => {
if (fitInViewport && popoverRef.current) { if (fitInViewport && popoverRef.current && top != null && left != null) {
const element = popoverRef.current; const container = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect(); const { width, height } = container.getBoundingClientRect();
//Position correctly when clicked on rightmost part or the bottom part of viewport // hack for StrictMode so this effect only runs once for
if (x + width - offsetLeft > viewportWidth) { // the same top/left position, otherwise
element.style.left = `${viewportWidth - width - 10}px`; // we'd potentically reposition twice (once for viewport overflow)
} // and once for top/left position afterwards
if (y + height - offsetTop > viewportHeight) { if (
element.style.top = `${viewportHeight - height}px`; lastInitializedPosRef.current?.top === top &&
lastInitializedPosRef.current?.left === left
) {
return;
} }
lastInitializedPosRef.current = { top, left };
//Resize to fit viewport on smaller screens
if (height >= viewportHeight) {
element.style.height = `${viewportHeight - 20}px`;
element.style.top = "10px";
element.style.overflowY = "scroll";
}
if (width >= viewportWidth) { if (width >= viewportWidth) {
element.style.width = `${viewportWidth}px`; container.style.width = `${viewportWidth}px`;
element.style.left = "0px"; container.style.left = "0px";
element.style.overflowX = "scroll"; container.style.overflowX = "scroll";
} else if (left + width - offsetLeft > viewportWidth) {
container.style.left = `${viewportWidth - width - 10}px`;
} else {
container.style.left = `${left}px`;
}
if (height >= viewportHeight) {
container.style.height = `${viewportHeight - 20}px`;
container.style.top = "10px";
container.style.overflowY = "scroll";
} else if (top + height - offsetTop > viewportHeight) {
container.style.top = `${viewportHeight - height}px`;
} else {
container.style.top = `${top}px`;
} }
} }
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]); }, [
top,
left,
fitInViewport,
viewportWidth,
viewportHeight,
offsetLeft,
offsetTop,
]);
useEffect(() => { useEffect(() => {
if (onCloseRequest) { if (onCloseRequest) {
@@ -105,7 +145,7 @@ export const Popover = ({
}, [onCloseRequest]); }, [onCloseRequest]);
return ( return (
<div className="popover" style={{ top, left }} ref={popoverRef}> <div className="popover" ref={popoverRef} tabIndex={-1}>
{children} {children}
</div> </div>
); );
+76
View File
@@ -93,4 +93,80 @@
display: block; display: block;
} }
} }
.single-library-item {
position: relative;
&-status {
position: absolute;
top: 0.3rem;
left: 0.3rem;
font-size: 0.7rem;
color: $oc-red-7;
background: rgba(255, 255, 255, 0.9);
padding: 0.1rem 0.2rem;
border-radius: 0.2rem;
}
&__svg {
background-color: $oc-white;
padding: 0.3rem;
width: 7.5rem;
height: 7.5rem;
border: 1px solid var(--button-gray-2);
svg {
width: 100%;
height: 100%;
}
}
.ToolIcon__icon {
background-color: $oc-white;
width: auto;
height: auto;
margin: 0 0.5rem;
}
.ToolIcon,
.ToolIcon_type_button:hover {
background-color: white;
}
.required,
.error {
color: $oc-red-8;
font-weight: bold;
font-size: 1rem;
margin: 0.2rem;
}
.error {
font-weight: 500;
margin: 0;
padding: 0.3em 0;
}
&--remove {
position: absolute;
top: 0.2rem;
right: 1rem;
.ToolIcon__icon {
margin: 0;
}
.ToolIcon__icon {
background-color: $oc-red-6;
&:hover {
background-color: $oc-red-7;
}
&:active {
background-color: $oc-red-8;
}
}
svg {
color: $oc-white;
padding: 0.26rem;
border-radius: 0.3em;
width: 1rem;
height: 1rem;
}
}
}
} }
+99 -5
View File
@@ -1,11 +1,11 @@
import { ReactNode, useCallback, useEffect, useState } from "react"; import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import OpenColor from "open-color"; import OpenColor from "open-color";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState, LibraryItems, LibraryItem } from "../types"; import { AppState, LibraryItems, LibraryItem } from "../types";
import { exportToCanvas } from "../packages/utils"; import { exportToCanvas, exportToSvg } from "../packages/utils";
import { import {
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
EXPORT_SOURCE, EXPORT_SOURCE,
@@ -13,12 +13,13 @@ import {
VERSIONS, VERSIONS,
} from "../constants"; } from "../constants";
import { ExportedLibraryData } from "../data/types"; import { ExportedLibraryData } from "../data/types";
import "./PublishLibrary.scss";
import SingleLibraryItem from "./SingleLibraryItem";
import { canvasToBlob, resizeImageFile } from "../data/blob"; import { canvasToBlob, resizeImageFile } from "../data/blob";
import { chunk } from "../utils"; import { chunk } from "../utils";
import DialogActionButton from "./DialogActionButton"; import DialogActionButton from "./DialogActionButton";
import { CloseIcon } from "./icons";
import { ToolButton } from "./ToolButton";
import "./PublishLibrary.scss";
interface PublishLibraryDataParams { interface PublishLibraryDataParams {
authorName: string; authorName: string;
@@ -126,6 +127,99 @@ const generatePreviewImage = async (libraryItems: LibraryItems) => {
); );
}; };
const SingleLibraryItem = ({
libItem,
appState,
index,
onChange,
onRemove,
}: {
libItem: LibraryItem;
appState: AppState;
index: number;
onChange: (val: string, index: number) => void;
onRemove: (id: string) => void;
}) => {
const svgRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const node = svgRef.current;
if (!node) {
return;
}
(async () => {
const svg = await exportToSvg({
elements: libItem.elements,
appState: {
...appState,
viewBackgroundColor: OpenColor.white,
exportBackground: true,
},
files: null,
});
node.innerHTML = svg.outerHTML;
})();
}, [libItem.elements, appState]);
return (
<div className="single-library-item">
{libItem.status === "published" && (
<span className="single-library-item-status">
{t("labels.statusPublished")}
</span>
)}
<div ref={svgRef} className="single-library-item__svg" />
<ToolButton
aria-label={t("buttons.remove")}
type="button"
icon={CloseIcon}
className="single-library-item--remove"
onClick={onRemove.bind(null, libItem.id)}
title={t("buttons.remove")}
/>
<div
style={{
display: "flex",
margin: "0.8rem 0",
width: "100%",
fontSize: "14px",
fontWeight: 500,
flexDirection: "column",
}}
>
<label
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
}}
>
<div style={{ padding: "0.5em 0" }}>
<span style={{ fontWeight: 500, color: OpenColor.gray[6] }}>
{t("publishDialog.itemName")}
</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
ref={inputRef}
style={{ width: "80%", padding: "0.2rem" }}
defaultValue={libItem.name}
placeholder="Item name"
onChange={(event) => {
onChange(event.target.value, index);
}}
/>
</label>
<span className="error">{libItem.error}</span>
</div>
</div>
);
};
const PublishLibrary = ({ const PublishLibrary = ({
onClose, onClose,
libraryItems, libraryItems,
-79
View File
@@ -1,79 +0,0 @@
@import "../css/variables.module";
.excalidraw {
.single-library-item {
position: relative;
&-status {
position: absolute;
top: 0.3rem;
left: 0.3rem;
font-size: 0.7rem;
color: $oc-red-7;
background: rgba(255, 255, 255, 0.9);
padding: 0.1rem 0.2rem;
border-radius: 0.2rem;
}
&__svg {
background-color: $oc-white;
padding: 0.3rem;
width: 7.5rem;
height: 7.5rem;
border: 1px solid var(--button-gray-2);
svg {
width: 100%;
height: 100%;
}
}
.ToolIcon__icon {
background-color: $oc-white;
width: auto;
height: auto;
margin: 0 0.5rem;
}
.ToolIcon,
.ToolIcon_type_button:hover {
background-color: white;
}
.required,
.error {
color: $oc-red-8;
font-weight: bold;
font-size: 1rem;
margin: 0.2rem;
}
.error {
font-weight: 500;
margin: 0;
padding: 0.3em 0;
}
&--remove {
position: absolute;
top: 0.2rem;
right: 1rem;
.ToolIcon__icon {
margin: 0;
}
.ToolIcon__icon {
background-color: $oc-red-6;
&:hover {
background-color: $oc-red-7;
}
&:active {
background-color: $oc-red-8;
}
}
svg {
color: $oc-white;
padding: 0.26rem;
border-radius: 0.3em;
width: 1rem;
height: 1rem;
}
}
}
}
-104
View File
@@ -1,104 +0,0 @@
import oc from "open-color";
import { useEffect, useRef } from "react";
import { t } from "../i18n";
import { exportToSvg } from "../packages/utils";
import { AppState, LibraryItem } from "../types";
import { CloseIcon } from "./icons";
import "./SingleLibraryItem.scss";
import { ToolButton } from "./ToolButton";
const SingleLibraryItem = ({
libItem,
appState,
index,
onChange,
onRemove,
}: {
libItem: LibraryItem;
appState: AppState;
index: number;
onChange: (val: string, index: number) => void;
onRemove: (id: string) => void;
}) => {
const svgRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const node = svgRef.current;
if (!node) {
return;
}
(async () => {
const svg = await exportToSvg({
elements: libItem.elements,
appState: {
...appState,
viewBackgroundColor: oc.white,
exportBackground: true,
},
files: null,
});
node.innerHTML = svg.outerHTML;
})();
}, [libItem.elements, appState]);
return (
<div className="single-library-item">
{libItem.status === "published" && (
<span className="single-library-item-status">
{t("labels.statusPublished")}
</span>
)}
<div ref={svgRef} className="single-library-item__svg" />
<ToolButton
aria-label={t("buttons.remove")}
type="button"
icon={CloseIcon}
className="single-library-item--remove"
onClick={onRemove.bind(null, libItem.id)}
title={t("buttons.remove")}
/>
<div
style={{
display: "flex",
margin: "0.8rem 0",
width: "100%",
fontSize: "14px",
fontWeight: 500,
flexDirection: "column",
}}
>
<label
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
}}
>
<div style={{ padding: "0.5em 0" }}>
<span style={{ fontWeight: 500, color: oc.gray[6] }}>
{t("publishDialog.itemName")}
</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
ref={inputRef}
style={{ width: "80%", padding: "0.2rem" }}
defaultValue={libItem.name}
placeholder="Item name"
onChange={(event) => {
onChange(event.target.value, index);
}}
/>
</label>
<span className="error">{libItem.error}</span>
</div>
</div>
);
};
export default SingleLibraryItem;
@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
<div
data-testid="brave-measure-text-error"
>
<p>
Looks like you are using Brave browser with the
 
<span
style="font-weight: 600;"
>
Aggressively Block Fingerprinting
</span>
setting enabled
.
<br />
<br />
This could result in breaking the
<span
style="font-weight: 600;"
>
Text Elements
</span>
in your drawings
.
</p>
<p>
We strongly recommend disabling this setting. You can follow
<a
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
>
these steps
</a>
on how to do so
.
</p>
<p>
If disabling this setting doesn't fix the display of text elements, please open an
<a
href="https://github.com/excalidraw/excalidraw/issues/new"
>
issue
</a>
on our GitHub, or write us on
<a
href="https://discord.gg/UexuTaE"
>
Discord
</a>
.
</p>
</div>
`;
+7
View File
@@ -1008,6 +1008,13 @@ export const UngroupIcon = React.memo(({ theme }: { theme: Theme }) =>
), ),
); );
export const FillZigZagIcon = createIcon(
<g strokeWidth={1.25}>
<path d="M5.879 2.625h8.242a3.27 3.27 0 0 1 3.254 3.254v8.242a3.27 3.27 0 0 1-3.254 3.254H5.88a3.27 3.27 0 0 1-3.254-3.254V5.88A3.27 3.27 0 0 1 5.88 2.626l-.001-.001ZM4.518 16.118l7.608-12.83m.198 13.934 5.051-9.897M2.778 9.675l9.348-6.387m-7.608 12.83 12.857-8.793" />
</g>,
modifiedTablerIconProps,
);
export const FillHachureIcon = createIcon( export const FillHachureIcon = createIcon(
<> <>
<path <path
+25 -1
View File
@@ -1,6 +1,7 @@
import cssVariables from "./css/variables.module.scss"; import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types"; import { AppProps } from "./types";
import { FontFamilyValues } from "./element/types"; import { ExcalidrawElement, FontFamilyValues } from "./element/types";
import oc from "open-color";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform); export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
export const isWindows = /^Win/.test(navigator.platform); export const isWindows = /^Win/.test(navigator.platform);
@@ -12,6 +13,9 @@ export const isFirefox =
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1; export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
export const isSafari = export const isSafari =
!isChrome && navigator.userAgent.indexOf("Safari") !== -1; !isChrome && navigator.userAgent.indexOf("Safari") !== -1;
// keeping function so it can be mocked in test
export const isBrave = () =>
(navigator as any).brave?.isBrave?.name === "isBrave";
export const APP_NAME = "Excalidraw"; export const APP_NAME = "Excalidraw";
@@ -251,3 +255,23 @@ export const ROUNDNESS = {
/** key containt id of precedeing elemnt id we use in reconciliation during /** key containt id of precedeing elemnt id we use in reconciliation during
* collaboration */ * collaboration */
export const PRECEDING_ELEMENT_KEY = "__precedingElement__"; export const PRECEDING_ELEMENT_KEY = "__precedingElement__";
export const DEFAULT_ELEMENT_PROPS: {
strokeColor: ExcalidrawElement["strokeColor"];
backgroundColor: ExcalidrawElement["backgroundColor"];
fillStyle: ExcalidrawElement["fillStyle"];
strokeWidth: ExcalidrawElement["strokeWidth"];
strokeStyle: ExcalidrawElement["strokeStyle"];
roughness: ExcalidrawElement["roughness"];
opacity: ExcalidrawElement["opacity"];
locked: ExcalidrawElement["locked"];
} = {
strokeColor: oc.black,
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
locked: false,
};
+3
View File
@@ -155,6 +155,9 @@
margin: 1px; margin: 1px;
} }
.welcome-screen-menu-item:focus-visible,
.dropdown-menu-item:focus-visible,
button:focus-visible,
.buttonList label:focus-within, .buttonList label:focus-within,
input:focus-visible { input:focus-visible {
outline: transparent; outline: transparent;
+1 -1
View File
@@ -157,7 +157,7 @@ export const loadSceneOrLibraryFromBlob = async (
}, },
localAppState, localAppState,
localElements, localElements,
{ repairBindings: true }, { repairBindings: true, refreshDimensions: true },
), ),
}; };
} else if (isValidLibrary(data)) { } else if (isValidLibrary(data)) {
+3 -1
View File
@@ -89,7 +89,9 @@ export const exportCanvas = async (
return await fileSave(blob, { return await fileSave(blob, {
description: "Export to PNG", description: "Export to PNG",
name, name,
extension: appState.exportEmbedScene ? "excalidraw.png" : "png", // FIXME reintroduce `excalidraw.png` when most people upgrade away
// from 111.0.5563.64 (arm64), see #6349
extension: /* appState.exportEmbedScene ? "excalidraw.png" : */ "png",
fileHandle, fileHandle,
}); });
} else if (type === "clipboard") { } else if (type === "clipboard") {
+42 -5
View File
@@ -31,10 +31,15 @@ import {
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement"; import { bumpVersion } from "../element/mutateElement";
import { getUpdatedTimestamp, updateActiveTool } from "../utils"; import { getFontString, 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"; import { MarkOptional, Mutable } from "../utility-types";
import {
detectLineHeight,
getDefaultLineHeight,
measureBaseline,
} from "../element/textElement";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@@ -165,17 +170,40 @@ const restoreElement = (
const [fontPx, _fontFamily]: [string, string] = ( const [fontPx, _fontFamily]: [string, string] = (
element as any element as any
).font.split(" "); ).font.split(" ");
fontSize = parseInt(fontPx, 10); fontSize = parseFloat(fontPx);
fontFamily = getFontFamilyByName(_fontFamily); fontFamily = getFontFamilyByName(_fontFamily);
} }
const text = element.text ?? "";
// line-height might not be specified either when creating elements
// programmatically, or when importing old diagrams.
// For the latter we want to detect the original line height which
// will likely differ from our per-font fixed line height we now use,
// to maintain backward compatibility.
const lineHeight =
element.lineHeight ||
(element.height
? // detect line-height from current element height and font-size
detectLineHeight(element)
: // no element height likely means programmatic use, so default
// to a fixed line height
getDefaultLineHeight(element.fontFamily));
const baseline = measureBaseline(
element.text,
getFontString(element),
lineHeight,
);
element = restoreElementWithProperties(element, { element = restoreElementWithProperties(element, {
fontSize, fontSize,
fontFamily, fontFamily,
text: element.text ?? "", text,
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,
originalText: element.originalText || element.text, originalText: element.originalText || text,
lineHeight,
baseline,
}); });
if (refreshDimensions) { if (refreshDimensions) {
@@ -341,6 +369,9 @@ export const restoreElements = (
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined,
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined, opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
// used to detect duplicate top-level element ids
const existingIds = new Set<string>();
const localElementsMap = localElements ? arrayToMap(localElements) : null; const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = (elements || []).reduce((elements, element) => { const restoredElements = (elements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
@@ -355,6 +386,10 @@ export const restoreElements = (
if (localElement && localElement.version > migratedElement.version) { if (localElement && localElement.version > migratedElement.version) {
migratedElement = bumpVersion(migratedElement, localElement.version); migratedElement = bumpVersion(migratedElement, localElement.version);
} }
if (existingIds.has(migratedElement.id)) {
migratedElement = { ...migratedElement, id: randomId() };
}
existingIds.add(migratedElement.id);
elements.push(migratedElement); elements.push(migratedElement);
} }
} }
@@ -479,7 +514,9 @@ export const restoreAppState = (
? { ? {
value: appState.zoom as NormalizedZoomValue, value: appState.zoom as NormalizedZoomValue,
} }
: appState.zoom || defaultAppState.zoom, : appState.zoom?.value
? appState.zoom
: defaultAppState.zoom,
// when sidebar docked and user left it open in last session, // when sidebar docked and user left it open in last session,
// keep it open. If not docked, keep it closed irrespective of last state. // keep it open. If not docked, keep it closed irrespective of last state.
openSidebar: openSidebar:
+6 -1
View File
@@ -786,7 +786,12 @@ export const findFocusPointForEllipse = (
orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) / orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
squares; squares;
const n = (-m * px - 1) / py; let n = (-m * px - 1) / py;
if (n === 0) {
// if zero {-0, 0}, fall back to a same-sign value in the similar range
n = (Object.is(n, -0) ? -1 : 1) * 0.01;
}
const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2); const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
return GA.point(x, (-m * x - 1) / n); return GA.point(x, (-m * x - 1) / n);
+339 -64
View File
@@ -1,8 +1,9 @@
import { duplicateElement } from "./newElement"; import { duplicateElement, duplicateElements } from "./newElement";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { FONT_FAMILY, ROUNDNESS } from "../constants"; import { FONT_FAMILY, ROUNDNESS } from "../constants";
import { isPrimitive } from "../utils"; import { isPrimitive } from "../utils";
import { ExcalidrawLinearElement } from "./types";
const assertCloneObjects = (source: any, clone: any) => { const assertCloneObjects = (source: any, clone: any) => {
for (const key in clone) { for (const key in clone) {
@@ -15,79 +16,353 @@ const assertCloneObjects = (source: any, clone: any) => {
} }
}; };
it("clones arrow element", () => { describe("duplicating single elements", () => {
const element = API.createElement({ it("clones arrow element", () => {
type: "arrow", const element = API.createElement({
x: 0, type: "arrow",
y: 0, x: 0,
strokeColor: "#000000", y: 0,
backgroundColor: "transparent", strokeColor: "#000000",
fillStyle: "hachure", backgroundColor: "transparent",
strokeWidth: 1, fillStyle: "hachure",
strokeStyle: "solid", strokeWidth: 1,
roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS }, strokeStyle: "solid",
roughness: 1, roundness: { type: ROUNDNESS.PROPORTIONAL_RADIUS },
opacity: 100, roughness: 1,
opacity: 100,
});
// @ts-ignore
element.__proto__ = { hello: "world" };
mutateElement(element, {
points: [
[1, 2],
[3, 4],
],
});
const copy = duplicateElement(null, new Map(), element);
assertCloneObjects(element, copy);
// assert we clone the object's prototype
// @ts-ignore
expect(copy.__proto__).toEqual({ hello: "world" });
expect(copy.hasOwnProperty("hello")).toBe(false);
expect(copy.points).not.toBe(element.points);
expect(copy).not.toHaveProperty("shape");
expect(copy.id).not.toBe(element.id);
expect(typeof copy.id).toBe("string");
expect(copy.seed).not.toBe(element.seed);
expect(typeof copy.seed).toBe("number");
expect(copy).toEqual({
...element,
id: copy.id,
seed: copy.seed,
});
}); });
// @ts-ignore it("clones text element", () => {
element.__proto__ = { hello: "world" }; const element = API.createElement({
type: "text",
x: 0,
y: 0,
strokeColor: "#000000",
backgroundColor: "transparent",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roundness: null,
roughness: 1,
opacity: 100,
text: "hello",
fontSize: 20,
fontFamily: FONT_FAMILY.Virgil,
textAlign: "left",
verticalAlign: "top",
});
mutateElement(element, { const copy = duplicateElement(null, new Map(), element);
points: [
[1, 2],
[3, 4],
],
});
const copy = duplicateElement(null, new Map(), element); assertCloneObjects(element, copy);
assertCloneObjects(element, copy); expect(copy).not.toHaveProperty("points");
expect(copy).not.toHaveProperty("shape");
// @ts-ignore expect(copy.id).not.toBe(element.id);
expect(copy.__proto__).toEqual({ hello: "world" }); expect(typeof copy.id).toBe("string");
expect(copy.hasOwnProperty("hello")).toBe(false); expect(typeof copy.seed).toBe("number");
expect(copy.points).not.toBe(element.points);
expect(copy).not.toHaveProperty("shape");
expect(copy.id).not.toBe(element.id);
expect(typeof copy.id).toBe("string");
expect(copy.seed).not.toBe(element.seed);
expect(typeof copy.seed).toBe("number");
expect(copy).toEqual({
...element,
id: copy.id,
seed: copy.seed,
}); });
}); });
it("clones text element", () => { describe("duplicating multiple elements", () => {
const element = API.createElement({ it("duplicateElements should clone bindings", () => {
type: "text", const rectangle1 = API.createElement({
x: 0, type: "rectangle",
y: 0, id: "rectangle1",
strokeColor: "#000000", boundElements: [
backgroundColor: "transparent", { id: "arrow1", type: "arrow" },
fillStyle: "hachure", { id: "arrow2", type: "arrow" },
strokeWidth: 1, { id: "text1", type: "text" },
strokeStyle: "solid", ],
roundness: null, });
roughness: 1,
opacity: 100, const text1 = API.createElement({
text: "hello", type: "text",
fontSize: 20, id: "text1",
fontFamily: FONT_FAMILY.Virgil, containerId: "rectangle1",
textAlign: "left", });
verticalAlign: "top",
const arrow1 = API.createElement({
type: "arrow",
id: "arrow1",
startBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
});
const arrow2 = API.createElement({
type: "arrow",
id: "arrow2",
endBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
boundElements: [{ id: "text2", type: "text" }],
});
const text2 = API.createElement({
type: "text",
id: "text2",
containerId: "arrow2",
});
// -------------------------------------------------------------------------
const origElements = [rectangle1, text1, arrow1, arrow2, text2] as const;
const clonedElements = duplicateElements(origElements);
// generic id in-equality checks
// --------------------------------------------------------------------------
expect(origElements.map((e) => e.type)).toEqual(
clonedElements.map((e) => e.type),
);
origElements.forEach((origElement, idx) => {
const clonedElement = clonedElements[idx];
expect(origElement).toEqual(
expect.objectContaining({
id: expect.not.stringMatching(clonedElement.id),
type: clonedElement.type,
}),
);
if ("containerId" in origElement) {
expect(origElement.containerId).not.toBe(
(clonedElement as any).containerId,
);
}
if ("endBinding" in origElement) {
if (origElement.endBinding) {
expect(origElement.endBinding.elementId).not.toBe(
(clonedElement as any).endBinding?.elementId,
);
} else {
expect((clonedElement as any).endBinding).toBeNull();
}
}
if ("startBinding" in origElement) {
if (origElement.startBinding) {
expect(origElement.startBinding.elementId).not.toBe(
(clonedElement as any).startBinding?.elementId,
);
} else {
expect((clonedElement as any).startBinding).toBeNull();
}
}
});
// --------------------------------------------------------------------------
const clonedArrows = clonedElements.filter(
(e) => e.type === "arrow",
) as ExcalidrawLinearElement[];
const [clonedRectangle, clonedText1, , clonedArrow2, clonedArrowLabel] =
clonedElements as any as typeof origElements;
expect(clonedText1.containerId).toBe(clonedRectangle.id);
expect(
clonedRectangle.boundElements!.find((e) => e.id === clonedText1.id),
).toEqual(
expect.objectContaining({
id: clonedText1.id,
type: clonedText1.type,
}),
);
clonedArrows.forEach((arrow) => {
// console.log(arrow);
expect(
clonedRectangle.boundElements!.find((e) => e.id === arrow.id),
).toEqual(
expect.objectContaining({
id: arrow.id,
type: arrow.type,
}),
);
if (arrow.endBinding) {
expect(arrow.endBinding.elementId).toBe(clonedRectangle.id);
}
if (arrow.startBinding) {
expect(arrow.startBinding.elementId).toBe(clonedRectangle.id);
}
});
expect(clonedArrow2.boundElements).toEqual([
{ type: "text", id: clonedArrowLabel.id },
]);
expect(clonedArrowLabel.containerId).toBe(clonedArrow2.id);
}); });
const copy = duplicateElement(null, new Map(), element); it("should remove id references of elements that aren't found", () => {
const rectangle1 = API.createElement({
type: "rectangle",
id: "rectangle1",
boundElements: [
// should keep
{ id: "arrow1", type: "arrow" },
// should drop
{ id: "arrow-not-exists", type: "arrow" },
// should drop
{ id: "text-not-exists", type: "text" },
],
});
assertCloneObjects(element, copy); const arrow1 = API.createElement({
type: "arrow",
id: "arrow1",
startBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
});
expect(copy).not.toHaveProperty("points"); const text1 = API.createElement({
expect(copy).not.toHaveProperty("shape"); type: "text",
expect(copy.id).not.toBe(element.id); id: "text1",
expect(typeof copy.id).toBe("string"); containerId: "rectangle-not-exists",
expect(typeof copy.seed).toBe("number"); });
const arrow2 = API.createElement({
type: "arrow",
id: "arrow2",
startBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
endBinding: {
elementId: "rectangle-not-exists",
focus: 0.2,
gap: 7,
},
});
const arrow3 = API.createElement({
type: "arrow",
id: "arrow2",
startBinding: {
elementId: "rectangle-not-exists",
focus: 0.2,
gap: 7,
},
endBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
});
// -------------------------------------------------------------------------
const origElements = [rectangle1, text1, arrow1, arrow2, arrow3] as const;
const clonedElements = duplicateElements(
origElements,
) as any as typeof origElements;
const [
clonedRectangle,
clonedText1,
clonedArrow1,
clonedArrow2,
clonedArrow3,
] = clonedElements;
expect(clonedRectangle.boundElements).toEqual([
{ id: clonedArrow1.id, type: "arrow" },
]);
expect(clonedText1.containerId).toBe(null);
expect(clonedArrow2.startBinding).toEqual({
...arrow2.startBinding,
elementId: clonedRectangle.id,
});
expect(clonedArrow2.endBinding).toBe(null);
expect(clonedArrow3.startBinding).toBe(null);
expect(clonedArrow3.endBinding).toEqual({
...arrow3.endBinding,
elementId: clonedRectangle.id,
});
});
describe("should duplicate all group ids", () => {
it("should regenerate all group ids and keep them consistent across elements", () => {
const rectangle1 = API.createElement({
type: "rectangle",
groupIds: ["g1"],
});
const rectangle2 = API.createElement({
type: "rectangle",
groupIds: ["g2", "g1"],
});
const rectangle3 = API.createElement({
type: "rectangle",
groupIds: ["g2", "g1"],
});
const origElements = [rectangle1, rectangle2, rectangle3] as const;
const clonedElements = duplicateElements(
origElements,
) as any as typeof origElements;
const [clonedRectangle1, clonedRectangle2, clonedRectangle3] =
clonedElements;
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
expect(rectangle2.groupIds[0]).not.toBe(clonedRectangle2.groupIds[0]);
expect(rectangle2.groupIds[1]).not.toBe(clonedRectangle2.groupIds[1]);
expect(clonedRectangle1.groupIds[0]).toBe(clonedRectangle2.groupIds[1]);
expect(clonedRectangle2.groupIds[0]).toBe(clonedRectangle3.groupIds[0]);
expect(clonedRectangle2.groupIds[1]).toBe(clonedRectangle3.groupIds[1]);
});
it("should keep and regenerate ids of groups even if invalid", () => {
// lone element shouldn't be able to be grouped with itself,
// but hard to check against in a performant way so we ignore it
const rectangle1 = API.createElement({
type: "rectangle",
groupIds: ["g1"],
});
const [clonedRectangle1] = duplicateElements([rectangle1]);
expect(typeof clonedRectangle1.groupIds[0]).toBe("string");
expect(rectangle1.groupIds[0]).not.toBe(clonedRectangle1.groupIds[0]);
});
});
}); });
+245 -48
View File
@@ -13,7 +13,12 @@ import {
FontFamilyValues, FontFamilyValues,
ExcalidrawTextContainer, ExcalidrawTextContainer,
} from "../element/types"; } from "../element/types";
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils"; import {
arrayToMap,
getFontString,
getUpdatedTimestamp,
isTestEnv,
} from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement"; import { mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups"; import { getNewGroupIdsForDuplication } from "../groups";
@@ -29,8 +34,16 @@ import {
normalizeText, normalizeText,
wrapText, wrapText,
getMaxContainerWidth, getMaxContainerWidth,
getDefaultLineHeight,
} from "./textElement"; } from "./textElement";
import { VERTICAL_ALIGN } from "../constants"; import {
DEFAULT_ELEMENT_PROPS,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { isArrowElement } from "./typeChecks"; import { isArrowElement } from "./typeChecks";
import { MarkOptional, Merge, Mutable } from "../utility-types"; import { MarkOptional, Merge, Mutable } from "../utility-types";
@@ -45,6 +58,15 @@ type ElementConstructorOpts = MarkOptional<
| "version" | "version"
| "versionNonce" | "versionNonce"
| "link" | "link"
| "strokeStyle"
| "fillStyle"
| "strokeColor"
| "backgroundColor"
| "roughness"
| "strokeWidth"
| "roundness"
| "locked"
| "opacity"
>; >;
const _newElementBase = <T extends ExcalidrawElement>( const _newElementBase = <T extends ExcalidrawElement>(
@@ -52,13 +74,13 @@ const _newElementBase = <T extends ExcalidrawElement>(
{ {
x, x,
y, y,
strokeColor, strokeColor = DEFAULT_ELEMENT_PROPS.strokeColor,
backgroundColor, backgroundColor = DEFAULT_ELEMENT_PROPS.backgroundColor,
fillStyle, fillStyle = DEFAULT_ELEMENT_PROPS.fillStyle,
strokeWidth, strokeWidth = DEFAULT_ELEMENT_PROPS.strokeWidth,
strokeStyle, strokeStyle = DEFAULT_ELEMENT_PROPS.strokeStyle,
roughness, roughness = DEFAULT_ELEMENT_PROPS.roughness,
opacity, opacity = DEFAULT_ELEMENT_PROPS.opacity,
width = 0, width = 0,
height = 0, height = 0,
angle = 0, angle = 0,
@@ -66,7 +88,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
roundness = null, roundness = null,
boundElements = null, boundElements = null,
link = null, link = null,
locked, locked = DEFAULT_ELEMENT_PROPS.locked,
...rest ...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">, }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => { ) => {
@@ -132,30 +154,47 @@ const getTextElementPositionOffsets = (
export const newTextElement = ( export const newTextElement = (
opts: { opts: {
text: string; text: string;
fontSize: number; fontSize?: number;
fontFamily: FontFamilyValues; fontFamily?: FontFamilyValues;
textAlign: TextAlign; textAlign?: TextAlign;
verticalAlign: VerticalAlign; verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"]; containerId?: ExcalidrawTextContainer["id"];
lineHeight?: ExcalidrawTextElement["lineHeight"];
strokeWidth?: ExcalidrawTextElement["strokeWidth"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => { ): NonDeleted<ExcalidrawTextElement> => {
const fontFamily = opts.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = opts.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = opts.lineHeight || getDefaultLineHeight(fontFamily);
const text = normalizeText(opts.text); const text = normalizeText(opts.text);
const metrics = measureText(text, getFontString(opts)); const metrics = measureText(
const offsets = getTextElementPositionOffsets(opts, metrics); text,
getFontString({ fontFamily, fontSize }),
lineHeight,
);
const textAlign = opts.textAlign || DEFAULT_TEXT_ALIGN;
const verticalAlign = opts.verticalAlign || DEFAULT_VERTICAL_ALIGN;
const offsets = getTextElementPositionOffsets(
{ textAlign, verticalAlign },
metrics,
);
const textElement = newElementWith( const textElement = newElementWith(
{ {
..._newElementBase<ExcalidrawTextElement>("text", opts), ..._newElementBase<ExcalidrawTextElement>("text", opts),
text, text,
fontSize: opts.fontSize, fontSize,
fontFamily: opts.fontFamily, fontFamily,
textAlign: opts.textAlign, textAlign,
verticalAlign: opts.verticalAlign, verticalAlign,
x: opts.x - offsets.x, x: opts.x - offsets.x,
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,
lineHeight,
}, },
{}, {},
); );
@@ -170,13 +209,15 @@ const getAdjustedDimensions = (
y: number; y: number;
width: number; width: number;
height: number; height: number;
baseline: number;
} => { } => {
const container = getContainerElement(element); const container = getContainerElement(element);
const { width: nextWidth, height: nextHeight } = measureText( const {
nextText, width: nextWidth,
getFontString(element), height: nextHeight,
); baseline: nextBaseline,
} = measureText(nextText, getFontString(element), element.lineHeight);
const { textAlign, verticalAlign } = element; const { textAlign, verticalAlign } = element;
let x: number; let x: number;
let y: number; let y: number;
@@ -185,7 +226,11 @@ const getAdjustedDimensions = (
verticalAlign === VERTICAL_ALIGN.MIDDLE && verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId !element.containerId
) { ) {
const prevMetrics = measureText(element.text, getFontString(element)); const prevMetrics = measureText(
element.text,
getFontString(element),
element.lineHeight,
);
const offsets = getTextElementPositionOffsets(element, { const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width, width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height, height: nextHeight - prevMetrics.height,
@@ -247,6 +292,7 @@ const getAdjustedDimensions = (
return { return {
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
baseline: nextBaseline,
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,
}; };
@@ -256,6 +302,9 @@ export const refreshTextDimensions = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
text = textElement.text, text = textElement.text,
) => { ) => {
if (textElement.isDeleted) {
return;
}
const container = getContainerElement(textElement); const container = getContainerElement(textElement);
if (container) { if (container) {
text = wrapText( text = wrapText(
@@ -341,16 +390,24 @@ export const newImageElement = (
}; };
}; };
// Simplified deep clone for the purpose of cloning ExcalidrawElement only // Simplified deep clone for the purpose of cloning ExcalidrawElement.
// (doesn't clone Date, RegExp, Map, Set, Typed arrays etc.) //
// Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
// Typed arrays and other non-null objects.
// //
// Adapted from https://github.com/lukeed/klona // Adapted from https://github.com/lukeed/klona
export const deepCopyElement = (val: any, depth: number = 0) => { //
// The reason for `deepCopyElement()` wrapper is type safety (only allow
// passing ExcalidrawElement as the top-level argument).
const _deepCopyElement = (val: any, depth: number = 0) => {
// only clone non-primitives
if (val == null || typeof val !== "object") { if (val == null || typeof val !== "object") {
return val; return val;
} }
if (Object.prototype.toString.call(val) === "[object Object]") { const objectType = Object.prototype.toString.call(val);
if (objectType === "[object Object]") {
const tmp = const tmp =
typeof val.constructor === "function" typeof val.constructor === "function"
? Object.create(Object.getPrototypeOf(val)) ? Object.create(Object.getPrototypeOf(val))
@@ -362,7 +419,7 @@ export const deepCopyElement = (val: any, depth: number = 0) => {
if (depth === 0 && (key === "shape" || key === "canvas")) { if (depth === 0 && (key === "shape" || key === "canvas")) {
continue; continue;
} }
tmp[key] = deepCopyElement(val[key], depth + 1); tmp[key] = _deepCopyElement(val[key], depth + 1);
} }
} }
return tmp; return tmp;
@@ -372,14 +429,67 @@ export const deepCopyElement = (val: any, depth: number = 0) => {
let k = val.length; let k = val.length;
const arr = new Array(k); const arr = new Array(k);
while (k--) { while (k--) {
arr[k] = deepCopyElement(val[k], depth + 1); arr[k] = _deepCopyElement(val[k], depth + 1);
} }
return arr; return arr;
} }
// we're not cloning non-array & non-plain-object objects because we
// don't support them on excalidraw elements yet. If we do, we need to make
// sure we start cloning them, so let's warn about it.
if (process.env.NODE_ENV === "development") {
if (
objectType !== "[object Object]" &&
objectType !== "[object Array]" &&
objectType.startsWith("[object ")
) {
console.warn(
`_deepCloneElement: unexpected object type ${objectType}. This value will not be cloned!`,
);
}
}
return val; return val;
}; };
/**
* Clones ExcalidrawElement data structure. Does not regenerate id, nonce, or
* any value. The purpose is to to break object references for immutability
* reasons, whenever we want to keep the original element, but ensure it's not
* mutated.
*
* Only clones plain objects and arrays. Doesn't clone Date, RegExp, Map, Set,
* Typed arrays and other non-null objects.
*/
export const deepCopyElement = <T extends ExcalidrawElement>(
val: T,
): Mutable<T> => {
return _deepCopyElement(val);
};
/**
* utility wrapper to generate new id. In test env it reuses the old + postfix
* for test assertions.
*/
const regenerateId = (
/** supply null if no previous id exists */
previousId: string | null,
) => {
if (isTestEnv() && previousId) {
let nextId = `${previousId}_copy`;
// `window.h` may not be defined in some unit tests
if (
window.h?.app
?.getSceneElementsIncludingDeleted()
.find((el) => el.id === nextId)
) {
nextId += "_copy";
}
return nextId;
}
return randomId();
};
/** /**
* Duplicate an element, often used in the alt-drag operation. * Duplicate an element, often used in the alt-drag operation.
* Note that this method has gotten a bit complicated since the * Note that this method has gotten a bit complicated since the
@@ -394,27 +504,15 @@ export const deepCopyElement = (val: any, depth: number = 0) => {
* @param element Element to duplicate * @param element Element to duplicate
* @param overrides Any element properties to override * @param overrides Any element properties to override
*/ */
export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>( export const duplicateElement = <TElement extends ExcalidrawElement>(
editingGroupId: AppState["editingGroupId"], editingGroupId: AppState["editingGroupId"],
groupIdMapForOperation: Map<GroupId, GroupId>, groupIdMapForOperation: Map<GroupId, GroupId>,
element: TElement, element: TElement,
overrides?: Partial<TElement>, overrides?: Partial<TElement>,
): TElement => { ): Readonly<TElement> => {
let copy: TElement = deepCopyElement(element); let copy = deepCopyElement(element);
if (isTestEnv()) { copy.id = regenerateId(copy.id);
copy.id = `${copy.id}_copy`;
// `window.h` may not be defined in some unit tests
if (
window.h?.app
?.getSceneElementsIncludingDeleted()
.find((el) => el.id === copy.id)
) {
copy.id += "_copy";
}
} else {
copy.id = randomId();
}
copy.boundElements = null; copy.boundElements = null;
copy.updated = getUpdatedTimestamp(); copy.updated = getUpdatedTimestamp();
copy.seed = randomInteger(); copy.seed = randomInteger();
@@ -423,7 +521,7 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
editingGroupId, editingGroupId,
(groupId) => { (groupId) => {
if (!groupIdMapForOperation.has(groupId)) { if (!groupIdMapForOperation.has(groupId)) {
groupIdMapForOperation.set(groupId, randomId()); groupIdMapForOperation.set(groupId, regenerateId(groupId));
} }
return groupIdMapForOperation.get(groupId)!; return groupIdMapForOperation.get(groupId)!;
}, },
@@ -433,3 +531,102 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
} }
return copy; return copy;
}; };
/**
* Clones elements, regenerating their ids (including bindings) and group ids.
*
* If bindings don't exist in the elements array, they are removed. Therefore,
* it's advised to supply the whole elements array, or sets of elements that
* are encapsulated (such as library items), if the purpose is to retain
* bindings to the cloned elements intact.
*/
export const duplicateElements = (elements: readonly ExcalidrawElement[]) => {
const clonedElements: ExcalidrawElement[] = [];
const origElementsMap = arrayToMap(elements);
// used for for migrating old ids to new ids
const elementNewIdsMap = new Map<
/* orig */ ExcalidrawElement["id"],
/* new */ ExcalidrawElement["id"]
>();
const maybeGetNewId = (id: ExcalidrawElement["id"]) => {
// if we've already migrated the element id, return the new one directly
if (elementNewIdsMap.has(id)) {
return elementNewIdsMap.get(id)!;
}
// if we haven't migrated the element id, but an old element with the same
// id exists, generate a new id for it and return it
if (origElementsMap.has(id)) {
const newId = regenerateId(id);
elementNewIdsMap.set(id, newId);
return newId;
}
// if old element doesn't exist, return null to mark it for removal
return null;
};
const groupNewIdsMap = new Map</* orig */ GroupId, /* new */ GroupId>();
for (const element of elements) {
const clonedElement: Mutable<ExcalidrawElement> = _deepCopyElement(element);
clonedElement.id = maybeGetNewId(element.id)!;
if (clonedElement.groupIds) {
clonedElement.groupIds = clonedElement.groupIds.map((groupId) => {
if (!groupNewIdsMap.has(groupId)) {
groupNewIdsMap.set(groupId, regenerateId(groupId));
}
return groupNewIdsMap.get(groupId)!;
});
}
if ("containerId" in clonedElement && clonedElement.containerId) {
const newContainerId = maybeGetNewId(clonedElement.containerId);
clonedElement.containerId = newContainerId;
}
if ("boundElements" in clonedElement && clonedElement.boundElements) {
clonedElement.boundElements = clonedElement.boundElements.reduce(
(
acc: Mutable<NonNullable<ExcalidrawElement["boundElements"]>>,
binding,
) => {
const newBindingId = maybeGetNewId(binding.id);
if (newBindingId) {
acc.push({ ...binding, id: newBindingId });
}
return acc;
},
[],
);
}
if ("endBinding" in clonedElement && clonedElement.endBinding) {
const newEndBindingId = maybeGetNewId(clonedElement.endBinding.elementId);
clonedElement.endBinding = newEndBindingId
? {
...clonedElement.endBinding,
elementId: newEndBindingId,
}
: null;
}
if ("startBinding" in clonedElement && clonedElement.startBinding) {
const newEndBindingId = maybeGetNewId(
clonedElement.startBinding.elementId,
);
clonedElement.startBinding = newEndBindingId
? {
...clonedElement.startBinding,
elementId: newEndBindingId,
}
: null;
}
clonedElements.push(clonedElement);
}
return clonedElements;
};
+49 -20
View File
@@ -39,13 +39,15 @@ import {
import { Point, PointerDownState } from "../types"; import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { import {
getApproxMinLineHeight,
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElement, getBoundTextElement,
getBoundTextElementId, getBoundTextElementId,
getContainerElement, getContainerElement,
handleBindTextResize, handleBindTextResize,
getMaxContainerWidth, getMaxContainerWidth,
getApproxMinLineHeight,
measureText,
getMaxContainerHeight,
} from "./textElement"; } from "./textElement";
export const normalizeAngle = (angle: number): number => { export const normalizeAngle = (angle: number): number => {
@@ -193,7 +195,8 @@ const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = ( const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>, element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number, nextWidth: number,
): number | null => { nextHeight: number,
): { 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;
@@ -208,8 +211,15 @@ const measureFontSizeFromWidth = (
if (nextFontSize < MIN_FONT_SIZE) { if (nextFontSize < MIN_FONT_SIZE) {
return null; return null;
} }
const metrics = measureText(
return nextFontSize; element.text,
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
element.lineHeight,
);
return {
size: nextFontSize,
baseline: metrics.baseline + (nextHeight - metrics.height),
};
}; };
const getSidesForTransformHandle = ( const getSidesForTransformHandle = (
@@ -280,8 +290,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 nextFontSize = measureFontSizeFromWidth(element, nextWidth); const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
if (nextFontSize === null) { if (metrics === null) {
return; return;
} }
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords( const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
@@ -305,9 +315,10 @@ const resizeSingleTextElement = (
deltaY2, deltaY2,
); );
mutateElement(element, { mutateElement(element, {
fontSize: nextFontSize, fontSize: metrics.size,
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
baseline: metrics.baseline,
x: nextElementX, x: nextElementX,
y: nextElementY, y: nextElementY,
}); });
@@ -360,7 +371,7 @@ export const resizeSingleElement = (
let scaleX = atStartBoundsWidth / boundsCurrentWidth; let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight; let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number } = {}; let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) { if (transformHandleDirection.includes("e")) {
@@ -412,6 +423,7 @@ export const resizeSingleElement = (
if (stateOfBoundTextElementAtResize) { if (stateOfBoundTextElementAtResize) {
boundTextFont = { boundTextFont = {
fontSize: stateOfBoundTextElementAtResize.fontSize, fontSize: stateOfBoundTextElementAtResize.fontSize,
baseline: stateOfBoundTextElementAtResize.baseline,
}; };
} }
if (shouldMaintainAspectRatio) { if (shouldMaintainAspectRatio) {
@@ -421,19 +433,27 @@ export const resizeSingleElement = (
height: eleNewHeight, height: eleNewHeight,
}; };
const nextFontSize = measureFontSizeFromWidth( const nextFont = measureFontSizeFromWidth(
boundTextElement, boundTextElement,
getMaxContainerWidth(updatedElement), getMaxContainerWidth(updatedElement),
getMaxContainerHeight(updatedElement),
); );
if (nextFontSize === null) { if (nextFont === null) {
return; return;
} }
boundTextFont = { boundTextFont = {
fontSize: nextFontSize, fontSize: nextFont.size,
baseline: nextFont.baseline,
}; };
} else { } else {
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement)); const minWidth = getApproxMinLineWidth(
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement)); getFontString(boundTextElement),
boundTextElement.lineHeight,
);
const minHeight = getApproxMinLineHeight(
boundTextElement.fontSize,
boundTextElement.lineHeight,
);
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth)); eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight)); eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
} }
@@ -566,8 +586,11 @@ export const resizeSingleElement = (
}); });
mutateElement(element, resizedElement); mutateElement(element, resizedElement);
if (boundTextElement && boundTextFont) { if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize }); mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize,
baseline: boundTextFont.baseline,
});
} }
handleBindTextResize(element, transformHandleDirection); handleBindTextResize(element, transformHandleDirection);
} }
@@ -673,6 +696,7 @@ const resizeMultipleElements = (
y: number; y: number;
points?: Point[]; points?: Point[];
fontSize?: number; fontSize?: number;
baseline?: number;
} = { } = {
width, width,
height, height,
@@ -681,7 +705,7 @@ const resizeMultipleElements = (
...rescaledPoints, ...rescaledPoints,
}; };
let boundTextUpdates: { fontSize: number } | null = null; let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
const boundTextElement = getBoundTextElement(element.latest); const boundTextElement = getBoundTextElement(element.latest);
@@ -691,24 +715,29 @@ const resizeMultipleElements = (
width, width,
height, height,
}; };
const fontSize = measureFontSizeFromWidth( const metrics = measureFontSizeFromWidth(
boundTextElement ?? (element.orig as ExcalidrawTextElement), boundTextElement ?? (element.orig as ExcalidrawTextElement),
boundTextElement boundTextElement
? getMaxContainerWidth(updatedElement) ? getMaxContainerWidth(updatedElement)
: updatedElement.width, : updatedElement.width,
boundTextElement
? getMaxContainerHeight(updatedElement)
: updatedElement.height,
); );
if (!fontSize) { if (!metrics) {
return; return;
} }
if (isTextElement(element.orig)) { if (isTextElement(element.orig)) {
update.fontSize = fontSize; update.fontSize = metrics.size;
update.baseline = metrics.baseline;
} }
if (boundTextElement) { if (boundTextElement) {
boundTextUpdates = { boundTextUpdates = {
fontSize, fontSize: metrics.size,
baseline: metrics.baseline,
}; };
} }
} }
+64 -18
View File
@@ -1,4 +1,4 @@
import { BOUND_TEXT_PADDING } from "../constants"; import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import { API } from "../tests/helpers/api"; import { API } from "../tests/helpers/api";
import { import {
computeContainerDimensionForBoundText, computeContainerDimensionForBoundText,
@@ -6,6 +6,9 @@ import {
getMaxContainerWidth, getMaxContainerWidth,
getMaxContainerHeight, getMaxContainerHeight,
wrapText, wrapText,
detectLineHeight,
getLineHeightInPx,
getDefaultLineHeight,
} from "./textElement"; } from "./textElement";
import { FontString } from "./types"; import { FontString } from "./types";
@@ -16,7 +19,7 @@ describe("Test wrapText", () => {
const text = "Hello whats up "; const text = "Hello whats up ";
const maxWidth = 200 - BOUND_TEXT_PADDING * 2; const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
const res = wrapText(text, font, maxWidth); const res = wrapText(text, font, maxWidth);
expect(res).toBe("Hello whats up "); expect(res).toBe(text);
}); });
it("should work with emojis", () => { it("should work with emojis", () => {
@@ -26,7 +29,7 @@ describe("Test wrapText", () => {
expect(res).toBe("😀"); expect(res).toBe("😀");
}); });
it("should show the text correctly when min width reached", () => { it("should show the text correctly when max width reached", () => {
const text = "Hello😀"; const text = "Hello😀";
const maxWidth = 10; const maxWidth = 10;
const res = wrapText(text, font, maxWidth); const res = wrapText(text, font, maxWidth);
@@ -40,9 +43,7 @@ describe("Test wrapText", () => {
{ {
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: 80, width: 80,
res: `Hello res: `Hello \nwhats \nup`,
whats
up`,
}, },
{ {
desc: "break all characters when width of each character is less than container width", desc: "break all characters when width of each character is less than container width",
@@ -64,8 +65,7 @@ p`,
desc: "break words as per the width", desc: "break words as per the width",
width: 140, width: 140,
res: `Hello whats res: `Hello whats \nup`,
up`,
}, },
{ {
desc: "fit the container", desc: "fit the container",
@@ -95,9 +95,7 @@ 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: 80, width: 80,
res: `Hello res: `Hello\nwhats \nup`,
whats
up`,
}, },
{ {
desc: "break all characters when width of each character is less than container width", desc: "break all characters when width of each character is less than container width",
@@ -136,17 +134,14 @@ whats up`,
}); });
}); });
}); });
describe("When text is long", () => { describe("When text is long", () => {
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`; const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
[ [
{ {
desc: "fit characters of long string as per container width", desc: "fit characters of long string as per container width",
width: 170, width: 170,
res: `hellolongtextth res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
isiswhatsupwith
youIamtypingggg
gandtypinggg
break it now`,
}, },
{ {
@@ -165,8 +160,7 @@ now`,
desc: "fit the long text when container width is greater than text length and move the rest to next line", desc: "fit the long text when container width is greater than text length and move the rest to next line",
width: 600, width: 600,
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
break it now`,
}, },
].forEach((data) => { ].forEach((data) => {
it(`should ${data.desc}`, () => { it(`should ${data.desc}`, () => {
@@ -175,6 +169,20 @@ break it now`,
}); });
}); });
}); });
it("should wrap the text correctly when word length is exactly equal to max width", () => {
const text = "Hello Excalidraw";
// Length of "Excalidraw" is 100 and exacty equal to max width
const res = wrapText(text, font, 100);
expect(res).toEqual(`Hello \nExcalidraw`);
});
it("should return the text as is if max width is invalid", () => {
const text = "Hello Excalidraw";
expect(wrapText(text, font, NaN)).toEqual(text);
expect(wrapText(text, font, -1)).toEqual(text);
expect(wrapText(text, font, Infinity)).toEqual(text);
});
}); });
describe("Test measureText", () => { describe("Test measureText", () => {
@@ -296,3 +304,41 @@ describe("Test measureText", () => {
}); });
}); });
}); });
const textElement = API.createElement({
type: "text",
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
fontSize: 20,
fontFamily: 1,
height: 175,
});
describe("Test detectLineHeight", () => {
it("should return correct line height", () => {
expect(detectLineHeight(textElement)).toBe(1.25);
});
});
describe("Test getLineHeightInPx", () => {
it("should return correct line height", () => {
expect(
getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
).toBe(25);
});
});
describe("Test getDefaultLineHeight", () => {
it("should return line height using default font family when not passed", () => {
//@ts-ignore
expect(getDefaultLineHeight()).toBe(1.25);
});
it("should return line height using default font family for unknown font", () => {
const UNKNOWN_FONT = 5;
expect(getDefaultLineHeight(UNKNOWN_FONT)).toBe(1.25);
});
it("should return correct line height", () => {
expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
});
});
+224 -63
View File
@@ -4,11 +4,20 @@ import {
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
FontFamilyValues,
FontString, FontString,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./types"; } from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; import {
BOUND_TEXT_PADDING,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
FONT_FAMILY,
isSafari,
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 ".";
@@ -35,18 +44,22 @@ export const normalizeText = (text: string) => {
); );
}; };
export const splitIntoLines = (text: string) => {
return normalizeText(text).split("\n");
};
export const redrawTextBoundingBox = ( export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null, container: ExcalidrawElement | null,
) => { ) => {
let maxWidth = undefined; let maxWidth = undefined;
const boundTextUpdates = { const boundTextUpdates = {
x: textElement.x, x: textElement.x,
y: textElement.y, y: textElement.y,
text: textElement.text, text: textElement.text,
width: textElement.width, width: textElement.width,
height: textElement.height, height: textElement.height,
baseline: textElement.baseline,
}; };
boundTextUpdates.text = textElement.text; boundTextUpdates.text = textElement.text;
@@ -62,10 +75,12 @@ export const redrawTextBoundingBox = (
const metrics = measureText( const metrics = measureText(
boundTextUpdates.text, boundTextUpdates.text,
getFontString(textElement), getFontString(textElement),
textElement.lineHeight,
); );
boundTextUpdates.width = metrics.width; boundTextUpdates.width = metrics.width;
boundTextUpdates.height = metrics.height; boundTextUpdates.height = metrics.height;
boundTextUpdates.baseline = metrics.baseline;
if (container) { if (container) {
if (isArrowElement(container)) { if (isArrowElement(container)) {
@@ -171,6 +186,7 @@ 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(
@@ -179,9 +195,14 @@ export const handleBindTextResize = (
maxWidth, maxWidth,
); );
} }
const dimensions = measureText(text, getFontString(textElement)); const metrics = measureText(
nextHeight = dimensions.height; text,
nextWidth = dimensions.width; getFontString(textElement),
textElement.lineHeight,
);
nextHeight = metrics.height;
nextWidth = metrics.width;
nextBaseLine = metrics.baseline;
} }
// increase height in case text element height exceeds // increase height in case text element height exceeds
if (nextHeight > maxHeight) { if (nextHeight > maxHeight) {
@@ -209,6 +230,7 @@ export const handleBindTextResize = (
text, text,
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
baseline: nextBaseLine,
}); });
if (!isArrowElement(container)) { if (!isArrowElement(container)) {
@@ -223,10 +245,16 @@ export const handleBindTextResize = (
} }
}; };
const computeBoundTextPosition = ( export const computeBoundTextPosition = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
) => { ) => {
if (isArrowElement(container)) {
return LinearElementEditor.getBoundTextElementPosition(
container,
boundTextElement,
);
}
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container); const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container); const maxContainerWidth = getMaxContainerWidth(container);
@@ -255,32 +283,103 @@ const computeBoundTextPosition = (
// 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, font: FontString) => { export const measureText = (
text: string,
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
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 fontSize = parseFloat(font);
const height = getTextHeight(text, font); const height = getTextHeight(text, fontSize, lineHeight);
const width = getTextWidth(text, font); const width = getTextWidth(text, font);
const baseline = measureBaseline(text, font, lineHeight);
return { width, height }; return { width, height, baseline };
}; };
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase(); export const measureBaseline = (
const cacheApproxLineHeight: { [key: FontString]: number } = {}; text: string,
font: FontString,
export const getApproxLineHeight = (font: FontString) => { lineHeight: ExcalidrawTextElement["lineHeight"],
if (cacheApproxLineHeight[font]) { wrapInContainer?: boolean,
return cacheApproxLineHeight[font]; ) => {
const container = document.createElement("div");
container.style.position = "absolute";
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
if (wrapInContainer) {
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.whiteSpace = "pre-wrap";
} }
const fontSize = parseInt(font);
// Calculate line height relative to font size container.style.lineHeight = String(lineHeight);
cacheApproxLineHeight[font] = fontSize * 1.2;
return cacheApproxLineHeight[font]; container.innerText = text;
// Baseline is important for positioning text on canvas
document.body.appendChild(container);
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.overflow = "hidden";
span.style.width = "1px";
span.style.height = "1px";
container.appendChild(span);
let baseline = span.offsetTop + span.offsetHeight;
const height = container.offsetHeight;
if (isSafari) {
const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight);
const fontSize = parseFloat(font);
// In Safari the font size gets rounded off when rendering hence calculating the safari height and shifting the baseline if it differs
// from the actual canvas height
const domHeight = getTextHeight(text, Math.round(fontSize), lineHeight);
if (canvasHeight > height) {
baseline += canvasHeight - domHeight;
}
if (height > canvasHeight) {
baseline -= domHeight - canvasHeight;
}
}
document.body.removeChild(container);
return baseline;
};
/**
* To get unitless line-height (if unknown) we can calculate it by dividing
* height-per-line by fontSize.
*/
export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
const lineCount = splitIntoLines(textElement.text).length;
return (textElement.height /
lineCount /
textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
};
/**
* We calculate the line height from the font size and the unitless line height,
* aligning with the W3C spec.
*/
export const getLineHeightInPx = (
fontSize: ExcalidrawTextElement["fontSize"],
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
return fontSize * lineHeight;
};
// FIXME rename to getApproxMinContainerHeight
export const getApproxMinLineHeight = (
fontSize: ExcalidrawTextElement["fontSize"],
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
}; };
let canvas: HTMLCanvasElement | undefined; let canvas: HTMLCanvasElement | undefined;
@@ -303,7 +402,7 @@ const getLineWidth = (text: string, font: FontString) => {
}; };
export const getTextWidth = (text: string, font: FontString) => { export const getTextWidth = (text: string, font: FontString) => {
const lines = text.replace(/\r\n?/g, "\n").split("\n"); const lines = splitIntoLines(text);
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));
@@ -311,35 +410,58 @@ export const getTextWidth = (text: string, font: FontString) => {
return width; return width;
}; };
export const getTextHeight = (text: string, font: FontString) => { export const getTextHeight = (
const lines = text.replace(/\r\n?/g, "\n").split("\n"); text: string,
const lineHeight = getApproxLineHeight(font); fontSize: number,
return lineHeight * lines.length; lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
const lineCount = splitIntoLines(text).length;
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
}; };
export const wrapText = (text: string, font: FontString, maxWidth: number) => { export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// if maxWidth is not finite or NaN which can happen in case of bugs in
// computation, we need to make sure we don't continue as we'll end up
// in an infinite loop
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
return text;
}
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);
let currentLine = "";
let currentLineWidthTillNow = 0;
const push = (str: string) => { const push = (str: string) => {
if (str.trim()) { if (str.trim()) {
lines.push(str); lines.push(str);
} }
}; };
const resetParams = () => {
currentLine = "";
currentLineWidthTillNow = 0;
};
originalLines.forEach((originalLine) => { originalLines.forEach((originalLine) => {
const words = originalLine.split(" "); const currentLineWidth = getTextWidth(originalLine, font);
// This means its newline so push it
if (words.length === 1 && words[0] === "") { //Push the line if its <= maxWidth
lines.push(words[0]); if (currentLineWidth <= maxWidth) {
lines.push(originalLine);
return; // continue return; // continue
} }
let currentLine = ""; const words = originalLine.split(" ");
let currentLineWidthTillNow = 0;
resetParams();
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 // This will only happen when single word takes entire width
if (currentWordWidth === maxWidth) { if (currentWordWidth === maxWidth) {
push(words[index]); push(words[index]);
@@ -351,8 +473,8 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// 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 = "";
currentLineWidthTillNow = 0; resetParams();
while (words[index].length > 0) { while (words[index].length > 0) {
const currentChar = String.fromCodePoint( const currentChar = String.fromCodePoint(
@@ -363,10 +485,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
words[index] = words[index].slice(currentChar.length); words[index] = words[index].slice(currentChar.length);
if (currentLineWidthTillNow >= maxWidth) { if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
push(currentLine); push(currentLine);
currentLine = currentChar; currentLine = currentChar;
currentLineWidthTillNow = width; currentLineWidthTillNow = width;
@@ -374,11 +492,11 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine += currentChar; currentLine += currentChar;
} }
} }
// push current line if appending space exceeds max width // push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) { if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
push(currentLine); push(currentLine);
currentLine = ""; resetParams();
currentLineWidthTillNow = 0;
} else { } else {
// space needs to be appended before next word // space needs to be appended before next word
// as currentLine contains chars which couldn't be appended // as currentLine contains chars which couldn't be appended
@@ -386,7 +504,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine += " "; currentLine += " ";
currentLineWidthTillNow += spaceWidth; currentLineWidthTillNow += spaceWidth;
} }
index++; index++;
} else { } else {
// Start appending words in a line till max width reached // Start appending words in a line till max width reached
@@ -396,8 +513,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
if (currentLineWidthTillNow > maxWidth) { if (currentLineWidthTillNow > maxWidth) {
push(currentLine); push(currentLine);
currentLineWidthTillNow = 0; resetParams();
currentLine = "";
break; break;
} }
@@ -408,22 +524,15 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
if (currentLineWidthTillNow + spaceWidth >= maxWidth) { if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1); const word = currentLine.slice(0, -1);
push(word); push(word);
currentLine = ""; resetParams();
currentLineWidthTillNow = 0;
break; break;
} }
} }
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
} }
} }
if (currentLine) { if (currentLine.slice(-1) === " ") {
// only remove last trailing space which we have added when joining words // only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") { currentLine = currentLine.slice(0, -1);
currentLine = currentLine.slice(0, -1);
}
push(currentLine); push(currentLine);
} }
}); });
@@ -455,21 +564,23 @@ export const charWidth = (() => {
}; };
})(); })();
export const getApproxMinLineWidth = (font: FontString) => { const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
// FIXME rename to getApproxMinContainerWidth
export const getApproxMinLineWidth = (
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
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, lineHeight).width +
BOUND_TEXT_PADDING * 2 BOUND_TEXT_PADDING * 2
); );
} }
return maxCharWidth + BOUND_TEXT_PADDING * 2; return maxCharWidth + BOUND_TEXT_PADDING * 2;
}; };
export const getApproxMinLineHeight = (font: FontString) => {
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
};
export const getMinCharWidth = (font: FontString) => { export const getMinCharWidth = (font: FontString) => {
const cache = charWidth.getCache(font); const cache = charWidth.getCache(font);
if (!cache) { if (!cache) {
@@ -662,14 +773,24 @@ export const shouldAllowVerticalAlign = (
} }
return true; return true;
} }
const boundTextElement = getBoundTextElement(element); return false;
if (boundTextElement) { });
if (isArrowElement(element)) { };
export const suppportsHorizontalAlign = (
selectedElements: NonDeletedExcalidrawElement[],
) => {
return selectedElements.some((element) => {
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
return false; return false;
} }
return true; return true;
} }
return false;
return isTextElement(element);
}); });
}; };
@@ -794,3 +915,43 @@ export const getMaxContainerHeight = (container: ExcalidrawElement) => {
} }
return height - BOUND_TEXT_PADDING * 2; return height - BOUND_TEXT_PADDING * 2;
}; };
export const isMeasureTextSupported = () => {
const width = getTextWidth(
DUMMY_TEXT,
getFontString({
fontSize: DEFAULT_FONT_SIZE,
fontFamily: DEFAULT_FONT_FAMILY,
}),
);
return width > 0;
};
/**
* Unitless line height
*
* In previous versions we used `normal` line height, which browsers interpret
* differently, and based on font-family and font-size.
*
* To make line heights consistent across browsers we hardcode the values for
* each of our fonts based on most common average line-heights.
* See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
* where the values come from.
*/
const DEFAULT_LINE_HEIGHT = {
// ~1.25 is the average for Virgil in WebKit and Blink.
// Gecko (FF) uses ~1.28.
[FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
// ~1.15 is the average for Virgil in WebKit and Blink.
// Gecko if all over the place.
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
// ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
[FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
};
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
if (fontFamily in DEFAULT_LINE_HEIGHT) {
return DEFAULT_LINE_HEIGHT[fontFamily];
}
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
};
+136 -19
View File
@@ -526,6 +526,44 @@ describe("textWysiwyg", () => {
]); ]);
}); });
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
const diamond = API.createElement({
type: "diamond",
x: 10,
y: 20,
width: 90,
height: 75,
});
h.elements = [diamond];
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(diamond.id);
API.setSelectedElements([diamond]);
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
const value = new Array(1000).fill("1").join("\n");
// Pasting large text to simulate height increase
expect(() =>
fireEvent.input(editor, { target: { value } }),
).not.toThrow();
expect(diamond.height).toBe(50020);
// Clearing text to simulate height decrease
expect(() =>
fireEvent.input(editor, { target: { value: "" } }),
).not.toThrow();
expect(diamond.height).toBe(70);
});
it("should bind text to container when double clicked on center of transparent container", async () => { it("should bind text to container when double clicked on center of transparent container", async () => {
const rectangle = API.createElement({ const rectangle = API.createElement({
type: "rectangle", type: "rectangle",
@@ -702,6 +740,52 @@ describe("textWysiwyg", () => {
expect(rectangle.boundElements).toBe(null); expect(rectangle.boundElements).toBe(null);
}); });
it("should bind text to container when triggered via context menu", async () => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
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));
expect(h.elements.length).toBe(2);
expect(h.elements[1].type).toBe("text");
API.setSelectedElements([h.elements[0], h.elements[1]]);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 20,
clientY: 30,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(
queryByText(contextMenu as HTMLElement, "Bind text to the container")!,
);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(rectangle.boundElements).toStrictEqual([
{ id: h.elements[1].id, type: "text" },
]);
expect(text.containerId).toBe(rectangle.id);
expect(text.verticalAlign).toBe(VERTICAL_ALIGN.MIDDLE);
expect(text.textAlign).toBe(TEXT_ALIGN.CENTER);
expect(text.x).toBe(
h.elements[0].x + h.elements[0].width / 2 - text.width / 2,
);
expect(text.y).toBe(
h.elements[0].y + h.elements[0].height / 2 - text.height / 2,
);
});
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => { it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
expect(h.elements.length).toBe(1); expect(h.elements.length).toBe(1);
@@ -783,7 +867,7 @@ describe("textWysiwyg", () => {
rectangle.y + h.elements[0].height / 2 - text.height / 2, rectangle.y + h.elements[0].height / 2 - text.height / 2,
); );
expect(text.x).toBe(25); expect(text.x).toBe(25);
expect(text.height).toBe(48); expect(text.height).toBe(50);
expect(text.width).toBe(60); 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
@@ -810,7 +894,7 @@ 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.height).toBe(25);
expect(text.width).toBe(50); expect(text.width).toBe(50);
expect(text.y).toBe( expect(text.y).toBe(
rectangle.y + h.elements[0].height / 2 - text.height / 2, rectangle.y + h.elements[0].height / 2 - text.height / 2,
@@ -903,7 +987,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 [
85, 85,
5, 4.5,
] ]
`); `);
@@ -929,7 +1013,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,
66, 65,
] ]
`); `);
@@ -1067,9 +1151,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(-35); expect(rectangle.y).toBe(-40);
expect(text.x).toBe(85); expect(text.x).toBe(85);
expect(text.y).toBe(-30); expect(text.y).toBe(-35);
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z); Keyboard.keyPress(KEYS.Z);
@@ -1112,7 +1196,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(178); expect(rectangle.height).toBe(185);
mouse.select(rectangle); mouse.select(rectangle);
fireEvent.contextMenu(GlobalTestState.canvas, { fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2, button: 2,
@@ -1181,9 +1265,42 @@ 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( expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(97);
96.39999999999999, });
);
it("should update line height when font family updated", async () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
editor.blur();
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.25);
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
fireEvent.click(screen.getByTitle(/code/i));
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.2);
fireEvent.click(screen.getByTitle(/normal/i));
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Helvetica);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.15);
}); });
describe("should align correctly", () => { describe("should align correctly", () => {
@@ -1245,7 +1362,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,
45.5, 45,
] ]
`); `);
}); });
@@ -1257,7 +1374,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 [
30, 30,
45.5, 45,
] ]
`); `);
}); });
@@ -1269,7 +1386,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 [
45, 45,
45.5, 45,
] ]
`); `);
}); });
@@ -1281,7 +1398,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,
66, 65,
] ]
`); `);
}); });
@@ -1292,7 +1409,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 [
30, 30,
66, 65,
] ]
`); `);
}); });
@@ -1303,7 +1420,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 [
45, 45,
66, 65,
] ]
`); `);
}); });
@@ -1333,7 +1450,7 @@ describe("textWysiwyg", () => {
const textElement = h.elements[1] as ExcalidrawTextElement; const textElement = h.elements[1] as ExcalidrawTextElement;
expect(textElement.width).toBe(600); expect(textElement.width).toBe(600);
expect(textElement.height).toBe(24); expect(textElement.height).toBe(25);
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT); expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
expect((textElement as ExcalidrawTextElement).text).toBe( expect((textElement as ExcalidrawTextElement).text).toBe(
"Excalidraw is an opensource virtual collaborative whiteboard", "Excalidraw is an opensource virtual collaborative whiteboard",
@@ -1365,7 +1482,7 @@ describe("textWysiwyg", () => {
], ],
fillStyle: "hachure", fillStyle: "hachure",
groupIds: [], groupIds: [],
height: 34, height: 35,
isDeleted: false, isDeleted: false,
link: null, link: null,
locked: false, locked: false,
@@ -1389,7 +1506,7 @@ describe("textWysiwyg", () => {
expect.objectContaining({ expect.objectContaining({
text: "Excalidraw is an opensource virtual collaborative whiteboard", text: "Excalidraw is an opensource virtual collaborative whiteboard",
verticalAlign: VERTICAL_ALIGN.MIDDLE, verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.LEFT, textAlign: TEXT_ALIGN.CENTER,
boundElements: null, boundElements: null,
}), }),
); );
+32 -21
View File
@@ -11,7 +11,7 @@ import {
isBoundToContainer, isBoundToContainer,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants"; import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@@ -22,7 +22,6 @@ import {
import { AppState } from "../types"; import { AppState } from "../types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
getApproxLineHeight,
getBoundTextElementId, getBoundTextElementId,
getContainerCoords, getContainerCoords,
getContainerDims, getContainerDims,
@@ -35,6 +34,8 @@ import {
wrapText, wrapText,
getMaxContainerHeight, getMaxContainerHeight,
getMaxContainerWidth, getMaxContainerWidth,
computeContainerDimensionForBoundText,
detectLineHeight,
} from "./textElement"; } from "./textElement";
import { import {
actionDecreaseFontSize, actionDecreaseFontSize,
@@ -150,9 +151,7 @@ export const textWysiwyg = ({
return; return;
} }
const { textAlign, verticalAlign } = updatedTextElement; const { textAlign, verticalAlign } = updatedTextElement;
const approxLineHeight = getApproxLineHeight(
getFontString(updatedTextElement),
);
if (updatedTextElement && isTextElement(updatedTextElement)) { if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x; let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y; let coordY = updatedTextElement.y;
@@ -211,11 +210,12 @@ export const textWysiwyg = ({
// autogrow container height if text exceeds // autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) { if (!isArrowElement(container) && textElementHeight > maxHeight) {
const diff = Math.min( const targetContainerHeight = computeContainerDimensionForBoundText(
textElementHeight - maxHeight, textElementHeight,
approxLineHeight, container.type,
); );
mutateElement(container, { height: containerDims.height + diff });
mutateElement(container, { height: targetContainerHeight });
return; return;
} else if ( } else if (
// autoshrink container height until original container height // autoshrink container height until original container height
@@ -224,11 +224,11 @@ export const textWysiwyg = ({
containerDims.height > originalContainerData.height && containerDims.height > originalContainerData.height &&
textElementHeight < maxHeight textElementHeight < maxHeight
) { ) {
const diff = Math.min( const targetContainerHeight = computeContainerDimensionForBoundText(
maxHeight - textElementHeight, textElementHeight,
approxLineHeight, container.type,
); );
mutateElement(container, { height: containerDims.height - diff }); mutateElement(container, { height: targetContainerHeight });
} }
// Start pushing text upward until a diff of 30px (padding) // Start pushing text upward until a diff of 30px (padding)
// is reached // is reached
@@ -266,24 +266,30 @@ export const textWysiwyg = ({
editable.selectionEnd = editable.value.length - diff; editable.selectionEnd = editable.value.length - diff;
} }
const lines = updatedTextElement.originalText.split("\n");
const lineHeight = updatedTextElement.containerId
? approxLineHeight
: 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); textElementWidth = Math.min(textElementWidth, maxWidth);
} else if (isFirefox || isSafari) { } else {
// As firefox, Safari needs little higher dimensions on DOM
textElementWidth += 0.5; textElementWidth += 0.5;
} }
let lineHeight = updatedTextElement.lineHeight;
// In Safari the font size gets rounded off when rendering hence calculating the line height by rounding off font size
if (isSafari) {
lineHeight = detectLineHeight({
...updatedTextElement,
fontSize: Math.round(updatedTextElement.fontSize),
});
}
// 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;
Object.assign(editable.style, { Object.assign(editable.style, {
font: getFontString(updatedTextElement), font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯ // must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`, lineHeight,
width: `${textElementWidth}px`, width: `${textElementWidth}px`,
height: `${textElementHeight}px`, height: `${textElementHeight}px`,
left: `${viewportX}px`, left: `${viewportX}px`,
@@ -303,6 +309,7 @@ export const textWysiwyg = ({
filter: "var(--theme-filter)", filter: "var(--theme-filter)",
maxHeight: `${editorMaxHeight}px`, maxHeight: `${editorMaxHeight}px`,
}); });
editable.scrollTop = 0;
// For some reason updating font attribute doesn't set font family // For some reason updating font attribute doesn't set font family
// hence updating font family explicitly for test environment // hence updating font family explicitly for test environment
if (isTestEnv()) { if (isTestEnv()) {
@@ -389,7 +396,11 @@ export const textWysiwyg = ({
font, font,
getMaxContainerWidth(container!), getMaxContainerWidth(container!),
); );
const { width, height } = measureText(wrappedText, font); const { width, height } = measureText(
wrappedText,
font,
updatedTextElement.lineHeight,
);
editable.style.width = `${width}px`; editable.style.width = `${width}px`;
editable.style.height = `${height}px`; editable.style.height = `${height}px`;
} }
+7 -1
View File
@@ -9,7 +9,7 @@ import {
import { MarkNonNullable, ValueOf } from "../utility-types"; 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" | "zigzag";
export type FontFamilyKeys = keyof typeof FONT_FAMILY; export type FontFamilyKeys = keyof typeof FONT_FAMILY;
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys]; export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
export type Theme = typeof THEME[keyof typeof THEME]; export type Theme = typeof THEME[keyof typeof THEME];
@@ -131,10 +131,16 @@ 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;
originalText: string; originalText: string;
/**
* Unitless line height (aligned to W3C). To get line height in px, multiply
* with font size (using `getLineHeightInPx` helper).
*/
lineHeight: number & { _brand: "unitlessLineHeight" };
}>; }>;
export type ExcalidrawBindableElement = export type ExcalidrawBindableElement =
+3 -4
View File
@@ -838,10 +838,9 @@ class Collab extends PureComponent<Props, CollabState> {
/> />
)} )}
{errorMessage && ( {errorMessage && (
<ErrorDialog <ErrorDialog onClose={() => this.setState({ errorMessage: "" })}>
message={errorMessage} {errorMessage}
onClose={() => this.setState({ errorMessage: "" })} </ErrorDialog>
/>
)} )}
</> </>
); );
+1 -1
View File
@@ -263,7 +263,7 @@ export const loadScene = async (
await importFromBackend(id, privateKey), await importFromBackend(id, privateKey),
localDataState?.appState, localDataState?.appState,
localDataState?.elements, localDataState?.elements,
{ repairBindings: true }, { repairBindings: true, refreshDimensions: true },
); );
} else { } else {
data = restore(localDataState || null, null, null, { data = restore(localDataState || null, null, null, {
+3 -4
View File
@@ -673,10 +673,9 @@ const ExcalidrawWrapper = () => {
</Excalidraw> </Excalidraw>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />} {excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && ( {errorMessage && (
<ErrorDialog <ErrorDialog onClose={() => setErrorMessage("")}>
message={errorMessage} {errorMessage}
onClose={() => setErrorMessage("")} </ErrorDialog>
/>
)} )}
</div> </div>
); );
+2
View File
@@ -18,6 +18,8 @@ interface Window {
EXCALIDRAW_EXPORT_SOURCE: string; EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined; EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function; gtag: Function;
_paq: any[];
_mtm: any[];
} }
interface CanvasRenderingContext2D { interface CanvasRenderingContext2D {
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "تكبير حجم الخط", "increaseFontSize": "تكبير حجم الخط",
"unbindText": "فك ربط النص", "unbindText": "فك ربط النص",
"bindText": "ربط النص بالحاوية", "bindText": "ربط النص بالحاوية",
"createContainerFromText": "",
"link": { "link": {
"edit": "تعديل الرابط", "edit": "تعديل الرابط",
"create": "إنشاء رابط", "create": "إنشاء رابط",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.", "cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
"importLibraryError": "تعذر تحميل المكتبة", "importLibraryError": "تعذر تحميل المكتبة",
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.", "collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك." "collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "تحديد", "selection": "تحديد",
@@ -303,7 +319,8 @@
"doubleClick": "انقر مرتين", "doubleClick": "انقر مرتين",
"drag": "اسحب", "drag": "اسحب",
"editor": "المحرر", "editor": "المحرر",
"editSelectedShape": "تعديل الشكل المحدد (النص/السهم/الخط)", "editLineArrowPoints": "",
"editText": "",
"github": "عثرت على مشكلة؟ إرسال", "github": "عثرت على مشكلة؟ إرسال",
"howto": "اتبع التعليمات", "howto": "اتبع التعليمات",
"or": "أو", "or": "أو",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "", "increaseFontSize": "",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "", "edit": "",
"create": "", "create": "",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Селекция", "selection": "Селекция",
@@ -303,7 +319,8 @@
"doubleClick": "", "doubleClick": "",
"drag": "плъзнете", "drag": "плъзнете",
"editor": "Редактор", "editor": "Редактор",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "Намерихте проблем? Изпратете", "github": "Намерихте проблем? Изпратете",
"howto": "Следвайте нашите ръководства", "howto": "Следвайте нашите ръководства",
"or": "или", "or": "или",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "লেখনীর মাত্রা বাড়ান", "increaseFontSize": "লেখনীর মাত্রা বাড়ান",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "লিঙ্ক সংশোধন", "edit": "লিঙ্ক সংশোধন",
"create": "লিঙ্ক তৈরী", "create": "লিঙ্ক তৈরী",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।", "cannotResolveCollabServer": "কোল্যাব সার্ভারের সাথে সংযোগ করা যায়নি। পৃষ্ঠাটি পুনরায় লোড করে আবার চেষ্টা করুন।",
"importLibraryError": "সংগ্রহ লোড করা যায়নি", "importLibraryError": "সংগ্রহ লোড করা যায়নি",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "বাছাই", "selection": "বাছাই",
@@ -303,7 +319,8 @@
"doubleClick": "", "doubleClick": "",
"drag": "", "drag": "",
"editor": "", "editor": "",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "", "github": "",
"howto": "", "howto": "",
"or": "অথবা", "or": "অথবা",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Augmenta la mida de la lletra", "increaseFontSize": "Augmenta la mida de la lletra",
"unbindText": "Desvincular el text", "unbindText": "Desvincular el text",
"bindText": "Ajusta el text al contenidor", "bindText": "Ajusta el text al contenidor",
"createContainerFromText": "",
"link": { "link": {
"edit": "Edita l'enllaç", "edit": "Edita l'enllaç",
"create": "Crea un enllaç", "create": "Crea un enllaç",
@@ -204,7 +205,22 @@
"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": "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": "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": "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." "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.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Selecció", "selection": "Selecció",
@@ -303,7 +319,8 @@
"doubleClick": "doble clic", "doubleClick": "doble clic",
"drag": "arrossega", "drag": "arrossega",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Edita la forma seleccionada (text, fletxa o línia)", "editLineArrowPoints": "",
"editText": "",
"github": "Hi heu trobat un problema? Informeu-ne", "github": "Hi heu trobat un problema? Informeu-ne",
"howto": "Seguiu les nostres guies", "howto": "Seguiu les nostres guies",
"or": "o", "or": "o",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Zvětšit písmo", "increaseFontSize": "Zvětšit písmo",
"unbindText": "Zrušit vazbu textu", "unbindText": "Zrušit vazbu textu",
"bindText": "Vázat text s kontejnerem", "bindText": "Vázat text s kontejnerem",
"createContainerFromText": "",
"link": { "link": {
"edit": "Upravit odkaz", "edit": "Upravit odkaz",
"create": "Vytvořit odkaz", "create": "Vytvořit odkaz",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Výběr", "selection": "Výběr",
@@ -303,7 +319,8 @@
"doubleClick": "dvojklik", "doubleClick": "dvojklik",
"drag": "tažení", "drag": "tažení",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "", "github": "",
"howto": "", "howto": "",
"or": "nebo", "or": "nebo",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "", "increaseFontSize": "",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "", "edit": "",
"create": "", "create": "",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "", "selection": "",
@@ -303,7 +319,8 @@
"doubleClick": "", "doubleClick": "",
"drag": "", "drag": "",
"editor": "", "editor": "",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "", "github": "",
"howto": "", "howto": "",
"or": "", "or": "",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Schrift vergrößern", "increaseFontSize": "Schrift vergrößern",
"unbindText": "Text lösen", "unbindText": "Text lösen",
"bindText": "Text an Container binden", "bindText": "Text an Container binden",
"createContainerFromText": "Text in Container einbetten",
"link": { "link": {
"edit": "Link bearbeiten", "edit": "Link bearbeiten",
"create": "Link erstellen", "create": "Link erstellen",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.", "cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut.",
"importLibraryError": "Bibliothek konnte nicht geladen werden", "importLibraryError": "Bibliothek konnte nicht geladen werden",
"collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.", "collabSaveFailed": "Keine Speicherung in der Backend-Datenbank möglich. Wenn die Probleme weiterhin bestehen, solltest Du Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.",
"collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst." "collabSaveFailed_sizeExceeded": "Keine Speicherung in der Backend-Datenbank möglich, die Zeichenfläche scheint zu groß zu sein. Du solltest Deine Datei lokal speichern, um sicherzustellen, dass Du Deine Arbeit nicht verlierst.",
"brave_measure_text_error": {
"start": "Sieht so aus, als ob du den Brave Browser benutzt mit der",
"aggressive_block_fingerprint": "\"Fingerprinting aggressiv blockieren\"",
"setting_enabled": "Einstellung aktiviert",
"break": "Dies könnte zur inkorrekten Darstellung der",
"text_elements": "Textelemente",
"in_your_drawings": "in deinen Zeichnungen führen",
"strongly_recommend": "Wir empfehlen dringend, diese Einstellung zu deaktivieren. Du kannst",
"steps": "diesen Schritten entsprechend",
"how": "folgen",
"disable_setting": " Wenn die Deaktivierung dieser Einstellung nicht zu einer korrekten Textdarstellung führt, öffne bitte einen",
"issue": "Issue",
"write": "auf GitHub, oder schreibe uns auf",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Auswahl", "selection": "Auswahl",
@@ -303,7 +319,8 @@
"doubleClick": "doppelklicken", "doubleClick": "doppelklicken",
"drag": "ziehen", "drag": "ziehen",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Ausgewählte Form bearbeiten (Text/Pfeil/Linie)", "editLineArrowPoints": "Linien-/Pfeil-Punkte bearbeiten",
"editText": "Text bearbeiten / Label hinzufügen",
"github": "Ein Problem gefunden? Informiere uns", "github": "Ein Problem gefunden? Informiere uns",
"howto": "Folge unseren Anleitungen", "howto": "Folge unseren Anleitungen",
"or": "oder", "or": "oder",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Αύξηση μεγέθους γραμματοσειράς", "increaseFontSize": "Αύξηση μεγέθους γραμματοσειράς",
"unbindText": "Αποσύνδεση κειμένου", "unbindText": "Αποσύνδεση κειμένου",
"bindText": "Δέσμευση κειμένου στο δοχείο", "bindText": "Δέσμευση κειμένου στο δοχείο",
"createContainerFromText": "",
"link": { "link": {
"edit": "Επεξεργασία συνδέσμου", "edit": "Επεξεργασία συνδέσμου",
"create": "Δημιουργία συνδέσμου", "create": "Δημιουργία συνδέσμου",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.", "cannotResolveCollabServer": "Αδυναμία σύνδεσης με τον διακομιστή συνεργασίας. Παρακαλώ ανανεώστε τη σελίδα και προσπαθήστε ξανά.",
"importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης", "importLibraryError": "Αδυναμία φόρτωσης βιβλιοθήκης",
"collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.", "collabSaveFailed": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή. Αν το προβλήματα παραμείνει, θα πρέπει να αποθηκεύσετε το αρχείο σας τοπικά για να βεβαιωθείτε ότι δεν χάνετε την εργασία σας.",
"collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας." "collabSaveFailed_sizeExceeded": "Η αποθήκευση στη βάση δεδομένων δεν ήταν δυνατή, ο καμβάς φαίνεται να είναι πολύ μεγάλος. Θα πρέπει να αποθηκεύσετε το αρχείο τοπικά για να βεβαιωθείτε ότι δεν θα χάσετε την εργασία σας.",
"brave_measure_text_error": {
"start": "Φαίνεται ότι χρησιμοποιείτε το Brave browser με το",
"aggressive_block_fingerprint": "Αποκλεισμός \"Δακτυλικών Αποτυπωμάτων\"",
"setting_enabled": "ρύθμιση ενεργοποιημένη",
"break": "Αυτό θα μπορούσε να σπάσει το",
"text_elements": "Στοιχεία Κειμένου",
"in_your_drawings": "στα σχέδιά σας",
"strongly_recommend": "Συνιστούμε να απενεργοποιήσετε αυτή τη ρύθμιση. Μπορείτε να ακολουθήσετε",
"steps": "αυτά τα βήματα",
"how": "για το πώς να το κάνετε",
"disable_setting": " Εάν η απενεργοποίηση αυτής της ρύθμισης δεν διορθώνει την εμφάνιση των στοιχείων κειμένου, παρακαλώ ανοίξτε ένα",
"issue": "πρόβλημα",
"write": "στο GitHub, ή γράψτε μας στο",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Επιλογή", "selection": "Επιλογή",
@@ -303,7 +319,8 @@
"doubleClick": "διπλό κλικ", "doubleClick": "διπλό κλικ",
"drag": "σύρε", "drag": "σύρε",
"editor": "Επεξεργαστής", "editor": "Επεξεργαστής",
"editSelectedShape": "Επεξεργασία επιλεγμένου σχήματος (κείμενο/βέλος/γραμμή)", "editLineArrowPoints": "",
"editText": "",
"github": "Βρήκατε πρόβλημα; Υποβάλετε το", "github": "Βρήκατε πρόβλημα; Υποβάλετε το",
"howto": "Ακολουθήστε τους οδηγούς μας", "howto": "Ακολουθήστε τους οδηγούς μας",
"or": "ή", "or": "ή",
+18 -3
View File
@@ -120,7 +120,6 @@
"edit": "Edit line", "edit": "Edit line",
"exit": "Exit line editor" "exit": "Exit line editor"
}, },
"elementLock": { "elementLock": {
"lock": "Lock", "lock": "Lock",
"unlock": "Unlock", "unlock": "Unlock",
@@ -206,7 +205,22 @@
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.", "cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
"importLibraryError": "Couldn't load library", "importLibraryError": "Couldn't load library",
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.", "collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work." "collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
"brave_measure_text_error": {
"start": "Looks like you are using Brave browser with the",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
"setting_enabled": "setting enabled",
"break": "This could result in breaking the",
"text_elements": "Text Elements",
"in_your_drawings": "in your drawings",
"strongly_recommend": "We strongly recommend disabling this setting. You can follow",
"steps": "these steps",
"how": "on how to do so",
"disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
"issue": "issue",
"write": "on our GitHub, or write us on",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Selection", "selection": "Selection",
@@ -305,7 +319,8 @@
"doubleClick": "double-click", "doubleClick": "double-click",
"drag": "drag", "drag": "drag",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Edit selected shape (text/arrow/line)", "editLineArrowPoints": "Edit line/arrow points",
"editText": "Edit text / add label",
"github": "Found an issue? Submit", "github": "Found an issue? Submit",
"howto": "Follow our guides", "howto": "Follow our guides",
"or": "or", "or": "or",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Aumentar el tamaño de letra", "increaseFontSize": "Aumentar el tamaño de letra",
"unbindText": "Desvincular texto", "unbindText": "Desvincular texto",
"bindText": "Vincular texto al contenedor", "bindText": "Vincular texto al contenedor",
"createContainerFromText": "Envolver el texto en un contenedor",
"link": { "link": {
"edit": "Editar enlace", "edit": "Editar enlace",
"create": "Crear enlace", "create": "Crear enlace",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.", "cannotResolveCollabServer": "No se pudo conectar al servidor colaborador. Por favor, vuelva a cargar la página y vuelva a intentarlo.",
"importLibraryError": "No se pudo cargar la librería", "importLibraryError": "No se pudo cargar la librería",
"collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.", "collabSaveFailed": "No se pudo guardar en la base de datos del backend. Si los problemas persisten, debería guardar su archivo localmente para asegurarse de que no pierde su trabajo.",
"collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo." "collabSaveFailed_sizeExceeded": "No se pudo guardar en la base de datos del backend, el lienzo parece ser demasiado grande. Debería guardar el archivo localmente para asegurarse de que no pierde su trabajo.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "ajuste activado",
"break": "Esto podría resultar en romper los",
"text_elements": "Elementos de texto",
"in_your_drawings": "en tus dibujos",
"strongly_recommend": "Recomendamos desactivar esta configuración. Puedes seguir",
"steps": "estos pasos",
"how": "sobre cómo hacerlo",
"disable_setting": " Si deshabilitar esta opción no arregla la visualización de elementos de texto, por favor abre un",
"issue": "issue",
"write": "en GitHub, o escríbenos en",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Selección", "selection": "Selección",
@@ -303,7 +319,8 @@
"doubleClick": "doble clic", "doubleClick": "doble clic",
"drag": "arrastrar", "drag": "arrastrar",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Editar la forma seleccionada (texto/flecha/línea)", "editLineArrowPoints": "",
"editText": "",
"github": "¿Ha encontrado un problema? Envíelo", "github": "¿Ha encontrado un problema? Envíelo",
"howto": "Siga nuestras guías", "howto": "Siga nuestras guías",
"or": "o", "or": "o",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Handitu letra tamaina", "increaseFontSize": "Handitu letra tamaina",
"unbindText": "Askatu testua", "unbindText": "Askatu testua",
"bindText": "Lotu testua edukiontziari", "bindText": "Lotu testua edukiontziari",
"createContainerFromText": "Bilatu testua edukiontzi batean",
"link": { "link": {
"edit": "Editatu esteka", "edit": "Editatu esteka",
"create": "Sortu esteka", "create": "Sortu esteka",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.", "cannotResolveCollabServer": "Ezin izan da elkarlaneko zerbitzarira konektatu. Mesedez, berriro kargatu orria eta saiatu berriro.",
"importLibraryError": "Ezin izan da liburutegia kargatu", "importLibraryError": "Ezin izan da liburutegia kargatu",
"collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.", "collabSaveFailed": "Ezin izan da backend datu-basean gorde. Arazoak jarraitzen badu, zure fitxategia lokalean gorde beharko zenuke zure lana ez duzula galtzen ziurtatzeko.",
"collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko." "collabSaveFailed_sizeExceeded": "Ezin izan da backend datu-basean gorde, ohiala handiegia dela dirudi. Fitxategia lokalean gorde beharko zenuke zure lana galtzen ez duzula ziurtatzeko.",
"brave_measure_text_error": {
"start": "Brave nabigatzailea erabiltzen ari zarela dirudi",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
"setting_enabled": "ezarpena gaituta",
"break": "Honek honen haustea eragin dezake",
"text_elements": "Testu-elementuak",
"in_your_drawings": "zure marrazkietan",
"strongly_recommend": "Ezarpen hau desgaitzea gomendatzen dugu. Jarrai dezakezu",
"steps": "urrats hauek",
"how": "jakiteko nola egin",
"disable_setting": " Ezarpen hau desgaitzeak testu-elementuen bistaratzea konpontzen ez badu, ireki",
"issue": "eskaera (issue) bat",
"write": "gure Github-en edo idatz iezaguzu",
"discord": "Discord-en"
}
}, },
"toolBar": { "toolBar": {
"selection": "Hautapena", "selection": "Hautapena",
@@ -303,7 +319,8 @@
"doubleClick": "klik bikoitza", "doubleClick": "klik bikoitza",
"drag": "arrastatu", "drag": "arrastatu",
"editor": "Editorea", "editor": "Editorea",
"editSelectedShape": "Editatu hautatutako forma (testua/gezia/lerroa)", "editLineArrowPoints": "",
"editText": "",
"github": "Arazorik izan al duzu? Eman horren berri", "github": "Arazorik izan al duzu? Eman horren berri",
"howto": "Jarraitu gure gidak", "howto": "Jarraitu gure gidak",
"or": "edo", "or": "edo",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "افزایش دادن اندازه فونت", "increaseFontSize": "افزایش دادن اندازه فونت",
"unbindText": "بازکردن نوشته", "unbindText": "بازکردن نوشته",
"bindText": "بستن نوشته", "bindText": "بستن نوشته",
"createContainerFromText": "",
"link": { "link": {
"edit": "ویرایش لینک", "edit": "ویرایش لینک",
"create": "ایجاد پیوند", "create": "ایجاد پیوند",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.", "cannotResolveCollabServer": "به سرور collab متصل نشد. لطفا صفحه را مجددا بارگذاری کنید و دوباره تلاش کنید.",
"importLibraryError": "داده‌ها بارگذاری نشدند", "importLibraryError": "داده‌ها بارگذاری نشدند",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "گزینش", "selection": "گزینش",
@@ -303,7 +319,8 @@
"doubleClick": "دابل کلیک", "doubleClick": "دابل کلیک",
"drag": "کشیدن", "drag": "کشیدن",
"editor": "ویرایشگر", "editor": "ویرایشگر",
"editSelectedShape": "ویرایش شکل انتخاب شده (متن/فلش/خط)", "editLineArrowPoints": "",
"editText": "",
"github": "اشکالی می بینید؟ گزارش دهید", "github": "اشکالی می بینید؟ گزارش دهید",
"howto": "راهنمای ما را دنبال کنید", "howto": "راهنمای ما را دنبال کنید",
"or": "یا", "or": "یا",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Kasvata kirjasinkokoa", "increaseFontSize": "Kasvata kirjasinkokoa",
"unbindText": "Irroita teksti", "unbindText": "Irroita teksti",
"bindText": "Kiinnitä teksti säiliöön", "bindText": "Kiinnitä teksti säiliöön",
"createContainerFromText": "",
"link": { "link": {
"edit": "Muokkaa linkkiä", "edit": "Muokkaa linkkiä",
"create": "Luo linkki", "create": "Luo linkki",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.", "cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
"importLibraryError": "Kokoelman lataaminen epäonnistui", "importLibraryError": "Kokoelman lataaminen epäonnistui",
"collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.", "collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
"collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi." "collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Valinta", "selection": "Valinta",
@@ -303,7 +319,8 @@
"doubleClick": "kaksoisnapsautus", "doubleClick": "kaksoisnapsautus",
"drag": "vedä", "drag": "vedä",
"editor": "Muokkausohjelma", "editor": "Muokkausohjelma",
"editSelectedShape": "Muokkaa valittua muotoa (teksti/nuoli/viiva)", "editLineArrowPoints": "",
"editText": "",
"github": "Löysitkö ongelman? Kerro meille", "github": "Löysitkö ongelman? Kerro meille",
"howto": "Seuraa oppaitamme", "howto": "Seuraa oppaitamme",
"or": "tai", "or": "tai",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Augmenter la taille de la police", "increaseFontSize": "Augmenter la taille de la police",
"unbindText": "Dissocier le texte", "unbindText": "Dissocier le texte",
"bindText": "Associer le texte au conteneur", "bindText": "Associer le texte au conteneur",
"createContainerFromText": "Encadrer le texte dans un conteneur",
"link": { "link": {
"edit": "Modifier le lien", "edit": "Modifier le lien",
"create": "Ajouter un lien", "create": "Ajouter un lien",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.", "cannotResolveCollabServer": "Impossible de se connecter au serveur collaboratif. Veuillez recharger la page et réessayer.",
"importLibraryError": "Impossible de charger la bibliothèque", "importLibraryError": "Impossible de charger la bibliothèque",
"collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.", "collabSaveFailed": "Impossible d'enregistrer dans la base de données en arrière-plan. Si des problèmes persistent, vous devriez enregistrer votre fichier localement pour vous assurer de ne pas perdre votre travail.",
"collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail." "collabSaveFailed_sizeExceeded": "Impossible d'enregistrer dans la base de données en arrière-plan, le tableau semble trop grand. Vous devriez enregistrer le fichier localement pour vous assurer de ne pas perdre votre travail.",
"brave_measure_text_error": {
"start": "Il semble que vous utilisiez le navigateur Brave avec le",
"aggressive_block_fingerprint": "blocage d'empreinte agressif",
"setting_enabled": "activé",
"break": "Ceci pourrait avoir pour conséquence de « casser » les",
"text_elements": "éléments texte",
"in_your_drawings": "dans vos dessins",
"strongly_recommend": "Nous recommandons fortement de désactiver ce paramètre. Vous pouvez suivre",
"steps": "ces étapes",
"how": "sur la manière de procéder",
"disable_setting": " Si la désactivation de ce paramètre ne résout pas l'affichage des éléments texte, veuillez ouvrir un",
"issue": "ticket",
"write": "sur notre GitHub, ou nous écrire sur",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Sélection", "selection": "Sélection",
@@ -303,7 +319,8 @@
"doubleClick": "double-clic", "doubleClick": "double-clic",
"drag": "glisser", "drag": "glisser",
"editor": "Éditeur", "editor": "Éditeur",
"editSelectedShape": "Modifier la forme sélectionnée (texte/flèche/ligne)", "editLineArrowPoints": "",
"editText": "",
"github": "Problème trouvé ? Soumettre", "github": "Problème trouvé ? Soumettre",
"howto": "Suivez nos guides", "howto": "Suivez nos guides",
"or": "ou", "or": "ou",
+20 -3
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Aumentar o tamaño da fonte", "increaseFontSize": "Aumentar o tamaño da fonte",
"unbindText": "Desvincular texto", "unbindText": "Desvincular texto",
"bindText": "Ligar o texto ao contedor", "bindText": "Ligar o texto ao contedor",
"createContainerFromText": "Envolver o texto nun contedor",
"link": { "link": {
"edit": "Editar ligazón", "edit": "Editar ligazón",
"create": "Crear ligazón", "create": "Crear ligazón",
@@ -193,7 +194,7 @@
"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": "" "collabOfflineWarning": "Non hai conexión a Internet dispoñible.\nOs teus cambios non serán gardados!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Tipo de ficheiro non soportado.", "unsupportedFileType": "Tipo de ficheiro non soportado.",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.", "cannotResolveCollabServer": "Non se puido conectar ao servidor de colaboración. Por favor recargue a páxina e probe de novo.",
"importLibraryError": "Non se puido cargar a biblioteca", "importLibraryError": "Non se puido cargar a biblioteca",
"collabSaveFailed": "Non se puido gardar na base de datos. Se o problema persiste, deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.", "collabSaveFailed": "Non se puido gardar na base de datos. Se o problema persiste, deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.",
"collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo." "collabSaveFailed_sizeExceeded": "Non se puido gardar na base de datos, o lenzo semella demasiado grande. Deberías gardar o teu arquivo de maneira local para asegurarte de non perdelo teu traballo.",
"brave_measure_text_error": {
"start": "Semella que estás usando o navegador Brave coa opción",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
"setting_enabled": "activada",
"break": "Isto podería provocar unha ruptura dos",
"text_elements": "Elementos de Texto",
"in_your_drawings": "nos seus debuxos",
"strongly_recommend": "Recomendámoslle encarecidamente que desactive esa opción. Pode seguir",
"steps": "estes pasos",
"how": "sobre como facelo",
"disable_setting": " Se ao desactivar esta opción non se arranxa o problema ao mostrar os elementos de texto, por favor abra unha",
"issue": "issue",
"write": "no noso GitHub, ou escríbenos ao",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Selección", "selection": "Selección",
@@ -303,7 +319,8 @@
"doubleClick": "dobre-clic", "doubleClick": "dobre-clic",
"drag": "arrastrar", "drag": "arrastrar",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Editar a forma seleccionada (texto/frecha/liña)", "editLineArrowPoints": "",
"editText": "",
"github": "Encontrou un problema? Envíeo", "github": "Encontrou un problema? Envíeo",
"howto": "Sigue as nosas normas", "howto": "Sigue as nosas normas",
"or": "ou", "or": "ou",
+162 -145
View File
@@ -1,18 +1,18 @@
{ {
"labels": { "labels": {
"paste": "הדבק", "paste": "הדבק",
"pasteAsPlaintext": "", "pasteAsPlaintext": "הדבק ללא עיצוב",
"pasteCharts": "הדבק גרפים", "pasteCharts": "הדבק גרפים",
"selectAll": "בחר הכל", "selectAll": "בחר הכל",
"multiSelect": "הוסף אובייקט לבחירה", "multiSelect": "הוסף רכיב לבחירה",
"moveCanvas": "הזז את הקנבס", "moveCanvas": "הזז את הקנבס",
"cut": "חתוך", "cut": "גזור",
"copy": "העתק", "copy": "העתק",
"copyAsPng": "העתק ללוח כ PNG", "copyAsPng": "העתק ללוח כ PNG",
"copyAsSvg": "העתק ללוח כ SVG", "copyAsSvg": "העתק ללוח כ SVG",
"copyText": "העתק ללוח כ-PNG", "copyText": "העתק ללוח כטקסט",
"bringForward": "הבא שכבה קדימה", "bringForward": "הבא שכבה קדימה",
"sendToBack": "העבר לסוף", "sendToBack": "שלח אחורה",
"bringToFront": "העבר לחזית", "bringToFront": "העבר לחזית",
"sendBackward": "העבר שכבה אחורה", "sendBackward": "העבר שכבה אחורה",
"delete": "מחק", "delete": "מחק",
@@ -26,7 +26,7 @@
"strokeStyle_solid": "מלא", "strokeStyle_solid": "מלא",
"strokeStyle_dashed": "מקווקו", "strokeStyle_dashed": "מקווקו",
"strokeStyle_dotted": "מנוקד", "strokeStyle_dotted": "מנוקד",
"sloppiness": "סגנון", "sloppiness": "רישול",
"opacity": "אטימות", "opacity": "אטימות",
"textAlign": "יישור טקסט", "textAlign": "יישור טקסט",
"edges": "קצוות", "edges": "קצוות",
@@ -35,57 +35,57 @@
"arrowheads": "ראשי חצים", "arrowheads": "ראשי חצים",
"arrowhead_none": "ללא", "arrowhead_none": "ללא",
"arrowhead_arrow": "חץ", "arrowhead_arrow": "חץ",
"arrowhead_bar": "שורה", "arrowhead_bar": "קצה אנכי",
"arrowhead_dot": "נקודה", "arrowhead_dot": "נקודה",
"arrowhead_triangle": "משולש", "arrowhead_triangle": "משולש",
"fontSize": "גודל גופן", "fontSize": "גודל גופן",
"fontFamily": "סוג הגופן", "fontFamily": "גופן",
"onlySelected": "רק מה שנבחר", "onlySelected": "רק מה שנבחר",
"withBackground": "רקע", "withBackground": "רקע",
"exportEmbedScene": "הטמעה של מידע הסצנה", "exportEmbedScene": "הטמעה של מידע הסצנה",
"exportEmbedScene_details": "מידע התצוגה יישמר לקובץ המיוצא מסוג PNG/SVG כך שיהיה ניתן לשחזרה ממנו.\nהפעולה תגדיל את גודל הקובץ המיוצא.", "exportEmbedScene_details": "הייצוא יבוצע לקובץ מסוג PNG/SVG כדי שהמידע על הסצנה ישמר בו וניתן יהיה לבצע שחזור ממנו.\nיגדיל את גודל הקובץ של הייצוא.",
"addWatermark": "הוסף \"נוצר באמצעות Excalidraw\"", "addWatermark": "הוסף \"נוצר באמצעות Excalidraw\"",
"handDrawn": "כתב יד", "handDrawn": "ציור יד",
"normal": "רגיל", "normal": "רגיל",
"code": "קוד", "code": "קוד",
"small": "קטן", "small": "קטן",
"medium": "בנוני", "medium": ינוני",
"large": "גדול", "large": "גדול",
"veryLarge": "ענק", "veryLarge": "גדול מאוד",
"solid": "מוצק", "solid": "מוצק",
"hachure": "קווים משופעים", "hachure": "קווים מקבילים קצרים להצגת כיוון וחדות שיפוע במפה",
"crossHatch": "קווים מוצלבים", "crossHatch": "קווים מוצלבים שתי וערב",
"thin": "דק", "thin": "דק",
"bold": "מודגש", "bold": "מודגש",
"left": "שמאל", "left": "שמאל",
"center": "מרכז", "center": "מרכז",
"right": "ימין", "right": "ימין",
"extraBold": "עבה", "extraBold": "מודגש במיוחד",
"architect": "ארכיטקט", "architect": "ארכיטקט",
"artist": "אמן", "artist": "אמן",
"cartoonist": "קריקטוריסט", "cartoonist": "קריקטוריסט",
"fileTitle": "שם קובץ", "fileTitle": "שם קובץ",
"colorPicker": חירת צבע", "colorPicker": וחר צבעים",
"canvasColors": "בשימוש בקנבס", "canvasColors": "בשימוש בקנבס",
"canvasBackground": "רקע הלוח", "canvasBackground": "רקע קנבס",
"drawingCanvas": "לוח ציור", "drawingCanvas": "קנבס ציור",
"layers": "שכבות", "layers": "שכבות",
"actions": "פעולות", "actions": "פעולות",
"language": "שפה", "language": "שפה",
"liveCollaboration": "", "liveCollaboration": "התחל שיתוף חי...",
"duplicateSelection": "שכפל", "duplicateSelection": "שכפל",
"untitled": "ללא כותרת", "untitled": "ללא כותרת",
"name": "שם", "name": "שם",
"yourName": ם", "yourName": מך",
"madeWithExcalidraw": "נוצר באמצעות Excalidraw", "madeWithExcalidraw": "נוצר באמצעות Excalidraw",
"group": "אחד לקבוצה", "group": "קבץ",
"ungroup": "פרק קבוצה", "ungroup": "פרק קבוצה",
"collaborators": "שותפים", "collaborators": "שותפים",
"showGrid": "הצג רשת", "showGrid": "הצג רשת",
"addToLibrary": "הוסף לספריה", "addToLibrary": "הוסף לספריה",
"removeFromLibrary": "הסר מספריה", "removeFromLibrary": "הסר מספריה",
"libraryLoadingMessage": "טוען ספריה…", "libraryLoadingMessage": "טוען ספריה…",
"libraries": "דפדף בספריות", "libraries": "עיין בספריות",
"loadingScene": "טוען תצוגה…", "loadingScene": "טוען תצוגה…",
"align": "יישר", "align": "יישר",
"alignTop": "יישר למעלה", "alignTop": "יישר למעלה",
@@ -96,28 +96,29 @@
"centerHorizontally": "מרכז אופקית", "centerHorizontally": "מרכז אופקית",
"distributeHorizontally": "חלוקה אופקית", "distributeHorizontally": "חלוקה אופקית",
"distributeVertically": "חלוקה אנכית", "distributeVertically": "חלוקה אנכית",
"flipHorizontal": "סובב אופקית", "flipHorizontal": "הפוך אופקית",
"flipVertical": "סובב אנכית", "flipVertical": "הפוך אנכית",
"viewMode": "מצב תצוגה", "viewMode": "מצב תצוגה",
"toggleExportColorScheme": "שנה את ערכת צבעי הייצוא", "toggleExportColorScheme": "מתג ערכת צבעים לייצוא",
"share": "שתף", "share": "שתף",
"showStroke": "הצג צבעי קו מתאר", "showStroke": "הצג בוחר צבע מברשת",
"showBackground": "הצג צבעי רקע", "showBackground": "הצג בוחר צבע רקע",
"toggleTheme": "שינוי ערכת העיצוב", "toggleTheme": "שינוי ערכת העיצוב",
"personalLib": "ספריה פרטית", "personalLib": "ספריה פרטית",
"excalidrawLib": "הספריה של Excalidraw", "excalidrawLib": "הספריה של Excalidraw",
"decreaseFontSize": "הקטן את גודל הגופן", "decreaseFontSize": "הקטן את גודל הגופן",
"increaseFontSize": "הגדל את גודל הגופן", "increaseFontSize": "הגדל את גודל הגופן",
"unbindText": "ביטול קיבוע הטקסט", "unbindText": "ביטול קיבוע הטקסט",
"bindText": "קיבוע הטקסט לאוגד", "bindText": "קיבוע הטקסט למיכל",
"createContainerFromText": "ארוז טקסט במיכל",
"link": { "link": {
"edit": "עריכת קישור", "edit": "עריכת קישור",
"create": "יצירת קישור", "create": "יצירת קישור",
"label": "קישור" "label": "קישור"
}, },
"lineEditor": { "lineEditor": {
"edit": "", "edit": "ערוך קו",
"exit": "" "exit": "צא מעורך הקו"
}, },
"elementLock": { "elementLock": {
"lock": "נעילה", "lock": "נעילה",
@@ -125,16 +126,16 @@
"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": "שמור ל...",
@@ -143,7 +144,7 @@
"copyToClipboard": "העתק ללוח", "copyToClipboard": "העתק ללוח",
"copyPngToClipboard": "העתק PNG ללוח", "copyPngToClipboard": "העתק PNG ללוח",
"scale": "קנה מידה", "scale": "קנה מידה",
"save": "שמירת קובץ נוכחי", "save": "שמור לקובץ נוכחי",
"saveAs": "שמירה בשם", "saveAs": "שמירה בשם",
"load": "פתח", "load": "פתח",
"getShareableLink": "קבל קישור לשיתוף", "getShareableLink": "קבל קישור לשיתוף",
@@ -159,58 +160,73 @@
"undo": "בטל", "undo": "בטל",
"redo": "בצע מחדש", "redo": "בצע מחדש",
"resetLibrary": "איפוס ספריה", "resetLibrary": "איפוס ספריה",
"createNewRoom": "צור חדר", "createNewRoom": "צור חדר חדש",
"fullScreen": "מסך מלא", "fullScreen": "מסך מלא",
"darkMode": "מצב כהה", "darkMode": "מצב כהה",
"lightMode": "מצב בהיר", "lightMode": "מצב בהיר",
"zenMode": "מצב זן", "zenMode": "מצב זן",
"exitZenMode": "צא ממצב תפריט מרחף", "exitZenMode": "צא ממצב זן",
"cancel": "ביטול", "cancel": "ביטול",
"clear": "ניקוי", "clear": "ניקוי",
"remove": "מחיקה", "remove": סר",
"publishLibrary": ירסום", "publishLibrary": "פרסום",
"submit": "שליחה", "submit": "שליחה",
"confirm": "לאשר" "confirm": "אשר"
}, },
"alerts": { "alerts": {
"clearReset": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "clearReset": "פעולה זו תנקה את כל הקנבס. אתה בטוח?",
"couldNotCreateShareableLink": "לא ניתן לייצר לינק לשיתוף.", "couldNotCreateShareableLink": "יצירת קישור לשיתוף נכשל.",
"couldNotCreateShareableLinkTooBig": "לא הצלחנו לייצר קישור לשיתוף: התצוגה גדולה מדי", "couldNotCreateShareableLinkTooBig": "יצירת קישור לשיתוף נכשל: התצוגה גדולה מדי",
"couldNotLoadInvalidFile": "לא ניתן לטעון קובץ שאיננו תואם", "couldNotLoadInvalidFile": "טעינת קובץ לא תקין נכשלה",
"importBackendFailed": "ייבוא מהשרת נכשל.", "importBackendFailed": "ייבוא מהשרת נכשל.",
"cannotExportEmptyCanvas": "לא ניתן לייצא לוח ריק.", "cannotExportEmptyCanvas": "לא ניתן לייצא קנבאס ריק.",
"couldNotCopyToClipboard": "לא ניתן היה להעתיק ללוח", "couldNotCopyToClipboard": "לא ניתן היה להעתיק ללוח.",
"decryptFailed": "לא ניתן לפענח מידע.", "decryptFailed": "פיענוח ההצפנה של המידע נכשל.",
"uploadedSecurly": "ההעלאה הוצפנה מקצה לקצה, ולכן שרת Excalidraw וצד שלישי לא יכולים לקרוא את התוכן.", "uploadedSecurly": "ההעלאה אובטחה באמצעות הצפנה מקצה לקצה, פירוש הדבר שהשרת של Excalidraw וגורמי צד ג׳ לא יכולים לקרוא את התוכן.",
"loadSceneOverridePrompt": "טעינה של ציור חיצוני תחליף את התוכן הקיים שלך. האם תרצה להמשיך?", "loadSceneOverridePrompt": "טעינה של ציור חיצוני תחליף את התוכן הקיים שלך. האם תרצה להמשיך?",
"collabStopOverridePrompt": "עצירת השיתוף תוביל למחיקת התרשימים השמורים בדפדפן. האם את/ה בטוח/ה?\n(אם תרצה לשמור את התרשימים הקיימים, תוכל לסגור את הדפדפן מבלי לסיים את השיתוף.)", "collabStopOverridePrompt": "עצירת השיתוף תוביל למחיקת הציור הקודם ששמור מקומית בדפדפן. האם אתה בטוח?\n\n(אם תרצה לשמור את הציור המקומי, סגור את הטאב של הדפדפן במקום.)",
"errorAddingToLibrary": "לא ניתן להוסיף פריט לספרייה", "errorAddingToLibrary": "לא ניתן להוסיף פריט לספרייה",
"errorRemovingFromLibrary": "לא ניתן למחוק פריט מהספריה", "errorRemovingFromLibrary": "לא ניתן להסיר פריט מהספריה",
"confirmAddLibrary": "הפעולה תוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?", "confirmAddLibrary": "זה יוסיף {{numShapes}} צורה(ות) לספריה שלך. האם אתה בטוח?",
"imageDoesNotContainScene": "נראה שהתמונה לא מכילה מידע על הסצינה. האם אפשרת הטמעת מידע הסצינה בעת השמירה?", "imageDoesNotContainScene": "נראה שהתמונה לא מכילה מידע על הסצינה. האם אפשרת הטמעת מידע הסצינה בעת השמירה?",
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה", "cannotRestoreFromImage": "לא הצלחנו לשחזר את הסצנה מקובץ התמונה",
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.", "invalidSceneUrl": "ייבוא מידע סצנה מהקישור שסופק כשל. או שהוא משובש, או שאינו מכיל מידע של Excalidraw בפורמט JSON.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "resetLibrary": "פעולה זו תנקה את כל הספריה שלך. אתה בטוח?",
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?", "removeItemsFromsLibrary": "מחק {{count}} פריט(ים) מהספריה?",
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.", "invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מנוטרל.",
"collabOfflineWarning": "" "collabOfflineWarning": "אין חיבור זמין לאינטרנט.\nהשינויים שלך לא ישמרו!"
}, },
"errors": { "errors": {
"unsupportedFileType": "סוג הקובץ אינו נתמך.", "unsupportedFileType": "סוג הקובץ אינו נתמך.",
"imageInsertError": "לא ניתן היה להטמיע את התמונה, אנא נסו שוב מאוחר יותר...", "imageInsertError": "לא ניתן היה להוסיף את התמונה. אנא נסה שוב מאוחר יותר...",
"fileTooBig": "הקובץ כבד מדי. הגודל המקסימלי המותר הוא {{maxSize}}.", "fileTooBig": "הקובץ גדול מדי. הגודל המירבי המותר הינו {{maxSize}}.",
"svgImageInsertError": "לא ניתן היה להטמיע את תמונת ה-SVG. קידוד ה-SVG אינו תקני.", "svgImageInsertError": "לא ניתן היה להוסיף את תמונת ה-SVG. הסימונים בתוך קובץ ה-SVG עשויים להיות שגויים.",
"invalidSVGString": "SVG בלתי תקני.", "invalidSVGString": "SVG שגוי.",
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "לא הצלחתי להתחבר לשרת השיתוף. אנא רענן את הדף ונסה שוב.",
"importLibraryError": "לא ניתן היה לטעון את הספריה", "importLibraryError": "לא ניתן היה לטעון את הספריה",
"collabSaveFailed": "", "collabSaveFailed": "לא הצלחתי להתחבר למסד הנתונים האחורי. אם הבעיה ממשיכה, כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "לא הצלחתי לשמור למסד הנתונים האחורי, נראה שהקנבס שלך גדול מדי. כדאי שתשמור את הקובץ מקומית כדי לוודא שלא תאבד את העבודה שלך.",
"brave_measure_text_error": {
"start": "נראה שאתה משתמש בדפדפן Brave עם ה-",
"aggressive_block_fingerprint": "חסימת Fingerprinting אגרסיבית",
"setting_enabled": "ההגדרה מופעלת",
"break": "זה יכול לגרום לבעיה ב-",
"text_elements": "רכיבי טקסט",
"in_your_drawings": "בציורים שלך",
"strongly_recommend": "אנו ממליצים בחום לנטרל את ההגדרה הזו. אתה יכול לעקוב",
"steps": "הצעדים הבאים",
"how": "כיצד לעשות את זה",
"disable_setting": " אם נטרול ההגדרה לא מתקן את תצוגת רכיבי הטקסט, אנא פתח",
"issue": "בעיה",
"write": "ב- GitHub שלנו, או כתוב לנו ב-",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "בחירה", "selection": "בחירה",
"image": "הוספת תמונה", "image": "הוספת תמונה",
"rectangle": רובע", "rectangle": לבן",
"diamond": "מעוין", "diamond": "יהלום",
"ellipse": "אליפסה", "ellipse": "אליפסה",
"arrow": "חץ", "arrow": "חץ",
"line": "קו", "line": "קו",
@@ -218,79 +234,79 @@
"text": "טקסט", "text": "טקסט",
"library": "ספריה", "library": "ספריה",
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור", "lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
"penMode": "", "penMode": "מצב עט - מנע נגיעה",
"link": "הוספה/עדכון של קישור עבור הצורה הנבחרת", "link": "הוספה/עדכון קישור של הצורה שנבחרה",
"eraser": "מחק", "eraser": "מחק",
"hand": "" "hand": "יד (כלי הזזה)"
}, },
"headings": { "headings": {
"canvasActions": "פעולות הלוח", "canvasActions": "פעולות קנבאס",
"selectedShapeActions": "פעולות צורה שנבחרה", "selectedShapeActions": "פעולות על הצורות שנבחרו",
"shapes": "צורות" "shapes": "צורות"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "כדי להזיז את הקנבס, החזק את גלגל העכבר לחוץ או את מקש הרווח לחוץ תוך כדי גרירה, או השתמש בכלי היד",
"linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד", "linearElement": "לחץ להתחלת מספר נקודות, גרור לקו יחיד",
"freeDraw": "לחץ וגרור, שחרר כשסיימת", "freeDraw": "לחץ וגרור, שחרר כשסיימת",
"text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה", "text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה",
"text_selected": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות", "text_selected": "לחץ לחיצה כפולה או הקש על אנטר לעריכת הטקסט",
"text_editing": "כדי לסיים את העריכה לחצו על מקש Escape או על Ctrl ומקש Enter (Cmd במחשבי אפל)", "text_editing": "כדי לסיים את העריכה לחץ על מקש Escape או על Ctrl (Cmd במחשבי אפל) ומקש Enter",
"linearElementMulti": "הקלק על הנקודה האחרונה או הקש Escape או Enter לסיום", "linearElementMulti": "הקלק על הנקודה האחרונה או הקש Escape או Enter לסיום",
"lockAngle": "אתה יכול להגביל זווית ע״י לחיצה על SHIFT", "lockAngle": "ניתן להגביל את הזוויות על ידי החזקה של מקש ה- SHIFT",
"resize": "ניתן להגביל פרופורציות על ידי לחיצה על SHIFT תוך כדי שינוי גודל,\nהחזק ALT בשביל לשנות גודל ביחס למרכז", "resize": "ניתן להגביל פרופורציות על ידי לחיצה על SHIFT תוך כדי שינוי גודל,\nהחזק ALT בשביל לשנות גודל ביחס למרכז",
"resizeImage": "", "resizeImage": "אתה יכול לשנות את הגודל בחופשיות על ידי החזקת מקש SHIFT,\nהחזק את מקש ALT כדי לבצע שינוי גודל מהמרכז",
"rotate": "ניתן להגביל זוויות על ידי לחיצה על SHIFT תוך כדי סיבוב", "rotate": "ניתן להגביל זוויות על ידי לחיצה על SHIFT תוך כדי סיבוב",
"lineEditor_info": "", "lineEditor_info": "החזק Ctrl / Cmd ובצע לחיצה כפולה או לחץ Ctrl / Cmd + Enter לעריכת נקודות",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "לחץ Delete למחיקת נקודה/ות,\nCtrl / Cmd + D לשכפול, או גרור להזזה",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "בחר נקודה כדי לערוך (החזק SHIFT לבחירת כמה),\nאו החזק Alt והקלק להוספת נקודות חדשות",
"placeImage": "", "placeImage": "הקלק להנחת התמונה, או הקלק וגרור להגדרת הגודל שלו ידנית",
"publishLibrary": ירסום ספריה אישית", "publishLibrary": "פרסם ספריה משלך",
"bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט", "bindTextToElement": "הקש Enter כדי להוספת טקסט",
"deepBoxSelect": "", "deepBoxSelect": "החזק Ctrl / Cmd לבחירה עמוקה ולמניעת גרירה",
"eraserRevert": "", "eraserRevert": "החזק Alt להחזרת רכיבים מסומנים למחיקה",
"firefox_clipboard_write": "" "firefox_clipboard_write": "יכולות זה ניתנת להפעלה על ידי שינוי הדגל של \"dom.events.asyncClipboard.clipboardItem\" למצב \"true\". כדי לשנות את הדגל בדפדפן Firefox, בקר בעמוד ״about:config״."
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה", "cannotShowPreview": "לא ניתן להראות תצוגה מקדימה",
"canvasTooBig": "הקנבס עלול להיות גדול מדי.", "canvasTooBig": "הקנבס עלול להיות גדול מדי.",
"canvasTooBigTip": "טיפ: נסה להזיז את האלמנטים הרחוקים ביותר מעט קרוב יותר יחד." "canvasTooBigTip": "טיפ: נסה להזיז את הרכיבים הרחוקים ביותר מעט קרוב יותר האחד לשני."
}, },
"errorSplash": { "errorSplash": {
"headingMain_pre": "אירעה שגיאה. נסה ", "headingMain_pre": "אירעה שגיאה. נסה ",
"headingMain_button": "טוען את העמוד מחדש.", "headingMain_button": "טוען את העמוד מחדש.",
"clearCanvasMessage": "אם טעינה מחדש לא עובדת, נסה ", "clearCanvasMessage": "אם טעינה מחדש לא עובדת, נסה ",
"clearCanvasMessage_button": "מנקה את הלוח.", "clearCanvasMessage_button": "מנקה את הקנבאס.",
"clearCanvasCaveat": " זה יביא לאובדן עבודה ", "clearCanvasCaveat": " זה יגרום לאובדן העבודה ",
"trackedToSentry_pre": "שגיאה עם מזהה ", "trackedToSentry_pre": "השגיאה עם מזהה ",
"trackedToSentry_post": " נמצאה במערכת שלנו.", "trackedToSentry_post": " נמצאה במערכת שלנו.",
"openIssueMessage_pre": "נזהרנו מאוד שלא לכלול מידע שלך בשגיאה. אם המידע איננו אישי, בבקשה עקוב אחר ", "openIssueMessage_pre": "נזהרנו מאוד שלא לכלול מידע מהקנבאס שלך בשגיאה. אם המידע בקנבאס אינו אישי, שקול לבצע מעקב אחר הטיפול שלנו ",
"openIssueMessage_button": "מעקב באגים.", "openIssueMessage_button": "מעקב באגים.",
"openIssueMessage_post": " בבקשה כלול את המידע למטה באמצעות העתקה והדבקה בנושא ב GitHub.", "openIssueMessage_post": " בבקשה כלול את המידע מטה באמצעות העתקתה שלו, והדבקה שלו ב- GitHub Issue.",
"sceneContent": "תוכן הלוח:" "sceneContent": "תוכן הקנבאס:"
}, },
"roomDialog": { "roomDialog": {
"desc_intro": "אתה יכול להזמין אנשים ללוח הנוכחי שלך בכדי לשתף פעולה.", "desc_intro": "אתה יכול להזמין אנשים לקנבאס הנוכחי שלך לעבודה משותפת.",
"desc_privacy": "אל דאגה, השיתוף מוצפן מקצה לקצה, כך שכל מה שתצייר ישאר פרטי. אפילו השרתים שלנו לא יוכלו לראות את מה שאתה ממציא.", "desc_privacy": "אל דאגה, השיתוף מוצפן מקצה לקצה, כך שכל מה שתצייר ישאר פרטי. אפילו השרתים שלנו לא יוכלו לראות את מה שאתה ממציא.",
"button_startSession": "התחל שיתוף", "button_startSession": "התחל שיתוף",
"button_stopSession": "הפסק שיתוף", "button_stopSession": "הפסק שיתוף",
"desc_inProgressIntro": "שיתוף חי כרגע בפעולה.", "desc_inProgressIntro": "שיתוף חי פעיל כרגע.",
"desc_shareLink": "שתף את הקישור עם כל מי שאתה מעוניין לעבוד אתו:", "desc_shareLink": "שתף את הקישור עם כל מי שאתה מעוניין לעבוד אתו:",
"desc_exitSession": "עצירת השיתוף תנתק אותך מהחדר, אבל עדיין תוכל להמשיך לעבוד על הלוח, מקומית. שים לב שזה לא ישפיע על אנשים אחרים, והם עדיין יוכלו לשתף פעולה עם הגירסה שלהם.", "desc_exitSession": "עצירת השיתוף תנתק אותך מהחדר, אבל עדיין תוכל להמשיך לעבוד על הקנבאס, מקומית. שים לב שזה לא ישפיע על אנשים אחרים, והם עדיין יוכלו לבצע שיתוף עם הגרסה שלהם.",
"shareTitle": "הצטרף לסשן שיתוף בזמן אמת של Excalidraw" "shareTitle": "הצטרף לשיתוף לעבודה משותפת חיה, בזמן אמת, על גבי Excalidraw"
}, },
"errorDialog": { "errorDialog": {
"title": "שגיאה" "title": "שגיאה"
}, },
"exportDialog": { "exportDialog": {
"disk_title": "שמור לכונן", "disk_title": "שמור לכונן",
"disk_details": "ייצוא מידע הסצינה לקובץ אותו ניתן יהיה לייבא בהמשך.", "disk_details": "ייצא מידע של הקנבאס לקובץ שתוכל לייבא אחר כך.",
"disk_button": "שמירה לקובץ", "disk_button": "שמירה לקובץ",
"link_title": "העתקת קישור לשיתוף", "link_title": "קבל קישור לשיתוף",
"link_details": "ייצוא כקישור לקריאה בלבד.", "link_details": "ייצוא כקישור לקריאה בלבד.",
"link_button": "ייצוא כקישור", "link_button": "ייצוא לקישור",
"excalidrawplus_description": "שמור את המפה לסביבת העבודה שלך ב-Excalidraw+.", "excalidrawplus_description": "שמור את הקנבאס לסביבת העבודה שלך ב- +Excalidraw.",
"excalidrawplus_button": "ייצוא", "excalidrawplus_button": "ייצוא",
"excalidrawplus_exportError": "הייצוא ל-Excalidraw+ לא הצליח לעת עתה..." "excalidrawplus_exportError": "לא הצלחתי לייצא ל- +Excalidraw כרגע..."
}, },
"helpDialog": { "helpDialog": {
"blog": "קרא את הבלוג שלנו", "blog": "קרא את הבלוג שלנו",
@@ -301,30 +317,31 @@
"curvedLine": "קו מעוגל", "curvedLine": "קו מעוגל",
"documentation": "תיעוד", "documentation": "תיעוד",
"doubleClick": "לחיצה כפולה", "doubleClick": "לחיצה כפולה",
"drag": "לגרור", "drag": "גרור",
"editor": "עורך", "editor": "עורך",
"editSelectedShape": "ערוך את הצורה הנבחרת (טקסט/חץ/קו)", "editLineArrowPoints": "",
"editText": "",
"github": "מצאת בעיה? דווח", "github": "מצאת בעיה? דווח",
"howto": "עקוב אחר המדריכים שלנו", "howto": "עקוב אחר המדריכים שלנו",
"or": "או", "or": "או",
"preventBinding": "למנוע נעיצת חיצים", "preventBinding": "למנוע נעיצת חיצים",
"tools": "כלים", "tools": "כלים",
"shortcuts": "קיצורי מקלדת", "shortcuts": "קיצורי מקלדת",
"textFinish": "סיים עריכה (טקסט)", "textFinish": "סיים עריכה (עורך טקסט)",
"textNewLine": "הוסף שורה חדשה (טקסט)", "textNewLine": "הוסף שורה חדשה (עורך טקסט)",
"title": "עזרה", "title": "עזרה",
"view": "תצוגה", "view": "תצוגה",
"zoomToFit": "גלילה להצגת כל האלמנטים במסך", "zoomToFit": "זום להתאמת כל האלמנטים למסך",
"zoomToSelection": "התמקד בבחירה", "zoomToSelection": "התמקד בבחירה",
"toggleElementLock": "נעילה/ביטול הנעילה של הרכיבים הנבחרים", "toggleElementLock": "נעילה/ביטול הנעילה של הרכיבים הנבחרים",
"movePageUpDown": "", "movePageUpDown": "זוז עמוד למעלה/למטה",
"movePageLeftRight": "" "movePageLeftRight": "זוז עמוד שמאלה/ימינה"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "ניקוי הקנבס" "title": "ניקוי הקנבס"
}, },
"publishDialog": { "publishDialog": {
"title": "פרסום ספריה", "title": "פרסם ספריה",
"itemName": "שם הפריט", "itemName": "שם הפריט",
"authorName": "שם היוצר", "authorName": "שם היוצר",
"githubUsername": "שם המשתמש שלך ב-GitHub", "githubUsername": "שם המשתמש שלך ב-GitHub",
@@ -333,11 +350,11 @@
"libraryDesc": "תיאור הספריה", "libraryDesc": "תיאור הספריה",
"website": "אתר", "website": "אתר",
"placeholder": { "placeholder": {
"authorName": ם או שם משתמש", "authorName": מך או שם המשתמש שלך",
"libraryName": "תנו שם לספריה", "libraryName": "תנו שם לספריה",
"libraryDesc": "תיאור של הספריה שלך כדי לסייע למשתמשים להבין את השימוש בה", "libraryDesc": "תיאור של הספריה שלך כדי לסייע למשתמשים להבין את השימוש בה",
"githubHandle": "", "githubHandle": "כינוי GitHub (לא חובה), כדי שתוכל לערוך את הספרית לאחר שנשלחה לבדיקה",
"twitterHandle": "", "twitterHandle": "שם משתמש טוויטר (לא חובה), כדי שנדע למי לתת קרדיט כשאנחנו מפרסמים בטוויטר",
"website": "קישור לאתר הפרטי שלך או לכל מקום אחר (אופציונאלי)" "website": "קישור לאתר הפרטי שלך או לכל מקום אחר (אופציונאלי)"
}, },
"errors": { "errors": {
@@ -345,44 +362,44 @@
"website": "הזינו כתובת URL תקינה" "website": "הזינו כתובת URL תקינה"
}, },
"noteDescription": { "noteDescription": {
"pre": "להציע את הספריה שלך להיות כלולה ב", "pre": "הגש את הספריה שלך להכללתה ב ",
"link": "מאגר הספריה הציבורי", "link": "מאגר הספריה הציבורית",
"post": "כך שאחרים יוכלו לעשות שימוש בציורים שלהם." "post": "לשימושם של אנשים אחרים בציורים שלהם."
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "הספריה צריכה לקבל אישור ידני. אנא קרא את ", "pre": "הספריה צריכה לקבל אישור ידני קודם לכן. אנא קרא את ",
"link": "הנחיות", "link": "הנחיות",
"post": "" "post": " לפני השליחה. אתה תצטרך לחשבון GitHub כדי לתקשר ולבצע שינויים אם תתבקש, אבל זה לא דרישה הכרחית."
}, },
"noteLicense": { "noteLicense": {
"pre": "", "pre": "על ידי שליחה, אתה מסכים שהסיפריה תפורסם תחת ה- ",
"link": "רישיון MIT, ", "link": "רישיון MIT, ",
"post": "שאומר בקצרה שכל אחד יכול לעשות בהם שימוש ללא מגבלות." "post": "שאומר בקצרה, שכל אחד יכול לעשות בהם שימוש ללא מגבלות."
}, },
"noteItems": "", "noteItems": "לכל פריט בסיפריה חייב להיות שם כדי שאפשר יהיה לסנן. הפריטי סיפריה הבאים יהיו כלולים:",
"atleastOneLibItem": "", "atleastOneLibItem": "אנא בחר לפחות פריט אחד מספריה כדי להתחיל",
"republishWarning": "" "republishWarning": "הערה: חלק מהפריטים שבחרת מסומנים ככאלו שכבר פורסמו/נשלחו. אתה צריך לשלוח פריטים מחדש כאשר אתה מעדכן ספריה או הגשה קיימים."
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "הספריה נשלחה", "title": "הספריה הוגשה",
"content": "תודה {{authorName}}. הספריה שלך נשלחה לבחינה. תוכל לעקוב אחרי סטטוס הפרסום", "content": "תודה {{authorName}}. הספריה שלך נשלחה לבחינה. תוכל לעקוב אחרי סטטוס הפרסום",
"link": "כאן" "link": "כאן"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "איפוס ספריה", "resetLibrary": "איפוס ספריה",
"removeItemsFromLib": "להסיר את הפריטים הנבחרים מהספריה" "removeItemsFromLib": "הסר את הפריטים הנבחרים מהספריה"
}, },
"encrypted": { "encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.", "tooltip": ציורים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
"link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw" "link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw"
}, },
"stats": { "stats": {
"angle": "זווית", "angle": "זווית",
"element": "אלמנט", "element": "רכיב",
"elements": "אלמנטים", "elements": "רכיבים",
"height": "גובה", "height": "גובה",
"scene": "תצוגה", "scene": "תצוגה",
"selected": "נבחר/ים", "selected": "נבחר",
"storage": "אחסון", "storage": "אחסון",
"title": "סטטיסטיקות לחנונים", "title": "סטטיסטיקות לחנונים",
"total": "סה״כ", "total": "סה״כ",
@@ -393,14 +410,14 @@
}, },
"toast": { "toast": {
"addedToLibrary": "נוסף לספריה", "addedToLibrary": "נוסף לספריה",
"copyStyles": "העתק סגנונות.", "copyStyles": "סגנונות הועתקו.",
"copyToClipboard": "הועתק אל הלוח.", "copyToClipboard": "הועתק ללוח.",
"copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})",
"fileSaved": "קובץ נשמר.", "fileSaved": "קובץ נשמר.",
"fileSavedToFilename": "נשמר לקובץ {filename}", "fileSavedToFilename": "נשמר לקובץ {filename}",
"canvas": "משטח ציור", "canvas": "קנבאס",
"selection": "בחירה", "selection": "בחירה",
"pasteAsSingleElement": "" "pasteAsSingleElement": "השתמש ב- {{shortcut}} כדי להדביק כפריט יחיד,\nאו הדבק לתוך עורך טקסט קיים"
}, },
"colors": { "colors": {
"ffffff": "לבן", "ffffff": "לבן",
@@ -443,23 +460,23 @@
"364fc7": "כחול כהה 9", "364fc7": "כחול כהה 9",
"1864ab": "כחול 9", "1864ab": "כחול 9",
"0b7285": "טורקיז 9", "0b7285": "טורקיז 9",
"087f5b": "ירקרק 9", "087f5b": "ירוק-כחול 9",
"2b8a3e": "ירוק 9", "2b8a3e": "ירוק 9",
"5c940d": "ליים 9", "5c940d": "ליים 9",
"e67700": "ירוק 9", "e67700": "צהוב 9",
"d9480f": "כתום 9" "d9480f": "כתום 9"
}, },
"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": "קיצורים & עזרה"
} }
} }
} }
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ", "increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ",
"unbindText": "", "unbindText": "",
"bindText": "लेखन को कोश से जोड़े", "bindText": "लेखन को कोश से जोड़े",
"createContainerFromText": "मूलपाठ कंटेनर में मोड के दिखाए",
"link": { "link": {
"edit": "", "edit": "",
"create": "", "create": "",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "कॉलेब सर्वर से कनेक्शन नहीं हो पा रहा. कृपया पृष्ठ को पुनः लाने का प्रयास करे.", "cannotResolveCollabServer": "कॉलेब सर्वर से कनेक्शन नहीं हो पा रहा. कृपया पृष्ठ को पुनः लाने का प्रयास करे.",
"importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका", "importLibraryError": "संग्रह प्रतिष्ठापित नहीं किया जा सका",
"collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।", "collabSaveFailed": "किसी कारण वश अंदरूनी डेटाबेस में सहेजा नहीं जा सका। यदि समस्या बनी रहती है, तो किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
"collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।" "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।",
"brave_measure_text_error": {
"start": "ऐसा लगता है कि आप ब्रेव ब्राउज़र का उपयोग कर रहे है, उसके साथ",
"aggressive_block_fingerprint": "उग्रतापूर्वक उंगलियों के निशान रोकने की",
"setting_enabled": "सेटिंग सक्रिय की गई है।",
"break": "इसके परिणाम स्वरूम टूट सकता है यह",
"text_elements": "मूल पाठ अवयव टूट सकते हैं",
"in_your_drawings": "आपके चित्र में उपस्थित",
"strongly_recommend": "हमारा शशक्त अनुरोध इस सेटिंग्स को अक्षम करने का है. आप",
"steps": "इन दिए हुए स्टेप्स को अनुकरीत करके देख सकते है कि",
"how": "ये कैसे करे",
"disable_setting": " यदि इन सेटिंग्स को निष्क्रिय करने पर भी, पाठ्य दिखना ठीक न हो तो",
"issue": "समस्या",
"write": "हमारे \"GitHub\" पर, अथवा",
"discord": "\"Discord\" पर लिख सकते हैं."
}
}, },
"toolBar": { "toolBar": {
"selection": "चयन", "selection": "चयन",
@@ -303,7 +319,8 @@
"doubleClick": "", "doubleClick": "",
"drag": "खींचें", "drag": "खींचें",
"editor": "संपादक", "editor": "संपादक",
"editSelectedShape": "", "editLineArrowPoints": "रेखा/तीर बिंदु सम्पादित करे",
"editText": "पाठ्य सम्पादित करे/ लेबल जोड़े",
"github": "मुद्दा मिला? प्रस्तुत करें", "github": "मुद्दा मिला? प्रस्तुत करें",
"howto": "हमारे गाइड का पालन करें", "howto": "हमारे गाइड का पालन करें",
"or": "या", "or": "या",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Betűméret növelése", "increaseFontSize": "Betűméret növelése",
"unbindText": "Szövegkötés feloldása", "unbindText": "Szövegkötés feloldása",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "Hivatkozás szerkesztése", "edit": "Hivatkozás szerkesztése",
"create": "Hivatkozás létrehozása", "create": "Hivatkozás létrehozása",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Kijelölés", "selection": "Kijelölés",
@@ -303,7 +319,8 @@
"doubleClick": "dupla kattintás", "doubleClick": "dupla kattintás",
"drag": "vonszolás", "drag": "vonszolás",
"editor": "Szerkesztő", "editor": "Szerkesztő",
"editSelectedShape": "Kijelölt alakzat szerkesztése (szöveg/nyíl/vonal)", "editLineArrowPoints": "",
"editText": "",
"github": "Hibát találtál? Küld be", "github": "Hibát találtál? Küld be",
"howto": "Kövesd az útmutatóinkat", "howto": "Kövesd az útmutatóinkat",
"or": "vagy", "or": "vagy",
+20 -3
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Besarkan ukuran font", "increaseFontSize": "Besarkan ukuran font",
"unbindText": "Lepas teks", "unbindText": "Lepas teks",
"bindText": "Kunci teks ke kontainer", "bindText": "Kunci teks ke kontainer",
"createContainerFromText": "",
"link": { "link": {
"edit": "Edit tautan", "edit": "Edit tautan",
"create": "Buat tautan", "create": "Buat tautan",
@@ -193,7 +194,7 @@
"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": "" "collabOfflineWarning": "Tidak ada koneksi internet.\nPerubahan tidak akan disimpan!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Tipe file tidak didukung.", "unsupportedFileType": "Tipe file tidak didukung.",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.", "cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi.",
"importLibraryError": "Tidak dapat memuat pustaka", "importLibraryError": "Tidak dapat memuat pustaka",
"collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.", "collabSaveFailed": "Tidak dapat menyimpan ke dalam basis data server. Jika masih berlanjut, Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.",
"collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang." "collabSaveFailed_sizeExceeded": "Tidak dapat menyimpan ke dalam basis data server, tampaknya ukuran kanvas terlalu besar. Anda sebaiknya simpan berkas Anda secara lokal untuk memastikan pekerjaan Anda tidak hilang.",
"brave_measure_text_error": {
"start": "Sepertinya kamu menggunakan browser Brave dengan",
"aggressive_block_fingerprint": "",
"setting_enabled": "pengaturan diaktifkan",
"break": "",
"text_elements": "Elemen Teks",
"in_your_drawings": "dalam gambar anda",
"strongly_recommend": "Kami sangat menyarankan untuk mematikan pengaturan ini. Kamu dapat mengikuti",
"steps": "langkah-langkah ini",
"how": "dalam bagaimana melakukan itu",
"disable_setting": " Jika pengaturan ini dimatikan tidak mengatasi masalah tampilan dari elemen teks, silahkan buka",
"issue": "isu",
"write": "di GitHub kami, atau tulis kami di",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Pilihan", "selection": "Pilihan",
@@ -303,7 +319,8 @@
"doubleClick": "klik-ganda", "doubleClick": "klik-ganda",
"drag": "seret", "drag": "seret",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Edit bentuk yang dipilih (teks/panah/garis)", "editLineArrowPoints": "",
"editText": "",
"github": "Menemukan masalah? Kirimkan", "github": "Menemukan masalah? Kirimkan",
"howto": "Ikuti panduan kami", "howto": "Ikuti panduan kami",
"or": "atau", "or": "atau",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Aumenta la dimensione dei caratteri", "increaseFontSize": "Aumenta la dimensione dei caratteri",
"unbindText": "Scollega testo", "unbindText": "Scollega testo",
"bindText": "Associa il testo al container", "bindText": "Associa il testo al container",
"createContainerFromText": "",
"link": { "link": {
"edit": "Modifica link", "edit": "Modifica link",
"create": "Crea link", "create": "Crea link",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.", "cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova.",
"importLibraryError": "Impossibile caricare la libreria", "importLibraryError": "Impossibile caricare la libreria",
"collabSaveFailed": "Impossibile salvare nel database di backend. Se i problemi persistono, dovresti salvare il tuo file localmente per assicurarti di non perdere il tuo lavoro.", "collabSaveFailed": "Impossibile salvare nel database di backend. Se i problemi persistono, dovresti salvare il tuo file localmente per assicurarti di non perdere il tuo lavoro.",
"collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro." "collabSaveFailed_sizeExceeded": "Impossibile salvare nel database di backend, la tela sembra essere troppo grande. Dovresti salvare il file localmente per assicurarti di non perdere il tuo lavoro.",
"brave_measure_text_error": {
"start": "Sembra che tu stia usando il browser Brave con il",
"aggressive_block_fingerprint": "Blocco Aggressivamente Impronte Digitali",
"setting_enabled": "impostazioni abilitate",
"break": "Questo potrebbe portare a rompere gli",
"text_elements": "Elementi Di Testo",
"in_your_drawings": "nei tuoi disegni",
"strongly_recommend": "Si consiglia vivamente di disabilitare questa impostazione. È possibile seguire",
"steps": "questi passaggi",
"how": "su come fare",
"disable_setting": " Se la disabilitazione di questa impostazione non corregge la visualizzazione degli elementi di testo, apri un",
"issue": "problema",
"write": "sul nostro GitHub, o scrivici su",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Selezione", "selection": "Selezione",
@@ -303,7 +319,8 @@
"doubleClick": "doppio-click", "doubleClick": "doppio-click",
"drag": "trascina", "drag": "trascina",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Modifica la forma selezionata (testo/freccia/linea)", "editLineArrowPoints": "",
"editText": "Modifica testo / aggiungi etichetta",
"github": "Trovato un problema? Segnalalo", "github": "Trovato un problema? Segnalalo",
"howto": "Segui le nostre guide", "howto": "Segui le nostre guide",
"or": "oppure", "or": "oppure",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "フォントサイズを拡大", "increaseFontSize": "フォントサイズを拡大",
"unbindText": "テキストのバインド解除", "unbindText": "テキストのバインド解除",
"bindText": "テキストをコンテナにバインド", "bindText": "テキストをコンテナにバインド",
"createContainerFromText": "",
"link": { "link": {
"edit": "リンクを編集", "edit": "リンクを編集",
"create": "リンクを作成", "create": "リンクを作成",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。", "cannotResolveCollabServer": "コラボレーションサーバに接続できませんでした。ページを再読み込みして、もう一度お試しください。",
"importLibraryError": "ライブラリを読み込めませんでした。", "importLibraryError": "ライブラリを読み込めませんでした。",
"collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。", "collabSaveFailed": "バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。" "collabSaveFailed_sizeExceeded": "キャンバスが大きすぎるため、バックエンドデータベースに保存できませんでした。問題が解決しない場合は、作業を失わないようにローカルにファイルを保存してください。",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "設定が有効化されました",
"break": "",
"text_elements": "テキスト要素",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "選択", "selection": "選択",
@@ -303,7 +319,8 @@
"doubleClick": "ダブルクリック", "doubleClick": "ダブルクリック",
"drag": "ドラッグ", "drag": "ドラッグ",
"editor": "エディタ", "editor": "エディタ",
"editSelectedShape": "選択した図形の編集 (テキスト/矢印/線)", "editLineArrowPoints": "",
"editText": "テキストの編集 / ラベルの追加",
"github": "不具合報告はこちら", "github": "不具合報告はこちら",
"howto": "ヘルプ・マニュアル", "howto": "ヘルプ・マニュアル",
"or": "または", "or": "または",
+25 -8
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Sali tiddi n tsefsit", "increaseFontSize": "Sali tiddi n tsefsit",
"unbindText": "Serreḥ iweḍris", "unbindText": "Serreḥ iweḍris",
"bindText": "Arez aḍris s anagbar", "bindText": "Arez aḍris s anagbar",
"createContainerFromText": "",
"link": { "link": {
"edit": "Ẓreg aseɣwen", "edit": "Ẓreg aseɣwen",
"create": "Snulfu-d aseɣwen", "create": "Snulfu-d aseɣwen",
@@ -193,7 +194,7 @@
"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": "" "collabOfflineWarning": "Ulac tuqqna n internet.\nIbedilen-ik ur ttwaklasen ara!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.", "unsupportedFileType": "Anaw n ufaylu ur yettwasefrak ara.",
@@ -204,7 +205,22 @@
"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": "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": "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": "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." "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.",
"brave_measure_text_error": {
"start": "Ittban-d am wakken tsseqdaceḍ iminig Brave akked",
"aggressive_block_fingerprint": "",
"setting_enabled": "yermed",
"break": "Ayagi yezmer ad d-iseglu s truẓi n",
"text_elements": "Iferdisen iḍrisen",
"in_your_drawings": "deg wunuɣen-inek",
"strongly_recommend": "",
"steps": "isurifen-agi",
"how": "",
"disable_setting": "",
"issue": "",
"write": "di GitHub inek neɣ aru-yaɣ-d di",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Tafrayt", "selection": "Tafrayt",
@@ -229,7 +245,7 @@
"shapes": "Talɣiwin" "shapes": "Talɣiwin"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "Akken ad tesmuttiḍ taɣzut n usuneɣ, ṭṭef ṛṛuda n umumed, neɣ seqdec afecku Afus",
"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",
@@ -240,7 +256,7 @@
"resize": "Tzemreḍ ad tḥettemeḍ assaɣ s tuṭṭfa n tqeffalt SHIFT mi ara tettbeddileḍ tiddi,\nma teṭṭfeḍ ALT abeddel n tiddi ad yili si tlemmast", "resize": "Tzemreḍ ad tḥettemeḍ assaɣ s tuṭṭfa n tqeffalt SHIFT mi ara tettbeddileḍ tiddi,\nma teṭṭfeḍ ALT abeddel n tiddi ad yili si tlemmast",
"resizeImage": "Tzemreḍ ad talseḍ tiddi s tilelli s tuṭṭfa n SHIFT,\nṭṭef ALT akken ad talseḍ tiddi si tlemmast", "resizeImage": "Tzemreḍ ad talseḍ tiddi s tilelli s tuṭṭfa n SHIFT,\nṭṭef ALT akken ad talseḍ tiddi si tlemmast",
"rotate": "Tzemreḍ ad tḥettemeḍ tiɣemmar s tuṭṭfa n SHIFT di tuzzya", "rotate": "Tzemreḍ ad tḥettemeḍ tiɣemmar s tuṭṭfa n SHIFT di tuzzya",
"lineEditor_info": "", "lineEditor_info": "Ssed ɣef CtrlOrCmd yerna ssit snat n tikkal neɣ ssed ɣef CtrlOrCmd + Kcem akken ad tẓergeḍ tineqqiḍin",
"lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍ (tinqiḍin),\nCtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ", "lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍ (tinqiḍin),\nCtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ",
"lineEditor_nothingSelected": "Fren tanqiḍt akken ad tẓergeḍ (ṭṭef SHIFT akken ad tferneḍ aṭas),\nneɣ ṭṭef Alt akken ad ternuḍ tinqiḍin timaynutin", "lineEditor_nothingSelected": "Fren tanqiḍt akken ad tẓergeḍ (ṭṭef SHIFT akken ad tferneḍ aṭas),\nneɣ ṭṭef Alt akken ad ternuḍ tinqiḍin timaynutin",
"placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus", "placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus",
@@ -303,7 +319,8 @@
"doubleClick": "ssit snat n tikkal", "doubleClick": "ssit snat n tikkal",
"drag": "zuɣer", "drag": "zuɣer",
"editor": "Amaẓrag", "editor": "Amaẓrag",
"editSelectedShape": "Ẓreg talɣa yettwafernen (aḍris/taneccabt/izirig)", "editLineArrowPoints": "",
"editText": "",
"github": "Tufiḍ-d ugur? Azen-aɣ-d", "github": "Tufiḍ-d ugur? Azen-aɣ-d",
"howto": "Ḍfer imniren-nneɣ", "howto": "Ḍfer imniren-nneɣ",
"or": "neɣ", "or": "neɣ",
@@ -452,11 +469,11 @@
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "", "center_heading": "",
"center_heading_plus": "", "center_heading_plus": "Tebɣiḍ ad tedduḍ ɣer Excalidraw+ deg umḍiq?",
"menuHint": "" "menuHint": "Asifeḍ, ismenyifen, tutlayin, ..."
}, },
"defaults": { "defaults": {
"menuHint": "", "menuHint": "Asifeḍ, ismenyifen, d wayen-nniḍen...",
"center_heading": "", "center_heading": "",
"toolbarHint": "", "toolbarHint": "",
"helpHint": "" "helpHint": ""
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "", "increaseFontSize": "",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "", "edit": "",
"create": "", "create": "",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "", "selection": "",
@@ -303,7 +319,8 @@
"doubleClick": "қос шерту", "doubleClick": "қос шерту",
"drag": "апару", "drag": "апару",
"editor": "Өңдеу", "editor": "Өңдеу",
"editSelectedShape": "Таңдалған пішінді өңдеу (мәтін/нұсқар/сызық)", "editLineArrowPoints": "",
"editText": "",
"github": "Қате таптыңыз ба? Жолдаңыз", "github": "Қате таптыңыз ба? Жолдаңыз",
"howto": "Біздің нұсқаулықтарды орындаңыз", "howto": "Біздің нұсқаулықтарды орындаңыз",
"or": "немесе", "or": "немесе",
+27 -10
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "폰트 사이즈 키우기", "increaseFontSize": "폰트 사이즈 키우기",
"unbindText": "텍스트 분리", "unbindText": "텍스트 분리",
"bindText": "텍스트를 컨테이너에 결합", "bindText": "텍스트를 컨테이너에 결합",
"createContainerFromText": "텍스트를 컨테이너에 담기",
"link": { "link": {
"edit": "링크 수정하기", "edit": "링크 수정하기",
"create": "링크 만들기", "create": "링크 만들기",
@@ -193,7 +194,7 @@
"resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?", "resetLibrary": "당신의 라이브러리를 초기화 합니다. 계속하시겠습니까?",
"removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?", "removeItemsFromsLibrary": "{{count}}개의 아이템을 라이브러리에서 삭제하시겠습니까?",
"invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.", "invalidEncryptionKey": "암호화 키는 반드시 22글자여야 합니다. 실시간 협업이 비활성화됩니다.",
"collabOfflineWarning": "" "collabOfflineWarning": "인터넷에 연결되어 있지 않습니다.\n변경 사항들이 저장되지 않습니다!"
}, },
"errors": { "errors": {
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.", "unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "협업 서버에 접속하는데 실패했습니다. 페이지를 새로고침하고 다시 시도해보세요.", "cannotResolveCollabServer": "협업 서버에 접속하는데 실패했습니다. 페이지를 새로고침하고 다시 시도해보세요.",
"importLibraryError": "라이브러리를 불러오지 못했습니다.", "importLibraryError": "라이브러리를 불러오지 못했습니다.",
"collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.", "collabSaveFailed": "데이터베이스에 저장하지 못했습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.",
"collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요." "collabSaveFailed_sizeExceeded": "데이터베이스에 저장하지 못했습니다. 캔버스가 너무 큰 거 같습니다. 문제가 계속 된다면, 작업 내용을 잃지 않도록 로컬 저장소에 저장해 주세요.",
"brave_measure_text_error": {
"start": "귀하께서는",
"aggressive_block_fingerprint": "강력한 지문 차단",
"setting_enabled": "설정이 활성화된 Brave browser를 사용하고 계신 것 같습니다",
"break": "이 기능으로 인해 화이트보드의",
"text_elements": "텍스트 요소들이",
"in_your_drawings": "손상될 수 있습니다",
"strongly_recommend": "해당 기능을 설정에서 비활성화하는 것을 강력히 권장 드립니다. 비활성화 방법에 대해서는",
"steps": "이 게시글을",
"how": "참고해주세요.",
"disable_setting": " 만약 이 설정을 껐음에도 텍스트 요소들이 올바르게 표시되지 않는다면, 저희",
"issue": "Github에 이슈를",
"write": "올려주시거나",
"discord": "Discord에 제보해주세요"
}
}, },
"toolBar": { "toolBar": {
"selection": "선택", "selection": "선택",
@@ -221,7 +237,7 @@
"penMode": "펜 모드 - 터치 방지", "penMode": "펜 모드 - 터치 방지",
"link": "선택한 도형에 대해서 링크를 추가/업데이트", "link": "선택한 도형에 대해서 링크를 추가/업데이트",
"eraser": "지우개", "eraser": "지우개",
"hand": "" "hand": "손 (패닝 도구)"
}, },
"headings": { "headings": {
"canvasActions": "캔버스 동작", "canvasActions": "캔버스 동작",
@@ -229,7 +245,7 @@
"shapes": "모양" "shapes": "모양"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "캔버스를 옮기려면 마우스 휠이나 스페이스바를 누르고 드래그하거나, 손 도구를 사용하기",
"linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.", "linearElement": "여러 점을 연결하려면 클릭하고, 직선을 그리려면 바로 드래그하세요.",
"freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.", "freeDraw": "클릭 후 드래그하세요. 완료되면 놓으세요.",
"text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.", "text": "팁: 선택 툴로 아무 곳이나 더블 클릭해 텍스트를 추가할 수도 있습니다.",
@@ -248,7 +264,7 @@
"bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기", "bindTextToElement": "Enter 키를 눌러서 텍스트 추가하기",
"deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기", "deepBoxSelect": "CtrlOrCmd 키를 눌러서 깊게 선택하고, 드래그하지 않도록 하기",
"eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기", "eraserRevert": "Alt를 눌러서 삭제하도록 지정된 요소를 되돌리기",
"firefox_clipboard_write": "" "firefox_clipboard_write": "이 기능은 설정에서 \"dom.events.asyncClipboard.clipboardItem\" 플래그를 \"true\"로 설정하여 활성화할 수 있습니다. Firefox에서 브라우저 플래그를 수정하려면, \"about:config\" 페이지에 접속하세요."
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "미리보기를 볼 수 없습니다", "cannotShowPreview": "미리보기를 볼 수 없습니다",
@@ -303,7 +319,8 @@
"doubleClick": "더블 클릭", "doubleClick": "더블 클릭",
"drag": "드래그", "drag": "드래그",
"editor": "에디터", "editor": "에디터",
"editSelectedShape": "선택한 도형 편집하기(텍스트/화살표/라인)", "editLineArrowPoints": "",
"editText": "",
"github": "문제 제보하기", "github": "문제 제보하기",
"howto": "가이드 참고하기", "howto": "가이드 참고하기",
"or": "또는", "or": "또는",
@@ -451,14 +468,14 @@
}, },
"welcomeScreen": { "welcomeScreen": {
"app": { "app": {
"center_heading": "당신의 모든 데이터는 브라우저에 저장되었습니다.", "center_heading": "모든 데이터는 브라우저에 안전하게 저장됩니다.",
"center_heading_plus": "대신 Excalidraw+로 이동하시겠습니까?", "center_heading_plus": "대신 Excalidraw+로 이동하시겠습니까?",
"menuHint": "내보내기, 설정, 언어, ..." "menuHint": "내보내기, 설정, 언어, ..."
}, },
"defaults": { "defaults": {
"menuHint": "내보내기, 설정, 더 보기...", "menuHint": "내보내기, 설정, 등등...",
"center_heading": "", "center_heading": "간단하게 만드는 다이어그램.",
"toolbarHint": "도구 선택 & 그리기 시작", "toolbarHint": "도구 선택하고, 그리세요!",
"helpHint": "단축키 & 도움말" "helpHint": "단축키 & 도움말"
} }
} }
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "زایدکردنی قەبارەی فۆنت", "increaseFontSize": "زایدکردنی قەبارەی فۆنت",
"unbindText": "دەقەکە جیابکەرەوە", "unbindText": "دەقەکە جیابکەرەوە",
"bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە", "bindText": "دەقەکە ببەستەوە بە کۆنتەینەرەکەوە",
"createContainerFromText": "",
"link": { "link": {
"edit": "دەستکاریکردنی بەستەر", "edit": "دەستکاریکردنی بەستەر",
"create": "دروستکردنی بەستەر", "create": "دروستکردنی بەستەر",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.", "cannotResolveCollabServer": "ناتوانێت پەیوەندی بکات بە سێرڤەری کۆلاب. تکایە لاپەڕەکە دووبارە باربکەوە و دووبارە هەوڵ بدەوە.",
"importLibraryError": "نەیتوانی کتێبخانە بار بکات", "importLibraryError": "نەیتوانی کتێبخانە بار بکات",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "دەستنیشانکردن", "selection": "دەستنیشانکردن",
@@ -303,7 +319,8 @@
"doubleClick": "دوو گرتە", "doubleClick": "دوو گرتە",
"drag": "راکێشان", "drag": "راکێشان",
"editor": "دەستکاریکەر", "editor": "دەستکاریکەر",
"editSelectedShape": "دەستکاریکردنی شێوەی هەڵبژێردراو (دەق/تیر/هێڵ)", "editLineArrowPoints": "",
"editText": "",
"github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن", "github": "کێشەیەکت دۆزیەوە؟ پێشکەشکردن",
"howto": "شوێن ڕینماییەکانمان بکەوە", "howto": "شوێن ڕینماییەکانمان بکەوە",
"or": "یان", "or": "یان",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "", "increaseFontSize": "",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "Redeguoti nuorodą", "edit": "Redeguoti nuorodą",
"create": "Sukurti nuorodą", "create": "Sukurti nuorodą",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Nepavyko prisijungti prie serverio bendradarbiavimui. Perkrauk puslapį ir pabandyk prisijungti dar kartą.", "cannotResolveCollabServer": "Nepavyko prisijungti prie serverio bendradarbiavimui. Perkrauk puslapį ir pabandyk prisijungti dar kartą.",
"importLibraryError": "Nepavyko įkelti bibliotekos", "importLibraryError": "Nepavyko įkelti bibliotekos",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Žymėjimas", "selection": "Žymėjimas",
@@ -303,7 +319,8 @@
"doubleClick": "dvigubas paspaudimas", "doubleClick": "dvigubas paspaudimas",
"drag": "vilkti", "drag": "vilkti",
"editor": "Redaktorius", "editor": "Redaktorius",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "Radai klaidą? Pateik", "github": "Radai klaidą? Pateik",
"howto": "Vadovaukis mūsų gidu", "howto": "Vadovaukis mūsų gidu",
"or": "arba", "or": "arba",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Palielināt fonta izmēru", "increaseFontSize": "Palielināt fonta izmēru",
"unbindText": "Atdalīt tekstu", "unbindText": "Atdalīt tekstu",
"bindText": "Piesaistīt tekstu figūrai", "bindText": "Piesaistīt tekstu figūrai",
"createContainerFromText": "",
"link": { "link": {
"edit": "Rediģēt saiti", "edit": "Rediģēt saiti",
"create": "Izveidot saiti", "create": "Izveidot saiti",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Nevarēja savienoties ar sadarbošanās serveri. Lūdzu, pārlādējiet lapu un mēģiniet vēlreiz.", "cannotResolveCollabServer": "Nevarēja savienoties ar sadarbošanās serveri. Lūdzu, pārlādējiet lapu un mēģiniet vēlreiz.",
"importLibraryError": "Nevarēja ielādēt bibliotēku", "importLibraryError": "Nevarēja ielādēt bibliotēku",
"collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.", "collabSaveFailed": "Darbs nav saglabāts datubāzē. Ja problēma turpinās, saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu." "collabSaveFailed_sizeExceeded": "Darbs nav saglabāts datubāzē, šķiet, ka tāfele ir pārāk liela. Saglabājiet datni lokālajā krātuvē, lai nodrošinātos pret darba pazaudēšanu.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Atlase", "selection": "Atlase",
@@ -303,7 +319,8 @@
"doubleClick": "dubultklikšķis", "doubleClick": "dubultklikšķis",
"drag": "vilkt", "drag": "vilkt",
"editor": "Redaktors", "editor": "Redaktors",
"editSelectedShape": "Rediģēt atlasīto figūru (tekstu/bultu/līniju)", "editLineArrowPoints": "",
"editText": "",
"github": "Sastapāt kļūdu? Ziņot", "github": "Sastapāt kļūdu? Ziņot",
"howto": "Sekojiet mūsu instrukcijām", "howto": "Sekojiet mūsu instrukcijām",
"or": "vai", "or": "vai",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "अक्षर आकार मोठा करा", "increaseFontSize": "अक्षर आकार मोठा करा",
"unbindText": "लेखन संबंध संपवा", "unbindText": "लेखन संबंध संपवा",
"bindText": "शब्द समूह ला पात्रात घ्या", "bindText": "शब्द समूह ला पात्रात घ्या",
"createContainerFromText": "मजकूर कंटेनर मधे मोडून दाखवा",
"link": { "link": {
"edit": "दुवा संपादन", "edit": "दुवा संपादन",
"create": "दुवा तयार करा", "create": "दुवा तयार करा",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "कॉलेब-सर्वर हे पोहोचत नाही आहे. पान परत लोड करायचा प्रयत्न करावे.", "cannotResolveCollabServer": "कॉलेब-सर्वर हे पोहोचत नाही आहे. पान परत लोड करायचा प्रयत्न करावे.",
"importLibraryError": "संग्रह प्रतिस्थापित नाही करता आला", "importLibraryError": "संग्रह प्रतिस्थापित नाही करता आला",
"collabSaveFailed": "काही कारणा निमित्त आतल्या डेटाबेसमध्ये जतन करू शकत नाही। समस्या तशिस राहिल्यास, तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही तुमची फाइल स्थानिक जतन करावी.", "collabSaveFailed": "काही कारणा निमित्त आतल्या डेटाबेसमध्ये जतन करू शकत नाही। समस्या तशिस राहिल्यास, तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही तुमची फाइल स्थानिक जतन करावी.",
"collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।\n\nबॅकएंड डेटाबेसमध्ये जतन करू शकत नाही, कॅनव्हास खूप मोठा असल्याचे दिसते. तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही फाइल स्थानिक पातळीवर जतन करावी." "collabSaveFailed_sizeExceeded": "लगता है कि पृष्ठ तल काफ़ी बड़ा है, इस्कारण अंदरूनी डेटाबेस में सहेजा नहीं जा सका। किये काम को खोने न देने के लिये अपनी फ़ाइल को स्थानीय रूप से सहेजे।\n\nबॅकएंड डेटाबेसमध्ये जतन करू शकत नाही, कॅनव्हास खूप मोठा असल्याचे दिसते. तुम्ही तुमचे काम गमावणार नाही याची खात्री करण्यासाठी तुम्ही फाइल स्थानिक पातळीवर जतन करावी.",
"brave_measure_text_error": {
"start": "असं वाटते की तुम्हीं \"Brave\" \"Browser\" वापरत आहात, त्या बरोबार",
"aggressive_block_fingerprint": "बोटांचे ठसे उग्रतेने थाम्बवाचे",
"setting_enabled": "सेटिंग्स सक्रिय केले आहेत",
"break": "ह्या कारणानिं",
"text_elements": "पाठ अवयव तुटु शकतात",
"in_your_drawings": "तुमच्या चित्रिकराणतले",
"strongly_recommend": "आमचा ज़ोरदार सल्ला असा कि सेटिंग्स निष्क्रिय करावे. तुम्हीं",
"steps": "ह्या स्टेप्स",
"how": "घेउ शकतात",
"disable_setting": " जर सेटिंग्स निष्क्रिय करून पाठ्य दिसणे ठीक नसेल होत तर",
"issue": "मुद्दा",
"write": "आमच्या Github वर, किव्हा आम्हाला",
"discord": "\"Discord\" वर लिहां"
}
}, },
"toolBar": { "toolBar": {
"selection": "निवड", "selection": "निवड",
@@ -303,7 +319,8 @@
"doubleClick": "दुहेरी क्लिक", "doubleClick": "दुहेरी क्लिक",
"drag": "ओढा", "drag": "ओढा",
"editor": "संपादक", "editor": "संपादक",
"editSelectedShape": "निवडलेला प्रकार संपादित करा (मजकूर/बाण/रेघ)", "editLineArrowPoints": "रेघ/तीर बिंदु सम्पादित करा",
"editText": "पाठ्य सम्पादित करा/ लेबल जोडा",
"github": "समस्या मिळाली? प्रस्तुत करा", "github": "समस्या मिळाली? प्रस्तुत करा",
"howto": "आमच्या मार्गदर्शकाचे अनुसरण करा", "howto": "आमच्या मार्गदर्शकाचे अनुसरण करा",
"or": "किंवा", "or": "किंवा",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "", "increaseFontSize": "",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "", "edit": "",
"create": "", "create": "",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "ရွေးချယ်", "selection": "ရွေးချယ်",
@@ -303,7 +319,8 @@
"doubleClick": "", "doubleClick": "",
"drag": "", "drag": "",
"editor": "", "editor": "",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "", "github": "",
"howto": "", "howto": "",
"or": "", "or": "",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Øk skriftstørrelse", "increaseFontSize": "Øk skriftstørrelse",
"unbindText": "Avbind tekst", "unbindText": "Avbind tekst",
"bindText": "Bind tekst til beholderen", "bindText": "Bind tekst til beholderen",
"createContainerFromText": "La tekst flyte i en beholder",
"link": { "link": {
"edit": "Rediger lenke", "edit": "Rediger lenke",
"create": "Opprett lenke", "create": "Opprett lenke",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Kunne ikke koble til samarbeidsserveren. Vennligst oppdater siden og prøv på nytt.", "cannotResolveCollabServer": "Kunne ikke koble til samarbeidsserveren. Vennligst oppdater siden og prøv på nytt.",
"importLibraryError": "Kunne ikke laste bibliotek", "importLibraryError": "Kunne ikke laste bibliotek",
"collabSaveFailed": "Kan ikke lagre i backend-databasen. Hvis problemer vedvarer, bør du lagre filen lokalt for å sikre at du ikke mister arbeidet.", "collabSaveFailed": "Kan ikke lagre i backend-databasen. Hvis problemer vedvarer, bør du lagre filen lokalt for å sikre at du ikke mister arbeidet.",
"collabSaveFailed_sizeExceeded": "Kunne ikke lagre til backend-databasen, lerretet ser ut til å være for stort. Du bør lagre filen lokalt for å sikre at du ikke mister arbeidet ditt." "collabSaveFailed_sizeExceeded": "Kunne ikke lagre til backend-databasen, lerretet ser ut til å være for stort. Du bør lagre filen lokalt for å sikre at du ikke mister arbeidet ditt.",
"brave_measure_text_error": {
"start": "Ser ut som du bruker Brave nettleser med",
"aggressive_block_fingerprint": "Blokker fingeravtrykk aggressivt",
"setting_enabled": "innstilling aktivert",
"break": "Dette kan føre til at den bryter",
"text_elements": "Tekstelementer",
"in_your_drawings": "i tegningene dine",
"strongly_recommend": "Vi anbefaler på det sterkeste å deaktivere denne innstillingen. Du kan følge dette",
"steps": "disse trinnene",
"how": "om hvordan det skal gjøres",
"disable_setting": " Hvis deaktivering av denne innstillingen ikke fikser visningen av tekstelementer, kan du åpne en",
"issue": "sak",
"write": "på vår GitHub, eller skriv oss på",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Velg", "selection": "Velg",
@@ -303,7 +319,8 @@
"doubleClick": "dobbeltklikk", "doubleClick": "dobbeltklikk",
"drag": "dra", "drag": "dra",
"editor": "Redigeringsvisning", "editor": "Redigeringsvisning",
"editSelectedShape": "Rediger valgt figur (tekst/pil/linje)", "editLineArrowPoints": "Rediger linje/pilpunkter",
"editText": "Rediger tekst / legg til etikett",
"github": "Funnet et problem? Send inn", "github": "Funnet et problem? Send inn",
"howto": "Følg våre veiledninger", "howto": "Følg våre veiledninger",
"or": "eller", "or": "eller",
+32 -15
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Letters vergroten", "increaseFontSize": "Letters vergroten",
"unbindText": "Ontkoppel tekst", "unbindText": "Ontkoppel tekst",
"bindText": "Koppel tekst aan de container", "bindText": "Koppel tekst aan de container",
"createContainerFromText": "",
"link": { "link": {
"edit": "Wijzig link", "edit": "Wijzig link",
"create": "Maak link", "create": "Maak link",
@@ -193,7 +194,7 @@
"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": "" "collabOfflineWarning": "Geen internetverbinding beschikbaar.\nJe wijzigingen worden niet opgeslagen!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Niet-ondersteund bestandstype.", "unsupportedFileType": "Niet-ondersteund bestandstype.",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Kan geen verbinding maken met de collab server. Herlaad de pagina en probeer het opnieuw.", "cannotResolveCollabServer": "Kan geen verbinding maken met de collab server. Herlaad de pagina en probeer het opnieuw.",
"importLibraryError": "Kon bibliotheek niet laden", "importLibraryError": "Kon bibliotheek niet laden",
"collabSaveFailed": "Kan niet opslaan in de backend database. Als de problemen blijven bestaan, moet u het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.", "collabSaveFailed": "Kan niet opslaan in de backend database. Als de problemen blijven bestaan, moet u het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.",
"collabSaveFailed_sizeExceeded": "Kan de backend database niet opslaan, het canvas lijkt te groot te zijn. U moet het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest." "collabSaveFailed_sizeExceeded": "Kan de backend database niet opslaan, het canvas lijkt te groot te zijn. U moet het bestand lokaal opslaan om ervoor te zorgen dat u uw werk niet verliest.",
"brave_measure_text_error": {
"start": "Het ziet er uit dat u de Brave browser gebruikt met de",
"aggressive_block_fingerprint": "",
"setting_enabled": "instelling ingeschakeld",
"break": "Dit kan leiden tot het breken van de",
"text_elements": "Tekst Elementen",
"in_your_drawings": "in je tekeningen",
"strongly_recommend": "We raden u ten zeerste aan deze instelling uit te schakelen. U kunt dit volgen",
"steps": "deze stappen",
"how": "over hoe dit te doen",
"disable_setting": " Indien het uitschakelen van deze instelling de weergave van tekst elementen niet wijzigt, open dan een",
"issue": "probleem",
"write": "",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Selectie", "selection": "Selectie",
@@ -219,7 +235,7 @@
"library": "Bibliotheek", "library": "Bibliotheek",
"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": "Link toevoegen / bijwerken voor een geselecteerde vorm",
"eraser": "Gum", "eraser": "Gum",
"hand": "" "hand": ""
}, },
@@ -229,7 +245,7 @@
"shapes": "Vormen" "shapes": "Vormen"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "Om de canvas te verplaatsen, houd muiswiel of spatiebalk ingedrukt tijdens slepen, of gebruik het handgereedschap",
"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 +261,7 @@
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "Publiceer je eigen bibliotheek", "publishLibrary": "Publiceer je eigen bibliotheek",
"bindTextToElement": "", "bindTextToElement": "Druk op enter om tekst toe te voegen",
"deepBoxSelect": "", "deepBoxSelect": "",
"eraserRevert": "", "eraserRevert": "",
"firefox_clipboard_write": "" "firefox_clipboard_write": ""
@@ -295,7 +311,7 @@
"helpDialog": { "helpDialog": {
"blog": "Lees onze blog", "blog": "Lees onze blog",
"click": "klik", "click": "klik",
"deepSelect": "", "deepSelect": "Deep selecteer",
"deepBoxSelect": "", "deepBoxSelect": "",
"curvedArrow": "Gebogen pijl", "curvedArrow": "Gebogen pijl",
"curvedLine": "Kromme lijn", "curvedLine": "Kromme lijn",
@@ -303,7 +319,8 @@
"doubleClick": "dubbelklikken", "doubleClick": "dubbelklikken",
"drag": "slepen", "drag": "slepen",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Bewerk geselecteerde vorm (tekst/pijl/lijn)", "editLineArrowPoints": "",
"editText": "",
"github": "Probleem gevonden? Verzenden", "github": "Probleem gevonden? Verzenden",
"howto": "Volg onze handleidingen", "howto": "Volg onze handleidingen",
"or": "of", "or": "of",
@@ -364,13 +381,13 @@
"republishWarning": "" "republishWarning": ""
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "Bibliotheek ingediend",
"content": "", "content": "",
"link": "Hier" "link": "Hier"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "Reset bibliotheek",
"removeItemsFromLib": "" "removeItemsFromLib": "Verwijder geselecteerde items uit bibliotheek"
}, },
"encrypted": { "encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.", "tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.",
@@ -392,7 +409,7 @@
"width": "Breedte" "width": "Breedte"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "Toegevoegd aan bibliotheek",
"copyStyles": "Stijlen gekopieerd.", "copyStyles": "Stijlen gekopieerd.",
"copyToClipboard": "Gekopieerd naar het klembord.", "copyToClipboard": "Gekopieerd naar het klembord.",
"copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})",
@@ -456,10 +473,10 @@
"menuHint": "" "menuHint": ""
}, },
"defaults": { "defaults": {
"menuHint": "", "menuHint": "Exporteren, voorkeuren en meer...",
"center_heading": "", "center_heading": "Diagrammen. Eenvoudig. Gemaakt.",
"toolbarHint": "", "toolbarHint": "Kies een tool & begin met tekenen!",
"helpHint": "" "helpHint": "Snelkoppelingen en hulp"
} }
} }
} }
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Gjer skriftstorleik større", "increaseFontSize": "Gjer skriftstorleik større",
"unbindText": "Avbind tekst", "unbindText": "Avbind tekst",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "Rediger lenke", "edit": "Rediger lenke",
"create": "Lag lenke", "create": "Lag lenke",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Kunne ikkje kople til samarbeidsserveren. Ver vennleg å oppdatere inn sida og prøv på nytt.", "cannotResolveCollabServer": "Kunne ikkje kople til samarbeidsserveren. Ver vennleg å oppdatere inn sida og prøv på nytt.",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Vel", "selection": "Vel",
@@ -303,7 +319,8 @@
"doubleClick": "dobbelklikk", "doubleClick": "dobbelklikk",
"drag": "drag", "drag": "drag",
"editor": "Redigering", "editor": "Redigering",
"editSelectedShape": "Rediger valt form (tekst/pil/linje)", "editLineArrowPoints": "",
"editText": "",
"github": "Funne eit problem? Send inn", "github": "Funne eit problem? Send inn",
"howto": "Følg vegleiinga vår", "howto": "Følg vegleiinga vår",
"or": "eller", "or": "eller",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Aumentar talha polissa", "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",
"createContainerFromText": "",
"link": { "link": {
"edit": "Modificar lo ligam", "edit": "Modificar lo ligam",
"create": "Crear un ligam", "create": "Crear un ligam",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Connexion impossibla al servidor collab. Mercés de recargar la pagina e tornar ensajar.", "cannotResolveCollabServer": "Connexion impossibla al servidor collab. Mercés de recargar la pagina e tornar ensajar.",
"importLibraryError": "Impossible de cargar la bibliotèca", "importLibraryError": "Impossible de cargar la bibliotèca",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Seleccion", "selection": "Seleccion",
@@ -303,7 +319,8 @@
"doubleClick": "doble clic", "doubleClick": "doble clic",
"drag": "lisar", "drag": "lisar",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Modificar la fòrma seleccionada (tèxt/sageta/linha)", "editLineArrowPoints": "",
"editText": "",
"github": "Problèma trobat? Senhalatz-lo", "github": "Problèma trobat? Senhalatz-lo",
"howto": "Seguissètz nòstras guidas", "howto": "Seguissètz nòstras guidas",
"or": "o", "or": "o",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "ਫੌਂਟ ਦਾ ਅਕਾਰ ਵਧਾਓ", "increaseFontSize": "ਫੌਂਟ ਦਾ ਅਕਾਰ ਵਧਾਓ",
"unbindText": "", "unbindText": "",
"bindText": "ਪਾਠ ਨੂੰ ਕੰਟੇਨਰ ਨਾਲ ਬੰਨ੍ਹੋ", "bindText": "ਪਾਠ ਨੂੰ ਕੰਟੇਨਰ ਨਾਲ ਬੰਨ੍ਹੋ",
"createContainerFromText": "",
"link": { "link": {
"edit": "ਕੜੀ ਸੋਧੋ", "edit": "ਕੜੀ ਸੋਧੋ",
"create": "ਕੜੀ ਬਣਾਓ", "create": "ਕੜੀ ਬਣਾਓ",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "ਚੋਣਕਾਰ", "selection": "ਚੋਣਕਾਰ",
@@ -303,7 +319,8 @@
"doubleClick": "ਡਬਲ-ਕਲਿੱਕ", "doubleClick": "ਡਬਲ-ਕਲਿੱਕ",
"drag": "ਘਸੀਟੋ", "drag": "ਘਸੀਟੋ",
"editor": "ਸੋਧਕ", "editor": "ਸੋਧਕ",
"editSelectedShape": "ਚੁਣਿਆ ਰੂਪ ਸੋਧੋ (ਪਾਠ/ਤੀਰ/ਲਾਈਨ)", "editLineArrowPoints": "",
"editText": "",
"github": "ਕੋਈ ਸਮੱਸਿਆ ਲੱਭੀ? ਜਮ੍ਹਾਂ ਕਰਵਾਓ", "github": "ਕੋਈ ਸਮੱਸਿਆ ਲੱਭੀ? ਜਮ੍ਹਾਂ ਕਰਵਾਓ",
"howto": "ਸਾਡੀਆਂ ਗਾਈਡਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ", "howto": "ਸਾਡੀਆਂ ਗਾਈਡਾਂ ਦੀ ਪਾਲਣਾ ਕਰੋ",
"or": "ਜਾਂ", "or": "ਜਾਂ",
+40 -39
View File
@@ -1,52 +1,53 @@
{ {
"ar-SA": 92, "ar-SA": 89,
"bg-BG": 54, "bg-BG": 52,
"bn-BD": 59, "bn-BD": 57,
"ca-ES": 100, "ca-ES": 96,
"cs-CZ": 74, "cs-CZ": 72,
"da-DK": 32, "da-DK": 31,
"de-DE": 100, "de-DE": 100,
"el-GR": 99, "el-GR": 98,
"en": 100, "en": 100,
"es-ES": 100, "es-ES": 99,
"eu-ES": 100, "eu-ES": 99,
"fa-IR": 95, "fa-IR": 91,
"fi-FI": 100, "fi-FI": 96,
"fr-FR": 100, "fr-FR": 99,
"gl-ES": 99, "gl-ES": 99,
"he-IL": 89, "he-IL": 99,
"hi-IN": 71, "hi-IN": 73,
"hu-HU": 88, "hu-HU": 85,
"id-ID": 99, "id-ID": 98,
"it-IT": 100, "it-IT": 99,
"ja-JP": 100, "ja-JP": 97,
"kab-KAB": 93, "kab-KAB": 93,
"kk-KZ": 20, "kk-KZ": 19,
"ko-KR": 98, "ko-KR": 99,
"ku-TR": 95, "ku-TR": 91,
"lt-LT": 63, "lt-LT": 61,
"lv-LV": 97, "lv-LV": 93,
"mr-IN": 100, "mr-IN": 100,
"my-MM": 41, "my-MM": 40,
"nb-NO": 100, "nb-NO": 100,
"nl-NL": 90, "nl-NL": 92,
"nn-NO": 89, "nn-NO": 86,
"oc-FR": 98, "oc-FR": 94,
"pa-IN": 82, "pa-IN": 79,
"pl-PL": 84, "pl-PL": 87,
"pt-BR": 100, "pt-BR": 96,
"pt-PT": 100, "pt-PT": 99,
"ro-RO": 100, "ro-RO": 100,
"ru-RU": 100, "ru-RU": 96,
"si-LK": 8, "si-LK": 8,
"sk-SK": 100, "sk-SK": 99,
"sl-SI": 100, "sl-SI": 100,
"sv-SE": 100, "sv-SE": 99,
"ta-IN": 94, "ta-IN": 90,
"tr-TR": 97, "th-TH": 39,
"uk-UA": 96, "tr-TR": 98,
"vi-VN": 20, "uk-UA": 93,
"zh-CN": 100, "vi-VN": 52,
"zh-CN": 99,
"zh-HK": 25, "zh-HK": 25,
"zh-TW": 100 "zh-TW": 100
} }
+47 -30
View File
@@ -1,7 +1,7 @@
{ {
"labels": { "labels": {
"paste": "Wklej", "paste": "Wklej",
"pasteAsPlaintext": "", "pasteAsPlaintext": "Wklej jako zwykły tekst",
"pasteCharts": "Wklej wykresy", "pasteCharts": "Wklej wykresy",
"selectAll": "Zaznacz wszystko", "selectAll": "Zaznacz wszystko",
"multiSelect": "Dodaj element do zaznaczenia", "multiSelect": "Dodaj element do zaznaczenia",
@@ -72,7 +72,7 @@
"layers": "Warstwy", "layers": "Warstwy",
"actions": "Akcje", "actions": "Akcje",
"language": "Język", "language": "Język",
"liveCollaboration": "", "liveCollaboration": "Współpraca w czasie rzeczywistym...",
"duplicateSelection": "Powiel", "duplicateSelection": "Powiel",
"untitled": "Bez tytułu", "untitled": "Bez tytułu",
"name": "Nazwa", "name": "Nazwa",
@@ -96,8 +96,8 @@
"centerHorizontally": "Wyśrodkuj w poziomie", "centerHorizontally": "Wyśrodkuj w poziomie",
"distributeHorizontally": "Rozłóż poziomo", "distributeHorizontally": "Rozłóż poziomo",
"distributeVertically": "Rozłóż pionowo", "distributeVertically": "Rozłóż pionowo",
"flipHorizontal": "Odbij w poziomie", "flipHorizontal": "Odwróć w poziomie",
"flipVertical": "Odbij w pionie", "flipVertical": "Odwróć w pionie",
"viewMode": "Tryb widoku", "viewMode": "Tryb widoku",
"toggleExportColorScheme": "Przełącz schemat kolorów przy eksporcie", "toggleExportColorScheme": "Przełącz schemat kolorów przy eksporcie",
"share": "Udostępnij", "share": "Udostępnij",
@@ -106,46 +106,47 @@
"toggleTheme": "Przełącz motyw", "toggleTheme": "Przełącz motyw",
"personalLib": "Biblioteka prywatna", "personalLib": "Biblioteka prywatna",
"excalidrawLib": "Biblioteka Excalidraw", "excalidrawLib": "Biblioteka Excalidraw",
"decreaseFontSize": "", "decreaseFontSize": "Zmniejsz rozmiar czcionki",
"increaseFontSize": "", "increaseFontSize": "Zwiększ rozmiar czcionki",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "", "edit": "Edytuj łącze",
"create": "", "create": "Utwórz łącze",
"label": "" "label": "Łącze"
}, },
"lineEditor": { "lineEditor": {
"edit": "", "edit": "",
"exit": "" "exit": ""
}, },
"elementLock": { "elementLock": {
"lock": "", "lock": "Zablokuj",
"unlock": "", "unlock": "Odblokuj",
"lockAll": "", "lockAll": "Zablokuj wszystko",
"unlockAll": "" "unlockAll": "Odblokuj wszystko"
}, },
"statusPublished": "", "statusPublished": "Opublikowano",
"sidebarLock": "" "sidebarLock": "Panel boczny zawsze otwarty"
}, },
"library": { "library": {
"noItems": "", "noItems": "Nie dodano jeszcze żadnych elementów...",
"hint_emptyLibrary": "", "hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": "" "hint_emptyPrivateLibrary": "Wybierz element, aby dodać go tutaj."
}, },
"buttons": { "buttons": {
"clearReset": "Wyczyść dokument i zresetuj kolor dokumentu", "clearReset": "Wyczyść dokument i zresetuj kolor dokumentu",
"exportJSON": "Eksportuj do pliku", "exportJSON": "Eksportuj do pliku",
"exportImage": "", "exportImage": "Eksportuj obraz...",
"export": "", "export": "Zapisz jako...",
"exportToPng": "Zapisz jako PNG", "exportToPng": "Zapisz jako PNG",
"exportToSvg": "Zapisz jako SVG", "exportToSvg": "Zapisz jako SVG",
"copyToClipboard": "Skopiuj do schowka", "copyToClipboard": "Skopiuj do schowka",
"copyPngToClipboard": "Skopiuj do schowka jako plik PNG", "copyPngToClipboard": "Skopiuj do schowka jako plik PNG",
"scale": "Skala", "scale": "Skala",
"save": "", "save": "Zapisz do bieżącego pliku",
"saveAs": "Zapisz jako", "saveAs": "Zapisz jako",
"load": "", "load": "Otwórz",
"getShareableLink": "Udostępnij", "getShareableLink": "Udostępnij",
"close": "Zamknij", "close": "Zamknij",
"selectLanguage": "Wybierz język", "selectLanguage": "Wybierz język",
@@ -179,11 +180,11 @@
"couldNotLoadInvalidFile": "Nie udało się otworzyć pliku. Wybrany plik jest nieprawidłowy.", "couldNotLoadInvalidFile": "Nie udało się otworzyć pliku. Wybrany plik jest nieprawidłowy.",
"importBackendFailed": "Wystąpił błąd podczas importowania pliku.", "importBackendFailed": "Wystąpił błąd podczas importowania pliku.",
"cannotExportEmptyCanvas": "Najpierw musisz coś narysować, aby zapisać dokument.", "cannotExportEmptyCanvas": "Najpierw musisz coś narysować, aby zapisać dokument.",
"couldNotCopyToClipboard": "", "couldNotCopyToClipboard": "Nie udało się skopiować do schowka.",
"decryptFailed": "Nie udało się odszyfrować danych.", "decryptFailed": "Nie udało się odszyfrować danych.",
"uploadedSecurly": "By zapewnić Ci prywatność, udostępnianie projektu jest zabezpieczone szyfrowaniem end-to-end, co oznacza, że poza tobą i osobą z którą podzielisz się linkiem, nikt nie ma dostępu do tego co udostępniasz.", "uploadedSecurly": "By zapewnić Ci prywatność, udostępnianie projektu jest zabezpieczone szyfrowaniem end-to-end, co oznacza, że poza tobą i osobą z którą podzielisz się linkiem, nikt nie ma dostępu do tego co udostępniasz.",
"loadSceneOverridePrompt": "Wczytanie zewnętrznego rysunku zastąpi istniejącą zawartość. Czy chcesz kontynuować?", "loadSceneOverridePrompt": "Wczytanie zewnętrznego rysunku zastąpi istniejącą zawartość. Czy chcesz kontynuować?",
"collabStopOverridePrompt": "Zatrzymanie sesji nadpisze poprzedni, zapisany lokalnie rysunk. Jesteś pewien?\n\n(Jeśli chcesz zachować swój lokalny rysunek, po prostu zamknij zakładkę przeglądarki.)", "collabStopOverridePrompt": "Zatrzymanie sesji nadpisze poprzedni, zapisany lokalnie rysunek. Czy jesteś pewien?\n\n(Jeśli chcesz zachować swój lokalny rysunek, po prostu zamknij zakładkę przeglądarki.)",
"errorAddingToLibrary": "Nie udało się dodać elementu do biblioteki", "errorAddingToLibrary": "Nie udało się dodać elementu do biblioteki",
"errorRemovingFromLibrary": "Nie udało się usunąć elementu z biblioteki", "errorRemovingFromLibrary": "Nie udało się usunąć elementu z biblioteki",
"confirmAddLibrary": "To doda {{numShapes}} kształtów do twojej biblioteki. Jesteś pewien?", "confirmAddLibrary": "To doda {{numShapes}} kształtów do twojej biblioteki. Jesteś pewien?",
@@ -193,7 +194,7 @@
"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": "" "collabOfflineWarning": "Brak połączenia z Internetem.\nTwoje zmiany nie zostaną zapisane!"
}, },
"errors": { "errors": {
"unsupportedFileType": "Nieobsługiwany typ pliku.", "unsupportedFileType": "Nieobsługiwany typ pliku.",
@@ -201,10 +202,25 @@
"fileTooBig": "Plik jest zbyt duży. Maksymalny dozwolony rozmiar to {{maxSize}}.", "fileTooBig": "Plik jest zbyt duży. Maksymalny dozwolony rozmiar to {{maxSize}}.",
"svgImageInsertError": "Nie udało się wstawić obrazu SVG. Znacznik SVG wygląda na nieprawidłowy.", "svgImageInsertError": "Nie udało się wstawić obrazu SVG. Znacznik SVG wygląda na nieprawidłowy.",
"invalidSVGString": "Nieprawidłowy SVG.", "invalidSVGString": "Nieprawidłowy SVG.",
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "Nie można połączyć się z serwerem współpracy w czasie rzeczywistym. Proszę odświeżyć stronę i spróbować ponownie.",
"importLibraryError": "", "importLibraryError": "Wystąpił błąd w trakcie ładowania biblioteki",
"collabSaveFailed": "", "collabSaveFailed": "Nie udało się zapisać w bazie danych. Jeśli problemy nie ustąpią, zapisz plik lokalnie, aby nie utracić swojej pracy.",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "Nie udało się zapisać w bazie danych — dokument jest za duży. Zapisz plik lokalnie, aby nie utracić swojej pracy.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "problem",
"write": "na naszym GitHubie lub napisz do nas na",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Zaznaczenie", "selection": "Zaznaczenie",
@@ -303,7 +319,8 @@
"doubleClick": "podwójne kliknięcie", "doubleClick": "podwójne kliknięcie",
"drag": "przeciągnij", "drag": "przeciągnij",
"editor": "Edytor", "editor": "Edytor",
"editSelectedShape": "Edytuj wybrany kształt (tekst/strzałka/linia)", "editLineArrowPoints": "",
"editText": "",
"github": "Znalazłeś problem? Prześlij", "github": "Znalazłeś problem? Prześlij",
"howto": "Skorzystaj z instrukcji", "howto": "Skorzystaj z instrukcji",
"or": "lub", "or": "lub",
@@ -317,7 +334,7 @@
"zoomToFit": "Powiększ, aby wyświetlić wszystkie elementy", "zoomToFit": "Powiększ, aby wyświetlić wszystkie elementy",
"zoomToSelection": "Przybliż do zaznaczenia", "zoomToSelection": "Przybliż do zaznaczenia",
"toggleElementLock": "", "toggleElementLock": "",
"movePageUpDown": "", "movePageUpDown": "Przesuń stronę w górę/w dół",
"movePageLeftRight": "" "movePageLeftRight": ""
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Aumentar o tamanho da fonte", "increaseFontSize": "Aumentar o tamanho da fonte",
"unbindText": "Desvincular texto", "unbindText": "Desvincular texto",
"bindText": "Vincular texto ao contêiner", "bindText": "Vincular texto ao contêiner",
"createContainerFromText": "",
"link": { "link": {
"edit": "Editar link", "edit": "Editar link",
"create": "Criar link", "create": "Criar link",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Não foi possível conectar-se ao servidor colaborativo. Por favor, recarregue a página e tente novamente.", "cannotResolveCollabServer": "Não foi possível conectar-se ao servidor colaborativo. Por favor, recarregue a página e tente novamente.",
"importLibraryError": "Não foi possível carregar a biblioteca", "importLibraryError": "Não foi possível carregar a biblioteca",
"collabSaveFailed": "Não foi possível salvar no banco de dados do servidor. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.", "collabSaveFailed": "Não foi possível salvar no banco de dados do servidor. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.",
"collabSaveFailed_sizeExceeded": "Não foi possível salvar no banco de dados do servidor, a tela parece ser muito grande. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho." "collabSaveFailed_sizeExceeded": "Não foi possível salvar no banco de dados do servidor, a tela parece ser muito grande. Se os problemas persistirem, salve o arquivo localmente para garantir que não perca o seu trabalho.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Seleção", "selection": "Seleção",
@@ -303,7 +319,8 @@
"doubleClick": "clique duplo", "doubleClick": "clique duplo",
"drag": "arrastar", "drag": "arrastar",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Editar forma selecionada (texto/seta/linha)", "editLineArrowPoints": "",
"editText": "",
"github": "Encontrou algum problema? Nos informe", "github": "Encontrou algum problema? Nos informe",
"howto": "Siga nossos guias", "howto": "Siga nossos guias",
"or": "ou", "or": "ou",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Aumentar o tamanho do tipo de letra", "increaseFontSize": "Aumentar o tamanho do tipo de letra",
"unbindText": "Desvincular texto", "unbindText": "Desvincular texto",
"bindText": "Ligar texto ao recipiente", "bindText": "Ligar texto ao recipiente",
"createContainerFromText": "Envolver texto num recipiente",
"link": { "link": {
"edit": "Editar ligação", "edit": "Editar ligação",
"create": "Criar ligação", "create": "Criar ligação",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.", "cannotResolveCollabServer": "Não foi possível fazer a ligação ao servidor colaborativo. Por favor, volte a carregar a página e tente novamente.",
"importLibraryError": "Não foi possível carregar a biblioteca", "importLibraryError": "Não foi possível carregar a biblioteca",
"collabSaveFailed": "Não foi possível guardar na base de dados de backend. Se os problemas persistirem, guarde o ficheiro localmente para garantir que não perde o seu trabalho.", "collabSaveFailed": "Não foi possível guardar na base de dados de backend. Se os problemas persistirem, guarde o ficheiro localmente para garantir que não perde o seu trabalho.",
"collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho." "collabSaveFailed_sizeExceeded": "Não foi possível guardar na base de dados de backend, o ecrã parece estar muito grande. Deve guardar o ficheiro localmente para garantir que não perde o seu trabalho.",
"brave_measure_text_error": {
"start": "Parece que está a usar o navegador Brave com o",
"aggressive_block_fingerprint": "Bloqueio Agressivo via Impressão Digital",
"setting_enabled": "activo",
"break": "Isso pode desconfigurar os",
"text_elements": "Elementos de Texto",
"in_your_drawings": "nos seus desenhos",
"strongly_recommend": "Recomendamos fortemente a desactivação desta configuração. Pode seguir",
"steps": "os seguintes passos",
"how": "para saber como o fazer",
"disable_setting": " Se desactivar esta configuração não consertar a exibição de elementos de texto, por favor, abra um",
"issue": "problema",
"write": "no nosso GitHub ou então escreva-nos no",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Seleção", "selection": "Seleção",
@@ -303,7 +319,8 @@
"doubleClick": "clique duplo", "doubleClick": "clique duplo",
"drag": "arrastar", "drag": "arrastar",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Editar forma selecionada (texto/seta/linha)", "editLineArrowPoints": "",
"editText": "",
"github": "Encontrou algum problema? Informe-nos", "github": "Encontrou algum problema? Informe-nos",
"howto": "Siga os nossos guias", "howto": "Siga os nossos guias",
"or": "ou", "or": "ou",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Mărește dimensiunea fontului", "increaseFontSize": "Mărește dimensiunea fontului",
"unbindText": "Deconectare text", "unbindText": "Deconectare text",
"bindText": "Legare text de container", "bindText": "Legare text de container",
"createContainerFromText": "Încadrare text într-un container",
"link": { "link": {
"edit": "Editare URL", "edit": "Editare URL",
"create": "Creare URL", "create": "Creare URL",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Nu a putut fi realizată conexiunea la serverul de colaborare. Reîncarcă pagina și încearcă din nou.", "cannotResolveCollabServer": "Nu a putut fi realizată conexiunea la serverul de colaborare. Reîncarcă pagina și încearcă din nou.",
"importLibraryError": "Biblioteca nu a putut fi încărcată", "importLibraryError": "Biblioteca nu a putut fi încărcată",
"collabSaveFailed": "Nu s-a putut salva în baza de date la nivel de server. Dacă problemele persistă, ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.", "collabSaveFailed": "Nu s-a putut salva în baza de date la nivel de server. Dacă problemele persistă, ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.",
"collabSaveFailed_sizeExceeded": "Nu s-a putut salva în baza de date la nivel de server, întrucât se pare că pânza este prea mare. Ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca." "collabSaveFailed_sizeExceeded": "Nu s-a putut salva în baza de date la nivel de server, întrucât se pare că pânza este prea mare. Ar trebui să salvezi fișierul la nivel local pentru a te asigura că nu îți pierzi munca.",
"brave_measure_text_error": {
"start": "Se pare că folosești navigatorul Brave cu",
"aggressive_block_fingerprint": "setarea „Blocați amprentarea” în modul strict",
"setting_enabled": "activată",
"break": "Acest lucru ar putea duce la întreruperea",
"text_elements": "elementelor de text",
"in_your_drawings": "din desenele tale",
"strongly_recommend": "Recomandăm insistent dezactivarea acestei setări. Poți urma",
"steps": "acești pași",
"how": "pentru efectuarea procedurii",
"disable_setting": " Dacă dezactivarea acestei setări nu remediază afișarea elementelor de text, deschide",
"issue": "un tichet cu probleme",
"write": "pe pagina noastră de GitHub sau scrie-ne pe",
"discord": "Discord"
}
}, },
"toolBar": { "toolBar": {
"selection": "Selecție", "selection": "Selecție",
@@ -303,7 +319,8 @@
"doubleClick": "dublu clic", "doubleClick": "dublu clic",
"drag": "glisare", "drag": "glisare",
"editor": "Editor", "editor": "Editor",
"editSelectedShape": "Editează forma selectată (text/săgeată/linie)", "editLineArrowPoints": "Editare puncte de săgeată/rând",
"editText": "Editare text/adăugare etichetă",
"github": "Ai întâmpinat o problemă? Trimite un raport", "github": "Ai întâmpinat o problemă? Trimite un raport",
"howto": "Urmărește ghidurile noastre", "howto": "Urmărește ghidurile noastre",
"or": "sau", "or": "sau",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "Увеличить шрифт", "increaseFontSize": "Увеличить шрифт",
"unbindText": "Отвязать текст", "unbindText": "Отвязать текст",
"bindText": "Привязать текст к контейнеру", "bindText": "Привязать текст к контейнеру",
"createContainerFromText": "",
"link": { "link": {
"edit": "Редактировать ссылку", "edit": "Редактировать ссылку",
"create": "Создать ссылку", "create": "Создать ссылку",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "Не удалось подключиться к серверу совместного редактирования. Перезагрузите страницу и повторите попытку.", "cannotResolveCollabServer": "Не удалось подключиться к серверу совместного редактирования. Перезагрузите страницу и повторите попытку.",
"importLibraryError": "Не удалось загрузить библиотеку", "importLibraryError": "Не удалось загрузить библиотеку",
"collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.", "collabSaveFailed": "Не удалось сохранить в базу данных. Если проблема повторится, нужно будет сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.",
"collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу." "collabSaveFailed_sizeExceeded": "Не удалось сохранить в базу данных. Похоже, что холст слишком большой. Нужно сохранить файл локально, чтобы быть уверенным, что вы не потеряете вашу работу.",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "Выделение области", "selection": "Выделение области",
@@ -303,7 +319,8 @@
"doubleClick": "двойной клик", "doubleClick": "двойной клик",
"drag": "перетащить", "drag": "перетащить",
"editor": "Редактор", "editor": "Редактор",
"editSelectedShape": "Редактировать выбранную фигуру (текст/стрелка/линия)", "editLineArrowPoints": "",
"editText": "",
"github": "Нашли проблему? Отправьте", "github": "Нашли проблему? Отправьте",
"howto": "Следуйте нашим инструкциям", "howto": "Следуйте нашим инструкциям",
"or": "или", "or": "или",
+19 -2
View File
@@ -110,6 +110,7 @@
"increaseFontSize": "", "increaseFontSize": "",
"unbindText": "", "unbindText": "",
"bindText": "", "bindText": "",
"createContainerFromText": "",
"link": { "link": {
"edit": "", "edit": "",
"create": "", "create": "",
@@ -204,7 +205,22 @@
"cannotResolveCollabServer": "", "cannotResolveCollabServer": "",
"importLibraryError": "", "importLibraryError": "",
"collabSaveFailed": "", "collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": "" "collabSaveFailed_sizeExceeded": "",
"brave_measure_text_error": {
"start": "",
"aggressive_block_fingerprint": "",
"setting_enabled": "",
"break": "",
"text_elements": "",
"in_your_drawings": "",
"strongly_recommend": "",
"steps": "",
"how": "",
"disable_setting": "",
"issue": "",
"write": "",
"discord": ""
}
}, },
"toolBar": { "toolBar": {
"selection": "", "selection": "",
@@ -303,7 +319,8 @@
"doubleClick": "", "doubleClick": "",
"drag": "", "drag": "",
"editor": "", "editor": "",
"editSelectedShape": "", "editLineArrowPoints": "",
"editText": "",
"github": "", "github": "",
"howto": "", "howto": "",
"or": "", "or": "",

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