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
David LuzarandGitHub de6371aac4 fix: link popup position not accounting for offsets (#4695) 2022-02-03 17:00:00 +01:00
f47ddb988f feat: Support hyperlinks 🔥 (#4620)
* feat: Support hypelinks

* dont show edit when link not present

* auto submit on blur

* Add link button in sidebar and do it react way

* add key to hyperlink to remount when element selection changes

* autofocus input

* remove click handler and use pointerup/down to show /hide popup

* add keydown and support enter/escape to submit

* show extrrnal link icon when element has link

* use icons and open link in new tab

* dnt submit unless link updated

* renamed ffiles

* remove unnecessary changes

* update snap

* hide link popup once user starts interacting with element and show again only if clicked outside and clicked on element again

* render link icon outside the element

* fix hit testing

* rewrite implementation to render hyperlinks outside elements and hide when element selected

* remove

* remove

* tweak icon position and size

* rotate link icon when element rotated, handle zooming and render exactly where ne resize handle is rendered

* no need to create a new reference anymore for element when link added/updated

* rotate the link image as well when rotating element

* calculate hitbox of link icon and show pointer when hovering over link icon

* open link when clicked on link icon

* show tooltip when hovering over link icon

* show link action only when single element selected

* support other protocols

* add shortcut cmd/ctrl+k to edit/update link

* don't hide popup after submit

* renderes decreased woo

* Add context mneu label to add/edit link

* fix tests

* remove tick and show trash when in edit mode

* show edit view when element contains link

* fix snap

* horizontally center the hyperlink container with respect to elemnt

* fix padding

* remove checkcircle

* show popup on hover of selected element and dismiss when outside hitbox

* check if element has link before setting popup state

* move logic of auto hide to hyperlink and dnt hide when editing

* hide popover when drag/resize/rotate

* unmount during autohide

* autohide after 500ms

* fix regression

* prevent cmd/ctrl+k when inside link editor

* submit when input not updated

* allow custom urls

* fix centering of popup when zoomed

* fix hitbox during zoom

* fix

* tweak link normalization

* touch hyperlink tooltip DOM only if needed

* consider 0 if no offsetY

* reduce hitbox of link icon and make sure link icon doesn't show on top of higher z-index elements

* show link tooltip only if element has higher z-index

* dnt show hyperlink popup when selection changes from element with link to element with no link and also hide popover when element type changes from selection to something else

* lint: EOL

* fix link icon tooltip positioning

* open the link only when last pointer down and last pointer up hit the link hitbox

* render tooltip after 300ms delay

* ensure link popup and editor input have same height

* wip: cache the link icon canvas

* fix the image quality after caching using device pixel ratio yay

* some cleanup

* remove unused selectedElementIds from renderConfig

* Update src/renderer/renderElement.ts

* fix `opener` vulnerability

* tweak styling

* decrease padding

* open local links in the same tab

* fix caching

* code style refactor

* remove unnecessary save & restore

* show link shortcut in help dialog

* submit on cmd/ctrl+k

* merge state props

* Add title for link

* update editview if prop changes

* tweak link action logic

* make `Hyperlink` compo editor state fully controlled

* dont show popup when context menu open

* show in contextMenu only for single selection & change pos

* set button `selected` state

* set contextMenuOpen on pointerdown

* set contextMenyOpen to false when action triggered

* don't render link icons on export

* fix tests

* fix buttons wrap

* move focus states to input top-level rule

* fix elements sharing `Hyperlink` state

* fix hitbox for link icon in case of rect

* Early return if hitting link icon

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-03 20:34:59 +05:30
zsviczianandGitHub 59cbf5fde5 fix: penMode darkmode style (#4692) 2022-02-03 00:09:59 +01:00
4486fbc2c6 feat: Added penMode for palm rejection (#4657)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-02-02 14:31:38 +01:00
Aakansha DoshiandGitHub edfbac9d7d feat: support unbinding bound text (#4686)
* feat: support unbinding text

* fix unbound text

* move the unbind option next to group action

* use boundTextElement.id when unbinding

* update original text so it takes same bounding box when unbind

* Add spec

* recompute measurements when unbinding
2022-02-01 20:11:24 +05:30
719ae7b72f fix: reset unmounted state for the component (#4682)
* Reset unmounted state for the component

* update changelog

Co-authored-by: ad1992 <aakansha1216@gmail.com>
2022-02-01 16:32:22 +05:30
David LuzarandGitHub 631a228ca1 fix: typing _+ in wysiwyg not working (#4681) 2022-01-31 14:29:42 +01:00
David LuzarandGitHub 4b5270ab12 fix: keyboard-zooming in wysiwyg should zoom canvas (#4676) 2022-01-31 10:43:03 +01:00
dwelle dcee594b66 remove forgotten debug statements 2022-01-29 22:07:09 +01:00
David LuzarandGitHub 79d323fab1 refactor: simplify zoom by removing zoom.translation (#4477) 2022-01-29 21:12:44 +01:00
e4edda4555 fix: sceneCoordsToViewportCoords, jumping text when there is an offset (#4413) (#4630)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: thxnder <tswwe@qq.com>
2022-01-29 14:27:03 +01:00
ca89d47d4c feat: Sync local storage state across tabs when out of sync (#4545)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-01-27 13:21:55 +01:00
Aakansha DoshiandGitHub 18c526d877 feat: support contextMenuLabel to be of function type to support dynmaic labels (#4654) 2022-01-27 17:47:23 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cbc6bd1ad8 chore(deps): bump nanoid in /src/packages/excalidraw (#4628)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.23 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.23...3.2.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-26 14:46:28 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
83d9282dbf chore(deps): bump nanoid from 3.1.23 to 3.2.0 in /src/packages/utils (#4629)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.23 to 3.2.0.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.23...3.2.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-26 14:46:03 +05:30
Aakansha DoshiandGitHub abff780983 perf: cache approx line height in textwysiwg (#4651) 2022-01-25 17:01:12 +05:30
zsviczianandGitHub c009e03c8e fix: Right-click object menu displays partially off-screen (#4572) (#4631) 2022-01-23 17:28:38 +01:00
24bf4cb5fb fix: support collaboration in bound text (#4573)
* fix: support collaboration in bounded text

* align implementation irrespective of collab/submit

* don't wrap when submitted

* fix

* tests: exit editor via ESCAPE instead to remove async hacks

* simplify and remove dead comment

* remove mutating coords in submit since its taken care in updateWysiwygStyle

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-01-17 17:35:35 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0850ab0dd0 chore(deps-dev): bump @babel/plugin-transform-runtime (#4577)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.16.4 to 7.16.8.
- [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.16.8/packages/babel-plugin-transform-runtime)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-runtime"
  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-01-17 12:36:53 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7473169ba chore(deps-dev): bump webpack-dev-server in /src/packages/excalidraw (#4595)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.7.2 to 4.7.3.
- [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.2...v4.7.3)

---
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-01-17 12:36:40 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f6325b1e5e chore(deps-dev): bump webpack in /src/packages/utils (#4602)
Bumps [webpack](https://github.com/webpack/webpack) from 5.65.0 to 5.66.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.65.0...v5.66.0)

---
updated-dependencies:
- dependency-name: webpack
  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-01-17 12:36:26 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
466220a3a8 chore(deps): bump nanoid from 3.1.30 to 3.1.32 (#4607)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.30 to 3.1.32.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.30...3.1.32)

---
updated-dependencies:
- dependency-name: nanoid
  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-01-17 12:36:07 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d9cc7d1033 chore(deps): bump follow-redirects from 1.14.6 to 1.14.7 in /src/packages/excalidraw (#4609)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-16 10:31:04 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c037e9854c chore(deps): bump follow-redirects from 1.13.3 to 1.14.7 (#4593)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-16 10:30:24 +01:00
9373961857 chore: Update translations from Crowdin (#4322)
Co-authored-by: Panayiotis Lipiridis <lipiridis@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-01-13 19:06:48 +00:00
1fd2fe56ee fix: cmd/ctrl native browser behavior blocked in inputs (#4589)
* fix: cmd/ctrl native browser behavior blocked in inputs

* add basic test for fontSize increase/decrease via keyboard

* add tests for fontSize resizing via keyboard outside wysiwyg

* Update src/element/textWysiwyg.test.tsx

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>

* Update src/tests/resize.test.tsx

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>

* Update src/tests/resize.test.tsx

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-01-13 19:53:22 +01:00
Aakansha DoshiandGitHub dba71e358d fix: use cached width when calculating min width during resize (#4585) 2022-01-13 21:35:38 +05:30
177 changed files with 8672 additions and 5074 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_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries 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"}' 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_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries 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"}' REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
# production-only vars # production-only vars
@@ -23,4 +23,5 @@ jobs:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release - name: Auto release
run: | run: |
yarn add @actions/core
yarn autorelease 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 - name: Setup Node.js 14.x
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 16.x node-version: 14.x
- name: Install and test - name: Install and test
run: | run: |
yarn --frozen-lockfile yarn --frozen-lockfile
+4
View File
@@ -23,3 +23,7 @@ static
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
src/packages/excalidraw/types 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) [![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 ## Documentation
### Shortcuts ### Shortcuts
+9 -10
View File
@@ -21,15 +21,15 @@
"dependencies": { "dependencies": {
"@sentry/browser": "6.2.5", "@sentry/browser": "6.2.5",
"@sentry/integrations": "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", "@testing-library/react": "12.1.2",
"@tldraw/vec": "1.4.3", "@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/pica": "5.1.3", "@types/pica": "5.1.3",
"@types/react": "17.0.38", "@types/react": "17.0.39",
"@types/react-dom": "17.0.11", "@types/react-dom": "17.0.11",
"@types/socket.io-client": "1.4.36", "@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.23.0", "browser-fs-access": "0.24.1",
"clsx": "1.1.1", "clsx": "1.1.1",
"fake-indexeddb": "3.1.7", "fake-indexeddb": "3.1.7",
"firebase": "8.3.3", "firebase": "8.3.3",
@@ -37,7 +37,7 @@
"idb-keyval": "6.0.3", "idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1", "image-blob-reduce": "3.0.1",
"lodash.throttle": "4.1.1", "lodash.throttle": "4.1.1",
"nanoid": "3.1.30", "nanoid": "3.1.32",
"open-color": "1.9.1", "open-color": "1.9.1",
"pako": "1.0.11", "pako": "1.0.11",
"perfect-freehand": "1.0.16", "perfect-freehand": "1.0.16",
@@ -50,9 +50,9 @@
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.47.0", "sass": "1.49.7",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"typescript": "4.5.4" "typescript": "4.5.5"
}, },
"devDependencies": { "devDependencies": {
"@excalidraw/eslint-config": "1.0.0", "@excalidraw/eslint-config": "1.0.0",
@@ -61,20 +61,19 @@
"@types/lodash.throttle": "4.1.6", "@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.3", "@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.6", "@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4", "chai": "4.3.6",
"dotenv": "10.0.0", "dotenv": "10.0.0",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.23.0",
"husky": "7.0.4", "husky": "7.0.4",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.3.1",
"lint-staged": "12.1.7", "lint-staged": "12.3.3",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"prettier": "2.5.1", "prettier": "2.5.1",
"rewire": "5.0.0" "rewire": "5.0.0"
}, },
"resolutions": { "resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0" "@typescript-eslint/typescript-estree": "5.10.2"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=14.0.0"
-6
View File
@@ -72,12 +72,6 @@
crossorigin="anonymous" crossorigin="anonymous"
/> />
<link
href="%REACT_APP_SOCKET_SERVER_URL%/socket.io"
rel="preconnect"
crossorigin="anonymous"
/>
<link <link
rel="manifest" rel="manifest"
href="manifest.json" href="manifest.json"
+30 -5
View File
@@ -1,5 +1,6 @@
const fs = require("fs"); const fs = require("fs");
const { exec, execSync } = require("child_process"); const { exec, execSync } = require("child_process");
const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`; const excalidrawPackage = `${excalidrawDir}/package.json`;
@@ -15,18 +16,25 @@ const publish = () => {
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir }); execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir }); execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`); 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) { } catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
console.error(error); console.error(error);
process.exit(1);
} }
}; };
// get files changed between prev and head commit // get files changed between prev and head commit
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => { exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
if (error || stderr) { if (error || stderr) {
console.error(error); console.error(error);
core.setOutput("result", ":warning: Package couldn't be published!");
process.exit(1); process.exit(1);
} }
const changedFiles = stdout.trim().split("\n"); const changedFiles = stdout.trim().split("\n");
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/; 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) { 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); process.exit(0);
} }
// update package.json // update package.json
pkg.version = `${pkg.version}-${getShortCommitHash()}`;
pkg.name = "@excalidraw/excalidraw-next"; pkg.name = "@excalidraw/excalidraw-next";
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8"); let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme // 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"); fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
console.info("Publish in progress...");
publish(); publish();
}); });
+5
View File
@@ -11,6 +11,7 @@ const crowdinMap = {
"de-DE": "en-de", "de-DE": "en-de",
"el-GR": "en-el", "el-GR": "en-el",
"es-ES": "en-es", "es-ES": "en-es",
"eu-ES": "en-eu",
"fa-IR": "en-fa", "fa-IR": "en-fa",
"fi-FI": "en-fi", "fi-FI": "en-fi",
"fr-FR": "en-fr", "fr-FR": "en-fr",
@@ -42,6 +43,7 @@ const crowdinMap = {
"zh-CN": "en-zhcn", "zh-CN": "en-zhcn",
"zh-HK": "en-zhhk", "zh-HK": "en-zhhk",
"zh-TW": "en-zhtw", "zh-TW": "en-zhtw",
"lt-LT": "en-lt",
"lv-LV": "en-lv", "lv-LV": "en-lv",
"cs-CZ": "en-cs", "cs-CZ": "en-cs",
"kk-KZ": "en-kk", "kk-KZ": "en-kk",
@@ -69,6 +71,7 @@ const flags = {
"kab-KAB": "🏳", "kab-KAB": "🏳",
"kk-KZ": "🇰🇿", "kk-KZ": "🇰🇿",
"ko-KR": "🇰🇷", "ko-KR": "🇰🇷",
"lt-LT": "🇱🇹",
"lv-LV": "🇱🇻", "lv-LV": "🇱🇻",
"my-MM": "🇲🇲", "my-MM": "🇲🇲",
"nb-NO": "🇳🇴", "nb-NO": "🇳🇴",
@@ -102,6 +105,7 @@ const languages = {
"de-DE": "Deutsch", "de-DE": "Deutsch",
"el-GR": "Ελληνικά", "el-GR": "Ελληνικά",
"es-ES": "Español", "es-ES": "Español",
"eu-ES": "Euskara",
"fa-IR": "فارسی", "fa-IR": "فارسی",
"fi-FI": "Suomi", "fi-FI": "Suomi",
"fr-FR": "Français", "fr-FR": "Français",
@@ -114,6 +118,7 @@ const languages = {
"kab-KAB": "Taqbaylit", "kab-KAB": "Taqbaylit",
"kk-KZ": "Қазақ тілі", "kk-KZ": "Қазақ тілі",
"ko-KR": "한국어", "ko-KR": "한국어",
"lt-LT": "Lietuvių",
"lv-LV": "Latviešu", "lv-LV": "Latviešu",
"my-MM": "Burmese", "my-MM": "Burmese",
"nb-NO": "Norsk bokmål", "nb-NO": "Norsk bokmål",
+18 -11
View File
@@ -20,7 +20,7 @@ const headerForType = {
perf: "Performance", perf: "Performance",
build: "Build", build: "Build",
}; };
const badCommits = [];
const getCommitHashForLastVersion = async () => { const getCommitHashForLastVersion = async () => {
try { try {
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`; const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
@@ -53,19 +53,26 @@ const getLibraryCommitsSinceLastRelease = async () => {
const messageWithoutType = commit.slice(indexOfColon + 1).trim(); const messageWithoutType = commit.slice(indexOfColon + 1).trim();
const messageWithCapitalizeFirst = const messageWithCapitalizeFirst =
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1); 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 // return if the changelog already contains the pr number which would happen for package updates
if (existingChangeLog.includes(prNumber)) { if (existingChangeLog.includes(prNumber)) {
return; 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; return commitList;
}; };
+69 -36
View File
@@ -1,5 +1,5 @@
import { ColorPicker } from "../components/ColorPicker"; import { ColorPicker } from "../components/ColorPicker";
import { zoomIn, zoomOut } from "../components/icons"; import { eraser, zoomIn, zoomOut } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { DarkModeToggle } from "../components/DarkModeToggle"; import { DarkModeToggle } from "../components/DarkModeToggle";
import { THEME, ZOOM_STEP } from "../constants"; import { THEME, ZOOM_STEP } from "../constants";
@@ -9,14 +9,15 @@ import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene"; import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getNewZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { Tooltip } from "../components/Tooltip"; import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState } from "../appState"; import { getDefaultAppState, isEraserActive } from "../appState";
import ClearCanvas from "../components/ClearCanvas"; import ClearCanvas from "../components/ClearCanvas";
import clsx from "clsx";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor", name: "changeViewBackgroundColor",
@@ -26,7 +27,7 @@ export const actionChangeViewBackgroundColor = register({
commitToHistory: !!value.viewBackgroundColor, commitToHistory: !!value.viewBackgroundColor,
}; };
}, },
PanelComponent: ({ appState, updateData }) => { PanelComponent: ({ elements, appState, updateData }) => {
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<ColorPicker <ColorPicker
@@ -39,6 +40,8 @@ export const actionChangeViewBackgroundColor = register({
updateData({ openPopup: active ? "canvasColorPicker" : null }) updateData({ openPopup: active ? "canvasColorPicker" : null })
} }
data-testid="canvas-background-picker" data-testid="canvas-background-picker"
elements={elements}
appState={appState}
/> />
</div> </div>
); );
@@ -58,11 +61,15 @@ export const actionClearCanvas = register({
files: {}, files: {},
theme: appState.theme, theme: appState.theme,
elementLocked: appState.elementLocked, elementLocked: appState.elementLocked,
penMode: appState.penMode,
penDetected: appState.penDetected,
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene, exportEmbedScene: appState.exportEmbedScene,
gridSize: appState.gridSize, gridSize: appState.gridSize,
showStats: appState.showStats, showStats: appState.showStats,
pasteDialog: appState.pasteDialog, pasteDialog: appState.pasteDialog,
elementType:
appState.elementType === "image" ? "selection" : appState.elementType,
}, },
commitToHistory: true, commitToHistory: true,
}; };
@@ -73,17 +80,18 @@ export const actionClearCanvas = register({
export const actionZoomIn = register({ export const actionZoomIn = register({
name: "zoomIn", name: "zoomIn",
perform: (_elements, appState) => { perform: (_elements, appState, _, app) => {
const zoom = getNewZoom(
getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
appState.zoom,
{ left: appState.offsetLeft, top: appState.offsetTop },
{ x: appState.width / 2, y: appState.height / 2 },
);
return { return {
appState: { appState: {
...appState, ...appState,
zoom, ...getStateForZoom(
{
viewportX: appState.width / 2 + appState.offsetLeft,
viewportY: appState.height / 2 + appState.offsetTop,
nextZoom: getNormalizedZoom(appState.zoom.value + ZOOM_STEP),
},
appState,
),
}, },
commitToHistory: false, commitToHistory: false,
}; };
@@ -107,18 +115,18 @@ export const actionZoomIn = register({
export const actionZoomOut = register({ export const actionZoomOut = register({
name: "zoomOut", name: "zoomOut",
perform: (_elements, appState) => { perform: (_elements, appState, _, app) => {
const zoom = getNewZoom(
getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
appState.zoom,
{ left: appState.offsetLeft, top: appState.offsetTop },
{ x: appState.width / 2, y: appState.height / 2 },
);
return { return {
appState: { appState: {
...appState, ...appState,
zoom, ...getStateForZoom(
{
viewportX: appState.width / 2 + appState.offsetLeft,
viewportY: appState.height / 2 + appState.offsetTop,
nextZoom: getNormalizedZoom(appState.zoom.value - ZOOM_STEP),
},
appState,
),
}, },
commitToHistory: false, commitToHistory: false,
}; };
@@ -142,18 +150,17 @@ export const actionZoomOut = register({
export const actionResetZoom = register({ export const actionResetZoom = register({
name: "resetZoom", name: "resetZoom",
perform: (_elements, appState) => { perform: (_elements, appState, _, app) => {
return { return {
appState: { appState: {
...appState, ...appState,
zoom: getNewZoom( ...getStateForZoom(
1 as NormalizedZoomValue,
appState.zoom,
{ left: appState.offsetLeft, top: appState.offsetTop },
{ {
x: appState.width / 2, viewportX: appState.width / 2 + appState.offsetLeft,
y: appState.height / 2, viewportY: appState.height / 2 + appState.offsetTop,
nextZoom: getNormalizedZoom(1),
}, },
appState,
), ),
}, },
commitToHistory: false, commitToHistory: false,
@@ -212,14 +219,12 @@ const zoomToFitElements = (
? getCommonBounds(selectedElements) ? getCommonBounds(selectedElements)
: getCommonBounds(nonDeletedElements); : getCommonBounds(nonDeletedElements);
const zoomValue = zoomValueToFitBoundsOnViewport(commonBounds, { const newZoom = {
width: appState.width, value: zoomValueToFitBoundsOnViewport(commonBounds, {
height: appState.height, width: appState.width,
}); height: appState.height,
const newZoom = getNewZoom(zoomValue, appState.zoom, { }),
left: appState.offsetLeft, };
top: appState.offsetTop,
});
const [x1, y1, x2, y2] = commonBounds; const [x1, y1, x2, y2] = commonBounds;
const centerX = (x1 + x2) / 2; const centerX = (x1 + x2) / 2;
@@ -285,3 +290,31 @@ export const actionToggleTheme = register({
), ),
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D, 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>
),
});
+1 -1
View File
@@ -25,7 +25,7 @@ export const actionCut = register({
name: "cut", name: "cut",
perform: (elements, appState, data, app) => { perform: (elements, appState, data, app) => {
actionCopy.perform(elements, appState, data, app); actionCopy.perform(elements, appState, data, app);
return actionDeleteSelected.perform(elements, appState, data, app); return actionDeleteSelected.perform(elements, appState);
}, },
contextItemLabel: "labels.cut", contextItemLabel: "labels.cut",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.X,
+5 -3
View File
@@ -7,7 +7,7 @@ import { distributeElements, Distribution } from "../disitrubte";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES } from "../keys"; import { CODES, KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
@@ -49,7 +49,8 @@ export const distributeHorizontally = register({
commitToHistory: true, 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 }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
@@ -77,7 +78,8 @@ export const distributeVertically = register({
commitToHistory: true, 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 }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState)}
+1 -1
View File
@@ -155,7 +155,7 @@ const flipElement = (
// calculate new x-coord for transformation // calculate new x-coord for transformation
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width; newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
resizeSingleElement( resizeSingleElement(
element, new Map().set(element.id, element),
true, true,
element, element,
usingNWHandle ? "nw" : "ne", usingNWHandle ? "nw" : "ne",
+228 -62
View File
@@ -30,11 +30,15 @@ import {
TextAlignCenterIcon, TextAlignCenterIcon,
TextAlignLeftIcon, TextAlignLeftIcon,
TextAlignRightIcon, TextAlignRightIcon,
TextAlignTopIcon,
TextAlignBottomIcon,
TextAlignMiddleIcon,
} from "../components/icons"; } from "../components/icons";
import { import {
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
FONT_FAMILY, FONT_FAMILY,
VERTICAL_ALIGN,
} from "../constants"; } from "../constants";
import { import {
getNonDeletedElements, getNonDeletedElements,
@@ -47,6 +51,7 @@ import {
getContainerElement, getContainerElement,
} from "../element/textElement"; } from "../element/textElement";
import { import {
hasBoundTextElement,
isBoundToContainer, isBoundToContainer,
isLinearElement, isLinearElement,
isLinearElementType, isLinearElementType,
@@ -58,6 +63,7 @@ import {
ExcalidrawTextElement, ExcalidrawTextElement,
FontFamilyValues, FontFamilyValues,
TextAlign, TextAlign,
VerticalAlign,
} from "../element/types"; } from "../element/types";
import { getLanguage, t } from "../i18n"; import { getLanguage, t } from "../i18n";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@@ -101,6 +107,7 @@ const getFormValue = function <T>(
appState: AppState, appState: AppState,
getAttribute: (element: ExcalidrawElement) => T, getAttribute: (element: ExcalidrawElement) => T,
defaultValue?: T, defaultValue?: T,
onlyBoundTextElements: boolean = false,
): T | null { ): T | null {
const editingElement = appState.editingElement; const editingElement = appState.editingElement;
const nonDeletedElements = getNonDeletedElements(elements); const nonDeletedElements = getNonDeletedElements(elements);
@@ -111,6 +118,7 @@ const getFormValue = function <T>(
nonDeletedElements, nonDeletedElements,
appState, appState,
getAttribute, getAttribute,
onlyBoundTextElements,
) )
: defaultValue) ?? : defaultValue) ??
null null
@@ -145,6 +153,7 @@ const changeFontSize = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
getNewFontSize: (element: ExcalidrawTextElement) => number, getNewFontSize: (element: ExcalidrawTextElement) => number,
fallbackValue?: ExcalidrawTextElement["fontSize"],
) => { ) => {
const newFontSizes = new Set<number>(); const newFontSizes = new Set<number>();
@@ -182,7 +191,7 @@ const changeFontSize = (
currentItemFontSize: currentItemFontSize:
newFontSizes.size === 1 newFontSizes.size === 1
? [...newFontSizes][0] ? [...newFontSizes][0]
: appState.currentItemFontSize, : fallbackValue ?? appState.currentItemFontSize,
}, },
commitToHistory: true, commitToHistory: true,
}; };
@@ -190,8 +199,8 @@ const changeFontSize = (
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
export const actionChangeStrokeColor = register({ export const actionChangeFontColor = register({
name: "changeStrokeColor", name: "changeFontColor",
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return { return {
...(value.currentItemStrokeColor && { ...(value.currentItemStrokeColor && {
@@ -199,7 +208,7 @@ export const actionChangeStrokeColor = register({
elements, elements,
appState, appState,
(el) => { (el) => {
return hasStrokeColor(el.type) return isTextElement(el)
? newElementWith(el, { ? newElementWith(el, {
strokeColor: value.currentItemStrokeColor, strokeColor: value.currentItemStrokeColor,
}) })
@@ -215,26 +224,107 @@ export const actionChangeStrokeColor = register({
commitToHistory: !!value.currentItemStrokeColor, commitToHistory: !!value.currentItemStrokeColor,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => {
<> return (
<h3 aria-hidden="true">{t("labels.stroke")}</h3> <>
<ColorPicker <h3 aria-hidden="true">{t("labels.fontColor")}</h3>
type="elementStroke" <ColorPicker
label={t("labels.stroke")} type="elementFontColor"
color={getFormValue( 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, elements,
appState, appState,
(element) => element.strokeColor, (el) => {
appState.currentItemStrokeColor, return (hasStrokeColor(el.type) &&
)} !hasOnlyContainersWithBoundText) ||
onChange={(color) => updateData({ currentItemStrokeColor: color })} !isBoundToContainer(el)
isActive={appState.openPopup === "strokeColorPicker"} ? newElementWith(el, {
setActive={(active) => strokeColor: value.currentItemStrokeColor,
updateData({ openPopup: active ? "strokeColorPicker" : null }) })
} : 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({ export const actionChangeBackgroundColor = register({
@@ -272,6 +362,8 @@ export const actionChangeBackgroundColor = register({
setActive={(active) => setActive={(active) =>
updateData({ openPopup: active ? "backgroundColorPicker" : null }) updateData({ openPopup: active ? "backgroundColorPicker" : null })
} }
elements={elements}
appState={appState}
/> />
</> </>
), ),
@@ -520,7 +612,7 @@ export const actionChangeOpacity = register({
export const actionChangeFontSize = register({ export const actionChangeFontSize = register({
name: "changeFontSize", name: "changeFontSize",
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return changeFontSize(elements, appState, () => value); return changeFontSize(elements, appState, () => value, value);
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => (
<fieldset> <fieldset>
@@ -532,21 +624,25 @@ export const actionChangeFontSize = register({
value: 16, value: 16,
text: t("labels.small"), text: t("labels.small"),
icon: <FontSizeSmallIcon theme={appState.theme} />, icon: <FontSizeSmallIcon theme={appState.theme} />,
testId: "fontSize-small",
}, },
{ {
value: 20, value: 20,
text: t("labels.medium"), text: t("labels.medium"),
icon: <FontSizeMediumIcon theme={appState.theme} />, icon: <FontSizeMediumIcon theme={appState.theme} />,
testId: "fontSize-medium",
}, },
{ {
value: 28, value: 28,
text: t("labels.large"), text: t("labels.large"),
icon: <FontSizeLargeIcon theme={appState.theme} />, icon: <FontSizeLargeIcon theme={appState.theme} />,
testId: "fontSize-large",
}, },
{ {
value: 36, value: 36,
text: t("labels.veryLarge"), text: t("labels.veryLarge"),
icon: <FontSizeExtraLargeIcon theme={appState.theme} />, icon: <FontSizeExtraLargeIcon theme={appState.theme} />,
testId: "fontSize-veryLarge",
}, },
]} ]}
value={getFormValue( value={getFormValue(
@@ -704,9 +800,7 @@ export const actionChangeTextAlign = register({
if (isTextElement(oldElement)) { if (isTextElement(oldElement)) {
const newElement: ExcalidrawTextElement = newElementWith( const newElement: ExcalidrawTextElement = newElementWith(
oldElement, oldElement,
{ { textAlign: value },
textAlign: value,
},
); );
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
@@ -727,47 +821,119 @@ export const actionChangeTextAlign = register({
commitToHistory: true, commitToHistory: true,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData }) => {
<fieldset> return (
<legend>{t("labels.textAlign")}</legend> <fieldset>
<ButtonIconSelect<TextAlign | false> <legend>{t("labels.textAlign")}</legend>
group="text-align" <ButtonIconSelect<TextAlign | false>
options={[ group="text-align"
{ options={[
value: "left", {
text: t("labels.left"), value: "left",
icon: <TextAlignLeftIcon theme={appState.theme} />, text: t("labels.left"),
}, icon: <TextAlignLeftIcon theme={appState.theme} />,
{ },
value: "center", {
text: t("labels.center"), value: "center",
icon: <TextAlignCenterIcon theme={appState.theme} />, text: t("labels.center"),
}, icon: <TextAlignCenterIcon theme={appState.theme} />,
{ },
value: "right", {
text: t("labels.right"), value: "right",
icon: <TextAlignRightIcon theme={appState.theme} />, text: t("labels.right"),
}, icon: <TextAlignRightIcon theme={appState.theme} />,
]} },
value={getFormValue( ]}
elements, value={getFormValue(
appState, elements,
(element) => { appState,
if (isTextElement(element)) { (element) => {
return element.textAlign; 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); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
return boundTextElement.textAlign; return boundTextElement.verticalAlign;
} }
return null; return null;
}, })}
appState.currentItemTextAlign, onChange={(value) => updateData(value)}
)} />
onChange={(value) => updateData(value)} </fieldset>
/> );
</fieldset> },
),
}); });
export const actionChangeSharpness = register({ 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( redrawTextBoundingBox(
element, newElement,
getContainerElement(element), getContainerElement(newElement),
appState, appState,
); );
} }
+44
View File
@@ -0,0 +1,44 @@
import { getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { getBoundTextElement, measureText } from "../element/textElement";
import { ExcalidrawTextElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { getFontString } from "../utils";
import { register } from "./register";
export const actionUnbindText = register({
name: "unbindText",
contextItemLabel: "labels.unbindText",
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const { width, height, baseline } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
);
mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null,
width,
height,
baseline,
text: boundTextElement.originalText,
});
mutateElement(element, {
boundElements: element.boundElements?.filter(
(ele) => ele.id !== boundTextElement.id,
),
});
}
});
return {
elements,
appState,
commitToHistory: true,
};
},
});
+4
View File
@@ -8,6 +8,7 @@ export {
export { actionSelectAll } from "./actionSelectAll"; export { actionSelectAll } from "./actionSelectAll";
export { actionDuplicateSelection } from "./actionDuplicateSelection"; export { actionDuplicateSelection } from "./actionDuplicateSelection";
export { export {
actionChangeFontColor,
actionChangeStrokeColor, actionChangeStrokeColor,
actionChangeBackgroundColor, actionChangeBackgroundColor,
actionChangeStrokeWidth, actionChangeStrokeWidth,
@@ -17,6 +18,7 @@ export {
actionChangeFontSize, actionChangeFontSize,
actionChangeFontFamily, actionChangeFontFamily,
actionChangeTextAlign, actionChangeTextAlign,
actionChangeVerticalAlign,
} from "./actionProperties"; } from "./actionProperties";
export { export {
@@ -80,3 +82,5 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText } from "./actionUnbindText";
export { actionLink } from "../element/Hyperlink";
+37 -1
View File
@@ -10,6 +10,31 @@ import {
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppClassProperties, AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { MODES } from "../constants"; 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 { export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"]; 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; return false;
} }
const action = data[0];
const { viewModeEnabled } = this.getAppState(); const { viewModeEnabled } = this.getAppState();
if (viewModeEnabled) { if (viewModeEnabled) {
if (!Object.values(MODES).includes(data[0].name)) { if (!Object.values(MODES).includes(data[0].name)) {
@@ -75,6 +106,8 @@ export class ActionManager implements ActionsManagerInterface {
} }
} }
trackAction(action, "keyboard", null);
event.preventDefault(); event.preventDefault();
this.updater( this.updater(
data[0].perform( data[0].perform(
@@ -96,6 +129,7 @@ export class ActionManager implements ActionsManagerInterface {
this.app, this.app,
), ),
); );
trackAction(action, "api", null);
} }
/** /**
@@ -122,6 +156,8 @@ export class ActionManager implements ActionsManagerInterface {
this.app, this.app,
), ),
); );
trackAction(action, "ui", formState);
}; };
return ( return (
+4 -2
View File
@@ -2,7 +2,9 @@ import { Action } from "./types";
export let actions: readonly Action[] = []; export let actions: readonly Action[] = [];
export const register = (action: Action): Action => { export const register = <T extends Action>(action: T) => {
actions = actions.concat(action); actions = actions.concat(action);
return action; return action as T & {
keyTest?: unknown extends T["keyTest"] ? never : T["keyTest"];
};
}; };
+8 -3
View File
@@ -1,8 +1,10 @@
import { t } from "../i18n"; import { t } from "../i18n";
import { isDarwin } from "../keys"; import { isDarwin } from "../keys";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
export type ShortcutName = export type ShortcutName = SubtypeOf<
ActionName,
| "cut" | "cut"
| "copy" | "copy"
| "paste" | "paste"
@@ -25,7 +27,9 @@ export type ShortcutName =
| "addToLibrary" | "addToLibrary"
| "viewMode" | "viewMode"
| "flipHorizontal" | "flipHorizontal"
| "flipVertical"; | "flipVertical"
| "hyperlink"
>;
const shortcutMap: Record<ShortcutName, string[]> = { const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")], cut: [getShortcutKey("CtrlOrCmd+X")],
@@ -62,10 +66,11 @@ const shortcutMap: Record<ShortcutName, string[]> = {
flipHorizontal: [getShortcutKey("Shift+H")], flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")], flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")], viewMode: [getShortcutKey("Alt+R")],
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
}; };
export const getShortcutFromShortcutName = (name: ShortcutName) => { export const getShortcutFromShortcutName = (name: ShortcutName) => {
const shortcuts = shortcutMap[name]; 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] : ""; return shortcuts && shortcuts.length > 0 ? shortcuts[0] : "";
}; };
+15 -2
View File
@@ -49,6 +49,7 @@ export type ActionName =
| "gridMode" | "gridMode"
| "zenMode" | "zenMode"
| "stats" | "stats"
| "changeFontColor"
| "changeStrokeColor" | "changeStrokeColor"
| "changeBackgroundColor" | "changeBackgroundColor"
| "changeFillStyle" | "changeFillStyle"
@@ -82,6 +83,7 @@ export type ActionName =
| "zoomToSelection" | "zoomToSelection"
| "changeFontFamily" | "changeFontFamily"
| "changeTextAlign" | "changeTextAlign"
| "changeVerticalAlign"
| "toggleFullScreen" | "toggleFullScreen"
| "toggleShortcuts" | "toggleShortcuts"
| "group" | "group"
@@ -103,7 +105,10 @@ export type ActionName =
| "exportWithDarkMode" | "exportWithDarkMode"
| "toggleTheme" | "toggleTheme"
| "increaseFontSize" | "increaseFontSize"
| "decreaseFontSize"; | "decreaseFontSize"
| "unbindText"
| "hyperlink"
| "eraser";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
@@ -123,12 +128,20 @@ export interface Action {
appState: AppState, appState: AppState,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
) => boolean; ) => boolean;
contextItemLabel?: string; contextItemLabel?:
| string
| ((
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
) => string);
contextItemPredicate?: ( contextItemPredicate?: (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
) => boolean; ) => boolean;
checked?: (appState: Readonly<AppState>) => boolean; checked?: (appState: Readonly<AppState>) => boolean;
trackEvent?:
| boolean
| ((action: Action, type: "ui" | "keyboard" | "api", value: any) => void);
} }
export interface ActionsManagerInterface { export interface ActionsManagerInterface {
+5 -5
View File
@@ -3,16 +3,16 @@ export const trackEvent =
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID && process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
typeof window !== "undefined" && typeof window !== "undefined" &&
window.gtag window.gtag
? (category: string, name: string, label?: string, value?: number) => { ? (category: string, action: string, label?: string, value?: number) => {
window.gtag("event", name, { window.gtag("event", action, {
event_category: category, event_category: category,
event_label: label, event_label: label,
value, value,
}); });
} }
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID : typeof process !== "undefined" && process.env?.JEST_WORKER_ID
? (category: string, name: string, label?: string, value?: number) => {} ? (category: string, action: string, label?: string, value?: number) => {}
: (category: string, name: string, label?: string, value?: number) => { : (category: string, action: string, label?: string, value?: number) => {
// Uncomment the next line to track locally // Uncomment the next line to track locally
// console.info("Track Event", category, name, label, value); // console.info("Track Event", category, action, label, value);
}; };
+15 -1
View File
@@ -43,6 +43,8 @@ export const getDefaultAppState = (): Omit<
editingLinearElement: null, editingLinearElement: null,
elementLocked: false, elementLocked: false,
elementType: "selection", elementType: "selection",
penMode: false,
penDetected: false,
errorMessage: null, errorMessage: null,
exportBackground: true, exportBackground: true,
exportScale: defaultExportScale, exportScale: defaultExportScale,
@@ -77,9 +79,12 @@ export const getDefaultAppState = (): Omit<
toastMessage: null, toastMessage: null,
viewBackgroundColor: oc.white, viewBackgroundColor: oc.white,
zenModeEnabled: false, zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } }, zoom: {
value: 1 as NormalizedZoomValue,
},
viewModeEnabled: false, viewModeEnabled: false,
pendingImageElement: null, pendingImageElement: null,
showHyperlinkPopup: false,
}; };
}; };
@@ -127,6 +132,8 @@ const APP_STATE_STORAGE_CONF = (<
editingLinearElement: { browser: false, export: false, server: false }, editingLinearElement: { browser: false, export: false, server: false },
elementLocked: { browser: true, export: false, server: false }, elementLocked: { browser: true, export: false, server: false },
elementType: { browser: true, export: false, server: false }, elementType: { browser: true, export: false, server: false },
penMode: { browser: false, export: false, server: false },
penDetected: { browser: false, export: false, server: false },
errorMessage: { browser: false, export: false, server: false }, errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false }, exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false }, exportEmbedScene: { browser: true, export: false, server: false },
@@ -168,6 +175,7 @@ const APP_STATE_STORAGE_CONF = (<
zoom: { browser: true, export: false, server: false }, zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false }, viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElement: { browser: false, export: false, server: false }, pendingImageElement: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <
@@ -205,3 +213,9 @@ export const cleanAppStateForExport = (appState: Partial<AppState>) => {
export const clearAppStateForDatabase = (appState: Partial<AppState>) => { export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "server"); 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 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 { newElement, newLinearElement, newTextElement } from "./element";
import { NonDeletedExcalidrawElement } from "./element/types"; import { NonDeletedExcalidrawElement } from "./element/types";
import { randomId } from "./random"; import { randomId } from "./random";
@@ -103,7 +108,7 @@ const transposeCells = (cells: string[][]) => {
}; };
export const tryParseSpreadsheet = (text: string): ParseSpreadsheetResult => { 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 // For now we only accept 2 columns with an optional header
// Check for tab separated values // Check for tab separated values
@@ -161,7 +166,7 @@ const commonProps = {
strokeSharpness: "sharp", strokeSharpness: "sharp",
strokeStyle: "solid", strokeStyle: "solid",
strokeWidth: 1, strokeWidth: 1,
verticalAlign: "middle", verticalAlign: VERTICAL_ALIGN.MIDDLE,
} as const; } as const;
const getChartDimentions = (spreadsheet: Spreadsheet) => { 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 ( export const parseClipboard = async (
event: ClipboardEvent | null, event: ClipboardEvent | null,
+28 -6
View File
@@ -19,6 +19,7 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
@@ -29,12 +30,21 @@ export const SelectedShapeActions = ({
appState: AppState; appState: AppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
elementType: ExcalidrawElement["type"]; elementType: AppState["elementType"];
}) => { }) => {
const targetElements = getTargetElements( const targetElements = getTargetElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
); );
let isSingleElementBoundContainer = false;
if (
targetElements.length === 2 &&
(hasBoundTextElement(targetElements[0]) ||
hasBoundTextElement(targetElements[1]))
) {
isSingleElementBoundContainer = true;
}
const isEditing = Boolean(appState.editingElement); const isEditing = Boolean(appState.editingElement);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; 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 ( return (
<div className="panelColumn"> <div className="panelColumn">
{hasOnlyContainersWithBoundText && renderAction("changeFontColor")}
{((hasStrokeColor(elementType) && {((hasStrokeColor(elementType) &&
elementType !== "image" && elementType !== "image" &&
commonSelectedType !== "image") || commonSelectedType !== "image") ||
@@ -100,6 +117,10 @@ export const SelectedShapeActions = ({
</> </>
)} )}
{targetElements.some(
(element) =>
hasBoundTextElement(element) || isBoundToContainer(element),
) && renderAction("changeVerticalAlign")}
{(canHaveArrowheads(elementType) || {(canHaveArrowheads(elementType) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && ( targetElements.some((element) => canHaveArrowheads(element.type))) && (
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
@@ -117,7 +138,7 @@ export const SelectedShapeActions = ({
</div> </div>
</fieldset> </fieldset>
{targetElements.length > 1 && ( {targetElements.length > 1 && !isSingleElementBoundContainer && (
<fieldset> <fieldset>
<legend>{t("labels.align")}</legend> <legend>{t("labels.align")}</legend>
<div className="buttonList"> <div className="buttonList">
@@ -150,14 +171,15 @@ export const SelectedShapeActions = ({
</div> </div>
</fieldset> </fieldset>
)} )}
{!isMobile && !isEditing && targetElements.length > 0 && ( {!isEditing && targetElements.length > 0 && (
<fieldset> <fieldset>
<legend>{t("labels.actions")}</legend> <legend>{t("labels.actions")}</legend>
<div className="buttonList"> <div className="buttonList">
{renderAction("duplicateSelection")} {!isMobile && renderAction("duplicateSelection")}
{renderAction("deleteSelectedElements")} {!isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{targetElements.length === 1 && renderAction("hyperlink")}
</div> </div>
</fieldset> </fieldset>
)} )}
@@ -172,7 +194,7 @@ export const ShapesSwitcher = ({
onImageAction, onImageAction,
}: { }: {
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
elementType: ExcalidrawElement["type"]; elementType: AppState["elementType"];
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void; onImageAction: (data: { pointerType: PointerType | null }) => void;
}) => ( }) => (
+584 -149
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -7,7 +7,7 @@ export const ButtonIconSelect = <T extends Object>({
onChange, onChange,
group, group,
}: { }: {
options: { value: T; text: string; icon: JSX.Element }[]; options: { value: T; text: string; icon: JSX.Element; testId?: string }[];
value: T | null; value: T | null;
onChange: (value: T) => void; onChange: (value: T) => void;
group: string; group: string;
@@ -24,6 +24,7 @@ export const ButtonIconSelect = <T extends Object>({
name={group} name={group}
onChange={() => onChange(option.value)} onChange={() => onChange(option.value)}
checked={value === option.value} checked={value === option.value}
data-testid={option.testId}
/> />
{option.icon} {option.icon}
</label> </label>
+23 -2
View File
@@ -46,7 +46,7 @@
top: -11px; top: -11px;
} }
.color-picker-content { .color-picker-content--default {
padding: 0.5rem; padding: 0.5rem;
display: grid; display: grid;
grid-template-columns: repeat(5, auto); 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 { .color-picker-content .color-input-container {
grid-column: 1 / span 5; grid-column: 1 / span 5;
} }
@@ -235,7 +255,8 @@
color: #aaa; 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; color: #d4d4d4;
} }
+167 -46
View File
@@ -7,6 +7,53 @@ import { isArrowKey, KEYS } from "../keys";
import { t, getLanguage } from "../i18n"; import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils"; import { isWritableElement } from "../utils";
import colors from "../colors"; 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 isValidColor = (color: string) => {
const style = new Option().style; const style = new Option().style;
@@ -35,6 +82,7 @@ const keyBindings = [
["1", "2", "3", "4", "5"], ["1", "2", "3", "4", "5"],
["q", "w", "e", "r", "t"], ["q", "w", "e", "r", "t"],
["a", "s", "d", "f", "g"], ["a", "s", "d", "f", "g"],
["z", "x", "c", "v", "b"],
].flat(); ].flat();
const Picker = ({ const Picker = ({
@@ -45,6 +93,7 @@ const Picker = ({
label, label,
showInput = true, showInput = true,
type, type,
elements,
}: { }: {
colors: string[]; colors: string[];
color: string | null; color: string | null;
@@ -52,12 +101,25 @@ const Picker = ({
onClose: () => void; onClose: () => void;
label: string; label: string;
showInput: boolean; showInput: boolean;
type: "canvasBackground" | "elementBackground" | "elementStroke"; type:
| "canvasBackground"
| "elementBackground"
| "elementStroke"
| "elementFontColor";
elements: readonly ExcalidrawElement[];
}) => { }) => {
const firstItem = React.useRef<HTMLButtonElement>(); const firstItem = React.useRef<HTMLButtonElement>();
const activeItem = React.useRef<HTMLButtonElement>(); const activeItem = React.useRef<HTMLButtonElement>();
const gallery = React.useRef<HTMLDivElement>(); const gallery = React.useRef<HTMLDivElement>();
const colorInput = React.useRef<HTMLInputElement>(); 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(() => { React.useEffect(() => {
// After the component is first mounted focus on first input // After the component is first mounted focus on first input
@@ -85,23 +147,42 @@ const Picker = ({
} else if (isArrowKey(event.key)) { } else if (isArrowKey(event.key)) {
const { activeElement } = document; const { activeElement } = document;
const isRTL = getLanguage().rtl; const isRTL = getLanguage().rtl;
const index = Array.prototype.indexOf.call( let isCustom = false;
gallery!.current!.children, let index = Array.prototype.indexOf.call(
gallery!.current!.querySelector(".color-picker-content--default")!
.children,
activeElement, 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) { if (index !== -1) {
const length = gallery!.current!.children.length - (showInput ? 1 : 0); const length = parentSelector!.children.length - (showInput ? 1 : 0);
const nextIndex = const nextIndex =
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT) event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
? (index + 1) % length ? (index + 1) % length
: event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT) : event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
? (length + index - 1) % length ? (length + index - 1) % length
: event.key === KEYS.ARROW_DOWN : !isCustom && event.key === KEYS.ARROW_DOWN
? (index + 5) % length ? (index + 5) % length
: event.key === KEYS.ARROW_UP : !isCustom && event.key === KEYS.ARROW_UP
? (length + index - 5) % length ? (length + index - 5) % length
: index; : index;
(gallery!.current!.children![nextIndex] as any).focus(); (parentSelector!.children![nextIndex] as HTMLElement)?.focus();
} }
event.preventDefault(); event.preventDefault();
} else if ( } else if (
@@ -109,7 +190,15 @@ const Picker = ({
!isWritableElement(event.target) !isWritableElement(event.target)
) { ) {
const index = keyBindings.indexOf(event.key.toLowerCase()); 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(); event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) { } else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
event.preventDefault(); event.preventDefault();
@@ -119,6 +208,50 @@ const Picker = ({
event.stopPropagation(); 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 ( return (
<div <div
className={`color-picker color-picker-type-${type}`} className={`color-picker color-picker-type-${type}`}
@@ -138,41 +271,20 @@ const Picker = ({
}} }}
tabIndex={0} tabIndex={0}
> >
{colors.map((_color, i) => { <div className="color-picker-content--default">
const _colorWithoutHash = _color.replace("#", ""); {renderColors(colors)}
return ( </div>
<button {!!customColors.length && (
className="color-picker-swatch" <div className="color-picker-content--canvas">
onClick={(event) => { <span className="color-picker-content--canvas-title">
(event.currentTarget as HTMLButtonElement).focus(); {t("labels.canvasColors")}
onChange(_color); </span>
}} <div className="color-picker-content--canvas-colors">
title={`${t(`colors.${_colorWithoutHash}`)}${ {renderColors(customColors, true)}
!isTransparent(_color) ? ` (${_color})` : "" </div>
} — ${keyBindings[i].toUpperCase()}`} </div>
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>
);
})}
{showInput && ( {showInput && (
<ColorInput <ColorInput
color={color} color={color}
@@ -246,16 +358,24 @@ export const ColorPicker = ({
label, label,
isActive, isActive,
setActive, setActive,
elements,
appState,
}: { }: {
type: "canvasBackground" | "elementBackground" | "elementStroke"; type:
| "canvasBackground"
| "elementBackground"
| "elementStroke"
| "elementFontColor";
color: string | null; color: string | null;
onChange: (color: string) => void; onChange: (color: string) => void;
label: string; label: string;
isActive: boolean; isActive: boolean;
setActive: (active: boolean) => void; setActive: (active: boolean) => void;
elements: readonly ExcalidrawElement[];
appState: AppState;
}) => { }) => {
const pickerButton = React.useRef<HTMLButtonElement>(null); const pickerButton = React.useRef<HTMLButtonElement>(null);
const colorType = type === "elementFontColor" ? "elementStroke" : type;
return ( return (
<div> <div>
<div className="color-picker-control-container"> <div className="color-picker-control-container">
@@ -282,7 +402,7 @@ export const ColorPicker = ({
} }
> >
<Picker <Picker
colors={colors[type]} colors={colors[colorType]}
color={color || null} color={color || null}
onChange={(changedColor) => { onChange={(changedColor) => {
onChange(changedColor); onChange(changedColor);
@@ -294,6 +414,7 @@ export const ColorPicker = ({
label={label} label={label}
showInput={false} showInput={false}
type={type} type={type}
elements={elements}
/> />
</Popover> </Popover>
) : null} ) : null}
+17 -3
View File
@@ -11,6 +11,7 @@ import {
import { Action } from "../actions/types"; import { Action } from "../actions/types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { AppState } from "../types"; import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types";
export type ContextMenuOption = "separator" | Action; export type ContextMenuOption = "separator" | Action;
@@ -21,6 +22,7 @@ type ContextMenuProps = {
left: number; left: number;
actionManager: ActionManager; actionManager: ActionManager;
appState: Readonly<AppState>; appState: Readonly<AppState>;
elements: readonly NonDeletedExcalidrawElement[];
}; };
const ContextMenu = ({ const ContextMenu = ({
@@ -30,6 +32,7 @@ const ContextMenu = ({
left, left,
actionManager, actionManager,
appState, appState,
elements,
}: ContextMenuProps) => { }: ContextMenuProps) => {
return ( return (
<Popover <Popover
@@ -37,6 +40,10 @@ const ContextMenu = ({
top={top} top={top}
left={left} left={left}
fitInViewport={true} fitInViewport={true}
offsetLeft={appState.offsetLeft}
offsetTop={appState.offsetTop}
viewportWidth={appState.width}
viewportHeight={appState.height}
> >
<ul <ul
className="context-menu" className="context-menu"
@@ -48,9 +55,14 @@ const ContextMenu = ({
} }
const actionName = option.name; const actionName = option.name;
const label = option.contextItemLabel let label = "";
? t(option.contextItemLabel) if (option.contextItemLabel) {
: ""; if (typeof option.contextItemLabel === "function") {
label = t(option.contextItemLabel(elements, appState));
} else {
label = t(option.contextItemLabel);
}
}
return ( return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}> <li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button <button
@@ -97,6 +109,7 @@ type ContextMenuParams = {
actionManager: ContextMenuProps["actionManager"]; actionManager: ContextMenuProps["actionManager"];
appState: Readonly<AppState>; appState: Readonly<AppState>;
container: HTMLElement; container: HTMLElement;
elements: readonly NonDeletedExcalidrawElement[];
}; };
const handleClose = (container: HTMLElement) => { const handleClose = (container: HTMLElement) => {
@@ -125,6 +138,7 @@ export default {
onCloseRequest={() => handleClose(params.container)} onCloseRequest={() => handleClose(params.container)}
actionManager={params.actionManager} actionManager={params.actionManager}
appState={params.appState} appState={params.appState}
elements={params.elements}
/>, />,
getContextMenuNode(params.container), getContextMenuNode(params.container),
); );
+4
View File
@@ -205,6 +205,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("helpDialog.preventBinding")} label={t("helpDialog.preventBinding")}
shortcuts={[getShortcutKey("CtrlOrCmd")]} shortcuts={[getShortcutKey("CtrlOrCmd")]}
/> />
<Shortcut
label={t("toolBar.link")}
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
/>
</ShortcutIsland> </ShortcutIsland>
<ShortcutIsland caption={t("helpDialog.view")}> <ShortcutIsland caption={t("helpDialog.view")}>
<Shortcut <Shortcut
+4
View File
@@ -11,6 +11,7 @@ import {
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
import { isEraserActive } from "../appState";
interface HintViewerProps { interface HintViewerProps {
appState: AppState; appState: AppState;
@@ -22,6 +23,9 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState; const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null; const multiMode = appState.multiElement !== null;
if (isEraserActive(appState)) {
return t("hints.eraserRevert");
}
if (elementType === "arrow" || elementType === "line") { if (elementType === "arrow" || elementType === "line") {
if (!multiMode) { if (!multiMode) {
return t("hints.linearElement"); return t("hints.linearElement");
+20 -1
View File
@@ -36,6 +36,7 @@ import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss"; import "./LayerUI.scss";
import "./Toolbar.scss"; import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@@ -46,6 +47,7 @@ interface LayerUIProps {
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
onCollabButtonClick?: () => void; onCollabButtonClick?: () => void;
onLockToggle: () => void; onLockToggle: () => void;
onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
zenModeEnabled: boolean; zenModeEnabled: boolean;
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
@@ -76,6 +78,7 @@ const LayerUI = ({
elements, elements,
onCollabButtonClick, onCollabButtonClick,
onLockToggle, onLockToggle,
onPenModeToggle,
onInsertElements, onInsertElements,
zenModeEnabled, zenModeEnabled,
showExitZenModeBtn, showExitZenModeBtn,
@@ -235,7 +238,7 @@ const LayerUI = ({
className={CLASSES.SHAPE_ACTIONS_MENU} className={CLASSES.SHAPE_ACTIONS_MENU}
padding={2} padding={2}
style={{ 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 // 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 // if active file name is displayed, subtracting 248 to account for its height
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`, maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
@@ -313,6 +316,13 @@ const LayerUI = ({
"zen-mode": zenModeEnabled, "zen-mode": zenModeEnabled,
})} })}
> >
<PenModeButton
zenModeEnabled={zenModeEnabled}
checked={appState.penMode}
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
/>
<LockButton <LockButton
zenModeEnabled={zenModeEnabled} zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked} checked={appState.elementLocked}
@@ -418,6 +428,14 @@ const LayerUI = ({
{actionManager.renderAction("redo", { size: "small" })} {actionManager.renderAction("redo", { size: "small" })}
</div> </div>
)} )}
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</Section> </Section>
</Stack.Col> </Stack.Col>
</div> </div>
@@ -498,6 +516,7 @@ const LayerUI = ({
setAppState={setAppState} setAppState={setAppState}
onCollabButtonClick={onCollabButtonClick} onCollabButtonClick={onCollabButtonClick}
onLockToggle={onLockToggle} onLockToggle={onLockToggle}
onPenModeToggle={onPenModeToggle}
canvas={canvas} canvas={canvas}
isCollaborating={isCollaborating} isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter} renderCustomFooter={renderCustomFooter}
+21 -1
View File
@@ -8,7 +8,7 @@ import { NonDeletedExcalidrawElement } from "../element/types";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
import { Island } from "./Island"; import { Island } from "./Island";
import { HintViewer } from "./HintViewer"; import { HintViewer } from "./HintViewer";
import { calculateScrollCenter } from "../scene"; import { calculateScrollCenter, getSelectedElements } from "../scene";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section"; import { Section } from "./Section";
import CollabButton from "./CollabButton"; import CollabButton from "./CollabButton";
@@ -17,6 +17,7 @@ import { LockButton } from "./LockButton";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import { LibraryButton } from "./LibraryButton"; import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: AppState;
@@ -28,6 +29,7 @@ type MobileMenuProps = {
libraryMenu: JSX.Element | null; libraryMenu: JSX.Element | null;
onCollabButtonClick?: () => void; onCollabButtonClick?: () => void;
onLockToggle: () => void; onLockToggle: () => void;
onPenModeToggle: () => void;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
isCollaborating: boolean; isCollaborating: boolean;
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element; renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
@@ -50,6 +52,7 @@ export const MobileMenu = ({
setAppState, setAppState,
onCollabButtonClick, onCollabButtonClick,
onLockToggle, onLockToggle,
onPenModeToggle,
canvas, canvas,
isCollaborating, isCollaborating,
renderCustomFooter, renderCustomFooter,
@@ -92,6 +95,13 @@ export const MobileMenu = ({
setAppState={setAppState} setAppState={setAppState}
isMobile isMobile
/> />
<PenModeButton
checked={appState.penMode}
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
isMobile
penDetected={appState.penDetected}
/>
</Stack.Row> </Stack.Row>
{libraryMenu} {libraryMenu}
</Stack.Col> </Stack.Col>
@@ -103,6 +113,12 @@ export const MobileMenu = ({
}; };
const renderAppToolbar = () => { const renderAppToolbar = () => {
// Render eraser conditionally in mobile
const showEraser =
!appState.viewModeEnabled &&
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
if (viewModeEnabled) { if (viewModeEnabled) {
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
@@ -110,12 +126,16 @@ export const MobileMenu = ({
</div> </div>
); );
} }
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")} {actionManager.renderAction("toggleCanvasMenu")}
{actionManager.renderAction("toggleEditMenu")} {actionManager.renderAction("toggleEditMenu")}
{actionManager.renderAction("undo")} {actionManager.renderAction("undo")}
{actionManager.renderAction("redo")} {actionManager.renderAction("redo")}
{showEraser && actionManager.renderAction("eraser")}
{actionManager.renderAction( {actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection", appState.multiElement ? "finalize" : "duplicateSelection",
)} )}
+91
View File
@@ -0,0 +1,91 @@
import "./ToolIcon.scss";
import clsx from "clsx";
import { ToolButtonSize } from "./ToolButton";
type PenModeIconProps = {
title?: string;
name?: string;
checked: boolean;
onChange?(): void;
zenModeEnabled?: boolean;
isMobile?: boolean;
penDetected: boolean;
};
const DEFAULT_SIZE: ToolButtonSize = "medium";
const ICONS = {
CHECKED: (
<svg
width="205"
height="205"
viewBox="0 0 205 205"
xmlns="http://www.w3.org/2000/svg"
>
<path d="m35 195-25-29.17V50h50v115l-25 30" />
<path d="M10 40V10h50v30H10" />
<path d="M125 145h70v50h-70" />
<path d="M190 145v-30l-10-20h-40l-10 20v30h15v-30l5-5h20l5 5v30h15" />
</svg>
),
UNCHECKED: (
<svg
width="205"
height="205"
viewBox="0 0 205 205"
xmlns="http://www.w3.org/2000/svg"
className="unlocked-icon rtl-mirror"
>
<path d="m35 195-25-29.17V50h50v115l-25 30" />
<path d="M10 40V10h50v30H10" />
<path d="M125 145h70v50h-70" />
<path d="M145 145v-30l-10-20H95l-10 20v30h15v-30l5-5h20l5 5v30h15" />
</svg>
),
};
export const PenModeButton = (props: PenModeIconProps) => {
if (!props.penDetected) {
if (props.isMobile) {
return null;
}
return (
<label
className={clsx(
"ToolIcon ToolIcon__penMode ToolIcon_type_floating",
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"is-mobile": props.isMobile,
},
)}
>
<div className="ToolIcon__icon ToolIcon__hidden" />
</label>
);
}
return (
<label
className={clsx(
"ToolIcon ToolIcon__penMode ToolIcon_type_floating",
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"is-mobile": props.isMobile,
},
)}
title={`${props.title}`}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name={props.name}
onChange={props.onChange}
checked={props.checked}
aria-label={props.title}
/>
<div className="ToolIcon__icon">
{props.checked ? ICONS.CHECKED : ICONS.UNCHECKED}
</div>
</label>
);
};
+11 -6
View File
@@ -8,6 +8,10 @@ type Props = {
children?: React.ReactNode; children?: React.ReactNode;
onCloseRequest?(event: PointerEvent): void; onCloseRequest?(event: PointerEvent): void;
fitInViewport?: boolean; fitInViewport?: boolean;
offsetLeft?: number;
offsetTop?: number;
viewportWidth?: number;
viewportHeight?: number;
}; };
export const Popover = ({ export const Popover = ({
@@ -16,6 +20,10 @@ export const Popover = ({
top, top,
onCloseRequest, onCloseRequest,
fitInViewport = false, fitInViewport = false,
offsetLeft = 0,
offsetTop = 0,
viewportWidth = window.innerWidth,
viewportHeight = window.innerHeight,
}: Props) => { }: Props) => {
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);
@@ -24,17 +32,14 @@ export const Popover = ({
if (fitInViewport && popoverRef.current) { if (fitInViewport && popoverRef.current) {
const element = popoverRef.current; const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect(); const { x, y, width, height } = element.getBoundingClientRect();
if (x + width - offsetLeft > viewportWidth) {
const viewportWidth = window.innerWidth;
if (x + width > viewportWidth) {
element.style.left = `${viewportWidth - width}px`; element.style.left = `${viewportWidth - width}px`;
} }
const viewportHeight = window.innerHeight; if (y + height - offsetTop > viewportHeight) {
if (y + height > viewportHeight) {
element.style.top = `${viewportHeight - height}px`; element.style.top = `${viewportHeight - height}px`;
} }
} }
}, [fitInViewport]); }, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);
useEffect(() => { useEffect(() => {
if (onCloseRequest) { if (onCloseRequest) {
+4
View File
@@ -219,6 +219,10 @@
margin-inline-end: 0; margin-inline-end: 0;
top: 60px; top: 60px;
} }
.ToolIcon.ToolIcon__penMode {
margin-inline-end: 0;
top: 140px;
}
} }
.unlocked-icon { .unlocked-icon {
+14 -2
View File
@@ -46,6 +46,12 @@
} }
} }
.ToolIcon__hidden {
box-shadow: none !important;
background-color: transparent !important;
pointer-events: none !important;
}
.ToolIcon.ToolIcon__lock { .ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor); margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating { &.ToolIcon_type_floating {
@@ -81,8 +87,14 @@
.ToolIcon { .ToolIcon {
&:hover { &:hover {
--icon-fill-color: var(--color-primary-chubb); --icon-fill-color: var(
--keybinding-color: var(--color-primary-chubb); --color-primary-contrast-offset,
var(--color-primary)
);
--keybinding-color: var(
--color-primary-contrast-offset,
var(--color-primary)
);
} }
&:active { &:active {
--icon-fill-color: #{$oc-gray-9}; --icon-fill-color: #{$oc-gray-9};
+47 -31
View File
@@ -2,7 +2,7 @@ import "./Tooltip.scss";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
const getTooltipDiv = () => { export const getTooltipDiv = () => {
const existingDiv = document.querySelector<HTMLDivElement>( const existingDiv = document.querySelector<HTMLDivElement>(
".excalidraw-tooltip", ".excalidraw-tooltip",
); );
@@ -15,6 +15,50 @@ const getTooltipDiv = () => {
return div; return div;
}; };
export const updateTooltipPosition = (
tooltip: HTMLDivElement,
item: {
left: number;
top: number;
width: number;
height: number;
},
position: "bottom" | "top" = "bottom",
) => {
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const margin = 5;
let left = item.left + item.width / 2 - tooltipRect.width / 2;
if (left < 0) {
left = margin;
} else if (left + tooltipRect.width >= viewportWidth) {
left = viewportWidth - tooltipRect.width - margin;
}
let top: number;
if (position === "bottom") {
top = item.top + item.height + margin;
if (top + tooltipRect.height >= viewportHeight) {
top = item.top - tooltipRect.height - margin;
}
} else {
top = item.top - tooltipRect.height - margin;
if (top < 0) {
top = item.top + item.height + margin;
}
}
Object.assign(tooltip.style, {
top: `${top}px`,
left: `${left}px`,
});
};
const updateTooltip = ( const updateTooltip = (
item: HTMLDivElement, item: HTMLDivElement,
tooltip: HTMLDivElement, tooltip: HTMLDivElement,
@@ -27,35 +71,8 @@ const updateTooltip = (
tooltip.textContent = label; tooltip.textContent = label;
const { const itemRect = item.getBoundingClientRect();
x: itemX, updateTooltipPosition(tooltip, itemRect);
bottom: itemBottom,
top: itemTop,
width: itemWidth,
} = item.getBoundingClientRect();
const { width: labelWidth, height: labelHeight } =
tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const margin = 5;
const left = itemX + itemWidth / 2 - labelWidth / 2;
const offsetLeft =
left + labelWidth >= viewportWidth ? left + labelWidth - viewportWidth : 0;
const top = itemBottom + margin;
const offsetTop =
top + labelHeight >= viewportHeight
? itemBottom - itemTop + labelHeight + margin * 2
: 0;
Object.assign(tooltip.style, {
top: `${top - offsetTop}px`,
left: `${left - offsetLeft}px`,
});
}; };
type TooltipProps = { type TooltipProps = {
@@ -75,7 +92,6 @@ export const Tooltip = ({
return () => return () =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible"); getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
}, []); }, []);
return ( return (
<div <div
className="excalidraw-tooltip-wrapper" className="excalidraw-tooltip-wrapper"
+46
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( export const publishIcon = createIcon(
<path <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" 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"
@@ -892,3 +926,15 @@ export const publishIcon = createIcon(
/>, />,
{ width: 640, height: 512 }, { width: 640, height: 512 },
); );
export const editIcon = createIcon(
<path
fill="currentColor"
d="M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z"
></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" />,
);
+10 -1
View File
@@ -24,7 +24,7 @@ export const POINTER_BUTTON = {
WHEEL: 1, WHEEL: 1,
SECONDARY: 2, SECONDARY: 2,
TOUCH: -1, TOUCH: -1,
}; } as const;
export enum EVENT { export enum EVENT {
COPY = "copy", COPY = "copy",
@@ -52,6 +52,8 @@ export enum EVENT {
HASHCHANGE = "hashchange", HASHCHANGE = "hashchange",
VISIBILITY_CHANGE = "visibilitychange", VISIBILITY_CHANGE = "visibilitychange",
SCROLL = "scroll", SCROLL = "scroll",
// custom events
EXCALIDRAW_LINK = "excalidraw-link",
} }
export const ENV = { export const ENV = {
@@ -115,6 +117,7 @@ export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000; export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 100; export const SCROLL_TIMEOUT = 100;
export const ZOOM_STEP = 0.1; export const ZOOM_STEP = 0.1;
export const HYPERLINK_TOOLTIP_DELAY = 300;
// Report a user inactive after IDLE_THRESHOLD milliseconds // Report a user inactive after IDLE_THRESHOLD milliseconds
export const IDLE_THRESHOLD = 60_000; export const IDLE_THRESHOLD = 60_000;
@@ -179,3 +182,9 @@ export const VERSIONS = {
} as const; } as const;
export const BOUND_TEXT_PADDING = 5; 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%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
.eraser {
&.ToolIcon:hover {
--icon-fill-color: #fff;
--keybinding-color: #fff;
}
&.active {
background-color: var(--color-primary);
}
}
} }
.App-toolbar-content { .App-toolbar-content {
@@ -467,7 +477,8 @@
font-family: var(--ui-font); font-family: var(--ui-font);
} }
.undo-redo-buttons { .undo-redo-buttons,
.eraser-buttons {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
gap: 0.4em; gap: 0.4em;
-2
View File
@@ -38,7 +38,6 @@
--text-primary-color: #{$oc-gray-8}; --text-primary-color: #{$oc-gray-8};
--color-primary: #6965db; --color-primary: #6965db;
--color-primary-chubb: #625ee0; // to offset Chubb illusion
--color-primary-darker: #5b57d1; --color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1; --color-primary-darkest: #4a47b1;
--color-primary-light: #e2e1fc; --color-primary-light: #e2e1fc;
@@ -85,7 +84,6 @@
--text-primary-color: #{$oc-gray-4}; --text-primary-color: #{$oc-gray-4};
--color-primary: #5650f0; --color-primary: #5650f0;
--color-primary-chubb: #726dff; // to offset Chubb illusion
--color-primary-darker: #4b46d8; --color-primary-darker: #4b46d8;
--color-primary-darkest: #3e39be; --color-primary-darkest: #3e39be;
--color-primary-light: #3f3d64; --color-primary-light: #3f3d64;
+4 -3
View File
@@ -31,8 +31,8 @@ type RestoredAppState = Omit<
>; >;
export const AllowedExcalidrawElementTypes: Record< export const AllowedExcalidrawElementTypes: Record<
ExcalidrawElement["type"], AppState["elementType"],
true boolean
> = { > = {
selection: true, selection: true,
text: true, text: true,
@@ -43,6 +43,7 @@ export const AllowedExcalidrawElementTypes: Record<
image: true, image: true,
arrow: true, arrow: true,
freedraw: true, freedraw: true,
eraser: false,
}; };
export type RestoredDataState = { export type RestoredDataState = {
@@ -105,6 +106,7 @@ const restoreElementWithProperties = <
? element.boundElementIds.map((id) => ({ type: "arrow", id })) ? element.boundElementIds.map((id) => ({ type: "arrow", id }))
: element.boundElements ?? [], : element.boundElements ?? [],
updated: element.updated ?? getUpdatedTimestamp(), updated: element.updated ?? getUpdatedTimestamp(),
link: element.link ?? null,
}; };
return { return {
@@ -261,7 +263,6 @@ export const restoreAppState = (
typeof appState.zoom === "number" typeof appState.zoom === "number"
? { ? {
value: appState.zoom as NormalizedZoomValue, value: appState.zoom as NormalizedZoomValue,
translation: defaultAppState.zoom.translation,
} }
: appState.zoom || defaultAppState.zoom, : appState.zoom || defaultAppState.zoom,
}; };
+74
View File
@@ -0,0 +1,74 @@
@import "../css/variables.module";
.excalidraw-hyperlinkContainer {
display: flex;
align-items: center;
justify-content: space-between;
position: absolute;
box-shadow: 0px 2px 4px 0 rgb(0 0 0 / 30%);
z-index: 100;
background: var(--island-bg-color);
border-radius: var(--border-radius-md);
box-sizing: border-box;
// to account for LS due to rendering icons after new link created
min-height: 42px;
&-input,
button {
z-index: 100;
}
&-input,
&-link {
height: 24px;
padding: 0 8px;
line-height: 24px;
font-size: 0.9rem;
font-weight: 500;
font-family: var(--ui-font);
}
&-input {
width: 18rem;
border: none;
background-color: transparent;
color: var(--text-primary-color);
outline: none;
border: none;
box-shadow: none !important;
}
&-link {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 15rem;
}
button {
color: $oc-blue-6;
background-color: transparent !important;
font-weight: 500;
&.excalidraw-hyperlinkContainer--remove {
color: $oc-red-9;
}
}
.d-none {
display: none;
}
&--remove .ToolIcon__icon svg {
color: $oc-red-6;
}
.ToolIcon__icon {
width: 2rem;
height: 2rem;
}
&__buttons {
flex: 0 0 auto;
}
}
+465
View File
@@ -0,0 +1,465 @@
import { AppState, ExcalidrawProps, Point } from "../types";
import {
getShortcutKey,
sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords,
wrapEvent,
} from "../utils";
import { mutateElement } from "./mutateElement";
import { NonDeletedExcalidrawElement } from "./types";
import { register } from "../actions/register";
import { ToolButton } from "../components/ToolButton";
import { editIcon, link, trash } from "../components/icons";
import { t } from "../i18n";
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import clsx from "clsx";
import { KEYS } from "../keys";
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement";
import { rotate } from "../math";
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
import { Bounds } from "./bounds";
import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip";
import { getSelectedElements } from "../scene";
import { isPointHittingElementBoundingBox } from "./collision";
import { getElementAbsoluteCoords } from "./";
import "./Hyperlink.scss";
import { trackEvent } from "../analytics";
const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85;
const CONTAINER_PADDING = 5;
const CONTAINER_HEIGHT = 42;
const AUTO_HIDE_TIMEOUT = 500;
export const EXTERNAL_LINK_IMG = document.createElement("img");
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
)}`;
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
export const Hyperlink = ({
element,
appState,
setAppState,
onLinkOpen,
}: {
element: NonDeletedExcalidrawElement;
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
onLinkOpen: ExcalidrawProps["onLinkOpen"];
}) => {
const linkVal = element.link || "";
const [inputVal, setInputVal] = useState(linkVal);
const inputRef = useRef<HTMLInputElement>(null);
const isEditing = appState.showHyperlinkPopup === "editor" || !linkVal;
const handleSubmit = useCallback(() => {
if (!inputRef.current) {
return;
}
const link = normalizeLink(inputRef.current.value);
if (!element.link && link) {
trackEvent("hyperlink", "create");
}
mutateElement(element, { link });
setAppState({ showHyperlinkPopup: "info" });
}, [element, setAppState]);
useLayoutEffect(() => {
return () => {
handleSubmit();
};
}, [handleSubmit]);
useEffect(() => {
let timeoutId: number | null = null;
const handlePointerMove = (event: PointerEvent) => {
if (isEditing) {
return;
}
if (timeoutId) {
clearTimeout(timeoutId);
}
const shouldHide = shouldHideLinkPopup(element, appState, [
event.clientX,
event.clientY,
]) as boolean;
if (shouldHide) {
timeoutId = window.setTimeout(() => {
setAppState({ showHyperlinkPopup: false });
}, AUTO_HIDE_TIMEOUT);
}
};
window.addEventListener(EVENT.POINTER_MOVE, handlePointerMove, false);
return () => {
window.removeEventListener(EVENT.POINTER_MOVE, handlePointerMove, false);
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [appState, element, isEditing, setAppState]);
const handleRemove = useCallback(() => {
trackEvent("hyperlink", "delete");
mutateElement(element, { link: null });
if (isEditing) {
inputRef.current!.value = "";
}
setAppState({ showHyperlinkPopup: false });
}, [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.openMenu
) {
return null;
}
return (
<div
className="excalidraw-hyperlinkContainer"
style={{
top: `${y}px`,
left: `${x}px`,
width: CONTAINER_WIDTH,
padding: CONTAINER_PADDING,
}}
>
{isEditing ? (
<input
className={clsx("excalidraw-hyperlinkContainer-input")}
placeholder="Type or paste your link here"
ref={inputRef}
value={inputVal}
onChange={(event) => setInputVal(event.target.value)}
autoFocus
onKeyDown={(event) => {
event.stopPropagation();
// prevent cmd/ctrl+k shortcut when editing link
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K) {
event.preventDefault();
}
if (event.key === KEYS.ENTER || event.key === KEYS.ESCAPE) {
handleSubmit();
}
}}
/>
) : (
<a
href={element.link || ""}
className={clsx("excalidraw-hyperlinkContainer-link", {
"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}
</a>
)}
<div className="excalidraw-hyperlinkContainer__buttons">
{!isEditing && (
<ToolButton
type="button"
title={t("buttons.edit")}
aria-label={t("buttons.edit")}
label={t("buttons.edit")}
onClick={onEdit}
className="excalidraw-hyperlinkContainer--edit"
icon={editIcon}
/>
)}
{linkVal && (
<ToolButton
type="button"
title={t("buttons.remove")}
aria-label={t("buttons.remove")}
label={t("buttons.remove")}
onClick={handleRemove}
className="excalidraw-hyperlinkContainer--remove"
icon={trash}
/>
)}
</div>
</div>
);
};
const getCoordsForPopover = (
element: NonDeletedExcalidrawElement,
appState: AppState,
) => {
const [x1, y1] = getElementAbsoluteCoords(element);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width / 2, sceneY: y1 },
appState,
);
const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2;
const y = viewportY - appState.offsetTop - SPACE_BOTTOM;
return { x, y };
};
export const normalizeLink = (link: string) => {
link = link.trim();
if (link) {
// prefix with protocol if not fully-qualified
if (!link.includes("://") && !/^[[\\/]/.test(link)) {
link = `https://${link}`;
}
}
return link;
};
export const isLocalLink = (link: string | null) => {
return !!(link?.includes(location.origin) || link?.startsWith("/"));
};
export const actionLink = register({
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),
contextItemPredicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1;
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
return (
<ToolButton
type="button"
icon={link}
aria-label={t(getContextMenuLabel(elements, appState))}
title={`${t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`}
onClick={() => updateData(null)}
selected={selectedElements.length === 1 && !!selectedElements[0].link}
/>
);
},
});
export const getContextMenuLabel = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(elements, appState);
const label = selectedElements[0]!.link
? "labels.link.edit"
: "labels.link.create";
return label;
};
export const getLinkHandleFromCoords = (
[x1, y1, x2, y2]: Bounds,
angle: number,
appState: AppState,
): [x: number, y: number, width: number, height: number] => {
const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value;
const linkHeight = size / appState.zoom.value;
const linkMarginY = size / appState.zoom.value;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
const centeringOffset = (size - 8) / (2 * appState.zoom.value);
const dashedLineMargin = 4 / appState.zoom.value;
// Same as `ne` resize handle
const x = x2 + dashedLineMargin - centeringOffset;
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
const [rotatedX, rotatedY] = rotate(
x + linkWidth / 2,
y + linkHeight / 2,
centerX,
centerY,
angle,
);
return [
rotatedX - linkWidth / 2,
rotatedY - linkHeight / 2,
linkWidth,
linkHeight,
];
};
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(
[x1, y1, x2, y2],
element.angle,
appState,
);
const hitLink =
x > linkX - threshold &&
x < linkX + threshold + linkWidth &&
y > linkY - threshold &&
y < linkY + linkHeight + threshold;
return hitLink;
};
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
export const showHyperlinkTooltip = (
element: NonDeletedExcalidrawElement,
appState: AppState,
) => {
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
}
HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout(
() => renderTooltip(element, appState),
HYPERLINK_TOOLTIP_DELAY,
);
};
const renderTooltip = (
element: NonDeletedExcalidrawElement,
appState: AppState,
) => {
if (!element.link) {
return;
}
const tooltipDiv = getTooltipDiv();
tooltipDiv.classList.add("excalidraw-tooltip--visible");
tooltipDiv.style.maxWidth = "20rem";
tooltipDiv.textContent = element.link;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2],
element.angle,
appState,
);
const linkViewportCoords = sceneCoordsToViewportCoords(
{ sceneX: linkX, sceneY: linkY },
appState,
);
updateTooltipPosition(
tooltipDiv,
{
left: linkViewportCoords.x,
top: linkViewportCoords.y,
width: linkWidth,
height: linkHeight,
},
"top",
);
trackEvent("hyperlink", "tooltip", "link-icon");
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
};
export const hideHyperlinkToolip = () => {
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
}
if (IS_HYPERLINK_TOOLTIP_VISIBLE) {
IS_HYPERLINK_TOOLTIP_VISIBLE = false;
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
}
};
export const shouldHideLinkPopup = (
element: NonDeletedExcalidrawElement,
appState: AppState,
[clientX, clientY]: Point,
): Boolean => {
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX, clientY },
appState,
);
const threshold = 15 / appState.zoom.value;
// hitbox to prevent hiding when hovered in element bounding box
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 >= x1 &&
sceneX <= x2 &&
sceneY >= y1 - SPACE_BOTTOM &&
sceneY <= y1
) {
return false;
}
// hit box to prevent hiding when hovered around popover within threshold
const { x: popoverX, y: popoverY } = getCoordsForPopover(element, appState);
if (
clientX >= popoverX - threshold &&
clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
clientY >= popoverY - threshold &&
clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
) {
return false;
}
return true;
};
+2 -2
View File
@@ -185,7 +185,7 @@ const getLinearElementAbsoluteCoords = (
maxY + element.y, maxY + element.y,
]; ];
} else { } else {
const shape = getShapeForElement(element) as Drawable[]; const shape = getShapeForElement(element)!;
// first element is always the curve // first element is always the curve
const ops = getCurvePathOps(shape[0]); const ops = getCurvePathOps(shape[0]);
@@ -326,7 +326,7 @@ const getLinearElementRotatedBounds = (
return [minX, minY, maxX, maxY]; return [minX, minY, maxX, maxY];
} }
const shape = getShapeForElement(element) as Drawable[]; const shape = getShapeForElement(element)!;
// first element is always the curve // first element is always the curve
const ops = getCurvePathOps(shape[0]); const ops = getCurvePathOps(shape[0]);
+24 -7
View File
@@ -24,6 +24,7 @@ import {
NonDeleted, NonDeleted,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawLinearElement,
} from "./types"; } from "./types";
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds"; import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
@@ -96,7 +97,6 @@ export const isHittingElementNotConsideringBoundingBox = (
: isElementDraggableFromInside(element) : isElementDraggableFromInside(element)
? isInsideCheck ? isInsideCheck
: isNearCheck; : isNearCheck;
return hitTestPointAgainstElement({ element, point, threshold, check }); return hitTestPointAgainstElement({ element, point, threshold, check });
}; };
@@ -105,7 +105,7 @@ const isElementSelected = (
element: NonDeleted<ExcalidrawElement>, element: NonDeleted<ExcalidrawElement>,
) => appState.selectedElementIds[element.id]; ) => appState.selectedElementIds[element.id];
const isPointHittingElementBoundingBox = ( export const isPointHittingElementBoundingBox = (
element: NonDeleted<ExcalidrawElement>, element: NonDeleted<ExcalidrawElement>,
[x, y]: Point, [x, y]: Point,
threshold: number, threshold: number,
@@ -362,6 +362,14 @@ const hitTestFreeDrawElement = (
B = element.points[i + 1]; 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; return false;
}; };
@@ -384,7 +392,11 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
} }
const [relX, relY] = GAPoint.toTuple(point); 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) { if (args.check === isInsideCheck) {
const hit = shape.some((subshape) => const hit = shape.some((subshape) =>
@@ -634,7 +646,7 @@ const getCorners = (
// Returns intersection of `line` with `segment`, with `segment` moved by // Returns intersection of `line` with `segment`, with `segment` moved by
// `gap` in its polar direction. // `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 = ( const intersectSegment = (
line: GA.Line, line: GA.Line,
segment: [GA.Point, GA.Point], segment: [GA.Point, GA.Point],
@@ -822,7 +834,7 @@ const hitTestCurveInside = (
sharpness: ExcalidrawElement["strokeSharpness"], sharpness: ExcalidrawElement["strokeSharpness"],
) => { ) => {
const ops = getCurvePathOps(drawable); const ops = getCurvePathOps(drawable);
const points: Point[] = []; const points: Mutable<Point>[] = [];
let odd = false; // select one line out of double lines let odd = false; // select one line out of double lines
for (const operation of ops) { for (const operation of ops) {
if (operation.op === "move") { if (operation.op === "move") {
@@ -836,13 +848,17 @@ const hitTestCurveInside = (
points.push([operation.data[2], operation.data[3]]); points.push([operation.data[2], operation.data[3]]);
points.push([operation.data[4], operation.data[5]]); 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 (points.length >= 4) {
if (sharpness === "sharp") { if (sharpness === "sharp") {
return isPointInPolygon(points, x, y); 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 isPointInPolygon(polygonPoints, x, y);
} }
return false; return false;
@@ -897,9 +913,10 @@ const hitTestRoughShape = (
// position of the previous operation // position of the previous operation
return retVal; return retVal;
} else if (op === "lineTo") { } else if (op === "lineTo") {
// TODO: Implement this return hitTestCurveInside(drawable, x, y, "sharp");
} else if (op === "qcurveTo") { } else if (op === "qcurveTo") {
// TODO: Implement this // TODO: Implement this
console.warn("qcurveTo is not implemented yet");
} }
return false; return false;
+5 -9
View File
@@ -1,12 +1,10 @@
import { SHAPES } from "../shapes";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { getCommonBounds } from "./bounds"; import { getCommonBounds } from "./bounds";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types"; import { NonDeletedExcalidrawElement } from "./types";
import { AppState, PointerDownState } from "../types"; import { AppState, PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement"; import { getBoundTextElement } from "./textElement";
import { isSelectedViaGroup } from "../groups"; import { isSelectedViaGroup } from "../groups";
export const dragSelectedElements = ( export const dragSelectedElements = (
@@ -39,16 +37,14 @@ export const dragSelectedElements = (
// container is part of a group, but we're dragging the container directly // container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element)) (appState.editingGroupId && !isSelectedViaGroup(appState, element))
) { ) {
const boundTextElementId = getBoundTextElementId(element); const textElement = getBoundTextElement(element);
if (boundTextElementId) { if (textElement) {
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId);
updateElementCoords( updateElementCoords(
lockDirection, lockDirection,
distanceX, distanceX,
distanceY, distanceY,
pointerDownState, pointerDownState,
textElement!, textElement,
offset, offset,
); );
} }
@@ -96,7 +92,7 @@ export const getDragOffsetXY = (
export const dragNewElement = ( export const dragNewElement = (
draggingElement: NonDeletedExcalidrawElement, draggingElement: NonDeletedExcalidrawElement,
elementType: typeof SHAPES[number]["value"], elementType: AppState["elementType"],
originX: number, originX: number,
originY: number, originY: number,
x: number, x: number,
+1 -1
View File
@@ -401,7 +401,7 @@ export class LinearElementEditor {
ret.hitElement = element; ret.hitElement = element;
} else { } else {
// You might be wandering why we are storing the binding elements on // 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 // from the end points of the `linearElement` - this is to allow disabling
// binding (which needs to happen at the point the user finishes moving // binding (which needs to happen at the point the user finishes moving
// the point). // the point).
+17 -16
View File
@@ -21,9 +21,8 @@ import { AppState } from "../types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math"; import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds"; import { getResizedElementAbsoluteCoords } from "./bounds";
import { getContainerElement, measureText } from "./textElement"; import { getContainerElement, measureText, wrapText } from "./textElement";
import { isBoundToContainer } from "./typeChecks"; import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { BOUND_TEXT_PADDING } from "../constants";
type ElementConstructorOpts = MarkOptional< type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -35,6 +34,7 @@ type ElementConstructorOpts = MarkOptional<
| "seed" | "seed"
| "version" | "version"
| "versionNonce" | "versionNonce"
| "link"
>; >;
const _newElementBase = <T extends ExcalidrawElement>( const _newElementBase = <T extends ExcalidrawElement>(
@@ -55,6 +55,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
groupIds = [], groupIds = [],
strokeSharpness, strokeSharpness,
boundElements = null, boundElements = null,
link = null,
...rest ...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">, }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => { ) => {
@@ -81,6 +82,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
isDeleted: false as false, isDeleted: false as false,
boundElements, boundElements,
updated: getUpdatedTimestamp(), updated: getUpdatedTimestamp(),
link,
}; };
return element; return element;
}; };
@@ -172,7 +174,7 @@ const getAdjustedDimensions = (
let y: number; let y: number;
if ( if (
textAlign === "center" && textAlign === "center" &&
verticalAlign === "middle" && verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId !element.containerId
) { ) {
const prevMetrics = measureText( const prevMetrics = measureText(
@@ -218,8 +220,7 @@ const getAdjustedDimensions = (
// make sure container dimensions are set properly when // make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions // text editor overflows beyond viewport dimensions
if (isBoundToContainer(element)) { if (container) {
const container = getContainerElement(element)!;
let height = container.height; let height = container.height;
let width = container.width; let width = container.width;
if (nextHeight > height - BOUND_TEXT_PADDING * 2) { if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
@@ -247,17 +248,17 @@ export const updateTextElement = (
text, text,
isDeleted, isDeleted,
originalText, originalText,
}: { text: string; isDeleted?: boolean; originalText: string }, }: {
text: string;
isSubmit: boolean, isDeleted?: boolean;
originalText: string;
},
): ExcalidrawTextElement => { ): ExcalidrawTextElement => {
const boundToContainer = isBoundToContainer(element); const container = getContainerElement(element);
if (container) {
// Don't update dimensions and text value for bounded text unless submitted text = wrapText(text, getFontString(element), container.width);
const dimensions = }
boundToContainer && !isSubmit const dimensions = getAdjustedDimensions(element, text);
? undefined
: getAdjustedDimensions(element, text);
return newElementWith(element, { return newElementWith(element, {
text, text,
originalText, originalText,
+73 -20
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 { rescalePoints } from "../points";
import { import {
@@ -35,7 +35,9 @@ import {
import { Point, PointerDownState } from "../types"; import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { import {
getApproxMinLineHeight,
getApproxMinLineWidth, getApproxMinLineWidth,
getBoundTextElement,
getBoundTextElementId, getBoundTextElementId,
handleBindTextResize, handleBindTextResize,
measureText, measureText,
@@ -104,7 +106,7 @@ export const transformElements = (
updateBoundElements(element); updateBoundElements(element);
} else if (transformHandleType) { } else if (transformHandleType) {
resizeSingleElement( resizeSingleElement(
pointerDownState.originalElements.get(element.id) as typeof element, pointerDownState.originalElements,
shouldMaintainAspectRatio, shouldMaintainAspectRatio,
element, element,
transformHandleType, transformHandleType,
@@ -139,7 +141,6 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
handleBindTextResize(selectedElements, transformHandleType);
return true; return true;
} }
} }
@@ -396,7 +397,7 @@ const resizeSingleTextElement = (
}; };
export const resizeSingleElement = ( export const resizeSingleElement = (
stateAtResizeStart: NonDeletedExcalidrawElement, originalElements: PointerDownState["originalElements"],
shouldMaintainAspectRatio: boolean, shouldMaintainAspectRatio: boolean,
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
transformHandleDirection: TransformHandleDirection, transformHandleDirection: TransformHandleDirection,
@@ -404,6 +405,7 @@ export const resizeSingleElement = (
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const stateAtResizeStart = originalElements.get(element.id)!;
// Gets bounds corners // Gets bounds corners
const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords( const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords(
stateAtResizeStart, stateAtResizeStart,
@@ -428,8 +430,6 @@ export const resizeSingleElement = (
element.height, element.height,
); );
const boundTextElementId = getBoundTextElementId(element);
const boundsCurrentWidth = esx2 - esx1; const boundsCurrentWidth = esx2 - esx1;
const boundsCurrentHeight = esy2 - esy1; const boundsCurrentHeight = esy2 - esy1;
@@ -440,6 +440,9 @@ export const resizeSingleElement = (
let scaleX = atStartBoundsWidth / boundsCurrentWidth; let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight; let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) { if (transformHandleDirection.includes("e")) {
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth; scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
} }
@@ -452,6 +455,7 @@ export const resizeSingleElement = (
if (transformHandleDirection.includes("n")) { if (transformHandleDirection.includes("n")) {
scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight; scaleY = (startBottomRight[1] - rotatedPointer[1]) / boundsCurrentHeight;
} }
// Linear elements dimensions differ from bounds dimensions // Linear elements dimensions differ from bounds dimensions
const eleInitialWidth = stateAtResizeStart.width; const eleInitialWidth = stateAtResizeStart.width;
const eleInitialHeight = stateAtResizeStart.height; const eleInitialHeight = stateAtResizeStart.height;
@@ -481,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] = const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] =
getResizedElementAbsoluteCoords( getResizedElementAbsoluteCoords(
stateAtResizeStart, stateAtResizeStart,
@@ -490,11 +525,6 @@ export const resizeSingleElement = (
const newBoundsWidth = newBoundsX2 - newBoundsX1; const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1; 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 // Calculate new topLeft based on fixed corner during resize
let newTopLeft = [...startTopLeft] as [number, number]; let newTopLeft = [...startTopLeft] as [number, number];
if (["n", "w", "nw"].includes(transformHandleDirection)) { if (["n", "w", "nw"].includes(transformHandleDirection)) {
@@ -587,16 +617,9 @@ export const resizeSingleElement = (
], ],
}); });
} }
let minWidth = 0;
if (boundTextElementId) {
const boundTextElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
}
if ( if (
resizedElement.width > minWidth && resizedElement.width !== 0 &&
resizedElement.height !== 0 && resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) && Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y) Number.isFinite(resizedElement.y)
@@ -605,7 +628,10 @@ export const resizeSingleElement = (
newSize: { width: resizedElement.width, height: resizedElement.height }, newSize: { width: resizedElement.width, height: resizedElement.height },
}); });
mutateElement(element, resizedElement); mutateElement(element, resizedElement);
handleBindTextResize([element], transformHandleDirection); if (boundTextElement && boundTextFont) {
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
}
handleBindTextResize(element, transformHandleDirection);
} }
}; };
@@ -676,7 +702,25 @@ const resizeMultipleElements = (
} }
const width = element.width * scale; const width = element.width * scale;
const height = element.height * scale; const height = element.height * scale;
const boundTextElement = getBoundTextElement(element);
let font: { fontSize?: number; baseline?: number } = {}; 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)) { if (isTextElement(element)) {
const nextFont = measureFontSizeFromWH(element, width, height); const nextFont = measureFontSizeFromWH(element, width, height);
if (nextFont === null) { if (nextFont === null) {
@@ -720,6 +764,15 @@ const resizeMultipleElements = (
if (updates) { if (updates) {
elements.forEach((element, index) => { elements.forEach((element, index) => {
mutateElement(element, updates[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.viewModeEnabled &&
(appState.editingElement || (appState.editingElement ||
getSelectedElements(elements, appState).length || 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(width).toEqual(135);
expect(height).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); const { height, width } = getPerfectElementSize("arrow", 0, 0);
expect(width).toEqual(0); expect(width).toEqual(0);
expect(height).toEqual(0); expect(height).toEqual(0);
+119 -81
View File
@@ -1,6 +1,5 @@
import { getFontString, arrayToMap, isTestEnv } from "../utils"; import { getFontString, arrayToMap, isTestEnv } from "../utils";
import { import {
ExcalidrawBindableElement,
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextElement, ExcalidrawTextElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
@@ -8,10 +7,11 @@ import {
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./types"; } from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { BOUND_TEXT_PADDING } from "../constants"; import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles"; import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { isTextElement } from ".";
export const redrawTextBoundingBox = ( export const redrawTextBoundingBox = (
element: ExcalidrawTextElement, element: ExcalidrawTextElement,
@@ -39,11 +39,19 @@ export const redrawTextBoundingBox = (
let coordY = element.y; let coordY = element.y;
// Resize container and vertically center align the text // Resize container and vertically center align the text
if (container) { if (container) {
coordY = container.y + container.height / 2 - metrics.height / 2;
let nextHeight = container.height; let nextHeight = container.height;
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2; if (element.verticalAlign === VERTICAL_ALIGN.TOP) {
coordY = container.y + nextHeight / 2 - metrics.height / 2; 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 }); mutateElement(container, { height: nextHeight });
} }
@@ -71,91 +79,99 @@ export const bindTextToShapeAfterDuplication = (
const boundTextElementId = getBoundTextElementId(element); const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) { if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!; const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
mutateElement( if (newTextElementId) {
sceneElementMap.get(newElementId) as ExcalidrawBindableElement, const newContainer = sceneElementMap.get(newElementId);
{ if (newContainer) {
boundElements: element.boundElements?.concat({ mutateElement(newContainer, {
type: "text", boundElements: element.boundElements?.concat({
id: newTextElementId, type: "text",
}), id: newTextElementId,
}, }),
); });
mutateElement( }
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement, const newTextElement = sceneElementMap.get(newTextElementId);
{ if (newTextElement && isTextElement(newTextElement)) {
containerId: newElementId, mutateElement(newTextElement, {
}, containerId: newContainer ? newElementId : null,
); });
}
}
} }
}); });
}; };
export const handleBindTextResize = ( export const handleBindTextResize = (
elements: readonly NonDeletedExcalidrawElement[], element: NonDeletedExcalidrawElement,
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
) => { ) => {
elements.forEach((element) => { const boundTextElementId = getBoundTextElementId(element);
const boundTextElementId = getBoundTextElementId(element); if (boundTextElementId) {
if (boundTextElementId) { const textElement = Scene.getScene(element)!.getElement(
const textElement = Scene.getScene(element)!.getElement( boundTextElementId,
boundTextElementId, ) as ExcalidrawTextElement;
) as ExcalidrawTextElement; if (textElement && textElement.text) {
if (textElement && textElement.text) { if (!element) {
if (!element) { return;
return; }
} let text = textElement.text;
let text = textElement.text; let nextHeight = textElement.height;
let nextHeight = textElement.height; let containerHeight = element.height;
let containerHeight = element.height; let nextBaseLine = textElement.baseline;
let nextBaseLine = textElement.baseline; if (transformHandleType !== "n" && transformHandleType !== "s") {
if (transformHandleType !== "n" && transformHandleType !== "s") { if (text) {
if (text) { text = wrapText(
text = wrapText( textElement.originalText,
textElement.originalText,
getFontString(textElement),
element.width,
);
}
const dimensions = measureText(
text,
getFontString(textElement), getFontString(textElement),
element.width, 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; const dimensions = measureText(
mutateElement(textElement, {
text, text,
// preserve padding and set width correctly getFontString(textElement),
width: element.width - BOUND_TEXT_PADDING * 2, element.width,
height: nextHeight, );
x: element.x + BOUND_TEXT_PADDING, 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, 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 // https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
@@ -175,7 +191,6 @@ export const measureText = (
container.style.whiteSpace = "pre"; container.style.whiteSpace = "pre";
container.style.font = font; container.style.font = font;
container.style.minHeight = "1em"; container.style.minHeight = "1em";
if (maxWidth) { if (maxWidth) {
const lineHeight = getApproxLineHeight(font); const lineHeight = getApproxLineHeight(font);
container.style.width = `${String(maxWidth)}px`; container.style.width = `${String(maxWidth)}px`;
@@ -205,8 +220,14 @@ export const measureText = (
}; };
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase(); const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
const cacheApproxLineHeight: { [key: FontString]: number } = {};
export const getApproxLineHeight = (font: FontString) => { export const getApproxLineHeight = (font: FontString) => {
return measureText(DUMMY_TEXT, font, null).height; if (cacheApproxLineHeight[font]) {
return cacheApproxLineHeight[font];
}
cacheApproxLineHeight[font] = measureText(DUMMY_TEXT, font, null).height;
return cacheApproxLineHeight[font];
}; };
let canvas: HTMLCanvasElement | undefined; let canvas: HTMLCanvasElement | undefined;
@@ -350,6 +371,7 @@ export const charWidth = (() => {
const width = getTextWidth(char, font); const width = getTextWidth(char, font);
cachedCharWidth[font][ascii] = width; cachedCharWidth[font][ascii] = width;
} }
return cachedCharWidth[font][ascii]; return cachedCharWidth[font][ascii];
}; };
@@ -362,10 +384,14 @@ export const charWidth = (() => {
}; };
})(); })();
export const getApproxMinLineWidth = (font: FontString) => { export const getApproxMinLineWidth = (font: FontString) => {
return ( const maxCharWidth = getMaxCharWidth(font);
measureText(DUMMY_TEXT.split("").join("\n"), font).width + if (maxCharWidth === 0) {
BOUND_TEXT_PADDING * 2 return (
); measureText(DUMMY_TEXT.split("").join("\n"), font).width +
BOUND_TEXT_PADDING * 2
);
}
return maxCharWidth + BOUND_TEXT_PADDING * 2;
}; };
export const getApproxMinLineHeight = (font: FontString) => { export const getApproxMinLineHeight = (font: FontString) => {
@@ -382,6 +408,15 @@ export const getMinCharWidth = (font: FontString) => {
return Math.min(...cacheWithOutEmpty); 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) => { export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
// Generally lower case is used so converting to lower case // Generally lower case is used so converting to lower case
const dummyText = DUMMY_TEXT.toLocaleLowerCase(); const dummyText = DUMMY_TEXT.toLocaleLowerCase();
@@ -407,7 +442,10 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
}; };
export const getBoundTextElementId = (container: ExcalidrawElement | null) => { 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) => { export const getBoundTextElement = (element: ExcalidrawElement | null) => {
+358 -22
View File
@@ -1,12 +1,19 @@
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app"; import ExcalidrawApp from "../excalidraw-app";
import { render, screen } from "../tests/test-utils"; import { GlobalTestState, render, screen } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { fireEvent } from "../tests/test-utils"; import { fireEvent } from "../tests/test-utils";
import { queryByText } from "@testing-library/react";
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants"; import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import { ExcalidrawTextElementWithContainer } from "./types"; import {
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
} from "./types";
import * as textElementUtils from "./textElement"; import * as textElementUtils from "./textElement";
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
// Unmount ReactDOM from root // Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -14,14 +21,211 @@ const tab = " ";
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
describe("textWysiwyg", () => { 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; let textarea: HTMLTextAreaElement;
let textElement: ExcalidrawTextElement;
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<ExcalidrawApp />);
const element = UI.createElement("text"); textElement = UI.createElement("text");
mouse.clickOn(element); mouse.clickOn(textElement);
textarea = document.querySelector( textarea = document.querySelector(
".excalidraw-textEditorContainer > textarea", ".excalidraw-textEditorContainer > textarea",
)!; )!;
@@ -171,16 +375,65 @@ describe("textWysiwyg", () => {
expect(textarea.value).toEqual(`Line#1\nLine#2`); expect(textarea.value).toEqual(`Line#1\nLine#2`);
}); });
it("should resize text via shortcuts while in wysiwyg", () => {
textarea.value = "abc def";
const origFontSize = textElement.fontSize;
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
key: KEYS.CHEVRON_RIGHT,
ctrlKey: true,
shiftKey: true,
}),
);
expect(textElement.fontSize).toBe(origFontSize * 1.1);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
key: KEYS.CHEVRON_LEFT,
ctrlKey: true,
shiftKey: true,
}),
);
expect(textElement.fontSize).toBe(origFontSize);
});
it("zooming via keyboard should zoom canvas", () => {
expect(h.state.zoom.value).toBe(1);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.MINUS,
ctrlKey: true,
}),
);
expect(h.state.zoom.value).toBe(0.9);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.NUM_SUBTRACT,
ctrlKey: true,
}),
);
expect(h.state.zoom.value).toBe(0.8);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.NUM_ADD,
ctrlKey: true,
}),
);
expect(h.state.zoom.value).toBe(0.9);
textarea.dispatchEvent(
new KeyboardEvent("keydown", {
code: CODES.EQUAL,
ctrlKey: true,
}),
);
expect(h.state.zoom.value).toBe(1);
});
}); });
describe("Test bounded text", () => {
describe("Test container-bound text", () => {
let rectangle: any; let rectangle: any;
const { const { h } = window;
h,
}: {
h: {
elements: any;
};
} = window;
const DUMMY_HEIGHT = 240; const DUMMY_HEIGHT = 240;
const DUMMY_WIDTH = 160; const DUMMY_WIDTH = 160;
@@ -195,6 +448,7 @@ describe("textWysiwyg", () => {
beforeEach(async () => { beforeEach(async () => {
await render(<ExcalidrawApp />); await render(<ExcalidrawApp />);
h.elements = [];
rectangle = UI.createElement("rectangle", { rectangle = UI.createElement("rectangle", {
x: 10, x: 10,
@@ -222,9 +476,9 @@ describe("textWysiwyg", () => {
".excalidraw-textEditorContainer > textarea", ".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement; ) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } }); fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur(); editor.blur();
expect(rectangle.boundElements).toStrictEqual([ expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" }, { id: text.id, type: "text" },
@@ -257,7 +511,42 @@ 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 () => { 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);
mouse.doubleClickAt( mouse.doubleClickAt(
rectangle.x + rectangle.width / 2, rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2, rectangle.y + rectangle.height / 2,
@@ -289,19 +578,25 @@ describe("textWysiwyg", () => {
await new Promise((r) => setTimeout(r, 0)); await new Promise((r) => setTimeout(r, 0));
editor.blur(); editor.blur();
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia); expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
//undo //undo
Keyboard.withModifierKeys({ ctrl: true }, () => { Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z); Keyboard.keyPress(KEYS.Z);
}); });
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Virgil); expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Virgil);
//redo //redo
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.Z); Keyboard.keyPress(KEYS.Z);
}); });
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia); expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
}); });
it("should wrap text and vertcially center align once text submitted", async () => { it("should wrap text and vertcially center align once text submitted", async () => {
@@ -338,10 +633,9 @@ describe("textWysiwyg", () => {
}; };
}); });
Keyboard.withModifierKeys({}, () => { expect(h.elements.length).toBe(1);
Keyboard.keyPress(KEYS.ENTER);
});
Keyboard.keyDown(KEYS.ENTER);
let text = h.elements[1] as ExcalidrawTextElementWithContainer; let text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = document.querySelector( let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea", ".excalidraw-textEditorContainer > textarea",
@@ -409,5 +703,47 @@ describe("textWysiwyg", () => {
expect(text.height).toBe(APPROX_LINE_HEIGHT); expect(text.height).toBe(APPROX_LINE_HEIGHT);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2); expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
}); });
it("should unbind bound text when unbind action from context menu is triggred", async () => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
Keyboard.withModifierKeys({}, () => {
Keyboard.keyPress(KEYS.ENTER);
});
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.containerId).toBe(rectangle.id);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
mouse.reset();
UI.clickTool("selection");
mouse.clickAt(10, 20);
mouse.down();
mouse.up();
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 20,
clientY: 30,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(queryByText(contextMenu as HTMLElement, "Unbind text")!);
expect(h.elements[0].boundElements).toEqual([]);
expect((h.elements[1] as ExcalidrawTextElement).containerId).toEqual(
null,
);
});
}); });
}); });
+106 -96
View File
@@ -7,8 +7,12 @@ import {
} from "../utils"; } from "../utils";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { isBoundToContainer, isTextElement } from "./typeChecks"; import { isBoundToContainer, isTextElement } from "./typeChecks";
import { CLASSES, BOUND_TEXT_PADDING } from "../constants"; import { CLASSES, BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { ExcalidrawElement, ExcalidrawTextElement } from "./types"; import {
ExcalidrawElement,
ExcalidrawTextElement,
ExcalidrawLinearElement,
} from "./types";
import { AppState } from "../types"; import { AppState } from "../types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { import {
@@ -17,6 +21,12 @@ import {
getContainerElement, getContainerElement,
wrapText, wrapText,
} from "./textElement"; } from "./textElement";
import {
actionDecreaseFontSize,
actionIncreaseFontSize,
} from "../actions/actionProperties";
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
import App from "../components/App";
const normalizeText = (text: string) => { const normalizeText = (text: string) => {
return ( return (
@@ -36,33 +46,30 @@ const getTransform = (
maxWidth: number, maxWidth: number,
maxHeight: number, maxHeight: number,
) => { ) => {
const { zoom, offsetTop, offsetLeft } = appState; const { zoom } = appState;
const degree = (180 * angle) / Math.PI; const degree = (180 * angle) / Math.PI;
// offsets must be multiplied by 2 to account for the division by 2 of let translateX = (width * (zoom.value - 1)) / 2;
// the whole expression afterwards let translateY = (height * (zoom.value - 1)) / 2;
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
let translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
if (width > maxWidth && zoom.value !== 1) { if (width > maxWidth && zoom.value !== 1) {
translateX = (maxWidth / 2) * (zoom.value - 1); translateX = (maxWidth * (zoom.value - 1)) / 2;
} }
if (height > maxHeight && zoom.value !== 1) { if (height > maxHeight && zoom.value !== 1) {
translateY = ((maxHeight - offsetTop * 2) * (zoom.value - 1)) / 2; translateY = (maxHeight * (zoom.value - 1)) / 2;
} }
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`; return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
}; };
export const textWysiwyg = ({ export const textWysiwyg = ({
id, id,
appState,
onChange, onChange,
onSubmit, onSubmit,
getViewportCoords, getViewportCoords,
element, element,
canvas, canvas,
excalidrawContainer, excalidrawContainer,
app,
}: { }: {
id: ExcalidrawElement["id"]; id: ExcalidrawElement["id"];
appState: AppState;
onChange?: (text: string) => void; onChange?: (text: string) => void;
onSubmit: (data: { onSubmit: (data: {
text: string; text: string;
@@ -73,12 +80,13 @@ export const textWysiwyg = ({
element: ExcalidrawTextElement; element: ExcalidrawTextElement;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
excalidrawContainer: HTMLDivElement | null; excalidrawContainer: HTMLDivElement | null;
app: App;
}) => { }) => {
const textPropertiesUpdated = ( const textPropertiesUpdated = (
updatedElement: ExcalidrawTextElement, updatedElement: ExcalidrawTextElement,
editable: HTMLTextAreaElement, editable: HTMLTextAreaElement,
) => { ) => {
const currentFont = editable.style.fontFamily.replaceAll('"', ""); const currentFont = editable.style.fontFamily.replace(/"/g, "");
if ( if (
getFontFamilyString({ fontFamily: updatedElement.fontFamily }) !== getFontFamilyString({ fontFamily: updatedElement.fontFamily }) !==
currentFont currentFont
@@ -91,11 +99,15 @@ export const textWysiwyg = ({
return false; return false;
}; };
let originalContainerHeight: number; let originalContainerHeight: number;
let approxLineHeight = getApproxLineHeight(getFontString(element));
const initialText = element.originalText;
const updateWysiwygStyle = () => { const updateWysiwygStyle = () => {
const updatedElement = Scene.getScene(element)?.getElement(id); const appState = app.state;
const updatedElement = Scene.getScene(element)?.getElement(
id,
) as ExcalidrawTextElement;
const { textAlign, verticalAlign } = updatedElement;
const approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
if (updatedElement && isTextElement(updatedElement)) { if (updatedElement && isTextElement(updatedElement)) {
let coordX = updatedElement.x; let coordX = updatedElement.x;
let coordY = updatedElement.y; let coordY = updatedElement.y;
@@ -104,7 +116,7 @@ export const textWysiwyg = ({
let maxHeight = updatedElement.height; let maxHeight = updatedElement.height;
let width = updatedElement.width; 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 // what is going to be used for unbounded text
let height = updatedElement.height; let height = updatedElement.height;
if (container && updatedElement.containerId) { if (container && updatedElement.containerId) {
@@ -118,8 +130,6 @@ export const textWysiwyg = ({
height = editorHeight; height = editorHeight;
} }
if (propertiesUpdated) { if (propertiesUpdated) {
approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
originalContainerHeight = container.height; originalContainerHeight = container.height;
// update height of the editor after properties updated // update height of the editor after properties updated
@@ -132,7 +142,7 @@ export const textWysiwyg = ({
maxHeight = container.height - BOUND_TEXT_PADDING * 2; maxHeight = container.height - BOUND_TEXT_PADDING * 2;
width = maxWidth; width = maxWidth;
// The coordinates of text box set a distance of // The coordinates of text box set a distance of
// 30px to preserve padding // 5px to preserve padding
coordX = container.x + BOUND_TEXT_PADDING; coordX = container.x + BOUND_TEXT_PADDING;
// autogrow container height if text exceeds // autogrow container height if text exceeds
if (height > maxHeight) { if (height > maxHeight) {
@@ -152,40 +162,45 @@ export const textWysiwyg = ({
// is reached // is reached
else { else {
// vertically center align the text // 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 [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; 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 lines = updatedElement.originalText.split("\n");
const lineHeight = updatedElement.containerId const lineHeight = updatedElement.containerId
? approxLineHeight ? approxLineHeight
: updatedElement.height / lines.length; : updatedElement.height / lines.length;
if (!container) { if (!container) {
maxWidth = maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
(appState.offsetLeft + appState.width - viewportX - 8) /
appState.zoom.value -
// margin-right of parent if any
Number(
getComputedStyle(
excalidrawContainer?.parentNode as Element,
).marginRight.slice(0, -2),
);
} }
// Make sure text editor height doesn't go beyond viewport // Make sure text editor height doesn't go beyond viewport
const editorMaxHeight = const editorMaxHeight =
(appState.height - (appState.height - viewportY) / appState.zoom.value;
viewportY -
// There is a ~14px difference which keeps on increasing
// with every zoom step when offset present hence I am subtracting it here
// However this is not the best fix and breaks in
// few scenarios
(appState.offsetTop
? ((appState.zoom.value * 100 - 100) / 10) * 14
: 0)) /
appState.zoom.value;
const angle = container ? container.angle : updatedElement.angle; const angle = container ? container.angle : updatedElement.angle;
Object.assign(editable.style, { Object.assign(editable.style, {
font: getFontString(updatedElement), font: getFontString(updatedElement),
@@ -204,6 +219,7 @@ export const textWysiwyg = ({
editorMaxHeight, editorMaxHeight,
), ),
textAlign, textAlign,
verticalAlign,
color: updatedElement.strokeColor, color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100, opacity: updatedElement.opacity / 100,
filter: "var(--theme-filter)", filter: "var(--theme-filter)",
@@ -215,6 +231,7 @@ export const textWysiwyg = ({
if (isTestEnv()) { if (isTestEnv()) {
editable.style.fontFamily = getFontFamilyString(updatedElement); editable.style.fontFamily = getFontFamilyString(updatedElement);
} }
mutateElement(updatedElement, { x: coordX, y: coordY });
} }
}; };
@@ -257,12 +274,16 @@ export const textWysiwyg = ({
if (onChange) { if (onChange) {
editable.oninput = () => { editable.oninput = () => {
const updatedElement = Scene.getScene(element)?.getElement(
id,
) as ExcalidrawTextElement;
const font = getFontString(updatedElement);
// using scrollHeight here since we need to calculate // using scrollHeight here since we need to calculate
// number of lines so cannot use editable.style.height // number of lines so cannot use editable.style.height
// as that gets updated below // as that gets updated below
const lines = editable.scrollHeight / approxLineHeight; const lines = editable.scrollHeight / getApproxLineHeight(font);
// auto increase height only when lines > 1 so its // 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" // first line as well as setting height to "auto"
// doubles the height as soon as user starts typing // doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) { if (isBoundToContainer(element) && lines > 1) {
@@ -272,7 +293,7 @@ export const textWysiwyg = ({
const container = getContainerElement(element); const container = getContainerElement(element);
const actualLineCount = wrapText( const actualLineCount = wrapText(
editable.value, editable.value,
getFontString(element), font,
container!.width, container!.width,
).split("\n").length; ).split("\n").length;
@@ -293,16 +314,26 @@ export const textWysiwyg = ({
} }
editable.onkeydown = (event) => { editable.onkeydown = (event) => {
if (!event[KEYS.CTRL_OR_CMD]) { event.stopPropagation();
event.stopPropagation();
} if (!event.shiftKey && actionZoomIn.keyTest(event)) {
if (event.key === KEYS.ESCAPE) { event.preventDefault();
app.actionManager.executeAction(actionZoomIn);
updateWysiwygStyle();
} else if (!event.shiftKey && actionZoomOut.keyTest(event)) {
event.preventDefault();
app.actionManager.executeAction(actionZoomOut);
updateWysiwygStyle();
} else if (actionDecreaseFontSize.keyTest(event)) {
app.actionManager.executeAction(actionDecreaseFontSize);
} else if (actionIncreaseFontSize.keyTest(event)) {
app.actionManager.executeAction(actionIncreaseFontSize);
} else if (event.key === KEYS.ESCAPE) {
event.preventDefault(); event.preventDefault();
submittedViaKeyboard = true; submittedViaKeyboard = true;
handleSubmit(); handleSubmit();
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) { } else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
if (event.isComposing || event.keyCode === 229) { if (event.isComposing || event.keyCode === 229) {
return; return;
} }
@@ -315,7 +346,6 @@ export const textWysiwyg = ({
event.code === CODES.BRACKET_RIGHT)) event.code === CODES.BRACKET_RIGHT))
) { ) {
event.preventDefault(); event.preventDefault();
event.stopPropagation();
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) { if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
outdent(); outdent();
} else { } else {
@@ -392,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 = () => { const getSelectedLinesStartIndices = () => {
let { selectionStart, selectionEnd, value } = editable; let { selectionStart, selectionEnd, value } = editable;
@@ -434,61 +464,41 @@ export const textWysiwyg = ({
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the // it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
// wysiwyg on update // wysiwyg on update
cleanup(); cleanup();
const updateElement = Scene.getScene(element)?.getElement(element.id); const updateElement = Scene.getScene(element)?.getElement(
element.id,
) as ExcalidrawTextElement;
if (!updateElement) { if (!updateElement) {
return; return;
} }
let wrappedText = ""; let text = editable.value;
if (isTextElement(updateElement) && updateElement?.containerId) { const container = getContainerElement(updateElement);
const container = getContainerElement(updateElement);
if (container) { if (container) {
wrappedText = wrapText( text = updateElement.text;
editable.value, if (editable.value) {
getFontString(updateElement), const boundTextElementId = getBoundTextElementId(container);
container.width, if (!boundTextElementId || boundTextElementId !== element.id) {
); mutateElement(container, {
boundElements: (container.boundElements || []).concat({
if (updateElement.containerId) { type: "text",
const editorHeight = Number(editable.style.height.slice(0, -2)); id: element.id,
if (editable.value) { }),
// Don't mutate if text is not updated });
if (initialText !== editable.value) {
mutateElement(updateElement, {
// vertically center align
y: container.y + container.height / 2 - editorHeight / 2,
height: editorHeight,
width: Number(editable.style.width.slice(0, -2)),
// preserve padding
x: container.x + BOUND_TEXT_PADDING,
angle: container.angle,
});
}
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) {
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: element.id,
}),
});
}
} else {
mutateElement(container, {
boundElements: container.boundElements?.filter(
(ele) => ele.type !== "text",
),
});
}
} }
} else {
mutateElement(container, {
boundElements: container.boundElements?.filter(
(ele) =>
!isTextElement(
ele as ExcalidrawTextElement | ExcalidrawLinearElement,
),
),
});
} }
} else {
wrappedText = editable.value;
} }
onSubmit({ onSubmit({
text: normalizeText(wrappedText), text,
viaKeyboard: submittedViaKeyboard, viaKeyboard: submittedViaKeyboard,
originalText: editable.value, originalText: editable.value,
}); });
+7 -3
View File
@@ -1,3 +1,4 @@
import { AppState } from "../types";
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawTextElement, ExcalidrawTextElement,
@@ -8,6 +9,7 @@ import {
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawTextContainer,
} from "./types"; } from "./types";
export const isGenericElement = ( export const isGenericElement = (
@@ -59,7 +61,7 @@ export const isLinearElement = (
}; };
export const isLinearElementType = ( export const isLinearElementType = (
elementType: ExcalidrawElement["type"], elementType: AppState["elementType"],
): boolean => { ): boolean => {
return ( return (
elementType === "arrow" || elementType === "line" // || elementType === "freedraw" elementType === "arrow" || elementType === "line" // || elementType === "freedraw"
@@ -73,7 +75,7 @@ export const isBindingElement = (
}; };
export const isBindingElementType = ( export const isBindingElementType = (
elementType: ExcalidrawElement["type"], elementType: AppState["elementType"],
): boolean => { ): boolean => {
return elementType === "arrow"; 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 ( return (
element != null && element != null &&
(element.type === "rectangle" || (element.type === "rectangle" ||
+12 -3
View File
@@ -1,5 +1,5 @@
import { Point } from "../types"; 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 ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid"; export type FillStyle = "hachure" | "cross-hatch" | "solid";
@@ -12,7 +12,9 @@ export type PointerType = "mouse" | "pen" | "touch";
export type StrokeSharpness = "round" | "sharp"; export type StrokeSharpness = "round" | "sharp";
export type StrokeStyle = "solid" | "dashed" | "dotted"; export type StrokeStyle = "solid" | "dashed" | "dotted";
export type TextAlign = "left" | "center" | "right"; 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<{ type _ExcalidrawElementBase = Readonly<{
id: string; id: string;
@@ -52,6 +54,7 @@ type _ExcalidrawElementBase = Readonly<{
| null; | null;
/** epoch (ms) timestamp of last element update */ /** epoch (ms) timestamp of last element update */
updated: number; updated: number;
link: string | null;
}>; }>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & { export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
@@ -132,8 +135,14 @@ export type ExcalidrawBindableElement =
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawImageElement; | ExcalidrawImageElement;
export type ExcalidrawTextContainer =
| ExcalidrawRectangleElement
| ExcalidrawDiamondElement
| ExcalidrawEllipseElement
| ExcalidrawImageElement;
export type ExcalidrawTextElementWithContainer = { export type ExcalidrawTextElementWithContainer = {
containerId: ExcalidrawGenericElement["id"]; containerId: ExcalidrawTextContainer["id"];
} & ExcalidrawTextElement; } & ExcalidrawTextElement;
export type PointBinding = { export type PointBinding = {
+12
View File
@@ -4,6 +4,8 @@ export const INITIAL_SCENE_UPDATE_TIMEOUT = 5000;
export const FILE_UPLOAD_TIMEOUT = 300; export const FILE_UPLOAD_TIMEOUT = 300;
export const LOAD_IMAGES_TIMEOUT = 500; export const LOAD_IMAGES_TIMEOUT = 500;
export const SYNC_FULL_SCENE_INTERVAL_MS = 20000; 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 export const FILE_UPLOAD_MAX_BYTES = 3 * 1024 * 1024; // 3 MiB
// 1 year (https://stackoverflow.com/a/25201898/927631) // 1 year (https://stackoverflow.com/a/25201898/927631)
@@ -25,3 +27,13 @@ export const FIREBASE_STORAGE_PREFIXES = {
}; };
export const ROOM_ID_BYTES = 10; export const ROOM_ID_BYTES = 10;
export const STORAGE_KEYS = {
LOCAL_STORAGE_ELEMENTS: "excalidraw",
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag",
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
VERSION_DATA_STATE: "version-dataState",
VERSION_FILES: "version-files",
} as const;
+98 -42
View File
@@ -16,18 +16,20 @@ import {
withBatchedUpdates, withBatchedUpdates,
} from "../../utils"; } from "../../utils";
import { import {
CURSOR_SYNC_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,
FIREBASE_STORAGE_PREFIXES, FIREBASE_STORAGE_PREFIXES,
INITIAL_SCENE_UPDATE_TIMEOUT, INITIAL_SCENE_UPDATE_TIMEOUT,
LOAD_IMAGES_TIMEOUT, LOAD_IMAGES_TIMEOUT,
SCENE, SCENE,
STORAGE_KEYS,
SYNC_FULL_SCENE_INTERVAL_MS, SYNC_FULL_SCENE_INTERVAL_MS,
} from "../app_constants"; } from "../app_constants";
import { import {
generateCollaborationLinkData, generateCollaborationLinkData,
getCollaborationLink, getCollaborationLink,
getCollabServer,
SocketUpdateDataSource, SocketUpdateDataSource,
SOCKET_SERVER,
} from "../data"; } from "../data";
import { import {
isSavedToFirebase, isSavedToFirebase,
@@ -39,7 +41,6 @@ import {
import { import {
importUsernameFromLocalStorage, importUsernameFromLocalStorage,
saveUsernameToLocalStorage, saveUsernameToLocalStorage,
STORAGE_KEYS,
} from "../data/localStorage"; } from "../data/localStorage";
import Portal from "./Portal"; import Portal from "./Portal";
import RoomDialog from "./RoomDialog"; import RoomDialog from "./RoomDialog";
@@ -65,6 +66,7 @@ import {
reconcileElements as _reconcileElements, reconcileElements as _reconcileElements,
} from "./reconciliation"; } from "./reconciliation";
import { decryptData } from "../../data/encryption"; import { decryptData } from "../../data/encryption";
import { resetBrowserStateVersions } from "../data/tabSync";
interface CollabState { interface CollabState {
modalIsShown: boolean; modalIsShown: boolean;
@@ -86,6 +88,7 @@ export interface CollabAPI {
onCollabButtonClick: CollabInstance["onCollabButtonClick"]; onCollabButtonClick: CollabInstance["onCollabButtonClick"];
broadcastElements: CollabInstance["broadcastElements"]; broadcastElements: CollabInstance["broadcastElements"];
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"]; fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
setUsername: (username: string) => void;
} }
interface Props { interface Props {
@@ -246,6 +249,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.saveCollabRoomToFirebase(); this.saveCollabRoomToFirebase();
if (window.confirm(t("alerts.collabStopOverridePrompt"))) { if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
// hack to ensure that we prefer we disregard any new browser state
// that could have been saved in other tabs while we were collaborating
resetBrowserStateVersions();
window.history.pushState({}, APP_NAME, window.location.origin); window.history.pushState({}, APP_NAME, window.location.origin);
this.destroySocketClient(); this.destroySocketClient();
trackEvent("share", "room closed"); trackEvent("share", "room closed");
@@ -347,32 +354,29 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.isCollaborating = true; this.isCollaborating = true;
const { default: socketIOClient }: any = await import( const { default: socketIOClient } = await import(
/* webpackChunkName: "socketIoClient" */ "socket.io-client" /* webpackChunkName: "socketIoClient" */ "socket.io-client"
); );
this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomKey); try {
const socketServerData = await getCollabServer();
if (existingRoomLinkData) { this.portal.socket = this.portal.open(
this.excalidrawAPI.resetScene(); 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 { if (!existingRoomLinkData) {
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 {
const elements = this.excalidrawAPI.getSceneElements().map((element) => { const elements = this.excalidrawAPI.getSceneElements().map((element) => {
if (isImageElement(element) && element.status === "saved") { if (isImageElement(element) && element.status === "saved") {
return newElementWith(element, { status: "pending" }); return newElementWith(element, { status: "pending" });
@@ -396,14 +400,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
} }
// fallback in case you're not alone in the room but still don't receive // 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.socketInitializationTimer = window.setTimeout(() => {
this.initializeSocket(); this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
});
scenePromise.resolve(null); scenePromise.resolve(null);
}, INITIAL_SCENE_UPDATE_TIMEOUT); }, INITIAL_SCENE_UPDATE_TIMEOUT);
// All socket listeners are moving to Portal // All socket listeners are moving to Portal
this.portal.socket!.on( this.portal.socket.on(
"client-broadcast", "client-broadcast",
async (encryptedData: ArrayBuffer, iv: Uint8Array) => { async (encryptedData: ArrayBuffer, iv: Uint8Array) => {
if (!this.portal.roomKey) { if (!this.portal.roomKey) {
@@ -421,7 +428,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return; return;
case SCENE.INIT: { case SCENE.INIT: {
if (!this.portal.socketInitialized) { if (!this.portal.socketInitialized) {
this.initializeSocket(); this.initializeRoom({ fetchScene: false });
const remoteElements = decryptedData.payload.elements; const remoteElements = decryptedData.payload.elements;
const reconciledElements = this.reconcileElements(remoteElements); const reconciledElements = this.reconcileElements(remoteElements);
this.handleRemoteSceneUpdate(reconciledElements, { this.handleRemoteSceneUpdate(reconciledElements, {
@@ -475,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) { if (this.portal.socket) {
this.portal.socket.off("first-in-room"); this.portal.socket.off("first-in-room");
} }
this.initializeSocket(); const sceneData = await this.initializeRoom({
scenePromise.resolve(null); fetchScene: true,
roomLinkData: existingRoomLinkData,
});
scenePromise.resolve(sceneData);
}); });
this.initializeIdleDetector(); this.initializeIdleDetector();
@@ -492,9 +502,45 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return scenePromise; return scenePromise;
}; };
private initializeSocket = () => { private initializeRoom = async ({
this.portal.socketInitialized = true; fetchScene,
roomLinkData,
}:
| {
fetchScene: true;
roomLinkData: { roomId: string; roomKey: string } | null;
}
| { fetchScene: false; roomLinkData?: null }) => {
clearTimeout(this.socketInitializationTimer!); 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 = ( private reconcileElements = (
@@ -558,7 +604,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
window.clearTimeout(this.idleTimeoutId); window.clearTimeout(this.idleTimeoutId);
this.idleTimeoutId = null; this.idleTimeoutId = null;
} }
this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD); this.idleTimeoutId = window.setTimeout(this.reportIdle, IDLE_THRESHOLD);
if (!this.activeIntervalId) { if (!this.activeIntervalId) {
this.activeIntervalId = window.setInterval( this.activeIntervalId = window.setInterval(
this.reportActive, this.reportActive,
@@ -633,15 +681,18 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
return this.excalidrawAPI.getSceneElementsIncludingDeleted(); return this.excalidrawAPI.getSceneElementsIncludingDeleted();
}; };
onPointerUpdate = (payload: { onPointerUpdate = throttle(
pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"]; (payload: {
button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; pointer: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["pointer"];
pointersMap: Gesture["pointers"]; button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"];
}) => { pointersMap: Gesture["pointers"];
payload.pointersMap.size < 2 && }) => {
this.portal.socket && payload.pointersMap.size < 2 &&
this.portal.broadcastMouseLocation(payload); this.portal.socket &&
}; this.portal.broadcastMouseLocation(payload);
},
CURSOR_SYNC_TIMEOUT,
);
onIdleStateChange = (userState: UserIdleState) => { onIdleStateChange = (userState: UserIdleState) => {
this.setState({ userState }); this.setState({ userState });
@@ -677,8 +728,12 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.setState({ modalIsShown: false }); this.setState({ modalIsShown: false });
}; };
onUsernameChange = (username: string) => { setUsername = (username: string) => {
this.setState({ username }); this.setState({ username });
};
onUsernameChange = (username: string) => {
this.setUsername(username);
saveUsernameToLocalStorage(username); saveUsernameToLocalStorage(username);
}; };
@@ -712,6 +767,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.contextValue.broadcastElements = this.broadcastElements; this.contextValue.broadcastElements = this.broadcastElements;
this.contextValue.fetchImageFilesFromFirebase = this.contextValue.fetchImageFilesFromFirebase =
this.fetchImageFilesFromFirebase; this.fetchImageFilesFromFirebase;
this.contextValue.setUsername = this.setUsername;
return this.contextValue; return this.contextValue;
}; };
+2
View File
@@ -45,6 +45,8 @@ class Portal {
this.socket.on("room-user-change", (clients: string[]) => { this.socket.on("room-user-change", (clients: string[]) => {
this.collab.setCollaborators(clients); this.collab.setCollaborators(clients);
}); });
return socket;
} }
close() { close() {
+28 -1
View File
@@ -30,7 +30,34 @@ const generateRoomId = async () => {
return bytesToHexString(buffer); 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 = { export type EncryptedData = {
data: ArrayBuffer; data: ArrayBuffer;
+17 -8
View File
@@ -5,14 +5,8 @@ import {
getDefaultAppState, getDefaultAppState,
} from "../../appState"; } from "../../appState";
import { clearElementsForLocalStorage } from "../../element"; import { clearElementsForLocalStorage } from "../../element";
import { updateBrowserStateVersion } from "./tabSync";
export const STORAGE_KEYS = { import { STORAGE_KEYS } from "../app_constants";
LOCAL_STORAGE_ELEMENTS: "excalidraw",
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag",
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
};
export const saveUsernameToLocalStorage = (username: string) => { export const saveUsernameToLocalStorage = (username: string) => {
try { try {
@@ -53,6 +47,7 @@ export const saveToLocalStorage = (
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE, STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
JSON.stringify(clearAppStateForLocalStorage(appState)), JSON.stringify(clearAppStateForLocalStorage(appState)),
); );
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
} catch (error: any) { } catch (error: any) {
// Unable to access window.localStorage // Unable to access window.localStorage
console.error(error); console.error(error);
@@ -125,3 +120,17 @@ export const getTotalStorageSize = () => {
return 0; return 0;
} }
}; };
export const getLibraryItemsFromStorage = () => {
try {
const libraryItems =
JSON.parse(
localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string,
) || [];
return libraryItems;
} catch (e) {
console.error(e);
return [];
}
};
+29
View File
@@ -0,0 +1,29 @@
import { STORAGE_KEYS } from "../app_constants";
// in-memory state (this tab's current state) versions. Currently just
// timestamps of the last time the state was saved to browser storage.
const LOCAL_STATE_VERSIONS = {
[STORAGE_KEYS.VERSION_DATA_STATE]: -1,
[STORAGE_KEYS.VERSION_FILES]: -1,
};
type BrowserStateTypes = keyof typeof LOCAL_STATE_VERSIONS;
export const isBrowserStorageStateNewer = (type: BrowserStateTypes) => {
const storageTimestamp = JSON.parse(localStorage.getItem(type) || "-1");
return storageTimestamp > LOCAL_STATE_VERSIONS[type];
};
export const updateBrowserStateVersion = (type: BrowserStateTypes) => {
const timestamp = Date.now();
localStorage.setItem(type, JSON.stringify(timestamp));
LOCAL_STATE_VERSIONS[type] = timestamp;
};
export const resetBrowserStateVersions = () => {
for (const key of Object.keys(LOCAL_STATE_VERSIONS) as BrowserStateTypes[]) {
const timestamp = -1;
localStorage.setItem(key, JSON.stringify(timestamp));
LOCAL_STATE_VERSIONS[key] = timestamp;
}
};
+5
View File
@@ -1,4 +1,9 @@
.excalidraw { .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 { .layer-ui__wrapper__footer-center {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
+73 -11
View File
@@ -34,6 +34,7 @@ import {
import { import {
debounce, debounce,
getVersion, getVersion,
isTestEnv,
preventUnload, preventUnload,
ResolvablePromise, ResolvablePromise,
resolvablePromise, resolvablePromise,
@@ -41,6 +42,8 @@ import {
import { import {
FIREBASE_STORAGE_PREFIXES, FIREBASE_STORAGE_PREFIXES,
SAVE_TO_LOCAL_STORAGE_TIMEOUT, SAVE_TO_LOCAL_STORAGE_TIMEOUT,
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants"; } from "./app_constants";
import CollabWrapper, { import CollabWrapper, {
CollabAPI, CollabAPI,
@@ -50,9 +53,10 @@ import CollabWrapper, {
import { LanguageList } from "./components/LanguageList"; import { LanguageList } from "./components/LanguageList";
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data"; import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
import { import {
getLibraryItemsFromStorage,
importFromLocalStorage, importFromLocalStorage,
importUsernameFromLocalStorage,
saveToLocalStorage, saveToLocalStorage,
STORAGE_KEYS,
} from "./data/localStorage"; } from "./data/localStorage";
import CustomStats from "./CustomStats"; import CustomStats from "./CustomStats";
import { restoreAppState, RestoredDataState } from "../data/restore"; import { restoreAppState, RestoredDataState } from "../data/restore";
@@ -67,6 +71,10 @@ import { FileManager, updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { isInitializedImageElement } from "../element/typeChecks"; import { isInitializedImageElement } from "../element/typeChecks";
import { loadFilesFromFirebase } from "./data/firebase"; import { loadFilesFromFirebase } from "./data/firebase";
import {
isBrowserStorageStateNewer,
updateBrowserStateVersion,
} from "./data/tabSync";
const filesStore = createStore("files-db", "files-store"); const filesStore = createStore("files-db", "files-store");
@@ -104,6 +112,11 @@ const localFileStorage = new FileManager({
const savedFiles = new Map<FileId, true>(); const savedFiles = new Map<FileId, true>();
const erroredFiles = new Map<FileId, true>(); const erroredFiles = new Map<FileId, true>();
// before we use `storage` event synchronization, let's update the flag
// optimistically. Hopefully nothing fails, and an IDB read executed
// before an IDB write finishes will read the latest value.
updateBrowserStateVersion(STORAGE_KEYS.VERSION_FILES);
await Promise.all( await Promise.all(
[...addedFiles].map(async ([id, fileData]) => { [...addedFiles].map(async ([id, fileData]) => {
try { try {
@@ -142,7 +155,6 @@ const saveDebounced = debounce(
elements, elements,
files, files,
}); });
onFilesSaved(); onFilesSaved();
}, },
SAVE_TO_LOCAL_STORAGE_TIMEOUT, SAVE_TO_LOCAL_STORAGE_TIMEOUT,
@@ -278,7 +290,6 @@ const ExcalidrawWrapper = () => {
currentLangCode = currentLangCode[0]; currentLangCode = currentLangCode[0];
} }
const [langCode, setLangCode] = useState(currentLangCode); const [langCode, setLangCode] = useState(currentLangCode);
// initial state // initial state
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -372,14 +383,7 @@ const ExcalidrawWrapper = () => {
} }
} }
try { data.scene.libraryItems = getLibraryItemsFromStorage();
data.scene.libraryItems =
JSON.parse(
localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY) as string,
) || [];
} catch (error: any) {
console.error(error);
}
}; };
initializeScene({ collabAPI }).then((data) => { initializeScene({ collabAPI }).then((data) => {
@@ -415,13 +419,71 @@ const ExcalidrawWrapper = () => {
() => (document.title = APP_NAME), () => (document.title = APP_NAME),
TITLE_TIMEOUT, TITLE_TIMEOUT,
); );
const syncData = debounce(() => {
if (isTestEnv()) {
return;
}
if (!document.hidden && !collabAPI.isCollaborating()) {
// don't sync if local state is newer or identical to browser state
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
const localDataState = importFromLocalStorage();
const username = importUsernameFromLocalStorage();
let langCode = languageDetector.detect() || defaultLang.code;
if (Array.isArray(langCode)) {
langCode = langCode[0];
}
setLangCode(langCode);
excalidrawAPI.updateScene({
...localDataState,
libraryItems: getLibraryItemsFromStorage(),
});
collabAPI.setUsername(username || "");
}
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_FILES)) {
const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
const currFiles = excalidrawAPI.getFiles();
const fileIds =
elements?.reduce((acc, element) => {
if (
isInitializedImageElement(element) &&
// only load and update images that aren't already loaded
!currFiles[element.fileId]
) {
return acc.concat(element.fileId);
}
return acc;
}, [] as FileId[]) || [];
if (fileIds.length) {
localFileStorage
.getFiles(fileIds)
.then(({ loadedFiles, erroredFiles }) => {
if (loadedFiles.length) {
excalidrawAPI.addFiles(loadedFiles);
}
updateStaleImageStatuses({
excalidrawAPI,
erroredFiles,
elements: excalidrawAPI.getSceneElementsIncludingDeleted(),
});
});
}
}
}
}, SYNC_BROWSER_TABS_TIMEOUT);
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false); window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
window.addEventListener(EVENT.UNLOAD, onBlur, false); window.addEventListener(EVENT.UNLOAD, onBlur, false);
window.addEventListener(EVENT.BLUR, onBlur, false); window.addEventListener(EVENT.BLUR, onBlur, false);
document.addEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
window.addEventListener(EVENT.FOCUS, syncData, false);
return () => { return () => {
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false); window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
window.removeEventListener(EVENT.UNLOAD, onBlur, false); window.removeEventListener(EVENT.UNLOAD, onBlur, false);
window.removeEventListener(EVENT.BLUR, onBlur, false); window.removeEventListener(EVENT.BLUR, onBlur, false);
window.removeEventListener(EVENT.FOCUS, syncData, false);
document.removeEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
clearTimeout(titleTimeout); clearTimeout(titleTimeout);
}; };
}, [collabAPI, excalidrawAPI]); }, [collabAPI, excalidrawAPI]);
+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 => { export const nvector = (value: number = 0, index: number = 0): NVector => {
const result = [0, 0, 0, 0, 0, 0, 0, 0]; const result = [0, 0, 0, 0, 0, 0, 0, 0];
if (index < 0 || index > 7) { 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) { if (value !== 0) {
result[index] = value; 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`. * 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. * have a natural orientation, corresponding to that vector.
* *
* The magnitude ("norm") of the line is `sqrt(a ^ 2 + b ^ 2)`. * 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 { interface ProcessEnv {
readonly REACT_APP_BACKEND_V2_GET_URL: string; readonly REACT_APP_BACKEND_V2_GET_URL: string;
readonly REACT_APP_BACKEND_V2_POST_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; readonly REACT_APP_FIREBASE_CONFIG: string;
} }
} }
@@ -34,6 +34,10 @@ type Mutable<T> = {
-readonly [P in keyof T]: T[P]; -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 ( type ResolutionType<T extends (...args: any) => any> = T extends (
...args: any ...args: any
) => Promise<infer R> ) => Promise<infer R>
+6 -15
View File
@@ -1,13 +1,7 @@
import { import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
GroupId,
ExcalidrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
} from "./element/types";
import { AppState } from "./types"; import { AppState } from "./types";
import { getSelectedElements } from "./scene"; import { getSelectedElements } from "./scene";
import { getBoundTextElementId } from "./element/textElement"; import { getBoundTextElement } from "./element/textElement";
import Scene from "./scene/Scene";
export const selectGroup = ( export const selectGroup = (
groupId: GroupId, groupId: GroupId,
@@ -182,13 +176,10 @@ export const getMaximumGroups = (
const currentGroupMembers = groups.get(groupId) || []; const currentGroupMembers = groups.get(groupId) || [];
// Include bounded text if present when grouping // Include bound text if present when grouping
const boundTextElementId = getBoundTextElementId(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElementId) { if (boundTextElement) {
const textElement = Scene.getScene(element)!.getElement( currentGroupMembers.push(boundTextElement);
boundTextElementId,
) as ExcalidrawTextElementWithContainer;
currentGroupMembers.push(textElement);
} }
groups.set(groupId, [...currentGroupMembers, element]); groups.set(groupId, [...currentGroupMembers, element]);
}); });
+5 -3
View File
@@ -16,9 +16,11 @@ const allLanguages: Language[] = [
{ code: "ar-SA", label: "العربية", rtl: true }, { code: "ar-SA", label: "العربية", rtl: true },
{ code: "bg-BG", label: "Български" }, { code: "bg-BG", label: "Български" },
{ code: "ca-ES", label: "Català" }, { code: "ca-ES", label: "Català" },
{ code: "cs-CZ", label: "Česky" },
{ code: "de-DE", label: "Deutsch" }, { code: "de-DE", label: "Deutsch" },
{ code: "el-GR", label: "Ελληνικά" }, { code: "el-GR", label: "Ελληνικά" },
{ code: "es-ES", label: "Español" }, { code: "es-ES", label: "Español" },
{ code: "eu-ES", label: "Euskara" },
{ code: "fa-IR", label: "فارسی", rtl: true }, { code: "fa-IR", label: "فارسی", rtl: true },
{ code: "fi-FI", label: "Suomi" }, { code: "fi-FI", label: "Suomi" },
{ code: "fr-FR", label: "Français" }, { code: "fr-FR", label: "Français" },
@@ -29,7 +31,10 @@ const allLanguages: Language[] = [
{ code: "it-IT", label: "Italiano" }, { code: "it-IT", label: "Italiano" },
{ code: "ja-JP", label: "日本語" }, { code: "ja-JP", label: "日本語" },
{ code: "kab-KAB", label: "Taqbaylit" }, { code: "kab-KAB", label: "Taqbaylit" },
{ code: "kk-KZ", label: "Қазақ тілі" },
{ code: "ko-KR", label: "한국어" }, { code: "ko-KR", label: "한국어" },
{ code: "lt-LT", label: "Lietuvių" },
{ code: "lv-LV", label: "Latviešu" },
{ code: "my-MM", label: "Burmese" }, { code: "my-MM", label: "Burmese" },
{ code: "nb-NO", label: "Norsk bokmål" }, { code: "nb-NO", label: "Norsk bokmål" },
{ code: "nl-NL", label: "Nederlands" }, { code: "nl-NL", label: "Nederlands" },
@@ -47,9 +52,6 @@ const allLanguages: Language[] = [
{ code: "uk-UA", label: "Українська" }, { code: "uk-UA", label: "Українська" },
{ code: "zh-CN", label: "简体中文" }, { code: "zh-CN", label: "简体中文" },
{ code: "zh-TW", label: "繁體中文" }, { code: "zh-TW", label: "繁體中文" },
{ code: "lv-LV", label: "Latviešu" },
{ code: "cs-CZ", label: "Česky" },
{ code: "kk-KZ", label: "Қазақ тілі" },
].concat([defaultLang]); ].concat([defaultLang]);
export const languages: Language[] = allLanguages export const languages: Language[] = allLanguages
+3
View File
@@ -1,5 +1,6 @@
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); export const isDarwin = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
export const isWindows = /^Win/.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 = { export const CODES = {
EQUAL: "Equal", EQUAL: "Equal",
@@ -46,6 +47,7 @@ export const KEYS = {
COMMA: ",", COMMA: ",",
A: "a", A: "a",
C: "c",
D: "d", D: "d",
E: "e", E: "e",
G: "g", G: "g",
@@ -61,6 +63,7 @@ export const KEYS = {
X: "x", X: "x",
Y: "y", Y: "y",
Z: "z", Z: "z",
K: "k",
} as const; } as const;
export type Key = keyof typeof KEYS; export type Key = keyof typeof KEYS;
+57 -41
View File
@@ -64,6 +64,7 @@
"cartoonist": "كرتوني", "cartoonist": "كرتوني",
"fileTitle": "إسم الملف", "fileTitle": "إسم الملف",
"colorPicker": "منتقي اللون", "colorPicker": "منتقي اللون",
"canvasColors": "",
"canvasBackground": "خلفية اللوحة", "canvasBackground": "خلفية اللوحة",
"drawingCanvas": "لوحة الرسم", "drawingCanvas": "لوحة الرسم",
"layers": "الطبقات", "layers": "الطبقات",
@@ -101,8 +102,16 @@
"showStroke": "إظهار منتقي لون الخط", "showStroke": "إظهار منتقي لون الخط",
"showBackground": "إظهار منتقي لون الخلفية", "showBackground": "إظهار منتقي لون الخلفية",
"toggleTheme": "غير النمط", "toggleTheme": "غير النمط",
"personalLib": "", "personalLib": "المكتبة الشخصية",
"excalidrawLib": "" "excalidrawLib": "مكتبتنا",
"decreaseFontSize": "تصغير حجم الخط",
"increaseFontSize": "تكبير حجم الخط",
"unbindText": "",
"link": {
"edit": "تعديل الرابط",
"create": "إنشاء رابط",
"label": "رابط"
}
}, },
"buttons": { "buttons": {
"clearReset": "إعادة تعيين اللوحة", "clearReset": "إعادة تعيين اللوحة",
@@ -138,10 +147,10 @@
"exitZenMode": "إلغاء الوضع الليلى", "exitZenMode": "إلغاء الوضع الليلى",
"cancel": "إلغاء", "cancel": "إلغاء",
"clear": "مسح", "clear": "مسح",
"remove": "", "remove": "إزالة",
"publishLibrary": "", "publishLibrary": "انشر",
"submit": "", "submit": "أرسل",
"confirm": "" "confirm": "تأكيد"
}, },
"alerts": { "alerts": {
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟", "clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
@@ -159,7 +168,7 @@
"errorAddingToLibrary": "تعذر إضافة العنصر للمكتبة", "errorAddingToLibrary": "تعذر إضافة العنصر للمكتبة",
"errorRemovingFromLibrary": "تعذر إزالة العنصر من المكتبة", "errorRemovingFromLibrary": "تعذر إزالة العنصر من المكتبة",
"confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟", "confirmAddLibrary": "هذا سيضيف {{numShapes}} شكل إلى مكتبتك. هل أنت متأكد؟",
"imageDoesNotContainScene": "", "imageDoesNotContainScene": "يبدو أن هذه الصورة لا تحتوي على أي بيانات مشهد. هل قمت بتمكين تضمين المشهد أثناء التصدير؟",
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة", "cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.", "invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟", "resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
@@ -171,7 +180,8 @@
"imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...", "imageInsertError": "تعذر إدراج الصورة. حاول مرة أخرى لاحقاً...",
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.", "fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.", "svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
"invalidSVGString": "" "invalidSVGString": "SVG غير صالح.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "تحديد", "selection": "تحديد",
@@ -184,7 +194,9 @@
"freedraw": "رسم", "freedraw": "رسم",
"text": "نص", "text": "نص",
"library": "مكتبة", "library": "مكتبة",
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم" "lock": "الحفاظ على أداة التحديد نشطة بعد الرسم",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "إجراءات اللوحة", "canvasActions": "إجراءات اللوحة",
@@ -192,7 +204,7 @@
"shapes": "الأشكال" "shapes": "الأشكال"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "لتحريك لوحة الرسم ، استمر في الضغط على عجلة الماوس أو مفتاح المسافة أثناء السحب",
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد", "linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
"freeDraw": "انقر واسحب، افرج عند الانتهاء", "freeDraw": "انقر واسحب، افرج عند الانتهاء",
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار", "text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
@@ -204,10 +216,12 @@
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز", "resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران", "rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
"lineEditor_info": "انقر نقراً مزدوجاً أو اضغط Enter لتعديل النقاط", "lineEditor_info": "انقر نقراً مزدوجاً أو اضغط Enter لتعديل النقاط",
"lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة، Ctrl Or Cmd+D للتكرار، أو اسحب للانتقال", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "حدد نقطة لتحريك أو إزالتها، أو اضغط Alt ثم انقر لإضافة نقاط جديدة", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "نشر مكتبتك",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "تعذر عرض المعاينة", "cannotShowPreview": "تعذر عرض المعاينة",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "اقرأ مدونتنا", "blog": "اقرأ مدونتنا",
"click": "انقر", "click": "انقر",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "سهم مائل", "curvedArrow": "سهم مائل",
"curvedLine": "خط مائل", "curvedLine": "خط مائل",
"documentation": "دليل الاستخدام", "documentation": "دليل الاستخدام",
@@ -275,55 +291,55 @@
"zoomToSelection": "تكبير للعنصر المحدد" "zoomToSelection": "تكبير للعنصر المحدد"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": "مسح اللوحة"
}, },
"publishDialog": { "publishDialog": {
"title": "", "title": "نشر المكتبة",
"itemName": "", "itemName": "إسم العنصر",
"authorName": "", "authorName": "إسم المؤلف",
"githubUsername": "", "githubUsername": "اسم المستخدم في جيت هب",
"twitterUsername": "", "twitterUsername": "اسم المستخدم في تويتر",
"libraryName": "", "libraryName": "اسم المكتبة",
"libraryDesc": "", "libraryDesc": "وصف المكتبة",
"website": "", "website": "الموقع",
"placeholder": { "placeholder": {
"authorName": "", "authorName": "اسمك أو اسم المستخدم",
"libraryName": "", "libraryName": "اسم مكتبتك",
"libraryDesc": "", "libraryDesc": "وصف مكتبتك لمساعدة الناس على فهم استخدامها",
"githubHandle": "", "githubHandle": "",
"twitterHandle": "", "twitterHandle": "",
"website": "" "website": ""
}, },
"errors": { "errors": {
"required": "", "required": "مطلوب",
"website": "" "website": "أدخل عنوان URL صالح"
}, },
"noteDescription": { "noteDescription": {
"pre": "", "pre": "",
"link": "", "link": "مستودع المكتبة العامة",
"post": "" "post": "ليستخدمها الآخرون في رسوماتهم."
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "", "pre": "يجب الموافقة على المكتبة يدويًا أولاً. يرجى قراءة ",
"link": "", "link": "الإرشادات",
"post": "" "post": ""
}, },
"noteLicense": { "noteLicense": {
"pre": "", "pre": "",
"link": "", "link": "رخصة إم أي تي ",
"post": "" "post": "وهو ما يعني باختصار أنه يمكن لأي شخص استخدامها دون قيود."
}, },
"noteItems": "", "noteItems": "يجب أن يكون لكل عنصر مكتبة اسمه الخاص حتى يكون قابلاً للتصفية. سيتم تضمين عناصر المكتبة التالية:",
"atleastOneLibItem": "" "atleastOneLibItem": "يرجى تحديد عنصر مكتبة واحد على الأقل للبدء"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "تم إرسال المكتبة",
"content": "", "content": "شكرا لك {{authorName}}. لقد تم إرسال مكتبتك للمراجعة. يمكنك تتبع الحالة",
"link": "" "link": "هنا"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "إعادة ضبط المكتبة",
"removeItemsFromLib": "" "removeItemsFromLib": "إزالة العناصر المحددة من المكتبة"
}, },
"encrypted": { "encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.", "tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
@@ -345,7 +361,7 @@
"width": "العرض" "width": "العرض"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "تمت الاضافة الى المكتبة!",
"copyStyles": "نسخت الانماط.", "copyStyles": "نسخت الانماط.",
"copyToClipboard": "نسخ إلى الحافظة.", "copyToClipboard": "نسخ إلى الحافظة.",
"copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})",
+39 -23
View File
@@ -35,11 +35,11 @@
"arrowhead_arrow": "Стрелка", "arrowhead_arrow": "Стрелка",
"arrowhead_bar": "Връх на стрелката", "arrowhead_bar": "Връх на стрелката",
"arrowhead_dot": "Точка", "arrowhead_dot": "Точка",
"arrowhead_triangle": "", "arrowhead_triangle": "Триъгълник",
"fontSize": "Размер на шрифта", "fontSize": "Размер на шрифта",
"fontFamily": "Семейство шрифтове", "fontFamily": "Семейство шрифтове",
"onlySelected": "Само избраното", "onlySelected": "Само избраното",
"withBackground": "", "withBackground": "Фон",
"exportEmbedScene": "", "exportEmbedScene": "",
"exportEmbedScene_details": "Данните от сцената ще бъдат екпортирани в PNG/SVG файл, за да може сцената да бъде възстановена от него.\nТова ще увеличи размера на файла.", "exportEmbedScene_details": "Данните от сцената ще бъдат екпортирани в PNG/SVG файл, за да може сцената да бъде възстановена от него.\nТова ще увеличи размера на файла.",
"addWatermark": "Добави \"Направено с Excalidraw\"", "addWatermark": "Добави \"Направено с Excalidraw\"",
@@ -62,8 +62,9 @@
"architect": "Архитект", "architect": "Архитект",
"artist": "Художник", "artist": "Художник",
"cartoonist": "Карикатурист", "cartoonist": "Карикатурист",
"fileTitle": "", "fileTitle": "Име на файл",
"colorPicker": "Избор на цвят", "colorPicker": "Избор на цвят",
"canvasColors": "",
"canvasBackground": "Фон на платно", "canvasBackground": "Фон на платно",
"drawingCanvas": "Платно за рисуване", "drawingCanvas": "Платно за рисуване",
"layers": "Слоеве", "layers": "Слоеве",
@@ -93,21 +94,29 @@
"centerHorizontally": "Центрирай хоризонтално", "centerHorizontally": "Центрирай хоризонтално",
"distributeHorizontally": "Разпредели хоризонтално", "distributeHorizontally": "Разпредели хоризонтално",
"distributeVertically": "Разпредели вертикално", "distributeVertically": "Разпредели вертикално",
"flipHorizontal": "", "flipHorizontal": "Хоризонтално обръщане",
"flipVertical": "", "flipVertical": "Вертикално обръщане",
"viewMode": "Изглед", "viewMode": "Изглед",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "", "share": "Сподели",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": "",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "Нулиране на платно", "clearReset": "Нулиране на платно",
"exportJSON": "", "exportJSON": "",
"exportImage": "", "exportImage": "Запиши като изображение",
"export": "Експортиране", "export": "Експортиране",
"exportToPng": "Изнасяне в PNG", "exportToPng": "Изнасяне в PNG",
"exportToSvg": "Изнасяне в SVG", "exportToSvg": "Изнасяне в SVG",
@@ -129,19 +138,19 @@
"edit": "Редактиране", "edit": "Редактиране",
"undo": "Отмяна", "undo": "Отмяна",
"redo": "Повтори", "redo": "Повтори",
"resetLibrary": "", "resetLibrary": "Нулиране на библиотеката",
"createNewRoom": "Създай нова стая", "createNewRoom": "Създай нова стая",
"fullScreen": "На цял екран", "fullScreen": "На цял екран",
"darkMode": "Тъмен режим", "darkMode": "Тъмен режим",
"lightMode": "Светъл режим", "lightMode": "Светъл режим",
"zenMode": "Режим Zen", "zenMode": "Режим Zen",
"exitZenMode": "Спиране на Zen режим", "exitZenMode": "Спиране на Zen режим",
"cancel": "", "cancel": "Отмени",
"clear": "", "clear": "Изчисти",
"remove": "", "remove": "Премахване",
"publishLibrary": "", "publishLibrary": "Публикувай",
"submit": "", "submit": "Изпрати",
"confirm": "" "confirm": "Потвърждаване"
}, },
"alerts": { "alerts": {
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?", "clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
@@ -167,24 +176,27 @@
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
"unsupportedFileType": "", "unsupportedFileType": "Този файлов формат не се поддържа.",
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Селекция", "selection": "Селекция",
"image": "", "image": "Вмъкване на изображение",
"rectangle": "Правоъгълник", "rectangle": "Правоъгълник",
"diamond": "Диамант", "diamond": "Диамант",
"ellipse": "Елипс", "ellipse": "Елипс",
"arrow": "Стрелка", "arrow": "Стрелка",
"line": "Линия", "line": "Линия",
"freedraw": "", "freedraw": "Рисуване",
"text": "Текст", "text": "Текст",
"library": "Библиотека", "library": "Библиотека",
"lock": "Поддържайте избрания инструмент активен след рисуване" "lock": "Поддържайте избрания инструмент активен след рисуване",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "Действия по платното", "canvasActions": "Действия по платното",
@@ -204,10 +216,12 @@
"resizeImage": "", "resizeImage": "",
"rotate": "Можете да ограничите ъглите, като държите SHIFT, докато се въртите", "rotate": "Можете да ограничите ъглите, като държите SHIFT, докато се въртите",
"lineEditor_info": "Кликнете два пъти или натиснете Enter за да промените точките", "lineEditor_info": "Кликнете два пъти или натиснете Enter за да промените точките",
"lineEditor_pointSelected": "Натиснете Delete за да изтриете точка, CtrlOrCmd+D за дуплициране, или извлачете за да преместите", "lineEditor_pointSelected": "Натиснете Delete за да изтриете точка(и), CtrlOrCmd+D за дуплициране, или извлачете за да преместите",
"lineEditor_nothingSelected": "Изберете точка за местене или изтриване, или пък задръжте Alt и натиснете за да добавите нови точки", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "Натиснете Enter, за да добавите",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Невъзможност за показване на preview", "cannotShowPreview": "Невъзможност за показване на preview",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Прочетете нашия блог", "blog": "Прочетете нашия блог",
"click": "клик", "click": "клик",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "Извита стрелка", "curvedArrow": "Извита стрелка",
"curvedLine": "Извита линия", "curvedLine": "Извита линия",
"documentation": "Документация", "documentation": "Документация",
+20 -4
View File
@@ -64,6 +64,7 @@
"cartoonist": "", "cartoonist": "",
"fileTitle": "", "fileTitle": "",
"colorPicker": "", "colorPicker": "",
"canvasColors": "",
"canvasBackground": "", "canvasBackground": "",
"drawingCanvas": "", "drawingCanvas": "",
"layers": "", "layers": "",
@@ -102,7 +103,15 @@
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": "",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -171,7 +180,8 @@
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "", "selection": "",
@@ -184,7 +194,9 @@
"freedraw": "", "freedraw": "",
"text": "", "text": "",
"library": "", "library": "",
"lock": "" "lock": "",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
@@ -207,7 +219,9 @@
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "", "blog": "",
"click": "", "click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "", "curvedArrow": "",
"curvedLine": "", "curvedLine": "",
"documentation": "", "documentation": "",
+93 -77
View File
@@ -35,7 +35,7 @@
"arrowhead_arrow": "Fletxa", "arrowhead_arrow": "Fletxa",
"arrowhead_bar": "Barra", "arrowhead_bar": "Barra",
"arrowhead_dot": "Punt", "arrowhead_dot": "Punt",
"arrowhead_triangle": "", "arrowhead_triangle": "Triangle",
"fontSize": "Mida de lletra", "fontSize": "Mida de lletra",
"fontFamily": "Tipus de lletra", "fontFamily": "Tipus de lletra",
"onlySelected": "Només seleccionats", "onlySelected": "Només seleccionats",
@@ -64,6 +64,7 @@
"cartoonist": "Dibuixant", "cartoonist": "Dibuixant",
"fileTitle": "Nom del fitxer", "fileTitle": "Nom del fitxer",
"colorPicker": "Selector de colors", "colorPicker": "Selector de colors",
"canvasColors": "Usat al llenç",
"canvasBackground": "Fons del llenç", "canvasBackground": "Fons del llenç",
"drawingCanvas": "Llenç de dibuix", "drawingCanvas": "Llenç de dibuix",
"layers": "Capes", "layers": "Capes",
@@ -101,8 +102,16 @@
"showStroke": "Mostra el selector de color del traç", "showStroke": "Mostra el selector de color del traç",
"showBackground": "Mostra el selector de color de fons", "showBackground": "Mostra el selector de color de fons",
"toggleTheme": "Activa o desactiva el tema", "toggleTheme": "Activa o desactiva el tema",
"personalLib": "", "personalLib": "Biblioteca personal",
"excalidrawLib": "" "excalidrawLib": "Biblioteca d'Excalidraw",
"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": { "buttons": {
"clearReset": "Neteja el llenç", "clearReset": "Neteja el llenç",
@@ -136,12 +145,12 @@
"lightMode": "Mode clar", "lightMode": "Mode clar",
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Surt de mode zen", "exitZenMode": "Surt de mode zen",
"cancel": "", "cancel": "Cancel·la",
"clear": "", "clear": "Neteja",
"remove": "", "remove": "Suprimeix",
"publishLibrary": "", "publishLibrary": "Publica",
"submit": "", "submit": "Envia",
"confirm": "" "confirm": "Confirma"
}, },
"alerts": { "alerts": {
"clearReset": "S'esborrarà tot el llenç. N'esteu segur?", "clearReset": "S'esborrarà tot el llenç. N'esteu segur?",
@@ -159,23 +168,24 @@
"errorAddingToLibrary": "No s'ha pogut afegir l'element a la biblioteca", "errorAddingToLibrary": "No s'ha pogut afegir l'element a la biblioteca",
"errorRemovingFromLibrary": "No s'ha pogut eliminar l'element de 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?", "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": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge", "cannotRestoreFromImage": "L’escena no s’ha pogut restaurar des d’aquest fitxer d’imatge",
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.", "invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?", "resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
"removeItemsFromsLibrary": "", "removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
"invalidEncryptionKey": "" "invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
}, },
"errors": { "errors": {
"unsupportedFileType": "", "unsupportedFileType": "Tipus de fitxer no suportat.",
"imageInsertError": "", "imageInsertError": "No s'ha pogut insertar la imatge, torneu-ho a provar més tard...",
"fileTooBig": "", "fileTooBig": "El fitxer és massa gros. La mida màxima permesa és {{maxSize}}.",
"svgImageInsertError": "", "svgImageInsertError": "No ha estat possible inserir la imatge SVG. Les marques SVG semblen invàlides.",
"invalidSVGString": "" "invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Selecció", "selection": "Selecció",
"image": "", "image": "Insereix imatge",
"rectangle": "Rectangle", "rectangle": "Rectangle",
"diamond": "Rombe", "diamond": "Rombe",
"ellipse": "El·lipse", "ellipse": "El·lipse",
@@ -184,7 +194,9 @@
"freedraw": "Dibuix", "freedraw": "Dibuix",
"text": "Text", "text": "Text",
"library": "Biblioteca", "library": "Biblioteca",
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar" "lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
"penMode": "Evita el zoom i accepta solament el dibuix lliure amb bolígraf",
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada"
}, },
"headings": { "headings": {
"canvasActions": "Accions del llenç", "canvasActions": "Accions del llenç",
@@ -192,7 +204,7 @@
"shapes": "Formes" "shapes": "Formes"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "Per a moure el llenç, mantingueu premuda la roda del ratolí o la tecla espai mentre l'arrossegueu",
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia", "linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar", "freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció", "text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
@@ -201,13 +213,15 @@
"linearElementMulti": "Feu clic a l'ultim punt, o pitgeu Esc o Retorn per a finalitzar", "linearElementMulti": "Feu clic a l'ultim punt, o pitgeu Esc o Retorn per a finalitzar",
"lockAngle": "Per restringir els angles, mantenir premut el majúscul (SHIFT)", "lockAngle": "Per restringir els angles, mantenir premut el majúscul (SHIFT)",
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT", "resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
"resizeImage": "", "resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)", "rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
"lineEditor_info": "Fes doble clic o premi Enter per editar punts", "lineEditor_info": "Fes doble clic o premi Enter per editar punts",
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el punt, CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l", "lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
"lineEditor_nothingSelected": "Selecciona un punt per moure o eliminar, o manté premut Alt i fes clic per afegir punts nous", "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": "", "placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
"publishLibrary": "" "publishLibrary": "Publiqueu la vostra pròpia llibreria",
"bindTextToElement": "Premeu enter per a afegir-hi text",
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització", "cannotShowPreview": "No es pot mostrar la previsualització",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Llegiu el nostre blog", "blog": "Llegiu el nostre blog",
"click": "clic", "click": "clic",
"deepSelect": "Selecció profunda",
"deepBoxSelect": "",
"curvedArrow": "Fletxa corba", "curvedArrow": "Fletxa corba",
"curvedLine": "Línia corba", "curvedLine": "Línia corba",
"documentation": "Documentació", "documentation": "Documentació",
@@ -275,55 +291,55 @@
"zoomToSelection": "Zoom per veure la selecció" "zoomToSelection": "Zoom per veure la selecció"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": "Neteja el llenç"
}, },
"publishDialog": { "publishDialog": {
"title": "", "title": "Publica la biblioteca",
"itemName": "", "itemName": "Nom de l'element",
"authorName": "", "authorName": "Nom de l'autor/a",
"githubUsername": "", "githubUsername": "Nom d'usuari de GitHub",
"twitterUsername": "", "twitterUsername": "Nom d'usuari de Twitter",
"libraryName": "", "libraryName": "Nom de la biblioteca",
"libraryDesc": "", "libraryDesc": "Descripció de la biblioteca",
"website": "", "website": "Lloc web",
"placeholder": { "placeholder": {
"authorName": "", "authorName": "Nom o usuari",
"libraryName": "", "libraryName": "Nom de la vostra biblioteca",
"libraryDesc": "", "libraryDesc": "Descripció de la biblioteca per a ajudar a la gent a entendre'n el funcionament",
"githubHandle": "", "githubHandle": "",
"twitterHandle": "", "twitterHandle": "Usuari de twitter (opcional), per tal que puguem donar-vos crèdit quan fem la promoció a Twitter",
"website": "" "website": "Enllaç al vostre lloc web personal o a qualsevol altre (opcional)"
}, },
"errors": { "errors": {
"required": "", "required": "Requerit",
"website": "" "website": "Introduïu una URL vàlida"
}, },
"noteDescription": { "noteDescription": {
"pre": "", "pre": "Envieu la vostra biblioteca perquè sigui inclosa al ",
"link": "", "link": "repositori públic",
"post": "" "post": "per tal que altres persones puguin fer-ne ús en els seus dibuixos."
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "", "pre": "La biblioteca ha de ser aprovada manualment. Si us plau, llegiu les ",
"link": "", "link": "directrius",
"post": "" "post": " abans d'enviar-hi res. Necessitareu un compte de GitHub per a comunicar i fer-hi canvis si cal, però no és requisit imprescindible."
}, },
"noteLicense": { "noteLicense": {
"pre": "", "pre": "Quan l'envieu, accepteu que la biblioteca sigui publicada sota la ",
"link": "", "link": "llicència MIT, ",
"post": "" "post": "que, en resum, vol dir que qualsevol persona pot fer-ne ús sense restriccions."
}, },
"noteItems": "", "noteItems": "Cada element de la biblioteca ha de tenir el seu propi nom per tal que sigui filtrable. S'hi inclouran els elements següents:",
"atleastOneLibItem": "" "atleastOneLibItem": "Si us plau, seleccioneu si més no un element de la biblioteca per a començar"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "Biblioteca enviada",
"content": "", "content": "Gràcies, {{authorName}}. La vostra biblioteca ha estat enviada per a ser revisada. Podeu comprovar-ne l'estat",
"link": "" "link": "aquí"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "Restableix la biblioteca",
"removeItemsFromLib": "" "removeItemsFromLib": "Suprimeix els elements seleccionats de la llibreria"
}, },
"encrypted": { "encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai.", "tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors d’Excalidraw no els veuran mai.",
@@ -345,7 +361,7 @@
"width": "Amplada" "width": "Amplada"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "Afegit a la biblioteca",
"copyStyles": "S'han copiat els estils.", "copyStyles": "S'han copiat els estils.",
"copyToClipboard": "S'ha copiat al porta-retalls.", "copyToClipboard": "S'ha copiat al porta-retalls.",
"copyToClipboardAsPng": "S'ha copiat {{exportSelection}} al porta-retalls en format PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "S'ha copiat {{exportSelection}} al porta-retalls en format PNG\n({{exportColorScheme}})",
@@ -360,29 +376,29 @@
"f1f3f5": "Gris 1", "f1f3f5": "Gris 1",
"fff5f5": "Vermell 0", "fff5f5": "Vermell 0",
"fff0f6": "Rosa 0", "fff0f6": "Rosa 0",
"f8f0fc": "", "f8f0fc": "Malva 0",
"f3f0ff": "", "f3f0ff": "Violat 0",
"edf2ff": "", "edf2ff": "Indi 0",
"e7f5ff": "Blau 0", "e7f5ff": "Blau 0",
"e3fafc": "", "e3fafc": "Cian 0",
"e6fcf5": "", "e6fcf5": "Xarxet 0",
"ebfbee": "Verd 0", "ebfbee": "Verd 0",
"f4fce3": "", "f4fce3": "Llima 0",
"fff9db": "Groc 0", "fff9db": "Groc 0",
"fff4e6": "", "fff4e6": "Taronja 0",
"transparent": "Transparent", "transparent": "Transparent",
"ced4da": "Gris 4", "ced4da": "Gris 4",
"868e96": "Gris 6", "868e96": "Gris 6",
"fa5252": "Vermell 6", "fa5252": "Vermell 6",
"e64980": "Rosa 6", "e64980": "Rosa 6",
"be4bdb": "", "be4bdb": "Malva 6",
"7950f2": "", "7950f2": "Violat 6",
"4c6ef5": "", "4c6ef5": "Indi 6",
"228be6": "Blau 6", "228be6": "Blau 6",
"15aabf": "", "15aabf": "Cian 6",
"12b886": "", "12b886": "Xarxet 6",
"40c057": "Verd 6", "40c057": "Verd 6",
"82c91e": "", "82c91e": "Llima 6",
"fab005": "Groc 6", "fab005": "Groc 6",
"fd7e14": "Taronja 6", "fd7e14": "Taronja 6",
"000000": "Negre", "000000": "Negre",
@@ -390,14 +406,14 @@
"495057": "Gris 7", "495057": "Gris 7",
"c92a2a": "Vermell 9", "c92a2a": "Vermell 9",
"a61e4d": "Rosa 9", "a61e4d": "Rosa 9",
"862e9c": "", "862e9c": "Malva 9",
"5f3dc4": "", "5f3dc4": "Violat 9",
"364fc7": "", "364fc7": "Indi 9",
"1864ab": "Blau 9", "1864ab": "Blau 9",
"0b7285": "", "0b7285": "Cian 9",
"087f5b": "", "087f5b": "Xarxet 9",
"2b8a3e": "Verd 9", "2b8a3e": "Verd 9",
"5c940d": "", "5c940d": "Llima 9",
"e67700": "Groc 9", "e67700": "Groc 9",
"d9480f": "Taronja 9" "d9480f": "Taronja 9"
} }
+20 -4
View File
@@ -64,6 +64,7 @@
"cartoonist": "", "cartoonist": "",
"fileTitle": "", "fileTitle": "",
"colorPicker": "", "colorPicker": "",
"canvasColors": "",
"canvasBackground": "Pozadí plátna", "canvasBackground": "Pozadí plátna",
"drawingCanvas": "", "drawingCanvas": "",
"layers": "", "layers": "",
@@ -102,7 +103,15 @@
"showBackground": "", "showBackground": "",
"toggleTheme": "Přepnout tmavý řežim", "toggleTheme": "Přepnout tmavý řežim",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -171,7 +180,8 @@
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Výběr", "selection": "Výběr",
@@ -184,7 +194,9 @@
"freedraw": "Kreslení", "freedraw": "Kreslení",
"text": "Text", "text": "Text",
"library": "", "library": "",
"lock": "" "lock": "",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
@@ -207,7 +219,9 @@
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "", "blog": "",
"click": "kliknutí", "click": "kliknutí",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "", "curvedArrow": "",
"curvedLine": "", "curvedLine": "",
"documentation": "", "documentation": "",
+21 -5
View File
@@ -5,7 +5,7 @@
"selectAll": "Marker alle", "selectAll": "Marker alle",
"multiSelect": "", "multiSelect": "",
"moveCanvas": "", "moveCanvas": "",
"cut": "", "cut": "Klip",
"copy": "Kopier", "copy": "Kopier",
"copyAsPng": "Kopier til klippebord som PNG", "copyAsPng": "Kopier til klippebord som PNG",
"copyAsSvg": "Kopier til klippebord som SVG", "copyAsSvg": "Kopier til klippebord som SVG",
@@ -64,6 +64,7 @@
"cartoonist": "", "cartoonist": "",
"fileTitle": "Filnavn", "fileTitle": "Filnavn",
"colorPicker": "Farvevælger", "colorPicker": "Farvevælger",
"canvasColors": "Brugt på lærred",
"canvasBackground": "", "canvasBackground": "",
"drawingCanvas": "", "drawingCanvas": "",
"layers": "", "layers": "",
@@ -102,7 +103,15 @@
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": "",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -171,7 +180,8 @@
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "", "selection": "",
@@ -184,7 +194,9 @@
"freedraw": "", "freedraw": "",
"text": "", "text": "",
"library": "", "library": "",
"lock": "" "lock": "",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "", "canvasActions": "",
@@ -207,7 +219,9 @@
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Læs vores blog", "blog": "Læs vores blog",
"click": "", "click": "",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "", "curvedArrow": "",
"curvedLine": "", "curvedLine": "",
"documentation": "", "documentation": "",
+25 -9
View File
@@ -64,6 +64,7 @@
"cartoonist": "Karikaturist", "cartoonist": "Karikaturist",
"fileTitle": "Dateiname", "fileTitle": "Dateiname",
"colorPicker": "Farbauswähler", "colorPicker": "Farbauswähler",
"canvasColors": "Auf Leinwand verwendet",
"canvasBackground": "Zeichenflächenhintergrund", "canvasBackground": "Zeichenflächenhintergrund",
"drawingCanvas": "Leinwand", "drawingCanvas": "Leinwand",
"layers": "Ebenen", "layers": "Ebenen",
@@ -100,9 +101,17 @@
"share": "Teilen", "share": "Teilen",
"showStroke": "Auswahl für Strichfarbe anzeigen", "showStroke": "Auswahl für Strichfarbe anzeigen",
"showBackground": "Hintergrundfarbe auswählen", "showBackground": "Hintergrundfarbe auswählen",
"toggleTheme": "Design umschalten", "toggleTheme": "Thema umschalten",
"personalLib": "Persönliche Bibliothek", "personalLib": "Persönliche Bibliothek",
"excalidrawLib": "Excalidraw-Bibliothek" "excalidrawLib": "Excalidraw-Bibliothek",
"decreaseFontSize": "Schrift verkleinern",
"increaseFontSize": "Schrift vergrößern",
"unbindText": "Text lösen",
"link": {
"edit": "Link bearbeiten",
"create": "Link erstellen",
"label": "Link"
}
}, },
"buttons": { "buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen", "clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
@@ -171,7 +180,8 @@
"imageInsertError": "Das Bild konnte nicht eingefügt werden. Versuche es später erneut...", "imageInsertError": "Das Bild konnte nicht eingefügt werden. Versuche es später erneut...",
"fileTooBig": "Die Datei ist zu groß. Die maximal zulässige Größe ist {{maxSize}}.", "fileTooBig": "Die Datei ist zu groß. Die maximal zulässige Größe ist {{maxSize}}.",
"svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.", "svgImageInsertError": "SVG-Bild konnte nicht eingefügt werden. Das SVG-Markup sieht ungültig aus.",
"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": { "toolBar": {
"selection": "Auswahl", "selection": "Auswahl",
@@ -184,7 +194,9 @@
"freedraw": "Zeichnen", "freedraw": "Zeichnen",
"text": "Text", "text": "Text",
"library": "Bibliothek", "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": { "headings": {
"canvasActions": "Aktionen für Zeichenfläche", "canvasActions": "Aktionen für Zeichenfläche",
@@ -197,17 +209,19 @@
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist", "freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
"text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst", "text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst",
"text_selected": "Doppelklicken oder Eingabetaste drücken, um Text zu bearbeiten", "text_selected": "Doppelklicken oder Eingabetaste drücken, um Text zu bearbeiten",
"text_editing": "Drücke Escape oder Strg/Cmd+Eingabetaste, um die Bearbeitung abzuschließen", "text_editing": "Drücke Escape oder CtrlOrCmd+Eingabetaste, um die Bearbeitung abzuschließen",
"linearElementMulti": "Zum Beenden auf den letzten Punkt klicken oder Escape oder Eingabe drücken", "linearElementMulti": "Zum Beenden auf den letzten Punkt klicken oder Escape oder Eingabe drücken",
"lockAngle": "Du kannst Winkel einschränken, indem du SHIFT gedrückt hältst", "lockAngle": "Du kannst Winkel einschränken, indem du SHIFT gedrückt hältst",
"resize": "Du kannst die Proportionen einschränken, indem du SHIFT während der Größenänderung gedrückt hältst. Halte ALT gedrückt, um die Größe vom Zentrum aus zu ändern", "resize": "Du kannst die Proportionen einschränken, indem du SHIFT während der Größenänderung gedrückt hältst. Halte ALT gedrückt, um die Größe vom Zentrum aus zu ändern",
"resizeImage": "Du kannst die Größe frei ändern, indem du SHIFT gedrückt hältst; halte ALT, um die Größe vom Zentrum aus zu ändern", "resizeImage": "Du kannst die Größe frei ändern, indem du SHIFT gedrückt hältst; halte ALT, um die Größe vom Zentrum aus zu ändern",
"rotate": "Du kannst Winkel einschränken, indem du SHIFT während der Drehung gedrückt hältst", "rotate": "Du kannst Winkel einschränken, indem du SHIFT während der Drehung gedrückt hältst",
"lineEditor_info": "Doppelklicken oder Eingabetaste drücken, um Punkte zu bearbeiten", "lineEditor_info": "Doppelklicken oder Eingabetaste drücken, um Punkte zu bearbeiten",
"lineEditor_pointSelected": "Drücke Löschen, um Punkt zu entfernen, Strg+D oder Cmd+D zum Duplizieren oder ziehe zum Verschieben", "lineEditor_pointSelected": "Drücke Löschen, um Punkt(e) zu entfernen, CtrlOrCmd+D zum Duplizieren oder ziehe zum Verschieben",
"lineEditor_nothingSelected": "Wähle einen Punkt zum Verschieben oder Löschen oder halte die Alt-Taste gedrückt und klicke, um neue Punkte hinzuzufügen", "lineEditor_nothingSelected": "Wähle einen zu bearbeitenden Punkt (halte SHIFT gedrückt um mehrere Punkte auszuwählen),\noder halte Alt gedrückt und klicke um neue Punkte hinzuzufügen",
"placeImage": "Klicken, um das Bild zu platzieren oder klicken und ziehen um seine Größe manuell zu setzen", "placeImage": "Klicken, um das Bild zu platzieren oder klicken und ziehen um seine Größe manuell zu setzen",
"publishLibrary": "Veröffentliche deine eigene Bibliothek" "publishLibrary": "Veröffentliche deine eigene Bibliothek",
"bindTextToElement": "Zum Hinzufügen Eingabetaste drücken",
"deepBoxSelect": "Halte CtrlOrCmd gedrückt, um innerhalb der Gruppe auszuwählen, und um Ziehen zu vermeiden"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Vorschau kann nicht angezeigt werden", "cannotShowPreview": "Vorschau kann nicht angezeigt werden",
@@ -243,7 +257,7 @@
"exportDialog": { "exportDialog": {
"disk_title": "Auf Festplatte speichern", "disk_title": "Auf Festplatte speichern",
"disk_details": "Exportiere die Zeichnungsdaten in eine Datei, die Du später importieren kannst.", "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_title": "Teilbarer Link",
"link_details": "Als schreibgeschützten Link exportieren.", "link_details": "Als schreibgeschützten Link exportieren.",
"link_button": "Als Link exportieren", "link_button": "Als Link exportieren",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Lies unseren Blog", "blog": "Lies unseren Blog",
"click": "klicken", "click": "klicken",
"deepSelect": "Auswahl innerhalb der Gruppe",
"deepBoxSelect": "Auswahl innerhalb der Gruppe, und Ziehen vermeiden",
"curvedArrow": "Gebogener Pfeil", "curvedArrow": "Gebogener Pfeil",
"curvedLine": "Gebogene Linie", "curvedLine": "Gebogene Linie",
"documentation": "Dokumentation", "documentation": "Dokumentation",
+29 -13
View File
@@ -64,6 +64,7 @@
"cartoonist": "Σκιτσογράφος", "cartoonist": "Σκιτσογράφος",
"fileTitle": "Όνομα αρχείου", "fileTitle": "Όνομα αρχείου",
"colorPicker": "Επιλογή Χρώματος", "colorPicker": "Επιλογή Χρώματος",
"canvasColors": "",
"canvasBackground": "Φόντο καμβά", "canvasBackground": "Φόντο καμβά",
"drawingCanvas": "Σχεδίαση καμβά", "drawingCanvas": "Σχεδίαση καμβά",
"layers": "Στρώματα", "layers": "Στρώματα",
@@ -101,8 +102,16 @@
"showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς", "showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς",
"showBackground": "Εμφάνιση επιλογέα χρώματος φόντου", "showBackground": "Εμφάνιση επιλογέα χρώματος φόντου",
"toggleTheme": "Εναλλαγή θέματος", "toggleTheme": "Εναλλαγή θέματος",
"personalLib": "", "personalLib": "Προσωπική Βιβλιοθήκη",
"excalidrawLib": "" "excalidrawLib": "Βιβλιοθήκη Excalidraw",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "Επαναφορά του καμβά", "clearReset": "Επαναφορά του καμβά",
@@ -171,7 +180,8 @@
"imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...", "imageInsertError": "Αδυναμία εισαγωγής εικόνας. Προσπαθήστε ξανά αργότερα...",
"fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.", "fileTooBig": "Το αρχείο είναι πολύ μεγάλο. Το μέγιστο επιτρεπόμενο μέγεθος είναι {{maxSize}}.",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "Μη έγκυρο SVG." "invalidSVGString": "Μη έγκυρο SVG.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Επιλογή", "selection": "Επιλογή",
@@ -184,7 +194,9 @@
"freedraw": "Σχεδίαση", "freedraw": "Σχεδίαση",
"text": "Κείμενο", "text": "Κείμενο",
"library": "Βιβλιοθήκη", "library": "Βιβλιοθήκη",
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο" "lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "Ενέργειες καμβά", "canvasActions": "Ενέργειες καμβά",
@@ -204,10 +216,12 @@
"resizeImage": "", "resizeImage": "",
"rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή", "rotate": "Μπορείς να περιορίσεις τις γωνίες κρατώντας πατημένο το πλήκτρο SHIFT κατά την περιστροφή",
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία", "lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
"lineEditor_pointSelected": "Πιέστε Διαγραφή για να αφαιρέσετε το σημείου, CtrlOrCmd+D για να το αντιγράψετε ή σύρτε το για να το μετακινήσετε", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για μετακίνηση ή αφαίρεση, ή κρατήστε παρατεταμένα το Alt και κάντε κλικ για να προσθέσετε νέα σημεία", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη" "publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης", "cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Διαβάστε το Blog μας", "blog": "Διαβάστε το Blog μας",
"click": "κλικ", "click": "κλικ",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "Κυρτό βέλος", "curvedArrow": "Κυρτό βέλος",
"curvedLine": "Κυρτή γραμμή", "curvedLine": "Κυρτή γραμμή",
"documentation": "Εγχειρίδιο", "documentation": "Εγχειρίδιο",
@@ -295,7 +311,7 @@
"website": "" "website": ""
}, },
"errors": { "errors": {
"required": "", "required": "Απαιτείται",
"website": "Εισάγετε μια έγκυρη διεύθυνση URL" "website": "Εισάγετε μια έγκυρη διεύθυνση URL"
}, },
"noteDescription": { "noteDescription": {
@@ -305,7 +321,7 @@
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "", "pre": "",
"link": "", "link": "οδηγίες",
"post": "" "post": ""
}, },
"noteLicense": { "noteLicense": {
@@ -319,11 +335,11 @@
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "",
"content": "", "content": "",
"link": "" "link": "εδώ"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "Καθαρισμός βιβλιοθήκης",
"removeItemsFromLib": "" "removeItemsFromLib": "Αφαίρεση επιλεγμένων αντικειμένων από τη βιβλιοθήκη"
}, },
"encrypted": { "encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.", "tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
@@ -345,7 +361,7 @@
"width": "Πλάτος" "width": "Πλάτος"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "Προστέθηκε στη βιβλιοθήκη",
"copyStyles": "Αντιγράφηκαν στυλ.", "copyStyles": "Αντιγράφηκαν στυλ.",
"copyToClipboard": "Αντιγράφηκε στο πρόχειρο.", "copyToClipboard": "Αντιγράφηκε στο πρόχειρο.",
"copyToClipboardAsPng": "Αντιγράφηκε {{exportSelection}} στο πρόχειρο ως PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Αντιγράφηκε {{exportSelection}} στο πρόχειρο ως PNG\n({{exportColorScheme}})",
+17 -4
View File
@@ -16,6 +16,7 @@
"delete": "Delete", "delete": "Delete",
"copyStyles": "Copy styles", "copyStyles": "Copy styles",
"pasteStyles": "Paste styles", "pasteStyles": "Paste styles",
"fontColor": "Font color",
"stroke": "Stroke", "stroke": "Stroke",
"background": "Background", "background": "Background",
"fill": "Fill", "fill": "Fill",
@@ -64,6 +65,7 @@
"cartoonist": "Cartoonist", "cartoonist": "Cartoonist",
"fileTitle": "File name", "fileTitle": "File name",
"colorPicker": "Color picker", "colorPicker": "Color picker",
"canvasColors": "Used on canvas",
"canvasBackground": "Canvas background", "canvasBackground": "Canvas background",
"drawingCanvas": "Drawing canvas", "drawingCanvas": "Drawing canvas",
"layers": "Layers", "layers": "Layers",
@@ -104,7 +106,13 @@
"personalLib": "Personal Library", "personalLib": "Personal Library",
"excalidrawLib": "Excalidraw Library", "excalidrawLib": "Excalidraw Library",
"decreaseFontSize": "Decrease font size", "decreaseFontSize": "Decrease font size",
"increaseFontSize": "Increase font size" "increaseFontSize": "Increase font size",
"unbindText": "Unbind text",
"link": {
"edit": "Edit link",
"create": "Create link",
"label": "Link"
}
}, },
"buttons": { "buttons": {
"clearReset": "Reset the canvas", "clearReset": "Reset the canvas",
@@ -173,7 +181,8 @@
"imageInsertError": "Couldn't insert image. Try again later...", "imageInsertError": "Couldn't insert image. Try again later...",
"fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.", "fileTooBig": "File is too big. Maximum allowed size is {{maxSize}}.",
"svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.", "svgImageInsertError": "Couldn't insert SVG image. The SVG markup looks invalid.",
"invalidSVGString": "Invalid SVG." "invalidSVGString": "Invalid SVG.",
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again."
}, },
"toolBar": { "toolBar": {
"selection": "Selection", "selection": "Selection",
@@ -186,7 +195,10 @@
"freedraw": "Draw", "freedraw": "Draw",
"text": "Text", "text": "Text",
"library": "Library", "library": "Library",
"lock": "Keep selected tool active after drawing" "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",
"eraser": "Eraser"
}, },
"headings": { "headings": {
"canvasActions": "Canvas actions", "canvasActions": "Canvas actions",
@@ -211,7 +223,8 @@
"placeImage": "Click to place the image, or click and drag to set its size manually", "placeImage": "Click to place the image, or click and drag to set its size manually",
"publishLibrary": "Publish your own library", "publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text", "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": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",
+62 -46
View File
@@ -64,6 +64,7 @@
"cartoonist": "Caricatura", "cartoonist": "Caricatura",
"fileTitle": "Nombre del archivo", "fileTitle": "Nombre del archivo",
"colorPicker": "Selector de color", "colorPicker": "Selector de color",
"canvasColors": "Usado en lienzo",
"canvasBackground": "Fondo del lienzo", "canvasBackground": "Fondo del lienzo",
"drawingCanvas": "Lienzo de dibujo", "drawingCanvas": "Lienzo de dibujo",
"layers": "Capas", "layers": "Capas",
@@ -101,8 +102,16 @@
"showStroke": "Mostrar selector de color de trazo", "showStroke": "Mostrar selector de color de trazo",
"showBackground": "Mostrar el selector de color de fondo", "showBackground": "Mostrar el selector de color de fondo",
"toggleTheme": "Alternar tema", "toggleTheme": "Alternar tema",
"personalLib": "", "personalLib": "Biblioteca personal",
"excalidrawLib": "" "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": { "buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo", "clearReset": "Limpiar lienzo y reiniciar el color de fondo",
@@ -138,10 +147,10 @@
"exitZenMode": "Salir del modo Zen", "exitZenMode": "Salir del modo Zen",
"cancel": "Cancelar", "cancel": "Cancelar",
"clear": "Borrar", "clear": "Borrar",
"remove": "", "remove": "Eliminar",
"publishLibrary": "", "publishLibrary": "Publicar",
"submit": "", "submit": "Enviar",
"confirm": "" "confirm": "Confirmar"
}, },
"alerts": { "alerts": {
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?", "clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
@@ -163,7 +172,7 @@
"cannotRestoreFromImage": "No se pudo restaurar la escena desde este archivo de imagen", "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.", "invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?", "resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
"removeItemsFromsLibrary": "", "removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada." "invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
}, },
"errors": { "errors": {
@@ -171,7 +180,8 @@
"imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...", "imageInsertError": "No se pudo insertar la imagen. Inténtelo de nuevo más tarde...",
"fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.", "fileTooBig": "Archivo demasiado grande. El tamaño máximo permitido es {{maxSize}}.",
"svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.", "svgImageInsertError": "No se pudo insertar la imagen SVG. El código SVG parece inválido.",
"invalidSVGString": "SVG no válido." "invalidSVGString": "SVG no válido.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Selección", "selection": "Selección",
@@ -184,7 +194,9 @@
"freedraw": "Dibujar", "freedraw": "Dibujar",
"text": "Texto", "text": "Texto",
"library": "Biblioteca", "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": { "headings": {
"canvasActions": "Acciones del lienzo", "canvasActions": "Acciones del lienzo",
@@ -192,7 +204,7 @@
"shapes": "Formas" "shapes": "Formas"
}, },
"hints": { "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", "linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
"freeDraw": "Haz clic y arrastra, suelta al terminar", "freeDraw": "Haz clic y arrastra, suelta al terminar",
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección", "text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
@@ -204,10 +216,12 @@
"resizeImage": "Puede redimensionar libremente pulsando SHIFT,\npulse ALT para redimensionar desde el centro", "resizeImage": "Puede redimensionar libremente pulsando SHIFT,\npulse ALT para redimensionar desde el centro",
"rotate": "Puedes restringir los ángulos manteniendo presionado SHIFT mientras giras", "rotate": "Puedes restringir los ángulos manteniendo presionado SHIFT mientras giras",
"lineEditor_info": "Doble clic o pulse Enter para editar puntos", "lineEditor_info": "Doble clic o pulse Enter para editar puntos",
"lineEditor_pointSelected": "Presione Suprimir para eliminar el punto, CtrlOrCmd+D para duplicarlo, o arrástrelo para moverlo", "lineEditor_pointSelected": "Presione Suprimir para eliminar el/los punto(s), CtrlOrCmd+D para duplicarlo, o arrástrelo para moverlo",
"lineEditor_nothingSelected": "Selecciona un punto sea para mover o eliminar, o mantén pulsado Alt y haz clic para añadir nuevos puntos", "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", "placeImage": "Haga clic para colocar la imagen o haga clic y arrastre para establecer su tamaño manualmente",
"publishLibrary": "" "publishLibrary": "Publica tu propia biblioteca",
"bindTextToElement": "Presione Entrar para agregar",
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa", "cannotShowPreview": "No se puede mostrar la vista previa",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Lea nuestro blog", "blog": "Lea nuestro blog",
"click": "clic", "click": "clic",
"deepSelect": "Selección profunda",
"deepBoxSelect": "Seleccione en profundidad dentro de la caja, y evite arrastrar",
"curvedArrow": "Flecha curva", "curvedArrow": "Flecha curva",
"curvedLine": "Línea curva", "curvedLine": "Línea curva",
"documentation": "Documentación", "documentation": "Documentación",
@@ -278,52 +294,52 @@
"title": "Borrar lienzo" "title": "Borrar lienzo"
}, },
"publishDialog": { "publishDialog": {
"title": "", "title": "Publicar biblioteca",
"itemName": "", "itemName": "Nombre del artículo",
"authorName": "", "authorName": "Nombre del autor",
"githubUsername": "", "githubUsername": "Nombre de usuario de Github",
"twitterUsername": "", "twitterUsername": "Nombre de usuario de Twitter",
"libraryName": "", "libraryName": "Nombre de la librería",
"libraryDesc": "", "libraryDesc": "Descripción de la biblioteca",
"website": "", "website": "Sitio Web",
"placeholder": { "placeholder": {
"authorName": "", "authorName": "Nombre o nombre de usuario",
"libraryName": "", "libraryName": "Nombre de tu biblioteca",
"libraryDesc": "", "libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
"githubHandle": "", "githubHandle": "GitHub maneja (opcional), así que puede editar la biblioteca una vez enviada para su revisión",
"twitterHandle": "", "twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
"website": "" "website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
}, },
"errors": { "errors": {
"required": "", "required": "Requerido",
"website": "" "website": "Introduce una URL válida"
}, },
"noteDescription": { "noteDescription": {
"pre": "", "pre": "Envía tu biblioteca para ser incluida en el ",
"link": "", "link": "repositorio de librería pública",
"post": "" "post": "para que otras personas utilicen en sus dibujos."
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "", "pre": "La biblioteca debe ser aprobada manualmente primero. Por favor, lea la ",
"link": "", "link": "pautas",
"post": "" "post": " antes de enviar. Necesitará una cuenta de GitHub para comunicarse y hacer cambios si se solicita, pero no es estrictamente necesario."
}, },
"noteLicense": { "noteLicense": {
"pre": "", "pre": "Al enviar, usted acepta que la biblioteca se publicará bajo el ",
"link": "", "link": "Licencia MIT ",
"post": "" "post": "que en breve significa que cualquiera puede utilizarlos sin restricciones."
}, },
"noteItems": "", "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": "" "atleastOneLibItem": "Por favor, seleccione al menos un elemento de la biblioteca para empezar"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "Biblioteca enviada",
"content": "", "content": "Gracias {{authorName}}. Su biblioteca ha sido enviada para ser revisada. Puede seguir el estado",
"link": "" "link": "aquí"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "Reiniciar biblioteca",
"removeItemsFromLib": "" "removeItemsFromLib": "Eliminar elementos seleccionados de la biblioteca"
}, },
"encrypted": { "encrypted": {
"tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.", "tooltip": "Tus dibujos están cifrados de punto a punto, por lo que los servidores de Excalidraw nunca los verán.",
@@ -345,7 +361,7 @@
"width": "Ancho" "width": "Ancho"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "Añadido a la biblioteca",
"copyStyles": "Estilos copiados.", "copyStyles": "Estilos copiados.",
"copyToClipboard": "Copiado en el portapapeles.", "copyToClipboard": "Copiado en el portapapeles.",
"copyToClipboardAsPng": "Copiado {{exportSelection}} al portapapeles como PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Copiado {{exportSelection}} al portapapeles como PNG\n({{exportColorScheme}})",
+420
View File
@@ -0,0 +1,420 @@
{
"labels": {
"paste": "Itsatsi",
"pasteCharts": "Itsatsi grafikoak",
"selectAll": "Hautatu dena",
"multiSelect": "Gehitu elementua hautapenera",
"moveCanvas": "Mugitu oihala",
"cut": "Ebaki",
"copy": "Kopiatu",
"copyAsPng": "Kopiatu arbelera PNG gisa",
"copyAsSvg": "Kopiatu arbelera SVG gisa",
"bringForward": "Ekarri aurrerago",
"sendToBack": "Eraman atzera",
"bringToFront": "Ekarri aurrera",
"sendBackward": "Eraman atzerago",
"delete": "Ezabatu",
"copyStyles": "Kopiatu estiloak",
"pasteStyles": "Itsatsi estiloak",
"stroke": "Marra",
"background": "Atzeko planoa",
"fill": "Bete",
"strokeWidth": "Marraren zabalera",
"strokeStyle": "Marraren estiloa",
"strokeStyle_solid": "Solidoa",
"strokeStyle_dashed": "Marratua",
"strokeStyle_dotted": "Puntukatua",
"sloppiness": "Marraren trazoa",
"opacity": "Opakotasuna",
"textAlign": "Testuaren lerrokapena",
"edges": "Ertzak",
"sharp": "Ertz bizia",
"round": "Borobildua",
"arrowheads": "Gezi-puntak",
"arrowhead_none": "Bat ere ez",
"arrowhead_arrow": "Gezia",
"arrowhead_bar": "Barra",
"arrowhead_dot": "Puntua",
"arrowhead_triangle": "Hirukia",
"fontSize": "Letra-tamaina",
"fontFamily": "Letra-tipoa",
"onlySelected": "Hautapena soilik",
"withBackground": "Atzeko planoa",
"exportEmbedScene": "Txertatu eszena",
"exportEmbedScene_details": "Eszenaren datuak esportatutako PNG/SVG fitxategian gordeko dira, eszena bertatik berrezartzeko.\nEsportatutako fitxategien tamaina handituko da.",
"addWatermark": "Gehitu \"Excalidraw bidez egina\"",
"handDrawn": "Eskuz marraztua",
"normal": "Normala",
"code": "Kodea",
"small": "Txikia",
"medium": "Ertaina",
"large": "Handia",
"veryLarge": "Oso handia",
"solid": "Solidoa",
"hachure": "Itzalduna",
"crossHatch": "Marraduna",
"thin": "Mehea",
"bold": "Lodia",
"left": "Ezkerrean",
"center": "Erdian",
"right": "Eskuinean",
"extraBold": "Oso lodia",
"architect": "Arkitektoa",
"artist": "Artista",
"cartoonist": "Marrazkilaria",
"fileTitle": "Fitxategi izena",
"colorPicker": "Kolore-hautatzailea",
"canvasColors": "",
"canvasBackground": "Oihalaren atzeko planoa",
"drawingCanvas": "Marrazteko oihala",
"layers": "Geruzak",
"actions": "Ekintzak",
"language": "Hizkuntza",
"liveCollaboration": "Zuzeneko elkarlana",
"duplicateSelection": "Bikoiztu",
"untitled": "Izengabea",
"name": "Izena",
"yourName": "Zure izena",
"madeWithExcalidraw": "Excalidraw bidez egina",
"group": "Hautapena taldea bihurtu",
"ungroup": "Desegin hautapenaren taldea",
"collaborators": "Kolaboratzaileak",
"showGrid": "Erakutsi sareta",
"addToLibrary": "Gehitu liburutegira",
"removeFromLibrary": "Kendu liburutegitik",
"libraryLoadingMessage": "Liburutegia kargatzen…",
"libraries": "Arakatu liburutegiak",
"loadingScene": "Eszena kargatzen…",
"align": "Lerrokatu",
"alignTop": "Lerrokatu goian",
"alignBottom": "Lerrokatu behean",
"alignLeft": "Lerrokatu ezkerrean",
"alignRight": "Lerrokatu eskuinean",
"centerVertically": "Erdiratu bertikalki",
"centerHorizontally": "Erdiratu horizontalki",
"distributeHorizontally": "Banandu horizontalki",
"distributeVertically": "Banandu bertikalki",
"flipHorizontal": "Irauli horizontalki",
"flipVertical": "Irauli bertikalki",
"viewMode": "Ikuspegia",
"toggleExportColorScheme": "Aldatu esportatzeko kolorearen eszena",
"share": "Partekatu",
"showStroke": "Erakutsi marraren kolore-hautatzailea",
"showBackground": "Erakutsi atzeko planoaren kolore-hautatzailea",
"toggleTheme": "Aldatu gaia",
"personalLib": "Liburutegi pertsonala",
"excalidrawLib": "Excalidraw liburutegia",
"decreaseFontSize": "Txikitu letra tamaina",
"increaseFontSize": "Handitu letra tamaina",
"unbindText": "Askatu testua",
"link": {
"edit": "Editatu esteka",
"create": "Sortu esteka",
"label": "Esteka"
}
},
"buttons": {
"clearReset": "Garbitu oihala",
"exportJSON": "Esportatu fitxategira",
"exportImage": "Gorde irudi gisa",
"export": "Esportatu",
"exportToPng": "Esportatu PNG gisa",
"exportToSvg": "Esportatu SVG gisa",
"copyToClipboard": "Kopiatu arbelera",
"copyPngToClipboard": "Kopiatu PNG arbelera",
"scale": "Eskala",
"save": "Gorde uneko fitxategian",
"saveAs": "Gorde honela",
"load": "Kargatu",
"getShareableLink": "Lortu partekatzeko esteka",
"close": "Itxi",
"selectLanguage": "Hautatu hizkuntza",
"scrollBackToContent": "Joan atzera edukira",
"zoomIn": "Handiagotu",
"zoomOut": "Txikiagotu",
"resetZoom": "Leheneratu zooma",
"menu": "Menua",
"done": "Egina",
"edit": "Editatu",
"undo": "Desegin",
"redo": "Berregin",
"resetLibrary": "Leheneratu liburutegia",
"createNewRoom": "Sortu gela berria",
"fullScreen": "Pantaila osoa",
"darkMode": "Modu iluna",
"lightMode": "Modu argia",
"zenMode": "Zen modua",
"exitZenMode": "Irten Zen modutik",
"cancel": "Utzi",
"clear": "Garbitu",
"remove": "Kendu",
"publishLibrary": "Argitaratu",
"submit": "Bidali",
"confirm": "Bai"
},
"alerts": {
"clearReset": "Honek oihal osoa garbituko du. Ziur zaude?",
"couldNotCreateShareableLink": "Ezin izan da partekatzeko estekarik sortu.",
"couldNotCreateShareableLinkTooBig": "Ezin izan da partekatzeko estekarik sortu: eszena handiegia da",
"couldNotLoadInvalidFile": "Ezin izan da kargatu, fitxategiak ez du balio",
"importBackendFailed": "Inportazioak huts egin du.",
"cannotExportEmptyCanvas": "Ezin izan da oihal hutsa esportatu.",
"couldNotCopyToClipboard": "Ezin izan da arbelean kopiatu.",
"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": "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": "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": "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": "Canvas ekintzak",
"selectedShapeActions": "Hautatutako formaren ekintzak",
"shapes": "Formak"
},
"hints": {
"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": "Ezin da oihala aurreikusi",
"canvasTooBig": "Agian oihala handiegia da.",
"canvasTooBigTip": "Aholkua: saiatu urrunen dauden elementuak pixka bat hurbiltzen."
},
"errorSplash": {
"headingMain_pre": "Errore bat aurkitu da. Saiatu ",
"headingMain_button": "orria birkargatzen.",
"clearCanvasMessage": "Birkargatzea ez bada burutzen, saiatu ",
"clearCanvasMessage_button": "oihala garbitzen.",
"clearCanvasCaveat": " Honen ondorioz lana galduko da ",
"trackedToSentry_pre": "Identifikatzailearen errorea ",
"trackedToSentry_post": " gure sistemak behatu du.",
"openIssueMessage_pre": "Oso kontuz ibili gara zure eszenaren informazioa errorean ez sartzeko. Zure eszena pribatua ez bada, kontuan hartu gure ",
"openIssueMessage_button": "erroreen jarraipena egitea.",
"openIssueMessage_post": " Sartu beheko informazioa kopiatu eta itsatsi bidez GitHub issue-n.",
"sceneContent": "Eszenaren edukia:"
},
"roomDialog": {
"desc_intro": "Jendea zure uneko eszenara gonbida dezakezu zurekin elkarlanean aritzeko.",
"desc_privacy": "Ez kezkatu, saioak muturretik muturrerako enkriptatzea erabiltzen du, beraz, marrazten duzuna pribatua izango da. Gure zerbitzariak ere ezingo du ikusi zer egiten duzun.",
"button_startSession": "Hasi saioa",
"button_stopSession": "Itxi saioa",
"desc_inProgressIntro": "Zuzeneko lankidetza saioa abian da.",
"desc_shareLink": "Partekatu esteka hau elkarlanean aritu nahi duzun edonorekin:",
"desc_exitSession": "Saioa ixteak aretotik deskonektatuko zaitu, baina eszenarekin lanean jarraitu ahal izango duzu lokalean. Kontuan izan honek ez diela beste pertsonei eragingo, eta euren bertsioan elkarlanean aritu ahal izango dira.",
"shareTitle": "Sartu Excalidraw-en zuzeneko lankidetza-saio batean"
},
"errorDialog": {
"title": "Errorea"
},
"exportDialog": {
"disk_title": "Gorde diskoan",
"disk_details": "Esportatu eszenaren datuak geroago inportatu ahal izango duzun fitxategi batan.",
"disk_button": "Gorde fitxategian",
"link_title": "Partekatzeko esteka",
"link_details": "Esportatu irakurtzeko soilik moduko esteka.",
"link_button": "Esportatu esteka",
"excalidrawplus_description": "Gorde eszena zure Excalidraw+ laneko areara.",
"excalidrawplus_button": "Esportatu",
"excalidrawplus_exportError": "Une honetan ezin izan da esportatu Excalidraw+era..."
},
"helpDialog": {
"blog": "Irakurri gure bloga",
"click": "sakatu",
"deepSelect": "Hautapen sakona",
"deepBoxSelect": "Hautapen sakona egin laukizuzen bidez, eta saihestu arrastatzea",
"curvedArrow": "Gezi kurbatua",
"curvedLine": "Lerro kurbatua",
"documentation": "Dokumentazioa",
"doubleClick": "klik bikoitza",
"drag": "arrastatu",
"editor": "Editorea",
"editSelectedShape": "Editatu hautatutako forma (testua/gezia/lerroa)",
"github": "Arazorik izan al duzu? Eman horren berri",
"howto": "Jarraitu gure gidak",
"or": "edo",
"preventBinding": "Saihestu gezien gainjartzea",
"shapes": "Formak",
"shortcuts": "Laster-teklak",
"textFinish": "Bukatu edizioa (testu editorea)",
"textNewLine": "Gehitu lerro berri bat (testu editorea)",
"title": "Laguntza",
"view": "Bistaratu",
"zoomToFit": "Egin zoom elementu guztiak ikusteko",
"zoomToSelection": "Zooma hautapenera"
},
"clearCanvasDialog": {
"title": "Garbitu oihala"
},
"publishDialog": {
"title": "Argitaratu liburutegia",
"itemName": "Elementuaren izena",
"authorName": "Egilearen izena",
"githubUsername": "GitHub-eko erabiltzaile-izena",
"twitterUsername": "Twitter-eko erabiltzaile-izena",
"libraryName": "Liburutegiaren izena",
"libraryDesc": "Liburutegiaren deskripzioa",
"website": "Webgunea",
"placeholder": {
"authorName": "Zure izena edo erabiltzaile-izena",
"libraryName": "Zure liburutegiaren izena",
"libraryDesc": "Zure liburutegiaren deskripzioa laguntzeko jendeari ulertzen haren erabilpena",
"githubHandle": "GitHub heldulekua (aukerakoa), liburutegia editatu ahal izateko berrikustera bidalitakoan",
"twitterHandle": "Twitter-eko erabiltzaile-izena (aukerakoa), badakigu nori kreditatu behar dugun Twitter bidez sustatzeko",
"website": "Estekatu zure webgunera edo nahi duzun tokira (aukerakoa)"
},
"errors": {
"required": "Beharrezkoa",
"website": "Sartu baliozko URL bat"
},
"noteDescription": {
"pre": "Bidali zure liburutegira sartu ahal izateko ",
"link": "zure liburutegiko biltegian",
"post": "beste jendeak bere marrazkietan erabili ahal izateko."
},
"noteGuidelines": {
"pre": "Liburutegia eskuz onartu behar da. Irakurri ",
"link": "gidalerroak",
"post": " bidali aurretik. GitHub kontu bat edukitzea komeni da komunikatzeko eta aldaketak egin ahal izateko, baina ez da guztiz beharrezkoa."
},
"noteLicense": {
"pre": "Bidaltzen baduzu, onartzen duzu liburutegia ",
"link": "MIT lizentziarekin argitaratuko dela, ",
"post": "zeinak, laburbilduz, esan nahi du edozeinek erabiltzen ahal duela murrizketarik gabe."
},
"noteItems": "Liburutegiko elementu bakoitzak bere izena eduki behar du iragazi ahal izateko. Liburutegiko hurrengo elementuak barne daude:",
"atleastOneLibItem": "Hautatu gutxienez liburutegiko elementu bat gutxienez hasi ahal izateko"
},
"publishSuccessDialog": {
"title": "Liburutegia bidali da",
"content": "Eskerrik asko {{authorName}}. Zure liburutegia bidali da berrikustera. Jarraitu dezakezu haren egoera",
"link": "hemen"
},
"confirmDialog": {
"resetLibrary": "Leheneratu liburutegia",
"removeItemsFromLib": "Kendu hautatutako elementuak liburutegitik"
},
"encrypted": {
"tooltip": "Zure marrazkiak muturretik muturrera enkriptatu dira, beraz Excalidraw-ren zerbitzariek ezingo dituzte ikusi.",
"link": "Excalidraw-ren muturretik muturrerako enkriptatzearen gaineko mezua blogean"
},
"stats": {
"angle": "Angelua",
"element": "Elementua",
"elements": "Elementuak",
"height": "Altuera",
"scene": "Eszena",
"selected": "Hautatua",
"storage": "Biltegia",
"title": "Datuak",
"total": "Guztira",
"version": "Bertsioa",
"versionCopy": "Klikatu kopiatzeko",
"versionNotAvailable": "Bertsio ez eskuragarria",
"width": "Zabalera"
},
"toast": {
"addedToLibrary": "Liburutegira gehitu da",
"copyStyles": "Estiloak kopiatu dira.",
"copyToClipboard": "Arbelean kopiatu da.",
"copyToClipboardAsPng": "{{exportSelection}} kopiatu da arbelean PNG gisa\n({{exportColorScheme}})",
"fileSaved": "Fitxategia gorde da.",
"fileSavedToFilename": "{filename}-n gorde da",
"canvas": "oihala",
"selection": "hautapena"
},
"colors": {
"ffffff": "Zuria",
"f8f9fa": "Grisa 0",
"f1f3f5": "Grisa 1",
"fff5f5": "Gorria 0",
"fff0f6": "Arrosa 0",
"f8f0fc": "Mahats kolorea 0",
"f3f0ff": "Bioleta 0",
"edf2ff": "Indigoa 0",
"e7f5ff": "Urdina 0",
"e3fafc": "Ziana 0",
"e6fcf5": "Berde urdinxka 0",
"ebfbee": "Berdea 0",
"f4fce3": "Lima 0",
"fff9db": "Horia 0",
"fff4e6": "Laranja 0",
"transparent": "Gardena",
"ced4da": "Grisa 4",
"868e96": "Grisa 6",
"fa5252": "Gorria 6",
"e64980": "Arrosa 6",
"be4bdb": "Mahats kolorea 6",
"7950f2": "Bioleta 6",
"4c6ef5": "Indigoa 6",
"228be6": "Urdina 6",
"15aabf": "Ziana 6",
"12b886": "Berde urdinxka 6",
"40c057": "Berdea 6",
"82c91e": "Lima 6",
"fab005": "Horia 6",
"fd7e14": "Laranja 6",
"000000": "Beltza",
"343a40": "Grisa 8",
"495057": "Grisa 7",
"c92a2a": "Gorria 9",
"a61e4d": "Arrosa 9",
"862e9c": "Mahats kolorea 9",
"5f3dc4": "Bioleta 9",
"364fc7": "Indigoa 9",
"1864ab": "Urdina 9",
"0b7285": "Ziana 9",
"087f5b": "Berde urdinxka 9",
"2b8a3e": "Berdea 9",
"5c940d": "Lima 9",
"e67700": "Horia 9",
"d9480f": "Laranja 9"
}
}
+22 -6
View File
@@ -64,6 +64,7 @@
"cartoonist": "کارتونیست", "cartoonist": "کارتونیست",
"fileTitle": "نام فایل", "fileTitle": "نام فایل",
"colorPicker": "انتخابگر رنگ", "colorPicker": "انتخابگر رنگ",
"canvasColors": "",
"canvasBackground": "بوم", "canvasBackground": "بوم",
"drawingCanvas": "بوم نقاشی", "drawingCanvas": "بوم نقاشی",
"layers": "لایه ها", "layers": "لایه ها",
@@ -102,7 +103,15 @@
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه", "showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
"toggleTheme": "تغییر تم", "toggleTheme": "تغییر تم",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "پاکسازی بوم نقاشی", "clearReset": "پاکسازی بوم نقاشی",
@@ -171,7 +180,8 @@
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "گزینش", "selection": "گزینش",
@@ -184,7 +194,9 @@
"freedraw": "کشیدن", "freedraw": "کشیدن",
"text": "متن", "text": "متن",
"library": "کتابخانه", "library": "کتابخانه",
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار" "lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "عملیات روی بوم", "canvasActions": "عملیات روی بوم",
@@ -204,10 +216,12 @@
"resizeImage": "", "resizeImage": "",
"rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید", "rotate": "با نگه داشتن SHIFT هنگام چرخش می توانید زاویه ها را محدود کنید",
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید", "lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید.", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "یک نقطه را برای جابجایی یا حذف انتخاب کنید یا کلید Alt بگیرید و کلیک کنید تا بتوانید یک نقطه جدید اضافه کنید", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود", "cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "بلاگ ما را بخوانید", "blog": "بلاگ ما را بخوانید",
"click": "کلیک", "click": "کلیک",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "فلش خمیده", "curvedArrow": "فلش خمیده",
"curvedLine": "منحنی", "curvedLine": "منحنی",
"documentation": "مستندات", "documentation": "مستندات",
+22 -6
View File
@@ -64,6 +64,7 @@
"cartoonist": "Sarjakuva", "cartoonist": "Sarjakuva",
"fileTitle": "Tiedostonimi", "fileTitle": "Tiedostonimi",
"colorPicker": "Värin valinta", "colorPicker": "Värin valinta",
"canvasColors": "",
"canvasBackground": "Piirtoalueen tausta", "canvasBackground": "Piirtoalueen tausta",
"drawingCanvas": "Piirtoalue", "drawingCanvas": "Piirtoalue",
"layers": "Tasot", "layers": "Tasot",
@@ -102,7 +103,15 @@
"showBackground": "Näytä taustavärin valitsin", "showBackground": "Näytä taustavärin valitsin",
"toggleTheme": "Vaihda teema", "toggleTheme": "Vaihda teema",
"personalLib": "Oma kirjasto", "personalLib": "Oma kirjasto",
"excalidrawLib": "Excalidraw kirjasto" "excalidrawLib": "Excalidraw kirjasto",
"decreaseFontSize": "Pienennä kirjasinkokoa",
"increaseFontSize": "Kasvata kirjasinkokoa",
"unbindText": "",
"link": {
"edit": "Muokkaa linkkiä",
"create": "Luo linkki",
"label": "Linkki"
}
}, },
"buttons": { "buttons": {
"clearReset": "Tyhjennä piirtoalue", "clearReset": "Tyhjennä piirtoalue",
@@ -171,7 +180,8 @@
"imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...", "imageInsertError": "Kuvan lisääminen epäonnistui. Yritä myöhemmin uudelleen...",
"fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.", "fileTooBig": "Tiedosto on liian suuri. Suurin sallittu koko on {{maxSize}}.",
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.", "svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
"invalidSVGString": "Virheellinen SVG." "invalidSVGString": "Virheellinen SVG.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Valinta", "selection": "Valinta",
@@ -184,7 +194,9 @@
"freedraw": "Piirrä", "freedraw": "Piirrä",
"text": "Teksti", "text": "Teksti",
"library": "Kirjasto", "library": "Kirjasto",
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen" "lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
"penMode": "Estä nipistyszoomaus ja vastaanota ainoastaan kynällä piirretty",
"link": "Lisää/päivitä linkki valitulle muodolle"
}, },
"headings": { "headings": {
"canvasActions": "Piirtoalueen toiminnot", "canvasActions": "Piirtoalueen toiminnot",
@@ -204,10 +216,12 @@
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri", "resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi", "rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
"lineEditor_info": "Kaksoisnapauta tai paina Enter muokataksesi pisteitä", "lineEditor_info": "Kaksoisnapauta tai paina Enter muokataksesi pisteitä",
"lineEditor_pointSelected": "Paina Delete poistaaksesi pisteen, Ctrl tai Cmd+D monistaaksesi, tai raahaa liikuttaaksesi", "lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
"lineEditor_nothingSelected": "Valitse liikutettava tai poistettava piste, tai pidä ALT-näppäintä alaspainettuna ja napsauta lisätäksesi uusia pisteitä", "lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti", "placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
"publishLibrary": "Julkaise oma kirjasto" "publishLibrary": "Julkaise oma kirjasto",
"bindTextToElement": "Lisää tekstiä painamalla enter",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Esikatselua ei voitu näyttää", "cannotShowPreview": "Esikatselua ei voitu näyttää",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Lue blogiamme", "blog": "Lue blogiamme",
"click": "klikkaa", "click": "klikkaa",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "Kaareva nuoli", "curvedArrow": "Kaareva nuoli",
"curvedLine": "Kaareva viiva", "curvedLine": "Kaareva viiva",
"documentation": "Käyttöohjeet", "documentation": "Käyttöohjeet",
+22 -6
View File
@@ -64,6 +64,7 @@
"cartoonist": "Caricaturiste", "cartoonist": "Caricaturiste",
"fileTitle": "Nom du fichier", "fileTitle": "Nom du fichier",
"colorPicker": "Sélecteur de couleur", "colorPicker": "Sélecteur de couleur",
"canvasColors": "",
"canvasBackground": "Arrière-plan du canevas", "canvasBackground": "Arrière-plan du canevas",
"drawingCanvas": "Zone de dessin", "drawingCanvas": "Zone de dessin",
"layers": "Calques", "layers": "Calques",
@@ -102,7 +103,15 @@
"showBackground": "Afficher le sélecteur de couleur d'arrière-plan", "showBackground": "Afficher le sélecteur de couleur d'arrière-plan",
"toggleTheme": "Changer le thème", "toggleTheme": "Changer le thème",
"personalLib": "Bibliothèque personnelle", "personalLib": "Bibliothèque personnelle",
"excalidrawLib": "Bibliothèque Excalidraw" "excalidrawLib": "Bibliothèque Excalidraw",
"decreaseFontSize": "Réduire 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": { "buttons": {
"clearReset": "Réinitialiser le canevas", "clearReset": "Réinitialiser le canevas",
@@ -171,7 +180,8 @@
"imageInsertError": "Impossible d'insérer l'image. Réessayez plus tard...", "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}}.", "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.", "svgImageInsertError": "Impossible d'insérer l'image SVG. Le balisage SVG semble invalide.",
"invalidSVGString": "SVG invalide." "invalidSVGString": "SVG invalide.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Sélection", "selection": "Sélection",
@@ -184,7 +194,9 @@
"freedraw": "Dessiner", "freedraw": "Dessiner",
"text": "Texte", "text": "Texte",
"library": "Bibliothèque", "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": { "headings": {
"canvasActions": "Actions du canevas", "canvasActions": "Actions du canevas",
@@ -204,10 +216,12 @@
"resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre", "resizeImage": "Vous pouvez redimensionner librement en maintenant SHIFT,\nmaintenez ALT pour redimensionner depuis le centre",
"rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation", "rotate": "Vous pouvez restreindre les angles en maintenant MAJ pendant la rotation",
"lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points", "lineEditor_info": "Double-cliquez ou appuyez sur Entrée pour éditer les points",
"lineEditor_pointSelected": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le déplacer", "lineEditor_pointSelected": "Appuyer sur Suppr. pour supprimer des points, Ctrl ou Cmd+D pour dupliquer, ou faire glisser pour déplacer",
"lineEditor_nothingSelected": "Sélectionnez un point à déplacer ou supprimer, ou maintenez Alt et cliquez pour ajouter de nouveaux points", "lineEditor_nothingSelected": "Sélectionner un point pour éditer (maintenir la touche MAJ pour en sélectionner plusieurs),\nou maintenir la touche Alt enfoncée et cliquer pour ajouter de nouveaux points",
"placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement", "placeImage": "Cliquez pour placer l'image, ou cliquez et faites glisser pour définir sa taille manuellement",
"publishLibrary": "Publier votre propre bibliothèque" "publishLibrary": "Publier votre propre bibliothèque",
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
"deepBoxSelect": "Maintenir CtrlOuCmd pour sélectionner dans les groupes, et empêcher le déplacement"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossible d’afficher l’aperçu", "cannotShowPreview": "Impossible d’afficher l’aperçu",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Lire notre blog", "blog": "Lire notre blog",
"click": "clic", "click": "clic",
"deepSelect": "Sélection dans les groupes",
"deepBoxSelect": "Sélectionner dans les groupes, et empêcher le déplacement",
"curvedArrow": "Flèche courbée", "curvedArrow": "Flèche courbée",
"curvedLine": "Ligne courbée", "curvedLine": "Ligne courbée",
"documentation": "Documentation", "documentation": "Documentation",
+22 -6
View File
@@ -64,6 +64,7 @@
"cartoonist": "קריקטוריסט", "cartoonist": "קריקטוריסט",
"fileTitle": "שם קובץ", "fileTitle": "שם קובץ",
"colorPicker": "בחירת צבע", "colorPicker": "בחירת צבע",
"canvasColors": "",
"canvasBackground": "רקע הלוח", "canvasBackground": "רקע הלוח",
"drawingCanvas": "לוח ציור", "drawingCanvas": "לוח ציור",
"layers": "שכבות", "layers": "שכבות",
@@ -102,7 +103,15 @@
"showBackground": "הצג צבעי רקע", "showBackground": "הצג צבעי רקע",
"toggleTheme": "שינוי ערכת העיצוב", "toggleTheme": "שינוי ערכת העיצוב",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "",
"increaseFontSize": "",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "אפס את הלוח", "clearReset": "אפס את הלוח",
@@ -171,7 +180,8 @@
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "בחירה", "selection": "בחירה",
@@ -184,7 +194,9 @@
"freedraw": "צייר", "freedraw": "צייר",
"text": "טקסט", "text": "טקסט",
"library": "ספריה", "library": "ספריה",
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור" "lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "פעולות הלוח", "canvasActions": "פעולות הלוח",
@@ -204,10 +216,12 @@
"resizeImage": "", "resizeImage": "",
"rotate": "ניתן להגביל זוויות על ידי לחיצה על SHIFT תוך כדי סיבוב", "rotate": "ניתן להגביל זוויות על ידי לחיצה על SHIFT תוך כדי סיבוב",
"lineEditor_info": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות", "lineEditor_info": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות",
"lineEditor_pointSelected": "לחץ על Delete להסרת נקודה, CtrlOrCmd+D לשכפל, או גרור להזזה", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "בחר נקודה להזזה או הסרה, או החזק את כפתור Alt והקלק להוספת נקודות חדשות", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה", "cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "קרא את הבלוג שלנו", "blog": "קרא את הבלוג שלנו",
"click": "קליק", "click": "קליק",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "חץ מעוגל", "curvedArrow": "חץ מעוגל",
"curvedLine": "קו מעוגל", "curvedLine": "קו מעוגל",
"documentation": "תיעוד", "documentation": "תיעוד",
+32 -16
View File
@@ -35,11 +35,11 @@
"arrowhead_arrow": "तीर", "arrowhead_arrow": "तीर",
"arrowhead_bar": "बार", "arrowhead_bar": "बार",
"arrowhead_dot": "बिंदु", "arrowhead_dot": "बिंदु",
"arrowhead_triangle": "", "arrowhead_triangle": "त्रिकोण",
"fontSize": "फ़ॉन्ट का आकार", "fontSize": "फ़ॉन्ट का आकार",
"fontFamily": "फ़ॉन्ट का परिवार", "fontFamily": "फ़ॉन्ट का परिवार",
"onlySelected": "केवल चयनित", "onlySelected": "केवल चयनित",
"withBackground": "", "withBackground": "पृष्ठभूमि",
"exportEmbedScene": "", "exportEmbedScene": "",
"exportEmbedScene_details": "निर्यात एम्बेड दृश्य विवरण", "exportEmbedScene_details": "निर्यात एम्बेड दृश्य विवरण",
"addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"", "addWatermark": "ऐड \"मेड विथ एक्सकैलिडराव\"",
@@ -64,6 +64,7 @@
"cartoonist": "व्यंग्य चित्रकार", "cartoonist": "व्यंग्य चित्रकार",
"fileTitle": "फ़ाइल का नाम", "fileTitle": "फ़ाइल का नाम",
"colorPicker": "रंग चयन", "colorPicker": "रंग चयन",
"canvasColors": "",
"canvasBackground": "कैनवास बैकग्राउंड", "canvasBackground": "कैनवास बैकग्राउंड",
"drawingCanvas": "कैनवास बना रहे हैं", "drawingCanvas": "कैनवास बना रहे हैं",
"layers": "परतें", "layers": "परतें",
@@ -93,16 +94,24 @@
"centerHorizontally": "क्षैतिज केन्द्रित", "centerHorizontally": "क्षैतिज केन्द्रित",
"distributeHorizontally": "क्षैतिज रूप से वितरित करें", "distributeHorizontally": "क्षैतिज रूप से वितरित करें",
"distributeVertically": "खड़ी रूप से वितरित करें", "distributeVertically": "खड़ी रूप से वितरित करें",
"flipHorizontal": "", "flipHorizontal": "दायें बायें पलटे",
"flipVertical": "", "flipVertical": "ऊपर नीचे पलटे",
"viewMode": "", "viewMode": "अलग अलग देखें",
"toggleExportColorScheme": "", "toggleExportColorScheme": "",
"share": "", "share": "शेयर करें",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "", "toggleTheme": "",
"personalLib": "", "personalLib": "",
"excalidrawLib": "" "excalidrawLib": "",
"decreaseFontSize": "आकार घटाइऐ",
"increaseFontSize": "फ़ॉन्ट आकार बढ़ाएँ",
"unbindText": "",
"link": {
"edit": "",
"create": "",
"label": ""
}
}, },
"buttons": { "buttons": {
"clearReset": "कैनवास रीसेट करें", "clearReset": "कैनवास रीसेट करें",
@@ -171,7 +180,8 @@
"imageInsertError": "", "imageInsertError": "",
"fileTooBig": "", "fileTooBig": "",
"svgImageInsertError": "", "svgImageInsertError": "",
"invalidSVGString": "" "invalidSVGString": "",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "चयन", "selection": "चयन",
@@ -184,7 +194,9 @@
"freedraw": "", "freedraw": "",
"text": "पाठ", "text": "पाठ",
"library": "लाइब्रेरी", "library": "लाइब्रेरी",
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें" "lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें",
"penMode": "",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "कैनवास क्रिया", "canvasActions": "कैनवास क्रिया",
@@ -204,10 +216,12 @@
"resizeImage": "", "resizeImage": "",
"rotate": "आप घूर्णन करते समय SHIFT पकड़कर कोणों को विवश कर सकते हैं", "rotate": "आप घूर्णन करते समय SHIFT पकड़कर कोणों को विवश कर सकते हैं",
"lineEditor_info": "बिंदुओं को संपादित करने के लिए Enter पर डबल-क्लिक करें या दबाएँ", "lineEditor_info": "बिंदुओं को संपादित करने के लिए Enter पर डबल-क्लिक करें या दबाएँ",
"lineEditor_pointSelected": "बिंदु हटाने के लिए डिलीट दबाएं, प्रतिरूपित करने के लिए कण्ट्रोल या कमांड डी दबाएं या स्थानांतरित करने के लिए खींचे", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "स्थानांतरित करने या हटाने के लिए एक बिंदु का चयन करें, या Alt दबाए रखें और नए बिंदुओं को जोड़ने के लिए क्लिक करें", "lineEditor_nothingSelected": "",
"placeImage": "", "placeImage": "",
"publishLibrary": "" "publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं", "cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "हमारा ब्लॉग पढे", "blog": "हमारा ब्लॉग पढे",
"click": "क्लिक करें", "click": "क्लिक करें",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "वक्र तीर", "curvedArrow": "वक्र तीर",
"curvedLine": "वक्र रेखा", "curvedLine": "वक्र रेखा",
"documentation": "", "documentation": "",
@@ -394,11 +410,11 @@
"5f3dc4": "", "5f3dc4": "",
"364fc7": "", "364fc7": "",
"1864ab": "", "1864ab": "",
"0b7285": "", "0b7285": "आसमानी",
"087f5b": "", "087f5b": "",
"2b8a3e": "", "2b8a3e": "हरा",
"5c940d": "", "5c940d": "",
"e67700": "", "e67700": "पीला",
"d9480f": "" "d9480f": "नारंगी"
} }
} }
+187 -171
View File
@@ -35,12 +35,12 @@
"arrowhead_arrow": "Nyíl", "arrowhead_arrow": "Nyíl",
"arrowhead_bar": "Oszlop", "arrowhead_bar": "Oszlop",
"arrowhead_dot": "Pont", "arrowhead_dot": "Pont",
"arrowhead_triangle": "", "arrowhead_triangle": "Háromszög",
"fontSize": "Betűméret", "fontSize": "Betűméret",
"fontFamily": "Betűkészlet család", "fontFamily": "Betűkészlet család",
"onlySelected": "Csak a kijelölt", "onlySelected": "Csak a kijelölt",
"withBackground": "", "withBackground": "Háttér",
"exportEmbedScene": "", "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.", "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\"", "addWatermark": "Add hozzá, hogy \"Excalidraw-val készült\"",
"handDrawn": "Kézzel rajzolt", "handDrawn": "Kézzel rajzolt",
@@ -62,14 +62,15 @@
"architect": "Tervezői", "architect": "Tervezői",
"artist": "Művészi", "artist": "Művészi",
"cartoonist": "Karikatúrás", "cartoonist": "Karikatúrás",
"fileTitle": "", "fileTitle": "Fájlnév",
"colorPicker": "Színválasztó", "colorPicker": "Színválasztó",
"canvasColors": "Rajzvászonon használt",
"canvasBackground": "Vászon háttérszíne", "canvasBackground": "Vászon háttérszíne",
"drawingCanvas": "Rajzvászon", "drawingCanvas": "Rajzvászon",
"layers": "Rétegek", "layers": "Rétegek",
"actions": "Műveletek", "actions": "Műveletek",
"language": "Nyelv", "language": "Nyelv",
"liveCollaboration": "", "liveCollaboration": "Élő együttműködés",
"duplicateSelection": "Duplikálás", "duplicateSelection": "Duplikálás",
"untitled": "Névtelen", "untitled": "Névtelen",
"name": "Név", "name": "Név",
@@ -78,7 +79,7 @@
"group": "Csoportosítás", "group": "Csoportosítás",
"ungroup": "Csoportbontás", "ungroup": "Csoportbontás",
"collaborators": "Közreműködők", "collaborators": "Közreműködők",
"showGrid": "", "showGrid": "Rács megjelenítése",
"addToLibrary": "Hozzáadás a könyvtárhoz", "addToLibrary": "Hozzáadás a könyvtárhoz",
"removeFromLibrary": "Eltávólítás a könyvtárból", "removeFromLibrary": "Eltávólítás a könyvtárból",
"libraryLoadingMessage": "Könyvtár betöltése…", "libraryLoadingMessage": "Könyvtár betöltése…",
@@ -93,28 +94,36 @@
"centerHorizontally": "Vízszintesen középre igazított", "centerHorizontally": "Vízszintesen középre igazított",
"distributeHorizontally": "Vízszintes elosztás", "distributeHorizontally": "Vízszintes elosztás",
"distributeVertically": "Függőleges elosztás", "distributeVertically": "Függőleges elosztás",
"flipHorizontal": "", "flipHorizontal": "Vízszintes tükrözés",
"flipVertical": "", "flipVertical": "Függőleges tükrözés",
"viewMode": "", "viewMode": "Nézet",
"toggleExportColorScheme": "", "toggleExportColorScheme": "Exportált színséma váltása",
"share": "", "share": "Megosztás",
"showStroke": "", "showStroke": "Körvonal színválasztó megjelenítése",
"showBackground": "", "showBackground": "Háttérszín-választó megjelenítése",
"toggleTheme": "", "toggleTheme": "Téma váltása",
"personalLib": "", "personalLib": "Személyes könyvtár",
"excalidrawLib": "" "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": { "buttons": {
"clearReset": "Vászon törlése", "clearReset": "Vászon törlése",
"exportJSON": "", "exportJSON": "Exportálás fájlba",
"exportImage": "", "exportImage": "Mentés képként",
"export": "Exportálás", "export": "Exportálás",
"exportToPng": "Exportálás PNG-be", "exportToPng": "Exportálás PNG-be",
"exportToSvg": "Exportálás SVG-be", "exportToSvg": "Exportálás SVG-be",
"copyToClipboard": "Vágólapra másolás", "copyToClipboard": "Vágólapra másolás",
"copyPngToClipboard": "PNG másolása a vágólapra", "copyPngToClipboard": "PNG másolása a vágólapra",
"scale": "Nagyítás", "scale": "Nagyítás",
"save": "", "save": "Mentés az aktuális fájlba",
"saveAs": "Mentés másként", "saveAs": "Mentés másként",
"load": "Betöltés", "load": "Betöltés",
"getShareableLink": "Megosztható link létrehozása", "getShareableLink": "Megosztható link létrehozása",
@@ -129,19 +138,19 @@
"edit": "Szerkesztés", "edit": "Szerkesztés",
"undo": "Vissza", "undo": "Vissza",
"redo": "Újra", "redo": "Újra",
"resetLibrary": "", "resetLibrary": "Könyvtár alaphelyzetbe állítása",
"createNewRoom": "Új szoba létrehozása", "createNewRoom": "Új szoba létrehozása",
"fullScreen": "Teljes képernyő", "fullScreen": "Teljes képernyő",
"darkMode": "Sötét mód", "darkMode": "Sötét mód",
"lightMode": "Világos mód", "lightMode": "Világos mód",
"zenMode": "Letisztult mód", "zenMode": "Letisztult mód",
"exitZenMode": "Kilépés a letisztult módból", "exitZenMode": "Kilépés a letisztult módból",
"cancel": "", "cancel": "Mégsem",
"clear": "", "clear": "Kiűrítés",
"remove": "", "remove": "Eltávolítás",
"publishLibrary": "", "publishLibrary": "Közzététel",
"submit": "", "submit": "Elküldés",
"confirm": "" "confirm": "Megerősítés"
}, },
"alerts": { "alerts": {
"clearReset": "Ez a művelet törli a vászont. Biztos benne?", "clearReset": "Ez a művelet törli a vászont. Biztos benne?",
@@ -154,37 +163,40 @@
"decryptFailed": "Nem sikerült visszafejteni a titkosított adatot.", "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.", "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?", "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.", "errorLoadingLibrary": "Hibába ütközött a harmarmadik féltől származó könyvtár betöltése.",
"errorAddingToLibrary": "", "errorAddingToLibrary": "A tétel nem addható hozzá a könyvtárhoz",
"errorRemovingFromLibrary": "", "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?", "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", "cannotRestoreFromImage": "A jelenet visszaállítása nem sikerült ebből a kép fájlból",
"invalidSceneUrl": "", "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": "", "resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
"removeItemsFromsLibrary": "", "removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
"invalidEncryptionKey": "" "invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
}, },
"errors": { "errors": {
"unsupportedFileType": "", "unsupportedFileType": "Nem támogatott fájltípus.",
"imageInsertError": "", "imageInsertError": "Nem sikerült beszúrni a képet. Próbáld újra később...",
"fileTooBig": "", "fileTooBig": "A fájl túl nagy. A megengedett maximális méret {{maxSize}}.",
"svgImageInsertError": "", "svgImageInsertError": "Nem sikerült beszúrni az SVG-képet. Az SVG szintaktika érvénytelennek tűnik.",
"invalidSVGString": "" "invalidSVGString": "Érvénytelen SVG.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Kijelölés", "selection": "Kijelölés",
"image": "", "image": "Kép beszúrása",
"rectangle": "Téglalap", "rectangle": "Téglalap",
"diamond": "Rombusz", "diamond": "Rombusz",
"ellipse": "Ellipszis", "ellipse": "Ellipszis",
"arrow": "Nyíl", "arrow": "Nyíl",
"line": "Vonal", "line": "Vonal",
"freedraw": "", "freedraw": "Rajzolás",
"text": "Szöveg", "text": "Szöveg",
"library": "Könyvtár", "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": { "headings": {
"canvasActions": "Vászon műveletek", "canvasActions": "Vászon műveletek",
@@ -192,22 +204,24 @@
"shapes": "Alakzatok" "shapes": "Alakzatok"
}, },
"hints": { "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", "linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél", "freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre", "text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
"text_selected": "", "text_selected": "Kattints duplán, vagy nyomj entert a szöveg szerkesztéséhez",
"text_editing": "", "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", "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", "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", "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", "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_info": "Kattints duplán, vagy nyomj entert a pontok szerkesztéséhez",
"lineEditor_pointSelected": "Nyomd meg a delete gombot a pont eltávolításához, Ctrl vagy Cmd + D-t a duplikáláshoz, vagy húzva mozgasd", "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álassz ki egy pontot a mozgatáshoz vagy törtléshez, vagy az Alt lenyomása mellett kattintva hozz létre új pontokat", "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": "", "placeImage": "Kattints a kép elhelyezéséhez, vagy kattints és méretezd manuálisan",
"publishLibrary": "" "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": { "canvasError": {
"cannotShowPreview": "Előnézet nem jeleníthető meg", "cannotShowPreview": "Előnézet nem jeleníthető meg",
@@ -235,99 +249,101 @@
"desc_inProgressIntro": "Az élő együttműködési munkamenet folyamatban van.", "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_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.", "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": { "errorDialog": {
"title": "Hiba" "title": "Hiba"
}, },
"exportDialog": { "exportDialog": {
"disk_title": "", "disk_title": "Mentés lemezre",
"disk_details": "", "disk_details": "Exportálja a jelenetadatokat egy fájlba, amelyből később importálhatja.",
"disk_button": "", "disk_button": "Mentés fájlba",
"link_title": "", "link_title": "Megosztható hivatkozás",
"link_details": "", "link_details": "Exportálás csak olvasható hivatkozásként.",
"link_button": "", "link_button": "Exportálás hivatkozásba",
"excalidrawplus_description": "", "excalidrawplus_description": "Mentse el a jelenetet az Excalidraw+ munkaterületére.",
"excalidrawplus_button": "", "excalidrawplus_button": "Exportálás",
"excalidrawplus_exportError": "" "excalidrawplus_exportError": "Jelenleg nem lehet exportálni az Excalidraw+-ba..."
}, },
"helpDialog": { "helpDialog": {
"blog": "", "blog": "Olvasd a blogunkat",
"click": "", "click": "kattintás",
"curvedArrow": "", "deepSelect": "Mély kijelölés",
"curvedLine": "", "deepBoxSelect": "Mély kijelölés a dobozon belül, és a húzás megakadályozása",
"documentation": "", "curvedArrow": "Ívelt nyíl",
"doubleClick": "", "curvedLine": "Ívelt vonal",
"drag": "", "documentation": "Dokumentáció",
"editor": "", "doubleClick": "dupla kattintás",
"editSelectedShape": "", "drag": "vonszolás",
"github": "", "editor": "Szerkesztő",
"howto": "", "editSelectedShape": "Kijelölt alakzat szerkesztése (szöveg/nyíl/vonal)",
"or": "", "github": "Hibát találtál? Küld be",
"preventBinding": "", "howto": "Kövesd az útmutatóinkat",
"shapes": "", "or": "vagy",
"shortcuts": "", "preventBinding": "A nyíl ne ragadjon",
"textFinish": "", "shapes": "Alakzatok",
"textNewLine": "", "shortcuts": "Gyorsbillentyűk",
"title": "", "textFinish": "Szerkesztés befejezése (szöveg)",
"view": "", "textNewLine": "Új sor hozzáadása (szöveg)",
"zoomToFit": "", "title": "Segítség",
"zoomToSelection": "" "view": "Nézet",
"zoomToFit": "Az összes elem látótérbe hozása",
"zoomToSelection": "Kijelölésre nagyítás"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": "Rajzvászon alaphelyzetbe"
}, },
"publishDialog": { "publishDialog": {
"title": "", "title": "Könyvtár közzététele",
"itemName": "", "itemName": "Tétel neve",
"authorName": "", "authorName": "Szerző neve",
"githubUsername": "", "githubUsername": "GitHub felhasználónév",
"twitterUsername": "", "twitterUsername": "Twitter felhasználónév",
"libraryName": "", "libraryName": "Könyvtár neve",
"libraryDesc": "", "libraryDesc": "Könyvtár leírása",
"website": "", "website": "Weboldal",
"placeholder": { "placeholder": {
"authorName": "", "authorName": "Neved vagy felhasználóneved",
"libraryName": "", "libraryName": "A könyvtárad neve",
"libraryDesc": "", "libraryDesc": "A könyvtárad használatát segítő leírás",
"githubHandle": "", "githubHandle": "GitHub-handle(opcionális), így szerkesztheted a könyvtárat, miután elküldted ellenőrzésre",
"twitterHandle": "", "twitterHandle": "Twitter-felhasználónév (opcionális), így tudjuk, kinek kell jóváírni a Twitteren keresztüli reklámozást",
"website": "" "website": "Hivatkozás személyes webhelyedre vagy máshová (nem kötelező)"
}, },
"errors": { "errors": {
"required": "", "required": "Kötelező",
"website": "" "website": "Adj meg egy érvényes URL-t"
}, },
"noteDescription": { "noteDescription": {
"pre": "", "pre": "Küld be könyvtáradat, hogy bekerüljön a ",
"link": "", "link": "nyilvános könyvtár tárolóba",
"post": "" "post": "hogy mások is felhasználhassák a rajzaikban."
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "", "pre": "A könyvtárat először manuálisan kell jóváhagyni. Kérjük, olvassa el a ",
"link": "", "link": "segédletet",
"post": "" "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": { "noteLicense": {
"pre": "", "pre": "A beküldéssel elfogadja, hogy a könyvtár a következő alatt kerül közzétételre ",
"link": "", "link": "MIT Licensz ",
"post": "" "post": "ami röviden azt jelenti, hogy bárki korlátozás nélkül használhatja őket."
}, },
"noteItems": "", "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": "" "atleastOneLibItem": "A kezdéshez válassz ki legalább egy könyvtári elemet"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "A könyvtár beküldve",
"content": "", "content": "Köszönjük {{authorName}}. Könyvtáradat elküldtük felülvizsgálatra. Nyomon követheted az állapotot",
"link": "" "link": "itt"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "Könyvtár alaphelyzetbe állítása",
"removeItemsFromLib": "" "removeItemsFromLib": "A kiválasztott elemek eltávolítása a könyvtárból"
}, },
"encrypted": { "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.", "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": { "stats": {
"angle": "Szög", "angle": "Szög",
@@ -339,66 +355,66 @@
"storage": "Tárhely", "storage": "Tárhely",
"title": "Statisztikák", "title": "Statisztikák",
"total": "Összesen", "total": "Összesen",
"version": "", "version": "Verzió",
"versionCopy": "", "versionCopy": "Kattints a másoláshoz",
"versionNotAvailable": "", "versionNotAvailable": "A verzió nem elérhető",
"width": "Szélesség" "width": "Szélesség"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "Könyvtárhoz adva",
"copyStyles": "", "copyStyles": "Másolt stílusok.",
"copyToClipboard": "", "copyToClipboard": "Vágólapra másolva.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "Az {{exportSelection}} PNG formátumban a vágólapra másolva \n({{exportColorScheme}})",
"fileSaved": "", "fileSaved": "Fájl elmentve.",
"fileSavedToFilename": "", "fileSavedToFilename": "Mentve mint {filename}",
"canvas": "", "canvas": "rajzvászon",
"selection": "" "selection": "kijelölés"
}, },
"colors": { "colors": {
"ffffff": "", "ffffff": "Fehér",
"f8f9fa": "", "f8f9fa": "Szürke 0",
"f1f3f5": "", "f1f3f5": "Szürke 1",
"fff5f5": "", "fff5f5": "Piros 0",
"fff0f6": "", "fff0f6": "Pink 0",
"f8f0fc": "", "f8f0fc": "Szőlő 0",
"f3f0ff": "", "f3f0ff": "Ibolya 0",
"edf2ff": "", "edf2ff": "Indigó 0",
"e7f5ff": "", "e7f5ff": "Kék 0",
"e3fafc": "", "e3fafc": "Cián 0",
"e6fcf5": "", "e6fcf5": "Kékes-zöld 0",
"ebfbee": "", "ebfbee": "Zöld 0",
"f4fce3": "", "f4fce3": "Lime 0",
"fff9db": "", "fff9db": "Sárga 0",
"fff4e6": "", "fff4e6": "Narancs 0",
"transparent": "", "transparent": "Átlátszó",
"ced4da": "", "ced4da": "Szürke 4",
"868e96": "", "868e96": "Szürke 6",
"fa5252": "", "fa5252": "Piros 6",
"e64980": "", "e64980": "Pink 6",
"be4bdb": "", "be4bdb": "Szőlő 6",
"7950f2": "", "7950f2": "Ibolya 6",
"4c6ef5": "", "4c6ef5": "Indigó 6",
"228be6": "", "228be6": "Kék 6",
"15aabf": "", "15aabf": "Cián 6",
"12b886": "", "12b886": "Kékes-zöld 6",
"40c057": "", "40c057": "Zöld 6",
"82c91e": "", "82c91e": "Lime 6",
"fab005": "", "fab005": "Sárga 6",
"fd7e14": "", "fd7e14": "Narancs 6",
"000000": "", "000000": "Fekete",
"343a40": "", "343a40": "Szürke 8",
"495057": "", "495057": "Szürke 7",
"c92a2a": "", "c92a2a": "Piros 9",
"a61e4d": "", "a61e4d": "Pink 9",
"862e9c": "", "862e9c": "Szőlő 9",
"5f3dc4": "", "5f3dc4": "Ibolya 9",
"364fc7": "", "364fc7": "Indigó 9",
"1864ab": "", "1864ab": "Kék 9",
"0b7285": "", "0b7285": "Cián 9",
"087f5b": "", "087f5b": "Kékes-zöld 9",
"2b8a3e": "", "2b8a3e": "Zöld 9",
"5c940d": "", "5c940d": "Lime 9",
"e67700": "", "e67700": "Sárga 9",
"d9480f": "" "d9480f": "Narancs 9"
} }
} }
+21 -5
View File
@@ -64,6 +64,7 @@
"cartoonist": "Kartunis", "cartoonist": "Kartunis",
"fileTitle": "Nama file", "fileTitle": "Nama file",
"colorPicker": "Pilihan Warna", "colorPicker": "Pilihan Warna",
"canvasColors": "Digunakan di kanvas",
"canvasBackground": "Latar Kanvas", "canvasBackground": "Latar Kanvas",
"drawingCanvas": "Kanvas", "drawingCanvas": "Kanvas",
"layers": "Lapisan", "layers": "Lapisan",
@@ -102,7 +103,15 @@
"showBackground": "Tampilkan latar pengambil warna", "showBackground": "Tampilkan latar pengambil warna",
"toggleTheme": "Ubah tema", "toggleTheme": "Ubah tema",
"personalLib": "Pustaka Pribadi", "personalLib": "Pustaka Pribadi",
"excalidrawLib": "Pustaka Excalidraw" "excalidrawLib": "Pustaka Excalidraw",
"decreaseFontSize": "Kecilkan ukuran font",
"increaseFontSize": "Besarkan ukuran font",
"unbindText": "Lepas teks",
"link": {
"edit": "Edit tautan",
"create": "Buat tautan",
"label": "Tautan"
}
}, },
"buttons": { "buttons": {
"clearReset": "Setel Ulang Kanvas", "clearReset": "Setel Ulang Kanvas",
@@ -171,7 +180,8 @@
"imageInsertError": "Tidak dapat menyisipkan gambar. Coba lagi nanti...", "imageInsertError": "Tidak dapat menyisipkan gambar. Coba lagi nanti...",
"fileTooBig": "File terlalu besar. Ukuran maksimum yang dibolehkan {{maxSize}}.", "fileTooBig": "File terlalu besar. Ukuran maksimum yang dibolehkan {{maxSize}}.",
"svgImageInsertError": "Tidak dapat menyisipkan gambar SVG. Markup SVG sepertinya tidak valid.", "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": { "toolBar": {
"selection": "Pilihan", "selection": "Pilihan",
@@ -184,7 +194,9 @@
"freedraw": "Gambar", "freedraw": "Gambar",
"text": "Teks", "text": "Teks",
"library": "Pustaka", "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": { "headings": {
"canvasActions": "Opsi Kanvas", "canvasActions": "Opsi Kanvas",
@@ -205,9 +217,11 @@
"rotate": "Anda dapat menjaga sudut dengan menahan SHIFT sambil memutar", "rotate": "Anda dapat menjaga sudut dengan menahan SHIFT sambil memutar",
"lineEditor_info": "Klik ganda atau tekan Enter untuk mengedit titik", "lineEditor_info": "Klik ganda atau tekan Enter untuk mengedit titik",
"lineEditor_pointSelected": "Tekan Delete untuk menghapus titik, Ctrl/Cmd + D untuk menduplikasi, atau seret untuk memindahkan", "lineEditor_pointSelected": "Tekan Delete untuk menghapus titik, Ctrl/Cmd + D untuk menduplikasi, atau seret untuk memindahkan",
"lineEditor_nothingSelected": "Pilih sebuah titik untuk memindah atau menghapus, atau tekan Alt dan klik untuk menambahkan titik baru", "lineEditor_nothingSelected": "Pilih titik untuk mengedit (tekan SHIFT untuk pilih banyak), atau tekan Alt dan klik untuk tambahkan titik baru",
"placeImage": "Klik untuk tempatkan gambar, atau klik dan jatuhkan untuk tetapkan ukuran secara manual", "placeImage": "Klik untuk tempatkan gambar, atau klik dan jatuhkan untuk tetapkan ukuran secara manual",
"publishLibrary": "Terbitkan pustaka Anda" "publishLibrary": "Terbitkan pustaka Anda",
"bindTextToElement": "Tekan enter untuk tambahkan teks",
"deepBoxSelect": "Tekan Ctrl atau Cmd untuk memilih yang di dalam, dan mencegah penggeseran"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Tidak dapat menampilkan pratinjau", "cannotShowPreview": "Tidak dapat menampilkan pratinjau",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Baca blog kami", "blog": "Baca blog kami",
"click": "klik", "click": "klik",
"deepSelect": "Pilih dalam",
"deepBoxSelect": "Pilih dalam kotak, dan cegah penggeseran",
"curvedArrow": "Panah lengkung", "curvedArrow": "Panah lengkung",
"curvedLine": "Garis lengkung", "curvedLine": "Garis lengkung",
"documentation": "Dokumentasi", "documentation": "Dokumentasi",
+22 -6
View File
@@ -64,6 +64,7 @@
"cartoonist": "Fumettista", "cartoonist": "Fumettista",
"fileTitle": "Nome del file", "fileTitle": "Nome del file",
"colorPicker": "Selettore colore", "colorPicker": "Selettore colore",
"canvasColors": "Usato su tela",
"canvasBackground": "Sfondo tela", "canvasBackground": "Sfondo tela",
"drawingCanvas": "Area di disegno", "drawingCanvas": "Area di disegno",
"layers": "Livelli", "layers": "Livelli",
@@ -102,7 +103,15 @@
"showBackground": "Mostra selettore colore di sfondo", "showBackground": "Mostra selettore colore di sfondo",
"toggleTheme": "Cambia tema", "toggleTheme": "Cambia tema",
"personalLib": "Libreria Personale", "personalLib": "Libreria Personale",
"excalidrawLib": "Libreria di Excalidraw" "excalidrawLib": "Libreria di Excalidraw",
"decreaseFontSize": "Riduci dimensione dei caratteri",
"increaseFontSize": "Aumenta la dimensione dei caratteri",
"unbindText": "Scollega testo",
"link": {
"edit": "Modifica link",
"create": "Crea link",
"label": "Link"
}
}, },
"buttons": { "buttons": {
"clearReset": "Svuota la tela", "clearReset": "Svuota la tela",
@@ -171,7 +180,8 @@
"imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...", "imageInsertError": "Non è stato possibile inserire l'immagine. Riprova più tardi...",
"fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.", "fileTooBig": "Il file è troppo grande. La dimensione massima consentita è {{maxSize}}.",
"svgImageInsertError": "Impossibile inserire l'immagine SVG. Il markup SVG non sembra corretto.", "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": { "toolBar": {
"selection": "Selezione", "selection": "Selezione",
@@ -184,7 +194,9 @@
"freedraw": "Disegno", "freedraw": "Disegno",
"text": "Testo", "text": "Testo",
"library": "Libreria", "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": { "headings": {
"canvasActions": "Azioni sulla Tela", "canvasActions": "Azioni sulla Tela",
@@ -204,10 +216,12 @@
"resizeImage": "Puoi ridimensionare liberamente tenendo premuto SHIFT,\ntieni premuto ALT per ridimensionare dal centro", "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", "rotate": "Puoi mantenere gli angoli tenendo premuto SHIFT durante la rotazione",
"lineEditor_info": "Fai doppio click o premi invio per modificare i punti", "lineEditor_info": "Fai doppio click o premi invio per modificare i punti",
"lineEditor_pointSelected": "Premere Elimina per rimuovere il punto, CtrlOrCmd+D per duplicare o trascinare per spostare", "lineEditor_pointSelected": "Premi Elimina per rimuovere il punto(i),\nCtrlOCmd+D per duplicare o trascinare per spostare",
"lineEditor_nothingSelected": "Seleziona un punto per spostare o rimuovere, oppure tieni premuto Alt e fai clic per aggiungere nuovi punti", "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", "placeImage": "Fai click per posizionare l'immagine, o click e trascina per impostarne la dimensione manualmente",
"publishLibrary": "Pubblica la tua libreria" "publishLibrary": "Pubblica la tua libreria",
"bindTextToElement": "Premi invio per aggiungere il testo",
"deepBoxSelect": "Tieni premuto CtrlOCmd per selezionare in profondità e per impedire il trascinamento"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossibile visualizzare l'anteprima", "cannotShowPreview": "Impossibile visualizzare l'anteprima",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Leggi il nostro blog", "blog": "Leggi il nostro blog",
"click": "click", "click": "click",
"deepSelect": "Selezione profonda",
"deepBoxSelect": "Seleziona in profondità all'interno della casella e previene il trascinamento",
"curvedArrow": "Freccia curva", "curvedArrow": "Freccia curva",
"curvedLine": "Linea curva", "curvedLine": "Linea curva",
"documentation": "Documentazione", "documentation": "Documentazione",
+23 -7
View File
@@ -64,6 +64,7 @@
"cartoonist": "漫画風", "cartoonist": "漫画風",
"fileTitle": "ファイル名", "fileTitle": "ファイル名",
"colorPicker": "色選択", "colorPicker": "色選択",
"canvasColors": "",
"canvasBackground": "キャンバスの背景", "canvasBackground": "キャンバスの背景",
"drawingCanvas": "キャンバスの描画", "drawingCanvas": "キャンバスの描画",
"layers": "レイヤー", "layers": "レイヤー",
@@ -102,7 +103,15 @@
"showBackground": "背景色ピッカーを表示", "showBackground": "背景色ピッカーを表示",
"toggleTheme": "テーマの切り替え", "toggleTheme": "テーマの切り替え",
"personalLib": "個人ライブラリ", "personalLib": "個人ライブラリ",
"excalidrawLib": "Excalidrawライブラリ" "excalidrawLib": "Excalidrawライブラリ",
"decreaseFontSize": "フォントサイズを縮小",
"increaseFontSize": "フォントサイズを拡大",
"unbindText": "",
"link": {
"edit": "リンクを編集",
"create": "リンクを作成",
"label": "リンク"
}
}, },
"buttons": { "buttons": {
"clearReset": "キャンバスのリセット", "clearReset": "キャンバスのリセット",
@@ -171,7 +180,8 @@
"imageInsertError": "画像を挿入できませんでした。後でもう一度お試しください...", "imageInsertError": "画像を挿入できませんでした。後でもう一度お試しください...",
"fileTooBig": "ファイルが大きすぎます。許可される最大サイズは {{maxSize}} です。", "fileTooBig": "ファイルが大きすぎます。許可される最大サイズは {{maxSize}} です。",
"svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。", "svgImageInsertError": "SVGイメージを挿入できませんでした。SVGマークアップは無効に見えます。",
"invalidSVGString": "無効なSVGです。" "invalidSVGString": "無効なSVGです。",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "選択", "selection": "選択",
@@ -184,7 +194,9 @@
"freedraw": "描画", "freedraw": "描画",
"text": "テキスト", "text": "テキスト",
"library": "ライブラリ", "library": "ライブラリ",
"lock": "描画後も使用中のツールを選択したままにする" "lock": "描画後も使用中のツールを選択したままにする",
"penMode": "ピンチとズームを抑止し、ペンからのみ自由な入力を受け付けます",
"link": ""
}, },
"headings": { "headings": {
"canvasActions": "キャンバス操作", "canvasActions": "キャンバス操作",
@@ -204,10 +216,12 @@
"resizeImage": "SHIFTを長押しすると自由にサイズを変更できます。\n中央からサイズを変更するにはALTを長押しします", "resizeImage": "SHIFTを長押しすると自由にサイズを変更できます。\n中央からサイズを変更するにはALTを長押しします",
"rotate": "回転中にSHIFT キーを押すと角度を制限することができます", "rotate": "回転中にSHIFT キーを押すと角度を制限することができます",
"lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します", "lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します",
"lineEditor_pointSelected": "削除ボタンを押して点を削除します。Ctrl+D または Cmd+D で複製します。またはドラッグして移動します", "lineEditor_pointSelected": "Deleteキーを押すと点を削除、CtrlOrCmd+Dで複製、マウスドラッグで移動",
"lineEditor_nothingSelected": "移動または削除する点を選択するか、Altキーを押しながらクリックして新しい点を追加します", "lineEditor_nothingSelected": "編集する点を選択(SHIFTを押したままで複数選択)、\nAltキーを押しながらクリックすると新しい点を追加",
"placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します", "placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します",
"publishLibrary": "自分のライブラリを公開" "publishLibrary": "自分のライブラリを公開",
"bindTextToElement": "Enterを押してテキストを追加",
"deepBoxSelect": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "プレビューを表示できません", "cannotShowPreview": "プレビューを表示できません",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "公式ブログを読む", "blog": "公式ブログを読む",
"click": "クリック", "click": "クリック",
"deepSelect": "",
"deepBoxSelect": "",
"curvedArrow": "カーブした矢印", "curvedArrow": "カーブした矢印",
"curvedLine": "曲線", "curvedLine": "曲線",
"documentation": "ドキュメント", "documentation": "ドキュメント",
@@ -292,7 +308,7 @@
"libraryDesc": "ライブラリの使い方を理解するための説明", "libraryDesc": "ライブラリの使い方を理解するための説明",
"githubHandle": "GitHubハンドル(任意)。一度レビューのために送信されると、ライブラリを編集できます", "githubHandle": "GitHubハンドル(任意)。一度レビューのために送信されると、ライブラリを編集できます",
"twitterHandle": "Twitterのユーザー名 (任意)。Twitterでプロモーションする際にクレジットする人を知っておくためのものです", "twitterHandle": "Twitterのユーザー名 (任意)。Twitterでプロモーションする際にクレジットする人を知っておくためのものです",
"website": "個人のウェブサイトまたは他のサイトへのリンク (オプション)" "website": "個人のウェブサイトまたは他のサイトへのリンク (任意)"
}, },
"errors": { "errors": {
"required": "必須項目", "required": "必須項目",
+68 -52
View File
@@ -35,7 +35,7 @@
"arrowhead_arrow": "Taneccabt", "arrowhead_arrow": "Taneccabt",
"arrowhead_bar": "Afeggag", "arrowhead_bar": "Afeggag",
"arrowhead_dot": "Tanqiḍt", "arrowhead_dot": "Tanqiḍt",
"arrowhead_triangle": "", "arrowhead_triangle": "Akerdis",
"fontSize": "Tiddi n tsefsit", "fontSize": "Tiddi n tsefsit",
"fontFamily": "Tawacult n tsefsiyin", "fontFamily": "Tawacult n tsefsiyin",
"onlySelected": "Tafrayt kan", "onlySelected": "Tafrayt kan",
@@ -64,6 +64,7 @@
"cartoonist": "", "cartoonist": "",
"fileTitle": "Isem n ufaylu", "fileTitle": "Isem n ufaylu",
"colorPicker": "Amafran n yini", "colorPicker": "Amafran n yini",
"canvasColors": "",
"canvasBackground": "Agilal n teɣzut n usuneɣ", "canvasBackground": "Agilal n teɣzut n usuneɣ",
"drawingCanvas": "Taɣzut n usuneɣ", "drawingCanvas": "Taɣzut n usuneɣ",
"layers": "Tissiyin", "layers": "Tissiyin",
@@ -101,8 +102,16 @@
"showStroke": "Beqqeḍ amelqaḍ n yini n yizirig", "showStroke": "Beqqeḍ amelqaḍ n yini n yizirig",
"showBackground": "Beqqeḍ amelqaḍ n yini n ugilal", "showBackground": "Beqqeḍ amelqaḍ n yini n ugilal",
"toggleTheme": "Snifel asentel", "toggleTheme": "Snifel asentel",
"personalLib": "", "personalLib": "Tamkarḍit tudmawant",
"excalidrawLib": "" "excalidrawLib": "Tamkarḍit n Excalidraw",
"decreaseFontSize": "Senqes tiddi n tsefsit",
"increaseFontSize": "Sali tiddi n tsefsit",
"unbindText": "",
"link": {
"edit": "Ẓreg aseɣwen",
"create": "Snulfu-d aseɣwen",
"label": "Aseɣwen"
}
}, },
"buttons": { "buttons": {
"clearReset": "Ales awennez n teɣzut n usuneɣ", "clearReset": "Ales awennez n teɣzut n usuneɣ",
@@ -138,10 +147,10 @@
"exitZenMode": "Ffeɣ seg uskar Zen", "exitZenMode": "Ffeɣ seg uskar Zen",
"cancel": "Sefsex", "cancel": "Sefsex",
"clear": "Sfeḍ", "clear": "Sfeḍ",
"remove": "", "remove": "Kkes",
"publishLibrary": "", "publishLibrary": "Ẓreg",
"submit": "", "submit": "Azen",
"confirm": "" "confirm": "Sentem"
}, },
"alerts": { "alerts": {
"clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?", "clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?",
@@ -163,7 +172,7 @@
"cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna", "cannotRestoreFromImage": "Asayes ulamek ara d-yettwarr seg ufaylu-agi n tugna",
"invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.", "invalidSceneUrl": "Ulamek taktert n usayes seg URL i d-ittunefken. Ahat mačči d tameɣtut neɣ ur tegbir ara isefka JSON n Excalidraw.",
"resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?", "resetLibrary": "Ayagi ad isfeḍ tamkarḍit-inek•m. Tetḥeqqeḍ?",
"removeItemsFromsLibrary": "", "removeItemsFromsLibrary": "Ad tekkseḍ {{count}} n uferdis (en) si temkarḍit?",
"invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa." "invalidEncryptionKey": "Tasarut n uwgelhen isefk ad tesɛu 22 n yiekkilen. Amɛiwen srid yensa."
}, },
"errors": { "errors": {
@@ -171,7 +180,8 @@
"imageInsertError": "D awezɣi tugra n tugna. Eɛreḍ tikkelt-nniḍen ardeqqal...", "imageInsertError": "D awezɣi tugra n tugna. Eɛreḍ tikkelt-nniḍen ardeqqal...",
"fileTooBig": "Afaylu meqqer aṭas. Tiddi tafellayt yurgen d {{maxSize}}.", "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.", "svgImageInsertError": "D awezɣi tugra n tugna SVG. Acraḍ SVG yettban-d d armeɣtu.",
"invalidSVGString": "" "invalidSVGString": "SVG armeɣtu.",
"cannotResolveCollabServer": ""
}, },
"toolBar": { "toolBar": {
"selection": "Tafrayt", "selection": "Tafrayt",
@@ -184,7 +194,9 @@
"freedraw": "Suneɣ", "freedraw": "Suneɣ",
"text": "Aḍris", "text": "Aḍris",
"library": "Tamkarḍit", "library": "Tamkarḍit",
"lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ" "lock": "Eǧǧ afecku n tefrayt yermed mbaɛd asuneɣ",
"penMode": "",
"link": "Rnu/leqqem aseɣwen i talɣa yettwafernen"
}, },
"headings": { "headings": {
"canvasActions": "Tigawin n teɣzut n usuneɣ", "canvasActions": "Tigawin n teɣzut n usuneɣ",
@@ -192,7 +204,7 @@
"shapes": "Talɣiwin" "shapes": "Talɣiwin"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "Akken ad tesmuttiḍ taɣzut n usuneɣ, ṭṭef ṛṛuda n umumed, neɣ afeggag n tallunt mi ara tzuɣreḍ",
"linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig", "linearElement": "Ssit akken ad tebduḍ aṭas n tenqiḍin, zuɣer i yiwen n yizirig",
"freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ", "freeDraw": "Ssit yerna zuɣer, serreḥ ticki tfukeḍ",
"text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt", "text": "Tixidest: tzemreḍ daɣen ad ternuḍ aḍris s usiti snat n tikkal anida tebɣiḍ s ufecku n tefrayt",
@@ -204,10 +216,12 @@
"resizeImage": "Tzemreḍ ad talseḍ tiddi s tilelli s tuṭṭfa n SHIFT,\nṭṭef ALT akken ad talseḍ tiddi si tlemmast", "resizeImage": "Tzemreḍ ad talseḍ tiddi s tilelli s tuṭṭfa n SHIFT,\nṭṭef ALT akken ad talseḍ tiddi si tlemmast",
"rotate": "Tzemreḍ ad tḥettemeḍ tiɣemmar s tuṭṭfa n SHIFT di tuzzya", "rotate": "Tzemreḍ ad tḥettemeḍ tiɣemmar s tuṭṭfa n SHIFT di tuzzya",
"lineEditor_info": "Ssit snat n tikkal neɣ ssed taqeffalt Kcem akken ad tẓergeḍ tinqiḍin", "lineEditor_info": "Ssit snat n tikkal neɣ ssed taqeffalt Kcem akken ad tẓergeḍ tinqiḍin",
"lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍt, CtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ", "lineEditor_pointSelected": "Ssed taqeffalt kkes akken ad tekkseḍ tanqiḍ (tinqiḍin),\nCtrlOrCmd+D akken ad tsiselgeḍ, neɣ zuɣer akken ad tesmuttiḍ",
"lineEditor_nothingSelected": "Fren tanqiḍt ara tesmuttiḍ neɣ ara tekkseḍ, neɣ ṭṭef taqeffalt Alt akken ad ternuḍ tinqiḍin timaynutin", "lineEditor_nothingSelected": "Fren tanqiḍt akken ad tẓergeḍ (ṭṭef SHIFT akken ad tferneḍ aṭas),\nneɣ ṭṭef Alt akken ad ternuḍ tinqiḍin timaynutin",
"placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus", "placeImage": "Ssit akken ad tserseḍ tugna, neɣ ssit u zuɣer akken ad tesbaduḍ tiddi-ines s ufus",
"publishLibrary": "" "publishLibrary": "Siẓreg tamkarḍit-inek•inem",
"bindTextToElement": "Ssed ɣef kcem akken ad ternuḍ aḍris",
"deepBoxSelect": "Ṭṭef CtrlOrCmd akken ad tferneḍ s telqey, yerna ad trewleḍ i uzuɣer"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Ulamek abeqqeḍ n teskant", "cannotShowPreview": "Ulamek abeqqeḍ n teskant",
@@ -254,6 +268,8 @@
"helpDialog": { "helpDialog": {
"blog": "Ɣeṛ ablug-nneɣ", "blog": "Ɣeṛ ablug-nneɣ",
"click": "ssit", "click": "ssit",
"deepSelect": "Afran s telqey",
"deepBoxSelect": "Afran s telqey s tnaka, yerna ad tyrewleḍ i uzuɣer",
"curvedArrow": "Taneccabt izelgen", "curvedArrow": "Taneccabt izelgen",
"curvedLine": "Izirig izelgen", "curvedLine": "Izirig izelgen",
"documentation": "Tasemlit", "documentation": "Tasemlit",
@@ -275,55 +291,55 @@
"zoomToSelection": "Simɣur ɣer tefrayt" "zoomToSelection": "Simɣur ɣer tefrayt"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": "Sfeḍ taɣzut n usuneɣ"
}, },
"publishDialog": { "publishDialog": {
"title": "", "title": "Suffeɣ-d tamkarḍit",
"itemName": "", "itemName": "Isem n uferdis",
"authorName": "", "authorName": "Isem n umeskar",
"githubUsername": "", "githubUsername": "Isem n useqdac n GitHub",
"twitterUsername": "", "twitterUsername": "Isem n useqdac n Twitter",
"libraryName": "", "libraryName": "Isem n temkarḍit",
"libraryDesc": "", "libraryDesc": "Aglam n temkarḍit",
"website": "", "website": "Asmel n web",
"placeholder": { "placeholder": {
"authorName": "", "authorName": "Isem neɣ isem n useqdac inek•inem",
"libraryName": "", "libraryName": "Isem n temkarḍit-inek•inem",
"libraryDesc": "", "libraryDesc": "Aglam n temkarḍit-inek•inem akken ad tɛiwneḍ medden ad fehmen aseqdec-inec",
"githubHandle": "", "githubHandle": "Isem n useqdac n GitHub ( d anefrunan) akken ad tizmireḍ ad tisẓrigeḍ tamkarḍit ticki tuzneḍ-tt i uselken",
"twitterHandle": "", "twitterHandle": "Isem n useqdac n Twitter (d anefrunan) akken ad nẓer anwa ara nsenmer deg udellel di Twitter",
"website": "" "website": "Aseɣwen ɣer usmel-inek•inem neɣ wayeḍ (d anefrunan)"
}, },
"errors": { "errors": {
"required": "", "required": "Yettwasra",
"website": "" "website": "Sekcem URL ameɣtu"
}, },
"noteDescription": { "noteDescription": {
"pre": "", "pre": "Azen tamkarḍit-inek•inem akken ad teddu di ",
"link": "", "link": "akaram azayez n temkarḍit",
"post": "" "post": "i yimdanen-nniḍen ara isqedcen deg wunuɣen-nnsen."
}, },
"noteGuidelines": { "noteGuidelines": {
"pre": "", "pre": "Tamkarḍit teḥwaǧ ad tettwaqbel s ufus qbel. Ma ulac uɣilif ɣer ",
"link": "", "link": "",
"post": "" "post": " send ad tazneḍ. Tesriḍ amiḍan n GitHub akken ad tmmeslayeḍ yerna ad tgeḍ ibeddilen ma yelaq, maca mačči d ayen yettwaḥetmen."
}, },
"noteLicense": { "noteLicense": {
"pre": "", "pre": "Mi tuzneḍ ad tqebleḍ akken tamkarḍit ad d-teffeɣ s ",
"link": "", "link": "Turagt MIT, ",
"post": "" "post": "ayen yebɣan ad d-yini belli yal yiwen izmer ad ten-iseqdec war tilist."
}, },
"noteItems": "", "noteItems": "Yal aferdis n temkarḍit isefk ad isɛu isem-is i yiman-is akken ad yili wamek ara yettusizdeg. Iferdisen-agi n temkarḍit ad ddun:",
"atleastOneLibItem": "" "atleastOneLibItem": "Ma ulac uɣilif fern ma drus yiwen n uferdis n temkarḍit akken ad tebduḍ"
}, },
"publishSuccessDialog": { "publishSuccessDialog": {
"title": "", "title": "Tamkarḍit tettwazen",
"content": "", "content": "Tanemmirt-ik•im {{authorName}}. Tamkarḍit-inek•inem tettwazen i weselken. Tzemreḍ ad tḍefreḍ aẓayer",
"link": "" "link": "dagi"
}, },
"confirmDialog": { "confirmDialog": {
"resetLibrary": "", "resetLibrary": "Ales awennez n temkarḍit",
"removeItemsFromLib": "" "removeItemsFromLib": "Kkes iferdisen yettafernen si temkarḍit"
}, },
"encrypted": { "encrypted": {
"tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ", "tooltip": "Unuɣen-inek (m) ttuwgelhnen seg yixef s ixef dɣa iqeddacen n Excalidraw werǧin ad ten-walin. ",
@@ -345,7 +361,7 @@
"width": "Tehri" "width": "Tehri"
}, },
"toast": { "toast": {
"addedToLibrary": "", "addedToLibrary": "Yettwarna ɣer temkarḍit",
"copyStyles": "Iɣunab yettwaneɣlen.", "copyStyles": "Iɣunab yettwaneɣlen.",
"copyToClipboard": "Yettwaɣel ɣer tecfawit.", "copyToClipboard": "Yettwaɣel ɣer tecfawit.",
"copyToClipboardAsPng": "{{exportSelection}} yettwanɣel ɣer tecfawit am PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} yettwanɣel ɣer tecfawit am PNG\n({{exportColorScheme}})",
@@ -360,14 +376,14 @@
"f1f3f5": "Aɣiɣdi 1", "f1f3f5": "Aɣiɣdi 1",
"fff5f5": "Azeggaɣ", "fff5f5": "Azeggaɣ",
"fff0f6": "Axuxi 0", "fff0f6": "Axuxi 0",
"f8f0fc": "", "f8f0fc": "Tiẓurin 0",
"f3f0ff": "Amidadi 0", "f3f0ff": "Amidadi 0",
"edf2ff": "", "edf2ff": "",
"e7f5ff": "Anili 0", "e7f5ff": "Anili 0",
"e3fafc": "", "e3fafc": "",
"e6fcf5": "", "e6fcf5": "",
"ebfbee": "Azegzaw 0", "ebfbee": "Azegzaw 0",
"f4fce3": "", "f4fce3": "Llim 0",
"fff9db": "Awraɣ 0", "fff9db": "Awraɣ 0",
"fff4e6": "Aččinawi 0", "fff4e6": "Aččinawi 0",
"transparent": "Afrawan", "transparent": "Afrawan",
@@ -382,7 +398,7 @@
"15aabf": "", "15aabf": "",
"12b886": "", "12b886": "",
"40c057": "Azegzaw 0", "40c057": "Azegzaw 0",
"82c91e": "", "82c91e": "Llim 6",
"fab005": "Awraɣ 6", "fab005": "Awraɣ 6",
"fd7e14": "Aččinawi 6", "fd7e14": "Aččinawi 6",
"000000": "Aberkan", "000000": "Aberkan",
@@ -390,14 +406,14 @@
"495057": "Aɣiɣdi 7", "495057": "Aɣiɣdi 7",
"c92a2a": "Azeggaɣ 9", "c92a2a": "Azeggaɣ 9",
"a61e4d": "Axuxi 9", "a61e4d": "Axuxi 9",
"862e9c": "", "862e9c": "Tiẓurin 9",
"5f3dc4": "Amidadi 9", "5f3dc4": "Amidadi 9",
"364fc7": "", "364fc7": "",
"1864ab": "Anili 9", "1864ab": "Anili 9",
"0b7285": "", "0b7285": "",
"087f5b": "", "087f5b": "",
"2b8a3e": "Azegzaw 9", "2b8a3e": "Azegzaw 9",
"5c940d": "", "5c940d": "Llim 9",
"e67700": "Awraɣ 9", "e67700": "Awraɣ 9",
"d9480f": "Aččinawi 9" "d9480f": "Aččinawi 9"
} }

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