Compare commits

...
Author SHA1 Message Date
Zsolt Viczian 09ae07ed7f Added font color picker 2022-03-14 20:56:12 +01:00
Aakansha DoshiandGitHub 6d45430344 fix: undo when erasing elements by clicking (#4921)
* fix: undo when erasing elements by clicking

* newline remove
2022-03-14 14:59:55 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3aa0c5ebc0 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4911)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.6.0 to 6.7.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.6.0...v6.7.1)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-14 14:47:01 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e940993e0e chore(deps-dev): bump css-loader in /src/packages/utils (#4914)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.1 to 6.7.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.5.1...v6.7.1)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-14 14:45:33 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8f90aeb8d5 chore(deps-dev): bump ts-loader in /src/packages/utils (#4913)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.6 to 9.2.8.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.6...v9.2.8)

---
updated-dependencies:
- dependency-name: ts-loader
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-03-14 14:45:02 +05:30
Aakansha DoshiandGitHub e92d133973 fix: undo when erasing (#4900) 2022-03-11 20:44:17 +05:30
David LuzarandGitHub b682d88167 fix: incorrectly erasing on mobile (#4899)
* fix: incorrectly erasing on mobile

* reintroduce fix for erasing on single-point click

* fix snaps
2022-03-11 15:45:59 +01:00
Aakansha DoshiandGitHub 7daf1a7944 feat: Add Eraser 🎉 (#4887)
* feat: Add Eraser 🎉

* Eraser working

* remove unused state

* fix

* toggle eraser

* Support deselect with Alt/Option

* rename actionDelete -> actionErase

* Add util isEraserActive

* show eraser in mobile

* render eraser conditionally in mobile

* use selection if eraser in local storage state

* Add sampling to erase accurately

* use pointerDownState

* set eraser to false in AllowedExcalidrawElementTypes

* rename/reword fixes

* don't use updateScene

* handle bound text when erasing

* fix hover state in mobile

* consider all hitElements instead of a single

* code improvements

* revert to select if eraser active and elements selected

* show eraser in zenmode

* erase element when clicked on element while eraser active

* set groupIds to empty when eraser active

* fix test

* remove dragged distance
2022-03-11 19:53:42 +05:30
5c0eff50a0 chore: Update translations from Crowdin (#4729)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-03-09 12:21:26 +01:00
19056d635b feat: added optional REACT_APP_WS_SERVER_URL for forks usecases (#4889)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-03-09 12:13:59 +01:00
Tom MilliganandGitHub 4d5f00ff08 fix: don't crash on drop highlighted text onto canvas (#4890) 2022-03-09 11:51:13 +01:00
20de06ef50 fix: paste styles shortcut (#4886)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-03-09 10:59:44 +01:00
zsviczianandGitHub 1849ff6ee2 fix: freedraw element's background fill color missing from SVG when exporting with package API exportToSvg() (#4871) 2022-03-06 23:35:16 +01:00
6765fc16be fix: improve pointer syncing performance (#4883)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-03-06 22:11:14 +00:00
5ca4f5bbf4 feat: rewrite collab server connecting (#4881)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-03-06 22:43:02 +01:00
David LuzarandGitHub 9392ec276d fix: collab room initialization (#4882) 2022-03-06 15:59:56 +01:00
Aakansha DoshiandGitHub b26e4fcf99 build: support runtime React Jsx in @excalidraw/utils (#4866)
* build: support runtime React Jsx

* revert version

* upgrade
2022-03-04 10:58:02 +05:30
David LuzarandGitHub 45f3410da8 build: release @excalidraw/utils 0.1.1 (#4862) 2022-03-03 14:59:08 +01:00
David LuzarandGitHub 94b387ef7b fix: ensure verticalAlign properties not shown when no element selected (#4860)
* fix: ensure verticalAlign properties not shown when no element selected

* show verticalAlign prop if selection contains at least one applicable element

* simplify
2022-03-02 23:56:20 +01:00
David LuzarandGitHub 6d0716eb6b fix: binding text to non-bindable containers and not always preferring selection (#4655) 2022-03-02 17:04:09 +01:00
8e26d5b500 feat: support vertical text align for bound containers (#4852)
* feat: support vertical text align for bound containers

* update icons

* use const

* fix lint

* rename to  and show when text editor active

* don't update vertical align if not center

* fix svgs

* fix y coords when vertical align bottm

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-03-02 20:06:07 +05:30
luzpazandGitHub c5a7723185 chore: fix various typos (#4857)
Found via `codespell -q 3 -S ./src/locales,./yarn.lock,./src/packages/excalidraw/yarn.lock -L afterall,doubleclick,originaly,reenable,whats,sur`
2022-03-02 11:37:12 +05:30
49172ac2d3 feat: support custom colors 🎉 (#4843)
* feat: support custom colors 🎉

* remove canvasBackground

* fix tests

* Remove custom color when elements deleted

* persist custom color across sessions

* Choose 5 latest custom colors when populating from elements

* fix tests

* styling

* don't use up/down arrow for custom colors

* Always push latest color to the begining

* don't check if valid in custom color

* calculate custom colors on color picker open

* revert unnecessary changes

* remove newlines

* simplify state

* tweak label

* fix custom color shortcuts throwing if color not exists

* fix

* early return

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-28 19:04:26 +05:30
David LuzarandGitHub 618a846451 chore: remove firebase-tools (#4792) 2022-02-28 10:54:20 +01:00
Aakansha DoshiandGitHub d9f49ffd67 fix: Don't show align icons for single bound container element (#4846)
* fix: Don't show align icons for single bound container element

* check 2nd element as well
2022-02-28 11:08:28 +05:30
Jesse JurmanandGitHub 46e43baad1 feat: Support Links in Exported SVG (#4791) 2022-02-25 21:42:10 +01:00
Aakansha DoshiandGitHub bd35b682fa fix: redraw text bounding box when pasting styles (#4845) 2022-02-25 15:36:56 +05:30
David LuzarandGitHub b6f9a8005e docs: list who's integrating excalidraw (#4832)
* docs: list who's integrating excalidraw

* Update README.md
2022-02-23 23:28:17 +01:00
1acfaf6b6e feat: Scale font size when bound text containers resized with shift pressed (#4828)
* feat: Scale font size when bound text containers resized with shift pressed

* revert fontsize once shift pressed/released after resize

* make slightly more typesafe

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-22 18:45:59 +05:30
Aakansha DoshiandGitHub 5cf7087754 fix: restore cursor position after bound text container value updated (#4836)
* fix: restore cursor position after bound text container value updated

* only restore cursor when the cursor is not at the end of the line

* compute diff before setting the cursor
2022-02-22 18:24:06 +05:30
Aakansha DoshiandGitHub b2d49155ef build:remove build packages workflow (#4835) 2022-02-22 13:50:25 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9745461db7 chore(deps): bump browser-fs-access from 0.23.0 to 0.24.1 (#4820)
Bumps [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) from 0.23.0 to 0.24.1.
- [Release notes](https://github.com/GoogleChromeLabs/browser-fs-access/releases)
- [Commits](https://github.com/GoogleChromeLabs/browser-fs-access/compare/v0.23.0...v0.24.1)

---
updated-dependencies:
- dependency-name: browser-fs-access
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-22 13:26:53 +05:30
21e9fcb2f5 chore: Add tracking for hyperlinks (#4703)
* chore: Add tracking for hyperlinks

* update

* fix

* remove

* tweak

* disable ga logging in dev again

* add logging for hyperlink `edit` & support for tracking in manager

* event label tweaks

* fix tests & make more typesafe

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-21 17:44:28 +05:30
Aakansha DoshiandGitHub e203203993 refactor: don't pass array to handleBindTextResize (#4826) 2022-02-21 17:15:29 +05:30
Aakansha DoshiandGitHub f224e4d596 fix: support resizing multiple bound text containers (#4824) 2022-02-21 16:46:39 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e0ca689759 chore(deps): bump url-parse from 1.5.3 to 1.5.7 (#4807)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.3 to 1.5.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.3...1.5.7)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-19 15:16:30 +05:30
f792eb5ae7 fix: also check overflowY: overlay in detectScroll (#4806)
* fix: also check overflowY: overlay in detectScroll

* fix lint

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-18 19:17:43 +01:00
Aakansha DoshiandGitHub 4604c8d823 fix: stuck resizing when resizing bound text container very fast beyond threshold (#4804)
* fix: stuck resizing when resizing bound text container very fast beyond threshold

* fix

* fix
2022-02-18 18:20:55 +05:30
Aakansha DoshiandGitHub 0896892f8a docs: release @excalidraw/excalidraw@0.11.0 🎉 (#4799)
* docs: release @excalidraw/excalidraw@0.11.0  🎉

* Add commit link for bad commits
2022-02-17 18:52:44 +05:30
7fe225ee99 fix: rename --color-primary-chubb to --color-primary-contrast-offset and fallback to primary color if not present (#4803)
* fix: fallback to primary color if --color-primary-chubb not present

* rename to --color-primary-contrast-offset

* use contarst-offset

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

* Update src/packages/excalidraw/README_NEXT.md

* remove

* Update src/packages/excalidraw/README_NEXT.md

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

Co-authored-by: David Luzar <luzar.david@gmail.com>
2022-02-17 18:22:19 +05:30
Aakansha DoshiandGitHub d2fd7be457 fix: add commits directly pushed to master in changelog (#4798) 2022-02-16 21:01:59 +05:30
David LuzarandGitHub 5c61613a2e fix: don't bump element version when adding files data (#4794)
* fix: don't bump element version when adding files data

* fix lint
2022-02-16 18:26:36 +05:30
Aakansha DoshiandGitHub b2767924de feat: show group/group and link action in mobile (#4795) 2022-02-16 15:41:35 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
59d0a77862 chore(deps): bump @types/react from 17.0.38 to 17.0.39 (#4757)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.38 to 17.0.39.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-16 13:59:14 +05:30
Aakansha DoshiandGitHub 987526d1e5 docs: tweak documentation for release and add examples (#4786)
* docs: tweak documentation for release

* Add image in initial data

* Add image

* remove watermark and make export work

* update readme
2022-02-15 19:13:46 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e894d41a22 chore(deps): bump vm2 from 3.9.5 to 3.9.7 (#4785)
Bumps [vm2](https://github.com/patriksimek/vm2) from 3.9.5 to 3.9.7.
- [Release notes](https://github.com/patriksimek/vm2/releases)
- [Changelog](https://github.com/patriksimek/vm2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/patriksimek/vm2/compare/3.9.5...3.9.7)

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

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-15 16:47:23 +05:30
Aakansha DoshiandGitHub 14d1d39e8e chore: variable naming :) (#4782) 2022-02-15 16:31:14 +05:30
Aakansha DoshiandGitHub 69336b4832 build: rename release command to 'release package' (#4783) 2022-02-14 17:47:52 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
32b677fb8a chore(deps): bump follow-redirects in /src/packages/excalidraw (#4781)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-14 14:03:04 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
570f725516 chore(deps): bump follow-redirects from 1.14.7 to 1.14.8 (#4780)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-14 13:54:57 +05:30
Aakansha DoshiandGitHub a60860867c build: release preview package when triggered via comment (#4750)
* build: autorelease preview on every commit during pull request

* add github workflow

* update readme

* update docs

* log changed files

* remove depth

* fetch pr head

* remove console.log

* log pr number

* pull pr number

* use pull request number in release version

* dummy

* dummy

* dummy

* fix

* dummy

* fix

* Add comment and set output as version

* dummy

* fix

* fix

* set output through js toolkit

* install

* dummy

* update

* fix

* fix

* typo

* update

* condition

* typo

* testing

* wrap conditions

* echo

* hope it works

* test

* test

* yay test again

* test updated

* remove reaction

* run if comment triggered

* fix

* fix

* Update script after testing in fork

* remove

* update changelog

* update readme

* update

* remove

* append pr number then commit hash
2022-02-14 13:54:24 +05:30
7a61196462 fix: mobile link click (#4742)
* add tolerance to redirect pointerDown_Up check

* Update src/components/App.tsx

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

* Update App.tsx

* lint

* lint

* fix for ipad/mobile

* Update App.tsx

* Update App.tsx

* Update App.tsx

* testing if isIPad works on iOS15

* Update App.tsx

* Update keys.ts

* Update keys.ts

* lint

* test

* removed isTouchScreen

* isTouchScreen

* lint

* lint

* Update App.tsx

* tweak

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: ad1992 <aakansha1216@gmail.com>
2022-02-10 14:52:33 +05:30
David LuzarandGitHub 9653d676fe fix: contextMenu timer & pointers not correctly reset on iOS (#4765) 2022-02-09 20:42:02 +01:00
0cdd0eebf1 feat: support background fill for freedraw shapes (#4610)
* feat: support background fill for freedraw shapes

* refactor & support fill style

* make filled freedraw shapes selectable from inside

* get hit test on solid freedraw shapes to somewhat work

* fix SVG export of unclosed freedraw shapes & improve types

* fix lint

* type tweaks

* reuse `hitTestCurveInside` for collision tests

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-09 17:43:21 +01:00
Aakansha DoshiandGitHub ae8b1d8bf7 build: deploy excalidraw package example (#4762)
* build: deploy excalidraw package example

* deploy public

* install deps script

* new lines
2022-02-09 17:45:16 +05:30
Aakansha DoshiandGitHub 92ffe8dda6 fix: use absolute coords when rendering link popover (#4753) 2022-02-09 16:33:49 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4d9dbd5a45 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4712)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.1 to 6.6.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.5.1...v6.6.0)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 07:41:07 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c66cabaefd chore(deps-dev): bump webpack-cli in /src/packages/excalidraw (#4664)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.9.1 to 4.9.2.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.9.1...webpack-cli@4.9.2)

---
updated-dependencies:
- dependency-name: webpack-cli
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 07:37:33 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e073128469 chore(deps-dev): bump @babel/core in /src/packages/utils (#4754)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.16.7 to 7.17.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.2/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 07:25:15 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
835848d711 chore(deps): bump sass from 1.47.0 to 1.49.7 (#4723)
Bumps [sass](https://github.com/sass/dart-sass) from 1.47.0 to 1.49.7.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.47.0...1.49.7)

---
updated-dependencies:
- dependency-name: sass
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:52:23 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2bd1d7ef59 chore(deps-dev): bump lint-staged from 12.1.7 to 12.3.3 (#4724)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.1.7 to 12.3.3.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.1.7...v12.3.3)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:51:23 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
37c8b9c2ff chore(deps-dev): bump webpack-dev-server in /src/packages/excalidraw (#4713)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.7.3 to 4.7.4.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.7.3...v4.7.4)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:50:10 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cf9f00f55f chore(deps-dev): bump chai from 4.3.4 to 4.3.6 (#4667)
Bumps [chai](https://github.com/chaijs/chai) from 4.3.4 to 4.3.6.
- [Release notes](https://github.com/chaijs/chai/releases)
- [Changelog](https://github.com/chaijs/chai/blob/4.x.x/History.md)
- [Commits](https://github.com/chaijs/chai/compare/v4.3.4...v4.3.6)

---
updated-dependencies:
- dependency-name: chai
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:49:53 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7ae9043221 chore(deps): bump @testing-library/jest-dom from 5.16.1 to 5.16.2 (#4745)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.1 to 5.16.2.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.1...v5.16.2)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:49:33 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7c567408c5 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#4707)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.16.7 to 7.17.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.0/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:49:09 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
54612621aa chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#4709)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.3.0 to 5.3.1.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.3.0...v5.3.1)

---
updated-dependencies:
- dependency-name: terser-webpack-plugin
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-09 12:48:21 +05:30
d27b3bbebe fix: changing font size when text is not selected or edited (#4751)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-08 21:18:43 +00:00
Aakansha DoshiandGitHub e4ffc9812e docs: changelog tweaks (#4749) 2022-02-08 18:58:37 +05:30
a066317d3c feat: add onLinkOpen component prop (#4694)
Co-authored-by: ad1992 <aakansha1216@gmail.com>
2022-02-08 11:25:35 +01:00
David LuzarandGitHub 050bc1ce2b feat: keep selected tool on canvas reset (#4728) 2022-02-07 22:30:06 +01:00
David LuzarandGitHub 5007df6522 fix: disable contextmenu on non-secondary pen events or touch (#4675) 2022-02-07 20:01:36 +01:00
d450c36581 fix: mobile context menu won't show on long press (#4741)
* scribble fix only if not Android

* Update src/components/App.tsx

Co-authored-by: David Luzar <luzar.david@gmail.com>
2022-02-07 19:46:29 +01:00
Aakansha DoshiandGitHub 66c92fc65a feat: Make whole element clickable in view mode when it has hyperlink (#4735)
* feat: Make whole element clickable in view mode when it has hyperlink

* redirect to link if pointerup and pointer down is exactly same point

* don't make element clickable in mobile
2022-02-07 19:54:39 +05:30
David LuzarandGitHub 5f1cd4591a fix: do not open links twice (#4738) 2022-02-07 12:20:19 +00:00
Aakansha DoshiandGitHub 9be6243873 fix: make link icon clickable in mobile (#4736) 2022-02-07 17:24:51 +05:30
David LuzarandGitHub c3f6d6d344 test: revert node v16 requirement for tests (#4737) 2022-02-07 12:27:31 +01:00
David LuzarandGitHub 339636caab feat: allow any precision when zooming (#4730) 2022-02-06 21:58:59 +01:00
David LuzarandGitHub 08115ef311 fix: Apple Pen missing strokes (#4705) 2022-02-06 20:07:37 +01:00
Excalidraw BotandGitHub e68abdbab4 chore: Update translations from Crowdin (#4590) 2022-02-06 18:08:55 +01:00
David LuzarandGitHub 8aff076782 feat: throttle pointermove events per framerate (#4727) 2022-02-06 17:45:37 +01:00
96de887cc8 fix: freedraw slow movement jittery lines (#4726)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2022-02-06 17:45:23 +01:00
zsviczianandGitHub 98ea46664c fix: Disable three finger pinch zoom in penMode (#4725) 2022-02-06 16:56:52 +01:00
Aakansha DoshiandGitHub 00e30ca0e4 fix: remove click listener for opening popup (#4700)
* fix: remove click listener for oening popup

* fix
2022-02-04 20:36:21 +05:30
151 changed files with 4903 additions and 4080 deletions
+5 -1
View File
@@ -4,5 +4,9 @@ REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_SOCKET_SERVER_URL=http://localhost:3002
REACT_APP_PORTAL_URL=http://localhost:3002
# Fill to set socket server URL used for collaboration.
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
REACT_APP_WS_SERVER_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
+5 -1
View File
@@ -4,7 +4,11 @@ REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_SOCKET_SERVER_URL=https://oss-collab-us1.excalidraw.com
REACT_APP_PORTAL_URL=https://portal.excalidraw.com
# Fill to set socket server URL used for collaboration.
# Meant for forks only: excalidraw.com uses custom REACT_APP_PORTAL_URL flow
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"}'
# production-only vars
@@ -23,4 +23,5 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release
run: |
yarn add @actions/core
yarn autorelease
+55
View File
@@ -0,0 +1,55 @@
name: Auto release preview @excalidraw/excalidraw-preview
on:
issue_comment:
types: [created, edited]
jobs:
Auto-release-excalidraw-preview:
name: Auto release preview
if: github.event.comment.body == '@excalibot release package' && github.event.issue.pull_request
runs-on: ubuntu-latest
steps:
- name: React to release comment
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
comment-id: ${{ github.event.comment.id }}
reactions: "+1"
- name: Get PR SHA
id: sha
uses: actions/github-script@v4
with:
result-encoding: string
script: |
const { owner, repo, number } = context.issue;
const pr = await github.pulls.get({
owner,
repo,
pull_number: number,
});
return pr.data.head.sha
- uses: actions/checkout@v2
with:
ref: ${{ steps.sha.outputs.result }}
fetch-depth: 2
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release preview
id: "autorelease"
run: |
yarn add @actions/core
yarn autorelease preview ${{ github.event.issue.number }}
- name: Post comment post release
if: always()
uses: peter-evans/create-or-update-comment@v1
with:
token: ${{ secrets.PUSH_TRANSLATIONS_COVERAGE_PAT }}
issue-number: ${{ github.event.issue.number }}
body: "@${{ github.event.comment.user.login }} ${{ steps.autorelease.outputs.result }}"
-29
View File
@@ -1,29 +0,0 @@
name: Build packages
on:
push:
branches:
- master
pull_request:
jobs:
packages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn --cwd src/packages/excalidraw
yarn --cwd src/packages/utils
- name: Build @excalidraw/excalidraw
run: |
yarn --cwd src/packages/excalidraw run pack
- name: Build @excalidraw/utils
run: |
yarn --cwd src/packages/utils run pack
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 16.x
node-version: 14.x
- name: Install and test
run: |
yarn --frozen-lockfile
+4
View File
@@ -23,3 +23,7 @@ static
yarn-debug.log*
yarn-error.log*
src/packages/excalidraw/types
src/packages/excalidraw/example/public/bundle.js
src/packages/excalidraw/example/public/excalidraw-assets-dev
src/packages/excalidraw/example/public/excalidraw.development.js
+4
View File
@@ -32,6 +32,10 @@ Last but not least, we're thankful to these companies for offering their service
[![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) •
## Documentation
### Shortcuts
+8 -9
View File
@@ -21,15 +21,15 @@
"dependencies": {
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.1",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.2",
"@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "17.0.38",
"@types/react": "17.0.39",
"@types/react-dom": "17.0.11",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.23.0",
"browser-fs-access": "0.24.1",
"clsx": "1.1.1",
"fake-indexeddb": "3.1.7",
"firebase": "8.3.3",
@@ -50,9 +50,9 @@
"react-dom": "17.0.2",
"react-scripts": "4.0.3",
"roughjs": "4.5.2",
"sass": "1.47.0",
"sass": "1.49.7",
"socket.io-client": "2.3.1",
"typescript": "4.5.4"
"typescript": "4.5.5"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
@@ -61,20 +61,19 @@
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4",
"chai": "4.3.6",
"dotenv": "10.0.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.23.0",
"husky": "7.0.4",
"jest-canvas-mock": "2.3.1",
"lint-staged": "12.1.7",
"lint-staged": "12.3.3",
"pepjs": "0.5.3",
"prettier": "2.5.1",
"rewire": "5.0.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"
"@typescript-eslint/typescript-estree": "5.10.2"
},
"engines": {
"node": ">=14.0.0"
-6
View File
@@ -72,12 +72,6 @@
crossorigin="anonymous"
/>
<link
href="%REACT_APP_SOCKET_SERVER_URL%/socket.io"
rel="preconnect"
crossorigin="anonymous"
/>
<link
rel="manifest"
href="manifest.json"
+30 -5
View File
@@ -1,5 +1,6 @@
const fs = require("fs");
const { exec, execSync } = require("child_process");
const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
@@ -15,18 +16,25 @@ const publish = () => {
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
console.info("Published 🎉");
core.setOutput(
"result",
`**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
);
} catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
console.error(error);
process.exit(1);
}
};
// get files changed between prev and head commit
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
if (error || stderr) {
console.error(error);
core.setOutput("result", ":warning: Package couldn't be published!");
process.exit(1);
}
const changedFiles = stdout.trim().split("\n");
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/;
@@ -37,16 +45,33 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
);
});
if (!excalidrawPackageFiles.length) {
console.info("Skipping release as no valid diff found");
core.setOutput("result", "Skipping release as no valid diff found");
process.exit(0);
}
// update package.json
pkg.version = `${pkg.version}-${getShortCommitHash()}`;
pkg.name = "@excalidraw/excalidraw-next";
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme
const data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
const isPreview = process.argv.slice(2)[0] === "preview";
if (isPreview) {
// use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
// replace "excalidraw-next" with "excalidraw-preview"
pkg.name = "@excalidraw/excalidraw-preview";
data = data.replace(/excalidraw-next/g, "excalidraw-preview");
data = data.trim();
}
pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
console.info("Publish in progress...");
publish();
});
+18 -11
View File
@@ -20,7 +20,7 @@ const headerForType = {
perf: "Performance",
build: "Build",
};
const badCommits = [];
const getCommitHashForLastVersion = async () => {
try {
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
@@ -53,19 +53,26 @@ const getLibraryCommitsSinceLastRelease = async () => {
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
const messageWithCapitalizeFirst =
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
const prNumber = commit.match(/\(#([0-9]*)\)/)[1];
const prMatch = commit.match(/\(#([0-9]*)\)/);
if (prMatch) {
const prNumber = prMatch[1];
// return if the changelog already contains the pr number which would happen for package updates
if (existingChangeLog.includes(prNumber)) {
return;
// return if the changelog already contains the pr number which would happen for package updates
if (existingChangeLog.includes(prNumber)) {
return;
}
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
const messageWithPRLink = messageWithCapitalizeFirst.replace(
/\(#[0-9]*\)/,
prMarkdown,
);
commitList[type].push(messageWithPRLink);
} else {
badCommits.push(commit);
commitList[type].push(messageWithCapitalizeFirst);
}
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
const messageWithPRLink = messageWithCapitalizeFirst.replace(
/\(#[0-9]*\)/,
prMarkdown,
);
commitList[type].push(messageWithPRLink);
});
console.info("Bad commits:", badCommits);
return commitList;
};
+36 -3
View File
@@ -1,5 +1,5 @@
import { ColorPicker } from "../components/ColorPicker";
import { zoomIn, zoomOut } from "../components/icons";
import { eraser, zoomIn, zoomOut } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { THEME, ZOOM_STEP } from "../constants";
@@ -15,8 +15,9 @@ import { getShortcutKey } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState } from "../appState";
import { getDefaultAppState, isEraserActive } from "../appState";
import ClearCanvas from "../components/ClearCanvas";
import clsx from "clsx";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@@ -26,7 +27,7 @@ export const actionChangeViewBackgroundColor = register({
commitToHistory: !!value.viewBackgroundColor,
};
},
PanelComponent: ({ appState, updateData }) => {
PanelComponent: ({ elements, appState, updateData }) => {
return (
<div style={{ position: "relative" }}>
<ColorPicker
@@ -39,6 +40,8 @@ export const actionChangeViewBackgroundColor = register({
updateData({ openPopup: active ? "canvasColorPicker" : null })
}
data-testid="canvas-background-picker"
elements={elements}
appState={appState}
/>
</div>
);
@@ -65,6 +68,8 @@ export const actionClearCanvas = register({
gridSize: appState.gridSize,
showStats: appState.showStats,
pasteDialog: appState.pasteDialog,
elementType:
appState.elementType === "image" ? "selection" : appState.elementType,
},
commitToHistory: true,
};
@@ -285,3 +290,31 @@ export const actionToggleTheme = register({
),
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
});
export const actionErase = register({
name: "eraser",
perform: (elements, appState) => {
return {
appState: {
...appState,
selectedElementIds: {},
selectedGroupIds: {},
elementType: isEraserActive(appState) ? "selection" : "eraser",
},
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, data }) => (
<ToolButton
type="button"
icon={eraser}
className={clsx("eraser", { active: isEraserActive(appState) })}
title={t("toolBar.eraser")}
aria-label={t("toolBar.eraser")}
onClick={() => {
updateData(null);
}}
size={data?.size || "medium"}
></ToolButton>
),
});
+5 -3
View File
@@ -7,7 +7,7 @@ import { distributeElements, Distribution } from "../disitrubte";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES } from "../keys";
import { CODES, KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
@@ -49,7 +49,8 @@ export const distributeHorizontally = register({
commitToHistory: true,
};
},
keyTest: (event) => event.altKey && event.code === CODES.H,
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(elements, appState)}
@@ -77,7 +78,8 @@ export const distributeVertically = register({
commitToHistory: true,
};
},
keyTest: (event) => event.altKey && event.code === CODES.V,
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(elements, appState)}
+1 -1
View File
@@ -155,7 +155,7 @@ const flipElement = (
// calculate new x-coord for transformation
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
resizeSingleElement(
element,
new Map().set(element.id, element),
true,
element,
usingNWHandle ? "nw" : "ne",
+228 -62
View File
@@ -30,11 +30,15 @@ import {
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
TextAlignTopIcon,
TextAlignBottomIcon,
TextAlignMiddleIcon,
} from "../components/icons";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
FONT_FAMILY,
VERTICAL_ALIGN,
} from "../constants";
import {
getNonDeletedElements,
@@ -47,6 +51,7 @@ import {
getContainerElement,
} from "../element/textElement";
import {
hasBoundTextElement,
isBoundToContainer,
isLinearElement,
isLinearElementType,
@@ -58,6 +63,7 @@ import {
ExcalidrawTextElement,
FontFamilyValues,
TextAlign,
VerticalAlign,
} from "../element/types";
import { getLanguage, t } from "../i18n";
import { KEYS } from "../keys";
@@ -101,6 +107,7 @@ const getFormValue = function <T>(
appState: AppState,
getAttribute: (element: ExcalidrawElement) => T,
defaultValue?: T,
onlyBoundTextElements: boolean = false,
): T | null {
const editingElement = appState.editingElement;
const nonDeletedElements = getNonDeletedElements(elements);
@@ -111,6 +118,7 @@ const getFormValue = function <T>(
nonDeletedElements,
appState,
getAttribute,
onlyBoundTextElements,
)
: defaultValue) ??
null
@@ -145,6 +153,7 @@ const changeFontSize = (
elements: readonly ExcalidrawElement[],
appState: AppState,
getNewFontSize: (element: ExcalidrawTextElement) => number,
fallbackValue?: ExcalidrawTextElement["fontSize"],
) => {
const newFontSizes = new Set<number>();
@@ -182,7 +191,7 @@ const changeFontSize = (
currentItemFontSize:
newFontSizes.size === 1
? [...newFontSizes][0]
: appState.currentItemFontSize,
: fallbackValue ?? appState.currentItemFontSize,
},
commitToHistory: true,
};
@@ -190,8 +199,8 @@ const changeFontSize = (
// -----------------------------------------------------------------------------
export const actionChangeStrokeColor = register({
name: "changeStrokeColor",
export const actionChangeFontColor = register({
name: "changeFontColor",
perform: (elements, appState, value) => {
return {
...(value.currentItemStrokeColor && {
@@ -199,7 +208,7 @@ export const actionChangeStrokeColor = register({
elements,
appState,
(el) => {
return hasStrokeColor(el.type)
return isTextElement(el)
? newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
})
@@ -215,26 +224,107 @@ export const actionChangeStrokeColor = register({
commitToHistory: !!value.currentItemStrokeColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker
type="elementStroke"
label={t("labels.stroke")}
color={getFormValue(
PanelComponent: ({ elements, appState, updateData }) => {
return (
<>
<h3 aria-hidden="true">{t("labels.fontColor")}</h3>
<ColorPicker
type="elementFontColor"
label={t("labels.fontColor")}
color={getFormValue(
elements,
appState,
(element) => element.strokeColor,
appState.currentItemStrokeColor,
true,
)}
onChange={(color) => updateData({ currentItemStrokeColor: color })}
isActive={appState.openPopup === "fontColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "fontColorPicker" : null })
}
elements={elements}
appState={appState}
/>
</>
);
},
});
export const actionChangeStrokeColor = register({
name: "changeStrokeColor",
perform: (elements, appState, value) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState,
);
const hasOnlyContainersWithBoundText =
targetElements.length > 1 &&
targetElements.every(
(element) =>
hasBoundTextElement(element) || isBoundToContainer(element),
);
return {
...(value.currentItemStrokeColor && {
elements: changeProperty(
elements,
appState,
(element) => element.strokeColor,
appState.currentItemStrokeColor,
)}
onChange={(color) => updateData({ currentItemStrokeColor: color })}
isActive={appState.openPopup === "strokeColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "strokeColorPicker" : null })
}
/>
</>
),
(el) => {
return (hasStrokeColor(el.type) &&
!hasOnlyContainersWithBoundText) ||
!isBoundToContainer(el)
? newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
})
: el;
},
true,
),
}),
appState: {
...appState,
...value,
},
commitToHistory: !!value.currentItemStrokeColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState,
);
const hasOnlyContainersWithBoundText = targetElements.every(
(element) => hasBoundTextElement(element) || isBoundToContainer(element),
);
return (
<>
<h3 aria-hidden="true">{t("labels.stroke")}</h3>
<ColorPicker
type="elementStroke"
label={t("labels.stroke")}
color={getFormValue(
hasOnlyContainersWithBoundText
? elements.filter((element) => !isTextElement(element))
: elements,
appState,
(element) => element.strokeColor,
appState.currentItemStrokeColor,
)}
onChange={(color) => updateData({ currentItemStrokeColor: color })}
isActive={appState.openPopup === "strokeColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "strokeColorPicker" : null })
}
elements={elements}
appState={appState}
/>
</>
);
},
});
export const actionChangeBackgroundColor = register({
@@ -272,6 +362,8 @@ export const actionChangeBackgroundColor = register({
setActive={(active) =>
updateData({ openPopup: active ? "backgroundColorPicker" : null })
}
elements={elements}
appState={appState}
/>
</>
),
@@ -520,7 +612,7 @@ export const actionChangeOpacity = register({
export const actionChangeFontSize = register({
name: "changeFontSize",
perform: (elements, appState, value) => {
return changeFontSize(elements, appState, () => value);
return changeFontSize(elements, appState, () => value, value);
},
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
@@ -532,21 +624,25 @@ export const actionChangeFontSize = register({
value: 16,
text: t("labels.small"),
icon: <FontSizeSmallIcon theme={appState.theme} />,
testId: "fontSize-small",
},
{
value: 20,
text: t("labels.medium"),
icon: <FontSizeMediumIcon theme={appState.theme} />,
testId: "fontSize-medium",
},
{
value: 28,
text: t("labels.large"),
icon: <FontSizeLargeIcon theme={appState.theme} />,
testId: "fontSize-large",
},
{
value: 36,
text: t("labels.veryLarge"),
icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
testId: "fontSize-veryLarge",
},
]}
value={getFormValue(
@@ -704,9 +800,7 @@ export const actionChangeTextAlign = register({
if (isTextElement(oldElement)) {
const newElement: ExcalidrawTextElement = newElementWith(
oldElement,
{
textAlign: value,
},
{ textAlign: value },
);
redrawTextBoundingBox(
newElement,
@@ -727,47 +821,119 @@ export const actionChangeTextAlign = register({
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
<ButtonIconSelect<TextAlign | false>
group="text-align"
options={[
{
value: "left",
text: t("labels.left"),
icon: <TextAlignLeftIcon theme={appState.theme} />,
},
{
value: "center",
text: t("labels.center"),
icon: <TextAlignCenterIcon theme={appState.theme} />,
},
{
value: "right",
text: t("labels.right"),
icon: <TextAlignRightIcon theme={appState.theme} />,
},
]}
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.textAlign;
PanelComponent: ({ elements, appState, updateData }) => {
return (
<fieldset>
<legend>{t("labels.textAlign")}</legend>
<ButtonIconSelect<TextAlign | false>
group="text-align"
options={[
{
value: "left",
text: t("labels.left"),
icon: <TextAlignLeftIcon theme={appState.theme} />,
},
{
value: "center",
text: t("labels.center"),
icon: <TextAlignCenterIcon theme={appState.theme} />,
},
{
value: "right",
text: t("labels.right"),
icon: <TextAlignRightIcon theme={appState.theme} />,
},
]}
value={getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.textAlign;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.textAlign;
}
return null;
},
appState.currentItemTextAlign,
)}
onChange={(value) => updateData(value)}
/>
</fieldset>
);
},
});
export const actionChangeVerticalAlign = register({
name: "changeVerticalAlign",
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newElement: ExcalidrawTextElement = newElementWith(
oldElement,
{ verticalAlign: value },
);
redrawTextBoundingBox(
newElement,
getContainerElement(oldElement),
appState,
);
return newElement;
}
return oldElement;
},
true,
),
appState: {
...appState,
},
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData }) => {
return (
<fieldset>
<ButtonIconSelect<VerticalAlign | false>
group="text-align"
options={[
{
value: VERTICAL_ALIGN.TOP,
text: t("labels.alignTop"),
icon: <TextAlignTopIcon theme={appState.theme} />,
},
{
value: VERTICAL_ALIGN.MIDDLE,
text: t("labels.centerVertically"),
icon: <TextAlignMiddleIcon theme={appState.theme} />,
},
{
value: VERTICAL_ALIGN.BOTTOM,
text: t("labels.alignBottom"),
icon: <TextAlignBottomIcon theme={appState.theme} />,
},
]}
value={getFormValue(elements, appState, (element) => {
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.textAlign;
return boundTextElement.verticalAlign;
}
return null;
},
appState.currentItemTextAlign,
)}
onChange={(value) => updateData(value)}
/>
</fieldset>
),
})}
onChange={(value) => updateData(value)}
/>
</fieldset>
);
},
});
export const actionChangeSharpness = register({
+71
View File
@@ -0,0 +1,71 @@
import ExcalidrawApp from "../excalidraw-app";
import { t } from "../i18n";
import { CODES } from "../keys";
import { API } from "../tests/helpers/api";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { fireEvent, render, screen } from "../tests/test-utils";
import { copiedStyles } from "./actionStyles";
const { h } = window;
const mouse = new Pointer("mouse");
describe("actionStyles", () => {
beforeEach(async () => {
await render(<ExcalidrawApp />);
});
it("should copy & paste styles via keyboard", () => {
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(20, 20);
UI.clickTool("rectangle");
mouse.down(10, 10);
mouse.up(20, 20);
// Change some styles of second rectangle
UI.clickLabeledElement("Stroke");
UI.clickLabeledElement(t("colors.c92a2a"));
UI.clickLabeledElement("Background");
UI.clickLabeledElement(t("colors.e64980"));
// Fill style
fireEvent.click(screen.getByTitle("Cross-hatch"));
// Stroke width
fireEvent.click(screen.getByTitle("Bold"));
// Stroke style
fireEvent.click(screen.getByTitle("Dotted"));
// Roughness
fireEvent.click(screen.getByTitle("Cartoonist"));
// Opacity
fireEvent.change(screen.getByLabelText("Opacity"), {
target: { value: "60" },
});
mouse.reset();
API.setSelectedElements([h.elements[1]]);
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
Keyboard.codeDown(CODES.C);
});
const secondRect = JSON.parse(copiedStyles);
expect(secondRect.id).toBe(h.elements[1].id);
mouse.reset();
// Paste styles to first rectangle
API.setSelectedElements([h.elements[0]]);
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
Keyboard.codeDown(CODES.V);
});
const firstRect = API.getSelectedElement();
expect(firstRect.id).toBe(h.elements[0].id);
expect(firstRect.strokeColor).toBe("#c92a2a");
expect(firstRect.backgroundColor).toBe("#e64980");
expect(firstRect.fillStyle).toBe("cross-hatch");
expect(firstRect.strokeWidth).toBe(2); // Bold: 2
expect(firstRect.strokeStyle).toBe("dotted");
expect(firstRect.roughness).toBe(2); // Cartoonist: 2
expect(firstRect.opacity).toBe(60);
});
});
+2 -2
View File
@@ -64,8 +64,8 @@ export const actionPasteStyles = register({
});
redrawTextBoundingBox(
element,
getContainerElement(element),
newElement,
getContainerElement(newElement),
appState,
);
}
+2
View File
@@ -8,6 +8,7 @@ export {
export { actionSelectAll } from "./actionSelectAll";
export { actionDuplicateSelection } from "./actionDuplicateSelection";
export {
actionChangeFontColor,
actionChangeStrokeColor,
actionChangeBackgroundColor,
actionChangeStrokeWidth,
@@ -17,6 +18,7 @@ export {
actionChangeFontSize,
actionChangeFontFamily,
actionChangeTextAlign,
actionChangeVerticalAlign,
} from "./actionProperties";
export {
+37 -1
View File
@@ -10,6 +10,31 @@ import {
import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { MODES } from "../constants";
import { trackEvent } from "../analytics";
const trackAction = (
action: Action,
source: "ui" | "keyboard" | "api",
value: any,
) => {
if (action.trackEvent !== false) {
try {
if (action.trackEvent === true) {
trackEvent(
action.name,
source,
typeof value === "number" || typeof value === "string"
? String(value)
: undefined,
);
} else {
action.trackEvent?.(action, source, value);
}
} catch (error) {
console.error("error while logging action:", error);
}
}
};
export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"];
@@ -65,9 +90,15 @@ export class ActionManager implements ActionsManagerInterface {
),
);
if (data.length === 0) {
if (data.length !== 1) {
if (data.length > 1) {
console.warn("Canceling as multiple actions match this shortcut", data);
}
return false;
}
const action = data[0];
const { viewModeEnabled } = this.getAppState();
if (viewModeEnabled) {
if (!Object.values(MODES).includes(data[0].name)) {
@@ -75,6 +106,8 @@ export class ActionManager implements ActionsManagerInterface {
}
}
trackAction(action, "keyboard", null);
event.preventDefault();
this.updater(
data[0].perform(
@@ -96,6 +129,7 @@ export class ActionManager implements ActionsManagerInterface {
this.app,
),
);
trackAction(action, "api", null);
}
/**
@@ -122,6 +156,8 @@ export class ActionManager implements ActionsManagerInterface {
this.app,
),
);
trackAction(action, "ui", formState);
};
return (
+7 -4
View File
@@ -1,8 +1,10 @@
import { t } from "../i18n";
import { isDarwin } from "../keys";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
export type ShortcutName =
export type ShortcutName = SubtypeOf<
ActionName,
| "cut"
| "copy"
| "paste"
@@ -26,7 +28,8 @@ export type ShortcutName =
| "viewMode"
| "flipHorizontal"
| "flipVertical"
| "link";
| "hyperlink"
>;
const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")],
@@ -63,11 +66,11 @@ const shortcutMap: Record<ShortcutName, string[]> = {
flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
link: [getShortcutKey("CtrlOrCmd+K")],
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
};
export const getShortcutFromShortcutName = (name: ShortcutName) => {
const shortcuts = shortcutMap[name];
// if multiple shortcuts availiable, take the first one
// if multiple shortcuts available, take the first one
return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
};
+7 -1
View File
@@ -49,6 +49,7 @@ export type ActionName =
| "gridMode"
| "zenMode"
| "stats"
| "changeFontColor"
| "changeStrokeColor"
| "changeBackgroundColor"
| "changeFillStyle"
@@ -82,6 +83,7 @@ export type ActionName =
| "zoomToSelection"
| "changeFontFamily"
| "changeTextAlign"
| "changeVerticalAlign"
| "toggleFullScreen"
| "toggleShortcuts"
| "group"
@@ -105,7 +107,8 @@ export type ActionName =
| "increaseFontSize"
| "decreaseFontSize"
| "unbindText"
| "link";
| "hyperlink"
| "eraser";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
@@ -136,6 +139,9 @@ export interface Action {
appState: AppState,
) => boolean;
checked?: (appState: Readonly<AppState>) => boolean;
trackEvent?:
| boolean
| ((action: Action, type: "ui" | "keyboard" | "api", value: any) => void);
}
export interface ActionsManagerInterface {
+5 -5
View File
@@ -3,16 +3,16 @@ export const trackEvent =
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
typeof window !== "undefined" &&
window.gtag
? (category: string, name: string, label?: string, value?: number) => {
window.gtag("event", name, {
? (category: string, action: string, label?: string, value?: number) => {
window.gtag("event", action, {
event_category: category,
event_label: label,
value,
});
}
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
? (category: string, name: string, label?: string, value?: number) => {}
: (category: string, name: string, label?: string, value?: number) => {
? (category: string, action: string, label?: string, value?: number) => {}
: (category: string, action: string, label?: string, value?: number) => {
// Uncomment the next line to track locally
// console.info("Track Event", category, name, label, value);
// console.info("Track Event", category, action, label, value);
};
+6
View File
@@ -213,3 +213,9 @@ export const cleanAppStateForExport = (appState: Partial<AppState>) => {
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "server");
};
export const isEraserActive = ({
elementType,
}: {
elementType: AppState["elementType"];
}) => elementType === "eraser";
+8 -3
View File
@@ -1,5 +1,10 @@
import colors from "./colors";
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, ENV } from "./constants";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
ENV,
VERTICAL_ALIGN,
} from "./constants";
import { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random";
@@ -103,7 +108,7 @@ const transposeCells = (cells: string[][]) => {
};
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => {
// Copy/paste from excel, spreadhseets, tsv, csv.
// Copy/paste from excel, spreadsheets, tsv, csv.
// For now we only accept 2 columns with an optional header
// Check for tab separated values
@@ -161,7 +166,7 @@ const commonProps = {
strokeSharpness: "sharp",
strokeStyle: "solid",
strokeWidth: 1,
verticalAlign: "middle",
verticalAlign: VERTICAL_ALIGN.MIDDLE,
} as const;
const getChartDimentions = (spreadsheet: Spreadsheet) => {
+1 -1
View File
@@ -124,7 +124,7 @@ const getSystemClipboard = async (
};
/**
* Attemps to parse clipboard. Prefers system clipboard.
* Attempts to parse clipboard. Prefers system clipboard.
*/
export const parseClipboard = async (
event: ClipboardEvent | null,
+28 -7
View File
@@ -19,6 +19,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
export const SelectedShapeActions = ({
appState,
@@ -29,12 +30,21 @@ export const SelectedShapeActions = ({
appState: AppState;
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
elementType: ExcalidrawElement["type"];
elementType: AppState["elementType"];
}) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState,
);
let isSingleElementBoundContainer = false;
if (
targetElements.length === 2 &&
(hasBoundTextElement(targetElements[0]) ||
hasBoundTextElement(targetElements[1]))
) {
isSingleElementBoundContainer = true;
}
const isEditing = Boolean(appState.editingElement);
const isMobile = useIsMobile();
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
@@ -58,8 +68,15 @@ export const SelectedShapeActions = ({
}
}
const hasOnlyContainersWithBoundText =
targetElements.length > 1 &&
targetElements.every(
(element) => hasBoundTextElement(element) || isBoundToContainer(element),
);
return (
<div className="panelColumn">
{hasOnlyContainersWithBoundText && renderAction("changeFontColor")}
{((hasStrokeColor(elementType) &&
elementType !== "image" &&
commonSelectedType !== "image") ||
@@ -100,6 +117,10 @@ export const SelectedShapeActions = ({
</>
)}
{targetElements.some(
(element) =>
hasBoundTextElement(element) || isBoundToContainer(element),
) && renderAction("changeVerticalAlign")}
{(canHaveArrowheads(elementType) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
<>{renderAction("changeArrowhead")}</>
@@ -117,7 +138,7 @@ export const SelectedShapeActions = ({
</div>
</fieldset>
{targetElements.length > 1 && (
{targetElements.length > 1 && !isSingleElementBoundContainer && (
<fieldset>
<legend>{t("labels.align")}</legend>
<div className="buttonList">
@@ -150,15 +171,15 @@ export const SelectedShapeActions = ({
</div>
</fieldset>
)}
{!isMobile && !isEditing && targetElements.length > 0 && (
{!isEditing && targetElements.length > 0 && (
<fieldset>
<legend>{t("labels.actions")}</legend>
<div className="buttonList">
{renderAction("duplicateSelection")}
{renderAction("deleteSelectedElements")}
{!isMobile && renderAction("duplicateSelection")}
{!isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")}
{renderAction("ungroup")}
{targetElements.length === 1 && renderAction("link")}
{targetElements.length === 1 && renderAction("hyperlink")}
</div>
</fieldset>
)}
@@ -173,7 +194,7 @@ export const ShapesSwitcher = ({
onImageAction,
}: {
canvas: HTMLCanvasElement | null;
elementType: ExcalidrawElement["type"];
elementType: AppState["elementType"];
setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
}) => (
+338 -100
View File
@@ -35,7 +35,7 @@ import { ActionManager } from "../actions/manager";
import { actions } from "../actions/register";
import { ActionResult } from "../actions/types";
import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import { getDefaultAppState, isEraserActive } from "../appState";
import {
copyToClipboard,
parseClipboard,
@@ -69,6 +69,7 @@ import {
TOUCH_CTX_MENU_TIMEOUT,
URL_HASH_KEYS,
URL_QUERY_KEYS,
VERTICAL_ALIGN,
ZOOM_STEP,
} from "../constants";
import { loadFromBlob } from "../data";
@@ -115,11 +116,7 @@ import {
updateBoundElements,
} from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import {
bumpVersion,
mutateElement,
newElementWith,
} from "../element/mutateElement";
import { mutateElement, newElementWith } from "../element/mutateElement";
import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
import {
hasBoundTextElement,
@@ -130,6 +127,7 @@ import {
isInitializedImageElement,
isLinearElement,
isLinearElementType,
isTextBindableContainer,
} from "../element/typeChecks";
import {
ExcalidrawBindableElement,
@@ -143,6 +141,7 @@ import {
ExcalidrawImageElement,
FileId,
NonDeletedExcalidrawElement,
ExcalidrawTextContainer,
} from "../element/types";
import { getCenter, getDistance } from "../gesture";
import {
@@ -163,13 +162,14 @@ import {
shouldRotateWithDiscreteAngle,
isArrowKey,
KEYS,
isAndroid,
} from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer";
import { invalidateShapeForElement } from "../renderer/renderElement";
import {
calculateScrollCenter,
getElementContainingPosition,
getTextBindableContainerAtPosition,
getElementsAtPosition,
getElementsWithinSelection,
getNormalizedZoom,
@@ -212,6 +212,8 @@ import {
tupleToCoors,
viewportCoordsToSceneCoords,
withBatchedUpdates,
wrapEvent,
withBatchedUpdatesThrottled,
} from "../utils";
import ContextMenu, { ContextMenuOption } from "./ContextMenu";
import LayerUI from "./LayerUI";
@@ -238,7 +240,7 @@ import {
bindTextToShapeAfterDuplication,
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElementId,
getBoundTextElement,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import {
@@ -312,6 +314,7 @@ class App extends React.Component<AppProps, AppState> {
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
contextMenuOpen: boolean = false;
lastScenePointer: { x: number; y: number } | null = null;
constructor(props: AppProps) {
super(props);
@@ -415,7 +418,7 @@ class App extends React.Component<AppProps, AppState> {
ref={this.handleCanvasRef}
onContextMenu={this.handleCanvasContextMenu}
onPointerMove={this.handleCanvasPointerMove}
onPointerUp={this.removePointer}
onPointerUp={this.handleCanvasPointerUp}
onPointerCancel={this.removePointer}
onTouchMove={this.handleTouchMove}
onPointerDown={this.handleCanvasPointerDown}
@@ -438,7 +441,7 @@ class App extends React.Component<AppProps, AppState> {
onPointerDown={this.handleCanvasPointerDown}
onDoubleClick={this.handleCanvasDoubleClick}
onPointerMove={this.handleCanvasPointerMove}
onPointerUp={this.removePointer}
onPointerUp={this.handleCanvasPointerUp}
onPointerCancel={this.removePointer}
onTouchMove={this.handleTouchMove}
>
@@ -524,6 +527,7 @@ class App extends React.Component<AppProps, AppState> {
element={selectedElement[0]}
appState={this.state}
setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen}
/>
)}
{this.state.showStats && (
@@ -1041,6 +1045,12 @@ class App extends React.Component<AppProps, AppState> {
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
if (
Object.keys(this.state.selectedElementIds).length &&
isEraserActive(this.state)
) {
this.setState({ elementType: "selection" });
}
// Hide hyperlink popup if shown when element type is not selection
if (
prevState.elementType === "selection" &&
@@ -1272,6 +1282,12 @@ class App extends React.Component<AppProps, AppState> {
}
private onTapStart = (event: TouchEvent) => {
// fix for Apple Pencil Scribble
// On Android, preventing the event would disable contextMenu on tap-hold
if (!isAndroid) {
event.preventDefault();
}
if (!didTapTwice) {
didTapTwice = true;
clearTimeout(tappedTwiceTimer);
@@ -1293,7 +1309,10 @@ class App extends React.Component<AppProps, AppState> {
didTapTwice = false;
clearTimeout(tappedTwiceTimer);
}
event.preventDefault();
if (isAndroid) {
event.preventDefault();
}
if (event.touches.length === 2) {
this.setState({
selectedElementIds: {},
@@ -1302,11 +1321,14 @@ class App extends React.Component<AppProps, AppState> {
};
private onTapEnd = (event: TouchEvent) => {
this.resetContextMenuTimer();
if (event.touches.length > 0) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: this.state.previousSelectedElementIds,
});
} else {
gesture.pointers.clear();
}
};
@@ -1519,12 +1541,8 @@ class App extends React.Component<AppProps, AppState> {
};
removePointer = (event: React.PointerEvent<HTMLElement> | PointerEvent) => {
this.lastPointerUp = event;
// remove touch handler for context menu on touch devices
if (event.pointerType === "touch" && touchTimeout) {
clearTimeout(touchTimeout);
touchTimeout = 0;
invalidateContextMenu = false;
if (touchTimeout) {
this.resetContextMenuTimer();
}
gesture.pointers.delete(event.pointerId);
@@ -1608,9 +1626,6 @@ class App extends React.Component<AppProps, AppState> {
this.files = { ...this.files, ...Object.fromEntries(filesMap) };
// bump versions for elements that reference added files so that
// we/host apps can detect the change, and invalidate the image & shape
// cache
this.scene.getElements().forEach((element) => {
if (
isInitializedImageElement(element) &&
@@ -1618,7 +1633,6 @@ class App extends React.Component<AppProps, AppState> {
) {
this.imageCache.delete(element.fileId);
invalidateShapeForElement(element);
bumpVersion(element);
}
});
this.scene.informMutation();
@@ -1826,7 +1840,11 @@ class App extends React.Component<AppProps, AppState> {
event.preventDefault();
}
if (event.key === KEYS.G || event.key === KEYS.S) {
if (
event.key === KEYS.G ||
event.key === KEYS.S ||
event.key === KEYS.C
) {
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
@@ -1848,6 +1866,9 @@ class App extends React.Component<AppProps, AppState> {
if (event.key === KEYS.S) {
this.setState({ openPopup: "strokeColorPicker" });
}
if (event.key === KEYS.C) {
this.setState({ openPopup: "fontColorPicker" });
}
}
},
);
@@ -1924,7 +1945,7 @@ class App extends React.Component<AppProps, AppState> {
// zoom in at the right location on the touchMove handler already.
// On Macbook, we don't have those events so will zoom in at the
// current location instead.
if (gesture.pointers.size === 2) {
if (gesture.pointers.size >= 2) {
return;
}
@@ -2152,28 +2173,40 @@ class App extends React.Component<AppProps, AppState> {
window.devicePixelRatio,
);
// bind to container when shouldBind is true or
// clicked on center of container
const container =
shouldBind || parentCenterPosition
? getElementContainingPosition(
this.scene.getElements().filter((ele) => !isTextElement(ele)),
sceneX,
sceneY,
)
: null;
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
let container: ExcalidrawTextContainer | null = null;
let existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
);
// consider bounded text element if container present
if (container) {
const boundTextElementId = getBoundTextElementId(container);
if (boundTextElementId) {
existingTextElement = this.scene.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
if (selectedElements.length === 1) {
if (isTextElement(selectedElements[0])) {
existingTextElement = selectedElements[0];
} else if (isTextBindableContainer(selectedElements[0])) {
container = selectedElements[0];
existingTextElement = getBoundTextElement(container);
}
}
existingTextElement =
existingTextElement ?? this.getTextElementAtPosition(sceneX, sceneY);
// bind to container when shouldBind is true or
// clicked on center of container
if (
!container &&
!existingTextElement &&
(shouldBind || parentCenterPosition)
) {
container = getTextBindableContainerAtPosition(
this.scene.getElements().filter((ele) => !isTextElement(ele)),
sceneX,
sceneY,
);
}
if (!existingTextElement && container) {
const fontString = {
fontSize: this.state.currentItemFontSize,
@@ -2221,7 +2254,7 @@ class App extends React.Component<AppProps, AppState> {
? "center"
: this.state.currentItemTextAlign,
verticalAlign: parentCenterPosition
? "middle"
? VERTICAL_ALIGN.MIDDLE
: DEFAULT_VERTICAL_ALIGN,
containerId: container?.id ?? undefined,
groupIds: container?.groupIds ?? [],
@@ -2229,19 +2262,13 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ editingElement: element });
if (existingTextElement) {
// if text element is no longer centered to a container, reset
// verticalAlign to default because it's currently internal-only
if (!parentCenterPosition || element.textAlign !== "center") {
mutateElement(element, { verticalAlign: DEFAULT_VERTICAL_ALIGN });
}
} else {
if (!existingTextElement) {
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted(),
element,
]);
// case: creating new text not centered to parent elemenent → offset Y
// case: creating new text not centered to parent element → offset Y
// so that the text is centered to cursor position
if (!parentCenterPosition) {
mutateElement(element, {
@@ -2363,16 +2390,35 @@ class App extends React.Component<AppProps, AppState> {
}
return (
element.link &&
isPointHittingLinkIcon(element, this.state, [
scenePointer.x,
scenePointer.y,
]) &&
isPointHittingLinkIcon(
element,
this.state,
[scenePointer.x, scenePointer.y],
this.isMobile,
) &&
index <= hitElementIndex
);
});
};
private redirectToLink = () => {
private redirectToLink = (
event: React.PointerEvent<HTMLCanvasElement>,
isTouchScreen: boolean,
) => {
const draggedDistance = distance2d(
this.lastPointerDown!.clientX,
this.lastPointerDown!.clientY,
this.lastPointerUp!.clientX,
this.lastPointerUp!.clientY,
);
if (
!this.hitLinkElement ||
// For touch screen allow dragging threshold else strict check
(isTouchScreen && draggedDistance > DRAGGING_THRESHOLD) ||
(!isTouchScreen && draggedDistance !== 0)
) {
return;
}
const lastPointerDownCoords = viewportCoordsToSceneCoords(
this.lastPointerDown!,
this.state,
@@ -2381,41 +2427,43 @@ class App extends React.Component<AppProps, AppState> {
this.hitLinkElement!,
this.state,
[lastPointerDownCoords.x, lastPointerDownCoords.y],
this.isMobile,
);
const lastPointerUpCoords = viewportCoordsToSceneCoords(
this.lastPointerUp!,
this.state,
);
const LastPointerUpHittingLinkIcon = isPointHittingLinkIcon(
const lastPointerUpHittingLinkIcon = isPointHittingLinkIcon(
this.hitLinkElement!,
this.state,
[lastPointerUpCoords.x, lastPointerUpCoords.y],
this.isMobile,
);
if (lastPointerDownHittingLinkIcon && LastPointerUpHittingLinkIcon) {
const url = this.hitLinkElement?.link;
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
const url = this.hitLinkElement.link;
if (url) {
const target = isLocalLink(url) ? "_self" : "_blank";
const newWindow = window.open(undefined, target);
// https://mathiasbynens.github.io/rel-noopener/
if (newWindow) {
newWindow.opener = null;
newWindow.location = normalizeLink(url);
let customEvent;
if (this.props.onLinkOpen) {
customEvent = wrapEvent(EVENT.EXCALIDRAW_LINK, event.nativeEvent);
this.props.onLinkOpen(this.hitLinkElement, customEvent);
}
if (!customEvent?.defaultPrevented) {
const target = isLocalLink(url) ? "_self" : "_blank";
const newWindow = window.open(undefined, target);
// https://mathiasbynens.github.io/rel-noopener/
if (newWindow) {
newWindow.opener = null;
newWindow.location = normalizeLink(url);
}
}
}
}
};
private attachLinkListener = () => {
this.canvas?.addEventListener("click", this.redirectToLink);
};
private detachLinkListener = () => {
this.canvas?.removeEventListener("click", this.redirectToLink);
};
private handleCanvasPointerMove = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
if (gesture.pointers.has(event.pointerId)) {
gesture.pointers.set(event.pointerId, {
x: event.clientX,
@@ -2589,7 +2637,8 @@ class App extends React.Component<AppProps, AppState> {
if (
hasDeselectedButton ||
(this.state.elementType !== "selection" &&
this.state.elementType !== "text")
this.state.elementType !== "text" &&
this.state.elementType !== "eraser")
) {
return;
}
@@ -2654,10 +2703,8 @@ class App extends React.Component<AppProps, AppState> {
) {
setCursor(this.canvas, CURSOR_TYPE.POINTER);
showHyperlinkTooltip(this.hitLinkElement, this.state);
this.attachLinkListener();
} else {
hideHyperlinkToolip();
this.detachLinkListener();
if (
hitElement &&
hitElement.link &&
@@ -2666,8 +2713,9 @@ class App extends React.Component<AppProps, AppState> {
!this.state.showHyperlinkPopup
) {
this.setState({ showHyperlinkPopup: "info" });
}
if (this.state.elementType === "text") {
} else if (isEraserActive(this.state)) {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.elementType === "text") {
setCursor(
this.canvas,
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
@@ -2708,6 +2756,80 @@ class App extends React.Component<AppProps, AppState> {
}
};
private handleEraser = (
event: PointerEvent,
pointerDownState: PointerDownState,
scenePointer: { x: number; y: number },
) => {
const updateElementIds = (elements: ExcalidrawElement[]) => {
elements.forEach((element) => {
idsToUpdate.push(element.id);
if (event.altKey) {
if (pointerDownState.elementIdsToErase[element.id]) {
pointerDownState.elementIdsToErase[element.id] = false;
}
} else {
pointerDownState.elementIdsToErase[element.id] = true;
}
});
};
const idsToUpdate: Array<string> = [];
const distance = distance2d(
pointerDownState.lastCoords.x,
pointerDownState.lastCoords.y,
scenePointer.x,
scenePointer.y,
);
const threshold = 10 / this.state.zoom.value;
const point = { ...pointerDownState.lastCoords };
let samplingInterval = 0;
while (samplingInterval <= distance) {
const hitElements = this.getElementsAtPosition(point.x, point.y);
updateElementIds(hitElements);
// Exit since we reached current point
if (samplingInterval === distance) {
break;
}
// Calculate next point in the line at a distance of sampling interval
samplingInterval = Math.min(samplingInterval + threshold, distance);
const distanceRatio = samplingInterval / distance;
const nextX =
(1 - distanceRatio) * point.x + distanceRatio * scenePointer.x;
const nextY =
(1 - distanceRatio) * point.y + distanceRatio * scenePointer.y;
point.x = nextX;
point.y = nextY;
}
const elements = this.scene.getElements().map((ele) => {
const id =
isBoundToContainer(ele) && idsToUpdate.includes(ele.containerId)
? ele.containerId
: ele.id;
if (idsToUpdate.includes(id)) {
if (event.altKey) {
if (pointerDownState.elementIdsToErase[id] === false) {
return newElementWith(ele, {
opacity: this.state.currentItemOpacity,
});
}
} else {
return newElementWith(ele, { opacity: 20 });
}
}
return ele;
});
this.scene.replaceAllElements(elements);
pointerDownState.lastCoords.x = scenePointer.x;
pointerDownState.lastCoords.y = scenePointer.y;
};
// set touch moving for mobile context menu
private handleTouchMove = (event: React.TouchEvent<HTMLCanvasElement>) => {
invalidateContextMenu = true;
@@ -2740,6 +2862,7 @@ class App extends React.Component<AppProps, AppState> {
if (isPanning) {
return;
}
this.lastPointerDown = event;
this.setState({
lastPointerDownWith: event.pointerType,
@@ -2832,7 +2955,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.elementType,
pointerDownState,
);
} else {
} else if (this.state.elementType !== "eraser") {
this.createGenericElementOnPointerDown(
this.state.elementType,
pointerDownState,
@@ -2862,6 +2985,38 @@ class App extends React.Component<AppProps, AppState> {
}
};
private handleCanvasPointerUp = (
event: React.PointerEvent<HTMLCanvasElement>,
) => {
this.lastPointerUp = event;
const isTouchScreen = ["pen", "touch"].includes(event.pointerType);
if (isTouchScreen) {
const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
this.state,
);
const hitElement = this.getElementAtPosition(
scenePointer.x,
scenePointer.y,
);
this.hitLinkElement = this.getElementLinkAtPosition(
scenePointer,
hitElement,
);
}
if (isEraserActive(this.state)) {
}
if (
this.hitLinkElement &&
!this.state.selectedElementIds[this.hitLinkElement.id]
) {
this.redirectToLink(event, isTouchScreen);
}
this.removePointer(event);
};
private maybeOpenContextMenuAfterPointerDownOnTouchDevices = (
event: React.PointerEvent<HTMLCanvasElement>,
): void => {
@@ -2887,6 +3042,12 @@ class App extends React.Component<AppProps, AppState> {
}
};
private resetContextMenuTimer = () => {
clearTimeout(touchTimeout);
touchTimeout = 0;
invalidateContextMenu = false;
};
private maybeCleanupAfterMissingPointerUp(
event: React.PointerEvent<HTMLCanvasElement>,
): void {
@@ -2921,7 +3082,7 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
let { clientX: lastX, clientY: lastY } = event;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
const deltaX = lastX - event.clientX;
const deltaY = lastY - event.clientY;
lastX = event.clientX;
@@ -2947,7 +3108,7 @@ class App extends React.Component<AppProps, AppState> {
/*
* Reenable next paste in case of disabled middle click paste for
* any reason:
* - rigth click paste
* - right click paste
* - empty clipboard
*/
const enableNextPaste = () => {
@@ -2984,6 +3145,7 @@ class App extends React.Component<AppProps, AppState> {
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, teardown);
window.removeEventListener(EVENT.BLUR, teardown);
onPointerMove.flush();
}),
);
window.addEventListener(EVENT.BLUR, teardown);
@@ -3070,6 +3232,7 @@ class App extends React.Component<AppProps, AppState> {
boxSelection: {
hasOccurred: false,
},
elementIdsToErase: {},
};
}
@@ -3086,7 +3249,7 @@ class App extends React.Component<AppProps, AppState> {
isDraggingScrollBar = true;
pointerDownState.lastCoords.x = event.clientX;
pointerDownState.lastCoords.y = event.clientY;
const onPointerMove = withBatchedUpdates((event: PointerEvent) => {
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
@@ -3105,6 +3268,7 @@ class App extends React.Component<AppProps, AppState> {
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener(EVENT.POINTER_MOVE, onPointerMove);
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
onPointerMove.flush();
});
lastPointerUp = onPointerUp;
@@ -3213,10 +3377,12 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.hit.element) {
// Early return if pointer is hitting link icon
if (
isPointHittingLinkIcon(pointerDownState.hit.element, this.state, [
pointerDownState.origin.x,
pointerDownState.origin.y,
])
isPointHittingLinkIcon(
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
this.isMobile,
)
) {
return false;
}
@@ -3640,8 +3806,8 @@ class App extends React.Component<AppProps, AppState> {
private onPointerMoveFromPointerDownHandler(
pointerDownState: PointerDownState,
): (event: PointerEvent) => void {
return withBatchedUpdates((event: PointerEvent) => {
) {
return withBatchedUpdatesThrottled((event: PointerEvent) => {
// We need to initialize dragOffsetXY only after we've updated
// `state.selectedElementIds` on pointerDown. Doing it here in pointerMove
// event handler should hopefully ensure we're already working with
@@ -3655,7 +3821,6 @@ class App extends React.Component<AppProps, AppState> {
),
);
}
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
@@ -3666,6 +3831,12 @@ class App extends React.Component<AppProps, AppState> {
}
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords);
return;
}
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
@@ -3852,14 +4023,20 @@ class App extends React.Component<AppProps, AppState> {
const dx = pointerCoords.x - draggingElement.x;
const dy = pointerCoords.y - draggingElement.y;
const pressures = draggingElement.simulatePressure
? draggingElement.pressures
: [...draggingElement.pressures, event.pressure];
const lastPoint = points.length > 0 && points[points.length - 1];
const discardPoint =
lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
mutateElement(draggingElement, {
points: [...points, [dx, dy]],
pressures,
});
if (!discardPoint) {
const pressures = draggingElement.simulatePressure
? draggingElement.pressures
: [...draggingElement.pressures, event.pressure];
mutateElement(draggingElement, {
points: [...points, [dx, dy]],
pressures,
});
}
} else if (isLinearElement(draggingElement)) {
pointerDownState.drag.hasOccurred = true;
const points = draggingElement.points;
@@ -4012,7 +4189,6 @@ class App extends React.Component<AppProps, AppState> {
isResizing,
isRotating,
} = this.state;
this.setState({
isResizing: false,
isRotating: false,
@@ -4056,6 +4232,10 @@ class App extends React.Component<AppProps, AppState> {
lastPointerUp = null;
if (pointerDownState.eventListeners.onMove) {
pointerDownState.eventListeners.onMove.flush();
}
window.removeEventListener(
EVENT.POINTER_MOVE,
pointerDownState.eventListeners.onMove!,
@@ -4229,6 +4409,33 @@ class App extends React.Component<AppProps, AppState> {
// Code below handles selection when element(s) weren't
// drag or added to selection on pointer down phase.
const hitElement = pointerDownState.hit.element;
if (isEraserActive(this.state)) {
const draggedDistance = distance2d(
this.lastPointerDown!.clientX,
this.lastPointerDown!.clientY,
this.lastPointerUp!.clientX,
this.lastPointerUp!.clientY,
);
if (draggedDistance === 0) {
const scenePointer = viewportCoordsToSceneCoords(
{
clientX: this.lastPointerUp!.clientX,
clientY: this.lastPointerUp!.clientY,
},
this.state,
);
const hitElement = this.getElementAtPosition(
scenePointer.x,
scenePointer.y,
);
pointerDownState.hit.element = hitElement;
}
this.eraseElements(pointerDownState);
return;
}
if (
hitElement &&
!pointerDownState.drag.hasOccurred &&
@@ -4368,6 +4575,27 @@ class App extends React.Component<AppProps, AppState> {
});
}
private eraseElements = (pointerDownState: PointerDownState) => {
const hitElement = pointerDownState.hit.element;
const elements = this.scene.getElements().map((ele) => {
if (pointerDownState.elementIdsToErase[ele.id]) {
return newElementWith(ele, { isDeleted: true });
} else if (hitElement && ele.id === hitElement.id) {
return newElementWith(ele, { isDeleted: true });
} else if (
isBoundToContainer(ele) &&
(pointerDownState.elementIdsToErase[ele.containerId] ||
(hitElement && ele.containerId === hitElement.id))
) {
return newElementWith(ele, { isDeleted: true });
}
return ele;
});
this.history.resumeRecording();
this.scene.replaceAllElements(elements);
};
private initializeImage = async ({
imageFile,
imageElement: _imageElement,
@@ -4669,7 +4897,7 @@ class App extends React.Component<AppProps, AppState> {
const width = height * (image.naturalWidth / image.naturalHeight);
// add current imageElement width/height to account for previous centering
// of the placholder image
// of the placeholder image
const x = imageElement.x + imageElement.width / 2 - width / 2;
const y = imageElement.y + imageElement.height / 2 - height / 2;
@@ -4850,7 +5078,7 @@ class App extends React.Component<AppProps, AppState> {
private handleAppOnDrop = async (event: React.DragEvent<HTMLDivElement>) => {
try {
const file = event.dataTransfer.files[0];
const file = event.dataTransfer.files.item(0);
if (isSupportedImageFile(file)) {
// first attempt to decode scene from the image if it's embedded
@@ -4926,7 +5154,7 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const file = event.dataTransfer?.files[0];
const file = event.dataTransfer?.files.item(0);
if (
file?.type === MIME_TYPES.excalidrawlib ||
file?.name?.endsWith(".excalidrawlib")
@@ -4942,7 +5170,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isLoading: false, errorMessage: error.message }),
);
// default: assume an Excalidraw file regardless of extension/MimeType
} else {
} else if (file) {
this.setState({ isLoading: true });
if (nativeFileSystemSupported) {
try {
@@ -4981,6 +5209,16 @@ class App extends React.Component<AppProps, AppState> {
) => {
event.preventDefault();
if (
(event.nativeEvent.pointerType === "touch" ||
(event.nativeEvent.pointerType === "pen" &&
// always allow if user uses a pen secondary button
event.button !== POINTER_BUTTON.SECONDARY)) &&
this.state.elementType !== "selection"
) {
return;
}
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const element = this.getElementAtPosition(x, y, { preferSelected: true });
@@ -5354,7 +5592,7 @@ class App extends React.Component<AppProps, AppState> {
canvas: HTMLCanvasElement | null,
scale: number,
) {
const elementClickedInside = getElementContainingPosition(
const elementClickedInside = getTextBindableContainerAtPosition(
this.scene
.getElementsIncludingDeleted()
.filter((element) => !isTextElement(element)),
+2 -1
View File
@@ -7,7 +7,7 @@ export const ButtonIconSelect = <T extends Object>({
onChange,
group,
}: {
options: { value: T; text: string; icon: JSX.Element }[];
options: { value: T; text: string; icon: JSX.Element; testId?: string }[];
value: T | null;
onChange: (value: T) => void;
group: string;
@@ -24,6 +24,7 @@ export const ButtonIconSelect = <T extends Object>({
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value}
data-testid={option.testId}
/>
{option.icon}
</label>
+23 -2
View File
@@ -46,7 +46,7 @@
top: -11px;
}
.color-picker-content {
.color-picker-content--default {
padding: 0.5rem;
display: grid;
grid-template-columns: repeat(5, auto);
@@ -59,6 +59,26 @@
}
}
.color-picker-content--canvas {
display: flex;
flex-direction: column;
padding: 0.25rem;
&-title {
color: $oc-gray-6;
font-size: 12px;
padding: 0 0.25rem;
}
&-colors {
padding: 0.5rem 0;
.color-picker-swatch {
margin: 0 0.25rem;
}
}
}
.color-picker-content .color-input-container {
grid-column: 1 / span 5;
}
@@ -235,7 +255,8 @@
color: #aaa;
}
.color-picker-type-elementStroke .color-picker-keybinding {
.color-picker-type-elementStroke .color-picker-keybinding,
.color-picker-type-elementFontColor .color-picker-keybinding {
color: #d4d4d4;
}
+167 -46
View File
@@ -7,6 +7,53 @@ import { isArrowKey, KEYS } from "../keys";
import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils";
import colors from "../colors";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
const MAX_CUSTOM_COLORS = 5;
const MAX_DEFAULT_COLORS = 15;
export const getCustomColors = (
elements: readonly ExcalidrawElement[],
type: "elementBackground" | "elementStroke",
) => {
const customColors: string[] = [];
const updatedElements = elements
.filter((element) => !element.isDeleted)
.sort((ele1, ele2) => ele2.updated - ele1.updated);
let index = 0;
const elementColorTypeMap = {
elementBackground: "backgroundColor",
elementStroke: "strokeColor",
};
const colorType = elementColorTypeMap[type] as
| "backgroundColor"
| "strokeColor";
while (
index < updatedElements.length &&
customColors.length < MAX_CUSTOM_COLORS
) {
const element = updatedElements[index];
if (
customColors.length < MAX_CUSTOM_COLORS &&
isCustomColor(element[colorType], type) &&
!customColors.includes(element[colorType])
) {
customColors.push(element[colorType]);
}
index++;
}
return customColors;
};
const isCustomColor = (
color: string,
type: "elementBackground" | "elementStroke",
) => {
return !colors[type].includes(color);
};
const isValidColor = (color: string) => {
const style = new Option().style;
@@ -35,6 +82,7 @@ const keyBindings = [
["1", "2", "3", "4", "5"],
["q", "w", "e", "r", "t"],
["a", "s", "d", "f", "g"],
["z", "x", "c", "v", "b"],
].flat();
const Picker = ({
@@ -45,6 +93,7 @@ const Picker = ({
label,
showInput = true,
type,
elements,
}: {
colors: string[];
color: string | null;
@@ -52,12 +101,25 @@ const Picker = ({
onClose: () => void;
label: string;
showInput: boolean;
type: "canvasBackground" | "elementBackground" | "elementStroke";
type:
| "canvasBackground"
| "elementBackground"
| "elementStroke"
| "elementFontColor";
elements: readonly ExcalidrawElement[];
}) => {
const firstItem = React.useRef<HTMLButtonElement>();
const activeItem = React.useRef<HTMLButtonElement>();
const gallery = React.useRef<HTMLDivElement>();
const colorInput = React.useRef<HTMLInputElement>();
const colorType = type === "elementFontColor" ? "elementStroke" : type;
const [customColors] = React.useState(() => {
if (colorType === "canvasBackground") {
return [];
}
return getCustomColors(elements, colorType);
});
React.useEffect(() => {
// After the component is first mounted focus on first input
@@ -85,23 +147,42 @@ const Picker = ({
} else if (isArrowKey(event.key)) {
const { activeElement } = document;
const isRTL = getLanguage().rtl;
const index = Array.prototype.indexOf.call(
gallery!.current!.children,
let isCustom = false;
let index = Array.prototype.indexOf.call(
gallery!.current!.querySelector(".color-picker-content--default")!
.children,
activeElement,
);
if (index === -1) {
index = Array.prototype.indexOf.call(
gallery!.current!.querySelector(
".color-picker-content--canvas-colors",
)!.children,
activeElement,
);
if (index !== -1) {
isCustom = true;
}
}
const parentSelector = isCustom
? gallery!.current!.querySelector(
".color-picker-content--canvas-colors",
)!
: gallery!.current!.querySelector(".color-picker-content--default")!;
if (index !== -1) {
const length = gallery!.current!.children.length - (showInput ? 1 : 0);
const length = parentSelector!.children.length - (showInput ? 1 : 0);
const nextIndex =
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
? (index + 1) % length
: event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
? (length + index - 1) % length
: event.key === KEYS.ARROW_DOWN
: !isCustom && event.key === KEYS.ARROW_DOWN
? (index + 5) % length
: event.key === KEYS.ARROW_UP
: !isCustom && event.key === KEYS.ARROW_UP
? (length + index - 5) % length
: index;
(gallery!.current!.children![nextIndex] as any).focus();
(parentSelector!.children![nextIndex] as HTMLElement)?.focus();
}
event.preventDefault();
} else if (
@@ -109,7 +190,15 @@ const Picker = ({
!isWritableElement(event.target)
) {
const index = keyBindings.indexOf(event.key.toLowerCase());
(gallery!.current!.children![index] as any).focus();
const isCustom = index >= MAX_DEFAULT_COLORS;
const parentSelector = isCustom
? gallery!.current!.querySelector(
".color-picker-content--canvas-colors",
)!
: gallery!.current!.querySelector(".color-picker-content--default")!;
const actualIndex = isCustom ? index - MAX_DEFAULT_COLORS : index;
(parentSelector!.children![actualIndex] as HTMLElement)?.focus();
event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
event.preventDefault();
@@ -119,6 +208,50 @@ const Picker = ({
event.stopPropagation();
};
const renderColors = (colors: Array<string>, custom: boolean = false) => {
return colors.map((_color, i) => {
const _colorWithoutHash = _color.replace("#", "");
const keyBinding = custom
? keyBindings[i + MAX_DEFAULT_COLORS]
: keyBindings[i];
const label = custom
? _colorWithoutHash
: t(`colors.${_colorWithoutHash}`);
return (
<button
className="color-picker-swatch"
onClick={(event) => {
(event.currentTarget as HTMLButtonElement).focus();
onChange(_color);
}}
title={`${label}${
!isTransparent(_color) ? ` (${_color})` : ""
} ${keyBinding.toUpperCase()}`}
aria-label={label}
aria-keyshortcuts={keyBindings[i]}
style={{ color: _color }}
key={_color}
ref={(el) => {
if (!custom && el && i === 0) {
firstItem.current = el;
}
if (el && _color === color) {
activeItem.current = el;
}
}}
onFocus={() => {
onChange(_color);
}}
>
{isTransparent(_color) ? (
<div className="color-picker-transparent"></div>
) : undefined}
<span className="color-picker-keybinding">{keyBinding}</span>
</button>
);
});
};
return (
<div
className={`color-picker color-picker-type-${type}`}
@@ -138,41 +271,20 @@ const Picker = ({
}}
tabIndex={0}
>
{colors.map((_color, i) => {
const _colorWithoutHash = _color.replace("#", "");
return (
<button
className="color-picker-swatch"
onClick={(event) => {
(event.currentTarget as HTMLButtonElement).focus();
onChange(_color);
}}
title={`${t(`colors.${_colorWithoutHash}`)}${
!isTransparent(_color) ? ` (${_color})` : ""
} ${keyBindings[i].toUpperCase()}`}
aria-label={t(`colors.${_colorWithoutHash}`)}
aria-keyshortcuts={keyBindings[i]}
style={{ color: _color }}
key={_color}
ref={(el) => {
if (el && i === 0) {
firstItem.current = el;
}
if (el && _color === color) {
activeItem.current = el;
}
}}
onFocus={() => {
onChange(_color);
}}
>
{isTransparent(_color) ? (
<div className="color-picker-transparent"></div>
) : undefined}
<span className="color-picker-keybinding">{keyBindings[i]}</span>
</button>
);
})}
<div className="color-picker-content--default">
{renderColors(colors)}
</div>
{!!customColors.length && (
<div className="color-picker-content--canvas">
<span className="color-picker-content--canvas-title">
{t("labels.canvasColors")}
</span>
<div className="color-picker-content--canvas-colors">
{renderColors(customColors, true)}
</div>
</div>
)}
{showInput && (
<ColorInput
color={color}
@@ -246,16 +358,24 @@ export const ColorPicker = ({
label,
isActive,
setActive,
elements,
appState,
}: {
type: "canvasBackground" | "elementBackground" | "elementStroke";
type:
| "canvasBackground"
| "elementBackground"
| "elementStroke"
| "elementFontColor";
color: string | null;
onChange: (color: string) => void;
label: string;
isActive: boolean;
setActive: (active: boolean) => void;
elements: readonly ExcalidrawElement[];
appState: AppState;
}) => {
const pickerButton = React.useRef<HTMLButtonElement>(null);
const colorType = type === "elementFontColor" ? "elementStroke" : type;
return (
<div>
<div className="color-picker-control-container">
@@ -282,7 +402,7 @@ export const ColorPicker = ({
}
>
<Picker
colors={colors[type]}
colors={colors[colorType]}
color={color || null}
onChange={(changedColor) => {
onChange(changedColor);
@@ -294,6 +414,7 @@ export const ColorPicker = ({
label={label}
showInput={false}
type={type}
elements={elements}
/>
</Popover>
) : null}
+4
View File
@@ -11,6 +11,7 @@ import {
isTextElement,
} from "../element/typeChecks";
import { getShortcutKey } from "../utils";
import { isEraserActive } from "../appState";
interface HintViewerProps {
appState: AppState;
@@ -22,6 +23,9 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
if (isEraserActive(appState)) {
return t("hints.eraserRevert");
}
if (elementType === "arrow" || elementType === "line") {
if (!multiMode) {
return t("hints.linearElement");
+9 -1
View File
@@ -238,7 +238,7 @@ const LayerUI = ({
className={CLASSES.SHAPE_ACTIONS_MENU}
padding={2}
style={{
// we want to make sure this doesn't overflow so substracting 200
// we want to make sure this doesn't overflow so subtracting 200
// which is approximately height of zoom footer and top left menu items with some buffer
// if active file name is displayed, subtracting 248 to account for its height
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
@@ -428,6 +428,14 @@ const LayerUI = ({
{actionManager.renderAction("redo", { size: "small" })}
</div>
)}
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</Section>
</Stack.Col>
</div>
+11 -1
View File
@@ -8,7 +8,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
import { FixedSideContainer } from "./FixedSideContainer";
import { Island } from "./Island";
import { HintViewer } from "./HintViewer";
import { calculateScrollCenter } from "../scene";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section";
import CollabButton from "./CollabButton";
@@ -113,6 +113,12 @@ export const MobileMenu = ({
};
const renderAppToolbar = () => {
// Render eraser conditionally in mobile
const showEraser =
!appState.viewModeEnabled &&
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
if (viewModeEnabled) {
return (
<div className="App-toolbar-content">
@@ -120,12 +126,16 @@ export const MobileMenu = ({
</div>
);
}
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")}
{actionManager.renderAction("redo")}
{showEraser && actionManager.renderAction("eraser")}
{actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection",
)}
+9 -2
View File
@@ -49,6 +49,7 @@
.ToolIcon__hidden {
box-shadow: none !important;
background-color: transparent !important;
pointer-events: none !important;
}
.ToolIcon.ToolIcon__lock {
@@ -86,8 +87,14 @@
.ToolIcon {
&:hover {
--icon-fill-color: var(--color-primary-chubb);
--keybinding-color: var(--color-primary-chubb);
--icon-fill-color: var(
--color-primary-contrast-offset,
var(--color-primary)
);
--keybinding-color: var(
--color-primary-contrast-offset,
var(--color-primary)
);
}
&:active {
--icon-fill-color: #{$oc-gray-9};
+38
View File
@@ -885,6 +885,40 @@ export const TextAlignRightIcon = React.memo(({ theme }: { theme: Theme }) =>
),
);
export const TextAlignTopIcon = React.memo(({ theme }: { theme: Theme }) =>
createIcon(
<path
d="m16,132l416,0c8.837,0 16,-7.163 16,-16l0,-40c0,-8.837 -7.163,-16 -16,-16l-416,0c-8.837,0 -16,7.163 -16,16l0,40c0,8.837 7.163,16 16,16zm0,160l416,0c8.837,0 16,-7.163 16,-16l0,-40c0,-8.837 -7.163,-16 -16,-16l-416,0c-8.837,0 -16,7.163 -16,16l0,40c0,8.837 7.163,16 16,16z"
fill={iconFillColor(theme)}
strokeLinecap="round"
/>,
{ width: 448, height: 512 },
),
);
export const TextAlignBottomIcon = React.memo(({ theme }: { theme: Theme }) =>
createIcon(
<path
d="M16,292L432,292C440.837,292 448,284.837 448,276L448,236C448,227.163 440.837,220 432,220L16,220C7.163,220 0,227.163 0,236L0,276C0,284.837 7.163,292 16,292ZM16,452L432,452C440.837,452 448,444.837 448,436L448,396C448,387.163 440.837,380 432,380L16,380C7.163,380 0,387.163 0,396L0,436C0,444.837 7.163,452 16,452Z"
fill={iconFillColor(theme)}
strokeLinecap="round"
/>,
{ width: 448, height: 512 },
),
);
export const TextAlignMiddleIcon = React.memo(({ theme }: { theme: Theme }) =>
createIcon(
<path
transform="matrix(1,0,0,1,0,80)"
d="M16,132L432,132C440.837,132 448,124.837 448,116L448,76C448,67.163 440.837,60 432,60L16,60C7.163,60 0,67.163 0,76L0,116C0,124.837 7.163,132 16,132ZM16,292L432,292C440.837,292 448,284.837 448,276L448,236C448,227.163 440.837,220 432,220L16,220C7.163,220 0,227.163 0,236L0,276C0,284.837 7.163,292 16,292Z"
fill={iconFillColor(theme)}
strokeLinecap="round"
/>,
{ width: 448, height: 512 },
),
);
export const publishIcon = createIcon(
<path
d="M537.6 226.6c4.1-10.7 6.4-22.4 6.4-34.6 0-53-43-96-96-96-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32c-88.4 0-160 71.6-160 160 0 2.7.1 5.4.2 8.1C40.2 219.8 0 273.2 0 336c0 79.5 64.5 144 144 144h368c70.7 0 128-57.3 128-128 0-61.9-44-113.6-102.4-125.4zM393.4 288H328v112c0 8.8-7.2 16-16 16h-48c-8.8 0-16-7.2-16-16V288h-65.4c-14.3 0-21.4-17.2-11.3-27.3l105.4-105.4c6.2-6.2 16.4-6.2 22.6 0l105.4 105.4c10.1 10.1 2.9 27.3-11.3 27.3z"
@@ -900,3 +934,7 @@ export const editIcon = createIcon(
></path>,
{ width: 640, height: 512 },
);
export const eraser = createIcon(
<path d="M480 416C497.7 416 512 430.3 512 448C512 465.7 497.7 480 480 480H150.6C133.7 480 117.4 473.3 105.4 461.3L25.37 381.3C.3786 356.3 .3786 315.7 25.37 290.7L258.7 57.37C283.7 32.38 324.3 32.38 349.3 57.37L486.6 194.7C511.6 219.7 511.6 260.3 486.6 285.3L355.9 416H480zM265.4 416L332.7 348.7L195.3 211.3L70.63 336L150.6 416L265.4 416z" />,
);
+9 -1
View File
@@ -24,7 +24,7 @@ export const POINTER_BUTTON = {
WHEEL: 1,
SECONDARY: 2,
TOUCH: -1,
};
} as const;
export enum EVENT {
COPY = "copy",
@@ -52,6 +52,8 @@ export enum EVENT {
HASHCHANGE = "hashchange",
VISIBILITY_CHANGE = "visibilitychange",
SCROLL = "scroll",
// custom events
EXCALIDRAW_LINK = "excalidraw-link",
}
export const ENV = {
@@ -180,3 +182,9 @@ export const VERSIONS = {
} as const;
export const BOUND_TEXT_PADDING = 5;
export const VERTICAL_ALIGN = {
TOP: "top",
MIDDLE: "middle",
BOTTOM: "bottom",
};
+12 -1
View File
@@ -290,6 +290,16 @@
width: 100%;
box-sizing: border-box;
.eraser {
&.ToolIcon:hover {
--icon-fill-color: #fff;
--keybinding-color: #fff;
}
&.active {
background-color: var(--color-primary);
}
}
}
.App-toolbar-content {
@@ -467,7 +477,8 @@
font-family: var(--ui-font);
}
.undo-redo-buttons {
.undo-redo-buttons,
.eraser-buttons {
display: grid;
grid-auto-flow: column;
gap: 0.4em;
-2
View File
@@ -38,7 +38,6 @@
--text-primary-color: #{$oc-gray-8};
--color-primary: #6965db;
--color-primary-chubb: #625ee0; // to offset Chubb illusion
--color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1;
--color-primary-light: #e2e1fc;
@@ -85,7 +84,6 @@
--text-primary-color: #{$oc-gray-4};
--color-primary: #5650f0;
--color-primary-chubb: #726dff; // to offset Chubb illusion
--color-primary-darker: #4b46d8;
--color-primary-darkest: #3e39be;
--color-primary-light: #3f3d64;
+3 -2
View File
@@ -31,8 +31,8 @@ type RestoredAppState = Omit<
>;
export const AllowedExcalidrawElementTypes: Record<
ExcalidrawElement["type"],
true
AppState["elementType"],
boolean
> = {
selection: true,
text: true,
@@ -43,6 +43,7 @@ export const AllowedExcalidrawElementTypes: Record<
image: true,
arrow: true,
freedraw: true,
eraser: false,
};
export type RestoredDataState = {
+47 -11
View File
@@ -1,8 +1,9 @@
import { AppState, Point } from "../types";
import { AppState, ExcalidrawProps, Point } from "../types";
import {
getShortcutKey,
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
wrapEvent,
} from "../utils";
import { mutateElement } from "./mutateElement";
import { NonDeletedExcalidrawElement } from "./types";
@@ -30,6 +31,7 @@ import { isPointHittingElementBoundingBox } from "./collision";
import { getElementAbsoluteCoords } from "./";
import "./Hyperlink.scss";
import { trackEvent } from "../analytics";
const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85;
@@ -48,10 +50,12 @@ export const Hyperlink = ({
element,
appState,
setAppState,
onLinkOpen,
}: {
element: NonDeletedExcalidrawElement;
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
onLinkOpen: ExcalidrawProps["onLinkOpen"];
}) => {
const linkVal = element.link || "";
@@ -66,6 +70,10 @@ export const Hyperlink = ({
const link = normalizeLink(inputRef.current.value);
if (!element.link && link) {
trackEvent("hyperlink", "create");
}
mutateElement(element, { link });
setAppState({ showHyperlinkPopup: "info" });
}, [element, setAppState]);
@@ -105,6 +113,7 @@ export const Hyperlink = ({
}, [appState, element, isEditing, setAppState]);
const handleRemove = useCallback(() => {
trackEvent("hyperlink", "delete");
mutateElement(element, { link: null });
if (isEditing) {
inputRef.current!.value = "";
@@ -113,13 +122,15 @@ export const Hyperlink = ({
}, [setAppState, element, isEditing]);
const onEdit = () => {
trackEvent("hyperlink", "edit", "popup-ui");
setAppState({ showHyperlinkPopup: "editor" });
};
const { x, y } = getCoordsForPopover(element, appState);
if (
appState.draggingElement ||
appState.resizingElement ||
appState.isRotating
appState.isRotating ||
appState.openMenu
) {
return null;
}
@@ -159,6 +170,18 @@ export const Hyperlink = ({
"d-none": isEditing,
})}
target={isLocalLink(element.link) ? "_self" : "_blank"}
onClick={(event) => {
if (element.link && onLinkOpen) {
const customEvent = wrapEvent(
EVENT.EXCALIDRAW_LINK,
event.nativeEvent,
);
onLinkOpen(element, customEvent);
if (customEvent.defaultPrevented) {
event.preventDefault();
}
}
}}
rel="noopener noreferrer"
>
{element.link}
@@ -197,8 +220,9 @@ const getCoordsForPopover = (
element: NonDeletedExcalidrawElement,
appState: AppState,
) => {
const [x1, y1] = getElementAbsoluteCoords(element);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: element.x + element.width / 2, sceneY: element.y },
{ sceneX: x1 + element.width / 2, sceneY: y1 },
appState,
);
const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2;
@@ -222,20 +246,25 @@ export const isLocalLink = (link: string | null) => {
};
export const actionLink = register({
name: "link",
name: "hyperlink",
perform: (elements, appState) => {
if (appState.showHyperlinkPopup === "editor") {
return false;
}
return {
elements,
appState: {
...appState,
showHyperlinkPopup: "editor",
openMenu: null,
},
commitToHistory: true,
};
},
trackEvent: (action, source) => {
trackEvent("hyperlink", "edit", source);
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
contextItemLabel: (elements, appState) =>
getContextMenuLabel(elements, appState),
@@ -306,9 +335,16 @@ export const isPointHittingLinkIcon = (
element: NonDeletedExcalidrawElement,
appState: AppState,
[x, y]: Point,
isMobile: boolean,
) => {
const threshold = 4 / appState.zoom.value;
if (
!isMobile &&
appState.viewModeEnabled &&
isPointHittingElementBoundingBox(element, [x, y], threshold)
) {
return true;
}
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
@@ -321,7 +357,6 @@ export const isPointHittingLinkIcon = (
x < linkX + threshold + linkWidth &&
y > linkY - threshold &&
y < linkY + linkHeight + threshold;
return hitLink;
};
@@ -376,6 +411,7 @@ const renderTooltip = (
},
"top",
);
trackEvent("hyperlink", "tooltip", "link-icon");
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
};
@@ -404,13 +440,13 @@ export const shouldHideLinkPopup = (
if (isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold)) {
return false;
}
const [x1, y1, x2] = getElementAbsoluteCoords(element);
// hit box to prevent hiding when hovered in the vertical area between element and popover
if (
sceneX >= element.x &&
sceneX <= element.x + element.width &&
sceneY <= element.y &&
sceneY >= element.y - SPACE_BOTTOM
sceneX >= x1 &&
sceneX <= x2 &&
sceneY >= y1 - SPACE_BOTTOM &&
sceneY <= y1
) {
return false;
}
+2 -2
View File
@@ -185,7 +185,7 @@ const getLinearElementAbsoluteCoords = (
maxY + element.y,
];
} else {
const shape = getShapeForElement(element) as Drawable[];
const shape = getShapeForElement(element)!;
// first element is always the curve
const ops = getCurvePathOps(shape[0]);
@@ -326,7 +326,7 @@ const getLinearElementRotatedBounds = (
return [minX, minY, maxX, maxY];
}
const shape = getShapeForElement(element) as Drawable[];
const shape = getShapeForElement(element)!;
// first element is always the curve
const ops = getCurvePathOps(shape[0]);
+23 -5
View File
@@ -24,6 +24,7 @@ import {
NonDeleted,
ExcalidrawFreeDrawElement,
ExcalidrawImageElement,
ExcalidrawLinearElement,
} from "./types";
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
@@ -361,6 +362,14 @@ const hitTestFreeDrawElement = (
B = element.points[i + 1];
}
const shape = getShapeForElement(element);
// for filled freedraw shapes, support
// selecting from inside
if (shape && shape.sets.length) {
return hitTestRoughShape(shape, x, y, threshold);
}
return false;
};
@@ -383,7 +392,11 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
}
const [relX, relY] = GAPoint.toTuple(point);
const shape = getShapeForElement(element) as Drawable[];
const shape = getShapeForElement(element as ExcalidrawLinearElement);
if (!shape) {
return false;
}
if (args.check === isInsideCheck) {
const hit = shape.some((subshape) =>
@@ -633,7 +646,7 @@ const getCorners = (
// Returns intersection of `line` with `segment`, with `segment` moved by
// `gap` in its polar direction.
// If intersection conincides with second segment point returns empty array.
// If intersection coincides with second segment point returns empty array.
const intersectSegment = (
line: GA.Line,
segment: [GA.Point, GA.Point],
@@ -821,7 +834,7 @@ const hitTestCurveInside = (
sharpness: ExcalidrawElement["strokeSharpness"],
) => {
const ops = getCurvePathOps(drawable);
const points: Point[] = [];
const points: Mutable<Point>[] = [];
let odd = false; // select one line out of double lines
for (const operation of ops) {
if (operation.op === "move") {
@@ -835,13 +848,17 @@ const hitTestCurveInside = (
points.push([operation.data[2], operation.data[3]]);
points.push([operation.data[4], operation.data[5]]);
}
} else if (operation.op === "lineTo") {
if (odd) {
points.push([operation.data[0], operation.data[1]]);
}
}
}
if (points.length >= 4) {
if (sharpness === "sharp") {
return isPointInPolygon(points, x, y);
}
const polygonPoints = pointsOnBezierCurves(points as any, 10, 5);
const polygonPoints = pointsOnBezierCurves(points, 10, 5);
return isPointInPolygon(polygonPoints, x, y);
}
return false;
@@ -896,9 +913,10 @@ const hitTestRoughShape = (
// position of the previous operation
return retVal;
} else if (op === "lineTo") {
// TODO: Implement this
return hitTestCurveInside(drawable, x, y, "sharp");
} else if (op === "qcurveTo") {
// TODO: Implement this
console.warn("qcurveTo is not implemented yet");
}
return false;
+5 -9
View File
@@ -1,12 +1,10 @@
import { SHAPES } from "../shapes";
import { updateBoundElements } from "./binding";
import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement";
import { getBoundTextElement } from "./textElement";
import { isSelectedViaGroup } from "../groups";
export const dragSelectedElements = (
@@ -39,16 +37,14 @@ export const dragSelectedElements = (
// container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
) {
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId);
const textElement = getBoundTextElement(element);
if (textElement) {
updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
textElement!,
textElement,
offset,
);
}
@@ -96,7 +92,7 @@ export const getDragOffsetXY = (
export const dragNewElement = (
draggingElement: NonDeletedExcalidrawElement,
elementType: typeof SHAPES[number]["value"],
elementType: AppState["elementType"],
originX: number,
originY: number,
x: number,
+1 -1
View File
@@ -401,7 +401,7 @@ export class LinearElementEditor {
ret.hitElement = element;
} else {
// You might be wandering why we are storing the binding elements on
// LinearElementEditor and passing them in, insted of calculating them
// LinearElementEditor and passing them in, instead of calculating them
// from the end points of the `linearElement` - this is to allow disabling
// binding (which needs to happen at the point the user finishes moving
// the point).
+3 -5
View File
@@ -22,8 +22,7 @@ import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import { getContainerElement, measureText, wrapText } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import { BOUND_TEXT_PADDING } from "../constants";
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -175,7 +174,7 @@ const getAdjustedDimensions = (
let y: number;
if (
textAlign === "center" &&
verticalAlign === "middle" &&
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId
) {
const prevMetrics = measureText(
@@ -221,8 +220,7 @@ const getAdjustedDimensions = (
// make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions
if (isBoundToContainer(element)) {
const container = getContainerElement(element)!;
if (container) {
let height = container.height;
let width = container.width;
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
+73 -18
View File
@@ -1,4 +1,4 @@
import { SHIFT_LOCKING_ANGLE } from "../constants";
import { BOUND_TEXT_PADDING, SHIFT_LOCKING_ANGLE } from "../constants";
import { rescalePoints } from "../points";
import {
@@ -35,6 +35,7 @@ import {
import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElement,
getBoundTextElementId,
@@ -105,7 +106,7 @@ export const transformElements = (
updateBoundElements(element);
} else if (transformHandleType) {
resizeSingleElement(
pointerDownState.originalElements.get(element.id) as typeof element,
pointerDownState.originalElements,
shouldMaintainAspectRatio,
element,
transformHandleType,
@@ -140,7 +141,6 @@ export const transformElements = (
pointerX,
pointerY,
);
handleBindTextResize(selectedElements, transformHandleType);
return true;
}
}
@@ -397,7 +397,7 @@ const resizeSingleTextElement = (
};
export const resizeSingleElement = (
stateAtResizeStart: NonDeletedExcalidrawElement,
originalElements: PointerDownState["originalElements"],
shouldMaintainAspectRatio: boolean,
element: NonDeletedExcalidrawElement,
transformHandleDirection: TransformHandleDirection,
@@ -405,6 +405,7 @@ export const resizeSingleElement = (
pointerX: number,
pointerY: number,
) => {
const stateAtResizeStart = originalElements.get(element.id)!;
// Gets bounds corners
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
stateAtResizeStart,
@@ -429,8 +430,6 @@ export const resizeSingleElement = (
element.height,
);
const boundTextElementId = getBoundTextElementId(element);
const boundsCurrentWidth = esx2 - esx1;
const boundsCurrentHeight = esy2 - esy1;
@@ -441,6 +440,9 @@ export const resizeSingleElement = (
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) {
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
}
@@ -453,6 +455,7 @@ export const resizeSingleElement = (
if (transformHandleDirection.includes("n")) {
scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
}
// Linear elements dimensions differ from bounds dimensions
const eleInitialWidth = stateAtResizeStart.width;
const eleInitialHeight = stateAtResizeStart.height;
@@ -482,6 +485,37 @@ export const resizeSingleElement = (
}
}
if (boundTextElement) {
const stateOfBoundTextElementAtResize = originalElements.get(
boundTextElement.id,
) as typeof boundTextElement | undefined;
if (stateOfBoundTextElementAtResize) {
boundTextFont = {
fontSize: stateOfBoundTextElementAtResize.fontSize,
baseline: stateOfBoundTextElementAtResize.baseline,
};
}
if (shouldMaintainAspectRatio) {
const nextFont = measureFontSizeFromWH(
boundTextElement,
eleNewWidth - BOUND_TEXT_PADDING * 2,
eleNewHeight - BOUND_TEXT_PADDING * 2,
);
if (nextFont === null) {
return;
}
boundTextFont = {
fontSize: nextFont.size,
baseline: nextFont.baseline,
};
} else {
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
}
}
const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
getResizedElementAbsoluteCoords(
stateAtResizeStart,
@@ -491,11 +525,6 @@ export const resizeSingleElement = (
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
// don't allow resize to negative dimensions when text is bounded to container
if ((newBoundsWidth < 0 || newBoundsHeight < 0) && boundTextElementId) {
return;
}
// Calculate new topLeft based on fixed corner during resize
let newTopLeft = [...startTopLeft] as [number, number];
if (["n", "w", "nw"].includes(transformHandleDirection)) {
@@ -588,13 +617,9 @@ export const resizeSingleElement = (
],
});
}
let minWidth = 0;
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
}
if (
resizedElement.width >= minWidth &&
resizedElement.width !== 0 &&
resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y)
@@ -603,7 +628,10 @@ export const resizeSingleElement = (
newSize: { width: resizedElement.width, height: resizedElement.height },
});
mutateElement(element, resizedElement);
handleBindTextResize([element], transformHandleDirection);
if (boundTextElement && boundTextFont) {
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
}
handleBindTextResize(element, transformHandleDirection);
}
};
@@ -674,7 +702,25 @@ const resizeMultipleElements = (
}
const width = element.width * scale;
const height = element.height * scale;
const boundTextElement = getBoundTextElement(element);
let font: { fontSize?: number; baseline?: number } = {};
if (boundTextElement) {
const nextFont = measureFontSizeFromWH(
boundTextElement,
width - BOUND_TEXT_PADDING * 2,
height - BOUND_TEXT_PADDING * 2,
);
if (nextFont === null) {
return null;
}
font = {
fontSize: nextFont.size,
baseline: nextFont.baseline,
};
}
if (isTextElement(element)) {
const nextFont = measureFontSizeFromWH(element, width, height);
if (nextFont === null) {
@@ -718,6 +764,15 @@ const resizeMultipleElements = (
if (updates) {
elements.forEach((element, index) => {
mutateElement(element, updates[index]);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
mutateElement(boundTextElement, {
fontSize: updates[index].fontSize,
baseline: updates[index].baseline,
});
handleBindTextResize(element, transformHandleType);
}
});
}
}
+2 -1
View File
@@ -10,5 +10,6 @@ export const showSelectedShapeActions = (
!appState.viewModeEnabled &&
(appState.editingElement ||
getSelectedElements(elements, appState).length ||
appState.elementType !== "selection"),
(appState.elementType !== "selection" &&
appState.elementType !== "eraser")),
);
+1 -1
View File
@@ -32,7 +32,7 @@ describe("getPerfectElementSize", () => {
expect(width).toEqual(135);
expect(height).toEqual(135);
});
it("should return height:0 and width:0 when width and heigh are 0", () => {
it("should return height:0 and width:0 when width and height are 0", () => {
const { height, width } = getPerfectElementSize("arrow", 0, 0);
expect(width).toEqual(0);
expect(height).toEqual(0);
+107 -78
View File
@@ -1,6 +1,5 @@
import { getFontString, arrayToMap, isTestEnv } from "../utils";
import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
@@ -8,10 +7,11 @@ import {
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement";
import { BOUND_TEXT_PADDING } from "../constants";
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { AppState } from "../types";
import { isTextElement } from ".";
export const redrawTextBoundingBox = (
element: ExcalidrawTextElement,
@@ -39,11 +39,19 @@ export const redrawTextBoundingBox = (
let coordY = element.y;
// Resize container and vertically center align the text
if (container) {
coordY = container.y + container.height / 2 - metrics.height / 2;
let nextHeight = container.height;
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
coordY = container.y + nextHeight / 2 - metrics.height / 2;
if (element.verticalAlign === VERTICAL_ALIGN.TOP) {
coordY = container.y + BOUND_TEXT_PADDING;
} else if (element.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY =
container.y + container.height - metrics.height - BOUND_TEXT_PADDING;
} else {
coordY = container.y + container.height / 2 - metrics.height / 2;
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
coordY = container.y + nextHeight / 2 - metrics.height / 2;
}
}
mutateElement(container, { height: nextHeight });
}
@@ -71,91 +79,99 @@ export const bindTextToShapeAfterDuplication = (
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!;
mutateElement(
sceneElementMap.get(newElementId) as ExcalidrawBindableElement,
{
boundElements: element.boundElements?.concat({
type: "text",
id: newTextElementId,
}),
},
);
mutateElement(
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement,
{
containerId: newElementId,
},
);
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
if (newTextElementId) {
const newContainer = sceneElementMap.get(newElementId);
if (newContainer) {
mutateElement(newContainer, {
boundElements: element.boundElements?.concat({
type: "text",
id: newTextElementId,
}),
});
}
const newTextElement = sceneElementMap.get(newTextElementId);
if (newTextElement && isTextElement(newTextElement)) {
mutateElement(newTextElement, {
containerId: newContainer ? newElementId : null,
});
}
}
}
});
};
export const handleBindTextResize = (
elements: readonly NonDeletedExcalidrawElement[],
element: NonDeletedExcalidrawElement,
transformHandleType: MaybeTransformHandleType,
) => {
elements.forEach((element) => {
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
if (textElement && textElement.text) {
if (!element) {
return;
}
let text = textElement.text;
let nextHeight = textElement.height;
let containerHeight = element.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
if (text) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
}
const dimensions = measureText(
text,
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
if (textElement && textElement.text) {
if (!element) {
return;
}
let text = textElement.text;
let nextHeight = textElement.height;
let containerHeight = element.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
if (text) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
nextHeight = dimensions.height;
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
const diff = containerHeight - element.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
transformHandleType === "ne" ||
transformHandleType === "nw" ||
transformHandleType === "n"
? element.y - diff
: element.y;
mutateElement(element, {
height: containerHeight,
y: updatedY,
});
}
const updatedY = element.y + containerHeight / 2 - nextHeight / 2;
mutateElement(textElement, {
const dimensions = measureText(
text,
// preserve padding and set width correctly
width: element.width - BOUND_TEXT_PADDING * 2,
height: nextHeight,
x: element.x + BOUND_TEXT_PADDING,
getFontString(textElement),
element.width,
);
nextHeight = dimensions.height;
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
const diff = containerHeight - element.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
transformHandleType === "ne" ||
transformHandleType === "nw" ||
transformHandleType === "n"
? element.y - diff
: element.y;
mutateElement(element, {
height: containerHeight,
y: updatedY,
baseline: nextBaseLine,
});
}
let updatedY;
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
updatedY = element.y + BOUND_TEXT_PADDING;
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
updatedY = element.y + element.height - nextHeight - BOUND_TEXT_PADDING;
} else {
updatedY = element.y + element.height / 2 - nextHeight / 2;
}
mutateElement(textElement, {
text,
// preserve padding and set width correctly
width: element.width - BOUND_TEXT_PADDING * 2,
height: nextHeight,
x: element.x + BOUND_TEXT_PADDING,
y: updatedY,
baseline: nextBaseLine,
});
}
});
}
};
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
@@ -355,6 +371,7 @@ export const charWidth = (() => {
const width = getTextWidth(char, font);
cachedCharWidth[font][ascii] = width;
}
return cachedCharWidth[font][ascii];
};
@@ -367,14 +384,14 @@ export const charWidth = (() => {
};
})();
export const getApproxMinLineWidth = (font: FontString) => {
const minCharWidth = getMinCharWidth(font);
if (minCharWidth === 0) {
const maxCharWidth = getMaxCharWidth(font);
if (maxCharWidth === 0) {
return (
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
BOUND_TEXT_PADDING * 2
);
}
return minCharWidth + BOUND_TEXT_PADDING * 2;
return maxCharWidth + BOUND_TEXT_PADDING * 2;
};
export const getApproxMinLineHeight = (font: FontString) => {
@@ -391,6 +408,15 @@ export const getMinCharWidth = (font: FontString) => {
return Math.min(...cacheWithOutEmpty);
};
export const getMaxCharWidth = (font: FontString) => {
const cache = charWidth.getCache(font);
if (!cache) {
return 0;
}
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
return Math.max(...cacheWithOutEmpty);
};
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
// Generally lower case is used so converting to lower case
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
@@ -416,7 +442,10 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
};
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
return container?.boundElements?.length
? container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id ||
null
: null;
};
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
+231 -2
View File
@@ -12,6 +12,8 @@ import {
ExcalidrawTextElementWithContainer,
} from "./types";
import * as textElementUtils from "./textElement";
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -19,7 +21,201 @@ const tab = " ";
const mouse = new Pointer("mouse");
describe("textWysiwyg", () => {
describe("Test unbounded text", () => {
describe("start text editing", () => {
const { h } = window;
beforeEach(async () => {
await render(<ExcalidrawApp />);
h.elements = [];
});
it("should prefer editing selected text element (non-bindable container present)", async () => {
const line = API.createElement({
type: "line",
width: 100,
height: 0,
points: [
[0, 0],
[100, 0],
],
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: line.width / 2 - textSize / 2,
y: -textSize / 2,
width: textSize,
height: textSize,
});
h.elements = [text, line];
API.setSelectedElements([text]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(text.id);
expect(
(h.state.editingElement as ExcalidrawTextElement).containerId,
).toBe(null);
});
it("should prefer editing selected text element (bindable container present)", async () => {
const container = API.createElement({
type: "rectangle",
width: 100,
boundElements: [],
});
const textSize = 20;
const boundText = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
const boundText2 = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
h.elements = [container, boundText, boundText2];
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
API.setSelectedElements([boundText2]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(boundText2.id);
});
it("should not create bound text on ENTER if text exists at container center", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
});
const textSize = 20;
const text = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
h.elements = [container, text];
API.setSelectedElements([container]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(text.id);
});
it("should edit existing bound text on ENTER even if higher z-index unbound text exists at container center", () => {
const container = API.createElement({
type: "rectangle",
width: 100,
boundElements: [],
});
const textSize = 20;
const boundText = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
const boundText2 = API.createElement({
type: "text",
text: "ola",
x: container.width / 2 - textSize / 2,
y: container.height / 2 - textSize / 2,
width: textSize,
height: textSize,
containerId: container.id,
});
h.elements = [container, boundText, boundText2];
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
API.setSelectedElements([container]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.state.editingElement?.id).toBe(boundText.id);
});
it("should edit text under cursor when clicked with text tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
});
h.elements = [text];
UI.clickTool("text");
mouse.clickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1);
});
it("should edit text under cursor when double-clicked with selection tool", () => {
const text = API.createElement({
type: "text",
text: "ola",
x: 60,
y: 0,
width: 100,
height: 100,
});
h.elements = [text];
UI.clickTool("selection");
mouse.doubleClickAt(text.x + 50, text.y + 50);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
expect(editor).not.toBe(null);
expect(h.state.editingElement?.id).toBe(text.id);
expect(h.elements.length).toBe(1);
});
});
describe("Test container-unbound text", () => {
const { h } = window;
let textarea: HTMLTextAreaElement;
@@ -235,7 +431,7 @@ describe("textWysiwyg", () => {
});
});
describe("Test bounded text", () => {
describe("Test container-bound text", () => {
let rectangle: any;
const { h } = window;
@@ -315,6 +511,39 @@ describe("textWysiwyg", () => {
]);
});
it("shouldn't bind to non-text-bindable containers", async () => {
const line = API.createElement({
type: "line",
width: 100,
height: 0,
points: [
[0, 0],
[100, 0],
],
});
h.elements = [line];
UI.clickTool("text");
mouse.clickAt(line.x + line.width / 2, line.y + line.height / 2);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, {
target: {
value: "Hello World!",
},
});
fireEvent.keyDown(editor, { key: KEYS.ESCAPE });
editor.dispatchEvent(new Event("input"));
expect(line.boundElements).toBe(null);
expect(h.elements[1].type).toBe("text");
expect((h.elements[1] as ExcalidrawTextElement).containerId).toBe(null);
});
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);
+33 -8
View File
@@ -7,7 +7,7 @@ import {
} from "../utils";
import Scene from "../scene/Scene";
import { isBoundToContainer, isTextElement } from "./typeChecks";
import { CLASSES, BOUND_TEXT_PADDING } from "../constants";
import { CLASSES, BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import {
ExcalidrawElement,
ExcalidrawTextElement,
@@ -86,7 +86,7 @@ export const textWysiwyg = ({
updatedElement: ExcalidrawTextElement,
editable: HTMLTextAreaElement,
) => {
const currentFont = editable.style.fontFamily.replaceAll('"', "");
const currentFont = editable.style.fontFamily.replace(/"/g, "");
if (
getFontFamilyString({ fontFamily: updatedElement.fontFamily }) !==
currentFont
@@ -105,6 +105,8 @@ export const textWysiwyg = ({
const updatedElement = Scene.getScene(element)?.getElement(
id,
) as ExcalidrawTextElement;
const { textAlign, verticalAlign } = updatedElement;
const approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
if (updatedElement && isTextElement(updatedElement)) {
let coordX = updatedElement.x;
@@ -114,7 +116,7 @@ export const textWysiwyg = ({
let maxHeight = updatedElement.height;
let width = updatedElement.width;
// Set to element height by default since thats
// Set to element height by default since that's
// what is going to be used for unbounded text
let height = updatedElement.height;
if (container && updatedElement.containerId) {
@@ -140,7 +142,7 @@ export const textWysiwyg = ({
maxHeight = container.height - BOUND_TEXT_PADDING * 2;
width = maxWidth;
// The coordinates of text box set a distance of
// 30px to preserve padding
// 5px to preserve padding
coordX = container.x + BOUND_TEXT_PADDING;
// autogrow container height if text exceeds
if (height > maxHeight) {
@@ -160,12 +162,34 @@ export const textWysiwyg = ({
// is reached
else {
// vertically center align the text
coordY = container.y + container.height / 2 - height / 2;
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
coordY = container.y + container.height / 2 - height / 2;
}
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY =
container.y + container.height - height - BOUND_TEXT_PADDING;
}
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const { textAlign } = updatedElement;
const initialSelectionStart = editable.selectionStart;
const initialSelectionEnd = editable.selectionEnd;
const initialLength = editable.value.length;
editable.value = updatedElement.originalText;
// restore cursor position after value updated so it doesn't
// go to the end of text when container auto expanded
if (
initialSelectionStart === initialSelectionEnd &&
initialSelectionEnd !== initialLength
) {
// get diff between length and selection end and shift
// the cursor by "diff" times to position correctly
const diff = initialLength - initialSelectionEnd;
editable.selectionStart = editable.value.length - diff;
editable.selectionEnd = editable.value.length - diff;
}
const lines = updatedElement.originalText.split("\n");
const lineHeight = updatedElement.containerId
? approxLineHeight
@@ -195,6 +219,7 @@ export const textWysiwyg = ({
editorMaxHeight,
),
textAlign,
verticalAlign,
color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100,
filter: "var(--theme-filter)",
@@ -258,7 +283,7 @@ export const textWysiwyg = ({
// as that gets updated below
const lines = editable.scrollHeight / getApproxLineHeight(font);
// auto increase height only when lines > 1 so its
// measured correctly and vertically alignes for
// measured correctly and vertically aligns for
// first line as well as setting height to "auto"
// doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) {
@@ -397,7 +422,7 @@ export const textWysiwyg = ({
};
/**
* @returns indeces of start positions of selected lines, in reverse order
* @returns indices of start positions of selected lines, in reverse order
*/
const getSelectedLinesStartIndices = () => {
let { selectionStart, selectionEnd, value } = editable;
+7 -3
View File
@@ -1,3 +1,4 @@
import { AppState } from "../types";
import {
ExcalidrawElement,
ExcalidrawTextElement,
@@ -8,6 +9,7 @@ import {
InitializedExcalidrawImageElement,
ExcalidrawImageElement,
ExcalidrawTextElementWithContainer,
ExcalidrawTextContainer,
} from "./types";
export const isGenericElement = (
@@ -59,7 +61,7 @@ export const isLinearElement = (
};
export const isLinearElementType = (
elementType: ExcalidrawElement["type"],
elementType: AppState["elementType"],
): boolean => {
return (
elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
@@ -73,7 +75,7 @@ export const isBindingElement = (
};
export const isBindingElementType = (
elementType: ExcalidrawElement["type"],
elementType: AppState["elementType"],
): boolean => {
return elementType === "arrow";
};
@@ -91,7 +93,9 @@ export const isBindableElement = (
);
};
export const isTextBindableContainer = (element: ExcalidrawElement | null) => {
export const isTextBindableContainer = (
element: ExcalidrawElement | null,
): element is ExcalidrawTextContainer => {
return (
element != null &&
(element.type === "rectangle" ||
+11 -3
View File
@@ -1,5 +1,5 @@
import { Point } from "../types";
import { FONT_FAMILY, THEME } from "../constants";
import { FONT_FAMILY, THEME, VERTICAL_ALIGN } from "../constants";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid";
@@ -12,7 +12,9 @@ export type PointerType = "mouse" | "pen" | "touch";
export type StrokeSharpness = "round" | "sharp";
export type StrokeStyle = "solid" | "dashed" | "dotted";
export type TextAlign = "left" | "center" | "right";
export type VerticalAlign = "top" | "middle";
type VerticalAlignKeys = keyof typeof VERTICAL_ALIGN;
export type VerticalAlign = typeof VERTICAL_ALIGN[VerticalAlignKeys];
type _ExcalidrawElementBase = Readonly<{
id: string;
@@ -133,8 +135,14 @@ export type ExcalidrawBindableElement =
| ExcalidrawTextElement
| ExcalidrawImageElement;
export type ExcalidrawTextContainer =
| ExcalidrawRectangleElement
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement
| ExcalidrawImageElement;
export type ExcalidrawTextElementWithContainer = {
containerId: ExcalidrawGenericElement["id"];
containerId: ExcalidrawTextContainer["id"];
} & ExcalidrawTextElement;
export type PointBinding = {
+1
View File
@@ -5,6 +5,7 @@ export const FILE_UPLOAD_TIMEOUT = 300;
export const LOAD_IMAGES_TIMEOUT = 500;
export const SYNC_FULL_SCENE_INTERVAL_MS = 20000;
export const SYNC_BROWSER_TABS_TIMEOUT = 50;
export const CURSOR_SYNC_TIMEOUT = 33; // ~30fps
export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
// 1 year (https://stackoverflow.com/a/25201898/927631)
+85 -40
View File
@@ -16,6 +16,7 @@ import {
withBatchedUpdates,
} from "../../utils";
import {
CURSOR_SYNC_TIMEOUT,
FILE_UPLOAD_MAX_BYTES,
FIREBASE_STORAGE_PREFIXES,
INITIAL_SCENE_UPDATE_TIMEOUT,
@@ -27,8 +28,8 @@ import {
import {
generateCollaborationLinkData,
getCollaborationLink,
getCollabServer,
SocketUpdateDataSource,
SOCKET_SERVER,
} from "../data";
import {
isSavedToFirebase,
@@ -353,32 +354,29 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.isCollaborating = true;
const { default: socketIOClient }: any = await import(
const { default: socketIOClient } = await import(
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
);
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomKey);
try {
const socketServerData = await getCollabServer();
if (existingRoomLinkData) {
this.excalidrawAPI.resetScene();
this.portal.socket = this.portal.open(
socketIOClient(socketServerData.url, {
transports: socketServerData.polling
? ["websocket", "polling"]
: ["websocket"],
}),
roomId,
roomKey,
);
} catch (error: any) {
console.error(error);
this.setState({ errorMessage: error.message });
return null;
}
try {
const elements = await loadFromFirebase(
roomId,
roomKey,
this.portal.socket,
);
if (elements) {
scenePromise.resolve({
elements,
scrollToContent: true,
});
}
} catch (error: any) {
// log the error and move on. other peers will sync us the scene.
console.error(error);
}
} else {
if (!existingRoomLinkData) {
const elements = this.excalidrawAPI.getSceneElements().map((element) => {
if (isImageElement(element) && element.status === "saved") {
return newElementWith(element, { status: "pending" });
@@ -402,14 +400,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}
// fallback in case you're not alone in the room but still don't receive
// initial SCENE_UPDATE message
// initial SCENE_INIT message
this.socketInitializationTimer = window.setTimeout(() => {
this.initializeSocket();
this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
});
scenePromise.resolve(null);
}, INITIAL_SCENE_UPDATE_TIMEOUT);
// All socket listeners are moving to Portal
this.portal.socket!.on(
this.portal.socket.on(
"client-broadcast",
async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
if (!this.portal.roomKey) {
@@ -427,7 +428,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return;
case SCENE.INIT: {
if (!this.portal.socketInitialized) {
this.initializeSocket();
this.initializeRoom({ fetchScene: false });
const remoteElements = decryptedData.payload.elements;
const reconciledElements = this.reconcileElements(remoteElements);
this.handleRemoteSceneUpdate(reconciledElements, {
@@ -481,12 +482,15 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
},
);
this.portal.socket!.on("first-in-room", () => {
this.portal.socket.on("first-in-room", async () => {
if (this.portal.socket) {
this.portal.socket.off("first-in-room");
}
this.initializeSocket();
scenePromise.resolve(null);
const sceneData = await this.initializeRoom({
fetchScene: true,
roomLinkData: existingRoomLinkData,
});
scenePromise.resolve(sceneData);
});
this.initializeIdleDetector();
@@ -498,9 +502,45 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return scenePromise;
};
private initializeSocket = () => {
this.portal.socketInitialized = true;
private initializeRoom = async ({
fetchScene,
roomLinkData,
}:
| {
fetchScene: true;
roomLinkData: { roomId: string; roomKey: string } | null;
}
| { fetchScene: false; roomLinkData?: null }) => {
clearTimeout(this.socketInitializationTimer!);
if (fetchScene && roomLinkData && this.portal.socket) {
this.excalidrawAPI.resetScene();
try {
const elements = await loadFromFirebase(
roomLinkData.roomId,
roomLinkData.roomKey,
this.portal.socket,
);
if (elements) {
this.setLastBroadcastedOrReceivedSceneVersion(
getSceneVersion(elements),
);
return {
elements,
scrollToContent: true,
};
}
} catch (error: any) {
// log the error and move on. other peers will sync us the scene.
console.error(error);
} finally {
this.portal.socketInitialized = true;
}
} else {
this.portal.socketInitialized = true;
}
return null;
};
private reconcileElements = (
@@ -564,7 +604,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
window.clearTimeout(this.idleTimeoutId);
this.idleTimeoutId = null;
}
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
if (!this.activeIntervalId) {
this.activeIntervalId = window.setInterval(
this.reportActive,
@@ -639,15 +681,18 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return this.excalidrawAPI.getSceneElementsIncludingDeleted();
};
onPointerUpdate = (payload: {
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
pointersMap: Gesture["pointers"];
}) => {
payload.pointersMap.size < 2 &&
this.portal.socket &&
this.portal.broadcastMouseLocation(payload);
};
onPointerUpdate = throttle(
(payload: {
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
pointersMap: Gesture["pointers"];
}) => {
payload.pointersMap.size < 2 &&
this.portal.socket &&
this.portal.broadcastMouseLocation(payload);
},
CURSOR_SYNC_TIMEOUT,
);
onIdleStateChange = (userState: UserIdleState) => {
this.setState({ userState });
+2
View File
@@ -45,6 +45,8 @@ class Portal {
this.socket.on("room-user-change", (clients: string[]) => {
this.collab.setCollaborators(clients);
});
return socket;
}
close() {
+28 -1
View File
@@ -30,7 +30,34 @@ const generateRoomId = async () => {
return bytesToHexString(buffer);
};
export const SOCKET_SERVER = process.env.REACT_APP_SOCKET_SERVER_URL;
/**
* Right now the reason why we resolve connection params (url, polling...)
* from upstream is to allow changing the params immediately when needed without
* having to wait for clients to update the SW.
*
* If REACT_APP_WS_SERVER_URL env is set, we use that instead (useful for forks)
*/
export const getCollabServer = async (): Promise<{
url: string;
polling: boolean;
}> => {
if (process.env.REACT_APP_WS_SERVER_URL) {
return {
url: process.env.REACT_APP_WS_SERVER_URL,
polling: true,
};
}
try {
const resp = await fetch(
`${process.env.REACT_APP_PORTAL_URL}/collab-server`,
);
return await resp.json();
} catch (error) {
console.error(error);
throw new Error(t("errors.cannotResolveCollabServer"));
}
};
export type EncryptedData = {
data: ArrayBuffer;
+5
View File
@@ -1,4 +1,9 @@
.excalidraw {
--color-primary-contrast-offset: #625ee0; // to offset Chubb illusion
&.theme--dark {
--color-primary-contrast-offset: #726dff; // to offset Chubb illusion
}
.layer-ui__wrapper__footer-center {
display: flex;
justify-content: space-between;
+1 -1
View File
@@ -62,7 +62,7 @@ const NVECTOR_BASE = ["1", "e0", "e1", "e2", "e01", "e20", "e12", "e012"];
export const nvector = (value: number = 0, index: number = 0): NVector => {
const result = [0, 0, 0, 0, 0, 0, 0, 0];
if (index < 0 || index > 7) {
throw new Error(`Expected \`index\` betwen 0 and 7, got \`${index}\``);
throw new Error(`Expected \`index\` between 0 and 7, got \`${index}\``);
}
if (value !== 0) {
result[index] = value;
+1 -1
View File
@@ -7,7 +7,7 @@ import { Line, Point } from "./ga";
*
* This maps to a standard formula `a * x + b * y + c`.
*
* `(-b, a)` correponds to a 2D vector parallel to the line. The lines
* `(-b, a)` corresponds to a 2D vector parallel to the line. The lines
* have a natural orientation, corresponding to that vector.
*
* The magnitude ("norm") of the line is `sqrt(a ^ 2 + b ^ 2)`.
+5 -1
View File
@@ -21,7 +21,7 @@ declare namespace NodeJS {
interface ProcessEnv {
readonly REACT_APP_BACKEND_V2_GET_URL: string;
readonly REACT_APP_BACKEND_V2_POST_URL: string;
readonly REACT_APP_SOCKET_SERVER_URL: string;
readonly REACT_APP_PORTAL_URL: string;
readonly REACT_APP_FIREBASE_CONFIG: string;
}
}
@@ -34,6 +34,10 @@ type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
/** utility type to assert that the second type is a subtype of the first type.
* Returns the subtype. */
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
type ResolutionType<T extends (...args: any) => any> = T extends (
...args: any
) => Promise<infer R>
+6 -15
View File
@@ -1,13 +1,7 @@
import {
GroupId,
ExcalidrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
} from "./element/types";
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
import { AppState } from "./types";
import { getSelectedElements } from "./scene";
import { getBoundTextElementId } from "./element/textElement";
import Scene from "./scene/Scene";
import { getBoundTextElement } from "./element/textElement";
export const selectGroup = (
groupId: GroupId,
@@ -182,13 +176,10 @@ export const getMaximumGroups = (
const currentGroupMembers = groups.get(groupId) || [];
// Include bounded text if present when grouping
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer;
currentGroupMembers.push(textElement);
// Include bound text if present when grouping
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
currentGroupMembers.push(boundTextElement);
}
groups.set(groupId, [...currentGroupMembers, element]);
});
+2
View File
@@ -1,5 +1,6 @@
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
export const isWindows = /^Win/.test(window.navigator.platform);
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
export const CODES = {
EQUAL: "Equal",
@@ -46,6 +47,7 @@ export const KEYS = {
COMMA: ",",
A: "a",
C: "c",
D: "d",
E: "e",
G: "g",
+39 -29
View File
@@ -64,6 +64,7 @@
"cartoonist": "كرتوني",
"fileTitle": "إسم الملف",
"colorPicker": "منتقي اللون",
"canvasColors": "",
"canvasBackground": "خلفية اللوحة",
"drawingCanvas": "لوحة الرسم",
"layers": "الطبقات",
@@ -103,8 +104,14 @@
"toggleTheme": "غير النمط",
"personalLib": "المكتبة الشخصية",
"excalidrawLib": "مكتبتنا",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "تصغير حجم الخط",
"increaseFontSize": "تكبير حجم الخط",
"unbindText": "",
"link": {
"edit": "تعديل الرابط",
"create": "إنشاء رابط",
"label": "رابط"
}
},
"buttons": {
"clearReset": "إعادة تعيين اللوحة",
@@ -161,7 +168,7 @@
"errorAddingToLibrary": "تعذر إضافة العنصر للمكتبة",
"errorRemovingFromLibrary": "تعذر إزالة العنصر من المكتبة",
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
"imageDoesNotContainScene": "",
"imageDoesNotContainScene": "يبدو أن هذه الصورة لا تحتوي على أي بيانات مشهد. هل قمت بتمكين تضمين المشهد أثناء التصدير؟",
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
@@ -173,7 +180,8 @@
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
"invalidSVGString": ""
"invalidSVGString": "SVG غير صالح.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "تحديد",
@@ -186,7 +194,9 @@
"freedraw": "رسم",
"text": "نص",
"library": "مكتبة",
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم"
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "إجراءات اللوحة",
@@ -194,7 +204,7 @@
"shapes": "الأشكال"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "لتحريك لوحة الرسم ، استمر في الضغط على عجلة الماوس أو مفتاح المسافة أثناء السحب",
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
@@ -209,7 +219,7 @@
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"publishLibrary": "نشر مكتبتك",
"bindTextToElement": "",
"deepBoxSelect": ""
},
@@ -281,46 +291,46 @@
"zoomToSelection": "تكبير للعنصر المحدد"
},
"clearCanvasDialog": {
"title": ""
"title": "مسح اللوحة"
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"title": "نشر المكتبة",
"itemName": "إسم العنصر",
"authorName": "إسم المؤلف",
"githubUsername": "اسم المستخدم في جيت هب",
"twitterUsername": "اسم المستخدم في تويتر",
"libraryName": "اسم المكتبة",
"libraryDesc": "وصف المكتبة",
"website": "الموقع",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"authorName": "اسمك أو اسم المستخدم",
"libraryName": "اسم مكتبتك",
"libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
"required": "مطلوب",
"website": "أدخل عنوان URL صالح"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
"link": "مستودع المكتبة العامة",
"post": "ليستخدمها الآخرون في رسوماتهم."
},
"noteGuidelines": {
"pre": "",
"link": "",
"pre": "يجب الموافقة على المكتبة يدويًا أولاً. يرجى قراءة ",
"link": "الإرشادات",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
"link": "رخصة إم أي تي ",
"post": "وهو ما يعني باختصار أنه يمكن لأي شخص استخدامها دون قيود."
},
"noteItems": "",
"atleastOneLibItem": ""
"noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
"atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء"
},
"publishSuccessDialog": {
"title": "تم إرسال المكتبة",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Карикатурист",
"fileTitle": "Име на файл",
"colorPicker": "Избор на цвят",
"canvasColors": "",
"canvasBackground": "Фон на платно",
"drawingCanvas": "Платно за рисуване",
"layers": "Слоеве",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "Нулиране на платно",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Селекция",
@@ -186,7 +194,9 @@
"freedraw": "Рисуване",
"text": "Текст",
"library": "Библиотека",
"lock": "Поддържайте избрания инструмент активен след рисуване"
"lock": "Поддържайте избрания инструмент активен след рисуване",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "Действия по платното",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "",
"fileTitle": "",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "",
@@ -186,7 +194,9 @@
"freedraw": "",
"text": "",
"library": "",
"lock": ""
"lock": "",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "",
+20 -10
View File
@@ -64,6 +64,7 @@
"cartoonist": "Dibuixant",
"fileTitle": "Nom del fitxer",
"colorPicker": "Selector de colors",
"canvasColors": "Usat al llenç",
"canvasBackground": "Fons del llenç",
"drawingCanvas": "Llenç de dibuix",
"layers": "Capes",
@@ -103,8 +104,14 @@
"toggleTheme": "Activa o desactiva el tema",
"personalLib": "Biblioteca personal",
"excalidrawLib": "Biblioteca d'Excalidraw",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "Redueix la mida de la lletra",
"increaseFontSize": "Augmenta la mida de la lletra",
"unbindText": "Desvincular el text",
"link": {
"edit": "Edita l'enllaç",
"create": "Crea un enllaç",
"label": "Enllaç"
}
},
"buttons": {
"clearReset": "Neteja el llenç",
@@ -161,7 +168,7 @@
"errorAddingToLibrary": "No s'ha pogut afegir l'element a la biblioteca",
"errorRemovingFromLibrary": "No s'ha pogut eliminar l'element de la biblioteca",
"confirmAddLibrary": "Això afegirà {{numShapes}} forma(es) a la vostra biblioteca. Estàs segur?",
"imageDoesNotContainScene": "",
"imageDoesNotContainScene": "Aquesta imatge no sembla contenir cap dada d'escena. Heu activat l'incrustació de l'escena durant l'exportació?",
"cannotRestoreFromImage": "Lescena no sha pogut restaurar des daquest fitxer dimatge",
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
@@ -173,7 +180,8 @@
"imageInsertError": "No s'ha pogut insertar la imatge, torneu-ho a provar més tard...",
"fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
"svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
"invalidSVGString": "SVG no vàlid."
"invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Selecció",
@@ -186,7 +194,9 @@
"freedraw": "Dibuix",
"text": "Text",
"library": "Biblioteca",
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar"
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada"
},
"headings": {
"canvasActions": "Accions del llenç",
@@ -206,12 +216,12 @@
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
"lineEditor_info": "Fes doble clic o premi Enter per editar punts",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
"bindTextToElement": "Premeu enter per a afegir-hi text",
"deepBoxSelect": ""
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament"
},
"canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització",
@@ -258,7 +268,7 @@
"helpDialog": {
"blog": "Llegiu el nostre blog",
"click": "clic",
"deepSelect": "",
"deepSelect": "Selecció profunda",
"deepBoxSelect": "",
"curvedArrow": "Fletxa corba",
"curvedLine": "Línia corba",
@@ -297,7 +307,7 @@
"libraryName": "Nom de la vostra biblioteca",
"libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
"githubHandle": "",
"twitterHandle": "",
"twitterHandle": "Usuari de twitter (opcional), per tal que puguem donar-vos crèdit quan fem la promoció a Twitter",
"website": "Enllaç al vostre lloc web personal o a qualsevol altre (opcional)"
},
"errors": {
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "",
"fileTitle": "",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "Pozadí plátna",
"drawingCanvas": "",
"layers": "",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Výběr",
@@ -186,7 +194,9 @@
"freedraw": "Kreslení",
"text": "Text",
"library": "",
"lock": ""
"lock": "",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "",
+14 -4
View File
@@ -5,7 +5,7 @@
"selectAll": "Marker alle",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"cut": "Klip",
"copy": "Kopier",
"copyAsPng": "Kopier til klippebord som PNG",
"copyAsSvg": "Kopier til klippebord som SVG",
@@ -64,6 +64,7 @@
"cartoonist": "",
"fileTitle": "Filnavn",
"colorPicker": "Farvevælger",
"canvasColors": "Brugt på lærred",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "",
@@ -186,7 +194,9 @@
"freedraw": "",
"text": "",
"library": "",
"lock": ""
"lock": "",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "",
+15 -5
View File
@@ -64,6 +64,7 @@
"cartoonist": "Karikaturist",
"fileTitle": "Dateiname",
"colorPicker": "Farbauswähler",
"canvasColors": "Auf Leinwand verwendet",
"canvasBackground": "Zeichenflächenhintergrund",
"drawingCanvas": "Leinwand",
"layers": "Ebenen",
@@ -100,11 +101,17 @@
"share": "Teilen",
"showStroke": "Auswahl für Strichfarbe anzeigen",
"showBackground": "Hintergrundfarbe auswählen",
"toggleTheme": "Design umschalten",
"toggleTheme": "Thema umschalten",
"personalLib": "Persönliche Bibliothek",
"excalidrawLib": "Excalidraw-Bibliothek",
"decreaseFontSize": "Schrift verkleinern",
"increaseFontSize": "Schrift vergrößern"
"increaseFontSize": "Schrift vergrößern",
"unbindText": "Text lösen",
"link": {
"edit": "Link bearbeiten",
"create": "Link erstellen",
"label": "Link"
}
},
"buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
@@ -173,7 +180,8 @@
"imageInsertError": "Das Bild konnte nicht eingefügt werden. Versuche es später erneut...",
"fileTooBig": "Die Datei ist zu groß. Die maximal zulässige Größe ist {{maxSize}}.",
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
"invalidSVGString": "Ungültige SVG."
"invalidSVGString": "Ungültige SVG.",
"cannotResolveCollabServer": "Konnte keine Verbindung zum Collab-Server herstellen. Bitte lade die Seite neu und versuche es erneut."
},
"toolBar": {
"selection": "Auswahl",
@@ -186,7 +194,9 @@
"freedraw": "Zeichnen",
"text": "Text",
"library": "Bibliothek",
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen"
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen",
"penMode": "",
"link": "Link für ausgewählte Form hinzufügen / aktualisieren"
},
"headings": {
"canvasActions": "Aktionen für Zeichenfläche",
@@ -247,7 +257,7 @@
"exportDialog": {
"disk_title": "Auf Festplatte speichern",
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.",
"disk_button": "In Datei speichern",
"disk_button": "Als Datei speichern",
"link_title": "Teilbarer Link",
"link_details": "Als schreibgeschützten Link exportieren.",
"link_button": "Als Link exportieren",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Σκιτσογράφος",
"fileTitle": "Όνομα αρχείου",
"colorPicker": "Επιλογή Χρώματος",
"canvasColors": "",
"canvasBackground": "Φόντο καμβά",
"drawingCanvas": "Σχεδίαση καμβά",
"layers": "Στρώματα",
@@ -104,7 +105,13 @@
"personalLib": "Προσωπική Βιβλιοθήκη",
"excalidrawLib": "Βιβλιοθήκη Excalidraw",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "Επαναφορά του καμβά",
@@ -173,7 +180,8 @@
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
"svgImageInsertError": "",
"invalidSVGString": "Μη έγκυρο SVG."
"invalidSVGString": "Μη έγκυρο SVG.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Επιλογή",
@@ -186,7 +194,9 @@
"freedraw": "Σχεδίαση",
"text": "Κείμενο",
"library": "Βιβλιοθήκη",
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο"
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "Ενέργειες καμβά",
+8 -3
View File
@@ -16,6 +16,7 @@
"delete": "Delete",
"copyStyles": "Copy styles",
"pasteStyles": "Paste styles",
"fontColor": "Font color",
"stroke": "Stroke",
"background": "Background",
"fill": "Fill",
@@ -64,6 +65,7 @@
"cartoonist": "Cartoonist",
"fileTitle": "File name",
"colorPicker": "Color picker",
"canvasColors": "Used on canvas",
"canvasBackground": "Canvas background",
"drawingCanvas": "Drawing canvas",
"layers": "Layers",
@@ -179,7 +181,8 @@
"imageInsertError": "Couldn't insert image. Try again later...",
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
"invalidSVGString": "Invalid SVG."
"invalidSVGString": "Invalid SVG.",
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again."
},
"toolBar": {
"selection": "Selection",
@@ -194,7 +197,8 @@
"library": "Library",
"lock": "Keep selected tool active after drawing",
"penMode": "Prevent pinch-zoom and accept freedraw input only from pen",
"link": "Add/ Update link for a selected shape"
"link": "Add/ Update link for a selected shape",
"eraser": "Eraser"
},
"headings": {
"canvasActions": "Canvas actions",
@@ -219,7 +223,8 @@
"placeImage": "Click to place the image, or click and drag to set its size manually",
"publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging"
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging",
"eraserRevert": "Hold Alt to revert the elements marked for deletion"
},
"canvasError": {
"cannotShowPreview": "Cannot show preview",
+62 -52
View File
@@ -64,6 +64,7 @@
"cartoonist": "Caricatura",
"fileTitle": "Nombre del archivo",
"colorPicker": "Selector de color",
"canvasColors": "Usado en lienzo",
"canvasBackground": "Fondo del lienzo",
"drawingCanvas": "Lienzo de dibujo",
"layers": "Capas",
@@ -101,10 +102,16 @@
"showStroke": "Mostrar selector de color de trazo",
"showBackground": "Mostrar el selector de color de fondo",
"toggleTheme": "Alternar tema",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"personalLib": "Biblioteca personal",
"excalidrawLib": "Biblioteca Excalidraw",
"decreaseFontSize": "Disminuir tamaño de letra",
"increaseFontSize": "Aumentar el tamaño de letra",
"unbindText": "Desvincular texto",
"link": {
"edit": "Editar enlace",
"create": "Crear enlace",
"label": "Enlace"
}
},
"buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo",
@@ -140,10 +147,10 @@
"exitZenMode": "Salir del modo Zen",
"cancel": "Cancelar",
"clear": "Borrar",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
"remove": "Eliminar",
"publishLibrary": "Publicar",
"submit": "Enviar",
"confirm": "Confirmar"
},
"alerts": {
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
@@ -165,7 +172,7 @@
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen",
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
"removeItemsFromsLibrary": "",
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
},
"errors": {
@@ -173,7 +180,8 @@
"imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...",
"fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.",
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
"invalidSVGString": "SVG no válido."
"invalidSVGString": "SVG no válido.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Selección",
@@ -186,7 +194,9 @@
"freedraw": "Dibujar",
"text": "Texto",
"library": "Biblioteca",
"lock": "Mantener la herramienta seleccionada activa después de dibujar"
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
"penMode": "Evitar el zoom de pellizco y aceptar la entrada libre sólo desde el lápiz",
"link": "Añadir/Actualizar enlace para una forma seleccionada"
},
"headings": {
"canvasActions": "Acciones del lienzo",
@@ -194,7 +204,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover lienzo, mantenga la rueda del ratón o la barra espaciadora mientras arrastra",
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra de espacio mientras arrastra",
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
"freeDraw": "Haz clic y arrastra, suelta al terminar",
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
@@ -206,12 +216,12 @@
"resizeImage": "Puede redimensionar libremente pulsando SHIFT,\npulse ALT para redimensionar desde el centro",
"rotate": "Puedes restringir los ángulos manteniendo presionado SHIFT mientras giras",
"lineEditor_info": "Doble clic o pulse Enter para editar puntos",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"lineEditor_pointSelected": "Presione Suprimir para eliminar el/los punto(s), CtrlOrCmd+D para duplicarlo, o arrástrelo para moverlo",
"lineEditor_nothingSelected": "Seleccione un punto a editar (mantenga MAYÚSCULAS para seleccionar múltiples),\no mantenga pulsado Alt y haga clic para añadir nuevos puntos",
"placeImage": "Haga clic para colocar la imagen o haga clic y arrastre para establecer su tamaño manualmente",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
"publishLibrary": "Publica tu propia biblioteca",
"bindTextToElement": "Presione Entrar para agregar",
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar"
},
"canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa",
@@ -258,8 +268,8 @@
"helpDialog": {
"blog": "Lea nuestro blog",
"click": "clic",
"deepSelect": "",
"deepBoxSelect": "",
"deepSelect": "Selección profunda",
"deepBoxSelect": "Seleccione en profundidad dentro de la caja, y evite arrastrar",
"curvedArrow": "Flecha curva",
"curvedLine": "Línea curva",
"documentation": "Documentación",
@@ -284,52 +294,52 @@
"title": "Borrar lienzo"
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"title": "Publicar biblioteca",
"itemName": "Nombre del artículo",
"authorName": "Nombre del autor",
"githubUsername": "Nombre de usuario de Github",
"twitterUsername": "Nombre de usuario de Twitter",
"libraryName": "Nombre de la librería",
"libraryDesc": "Descripción de la biblioteca",
"website": "Sitio Web",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
"authorName": "Nombre o nombre de usuario",
"libraryName": "Nombre de tu biblioteca",
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
"githubHandle": "GitHub maneja (opcional), así que puede editar la biblioteca una vez enviada para su revisión",
"twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
},
"errors": {
"required": "",
"website": ""
"required": "Requerido",
"website": "Introduce una URL válida"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
"pre": "Envía tu biblioteca para ser incluida en el ",
"link": "repositorio de librería pública",
"post": "para que otras personas utilicen en sus dibujos."
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
"pre": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la ",
"link": "pautas",
"post": " antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario."
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
"pre": "Al enviar, usted acepta que la biblioteca se publicará bajo el ",
"link": "Licencia MIT ",
"post": "que en breve significa que cualquiera puede utilizarlos sin restricciones."
},
"noteItems": "",
"atleastOneLibItem": ""
"noteItems": "Cada elemento de la biblioteca debe tener su propio nombre para que sea filtrable. Los siguientes elementos de la biblioteca serán incluidos:",
"atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar"
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
"title": "Biblioteca enviada",
"content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estado",
"link": "aquí"
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
"resetLibrary": "Reiniciar biblioteca",
"removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
},
"encrypted": {
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
@@ -351,7 +361,7 @@
"width": "Ancho"
},
"toast": {
"addedToLibrary": "",
"addedToLibrary": "Añadido a la biblioteca",
"copyStyles": "Estilos copiados.",
"copyToClipboard": "Copiado en el portapapeles.",
"copyToClipboardAsPng": "Copiado {{exportSelection}} al portapapeles como PNG\n({{exportColorScheme}})",
+67 -57
View File
@@ -64,6 +64,7 @@
"cartoonist": "Marrazkilaria",
"fileTitle": "Fitxategi izena",
"colorPicker": "Kolore-hautatzailea",
"canvasColors": "",
"canvasBackground": "Oihalaren atzeko planoa",
"drawingCanvas": "Marrazteko oihala",
"layers": "Geruzak",
@@ -103,8 +104,14 @@
"toggleTheme": "Aldatu gaia",
"personalLib": "Liburutegi pertsonala",
"excalidrawLib": "Excalidraw liburutegia",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "Txikitu letra tamaina",
"increaseFontSize": "Handitu letra tamaina",
"unbindText": "Askatu testua",
"link": {
"edit": "Editatu esteka",
"create": "Sortu esteka",
"label": "Esteka"
}
},
"buttons": {
"clearReset": "Garbitu oihala",
@@ -143,7 +150,7 @@
"remove": "Kendu",
"publishLibrary": "Argitaratu",
"submit": "Bidali",
"confirm": "Baieztatu"
"confirm": "Bai"
},
"alerts": {
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
@@ -156,70 +163,73 @@
"decryptFailed": "Ezin izan da deszifratu.",
"uploadedSecurly": "Kargatzea muturretik muturrerako zifratze bidez ziurtatu da, hau da, Excalidraw zerbitzariak eta hirugarrenek ezin dutela edukia irakurri.",
"loadSceneOverridePrompt": "Kanpoko marrazkia kargatzeak lehendik duzun edukia ordezkatuko du. Jarraitu nahi duzu?",
"collabStopOverridePrompt": "",
"errorLoadingLibrary": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"collabStopOverridePrompt": "Saioa gelditzeak lokalean gordetako zure aurreko marrazkia gainidatziko du. Ziur zaude?\n\n(Zure marrazki lokala mantendu nahi baduzu, itxi arakatzailearen fitxa.)",
"errorLoadingLibrary": "Errore bat gertatu da hirugarrenen liburutegia kargatzean.",
"errorAddingToLibrary": "Ezin izan da elementua liburutegian gehitu",
"errorRemovingFromLibrary": "Ezin izan da elementua liburutegitik kendu",
"confirmAddLibrary": "Honek {{numShapes}} forma gehituko ditu zure liburutegian. Ziur zaude?",
"imageDoesNotContainScene": "Irudi honek ez dirudi eszena daturik duenik. Eszena kapsulatzea gaitu al duzu esportazioan?",
"cannotRestoreFromImage": "Ezin izan da eszena leheneratu irudi fitxategi honetatik",
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
"imageInsertError": "Ezin izan da irudia txertatu. Saiatu berriro geroago...",
"fileTooBig": "Fitxategia handiegia da. Onartutako gehienezko tamaina {{maxSize}} da.",
"svgImageInsertError": "Ezin izan da SVG irudia txertatu. SVG markak baliogabea dirudi.",
"invalidSVGString": "SVG baliogabea.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "",
"image": "",
"rectangle": "",
"diamond": "",
"ellipse": "",
"arrow": "",
"line": "",
"freedraw": "",
"text": "",
"library": "",
"lock": ""
"selection": "Hautapena",
"image": "Txertatu irudia",
"rectangle": "Laukizuzena",
"diamond": "Diamantea",
"ellipse": "Elipsea",
"arrow": "Gezia",
"line": "Lerroa",
"freedraw": "Marraztu",
"text": "Testua",
"library": "Liburutegia",
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
"penMode": "Saihestu txikiagotzea eta onartu marrazte libreko idazketa solik arkatza bidez",
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako"
},
"headings": {
"canvasActions": "",
"selectedShapeActions": "",
"shapes": ""
"canvasActions": "Canvas ekintzak",
"selectedShapeActions": "Hautatutako formaren ekintzak",
"shapes": "Formak"
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
"canvasPanning": "Oihala mugitzeko, sakatu saguaren gurpila edo zuriune-barra arrastatzean",
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
"text_selected": "Egin klik bikoitza edo sakatu SARTU testua editatzeko",
"text_editing": "Sakatu Esc edo Ctrl+SARTU editatzen amaitzeko",
"linearElementMulti": "Egin klik azken puntuan edo sakatu Esc edo Sartu amaitzeko",
"lockAngle": "SHIFT sakatuta angelua mantendu dezakezu",
"resize": "Proportzioak mantendu ditzakezu SHIFT sakatuta tamaina aldatzen duzun bitartean.\nsakatu ALT erditik tamaina aldatzeko",
"resizeImage": "Tamaina libreki alda dezakezu SHIFT sakatuta,\nsakatu ALT erditik tamaina aldatzeko",
"rotate": "Angeluak mantendu ditzakezu SHIFT sakatuta biratzen duzun bitartean",
"lineEditor_info": "Egin klik bikoitza edo sakatu Sartu puntuak editatzeko",
"lineEditor_pointSelected": "Sakatu Ezabatu puntuak kentzeko,\nKtrl+D bikoizteko, edo arrastatu mugitzeko",
"lineEditor_nothingSelected": "Hautatu editatzeko puntu bat (SHIFT sakatuta anitz hautatzeko),\nedo eduki Alt sakatuta eta egin klik puntu berriak gehitzeko",
"placeImage": "Egin klik irudia kokatzeko, edo egin klik eta arrastatu bere tamaina eskuz ezartzeko",
"publishLibrary": "Argitaratu zure liburutegia",
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko"
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
"cannotShowPreview": "Ezin da oihala aurreikusi",
"canvasTooBig": "Agian oihala handiegia da.",
"canvasTooBigTip": "Aholkua: saiatu urrunen dauden elementuak pixka bat hurbiltzen."
},
"errorSplash": {
"headingMain_pre": "",
"headingMain_pre": "Errore bat aurkitu da. Saiatu ",
"headingMain_button": "orria birkargatzen.",
"clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ",
"clearCanvasMessage_button": "oihala garbitzen.",
@@ -258,8 +268,8 @@
"helpDialog": {
"blog": "Irakurri gure bloga",
"click": "sakatu",
"deepSelect": "",
"deepBoxSelect": "",
"deepSelect": "Hautapen sakona",
"deepBoxSelect": "Hautapen sakona egin laukizuzen bidez, eta saihestu arrastatzea",
"curvedArrow": "Gezi kurbatua",
"curvedLine": "Lerro kurbatua",
"documentation": "Dokumentazioa",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "کارتونیست",
"fileTitle": "نام فایل",
"colorPicker": "انتخابگر رنگ",
"canvasColors": "",
"canvasBackground": "بوم",
"drawingCanvas": "بوم نقاشی",
"layers": "لایه ها",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "پاکسازی بوم نقاشی",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "گزینش",
@@ -186,7 +194,9 @@
"freedraw": "کشیدن",
"text": "متن",
"library": "کتابخانه",
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار"
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "عملیات روی بوم",
+17 -7
View File
@@ -64,6 +64,7 @@
"cartoonist": "Sarjakuva",
"fileTitle": "Tiedostonimi",
"colorPicker": "Värin valinta",
"canvasColors": "",
"canvasBackground": "Piirtoalueen tausta",
"drawingCanvas": "Piirtoalue",
"layers": "Tasot",
@@ -103,8 +104,14 @@
"toggleTheme": "Vaihda teema",
"personalLib": "Oma kirjasto",
"excalidrawLib": "Excalidraw kirjasto",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "Pienennä kirjasinkokoa",
"increaseFontSize": "Kasvata kirjasinkokoa",
"unbindText": "",
"link": {
"edit": "Muokkaa linkkiä",
"create": "Luo linkki",
"label": "Linkki"
}
},
"buttons": {
"clearReset": "Tyhjennä piirtoalue",
@@ -173,7 +180,8 @@
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
"invalidSVGString": "Virheellinen SVG."
"invalidSVGString": "Virheellinen SVG.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Valinta",
@@ -186,7 +194,9 @@
"freedraw": "Piirrä",
"text": "Teksti",
"library": "Kirjasto",
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen"
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
"penMode": "Estä nipistyszoomaus ja vastaanota ainoastaan kynällä piirretty",
"link": "Lisää/päivitä linkki valitulle muodolle"
},
"headings": {
"canvasActions": "Piirtoalueen toiminnot",
@@ -206,11 +216,11 @@
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
"lineEditor_info": "Kaksoisnapauta tai paina Enter muokataksesi pisteitä",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
"publishLibrary": "Julkaise oma kirjasto",
"bindTextToElement": "",
"bindTextToElement": "Lisää tekstiä painamalla enter",
"deepBoxSelect": ""
},
"canvasError": {
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Caricaturiste",
"fileTitle": "Nom du fichier",
"colorPicker": "Sélecteur de couleur",
"canvasColors": "",
"canvasBackground": "Arrière-plan du canevas",
"drawingCanvas": "Zone de dessin",
"layers": "Calques",
@@ -104,7 +105,13 @@
"personalLib": "Bibliothèque personnelle",
"excalidrawLib": "Bibliothèque Excalidraw",
"decreaseFontSize": "Réduire la taille de police",
"increaseFontSize": "Augmenter la taille de police"
"increaseFontSize": "Augmenter la taille de police",
"unbindText": "Délier le texte",
"link": {
"edit": "Modifier le lien",
"create": "Créer un lien",
"label": "Lien"
}
},
"buttons": {
"clearReset": "Réinitialiser le canevas",
@@ -173,7 +180,8 @@
"imageInsertError": "Impossible d'insérer l'image. Réessayez plus tard...",
"fileTooBig": "Le fichier est trop volumineux. La taille maximale autorisée est de {{maxSize}}.",
"svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
"invalidSVGString": "SVG invalide."
"invalidSVGString": "SVG invalide.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Sélection",
@@ -186,7 +194,9 @@
"freedraw": "Dessiner",
"text": "Texte",
"library": "Bibliothèque",
"lock": "Garder l'outil sélectionné actif après le dessin"
"lock": "Garder l'outil sélectionné actif après le dessin",
"penMode": "Empêcher le zoom tactile et accepter la saisie libre uniquement à partir du stylet",
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée"
},
"headings": {
"canvasActions": "Actions du canevas",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "קריקטוריסט",
"fileTitle": "שם קובץ",
"colorPicker": "בחירת צבע",
"canvasColors": "",
"canvasBackground": "רקע הלוח",
"drawingCanvas": "לוח ציור",
"layers": "שכבות",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "אפס את הלוח",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "בחירה",
@@ -186,7 +194,9 @@
"freedraw": "צייר",
"text": "טקסט",
"library": "ספריה",
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור"
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "פעולות הלוח",
+24 -14
View File
@@ -35,11 +35,11 @@
"arrowhead_arrow": "तीर",
"arrowhead_bar": "बार",
"arrowhead_dot": "बिंदु",
"arrowhead_triangle": "",
"arrowhead_triangle": "त्रिकोण",
"fontSize": "फ़ॉन्ट का आकार",
"fontFamily": "फ़ॉन्ट का परिवार",
"onlySelected": "केवल चयनित",
"withBackground": "",
"withBackground": "पृष्ठभूमि",
"exportEmbedScene": "",
"exportEmbedScene_details": "निर्यात एम्बेड दृश्य विवरण",
"addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"",
@@ -64,6 +64,7 @@
"cartoonist": "व्यंग्य चित्रकार",
"fileTitle": "फ़ाइल का नाम",
"colorPicker": "रंग चयन",
"canvasColors": "",
"canvasBackground": "कैनवास बैकग्राउंड",
"drawingCanvas": "कैनवास बना रहे हैं",
"layers": "परतें",
@@ -93,18 +94,24 @@
"centerHorizontally": "क्षैतिज केन्द्रित",
"distributeHorizontally": "क्षैतिज रूप से वितरित करें",
"distributeVertically": "खड़ी रूप से वितरित करें",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"flipHorizontal": "दायें बायें पलटे",
"flipVertical": "ऊपर नीचे पलटे",
"viewMode": "अलग अलग देखें",
"toggleExportColorScheme": "",
"share": "",
"share": "शेयर करें",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "आकार घटाइऐ",
"increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "कैनवास रीसेट करें",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "चयन",
@@ -186,7 +194,9 @@
"freedraw": "",
"text": "पाठ",
"library": "लाइब्रेरी",
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें"
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "कैनवास क्रिया",
@@ -400,11 +410,11 @@
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"0b7285": "आसमानी",
"087f5b": "",
"2b8a3e": "",
"2b8a3e": "हरा",
"5c940d": "",
"e67700": "",
"d9480f": ""
"e67700": "पीला",
"d9480f": "नारंगी"
}
}
+187 -177
View File
@@ -35,12 +35,12 @@
"arrowhead_arrow": "Nyíl",
"arrowhead_bar": "Oszlop",
"arrowhead_dot": "Pont",
"arrowhead_triangle": "",
"arrowhead_triangle": "Háromszög",
"fontSize": "Betűméret",
"fontFamily": "Betűkészlet család",
"onlySelected": "Csak a kijelölt",
"withBackground": "",
"exportEmbedScene": "",
"withBackground": "Háttér",
"exportEmbedScene": "Jelenet beágyazása",
"exportEmbedScene_details": "A jelenetet leíró adatok hozzá lesznek adva a PNG/SVG fájlhoz, így a jelenetet vissza lehet majd tölteni belőle. Ez megnöveli a fájl méretét.",
"addWatermark": "Add hozzá, hogy \"Excalidraw-val készült\"",
"handDrawn": "Kézzel rajzolt",
@@ -62,14 +62,15 @@
"architect": "Tervezői",
"artist": "Művészi",
"cartoonist": "Karikatúrás",
"fileTitle": "",
"fileTitle": "Fájlnév",
"colorPicker": "Színválasztó",
"canvasColors": "Rajzvászonon használt",
"canvasBackground": "Vászon háttérszíne",
"drawingCanvas": "Rajzvászon",
"layers": "Rétegek",
"actions": "Műveletek",
"language": "Nyelv",
"liveCollaboration": "",
"liveCollaboration": "Élő együttműködés",
"duplicateSelection": "Duplikálás",
"untitled": "Névtelen",
"name": "Név",
@@ -78,7 +79,7 @@
"group": "Csoportosítás",
"ungroup": "Csoportbontás",
"collaborators": "Közreműködők",
"showGrid": "",
"showGrid": "Rács megjelenítése",
"addToLibrary": "Hozzáadás a könyvtárhoz",
"removeFromLibrary": "Eltávólítás a könyvtárból",
"libraryLoadingMessage": "Könyvtár betöltése…",
@@ -93,30 +94,36 @@
"centerHorizontally": "Vízszintesen középre igazított",
"distributeHorizontally": "Vízszintes elosztás",
"distributeVertically": "Függőleges elosztás",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": "",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"flipHorizontal": "Vízszintes tükrözés",
"flipVertical": "Függőleges tükrözés",
"viewMode": "Nézet",
"toggleExportColorScheme": "Exportált színséma váltása",
"share": "Megosztás",
"showStroke": "Körvonal színválasztó megjelenítése",
"showBackground": "Háttérszín-választó megjelenítése",
"toggleTheme": "Téma váltása",
"personalLib": "Személyes könyvtár",
"excalidrawLib": "Excalidraw könyvtár",
"decreaseFontSize": "Betűméret csökkentése",
"increaseFontSize": "Betűméret növelése",
"unbindText": "Szövegkötés feloldása",
"link": {
"edit": "Hivatkozás szerkesztése",
"create": "Hivatkozás létrehozása",
"label": "Hivatkozás"
}
},
"buttons": {
"clearReset": "Vászon törlése",
"exportJSON": "",
"exportImage": "",
"exportJSON": "Exportálás fájlba",
"exportImage": "Mentés képként",
"export": "Exportálás",
"exportToPng": "Exportálás PNG-be",
"exportToSvg": "Exportálás SVG-be",
"copyToClipboard": "Vágólapra másolás",
"copyPngToClipboard": "PNG másolása a vágólapra",
"scale": "Nagyítás",
"save": "",
"save": "Mentés az aktuális fájlba",
"saveAs": "Mentés másként",
"load": "Betöltés",
"getShareableLink": "Megosztható link létrehozása",
@@ -131,19 +138,19 @@
"edit": "Szerkesztés",
"undo": "Vissza",
"redo": "Újra",
"resetLibrary": "",
"resetLibrary": "Könyvtár alaphelyzetbe állítása",
"createNewRoom": "Új szoba létrehozása",
"fullScreen": "Teljes képernyő",
"darkMode": "Sötét mód",
"lightMode": "Világos mód",
"zenMode": "Letisztult mód",
"exitZenMode": "Kilépés a letisztult módból",
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
"cancel": "Mégsem",
"clear": "Kiűrítés",
"remove": "Eltávolítás",
"publishLibrary": "Közzététel",
"submit": "Elküldés",
"confirm": "Megerősítés"
},
"alerts": {
"clearReset": "Ez a művelet törli a vászont. Biztos benne?",
@@ -156,37 +163,40 @@
"decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.",
"uploadedSecurly": "A feltöltést végpontok közötti titkosítással biztosítottuk, ami azt jelenti, hogy egy harmadik fél nem tudja megnézni a tartalmát, beleértve az Excalidraw szervereit is.",
"loadSceneOverridePrompt": "A betöltött külső rajz felül fogja írnia meglévőt. Szeretnéd folytatni?",
"collabStopOverridePrompt": "",
"collabStopOverridePrompt": "A munkamenet leállítása felül fogja írni az előzőleg helyben tárolt rajzot. Biztosan ezt akarod?\n(Ha meg akarod tartani a helyben tárolt rajzot, egyszerűen csak zárd be a böngésző fület)",
"errorLoadingLibrary": "Hibába ütközött a harmarmadik féltől származó könyvtár betöltése.",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"errorAddingToLibrary": "A tétel nem addható hozzá a könyvtárhoz",
"errorRemovingFromLibrary": "A tétel nem távolítható el a könyvtárból",
"confirmAddLibrary": "Ez a művelet {{numShapes}} formát fog hozzáadni a könyvtáradhoz. Biztos vagy benne?",
"imageDoesNotContainScene": "",
"imageDoesNotContainScene": "Úgy tűnik, hogy ez a kép nem tartalmaz jelenetadatokat. Engedélyezted a jelenetbeágyazást az exportálás során?",
"cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"unsupportedFileType": "Nem támogatott fájltípus.",
"imageInsertError": "Nem sikerült beszúrni a képet. Próbáld újra később...",
"fileTooBig": "A fájl túl nagy. A megengedett maximális méret {{maxSize}}.",
"svgImageInsertError": "Nem sikerült beszúrni az SVG-képet. Az SVG szintaktika érvénytelennek tűnik.",
"invalidSVGString": "Érvénytelen SVG.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Kijelölés",
"image": "",
"image": "Kép beszúrása",
"rectangle": "Téglalap",
"diamond": "Rombusz",
"ellipse": "Ellipszis",
"arrow": "Nyíl",
"line": "Vonal",
"freedraw": "",
"freedraw": "Rajzolás",
"text": "Szöveg",
"library": "Könyvtár",
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve"
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
"penMode": "Akadályozza meg a kicsinyítést, és csak tollról fogadja el a szabadkézi bevitelt",
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz"
},
"headings": {
"canvasActions": "Vászon műveletek",
@@ -194,24 +204,24 @@
"shapes": "Alakzatok"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "A vászon mozgatásához tartsd lenyomva az egér görgőjét vagy a szóköz billentyűt húzás közben",
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
"text_selected": "",
"text_editing": "",
"text_selected": "Kattints duplán, vagy nyomj entert a szöveg szerkesztéséhez",
"text_editing": "Nyomjd meg az Escape vagy a Ctrl/Cmd+ENTER billentyűkombinációt a szerkesztés befejezéséhez",
"linearElementMulti": "Kattints a következő ív pozíciójára, vagy fejezd be a nyilat az Escape vagy Enter megnyomásával",
"lockAngle": "A SHIFT billentyű lenyomva tartásával korlátozhatja forgatás szögét",
"resize": "A SHIFT billentyű lenyomva tartásával az átméretezés megtartja az arányokat,\naz ALT lenyomva tartásával pedig a középpont egy helyben marad",
"resizeImage": "",
"resizeImage": "A SHIFT billentyű lenyomva tartásával szabadon átméretezheted,\ntartsd lenyomva az ALT billentyűt a középről való átméretezéshez",
"rotate": "A SHIFT billentyű lenyomva tartásával korlátozhatja a szögek illesztését",
"lineEditor_info": "Kattints duplán, vagy nyomj entert a pontok szerkesztéséhez",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
"lineEditor_pointSelected": "Nyomd meg a Törlés gombot a pont(ok) eltávolításához,\nA Ctrl/Cmd+D a többszörözéshez, vagy húzással mozgathatja",
"lineEditor_nothingSelected": "Válaszd ki a szerkeszteni kívánt pontot (több kijelöléséhez tartsd lenyomva a SHIFT billentyűt),\nvagy Alt, és kattintson az új pontok hozzáadásához",
"placeImage": "Kattints a kép elhelyezéséhez, vagy kattints és méretezd manuálisan",
"publishLibrary": "Tedd közzé saját könyvtáradat",
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához"
},
"canvasError": {
"cannotShowPreview": "Előnézet nem jeleníthető meg",
@@ -239,101 +249,101 @@
"desc_inProgressIntro": "Az élő együttműködési munkamenet folyamatban van.",
"desc_shareLink": "Ossza meg ezt a linket bárkivel, akivel együtt szeretne működni:",
"desc_exitSession": "Az munkamenet leállítása kilépteti önt a szobából, de folytathatja a munkát a saját gépén. Vegye figyelembe, hogy ez nem érinti más emberek munkáját és ők továbbra is együttműködhetnek a saját változatukon.",
"shareTitle": ""
"shareTitle": "Csatlakozás egy élő együttműködési munkamenethez az Excalidraw-ban"
},
"errorDialog": {
"title": "Hiba"
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
"disk_title": "Mentés lemezre",
"disk_details": "Exportálja a jelenetadatokat egy fájlba, amelyből később importálhatja.",
"disk_button": "Mentés fájlba",
"link_title": "Megosztható hivatkozás",
"link_details": "Exportálás csak olvasható hivatkozásként.",
"link_button": "Exportálás hivatkozásba",
"excalidrawplus_description": "Mentse el a jelenetet az Excalidraw+ munkaterületére.",
"excalidrawplus_button": "Exportálás",
"excalidrawplus_exportError": "Jelenleg nem lehet exportálni az Excalidraw+-ba..."
},
"helpDialog": {
"blog": "",
"click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"doubleClick": "",
"drag": "",
"editor": "",
"editSelectedShape": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"shapes": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": ""
"blog": "Olvasd a blogunkat",
"click": "kattintás",
"deepSelect": "Mély kijelölés",
"deepBoxSelect": "Mély kijelölés a dobozon belül, és a húzás megakadályozása",
"curvedArrow": "Ívelt nyíl",
"curvedLine": "Ívelt vonal",
"documentation": "Dokumentáció",
"doubleClick": "dupla kattintás",
"drag": "vonszolás",
"editor": "Szerkesztő",
"editSelectedShape": "Kijelölt alakzat szerkesztése (szöveg/nyíl/vonal)",
"github": "Hibát találtál? Küld be",
"howto": "Kövesd az útmutatóinkat",
"or": "vagy",
"preventBinding": "A nyíl ne ragadjon",
"shapes": "Alakzatok",
"shortcuts": "Gyorsbillentyűk",
"textFinish": "Szerkesztés befejezése (szöveg)",
"textNewLine": "Új sor hozzáadása (szöveg)",
"title": "Segítség",
"view": "Nézet",
"zoomToFit": "Az összes elem látótérbe hozása",
"zoomToSelection": "Kijelölésre nagyítás"
},
"clearCanvasDialog": {
"title": ""
"title": "Rajzvászon alaphelyzetbe"
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"title": "Könyvtár közzététele",
"itemName": "Tétel neve",
"authorName": "Szerző neve",
"githubUsername": "GitHub felhasználónév",
"twitterUsername": "Twitter felhasználónév",
"libraryName": "Könyvtár neve",
"libraryDesc": "Könyvtár leírása",
"website": "Weboldal",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
"authorName": "Neved vagy felhasználóneved",
"libraryName": "A könyvtárad neve",
"libraryDesc": "A könyvtárad használatát segítő leírás",
"githubHandle": "GitHub-handle(opcionális), így szerkesztheted a könyvtárat, miután elküldted ellenőrzésre",
"twitterHandle": "Twitter-felhasználónév (opcionális), így tudjuk, kinek kell jóváírni a Twitteren keresztüli reklámozást",
"website": "Hivatkozás személyes webhelyedre vagy máshová (nem kötelező)"
},
"errors": {
"required": "",
"website": ""
"required": "Kötelező",
"website": "Adj meg egy érvényes URL-t"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
"pre": "Küld be könyvtáradat, hogy bekerüljön a ",
"link": "nyilvános könyvtár tárolóba",
"post": "hogy mások is felhasználhassák a rajzaikban."
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
"pre": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a ",
"link": "segédletet",
"post": " benyújtása előtt. Szüksége lesz egy GitHub-fiókra a kommunikációhoz és a módosításokhoz, ha kérik, de ez nem feltétlenül szükséges."
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
"pre": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre ",
"link": "MIT Licensz ",
"post": "ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket."
},
"noteItems": "",
"atleastOneLibItem": ""
"noteItems": "Minden könyvtárelemnek saját nevével kell rendelkeznie, hogy szűrhető legyen. A következő könyvtári tételek kerülnek bele:",
"atleastOneLibItem": "A kezdéshez válassz ki legalább egy könyvtári elemet"
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
"title": "A könyvtár beküldve",
"content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapotot",
"link": "itt"
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
"resetLibrary": "Könyvtár alaphelyzetbe állítása",
"removeItemsFromLib": "A kiválasztott elemek eltávolítása a könyvtárból"
},
"encrypted": {
"tooltip": "A rajzaidat végpontok közötti titkosítással tároljuk, tehát az Excalidraw szervereiről se tud más belenézni.",
"link": ""
"link": "Blogbejegyzés a végpontok közötti titkosításról az Excalidraw-ban"
},
"stats": {
"angle": "Szög",
@@ -345,66 +355,66 @@
"storage": "Tárhely",
"title": "Statisztikák",
"total": "Összesen",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"version": "Verzió",
"versionCopy": "Kattints a másoláshoz",
"versionNotAvailable": "A verzió nem elérhető",
"width": "Szélesség"
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": ""
"addedToLibrary": "Könyvtárhoz adva",
"copyStyles": "Másolt stílusok.",
"copyToClipboard": "Vágólapra másolva.",
"copyToClipboardAsPng": "Az {{exportSelection}} PNG formátumban a vágólapra másolva \n({{exportColorScheme}})",
"fileSaved": "Fájl elmentve.",
"fileSavedToFilename": "Mentve mint {filename}",
"canvas": "rajzvászon",
"selection": "kijelölés"
},
"colors": {
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "",
"fff4e6": "",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "",
"15aabf": "",
"12b886": "",
"40c057": "",
"82c91e": "",
"fab005": "",
"fd7e14": "",
"000000": "",
"343a40": "",
"495057": "",
"c92a2a": "",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"087f5b": "",
"2b8a3e": "",
"5c940d": "",
"e67700": "",
"d9480f": ""
"ffffff": "Fehér",
"f8f9fa": "Szürke 0",
"f1f3f5": "Szürke 1",
"fff5f5": "Piros 0",
"fff0f6": "Pink 0",
"f8f0fc": "Szőlő 0",
"f3f0ff": "Ibolya 0",
"edf2ff": "Indigó 0",
"e7f5ff": "Kék 0",
"e3fafc": "Cián 0",
"e6fcf5": "Kékes-zöld 0",
"ebfbee": "Zöld 0",
"f4fce3": "Lime 0",
"fff9db": "Sárga 0",
"fff4e6": "Narancs 0",
"transparent": "Átlátszó",
"ced4da": "Szürke 4",
"868e96": "Szürke 6",
"fa5252": "Piros 6",
"e64980": "Pink 6",
"be4bdb": "Szőlő 6",
"7950f2": "Ibolya 6",
"4c6ef5": "Indigó 6",
"228be6": "Kék 6",
"15aabf": "Cián 6",
"12b886": "Kékes-zöld 6",
"40c057": "Zöld 6",
"82c91e": "Lime 6",
"fab005": "Sárga 6",
"fd7e14": "Narancs 6",
"000000": "Fekete",
"343a40": "Szürke 8",
"495057": "Szürke 7",
"c92a2a": "Piros 9",
"a61e4d": "Pink 9",
"862e9c": "Szőlő 9",
"5f3dc4": "Ibolya 9",
"364fc7": "Indigó 9",
"1864ab": "Kék 9",
"0b7285": "Cián 9",
"087f5b": "Kékes-zöld 9",
"2b8a3e": "Zöld 9",
"5c940d": "Lime 9",
"e67700": "Sárga 9",
"d9480f": "Narancs 9"
}
}
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Kartunis",
"fileTitle": "Nama file",
"colorPicker": "Pilihan Warna",
"canvasColors": "Digunakan di kanvas",
"canvasBackground": "Latar Kanvas",
"drawingCanvas": "Kanvas",
"layers": "Lapisan",
@@ -104,7 +105,13 @@
"personalLib": "Pustaka Pribadi",
"excalidrawLib": "Pustaka Excalidraw",
"decreaseFontSize": "Kecilkan ukuran font",
"increaseFontSize": "Besarkan ukuran font"
"increaseFontSize": "Besarkan ukuran font",
"unbindText": "Lepas teks",
"link": {
"edit": "Edit tautan",
"create": "Buat tautan",
"label": "Tautan"
}
},
"buttons": {
"clearReset": "Setel Ulang Kanvas",
@@ -173,7 +180,8 @@
"imageInsertError": "Tidak dapat menyisipkan gambar. Coba lagi nanti...",
"fileTooBig": "File terlalu besar. Ukuran maksimum yang dibolehkan {{maxSize}}.",
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.",
"invalidSVGString": "SVG tidak valid."
"invalidSVGString": "SVG tidak valid.",
"cannotResolveCollabServer": "Tidak dapat terhubung ke server kolab. Muat ulang laman dan coba lagi."
},
"toolBar": {
"selection": "Pilihan",
@@ -186,7 +194,9 @@
"freedraw": "Gambar",
"text": "Teks",
"library": "Pustaka",
"lock": "Biarkan alat yang dipilih aktif setelah menggambar"
"lock": "Biarkan alat yang dipilih aktif setelah menggambar",
"penMode": "Cegah jepit perbesar dan terima hanya input freedraw dari pena",
"link": "Tambah/Perbarui tautan untuk bentuk yang dipilih"
},
"headings": {
"canvasActions": "Opsi Kanvas",
+19 -9
View File
@@ -64,6 +64,7 @@
"cartoonist": "Fumettista",
"fileTitle": "Nome del file",
"colorPicker": "Selettore colore",
"canvasColors": "Usato su tela",
"canvasBackground": "Sfondo tela",
"drawingCanvas": "Area di disegno",
"layers": "Livelli",
@@ -103,8 +104,14 @@
"toggleTheme": "Cambia tema",
"personalLib": "Libreria Personale",
"excalidrawLib": "Libreria di Excalidraw",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "Riduci dimensione dei caratteri",
"increaseFontSize": "Aumenta la dimensione dei caratteri",
"unbindText": "Scollega testo",
"link": {
"edit": "Modifica link",
"create": "Crea link",
"label": "Link"
}
},
"buttons": {
"clearReset": "Svuota la tela",
@@ -173,7 +180,8 @@
"imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...",
"fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.",
"svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.",
"invalidSVGString": "SVG non valido."
"invalidSVGString": "SVG non valido.",
"cannotResolveCollabServer": "Impossibile connettersi al server di collab. Ricarica la pagina e riprova."
},
"toolBar": {
"selection": "Selezione",
@@ -186,7 +194,9 @@
"freedraw": "Disegno",
"text": "Testo",
"library": "Libreria",
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato"
"lock": "Mantieni lo strumento selezionato attivo dopo aver disegnato",
"penMode": "Impedisci il pinch-zoom e accetta l'input di disegno libero solo dalla penna",
"link": "Aggiungi/ aggiorna il link per una forma selezionata"
},
"headings": {
"canvasActions": "Azioni sulla Tela",
@@ -206,12 +216,12 @@
"resizeImage": "Puoi ridimensionare liberamente tenendo premuto SHIFT,\ntieni premuto ALT per ridimensionare dal centro",
"rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione",
"lineEditor_info": "Fai doppio click o premi invio per modificare i punti",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"lineEditor_pointSelected": "Premi Elimina per rimuovere il punto(i),\nCtrlOCmd+D per duplicare o trascinare per spostare",
"lineEditor_nothingSelected": "Seleziona un punto da modificare (tieni premuto MAIUSC per selezionare più punti),\noppure tieni premuto Alt e fai clic per aggiungere nuovi punti",
"placeImage": "Fai click per posizionare l'immagine, o click e trascina per impostarne la dimensione manualmente",
"publishLibrary": "Pubblica la tua libreria",
"bindTextToElement": "Premi invio per aggiungere il testo",
"deepBoxSelect": ""
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento"
},
"canvasError": {
"cannotShowPreview": "Impossibile visualizzare l'anteprima",
@@ -258,8 +268,8 @@
"helpDialog": {
"blog": "Leggi il nostro blog",
"click": "click",
"deepSelect": "",
"deepBoxSelect": "",
"deepSelect": "Selezione profonda",
"deepBoxSelect": "Seleziona in profondità all'interno della casella e previene il trascinamento",
"curvedArrow": "Freccia curva",
"curvedLine": "Linea curva",
"documentation": "Documentazione",
+14 -4
View File
@@ -64,6 +64,7 @@
"cartoonist": "漫画風",
"fileTitle": "ファイル名",
"colorPicker": "色選択",
"canvasColors": "",
"canvasBackground": "キャンバスの背景",
"drawingCanvas": "キャンバスの描画",
"layers": "レイヤー",
@@ -103,8 +104,14 @@
"toggleTheme": "テーマの切り替え",
"personalLib": "個人ライブラリ",
"excalidrawLib": "Excalidrawライブラリ",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "フォントサイズを縮小",
"increaseFontSize": "フォントサイズを拡大",
"unbindText": "",
"link": {
"edit": "リンクを編集",
"create": "リンクを作成",
"label": "リンク"
}
},
"buttons": {
"clearReset": "キャンバスのリセット",
@@ -173,7 +180,8 @@
"imageInsertError": "画像を挿入できませんでした。後でもう一度お試しください...",
"fileTooBig": "ファイルが大きすぎます。許可される最大サイズは {{maxSize}} です。",
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
"invalidSVGString": "無効なSVGです。"
"invalidSVGString": "無効なSVGです。",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "選択",
@@ -186,7 +194,9 @@
"freedraw": "描画",
"text": "テキスト",
"library": "ライブラリ",
"lock": "描画後も使用中のツールを選択したままにする"
"lock": "描画後も使用中のツールを選択したままにする",
"penMode": "ピンチとズームを抑止し、ペンからのみ自由な入力を受け付けます",
"link": ""
},
"headings": {
"canvasActions": "キャンバス操作",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "",
"fileTitle": "Isem n ufaylu",
"colorPicker": "Amafran n yini",
"canvasColors": "",
"canvasBackground": "Agilal n teɣzut n usuneɣ",
"drawingCanvas": "Taɣzut n usuneɣ",
"layers": "Tissiyin",
@@ -104,7 +105,13 @@
"personalLib": "Tamkarḍit tudmawant",
"excalidrawLib": "Tamkarḍit n Excalidraw",
"decreaseFontSize": "Senqes tiddi n tsefsit",
"increaseFontSize": "Sali tiddi n tsefsit"
"increaseFontSize": "Sali tiddi n tsefsit",
"unbindText": "",
"link": {
"edit": "Ẓreg aseɣwen",
"create": "Snulfu-d aseɣwen",
"label": "Aseɣwen"
}
},
"buttons": {
"clearReset": "Ales awennez n teɣzut n usuneɣ",
@@ -173,7 +180,8 @@
"imageInsertError": "D awezɣi tugra n tugna. Eɛreḍ tikkelt-nniḍen ardeqqal...",
"fileTooBig": "Afaylu meqqer aṭas. Tiddi tafellayt yurgen d {{maxSize}}.",
"svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
"invalidSVGString": "SVG armeɣtu."
"invalidSVGString": "SVG armeɣtu.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Tafrayt",
@@ -186,7 +194,9 @@
"freedraw": "Suneɣ",
"text": "Aḍris",
"library": "Tamkarḍit",
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ"
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ",
"penMode": "",
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen"
},
"headings": {
"canvasActions": "Tigawin n teɣzut n usuneɣ",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "",
"fileTitle": "Файл атауы",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "",
@@ -173,7 +180,8 @@
"imageInsertError": "Суретті жүктеу мүмкін болмады. Кейінірек қайталап көріңіз...",
"fileTooBig": "Файл өте үлкен. Максималды рұқсат етілген көлем {{maxSize}}.",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "",
@@ -186,7 +194,9 @@
"freedraw": "",
"text": "Мәтін",
"library": "",
"lock": ""
"lock": "",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "",
+71 -61
View File
@@ -35,11 +35,11 @@
"arrowhead_arrow": "화살표",
"arrowhead_bar": "막대",
"arrowhead_dot": "점",
"arrowhead_triangle": "",
"arrowhead_triangle": "삼각형",
"fontSize": "글자 크기",
"fontFamily": "글꼴",
"onlySelected": "선택한 항목만",
"withBackground": "",
"withBackground": "배경",
"exportEmbedScene": "",
"exportEmbedScene_details": "화면 정보가 내보내는 PNG/SVG 파일에 저장되어 이후에 파일에서 화면을 복구할 수 있습니다. 파일 크기가 증가합니다.",
"addWatermark": "\"Made with Excalidraw\" 추가",
@@ -62,14 +62,15 @@
"architect": "건축가",
"artist": "예술가",
"cartoonist": "만화가",
"fileTitle": "",
"fileTitle": "파일 이름",
"colorPicker": "색상 선택기",
"canvasColors": "",
"canvasBackground": "캔버스 배경",
"drawingCanvas": "캔버스 그리기",
"layers": "레이어",
"actions": "동작",
"language": "언어",
"liveCollaboration": "",
"liveCollaboration": "라이브 협력",
"duplicateSelection": "복제",
"untitled": "제목 없음",
"name": "이름",
@@ -78,7 +79,7 @@
"group": "그룹 생성",
"ungroup": "그룹 해제",
"collaborators": "공동 작업자",
"showGrid": "",
"showGrid": "그리드 보기",
"addToLibrary": "라이브러리에 추가",
"removeFromLibrary": "라이브러리에서 제거",
"libraryLoadingMessage": "라이브러리 불러오는 중…",
@@ -93,30 +94,36 @@
"centerHorizontally": "수평으로 중앙 정렬",
"distributeHorizontally": "수평으로 분배",
"distributeVertically": "수직으로 분배",
"flipHorizontal": "",
"flipVertical": "",
"flipHorizontal": "좌우반전",
"flipVertical": "상하반전",
"viewMode": "보기 모드",
"toggleExportColorScheme": "",
"share": "",
"share": "공유",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"personalLib": "개인 라이브러리",
"excalidrawLib": "Excalidraw 라이브러리",
"decreaseFontSize": "폰트 사이즈 줄이기",
"increaseFontSize": "폰트 사이즈 키우기",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "캔버스 초기화",
"exportJSON": "",
"exportImage": "",
"exportJSON": "파일로 익스포트",
"exportImage": "이미지로 저장",
"export": "내보내기",
"exportToPng": "PNG로 내보내기",
"exportToSvg": "SVG로 내보내기",
"copyToClipboard": "클립보드로 복사",
"copyPngToClipboard": "클립보드로 PNG 이미지 복사",
"scale": "크기",
"save": "",
"save": "현재 파일에 저장",
"saveAs": "다른 이름으로 저장",
"load": "불러오기",
"getShareableLink": "공유 가능한 링크 생성",
@@ -131,19 +138,19 @@
"edit": "수정",
"undo": "실행 취소",
"redo": "다시 실행",
"resetLibrary": "",
"resetLibrary": "라이브러리 리셋",
"createNewRoom": "방 만들기",
"fullScreen": "전체화면",
"darkMode": "다크 모드",
"lightMode": "밝은 모드",
"zenMode": "젠 모드",
"exitZenMode": "젠 모드 종료하기",
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
"cancel": "취소",
"clear": "지우기",
"remove": "삭제",
"publishLibrary": "게시하기",
"submit": "제출",
"confirm": "확인"
},
"alerts": {
"clearReset": "모든 작업 내용이 초기화됩니다. 계속하시겠습니까?",
@@ -158,8 +165,8 @@
"loadSceneOverridePrompt": "외부 파일을 불러 오면 기존 콘텐츠가 대체됩니다. 계속 진행할까요?",
"collabStopOverridePrompt": "협업 세션을 종료하면 로컬 저장소에 있는 그림이 협업 세션의 그림으로 대체됩니다. 진행하겠습니까?\n\n(로컬 저장소에 있는 그림을 유지하려면 현재 브라우저 탭을 닫아주세요.)",
"errorLoadingLibrary": "외부 라이브러리를 불러오는 중에 문제가 발생했습니다.",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"errorAddingToLibrary": "아이템을 라이브러리에 추가 할수 없습니다",
"errorRemovingFromLibrary": "라이브러리에서 아이템을 삭제할수 없습니다",
"confirmAddLibrary": "{{numShapes}}개의 모양이 라이브러리에 추가됩니다. 계속하시겠어요?",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다",
@@ -169,24 +176,27 @@
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"unsupportedFileType": "지원하지 않는 파일 형식 입니다.",
"imageInsertError": "이미지를 삽입할 수 없습니다. 나중에 다시 시도 하십시오",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "선택",
"image": "",
"image": "이미지 삽입",
"rectangle": "사각형",
"diamond": "다이아몬드",
"ellipse": "타원",
"arrow": "화살표",
"line": "선",
"freedraw": "",
"freedraw": "그리기",
"text": "텍스트",
"library": "라이브러리",
"lock": "선택된 도구 유지하기"
"lock": "선택된 도구 유지하기",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "캔버스 동작",
@@ -245,14 +255,14 @@
"title": "오류"
},
"exportDialog": {
"disk_title": "",
"disk_title": "디스크에 저장",
"disk_details": "",
"disk_button": "",
"link_title": "",
"disk_button": "파일로 저장",
"link_title": "공유 가능한 링크 생성",
"link_details": "",
"link_button": "",
"link_button": "링크로 내보내기",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_button": "내보내기",
"excalidrawplus_exportError": ""
},
"helpDialog": {
@@ -263,35 +273,35 @@
"curvedArrow": "곡선 화살표",
"curvedLine": "곡선",
"documentation": "설명서",
"doubleClick": "",
"doubleClick": "더블 클릭",
"drag": "드래그",
"editor": "에디터",
"editSelectedShape": "",
"editSelectedShape": "선택한 도형 편집하기(텍스트/화살표/라인)",
"github": "문제 제보하기",
"howto": "가이드 참고하기",
"or": "또는",
"preventBinding": "화살표가 붙지 않게 하기",
"shapes": "도형",
"shortcuts": "키보드 단축키",
"textFinish": "",
"textNewLine": "",
"textFinish": "편집 완료 (텍스트 에디터)",
"textNewLine": "줄바꿈(텍스트 에디터)",
"title": "도움말",
"view": "보기",
"zoomToFit": "모든 요소가 보이도록 확대/축소",
"zoomToSelection": "선택 영역으로 확대/축소"
},
"clearCanvasDialog": {
"title": ""
"title": "캔버스 지우기"
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"itemName": "아이템 이름",
"authorName": "저자명",
"githubUsername": "깃허브 사용자이름",
"twitterUsername": "트위터 사용자이름",
"libraryName": "라이브러리 이름",
"libraryDesc": "",
"website": "",
"website": "웹사이트",
"placeholder": {
"authorName": "",
"libraryName": "",
@@ -301,7 +311,7 @@
"website": ""
},
"errors": {
"required": "",
"required": "필수사항",
"website": ""
},
"noteDescription": {
@@ -311,12 +321,12 @@
},
"noteGuidelines": {
"pre": "",
"link": "",
"link": "가이드라인",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"link": "MIT 라이선스, ",
"post": ""
},
"noteItems": "",
@@ -325,10 +335,10 @@
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
"link": "여기"
},
"confirmDialog": {
"resetLibrary": "",
"resetLibrary": "라이브러리 리셋",
"removeItemsFromLib": ""
},
"encrypted": {
@@ -351,21 +361,21 @@
"width": "너비"
},
"toast": {
"addedToLibrary": "",
"addedToLibrary": "라이브러리에 추가되었습니다",
"copyStyles": "스타일 복사.",
"copyToClipboard": "클립보드로 복사.",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": ""
"fileSaved": "파일이 저장되었습니다.",
"fileSavedToFilename": "{filename} 로 저장되었습니다",
"canvas": "캔버스",
"selection": "선택"
},
"colors": {
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"ffffff": "화이트",
"f8f9fa": "그레이 0",
"f1f3f5": "그레이 1",
"fff5f5": "레드 0",
"fff0f6": "핑크 0",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "",
"fileTitle": "Failo pavadinimas",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
@@ -104,7 +105,13 @@
"personalLib": "Asmeninė biblioteka",
"excalidrawLib": "Exaclidraw biblioteka",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "",
@@ -186,7 +194,9 @@
"freedraw": "Piešti",
"text": "Tekstas",
"library": "Biblioteka",
"lock": ""
"lock": "",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "",
+16 -6
View File
@@ -64,6 +64,7 @@
"cartoonist": "Karikatūrists",
"fileTitle": "Datnes nosaukums",
"colorPicker": "Krāsu atlasītājs",
"canvasColors": "Izmantots tāfelei",
"canvasBackground": "Ainas fons",
"drawingCanvas": "Tāfele",
"layers": "Slāņi",
@@ -104,7 +105,13 @@
"personalLib": "Personīgā bibliotēka",
"excalidrawLib": "Excalidraw bibliotēka",
"decreaseFontSize": "Samazināt fonta izmēru",
"increaseFontSize": "Palielināt fonta izmēru"
"increaseFontSize": "Palielināt fonta izmēru",
"unbindText": "Atdalīt tekstu",
"link": {
"edit": "Rediģēt saiti",
"create": "Izveidot saiti",
"label": "Saite"
}
},
"buttons": {
"clearReset": "Atiestatīt tāfeli",
@@ -151,7 +158,7 @@
"couldNotCreateShareableLinkTooBig": "Nevarēja izveidot kopīgojamo saiti aina ir par lielu",
"couldNotLoadInvalidFile": "Nevarēja ielādēt nederīgu datni",
"importBackendFailed": "Ielāde no krātuves neizdevās.",
"cannotExportEmptyCanvas": "Nevar eksportēt tukšu zīmējumu.",
"cannotExportEmptyCanvas": "Nevar eksportēt tukšu tāfeli.",
"couldNotCopyToClipboard": "Neizdevās kopēt starpliktuvē. Mēģiniet vēlreiz, izmantojot pārlūku Chrome.",
"decryptFailed": "Nevarēja atšifrēt datus.",
"uploadedSecurly": "Augšuplāde nodrošināta ar šifrēšanu no gala līdz galam, kas nozīmē, ka Excalidraw serveri un trešās puses nevar lasīt saturu.",
@@ -173,7 +180,8 @@
"imageInsertError": "Nevarēja ievietot attēlu. Mēģiniet vēlāk...",
"fileTooBig": "Datne ir par lielu. Lielākais atļautais izmērs ir {{maxSize}}.",
"svgImageInsertError": "Nevarēja ievietot SVG attēlu. Šķiet, ka SVG marķējums nav derīgs.",
"invalidSVGString": "Nederīgs SVG."
"invalidSVGString": "Nederīgs SVG.",
"cannotResolveCollabServer": "Nevarēja savienoties ar sadarbošanās serveri. Lūdzu, pārlādējiet lapu un mēģiniet vēlreiz."
},
"toolBar": {
"selection": "Atlase",
@@ -186,7 +194,9 @@
"freedraw": "Zīmēt",
"text": "Teksts",
"library": "Bibliotēka",
"lock": "Paturēt izvēlēto rīku pēc darbības"
"lock": "Paturēt izvēlēto rīku pēc darbības",
"penMode": "Lietojot pildspalvu, bloķēt tuvināšanu un atļaut tikai zīmēšanu",
"link": "Pievienot/rediģēt atlasītās figūras saiti"
},
"headings": {
"canvasActions": "Tāfeles darbības",
@@ -266,12 +276,12 @@
"doubleClick": "dubultklikšķis",
"drag": "vilkt",
"editor": "Redaktors",
"editSelectedShape": "Rediģēt atlasīto formu (tekstu/bultu/līniju)",
"editSelectedShape": "Rediģēt atlasīto figūru (tekstu/bultu/līniju)",
"github": "Sastapāt kļūdu? Ziņot",
"howto": "Sekojiet mūsu instrukcijām",
"or": "vai",
"preventBinding": "Novērst bultu piesaistīšanos",
"shapes": "Formas",
"shapes": "Figūras",
"shortcuts": "Tastatūras saīsnes",
"textFinish": "Pabeigt rediģēšanu (teksta redaktorā)",
"textNewLine": "Nākamā rindiņa (teksta redaktorā)",
+420
View File
@@ -0,0 +1,420 @@
{
"labels": {
"paste": "चिपकवा",
"pasteCharts": "चार्ट चिपकवा",
"selectAll": "समस्त निवडा",
"multiSelect": "",
"moveCanvas": "",
"cut": "",
"copy": "",
"copyAsPng": "",
"copyAsSvg": "",
"bringForward": "",
"sendToBack": "",
"bringToFront": "",
"sendBackward": "",
"delete": "",
"copyStyles": "",
"pasteStyles": "",
"stroke": "",
"background": "",
"fill": "",
"strokeWidth": "",
"strokeStyle": "",
"strokeStyle_solid": "",
"strokeStyle_dashed": "",
"strokeStyle_dotted": "",
"sloppiness": "",
"opacity": "",
"textAlign": "",
"edges": "",
"sharp": "",
"round": "",
"arrowheads": "",
"arrowhead_none": "",
"arrowhead_arrow": "",
"arrowhead_bar": "",
"arrowhead_dot": "",
"arrowhead_triangle": "",
"fontSize": "",
"fontFamily": "",
"onlySelected": "",
"withBackground": "",
"exportEmbedScene": "",
"exportEmbedScene_details": "",
"addWatermark": "",
"handDrawn": "",
"normal": "",
"code": "",
"small": "",
"medium": "",
"large": "",
"veryLarge": "",
"solid": "",
"hachure": "",
"crossHatch": "",
"thin": "",
"bold": "",
"left": "",
"center": "",
"right": "",
"extraBold": "",
"architect": "",
"artist": "",
"cartoonist": "",
"fileTitle": "",
"colorPicker": "",
"canvasColors": "",
"canvasBackground": "",
"drawingCanvas": "",
"layers": "",
"actions": "",
"language": "",
"liveCollaboration": "",
"duplicateSelection": "",
"untitled": "",
"name": "",
"yourName": "",
"madeWithExcalidraw": "",
"group": "",
"ungroup": "",
"collaborators": "",
"showGrid": "",
"addToLibrary": "",
"removeFromLibrary": "",
"libraryLoadingMessage": "",
"libraries": "",
"loadingScene": "",
"align": "",
"alignTop": "",
"alignBottom": "",
"alignLeft": "",
"alignRight": "",
"centerVertically": "",
"centerHorizontally": "",
"distributeHorizontally": "",
"distributeVertically": "",
"flipHorizontal": "",
"flipVertical": "",
"viewMode": "",
"toggleExportColorScheme": "",
"share": "",
"showStroke": "",
"showBackground": "",
"toggleTheme": "",
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "",
"exportJSON": "",
"exportImage": "",
"export": "",
"exportToPng": "",
"exportToSvg": "",
"copyToClipboard": "",
"copyPngToClipboard": "",
"scale": "",
"save": "",
"saveAs": "",
"load": "",
"getShareableLink": "",
"close": "",
"selectLanguage": "",
"scrollBackToContent": "",
"zoomIn": "",
"zoomOut": "",
"resetZoom": "",
"menu": "",
"done": "",
"edit": "",
"undo": "",
"redo": "",
"resetLibrary": "",
"createNewRoom": "",
"fullScreen": "",
"darkMode": "",
"lightMode": "",
"zenMode": "",
"exitZenMode": "",
"cancel": "",
"clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
},
"alerts": {
"clearReset": "",
"couldNotCreateShareableLink": "",
"couldNotCreateShareableLinkTooBig": "",
"couldNotLoadInvalidFile": "",
"importBackendFailed": "",
"cannotExportEmptyCanvas": "",
"couldNotCopyToClipboard": "",
"decryptFailed": "",
"uploadedSecurly": "",
"loadSceneOverridePrompt": "",
"collabStopOverridePrompt": "",
"errorLoadingLibrary": "",
"errorAddingToLibrary": "",
"errorRemovingFromLibrary": "",
"confirmAddLibrary": "",
"imageDoesNotContainScene": "",
"cannotRestoreFromImage": "",
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "",
"image": "",
"rectangle": "",
"diamond": "",
"ellipse": "",
"arrow": "",
"line": "",
"freedraw": "",
"text": "",
"library": "",
"lock": "",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "",
"selectedShapeActions": "",
"shapes": ""
},
"hints": {
"canvasPanning": "",
"linearElement": "",
"freeDraw": "",
"text": "",
"text_selected": "",
"text_editing": "",
"linearElementMulti": "",
"lockAngle": "",
"resize": "",
"resizeImage": "",
"rotate": "",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
},
"canvasError": {
"cannotShowPreview": "",
"canvasTooBig": "",
"canvasTooBigTip": ""
},
"errorSplash": {
"headingMain_pre": "",
"headingMain_button": "",
"clearCanvasMessage": "",
"clearCanvasMessage_button": "",
"clearCanvasCaveat": "",
"trackedToSentry_pre": "",
"trackedToSentry_post": "",
"openIssueMessage_pre": "",
"openIssueMessage_button": "",
"openIssueMessage_post": "",
"sceneContent": ""
},
"roomDialog": {
"desc_intro": "",
"desc_privacy": "",
"button_startSession": "",
"button_stopSession": "",
"desc_inProgressIntro": "",
"desc_shareLink": "",
"desc_exitSession": "",
"shareTitle": ""
},
"errorDialog": {
"title": ""
},
"exportDialog": {
"disk_title": "",
"disk_details": "",
"disk_button": "",
"link_title": "",
"link_details": "",
"link_button": "",
"excalidrawplus_description": "",
"excalidrawplus_button": "",
"excalidrawplus_exportError": ""
},
"helpDialog": {
"blog": "",
"click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "",
"curvedLine": "",
"documentation": "",
"doubleClick": "",
"drag": "",
"editor": "",
"editSelectedShape": "",
"github": "",
"howto": "",
"or": "",
"preventBinding": "",
"shapes": "",
"shortcuts": "",
"textFinish": "",
"textNewLine": "",
"title": "",
"view": "",
"zoomToFit": "",
"zoomToSelection": ""
},
"clearCanvasDialog": {
"title": ""
},
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": {
"tooltip": "",
"link": ""
},
"stats": {
"angle": "",
"element": "",
"elements": "",
"height": "",
"scene": "",
"selected": "",
"storage": "",
"title": "",
"total": "",
"version": "",
"versionCopy": "",
"versionNotAvailable": "",
"width": ""
},
"toast": {
"addedToLibrary": "",
"copyStyles": "",
"copyToClipboard": "",
"copyToClipboardAsPng": "",
"fileSaved": "",
"fileSavedToFilename": "",
"canvas": "",
"selection": ""
},
"colors": {
"ffffff": "",
"f8f9fa": "",
"f1f3f5": "",
"fff5f5": "",
"fff0f6": "",
"f8f0fc": "",
"f3f0ff": "",
"edf2ff": "",
"e7f5ff": "",
"e3fafc": "",
"e6fcf5": "",
"ebfbee": "",
"f4fce3": "",
"fff9db": "",
"fff4e6": "",
"transparent": "",
"ced4da": "",
"868e96": "",
"fa5252": "",
"e64980": "",
"be4bdb": "",
"7950f2": "",
"4c6ef5": "",
"228be6": "",
"15aabf": "",
"12b886": "",
"40c057": "",
"82c91e": "",
"fab005": "",
"fd7e14": "",
"000000": "",
"343a40": "",
"495057": "",
"c92a2a": "",
"a61e4d": "",
"862e9c": "",
"5f3dc4": "",
"364fc7": "",
"1864ab": "",
"0b7285": "",
"087f5b": "",
"2b8a3e": "",
"5c940d": "",
"e67700": "",
"d9480f": ""
}
}
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "ကာတွန်း",
"fileTitle": "",
"colorPicker": "အရောင်ရွေး",
"canvasColors": "",
"canvasBackground": "ကားချပ်နောက်ခံ",
"drawingCanvas": "ပုံဆွဲကားချပ်",
"layers": "အလွှာများ",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "ကားချပ်ရှင်းလင်း",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "ရွေးချယ်",
@@ -186,7 +194,9 @@
"freedraw": "",
"text": "စာသား",
"library": "မှတ်တမ်း",
"lock": "ရွေးချယ်ထားသောကိရိယာကိုသာဆက်သုံး"
"lock": "ရွေးချယ်ထားသောကိရိယာကိုသာဆက်သုံး",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "ကားချပ်လုပ်ဆောင်ချက်",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Tegner",
"fileTitle": "Filnavn",
"colorPicker": "Fargevelger",
"canvasColors": "Brukes på lerretet",
"canvasBackground": "Lerretsbakgrunn",
"drawingCanvas": "Lerret",
"layers": "Lag",
@@ -104,7 +105,13 @@
"personalLib": "Personlig bibliotek",
"excalidrawLib": "Excalidraw-bibliotek",
"decreaseFontSize": "Reduser skriftstørrelse",
"increaseFontSize": "Øk skriftstørrelse"
"increaseFontSize": "Øk skriftstørrelse",
"unbindText": "Avbind tekst",
"link": {
"edit": "Rediger lenke",
"create": "Opprett lenke",
"label": "Lenke"
}
},
"buttons": {
"clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen",
@@ -173,7 +180,8 @@
"imageInsertError": "Kunne ikke sette inn bildet. Prøv igjen senere...",
"fileTooBig": "Filen er for stor. Maksimal tillatt størrelse er {{maxSize}}.",
"svgImageInsertError": "Kunne ikke sette inn SVG-bilde. SVG-koden ser ugyldig ut.",
"invalidSVGString": "Ugyldig SVG."
"invalidSVGString": "Ugyldig SVG.",
"cannotResolveCollabServer": "Kunne ikke koble til samarbeidsserveren. Vennligst oppdater siden og prøv på nytt."
},
"toolBar": {
"selection": "Velg",
@@ -186,7 +194,9 @@
"freedraw": "Tegn",
"text": "Tekst",
"library": "Bibliotek",
"lock": "Behold merket verktøy som aktivt"
"lock": "Behold merket verktøy som aktivt",
"penMode": "Forhindre zoom ved kniping og godta frihåndstegning kun fra penn",
"link": "Legg til / oppdater link for en valgt figur"
},
"headings": {
"canvasActions": "Handlinger: lerret",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Cartoonist",
"fileTitle": "Bestandsnaam",
"colorPicker": "Kleurenkiezer",
"canvasColors": "Gebruikt op canvas",
"canvasBackground": "Canvas achtergrond",
"drawingCanvas": "Canvas",
"layers": "Lagen",
@@ -104,7 +105,13 @@
"personalLib": "Persoonlijke bibliotheek",
"excalidrawLib": "Excalidraw bibliotheek",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "Ontkoppel tekst",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "Canvas opnieuw instellen",
@@ -173,7 +180,8 @@
"imageInsertError": "Afbeelding invoegen mislukt. Probeer het later opnieuw...",
"fileTooBig": "Bestand is te groot. Maximale grootte is {{maxSize}}.",
"svgImageInsertError": "",
"invalidSVGString": "Ongeldige SVG."
"invalidSVGString": "Ongeldige SVG.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Selectie",
@@ -186,7 +194,9 @@
"freedraw": "Tekenen",
"text": "Tekst",
"library": "Bibliotheek",
"lock": "Geselecteerde tool actief houden na tekenen"
"lock": "Geselecteerde tool actief houden na tekenen",
"penMode": "Voorkom pinch-zoom en accepteer freedraw invoer alleen van pen",
"link": ""
},
"headings": {
"canvasActions": "Canvasacties",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Teiknar",
"fileTitle": "Filnamn",
"colorPicker": "Fargeveljar",
"canvasColors": "",
"canvasBackground": "Lerretsbakgrunn",
"drawingCanvas": "Lerret",
"layers": "Lag",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "Tilbakestill lerretet",
@@ -173,7 +180,8 @@
"imageInsertError": "Kunne ikkje sette inn biletet. Prøv igjen seinare...",
"fileTooBig": "Fila er for stor. Maksimal tillate storleik er {{maxSize}}.",
"svgImageInsertError": "Kunne ikkje sette inn SVG-biletet. SVG-koden ser ugyldig ut.",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Vel",
@@ -186,7 +194,9 @@
"freedraw": "Teikn",
"text": "Tekst",
"library": "Bibliotek",
"lock": "Hald fram med valt verktøy"
"lock": "Hald fram med valt verktøy",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "Handlingar: lerret",
+15 -5
View File
@@ -64,6 +64,7 @@
"cartoonist": "Dessenhaire",
"fileTitle": "Nom del fichièr",
"colorPicker": "Selector de color",
"canvasColors": "",
"canvasBackground": "Rèireplan del canabàs",
"drawingCanvas": "Zòna de dessenh",
"layers": "Calques",
@@ -103,8 +104,14 @@
"toggleTheme": "Alternar tèma",
"personalLib": "Bibliotèca personala",
"excalidrawLib": "Bibliotèca Excalidraw",
"decreaseFontSize": "",
"increaseFontSize": ""
"decreaseFontSize": "Reduire talha poliça",
"increaseFontSize": "Aumentar talha poliça",
"unbindText": "",
"link": {
"edit": "Modificar lo ligam",
"create": "Crear un ligam",
"label": "Ligam"
}
},
"buttons": {
"clearReset": "Reïnicializar lo canabàs",
@@ -173,7 +180,8 @@
"imageInsertError": "Insercion dimatge impossibla. Tornatz ensajar mai tard...",
"fileTooBig": "Fichièr tròp pesuc. La talha maximala autorizada es {{maxSize}}.",
"svgImageInsertError": "Insercion dimatge SVG impossibla. Las balisas SVG semblan invalidas.",
"invalidSVGString": "SVG invalid."
"invalidSVGString": "SVG invalid.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Seleccion",
@@ -186,7 +194,9 @@
"freedraw": "Dessenhar",
"text": "Tèxt",
"library": "Bibliotèca",
"lock": "Mantenir activa laisina aprèp dessenhar"
"lock": "Mantenir activa laisina aprèp dessenhar",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "Accions del canabàs",
@@ -302,7 +312,7 @@
},
"errors": {
"required": "Requerit",
"website": ""
"website": "Picatz una URL valida"
},
"noteDescription": {
"pre": "Enviatz vòstra bibliotèca per èsser compresa al ",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "ਕਾਰਟੂਨਿਸਟ",
"fileTitle": "ਫਾਈਲ ਦਾ ਨਾਂ",
"colorPicker": "ਰੰਗ ਚੋਣਕਾਰ",
"canvasColors": "",
"canvasBackground": "ਕੈਨਵਸ ਦਾ ਬੈਕਗਰਾਉਂਡ",
"drawingCanvas": "ਡਰਾਇੰਗ ਕੈਨਵਸ",
"layers": "ਪਰਤਾਂ",
@@ -104,7 +105,13 @@
"personalLib": "ਨਿੱਜੀ ਲਾਇਬ੍ਰੇਰੀ",
"excalidrawLib": "ਐਕਸਕਲੀਡਰਾਅ ਲਾਇਬ੍ਰੇਰੀ",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ",
@@ -173,7 +180,8 @@
"imageInsertError": "",
"fileTooBig": "",
"svgImageInsertError": "",
"invalidSVGString": "SVG ਨਜਾਇਜ਼ ਹੈ।"
"invalidSVGString": "SVG ਨਜਾਇਜ਼ ਹੈ।",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "ਚੋਣਕਾਰ",
@@ -186,7 +194,9 @@
"freedraw": "ਵਾਹੋ",
"text": "ਪਾਠ",
"library": "ਲਾਇਬ੍ਰੇਰੀ",
"lock": "ਡਰਾਇੰਗ ਤੋਂ ਬਾਅਦ ਵੀ ਚੁਣੇ ਹੋਏ ਸੰਦ ਨੂੰ ਸਰਗਰਮ ਰੱਖੋ "
"lock": "ਡਰਾਇੰਗ ਤੋਂ ਬਾਅਦ ਵੀ ਚੁਣੇ ਹੋਏ ਸੰਦ ਨੂੰ ਸਰਗਰਮ ਰੱਖੋ ",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "ਕੈਨਵਸ ਦੀਆਂ ਕਾਰਵਾਈਆਂ",
+33 -32
View File
@@ -1,47 +1,48 @@
{
"ar-SA": 88,
"bg-BG": 62,
"ar-SA": 94,
"bg-BG": 61,
"bn-BD": 0,
"ca-ES": 97,
"ca-ES": 99,
"cs-CZ": 24,
"da-DK": 17,
"de-DE": 100,
"el-GR": 88,
"de-DE": 99,
"el-GR": 86,
"en": 100,
"es-ES": 86,
"eu-ES": 84,
"fa-IR": 64,
"fi-FI": 97,
"fr-FR": 100,
"he-IL": 82,
"hi-IN": 56,
"hu-HU": 50,
"es-ES": 99,
"eu-ES": 99,
"fa-IR": 63,
"fi-FI": 98,
"fr-FR": 99,
"he-IL": 80,
"hi-IN": 58,
"hu-HU": 99,
"id-ID": 100,
"it-IT": 98,
"ja-JP": 98,
"kab-KAB": 96,
"it-IT": 100,
"ja-JP": 97,
"kab-KAB": 95,
"kk-KZ": 23,
"ko-KR": 57,
"lt-LT": 24,
"ko-KR": 72,
"lt-LT": 23,
"lv-LV": 100,
"my-MM": 47,
"mr-IN": 0,
"my-MM": 46,
"nb-NO": 100,
"nl-NL": 91,
"nn-NO": 85,
"oc-FR": 98,
"pa-IN": 88,
"pl-PL": 95,
"pt-BR": 100,
"pt-PT": 85,
"nl-NL": 90,
"nn-NO": 83,
"oc-FR": 97,
"pa-IN": 86,
"pl-PL": 93,
"pt-BR": 99,
"pt-PT": 83,
"ro-RO": 100,
"ru-RU": 100,
"ru-RU": 99,
"si-LK": 9,
"sk-SK": 99,
"sv-SE": 99,
"ta-IN": 97,
"tr-TR": 86,
"uk-UA": 83,
"sv-SE": 100,
"ta-IN": 99,
"tr-TR": 84,
"uk-UA": 81,
"zh-CN": 100,
"zh-HK": 29,
"zh-HK": 28,
"zh-TW": 100
}
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Rysunkowy",
"fileTitle": "Nazwa pliku",
"colorPicker": "Paleta kolorów",
"canvasColors": "",
"canvasBackground": "Kolor dokumentu",
"drawingCanvas": "Obszar roboczy",
"layers": "Warstwy",
@@ -104,7 +105,13 @@
"personalLib": "Biblioteka prywatna",
"excalidrawLib": "Biblioteka Excalidraw",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "Wyczyść dokument i zresetuj kolor dokumentu",
@@ -173,7 +180,8 @@
"imageInsertError": "Nie udało się wstawić obrazu. Spróbuj ponownie później...",
"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.",
"invalidSVGString": "Nieprawidłowy SVG."
"invalidSVGString": "Nieprawidłowy SVG.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Zaznaczenie",
@@ -186,7 +194,9 @@
"freedraw": "Rysuj",
"text": "Tekst",
"library": "Biblioteka",
"lock": "Zablokuj wybrane narzędzie"
"lock": "Zablokuj wybrane narzędzie",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "Narzędzia",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Cartunista",
"fileTitle": "Nome do arquivo",
"colorPicker": "Seletor de cores",
"canvasColors": "Usado na tela",
"canvasBackground": "Fundo da tela",
"drawingCanvas": "Tela de desenho",
"layers": "Camadas",
@@ -104,7 +105,13 @@
"personalLib": "Biblioteca Pessoal",
"excalidrawLib": "Biblioteca do Excalidraw",
"decreaseFontSize": "Diminuir o tamanho da fonte",
"increaseFontSize": "Aumentar o tamanho da fonte"
"increaseFontSize": "Aumentar o tamanho da fonte",
"unbindText": "Desvincular texto",
"link": {
"edit": "Editar link",
"create": "Criar link",
"label": "Link"
}
},
"buttons": {
"clearReset": "Limpar o canvas e redefinir a cor de fundo",
@@ -173,7 +180,8 @@
"imageInsertError": "Não foi possível inserir imagem. Tente novamente mais tarde...",
"fileTooBig": "O arquivo é muito grande. O tamanho máximo permitido é {{maxSize}}.",
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
"invalidSVGString": "SVG Inválido."
"invalidSVGString": "SVG Inválido.",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Seleção",
@@ -186,7 +194,9 @@
"freedraw": "Desenhar",
"text": "Texto",
"library": "Biblioteca",
"lock": "Manter ativa a ferramenta selecionada após desenhar"
"lock": "Manter ativa a ferramenta selecionada após desenhar",
"penMode": "Prevenir a ação de tocar-ampliar e permitir apenas interações da caneta",
"link": "Adicionar/Atualizar link para uma forma selecionada"
},
"headings": {
"canvasActions": "Ações da tela",
+13 -3
View File
@@ -64,6 +64,7 @@
"cartoonist": "Caricaturista",
"fileTitle": "Nome do ficheiro",
"colorPicker": "Seletor de cores",
"canvasColors": "",
"canvasBackground": "Fundo da área de desenho",
"drawingCanvas": "Área de desenho",
"layers": "Camadas",
@@ -104,7 +105,13 @@
"personalLib": "",
"excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": ""
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
},
"buttons": {
"clearReset": "Limpar a área de desenho e redefinir a cor de fundo",
@@ -173,7 +180,8 @@
"imageInsertError": "Não foi possível inserir a imagem, tente novamente mais tarde...",
"fileTooBig": "O ficheiro é muito grande. O tamanho máximo permitido é {{maxSize}}.",
"svgImageInsertError": "Não foi possível inserir a imagem SVG. A marcação SVG parece inválida.",
"invalidSVGString": ""
"invalidSVGString": "",
"cannotResolveCollabServer": ""
},
"toolBar": {
"selection": "Seleção",
@@ -186,7 +194,9 @@
"freedraw": "Desenhar",
"text": "Texto",
"library": "Biblioteca",
"lock": "Manter a ferramenta selecionada ativa após desenhar"
"lock": "Manter a ferramenta selecionada ativa após desenhar",
"penMode": "",
"link": ""
},
"headings": {
"canvasActions": "Ações da área de desenho",

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