Compare commits

...

55 Commits

Author SHA1 Message Date
dwelle 3c83a322b6 feat: support image background editor [wip] 2021-12-20 21:50:16 +01:00
Aakansha Doshi 8b5657e1ce fix: support updating stroke color for text by typing in color picker input (#4415)
* fix: support updating stroke color for text by typing in color picker input

* restore focus when clicked on same property unless its color picker input and submit text on color picker blur

* focus editor on color picker blur

* don't focus text editor when color picker is active
2021-12-17 20:15:22 +05:30
Aakansha Doshi 8b2b03347c fix: bound text not atomic with container when changing z-index (#4414)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-12-17 13:10:37 +00:00
Aakansha Doshi c2a8712593 fix: update viewport coords correctly when editing text (#4416) 2021-12-17 11:38:18 +01:00
Aakansha Doshi ff1d7728a0 fix: use word-break break-word only and update text editor height only when binded to container (#4410) 2021-12-17 15:24:23 +05:30
Aakansha Doshi 98b5c37e45 feat: bind text to shapes when pressing enter and support sticky notes 🎉 (#4343)
* feat: Word wrap inside rect and increase height when size exceeded

* fixes for auto increase in height

* fix height

* respect newlines when wrapping text

* shift text area when height increases beyond mid rect height until it reaches to the top

* select bound text if present when rect selected

* mutate y coord after text submit

* Add padding of 30px and update dimensions acordingly

* Don't allow selecting bound text element directly

* support deletion of bound text element when rect deleted

* trim text

* Support autoshrink and improve algo

* calculate approx line height instead of hardcoding

* use textContainerId instead of storing textContainer element itself

* rename boundTextElement -> boundTextElementId

* fix text properties not getting reflected after edit inside rect

* Support resizing

* remove ts ignore

* increase height of container when text height increases while resizing

* use original text when editing/resizing so it adjusts based on original text

* fix tests

* add util isRectangleElement

* use isTextElement util everywhere

* disable selecting text inside rect when selectAll

* Bind text to circle and diamond as well

* fix tests

* vertically center align the text always

* better vertical align

* Disable binding arrows for text inside shapes

* set min width for text container when text is binded to container

* update dimensions of container if its less than min width/ min height

* Allow selecting of text container for transparent containers when clicked inside

* fix test

* preserve whitespaces between long word exceeding width and next word
Use word break instead of whitespace no wrap for better readability and support safari

* Perf improvements for measuring text width and resizing
* Use canvas measureText instead of our algo. This has reduced the perf ~ 10 times
* Rewrite wrapText algo to break in words appropriately and for longer words
calculate the char width in order unless max width reached. This makes the
the number of runs linear (max text length times) which was earlier
textLength * textLength-1/2 as I was slicing the chars from end until max width reached for each run
* Add a util to calculate getApproxCharsToFitInWidth to calculate min chars to fit in a line

* use console.info so eslint doesnt warn :p

* cache char width and don't call resize unless min width exceeded

* update line height and height correctly when text properties inside container updated

* improve vertical centering when text properties updated, not yet perfect though

* when double clicked inside a conatiner  take the cursor to end of text same as what happens when enter is pressed

* Add hint when container selected

* Select container when escape key is pressed after submitting text

* fix copy/paste when using copy/paste action

* fix copy when dragged with alt pressed

* fix export to svg/png

* fix add to library

* Fix copy as png/svg

* Don't allow selecting text when using selection tool and support resizing when multiple elements include ones with binded text selectec

* fix rotation jump

* moove all text utils to textElement.ts

* resize text element only after container resized so that width doesnt change when editing

* insert the remaining chars for long words once it goes beyond line

* fix typo, use string for character type

* renaming

* fix bugs in word wrap algo

* make grouping work

* set boundTextElementId only when text present else unset it

* rename textContainerId to containerId

* fix

* fix snap

* use originalText in redrawTextBoundingBox so height is calculated properly and center align works after props updated

* use boundElementIds and also support binding text in images 🎉

* fix the sw/se ends when resizing from ne/nw

* fix y coord when resizing from north

* bind when enter is pressed, double click/text tool willl edit the binded text if present else create a new text

* bind when clicked on center of container

* use pre-wrap instead of normal so it works in ff

* use container boundTextElement when container present and trying to edit text

* review fixes

* make getBoundTextElementId type safe and check for existence when using this function

* fix

* don't duplicate boundElementIds when text submitted

* only remove last trailing space if present which we have added when joining words

* set width correctly when resizing to fix alignment issues

* make duplication work using cmd/ctrl+d

* set X coord correctly during resize

* don't allow resize to negative dimensions when text is bounded to container

* fix, check last char is space

* remove logs

* make sure text editor doesn't go beyond viewport and set container dimensions in case it overflows

* add a util isTextBindableContainer to check if the container could bind text
2021-12-16 21:14:03 +05:30
David Luzar 7db63bd397 feat: redesign toolbar & tweaks (#4387) 2021-12-15 15:31:44 +01:00
David Luzar 390da3fd0f feat: change boundElementIdsboundElements (#4404) 2021-12-14 16:07:01 +01:00
David Luzar 104664cb9e feat: support selecting multiple points when editing line (#4373) 2021-12-13 13:35:07 +01:00
dependabot[bot] c822055ec8 chore(deps-dev): bump sass-loader in /src/packages/utils (#4390)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.3.0 to 12.4.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.3.0...v12.4.0)

---
updated-dependencies:
- dependency-name: sass-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>
2021-12-13 11:42:29 +05:30
dependabot[bot] e15d73d94c chore(deps-dev): bump typescript in /src/packages/excalidraw (#4392)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.5.2...v4.5.3)

---
updated-dependencies:
- dependency-name: typescript
  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>
2021-12-13 11:42:18 +05:30
dependabot[bot] 80ee097b85 chore(deps-dev): bump webpack in /src/packages/utils (#4391)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.4 to 5.65.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.4...v5.65.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>
2021-12-13 11:42:03 +05:30
dependabot[bot] 10048b877b chore(deps-dev): bump webpack in /src/packages/excalidraw (#4389)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.4 to 5.65.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.4...v5.65.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>
2021-12-13 10:41:00 +05:30
dependabot[bot] 5dd5862bb9 chore(deps-dev): bump sass-loader in /src/packages/excalidraw (#4393)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.3.0 to 12.4.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.3.0...v12.4.0)

---
updated-dependencies:
- dependency-name: sass-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>
2021-12-13 10:40:41 +05:30
Preet 79989fedda chore: bump roughjs version (#4386)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-12-11 22:46:44 +01:00
Jai Kumar Dewani cecabc2196 fix: husky not able to execute pre-commit on windows (#4370) 2021-12-09 15:15:54 +01:00
David Luzar ed8fb40b63 fix: make firebase config parsing not fail on undefined env (#4381) 2021-12-09 12:24:41 +00:00
Aakansha Doshi 6e391728fe build: remove file loader and migrate to asset modules webpack for font assets (#4380)
* build: use type:javascript/auto so font file assets aren't duplicated

* update changelog

* remove file loader and use asset modules

* fix
2021-12-08 15:56:25 +05:30
David Luzar dfbfbc3f11 feat: set package build target to es2017 (#4341) 2021-12-07 16:38:46 +01:00
dwelle 9b8ee3cacf feat: horizontally center toolbar menu 2021-12-05 17:47:19 +01:00
Jai Kumar Dewani 4ea73d5d5b feat: Add support for rounded corners in diamond (#4369) 2021-12-05 16:56:19 +01:00
David Luzar 618f204ddd feat: allow zooming up to 3000% (#4358) 2021-12-04 13:51:28 +00:00
David Luzar 720588130c feat: stop discarding precision when rendering (#4357) 2021-12-04 13:49:57 +00:00
zsviczian f354788cd0 fix: adding to library via contextmenu when no image is selected (#4356) 2021-12-04 11:59:37 +01:00
zsviczian 1c7ee09010 feat: support Image binding (#4347) 2021-12-03 11:42:28 +01:00
dependabot[bot] ca15b0a008 chore(deps-dev): bump postcss-loader in /src/packages/excalidraw (#4329)
Bumps [postcss-loader](https://github.com/webpack-contrib/postcss-loader) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/webpack-contrib/postcss-loader/releases)
- [Changelog](https://github.com/webpack-contrib/postcss-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/postcss-loader/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: postcss-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>
2021-12-02 16:10:06 +02:00
dependabot[bot] 650930c5ce chore(deps-dev): bump webpack in /src/packages/utils (#4328)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.3 to 5.64.4.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.3...v5.64.4)

---
updated-dependencies:
- dependency-name: webpack
  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>
2021-12-02 16:09:53 +02:00
dependabot[bot] 79c0d59244 chore(deps-dev): bump webpack in /src/packages/excalidraw (#4327)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.3 to 5.64.4.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.3...v5.64.4)

---
updated-dependencies:
- dependency-name: webpack
  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>
2021-12-02 16:09:44 +02:00
dependabot[bot] cd50b5f7e9 chore(deps): bump @types/react from 17.0.35 to 17.0.37 (#4332)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.35 to 17.0.37.
- [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>
2021-12-02 16:09:20 +02:00
dependabot[bot] c0434957ff chore(deps): bump @testing-library/jest-dom from 5.15.0 to 5.15.1 (#4333)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.15.0 to 5.15.1.
- [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.15.0...v5.15.1)

---
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>
2021-12-02 16:08:59 +02:00
dependabot[bot] 66aeaeb38d chore(deps): bump @tldraw/vec from 0.1.3 to 1.1.5 (#4334)
Bumps [@tldraw/vec](https://github.com/tldraw/tldraw) from 0.1.3 to 1.1.5.
- [Release notes](https://github.com/tldraw/tldraw/releases)
- [Commits](https://github.com/tldraw/tldraw/compare/v0.1.3...v1.1.5)

---
updated-dependencies:
- dependency-name: "@tldraw/vec"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 16:08:30 +02:00
dependabot[bot] 7f545e74ab chore(deps): bump sass from 1.43.4 to 1.43.5 (#4330)
Bumps [sass](https://github.com/sass/dart-sass) from 1.43.4 to 1.43.5.
- [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.43.4...1.43.5)

---
updated-dependencies:
- dependency-name: sass
  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>
2021-12-02 15:12:01 +02:00
dependabot[bot] a776955579 chore(deps-dev): bump prettier from 2.4.1 to 2.5.0 (#4331)
Bumps [prettier](https://github.com/prettier/prettier) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.4.1...2.5.0)

---
updated-dependencies:
- dependency-name: prettier
  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>
2021-12-02 15:11:17 +02:00
David Luzar afa7932c9b feat: set appState.exportBackground to true when exporting to jpg (#4342) 2021-11-30 22:08:55 +01:00
Thomas Steiner 1ee8d7d082 chore(deps): Bump browser-fs-access to latest version (#4338)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-11-29 22:23:12 +01:00
David Luzar 06db702b5d feat: support selecting multiple library items via shift (#4306) 2021-11-26 12:46:23 +01:00
David Luzar b53d1f6f3e feat: improve library preview image generation on publish (#4321)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-11-26 11:46:13 +01:00
Excalidraw Bot ca1f3aa094 chore: Update translations from Crowdin (#4258) 2021-11-26 11:27:28 +01:00
David Luzar 8ff159e76e fix: export scale quality regression (#4316) 2021-11-25 14:05:22 +01:00
David Luzar f9d2d537a2 feat: add element.updated (#4070) 2021-11-24 18:38:33 +01:00
dependabot[bot] dac970c640 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#4291)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.0 to 7.16.4.
- [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.4/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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>
2021-11-24 16:55:14 +00:00
dependabot[bot] 78bb3b3d84 chore(deps-dev): bump @babel/plugin-transform-runtime (#4292)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.16.0 to 7.16.4.
- [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.4/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>
2021-11-24 18:54:33 +02:00
dependabot[bot] 7d9d7ad297 chore(deps-dev): bump @babel/plugin-transform-runtime (#4288)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.16.0 to 7.16.4.
- [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.4/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>
2021-11-24 18:54:13 +02:00
dependabot[bot] de20a5e3ba chore(deps-dev): bump webpack in /src/packages/excalidraw (#4314)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.0 to 5.64.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.0...v5.64.3)

---
updated-dependencies:
- dependency-name: webpack
  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>
2021-11-24 18:52:51 +02:00
dependabot[bot] 289f72e45d chore(deps-dev): bump webpack in /src/packages/utils (#4315)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.0 to 5.64.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.0...v5.64.3)

---
updated-dependencies:
- dependency-name: webpack
  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>
2021-11-24 18:52:36 +02:00
David Luzar 6dd0e6a4c5 fix: remove 100% height from tooltip container to fix layout issues (#3980) 2021-11-24 17:16:18 +01:00
David Luzar 96b31ecbce fix: inline ENV variables when building excalidraw package (#4311) 2021-11-24 16:25:19 +01:00
dependabot[bot] a132f154cb chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#4286)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.0 to 7.16.4.
- [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.4/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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>
2021-11-24 17:22:57 +02:00
dependabot[bot] 23acd8f6d1 chore(deps-dev): bump mini-css-extract-plugin (#4289)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.4.4 to 2.4.5.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.4.4...v2.4.5)

---
updated-dependencies:
- dependency-name: mini-css-extract-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>
2021-11-24 17:22:34 +02:00
dependabot[bot] a60709f5ea chore(deps-dev): bump lint-staged from 12.0.1 to 12.1.2 (#4312)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.0.1 to 12.1.2.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.0.1...v12.1.2)

---
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>
2021-11-24 17:21:06 +02:00
David Luzar 896c476716 feat: compress shareLink data when uploading to json server (#4225) 2021-11-24 14:45:13 +01:00
dependabot[bot] 133ba19919 chore(deps): bump @types/jest from 27.0.2 to 27.0.3 (#4295)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 27.0.2 to 27.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  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>
2021-11-24 15:40:49 +02:00
dependabot[bot] a2136bfe9d chore(deps): bump @types/react from 17.0.34 to 17.0.35 (#4297)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.34 to 17.0.35.
- [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>
2021-11-24 15:39:32 +02:00
dependabot[bot] 6fbd64fdaa chore(deps-dev): bump firebase-tools from 9.22.0 to 9.23.0 (#4293)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.22.0 to 9.23.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.22.0...v9.23.0)

---
updated-dependencies:
- dependency-name: firebase-tools
  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>
2021-11-24 15:39:16 +02:00
David Luzar cc4b0c2932 feat: supply version param when installing libraries (#4305) 2021-11-23 17:59:26 +01:00
155 changed files with 7289 additions and 2084 deletions
View File
+1
View File
@@ -1 +1,2 @@
#!/bin/sh
yarn lint-staged yarn lint-staged
+11 -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.15.0", "@testing-library/jest-dom": "5.15.1",
"@testing-library/react": "12.1.2", "@testing-library/react": "12.1.2",
"@tldraw/vec": "0.1.3", "@tldraw/vec": "1.1.5",
"@types/jest": "27.0.2", "@types/jest": "27.0.3",
"@types/pica": "5.1.3", "@types/pica": "5.1.3",
"@types/react": "17.0.34", "@types/react": "17.0.37",
"@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.21.1", "browser-fs-access": "0.23.0",
"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",
@@ -49,8 +49,8 @@
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"roughjs": "4.5.0", "roughjs": "4.5.2",
"sass": "1.43.4", "sass": "1.43.5",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"typescript": "4.5.2" "typescript": "4.5.2"
}, },
@@ -62,14 +62,15 @@
"@types/pako": "1.0.2", "@types/pako": "1.0.2",
"@types/resize-observer-browser": "0.1.6", "@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4", "chai": "4.3.4",
"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.22.0", "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.0.1", "lint-staged": "12.1.2",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"prettier": "2.4.1", "prettier": "2.5.0",
"rewire": "5.0.0" "rewire": "5.0.0"
}, },
"resolutions": { "resolutions": {
+7 -5
View File
@@ -8,7 +8,12 @@ import { t } from "../i18n";
export const actionAddToLibrary = register({ export const actionAddToLibrary = register({
name: "addToLibrary", name: "addToLibrary",
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
if (elements.some((element) => element.type === "image")) { const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
true,
);
if (selectedElements.some((element) => element.type === "image")) {
return { return {
commitToHistory: false, commitToHistory: false,
appState: { appState: {
@@ -25,10 +30,7 @@ export const actionAddToLibrary = register({
{ {
id: randomId(), id: randomId(),
status: "unpublished", status: "unpublished",
elements: getSelectedElements( elements: selectedElements.map(deepCopyElement),
getNonDeletedElements(elements),
appState,
).map(deepCopyElement),
created: Date.now(), created: Date.now(),
}, },
...items, ...items,
+6 -4
View File
@@ -8,13 +8,13 @@ import {
CenterVerticallyIcon, CenterVerticallyIcon,
} from "../components/icons"; } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { getElementMap, 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 { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const enableActionGroup = ( const enableActionGroup = (
@@ -34,9 +34,11 @@ const alignSelectedElements = (
const updatedElements = alignElements(selectedElements, alignment); const updatedElements = alignElements(selectedElements, alignment);
const updatedElementsMap = getElementMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element); return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
}; };
export const actionAlignTop = register({ export const actionAlignTop = register({
+1 -1
View File
@@ -160,7 +160,7 @@ export const actionResetZoom = register({
}; };
}, },
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData, appState }) => (
<Tooltip label={t("buttons.resetZoom")}> <Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
<ToolButton <ToolButton
type="button" type="button"
className="reset-zoom-button" className="reset-zoom-button"
+2
View File
@@ -42,6 +42,7 @@ export const actionCopyAsSvg = register({
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
try { try {
await exportCanvas( await exportCanvas(
@@ -81,6 +82,7 @@ export const actionCopyAsPng = register({
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
try { try {
await exportCanvas( await exportCanvas(
+22 -12
View File
@@ -11,6 +11,7 @@ import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups"; import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { isBoundToContainer } from "../element/typeChecks";
const deleteSelectedElements = ( const deleteSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -21,6 +22,12 @@ const deleteSelectedElements = (
if (appState.selectedElementIds[el.id]) { if (appState.selectedElementIds[el.id]) {
return newElementWith(el, { isDeleted: true }); return newElementWith(el, { isDeleted: true });
} }
if (
isBoundToContainer(el) &&
appState.selectedElementIds[el.containerId]
) {
return newElementWith(el, { isDeleted: true });
}
return el; return el;
}), }),
appState: { appState: {
@@ -55,7 +62,7 @@ export const actionDeleteSelected = register({
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { const {
elementId, elementId,
activePointIndex, selectedPointsIndices,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
} = appState.editingLinearElement; } = appState.editingLinearElement;
@@ -65,8 +72,7 @@ export const actionDeleteSelected = register({
} }
if ( if (
// case: no point selected → delete whole element // case: no point selected → delete whole element
activePointIndex == null || selectedPointsIndices == null ||
activePointIndex === -1 ||
// case: deleting last remaining point // case: deleting last remaining point
element.points.length < 2 element.points.length < 2
) { ) {
@@ -86,15 +92,17 @@ export const actionDeleteSelected = register({
// We cannot do this inside `movePoint` because it is also called // We cannot do this inside `movePoint` because it is also called
// when deleting the uncommitted point (which hasn't caused any binding) // when deleting the uncommitted point (which hasn't caused any binding)
const binding = { const binding = {
startBindingElement: startBindingElement: selectedPointsIndices?.includes(0)
activePointIndex === 0 ? null : startBindingElement, ? null
endBindingElement: : startBindingElement,
activePointIndex === element.points.length - 1 endBindingElement: selectedPointsIndices?.includes(
? null element.points.length - 1,
: endBindingElement, )
? null
: endBindingElement,
}; };
LinearElementEditor.movePoint(element, activePointIndex, "delete"); LinearElementEditor.deletePoints(element, selectedPointsIndices);
return { return {
elements, elements,
@@ -103,13 +111,15 @@ export const actionDeleteSelected = register({
editingLinearElement: { editingLinearElement: {
...appState.editingLinearElement, ...appState.editingLinearElement,
...binding, ...binding,
activePointIndex: activePointIndex > 0 ? activePointIndex - 1 : 0, selectedPointsIndices:
selectedPointsIndices?.[0] > 0
? [selectedPointsIndices[0] - 1]
: [0],
}, },
}, },
commitToHistory: true, commitToHistory: true,
}; };
} }
let { elements: nextElements, appState: nextAppState } = let { elements: nextElements, appState: nextAppState } =
deleteSelectedElements(elements, appState); deleteSelectedElements(elements, appState);
fixBindingsAfterDeletion( fixBindingsAfterDeletion(
+6 -4
View File
@@ -4,13 +4,13 @@ import {
} from "../components/icons"; } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../disitrubte"; import { distributeElements, Distribution } from "../disitrubte";
import { getElementMap, 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 } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const enableActionGroup = ( const enableActionGroup = (
@@ -30,9 +30,11 @@ const distributeSelectedElements = (
const updatedElements = distributeElements(selectedElements, distribution); const updatedElements = distributeElements(selectedElements, distribution);
const updatedElementsMap = getElementMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element); return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
}; };
export const distributeHorizontally = register({ export const distributeHorizontally = register({
+22 -32
View File
@@ -2,13 +2,12 @@ import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element"; import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { clone } from "../components/icons"; import { clone } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement } from "../element/mutateElement";
import { import {
selectGroupsForSelectedElements, selectGroupsForSelectedElements,
getSelectedGroupForElement, getSelectedGroupForElement,
@@ -18,41 +17,23 @@ import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding"; import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types"; import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants"; import { GRID_SIZE } from "../constants";
import { bindTextToShapeAfterDuplication } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
export const actionDuplicateSelection = register({ export const actionDuplicateSelection = register({
name: "duplicateSelection", name: "duplicateSelection",
perform: (elements, appState) => { perform: (elements, appState) => {
// duplicate point if selected while editing multi-point element // duplicate selected point(s) if editing a line
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { activePointIndex, elementId } = appState.editingLinearElement; const ret = LinearElementEditor.duplicateSelectedPoints(appState);
const element = LinearElementEditor.getElement(elementId);
if (!element || activePointIndex === null) { if (!ret) {
return false; return false;
} }
const { points } = element;
const selectedPoint = points[activePointIndex];
const nextPoint = points[activePointIndex + 1];
mutateElement(element, {
points: [
...points.slice(0, activePointIndex + 1),
nextPoint
? [
(selectedPoint[0] + nextPoint[0]) / 2,
(selectedPoint[1] + nextPoint[1]) / 2,
]
: [selectedPoint[0] + 30, selectedPoint[1] + 30],
...points.slice(activePointIndex + 1),
],
});
return { return {
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
activePointIndex: activePointIndex + 1,
},
},
elements, elements,
appState: ret.appState,
commitToHistory: true, commitToHistory: true,
}; };
} }
@@ -106,9 +87,12 @@ const duplicateElements = (
const finalElements: ExcalidrawElement[] = []; const finalElements: ExcalidrawElement[] = [];
let index = 0; let index = 0;
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, true),
);
while (index < elements.length) { while (index < elements.length) {
const element = elements[index]; const element = elements[index];
if (appState.selectedElementIds[element.id]) { if (selectedElementIds.get(element.id)) {
if (element.groupIds.length) { if (element.groupIds.length) {
const groupId = getSelectedGroupForElement(appState, element); const groupId = getSelectedGroupForElement(appState, element);
// if group selected, duplicate it atomically // if group selected, duplicate it atomically
@@ -130,7 +114,11 @@ const duplicateElements = (
} }
index++; index++;
} }
bindTextToShapeAfterDuplication(
finalElements,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId); fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
return { return {
@@ -140,7 +128,9 @@ const duplicateElements = (
...appState, ...appState,
selectedGroupIds: {}, selectedGroupIds: {},
selectedElementIds: newElements.reduce((acc, element) => { selectedElementIds: newElements.reduce((acc, element) => {
acc[element.id] = true; if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc; return acc;
}, {} as any), }, {} as any),
}, },
+52 -1
View File
@@ -14,10 +14,60 @@ import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
} from "../element/binding"; } from "../element/binding";
import { isBindingElement } from "../element/typeChecks"; import { isBindingElement } from "../element/typeChecks";
import { ExcalidrawImageElement } from "../element/types";
import { imageFromImageData } from "../element/image";
export const actionFinalize = register({ export const actionFinalize = register({
name: "finalize", name: "finalize",
perform: (elements, appState, _, { canvas, focusContainer }) => { perform: (
elements,
appState,
_,
{ canvas, focusContainer, imageCache, addFiles },
) => {
if (appState.editingImageElement) {
const { elementId, imageData } = appState.editingImageElement;
const editingImageElement = elements.find((el) => el.id === elementId) as
| ExcalidrawImageElement
| undefined;
if (editingImageElement?.fileId) {
const cachedImageData = imageCache.get(editingImageElement.fileId);
if (cachedImageData) {
const { image, dataURL } = imageFromImageData(imageData);
imageCache.set(editingImageElement.fileId, {
...cachedImageData,
image,
});
addFiles([
{
id: editingImageElement.fileId,
dataURL,
mimeType: cachedImageData.mimeType,
created: Date.now(),
},
]);
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
}
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } = const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement; appState.editingLinearElement;
@@ -162,6 +212,7 @@ export const actionFinalize = register({
keyTest: (event, appState) => keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE && (event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null || (appState.editingLinearElement !== null ||
appState.editingImageElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) || (!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null), appState.multiElement !== null),
+9 -7
View File
@@ -1,6 +1,6 @@
import { register } from "./register"; import { register } from "./register";
import { getSelectedElements } from "../scene"; import { getSelectedElements } from "../scene";
import { getElementMap, getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements"; import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
@@ -9,6 +9,7 @@ import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks"; import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding"; import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils";
const enableActionFlipHorizontal = ( const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -83,9 +84,11 @@ const flipSelectedElements = (
flipDirection, flipDirection,
); );
const updatedElementsMap = getElementMap(updatedElements); const updatedElementsMap = arrayToMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element); return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
}; };
const flipElements = ( const flipElements = (
@@ -142,10 +145,9 @@ const flipElement = (
} }
if (isLinearElement(element)) { if (isLinearElement(element)) {
for (let i = 1; i < element.points.length; i++) { for (let index = 1; index < element.points.length; index++) {
LinearElementEditor.movePoint(element, i, [ LinearElementEditor.movePoints(element, [
-element.points[i][0], { index, point: [-element.points[index][0], element.points[index][1]] },
element.points[i][1],
]); ]);
} }
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element);
+5 -2
View File
@@ -1,6 +1,6 @@
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { t } from "../i18n"; import { t } from "../i18n";
import { getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons"; import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
@@ -44,6 +44,7 @@ const enableActionGroup = (
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
return ( return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements) selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
@@ -56,6 +57,7 @@ export const actionGroup = register({
const selectedElements = getSelectedElements( const selectedElements = getSelectedElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
true,
); );
if (selectedElements.length < 2) { if (selectedElements.length < 2) {
// nothing to group // nothing to group
@@ -83,8 +85,9 @@ export const actionGroup = register({
} }
} }
const newGroupId = randomId(); const newGroupId = randomId();
const selectElementIds = arrayToMap(selectedElements);
const updatedElements = elements.map((element) => { const updatedElements = elements.map((element) => {
if (!appState.selectedElementIds[element.id]) { if (!selectElementIds.get(element.id)) {
return element; return element;
} }
return newElementWith(element, { return newElementWith(element, {
+5 -5
View File
@@ -6,9 +6,9 @@ import History, { HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { isWindows, KEYS } from "../keys"; import { isWindows, KEYS } from "../keys";
import { getElementMap } from "../element";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { arrayToMap } from "../utils";
const writeData = ( const writeData = (
prevElements: readonly ExcalidrawElement[], prevElements: readonly ExcalidrawElement[],
@@ -27,17 +27,17 @@ const writeData = (
return { commitToHistory }; return { commitToHistory };
} }
const prevElementMap = getElementMap(prevElements); const prevElementMap = arrayToMap(prevElements);
const nextElements = data.elements; const nextElements = data.elements;
const nextElementMap = getElementMap(nextElements); const nextElementMap = arrayToMap(nextElements);
const deletedElements = prevElements.filter( const deletedElements = prevElements.filter(
(prevElement) => !nextElementMap.hasOwnProperty(prevElement.id), (prevElement) => !nextElementMap.has(prevElement.id),
); );
const elements = nextElements const elements = nextElements
.map((nextElement) => .map((nextElement) =>
newElementWith( newElementWith(
prevElementMap[nextElement.id] || nextElement, prevElementMap.get(nextElement.id) || nextElement,
nextElement, nextElement,
), ),
) )
+75
View File
@@ -0,0 +1,75 @@
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
import { backgroundIcon } from "../components/icons";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import { isInitializedImageElement } from "../element/typeChecks";
import Scene from "../scene/Scene";
export const actionEditImageAlpha = register({
name: "editImageAlpha",
perform: async (elements, appState, _, app) => {
if (appState.editingImageElement) {
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
const selectedElements = getSelectedElements(elements, appState);
const selectedElement = selectedElements[0];
if (
selectedElements.length === 1 &&
isInitializedImageElement(selectedElement)
) {
const imgData = app.imageCache.get(selectedElement.fileId);
if (!imgData) {
return false;
}
const image = await imgData.image;
const { width, height } = image;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
Scene.mapElementToScene(selectedElement.id, app.scene);
return {
appState: {
...appState,
editingImageElement: {
editorType: "alpha",
elementId: selectedElement.id,
origImageData: imageData,
imageData,
pointerDownState: { screenX: 0, screenY: 0, sampledPixel: null },
},
},
commitToHistory: false,
};
}
return false;
},
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
icon={backgroundIcon}
label="Edit Image Alpha"
className={appState.editingImageElement ? "active" : ""}
title={"Edit image alpha"}
aria-label={"Edit image alpha"}
onClick={() => updateData(null)}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
/>
),
});
+5 -2
View File
@@ -1,7 +1,7 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups"; import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
export const actionSelectAll = register({ export const actionSelectAll = register({
name: "selectAll", name: "selectAll",
@@ -15,7 +15,10 @@ export const actionSelectAll = register({
...appState, ...appState,
editingGroupId: null, editingGroupId: null,
selectedElementIds: elements.reduce((map, element) => { selectedElementIds: elements.reduce((map, element) => {
if (!element.isDeleted) { if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId)
) {
map[element.id] = true; map[element.id] = true;
} }
return map; return map;
+1
View File
@@ -80,3 +80,4 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionEditImageAlpha } from "./actionImageEditing";
+2 -1
View File
@@ -101,7 +101,8 @@ export type ActionName =
| "flipVertical" | "flipVertical"
| "viewMode" | "viewMode"
| "exportWithDarkMode" | "exportWithDarkMode"
| "toggleTheme"; | "toggleTheme"
| "editImageAlpha";
export type PanelComponentProps = { export type PanelComponentProps = {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
+2
View File
@@ -41,6 +41,7 @@ export const getDefaultAppState = (): Omit<
editingElement: null, editingElement: null,
editingGroupId: null, editingGroupId: null,
editingLinearElement: null, editingLinearElement: null,
editingImageElement: null,
elementLocked: false, elementLocked: false,
elementType: "selection", elementType: "selection",
errorMessage: null, errorMessage: null,
@@ -125,6 +126,7 @@ const APP_STATE_STORAGE_CONF = (<
editingElement: { browser: false, export: false, server: false }, editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false }, editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false }, editingLinearElement: { browser: false, export: false, server: false },
editingImageElement: { 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 },
errorMessage: { browser: false, export: false, server: false }, errorMessage: { browser: false, export: false, server: false },
+2 -1
View File
@@ -58,7 +58,8 @@ export const copyToClipboard = async (
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const selectedElements = getSelectedElements(elements, appState); // select binded text elements when copying
const selectedElements = getSelectedElements(elements, appState, true);
const contents: ElementsClipboard = { const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard, type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: selectedElements, elements: selectedElements,
+8
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 { isImageElement } from "../element/typeChecks";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
@@ -105,6 +106,13 @@ export const SelectedShapeActions = ({
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
)} )}
<fieldset>
<div className="buttonList">
{targetElements.some((element) => isImageElement(element)) &&
renderAction("editImageAlpha")}
</div>
</fieldset>
{renderAction("changeOpacity")} {renderAction("changeOpacity")}
<fieldset> <fieldset>
+346 -108
View File
@@ -120,6 +120,7 @@ import {
} from "../element/mutateElement"; } from "../element/mutateElement";
import { deepCopyElement, newFreeDrawElement } from "../element/newElement"; import { deepCopyElement, newFreeDrawElement } from "../element/newElement";
import { import {
hasBoundTextElement,
isBindingElement, isBindingElement,
isBindingElementType, isBindingElementType,
isImageElement, isImageElement,
@@ -174,7 +175,7 @@ import {
isSomeElementSelected, isSomeElementSelected,
} from "../scene"; } from "../scene";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { SceneState, ScrollBars } from "../scene/types"; import { RenderConfig, ScrollBars } from "../scene/types";
import { getNewZoom } from "../scene/zoom"; import { getNewZoom } from "../scene/zoom";
import { findShapeByKey } from "../shapes"; import { findShapeByKey } from "../shapes";
import { import {
@@ -194,6 +195,7 @@ import {
import { import {
debounce, debounce,
distance, distance,
getFontString,
getNearestScrollableContainer, getNearestScrollableContainer,
isInputLike, isInputLike,
isToolIcon, isToolIcon,
@@ -228,6 +230,14 @@ import {
} from "../element/image"; } from "../element/image";
import throttle from "lodash.throttle"; import throttle from "lodash.throttle";
import { fileOpen, nativeFileSystemSupported } from "../data/filesystem"; import { fileOpen, nativeFileSystemSupported } from "../data/filesystem";
import {
bindTextToShapeAfterDuplication,
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElementId,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import { ImageEditor } from "../element/imageEditor";
const IsMobileContext = React.createContext(false); const IsMobileContext = React.createContext(false);
export const useIsMobile = () => useContext(IsMobileContext); export const useIsMobile = () => useContext(IsMobileContext);
@@ -272,7 +282,7 @@ class App extends React.Component<AppProps, AppState> {
UIOptions: DEFAULT_UI_OPTIONS, UIOptions: DEFAULT_UI_OPTIONS,
}; };
private scene: Scene; public scene: Scene;
private resizeObserver: ResizeObserver | undefined; private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: AppClassProperties["library"]; public library: AppClassProperties["library"];
@@ -1022,8 +1032,14 @@ class App extends React.Component<AppProps, AppState> {
); );
if ( if (
this.state.editingLinearElement && (this.state.editingLinearElement &&
!this.state.selectedElementIds[this.state.editingLinearElement.elementId] !this.state.selectedElementIds[
this.state.editingLinearElement.elementId
]) ||
(this.state.editingImageElement &&
!this.state.selectedElementIds[
this.state.editingImageElement.elementId
])
) { ) {
// defer so that the commitToHistory flag isn't reset via current update // defer so that the commitToHistory flag isn't reset via current update
setTimeout(() => { setTimeout(() => {
@@ -1053,8 +1069,10 @@ class App extends React.Component<AppProps, AppState> {
const cursorButton: { const cursorButton: {
[id: string]: string | undefined; [id: string]: string | undefined;
} = {}; } = {};
const pointerViewportCoords: SceneState["remotePointerViewportCoords"] = {}; const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] =
const remoteSelectedElementIds: SceneState["remoteSelectedElementIds"] = {}; {};
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
{};
const pointerUsernames: { [id: string]: string } = {}; const pointerUsernames: { [id: string]: string } = {};
const pointerUserStates: { [id: string]: string } = {}; const pointerUserStates: { [id: string]: string } = {};
this.state.collaborators.forEach((user, socketId) => { this.state.collaborators.forEach((user, socketId) => {
@@ -1122,10 +1140,9 @@ class App extends React.Component<AppProps, AppState> {
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom, shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
theme: this.state.theme, theme: this.state.theme,
imageCache: this.imageCache, imageCache: this.imageCache,
}, isExporting: false,
{
renderOptimizations: true,
renderScrollbars: !this.isMobile, renderScrollbars: !this.isMobile,
editingImageElement: this.state.editingImageElement,
}, },
); );
if (scrollBars) { if (scrollBars) {
@@ -1133,7 +1150,7 @@ class App extends React.Component<AppProps, AppState> {
} }
const scrolledOutside = const scrolledOutside =
// hide when editing text // hide when editing text
this.state.editingElement?.type === "text" isTextElement(this.state.editingElement)
? false ? false
: !atLeastOneVisibleElement && renderingElements.length > 0; : !atLeastOneVisibleElement && renderingElements.length > 0;
if (this.state.scrolledOutside !== scrolledOutside) { if (this.state.scrolledOutside !== scrolledOutside) {
@@ -1375,6 +1392,7 @@ class App extends React.Component<AppProps, AppState> {
oldIdToDuplicatedId.set(element.id, newElement.id); oldIdToDuplicatedId.set(element.id, newElement.id);
return newElement; return newElement;
}); });
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
const nextElements = [ const nextElements = [
...this.scene.getElementsIncludingDeleted(), ...this.scene.getElementsIncludingDeleted(),
...newElements, ...newElements,
@@ -1393,7 +1411,9 @@ class App extends React.Component<AppProps, AppState> {
...this.state, ...this.state,
isLibraryOpen: false, isLibraryOpen: false,
selectedElementIds: newElements.reduce((map, element) => { selectedElementIds: newElements.reduce((map, element) => {
map[element.id] = true; if (isTextElement(element) && !element.containerId) {
map[element.id] = true;
}
return map; return map;
}, {} as any), }, {} as any),
selectedGroupIds: {}, selectedGroupIds: {},
@@ -1709,9 +1729,11 @@ class App extends React.Component<AppProps, AppState> {
!isLinearElement(selectedElements[0]) !isLinearElement(selectedElements[0])
) { ) {
const selectedElement = selectedElements[0]; const selectedElement = selectedElements[0];
this.startTextEditing({ this.startTextEditing({
sceneX: selectedElement.x + selectedElement.width / 2, sceneX: selectedElement.x + selectedElement.width / 2,
sceneY: selectedElement.y + selectedElement.height / 2, sceneY: selectedElement.y + selectedElement.height / 2,
shouldBind: true,
}); });
event.preventDefault(); event.preventDefault();
return; return;
@@ -1866,14 +1888,24 @@ class App extends React.Component<AppProps, AppState> {
isExistingElement?: boolean; isExistingElement?: boolean;
}, },
) { ) {
const updateElement = (text: string, isDeleted = false) => { const updateElement = (
text: string,
originalText: string,
isDeleted = false,
updateDimensions = false,
) => {
this.scene.replaceAllElements([ this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted().map((_element) => { ...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) { if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement(_element, { return updateTextElement(
text, _element,
isDeleted, {
}); text,
isDeleted,
originalText,
},
updateDimensions,
);
} }
return _element; return _element;
}), }),
@@ -1898,21 +1930,24 @@ class App extends React.Component<AppProps, AppState> {
]; ];
}, },
onChange: withBatchedUpdates((text) => { onChange: withBatchedUpdates((text) => {
updateElement(text); updateElement(text, text, false, !element.containerId);
if (isNonDeletedElement(element)) { if (isNonDeletedElement(element)) {
updateBoundElements(element); updateBoundElements(element);
} }
}), }),
onSubmit: withBatchedUpdates(({ text, viaKeyboard }) => { onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
const isDeleted = !text.trim(); const isDeleted = !text.trim();
updateElement(text, isDeleted); updateElement(text, originalText, isDeleted, true);
// select the created text element only if submitting via keyboard // select the created text element only if submitting via keyboard
// (when submitting via click it should act as signal to deselect) // (when submitting via click it should act as signal to deselect)
if (!isDeleted && viaKeyboard) { if (!isDeleted && viaKeyboard) {
const elementIdToSelect = element.containerId
? element.containerId
: element.id;
this.setState((prevState) => ({ this.setState((prevState) => ({
selectedElementIds: { selectedElementIds: {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
[element.id]: true, [elementIdToSelect]: true,
}, },
})); }));
} }
@@ -1941,7 +1976,7 @@ class App extends React.Component<AppProps, AppState> {
// do an initial update to re-initialize element position since we were // do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote) // modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.text); updateElement(element.text, element.originalText);
} }
private deselectElements() { private deselectElements() {
@@ -1956,7 +1991,9 @@ class App extends React.Component<AppProps, AppState> {
x: number, x: number,
y: number, y: number,
): NonDeleted<ExcalidrawTextElement> | null { ): NonDeleted<ExcalidrawTextElement> | null {
const element = this.getElementAtPosition(x, y); const element = this.getElementAtPosition(x, y, {
includeBoundTextElement: true,
});
if (element && isTextElement(element) && !element.isDeleted) { if (element && isTextElement(element) && !element.isDeleted) {
return element; return element;
@@ -1971,9 +2008,14 @@ class App extends React.Component<AppProps, AppState> {
/** if true, returns the first selected element (with highest z-index) /** if true, returns the first selected element (with highest z-index)
of all hit elements */ of all hit elements */
preferSelected?: boolean; preferSelected?: boolean;
includeBoundTextElement?: boolean;
}, },
): NonDeleted<ExcalidrawElement> | null { ): NonDeleted<ExcalidrawElement> | null {
const allHitElements = this.getElementsAtPosition(x, y); const allHitElements = this.getElementsAtPosition(
x,
y,
opts?.includeBoundTextElement,
);
if (allHitElements.length > 1) { if (allHitElements.length > 1) {
if (opts?.preferSelected) { if (opts?.preferSelected) {
for (let index = allHitElements.length - 1; index > -1; index--) { for (let index = allHitElements.length - 1; index > -1; index--) {
@@ -2004,8 +2046,16 @@ class App extends React.Component<AppProps, AppState> {
private getElementsAtPosition( private getElementsAtPosition(
x: number, x: number,
y: number, y: number,
includeBoundTextElement: boolean = false,
): NonDeleted<ExcalidrawElement>[] { ): NonDeleted<ExcalidrawElement>[] {
return getElementsAtPosition(this.scene.getElements(), (element) => const elements = includeBoundTextElement
? this.scene.getElements()
: this.scene
.getElements()
.filter(
(element) => !(isTextElement(element) && element.containerId),
);
return getElementsAtPosition(elements, (element) =>
hitTest(element, this.state, x, y), hitTest(element, this.state, x, y),
); );
} }
@@ -2013,17 +2063,17 @@ class App extends React.Component<AppProps, AppState> {
private startTextEditing = ({ private startTextEditing = ({
sceneX, sceneX,
sceneY, sceneY,
shouldBind,
insertAtParentCenter = true, insertAtParentCenter = true,
}: { }: {
/** X position to insert text at */ /** X position to insert text at */
sceneX: number; sceneX: number;
/** Y position to insert text at */ /** Y position to insert text at */
sceneY: number; sceneY: number;
shouldBind: boolean;
/** whether to attempt to insert at element center if applicable */ /** whether to attempt to insert at element center if applicable */
insertAtParentCenter?: boolean; insertAtParentCenter?: boolean;
}) => { }) => {
const existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
const parentCenterPosition = const parentCenterPosition =
insertAtParentCenter && insertAtParentCenter &&
this.getTextWysiwygSnappedToCenterPosition( this.getTextWysiwygSnappedToCenterPosition(
@@ -2034,6 +2084,43 @@ class App extends React.Component<AppProps, AppState> {
window.devicePixelRatio, window.devicePixelRatio,
); );
// bind to container when shouldBind is true or
// clicked on center of container
const container =
shouldBind || parentCenterPosition
? getElementContainingPosition(
this.scene.getElements(),
sceneX,
sceneY,
"text",
)
: null;
let existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
// consider bounded text element if container present
if (container) {
const boundTextElementId = getBoundTextElementId(container);
if (boundTextElementId) {
existingTextElement = this.scene.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
}
}
if (!existingTextElement && container) {
const fontString = {
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
};
const minWidth = getApproxMinLineWidth(getFontString(fontString));
const minHeight = getApproxMinLineHeight(getFontString(fontString));
const newHeight = Math.max(container.height, minHeight);
const newWidth = Math.max(container.width, minWidth);
mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2;
}
const element = existingTextElement const element = existingTextElement
? existingTextElement ? existingTextElement
: newTextElement({ : newTextElement({
@@ -2060,6 +2147,7 @@ class App extends React.Component<AppProps, AppState> {
verticalAlign: parentCenterPosition verticalAlign: parentCenterPosition
? "middle" ? "middle"
: DEFAULT_VERTICAL_ALIGN, : DEFAULT_VERTICAL_ALIGN,
containerId: container?.id ?? undefined,
}); });
this.setState({ editingElement: element }); this.setState({ editingElement: element });
@@ -2130,7 +2218,7 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.canvas); resetCursor(this.canvas);
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event, event,
this.state, this.state,
); );
@@ -2162,9 +2250,22 @@ class App extends React.Component<AppProps, AppState> {
resetCursor(this.canvas); resetCursor(this.canvas);
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) { if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
);
if (selectedElements.length === 1) {
const selectedElement = selectedElements[0];
const canBindText = hasBoundTextElement(selectedElement);
if (canBindText) {
sceneX = selectedElement.x + selectedElement.width / 2;
sceneY = selectedElement.y + selectedElement.height / 2;
}
}
this.startTextEditing({ this.startTextEditing({
sceneX, sceneX,
sceneY, sceneY,
shouldBind: false,
insertAtParentCenter: !event.altKey, insertAtParentCenter: !event.altKey,
}); });
} }
@@ -2237,6 +2338,10 @@ class App extends React.Component<AppProps, AppState> {
const scenePointer = viewportCoordsToSceneCoords(event, this.state); const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer; const { x: scenePointerX, y: scenePointerY } = scenePointer;
if (this.state.editingImageElement) {
return;
}
if ( if (
this.state.editingLinearElement && this.state.editingLinearElement &&
!this.state.editingLinearElement.isDragging !this.state.editingLinearElement.isDragging
@@ -2263,10 +2368,9 @@ class App extends React.Component<AppProps, AppState> {
// and point // and point
const { draggingElement } = this.state; const { draggingElement } = this.state;
if (isBindingElement(draggingElement)) { if (isBindingElement(draggingElement)) {
this.maybeSuggestBindingForLinearElementAtCursor( this.maybeSuggestBindingsForLinearElementAtCoords(
draggingElement, draggingElement,
"end", [scenePointer],
scenePointer,
this.state.startBoundElement, this.state.startBoundElement,
); );
} else { } else {
@@ -2399,6 +2503,21 @@ class App extends React.Component<AppProps, AppState> {
setCursor(this.canvas, CURSOR_TYPE.GRAB); setCursor(this.canvas, CURSOR_TYPE.GRAB);
} else if (isOverScrollBar) { } else if (isOverScrollBar) {
setCursor(this.canvas, CURSOR_TYPE.AUTO); setCursor(this.canvas, CURSOR_TYPE.AUTO);
} else if (this.state.editingLinearElement) {
const element = LinearElementEditor.getElement(
this.state.editingLinearElement.elementId,
);
if (
element &&
isHittingElementNotConsideringBoundingBox(element, this.state, [
scenePointer.x,
scenePointer.y,
])
) {
setCursor(this.canvas, CURSOR_TYPE.MOVE);
} else {
setCursor(this.canvas, CURSOR_TYPE.AUTO);
}
} else if ( } else if (
// if using cmd/ctrl, we're not dragging // if using cmd/ctrl, we're not dragging
!event[KEYS.CTRL_OR_CMD] && !event[KEYS.CTRL_OR_CMD] &&
@@ -2736,6 +2855,7 @@ class App extends React.Component<AppProps, AppState> {
origin, origin,
selectedElements, selectedElements,
), ),
hasHitElementInside: false,
}, },
drag: { drag: {
hasOccurred: false, hasOccurred: false,
@@ -2747,6 +2867,9 @@ class App extends React.Component<AppProps, AppState> {
onKeyUp: null, onKeyUp: null,
onKeyDown: null, onKeyDown: null,
}, },
boxSelection: {
hasOccurred: false,
},
}; };
} }
@@ -2809,6 +2932,14 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
): boolean => { ): boolean => {
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
if (this.state.editingImageElement) {
ImageEditor.handlePointerDown(
this.state.editingImageElement,
pointerDownState.origin,
);
return false;
}
const elements = this.scene.getElements(); const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
@@ -2888,6 +3019,15 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
); );
if (pointerDownState.hit.element) {
pointerDownState.hit.hasHitElementInside =
isHittingElementNotConsideringBoundingBox(
pointerDownState.hit.element,
this.state,
[pointerDownState.origin.x, pointerDownState.origin.y],
);
}
// For overlapped elements one position may hit // For overlapped elements one position may hit
// multiple elements // multiple elements
pointerDownState.hit.allHitElements = this.getElementsAtPosition( pointerDownState.hit.allHitElements = this.getElementsAtPosition(
@@ -2908,8 +3048,14 @@ class App extends React.Component<AppProps, AppState> {
this.clearSelection(hitElement); this.clearSelection(hitElement);
} }
// If we click on something if (this.state.editingLinearElement) {
if (hitElement != null) { this.setState({
selectedElementIds: {
[this.state.editingLinearElement.elementId]: true,
},
});
// If we click on something
} else if (hitElement != null) {
// on CMD/CTRL, drill down to hit element regardless of groups etc. // on CMD/CTRL, drill down to hit element regardless of groups etc.
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
if (!this.state.selectedElementIds[hitElement.id]) { if (!this.state.selectedElementIds[hitElement.id]) {
@@ -3002,13 +3148,25 @@ class App extends React.Component<AppProps, AppState> {
// if we're currently still editing text, clicking outside // if we're currently still editing text, clicking outside
// should only finalize it, not create another (irrespective // should only finalize it, not create another (irrespective
// of state.elementLocked) // of state.elementLocked)
if (this.state.editingElement?.type === "text") { if (isTextElement(this.state.editingElement)) {
return; return;
} }
let sceneX = pointerDownState.origin.x;
let sceneY = pointerDownState.origin.y;
const element = this.getElementAtPosition(sceneX, sceneY, {
includeBoundTextElement: true,
});
const canBindText = hasBoundTextElement(element);
if (canBindText) {
sceneX = element.x + element.width / 2;
sceneY = element.y + element.height / 2;
}
this.startTextEditing({ this.startTextEditing({
sceneX: pointerDownState.origin.x, sceneX,
sceneY: pointerDownState.origin.y, sceneY,
shouldBind: false,
insertAtParentCenter: !event.altKey, insertAtParentCenter: !event.altKey,
}); });
@@ -3342,17 +3500,32 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
if (this.state.editingImageElement) {
const newImageData = ImageEditor.handlePointerMove(
this.state.editingImageElement,
pointerCoords,
);
if (newImageData) {
this.setState({
editingImageElement: {
...this.state.editingImageElement,
imageData: newImageData,
},
});
}
return;
}
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
const didDrag = LinearElementEditor.handlePointDragging( const didDrag = LinearElementEditor.handlePointDragging(
this.state, this.state,
(appState) => this.setState(appState), (appState) => this.setState(appState),
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
(element, startOrEnd) => { (element, pointsSceneCoords) => {
this.maybeSuggestBindingForLinearElementAtCursor( this.maybeSuggestBindingsForLinearElementAtCoords(
element, element,
startOrEnd, pointsSceneCoords,
pointerCoords,
); );
}, },
); );
@@ -3369,8 +3542,16 @@ class App extends React.Component<AppProps, AppState> {
); );
if ( if (
hasHitASelectedElement || (hasHitASelectedElement ||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
// this allows for box-selecting points when clicking inside the
// line's bounding box
(!this.state.editingLinearElement || !event.shiftKey) &&
// box-selecting without shift when editing line, not clicking on a line
(!this.state.editingLinearElement ||
this.state.editingLinearElement?.elementId !==
pointerDownState.hit.element?.id ||
pointerDownState.hit.hasHitElementInside)
) { ) {
// Marking that click was used for dragging to check // Marking that click was used for dragging to check
// if elements should be deselected on pointerup // if elements should be deselected on pointerup
@@ -3401,7 +3582,6 @@ class App extends React.Component<AppProps, AppState> {
selectedElements, selectedElements,
dragX, dragX,
dragY, dragY,
this.scene,
lockDirection, lockDirection,
dragDistanceX, dragDistanceX,
dragDistanceY, dragDistanceY,
@@ -3421,9 +3601,15 @@ class App extends React.Component<AppProps, AppState> {
const groupIdMap = new Map(); const groupIdMap = new Map();
const oldIdToDuplicatedId = new Map(); const oldIdToDuplicatedId = new Map();
const hitElement = pointerDownState.hit.element; const hitElement = pointerDownState.hit.element;
for (const element of this.scene.getElementsIncludingDeleted()) { const elements = this.scene.getElementsIncludingDeleted();
const selectedElementIds: Array<ExcalidrawElement["id"]> =
getSelectedElements(elements, this.state, true).map(
(element) => element.id,
);
for (const element of elements) {
if ( if (
this.state.selectedElementIds[element.id] || selectedElementIds.includes(element.id) ||
// case: the state.selectedElementIds might not have been // case: the state.selectedElementIds might not have been
// updated yet by the time this mousemove event is fired // updated yet by the time this mousemove event is fired
(element.id === hitElement?.id && (element.id === hitElement?.id &&
@@ -3451,6 +3637,11 @@ class App extends React.Component<AppProps, AppState> {
} }
} }
const nextSceneElements = [...nextElements, ...elementsToAppend]; const nextSceneElements = [...nextElements, ...elementsToAppend];
bindTextToShapeAfterDuplication(
nextElements,
elementsToAppend,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication( fixBindingsAfterDuplication(
nextSceneElements, nextSceneElements,
elementsToAppend, elementsToAppend,
@@ -3507,10 +3698,9 @@ class App extends React.Component<AppProps, AppState> {
if (isBindingElement(draggingElement)) { if (isBindingElement(draggingElement)) {
// When creating a linear element by dragging // When creating a linear element by dragging
this.maybeSuggestBindingForLinearElementAtCursor( this.maybeSuggestBindingsForLinearElementAtCoords(
draggingElement, draggingElement,
"end", [pointerCoords],
pointerCoords,
this.state.startBoundElement, this.state.startBoundElement,
); );
} }
@@ -3521,8 +3711,15 @@ class App extends React.Component<AppProps, AppState> {
} }
if (this.state.elementType === "selection") { if (this.state.elementType === "selection") {
pointerDownState.boxSelection.hasOccurred = true;
const elements = this.scene.getElements(); const elements = this.scene.getElements();
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) { if (
!event.shiftKey &&
// allows for box-selecting points (without shift)
!this.state.editingLinearElement &&
isSomeElementSelected(elements, this.state)
) {
if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) { if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
this.setState((prevState) => this.setState((prevState) =>
selectGroupsForSelectedElements( selectGroupsForSelectedElements(
@@ -3543,33 +3740,43 @@ class App extends React.Component<AppProps, AppState> {
}); });
} }
} }
const elementsWithinSelection = getElementsWithinSelection( // box-select line editor points
elements, if (this.state.editingLinearElement) {
draggingElement, LinearElementEditor.handleBoxSelection(
); event,
this.setState((prevState) => this.state,
selectGroupsForSelectedElements( this.setState.bind(this),
{ );
...prevState, // regular box-select
selectedElementIds: { } else {
...prevState.selectedElementIds, const elementsWithinSelection = getElementsWithinSelection(
...elementsWithinSelection.reduce((map, element) => { elements,
map[element.id] = true; draggingElement,
return map; );
}, {} as any), this.setState((prevState) =>
...(pointerDownState.hit.element selectGroupsForSelectedElements(
? { {
// if using ctrl/cmd, select the hitElement only if we ...prevState,
// haven't box-selected anything else selectedElementIds: {
[pointerDownState.hit.element.id]: ...prevState.selectedElementIds,
!elementsWithinSelection.length, ...elementsWithinSelection.reduce((map, element) => {
} map[element.id] = true;
: null), return map;
}, {} as any),
...(pointerDownState.hit.element
? {
// if using ctrl/cmd, select the hitElement only if we
// haven't box-selected anything else
[pointerDownState.hit.element.id]:
!elementsWithinSelection.length,
}
: null),
},
}, },
}, this.scene.getElements(),
this.scene.getElements(), ),
), );
); }
} }
}); });
} }
@@ -3631,19 +3838,32 @@ class App extends React.Component<AppProps, AppState> {
this.savePointer(childEvent.clientX, childEvent.clientY, "up"); this.savePointer(childEvent.clientX, childEvent.clientY, "up");
if (this.state.editingImageElement) {
ImageEditor.handlePointerUp(this.state.editingImageElement);
}
// Handle end of dragging a point of a linear element, might close a loop // Handle end of dragging a point of a linear element, might close a loop
// and sets binding element // and sets binding element
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
const editingLinearElement = LinearElementEditor.handlePointerUp( if (
childEvent, !pointerDownState.boxSelection.hasOccurred &&
this.state.editingLinearElement, (pointerDownState.hit?.element?.id !==
this.state, this.state.editingLinearElement.elementId ||
); !pointerDownState.hit.hasHitElementInside)
if (editingLinearElement !== this.state.editingLinearElement) { ) {
this.setState({ this.actionManager.executeAction(actionFinalize);
editingLinearElement, } else {
suggestedBindings: [], const editingLinearElement = LinearElementEditor.handlePointerUp(
}); childEvent,
this.state.editingLinearElement,
this.state,
);
if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({
editingLinearElement,
suggestedBindings: [],
});
}
} }
} }
@@ -3825,9 +4045,14 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
hitElement && hitElement &&
!pointerDownState.drag.hasOccurred && !pointerDownState.drag.hasOccurred &&
!pointerDownState.hit.wasAddedToSelection !pointerDownState.hit.wasAddedToSelection &&
// if we're editing a line, pointerup shouldn't switch selection if
// box selected
(!this.state.editingLinearElement ||
!pointerDownState.boxSelection.hasOccurred)
) { ) {
if (childEvent.shiftKey) { // when inside line editor, shift selects points instead
if (childEvent.shiftKey && !this.state.editingLinearElement) {
if (this.state.selectedElementIds[hitElement.id]) { if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) { if (isSelectedViaGroup(this.state, hitElement)) {
// We want to unselect all groups hitElement is part of // We want to unselect all groups hitElement is part of
@@ -3871,6 +4096,7 @@ class App extends React.Component<AppProps, AppState> {
} else { } else {
// add element to selection while // add element to selection while
// keeping prev elements selected // keeping prev elements selected
this.setState((_prevState) => ({ this.setState((_prevState) => ({
selectedElementIds: { selectedElementIds: {
..._prevState.selectedElementIds, ..._prevState.selectedElementIds,
@@ -4001,10 +4227,9 @@ class App extends React.Component<AppProps, AppState> {
const existingFileData = this.files[fileId]; const existingFileData = this.files[fileId];
if (!existingFileData?.dataURL) { if (!existingFileData?.dataURL) {
try { try {
imageFile = await resizeImageFile( imageFile = await resizeImageFile(imageFile, {
imageFile, maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT, });
);
} catch (error: any) { } catch (error: any) {
console.error("error trying to resing image file on insertion", error); console.error("error trying to resing image file on insertion", error);
} }
@@ -4113,7 +4338,9 @@ class App extends React.Component<AppProps, AppState> {
// https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property
const cursorImageSizePx = 96; const cursorImageSizePx = 96;
const imagePreview = await resizeImageFile(imageFile, cursorImageSizePx); const imagePreview = await resizeImageFile(imageFile, {
maxWidthOrHeight: cursorImageSizePx,
});
let previewDataURL = await getDataURL(imagePreview); let previewDataURL = await getDataURL(imagePreview);
@@ -4351,32 +4578,43 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
private maybeSuggestBindingForLinearElementAtCursor = ( private maybeSuggestBindingsForLinearElementAtCoords = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", /** scene coords */
pointerCoords: { pointerCoords: {
x: number; x: number;
y: number; y: number;
}, }[],
// During line creation the start binding hasn't been written yet // During line creation the start binding hasn't been written yet
// into `linearElement` // into `linearElement`
oppositeBindingBoundElement?: ExcalidrawBindableElement | null, oppositeBindingBoundElement?: ExcalidrawBindableElement | null,
): void => { ): void => {
const hoveredBindableElement = getHoveredElementForBinding( if (!pointerCoords.length) {
pointerCoords, return;
this.scene, }
const suggestedBindings = pointerCoords.reduce(
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
this.scene,
);
if (
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
) {
acc.push(hoveredBindableElement);
}
return acc;
},
[],
); );
this.setState({
suggestedBindings: this.setState({ suggestedBindings });
hoveredBindableElement != null &&
!isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement,
)
? [hoveredBindableElement]
: [],
});
}; };
private maybeSuggestBindingForAll( private maybeSuggestBindingForAll(
+11 -4
View File
@@ -3,15 +3,22 @@ import OpenColor from "open-color";
import "./Card.scss"; import "./Card.scss";
export const Card: React.FC<{ export const Card: React.FC<{
color: keyof OpenColor; color: keyof OpenColor | "primary";
}> = ({ children, color }) => { }> = ({ children, color }) => {
return ( return (
<div <div
className="Card" className="Card"
style={{ style={{
["--card-color" as any]: OpenColor[color][7], ["--card-color" as any]:
["--card-color-darker" as any]: OpenColor[color][8], color === "primary" ? "var(--color-primary)" : OpenColor[color][7],
["--card-color-darkest" as any]: OpenColor[color][9], ["--card-color-darker" as any]:
color === "primary"
? "var(--color-primary-darker)"
: OpenColor[color][8],
["--card-color-darkest" as any]:
color === "primary"
? "var(--color-primary-darkest)"
: OpenColor[color][9],
}} }}
> >
{children} {children}
+2 -2
View File
@@ -6,14 +6,14 @@ import "./CheckboxItem.scss";
export const CheckboxItem: React.FC<{ export const CheckboxItem: React.FC<{
checked: boolean; checked: boolean;
onChange: (checked: boolean) => void; onChange: (checked: boolean, event: React.MouseEvent) => void;
className?: string; className?: string;
}> = ({ children, checked, onChange, className }) => { }> = ({ children, checked, onChange, className }) => {
return ( return (
<div <div
className={clsx("Checkbox", className, { "is-checked": checked })} className={clsx("Checkbox", className, { "is-checked": checked })}
onClick={(event) => { onClick={(event) => {
onChange(!checked); onChange(!checked, event);
( (
(event.currentTarget as HTMLDivElement).querySelector( (event.currentTarget as HTMLDivElement).querySelector(
".Checkbox-box", ".Checkbox-box",
+12 -6
View File
@@ -7,6 +7,7 @@ import { AppState } from "../types";
import { import {
isImageElement, isImageElement,
isLinearElement, isLinearElement,
isTextBindableContainer,
isTextElement, isTextElement,
} from "../element/typeChecks"; } from "../element/typeChecks";
import { getShortcutKey } from "../utils"; import { getShortcutKey } from "../utils";
@@ -60,13 +61,18 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
return t("hints.rotate"); return t("hints.rotate");
} }
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1) {
if (appState.editingLinearElement) { if (isLinearElement(selectedElements[0])) {
return appState.editingLinearElement.activePointIndex if (appState.editingLinearElement) {
? t("hints.lineEditor_pointSelected") return appState.editingLinearElement.selectedPointsIndices
: t("hints.lineEditor_nothingSelected"); ? t("hints.lineEditor_pointSelected")
: t("hints.lineEditor_nothingSelected");
}
return t("hints.lineEditor_info");
}
if (isTextBindableContainer(selectedElements[0])) {
return t("hints.bindTextToElement");
} }
return t("hints.lineEditor_info");
} }
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) { if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
+1 -1
View File
@@ -22,7 +22,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:focus { &:focus-visible {
outline: transparent; outline: transparent;
background-color: var(--button-gray-2); background-color: var(--button-gray-2);
& svg { & svg {
+1 -1
View File
@@ -102,7 +102,7 @@ const ImageExportModal = ({
const { exportBackground, viewBackgroundColor } = appState; const { exportBackground, viewBackgroundColor } = appState;
const exportedElements = exportSelected const exportedElements = exportSelected
? getSelectedElements(elements, appState) ? getSelectedElements(elements, appState, true)
: elements; : elements;
useEffect(() => { useEffect(() => {
+1 -1
View File
@@ -3,7 +3,7 @@
--padding: 0; --padding: 0;
background-color: var(--island-bg-color); background-color: var(--island-bg-color);
box-shadow: var(--shadow-island); box-shadow: var(--shadow-island);
border-radius: 4px; border-radius: var(--border-radius-lg);
padding: calc(var(--padding) * var(--space-factor)); padding: calc(var(--padding) * var(--space-factor));
position: relative; position: relative;
transition: box-shadow 0.5s ease-in-out; transition: box-shadow 0.5s ease-in-out;
+13 -4
View File
@@ -19,7 +19,6 @@ import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
import { FixedSideContainer } from "./FixedSideContainer"; import { FixedSideContainer } from "./FixedSideContainer";
import { HintViewer } from "./HintViewer"; import { HintViewer } from "./HintViewer";
import { Island } from "./Island"; import { Island } from "./Island";
import "./LayerUI.scss";
import { LoadingMessage } from "./LoadingMessage"; import { LoadingMessage } from "./LoadingMessage";
import { LockButton } from "./LockButton"; import { LockButton } from "./LockButton";
import { MobileMenu } from "./MobileMenu"; import { MobileMenu } from "./MobileMenu";
@@ -35,6 +34,9 @@ import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
import { LibraryMenu } from "./LibraryMenu"; import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss";
import "./Toolbar.scss";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
appState: AppState; appState: AppState;
@@ -268,7 +270,7 @@ const LayerUI = ({
const libraryMenu = appState.isLibraryOpen ? ( const libraryMenu = appState.isLibraryOpen ? (
<LibraryMenu <LibraryMenu
pendingElements={getSelectedElements(elements, appState)} pendingElements={getSelectedElements(elements, appState, true)}
onClose={closeLibrary} onClose={closeLibrary}
onInsertShape={onInsertElements} onInsertShape={onInsertElements}
onAddToLibrary={deselectItems} onAddToLibrary={deselectItems}
@@ -305,7 +307,12 @@ const LayerUI = ({
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
<Stack.Col gap={4} align="start"> <Stack.Col gap={4} align="start">
<Stack.Row gap={1}> <Stack.Row
gap={1}
className={clsx("App-toolbar-container", {
"zen-mode": zenModeEnabled,
})}
>
<LockButton <LockButton
zenModeEnabled={zenModeEnabled} zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked} checked={appState.elementLocked}
@@ -314,7 +321,9 @@ const LayerUI = ({
/> />
<Island <Island
padding={1} padding={1}
className={clsx({ "zen-mode": zenModeEnabled })} className={clsx("App-toolbar", {
"zen-mode": zenModeEnabled,
})}
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
+4 -4
View File
@@ -16,18 +16,18 @@ const LIBRARY_ICON = (
export const LibraryButton: React.FC<{ export const LibraryButton: React.FC<{
appState: AppState; appState: AppState;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
}> = ({ appState, setAppState }) => { isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => {
return ( return (
<label <label
className={clsx( className={clsx(
"ToolIcon ToolIcon_type_floating ToolIcon__library zen-mode-visibility", "ToolIcon ToolIcon_type_floating ToolIcon__library",
`ToolIcon_size_medium`, `ToolIcon_size_medium`,
{ {
"zen-mode-visibility--hidden": appState.zenModeEnabled, "is-mobile": isMobile,
}, },
)} )}
title={`${capitalizeString(t("toolBar.library"))} — 0`} title={`${capitalizeString(t("toolBar.library"))} — 0`}
style={{ marginInlineStart: "var(--space-factor)" }}
> >
<input <input
className="ToolIcon_type_checkbox" className="ToolIcon_type_checkbox"
+42 -3
View File
@@ -18,6 +18,7 @@ import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems"; import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants"; import { EVENT } from "../constants";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
const useOnClickOutside = ( const useOnClickOutside = (
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>,
@@ -236,6 +237,10 @@ export const LibraryMenu = ({
], ],
); );
const [lastSelectedItem, setLastSelectedItem] = useState<
LibraryItem["id"] | null
>(null);
return loadingState === "preloading" ? null : ( return loadingState === "preloading" ? null : (
<Island padding={1} ref={ref} className="layer-ui__library"> <Island padding={1} ref={ref} className="layer-ui__library">
{showPublishLibraryDialog && ( {showPublishLibraryDialog && (
@@ -271,10 +276,44 @@ export const LibraryMenu = ({
files={files} files={files}
id={id} id={id}
selectedItems={selectedItems} selectedItems={selectedItems}
onToggle={(id) => { onToggle={(id, event) => {
if (!selectedItems.includes(id)) { const shouldSelect = !selectedItems.includes(id);
setSelectedItems([...selectedItems, id]);
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = libraryItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = libraryItems.findIndex(
(item) => item.id === id,
);
if (rangeStart === -1 || rangeEnd === -1) {
setSelectedItems([...selectedItems, id]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = libraryItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
setSelectedItems(nextSelectedIds);
} else {
setSelectedItems([...selectedItems, id]);
}
setLastSelectedItem(id);
} else { } else {
setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id)); setSelectedItems(selectedItems.filter((_id) => _id !== id));
} }
}} }}
+7 -6
View File
@@ -21,6 +21,7 @@ import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
import { VERSIONS } from "../constants";
const LibraryMenuItems = ({ const LibraryMenuItems = ({
libraryItems, libraryItems,
@@ -51,7 +52,7 @@ const LibraryMenuItems = ({
library: Library; library: Library;
id: string; id: string;
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
onToggle: (id: LibraryItem["id"]) => void; onToggle: (id: LibraryItem["id"], event: React.MouseEvent) => void;
onPublish: () => void; onPublish: () => void;
resetLibrary: () => void; resetLibrary: () => void;
}) => { }) => {
@@ -212,10 +213,8 @@ const LibraryMenuItems = ({
onClick={params.onClick || (() => {})} onClick={params.onClick || (() => {})}
id={params.item?.id || null} id={params.item?.id || null}
selected={!!params.item?.id && selectedItems.includes(params.item.id)} selected={!!params.item?.id && selectedItems.includes(params.item.id)}
onToggle={() => { onToggle={(id, event) => {
if (params.item?.id) { onToggle(id, event);
onToggle(params.item.id);
}
}} }}
/> />
</Stack.Col> </Stack.Col>
@@ -293,7 +292,9 @@ const LibraryMenuItems = ({
<a <a
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${ href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank" window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`} }&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary
}`}
target="_excalidraw_libraries" target="_excalidraw_libraries"
> >
{t("labels.libraries")} {t("labels.libraries")}
+7 -2
View File
@@ -99,8 +99,13 @@
margin-top: -10px; margin-top: -10px;
pointer-events: none; pointer-events: none;
} }
.library-unit--hover .library-unit__adder { .library-unit:hover .library-unit__adder {
color: $oc-blue-7; fill: $oc-blue-7;
}
.library-unit:active .library-unit__adder {
animation: none;
transform: scale(0.8);
fill: $oc-black;
} }
.library-unit__active { .library-unit__active {
+19 -6
View File
@@ -8,12 +8,15 @@ import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem"; import { CheckboxItem } from "./CheckboxItem";
// fa-plus
const PLUS_ICON = ( const PLUS_ICON = (
<svg viewBox="0 0 1792 1792"> <svg viewBox="0 0 1792 1792">
<path <path
fill="currentColor" d="M1600 736v192c0 26.667-9.33 49.333-28 68-18.67 18.67-41.33 28-68 28h-416v416c0 26.67-9.33 49.33-28 68s-41.33 28-68 28H800c-26.667 0-49.333-9.33-68-28s-28-41.33-28-68v-416H288c-26.667 0-49.333-9.33-68-28-18.667-18.667-28-41.333-28-68V736c0-26.667 9.333-49.333 28-68s41.333-28 68-28h416V224c0-26.667 9.333-49.333 28-68s41.333-28 68-28h192c26.67 0 49.33 9.333 68 28s28 41.333 28 68v416h416c26.67 0 49.33 9.333 68 28s28 41.333 28 68Z"
d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z" style={{
stroke: "#fff",
strokeWidth: 140,
}}
transform="translate(0 64)"
/> />
</svg> </svg>
); );
@@ -33,7 +36,7 @@ export const LibraryUnit = ({
isPending?: boolean; isPending?: boolean;
onClick: () => void; onClick: () => void;
selected: boolean; selected: boolean;
onToggle: (id: string) => void; onToggle: (id: string, event: React.MouseEvent) => void;
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
@@ -84,7 +87,17 @@ export const LibraryUnit = ({
})} })}
ref={ref} ref={ref}
draggable={!!elements} draggable={!!elements}
onClick={!!elements || !!isPending ? onClick : undefined} onClick={
!!elements || !!isPending
? (event) => {
if (id && event.shiftKey) {
onToggle(id, event);
} else {
onClick();
}
}
: undefined
}
onDragStart={(event) => { onDragStart={(event) => {
setIsHovered(false); setIsHovered(false);
event.dataTransfer.setData( event.dataTransfer.setData(
@@ -97,7 +110,7 @@ export const LibraryUnit = ({
{id && elements && (isHovered || isMobile || selected) && ( {id && elements && (isHovered || isMobile || selected) && (
<CheckboxItem <CheckboxItem
checked={selected} checked={selected}
onChange={() => onToggle(id)} onChange={(checked, event) => onToggle(id, event)}
className="library-unit__checkbox" className="library-unit__checkbox"
/> />
)} )}
+3 -2
View File
@@ -10,6 +10,7 @@ type LockIconProps = {
checked: boolean; checked: boolean;
onChange?(): void; onChange?(): void;
zenModeEnabled?: boolean; zenModeEnabled?: boolean;
isMobile?: boolean;
}; };
const DEFAULT_SIZE: ToolButtonSize = "medium"; const DEFAULT_SIZE: ToolButtonSize = "medium";
@@ -42,10 +43,10 @@ export const LockButton = (props: LockIconProps) => {
return ( return (
<label <label
className={clsx( className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility", "ToolIcon ToolIcon__lock ToolIcon_type_floating",
`ToolIcon_size_${DEFAULT_SIZE}`, `ToolIcon_size_${DEFAULT_SIZE}`,
{ {
"zen-mode-visibility--hidden": props.zenModeEnabled, "is-mobile": props.isMobile,
}, },
)} )}
title={`${props.title} — Q`} title={`${props.title} — Q`}
+8 -3
View File
@@ -64,8 +64,8 @@ export const MobileMenu = ({
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading) => (
<Stack.Col gap={4} align="center"> <Stack.Col gap={4} align="center">
<Stack.Row gap={1}> <Stack.Row gap={1} className="App-toolbar-container">
<Island padding={1}> <Island padding={1} className="App-toolbar">
{heading} {heading}
<Stack.Row gap={1}> <Stack.Row gap={1}>
<ShapesSwitcher <ShapesSwitcher
@@ -85,8 +85,13 @@ export const MobileMenu = ({
checked={appState.elementLocked} checked={appState.elementLocked}
onChange={onLockToggle} onChange={onLockToggle}
title={t("toolBar.lock")} title={t("toolBar.lock")}
isMobile
/>
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/> />
<LibraryButton appState={appState} setAppState={setAppState} />
</Stack.Row> </Stack.Row>
{libraryMenu} {libraryMenu}
</Stack.Col> </Stack.Col>
+83 -57
View File
@@ -1,5 +1,5 @@
import { ReactNode, useCallback, useEffect, useState } from "react"; import { ReactNode, useCallback, useEffect, useState } from "react";
import oc from "open-color"; import OpenColor from "open-color";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -7,16 +7,19 @@ import { t } from "../i18n";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { AppState, LibraryItems, LibraryItem } from "../types"; import { AppState, LibraryItems, LibraryItem } from "../types";
import { exportToBlob } from "../packages/utils"; import { exportToCanvas } from "../packages/utils";
import { EXPORT_DATA_TYPES, EXPORT_SOURCE } from "../constants"; import {
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
} from "../constants";
import { ExportedLibraryData } from "../data/types"; import { ExportedLibraryData } from "../data/types";
import "./PublishLibrary.scss"; import "./PublishLibrary.scss";
import { ExcalidrawElement } from "../element/types";
import { newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import { getCommonBoundingBox } from "../element/bounds";
import SingleLibraryItem from "./SingleLibraryItem"; import SingleLibraryItem from "./SingleLibraryItem";
import { canvasToBlob, resizeImageFile } from "../data/blob";
import { chunk } from "../utils";
interface PublishLibraryDataParams { interface PublishLibraryDataParams {
authorName: string; authorName: string;
@@ -55,6 +58,75 @@ const importPublishLibDataFromStorage = () => {
return null; return null;
}; };
const generatePreviewImage = async (libraryItems: LibraryItems) => {
const MAX_ITEMS_PER_ROW = 6;
const BOX_SIZE = 128;
const BOX_PADDING = Math.round(BOX_SIZE / 16);
const BORDER_WIDTH = Math.max(Math.round(BOX_SIZE / 64), 2);
const rows = chunk(libraryItems, MAX_ITEMS_PER_ROW);
const canvas = document.createElement("canvas");
canvas.width =
rows[0].length * BOX_SIZE +
(rows[0].length + 1) * (BOX_PADDING * 2) -
BOX_PADDING * 2;
canvas.height =
rows.length * BOX_SIZE +
(rows.length + 1) * (BOX_PADDING * 2) -
BOX_PADDING * 2;
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = OpenColor.white;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw items
// ---------------------------------------------------------------------------
for (const [index, item] of libraryItems.entries()) {
const itemCanvas = await exportToCanvas({
elements: item.elements,
files: null,
maxWidthOrHeight: BOX_SIZE,
});
const { width, height } = itemCanvas;
// draw item
// -------------------------------------------------------------------------
const rowOffset =
Math.floor(index / MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
const colOffset =
(index % MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
ctx.drawImage(
itemCanvas,
colOffset + (BOX_SIZE - width) / 2 + BOX_PADDING,
rowOffset + (BOX_SIZE - height) / 2 + BOX_PADDING,
);
// draw item border
// -------------------------------------------------------------------------
ctx.lineWidth = BORDER_WIDTH;
ctx.strokeStyle = OpenColor.gray[4];
ctx.strokeRect(
colOffset + BOX_PADDING / 2,
rowOffset + BOX_PADDING / 2,
BOX_SIZE + BOX_PADDING,
BOX_SIZE + BOX_PADDING,
);
}
return await resizeImageFile(
new File([await canvasToBlob(canvas)], "preview", { type: MIME_TYPES.png }),
{
outputType: MIME_TYPES.jpg,
maxWidthOrHeight: 5000,
},
);
};
const PublishLibrary = ({ const PublishLibrary = ({
onClose, onClose,
libraryItems, libraryItems,
@@ -129,59 +201,12 @@ const PublishLibrary = ({
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
const elements: ExcalidrawElement[] = [];
const prevBoundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
clonedLibItems.forEach((libItem) => {
const boundingBox = getCommonBoundingBox(libItem.elements);
const width = boundingBox.maxX - boundingBox.minX + 30;
const height = boundingBox.maxY - boundingBox.minY + 30;
const offset = {
x: prevBoundingBox.maxX - boundingBox.minX,
y: prevBoundingBox.maxY - boundingBox.minY,
};
const itemsWithUpdatedCoords = libItem.elements.map((element) => { const previewImage = await generatePreviewImage(clonedLibItems);
element = mutateElement(element, {
x: element.x + offset.x + 15,
y: element.y + offset.y + 15,
});
return element;
});
const items = [
...itemsWithUpdatedCoords,
newElement({
type: "rectangle",
width,
height,
x: prevBoundingBox.maxX,
y: prevBoundingBox.maxY,
strokeColor: "#ced4da",
backgroundColor: "transparent",
strokeStyle: "solid",
opacity: 100,
roughness: 0,
strokeSharpness: "sharp",
fillStyle: "solid",
strokeWidth: 1,
}),
];
elements.push(...items);
prevBoundingBox.maxX = prevBoundingBox.maxX + width + 30;
});
const png = await exportToBlob({
elements,
mimeType: "image/png",
appState: {
...appState,
viewBackgroundColor: oc.white,
exportBackground: true,
},
files: null,
});
const libContent: ExportedLibraryData = { const libContent: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary, type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 2, version: VERSIONS.excalidrawLibrary,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
libraryItems: clonedLibItems, libraryItems: clonedLibItems,
}; };
@@ -190,7 +215,8 @@ const PublishLibrary = ({
const formData = new FormData(); const formData = new FormData();
formData.append("excalidrawLib", lib); formData.append("excalidrawLib", lib);
formData.append("excalidrawPng", png!); formData.append("previewImage", previewImage);
formData.append("previewImageType", previewImage.type);
formData.append("title", libraryData.name); formData.append("title", libraryData.name);
formData.append("authorName", libraryData.authorName); formData.append("authorName", libraryData.authorName);
formData.append("githubHandle", libraryData.githubHandle); formData.append("githubHandle", libraryData.githubHandle);
+21 -28
View File
@@ -8,17 +8,7 @@
position: relative; position: relative;
cursor: pointer; cursor: pointer;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor);
user-select: none; user-select: none;
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
} }
.ToolIcon--plain { .ToolIcon--plain {
@@ -29,6 +19,20 @@
} }
} }
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon {
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
}
}
.ToolIcon__icon { .ToolIcon__icon {
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
@@ -38,7 +42,11 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: var(--space-factor); border-radius: var(--border-radius-lg);
& + .ToolIcon__label {
margin-inline-start: 0;
}
svg { svg {
position: relative; position: relative;
@@ -46,10 +54,6 @@
fill: var(--icon-fill-color); fill: var(--icon-fill-color);
color: var(--icon-fill-color); color: var(--icon-fill-color);
} }
& + .ToolIcon__label {
margin-inline-start: 0;
}
} }
.ToolIcon__label { .ToolIcon__label {
@@ -79,7 +83,7 @@
margin: 0; margin: 0;
font-size: inherit; font-size: inherit;
&:focus { &:focus-visible {
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -121,7 +125,7 @@
} }
} }
&:focus + .ToolIcon__icon { &:focus-visible + .ToolIcon__icon {
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -141,10 +145,6 @@
background-color: transparent; background-color: transparent;
} }
&:focus {
box-shadow: none;
}
.ToolIcon__icon { .ToolIcon__icon {
background-color: var(--button-gray-1); background-color: var(--button-gray-1);
&:hover { &:hover {
@@ -159,13 +159,6 @@
} }
} }
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__keybinding { .ToolIcon__keybinding {
position: absolute; position: absolute;
bottom: 2px; bottom: 2px;
+112
View File
@@ -0,0 +1,112 @@
@import "open-color/open-color.scss";
@mixin toolbarButtonColorStates {
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon:active {
background: var(--color-primary-light);
}
&:checked + .ToolIcon__icon {
background: var(--color-primary);
--icon-fill-color: #{$oc-white};
--keybinding-color: #{$oc-white};
}
&:checked + .ToolIcon__icon:active {
background: var(--color-primary-darker);
}
}
.ToolIcon__keybinding {
bottom: 4px;
right: 4px;
}
}
.excalidraw {
.App-toolbar-container {
.ToolIcon_type_floating {
@include toolbarButtonColorStates;
&:not(.is-mobile) {
.ToolIcon__icon {
padding: 1px;
background-color: var(--island-bg-color);
box-shadow: 1px 3px 4px 0px rgb(0 0 0 / 15%);
border-radius: 50%;
transition: box-shadow 0.5s ease, transform 0.5s ease;
}
}
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
&:focus-within + .ToolIcon__icon {
// override for custom floating button shadow
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
}
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__library {
margin-inline-start: var(--space-factor);
}
&.zen-mode {
.ToolIcon_type_floating {
.ToolIcon__icon {
box-shadow: none;
transform: scale(0.9);
}
.ToolIcon_type_checkbox:not(:checked):not(:hover):not(:active) {
& + .ToolIcon__icon {
svg {
fill: $oc-gray-5;
color: $oc-gray-5;
}
}
}
}
}
}
.App-toolbar {
border-radius: var(--border-radius-lg);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 15%);
.ToolIcon {
&:hover {
--icon-fill-color: var(--color-primary-chubb);
--keybinding-color: var(--color-primary-chubb);
}
&:active {
--icon-fill-color: #{$oc-gray-9};
--keybinding-color: #{$oc-gray-9};
}
.ToolIcon__icon {
background: transparent;
border-radius: var(--border-radius-lg);
}
@include toolbarButtonColorStates;
}
&.zen-mode {
.ToolIcon__keybinding,
.HintViewer {
display: none;
}
}
}
&.theme--dark .App-toolbar .ToolIcon:active {
--icon-fill-color: #{$oc-gray-3};
--keybinding-color: #{$oc-gray-3};
}
}
-1
View File
@@ -29,7 +29,6 @@
// wraps the element we want to apply the tooltip to // wraps the element we want to apply the tooltip to
.excalidraw-tooltip-wrapper { .excalidraw-tooltip-wrapper {
display: flex; display: flex;
height: 100%;
} }
.excalidraw-tooltip-icon { .excalidraw-tooltip-icon {
+8 -1
View File
@@ -62,9 +62,15 @@ type TooltipProps = {
children: React.ReactNode; children: React.ReactNode;
label: string; label: string;
long?: boolean; long?: boolean;
style?: React.CSSProperties;
}; };
export const Tooltip = ({ children, label, long = false }: TooltipProps) => { export const Tooltip = ({
children,
label,
long = false,
style,
}: TooltipProps) => {
useEffect(() => { useEffect(() => {
return () => return () =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible"); getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
@@ -84,6 +90,7 @@ export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
onPointerLeave={() => onPointerLeave={() =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible") getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
} }
style={style}
> >
{children} {children}
</div> </div>
+4
View File
@@ -7,6 +7,10 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
&:empty {
display: none;
}
} }
.UserList > * { .UserList > * {
+11 -2
View File
@@ -15,8 +15,9 @@ import { THEME } from "../constants";
const activeElementColor = (theme: Theme) => const activeElementColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9]; theme === THEME.LIGHT ? oc.orange[4] : oc.orange[9];
const iconFillColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.black : oc.gray[4]; const iconFillColor = (theme: Theme) => "var(--icon-fill-color)";
const handlerColor = (theme: Theme) => const handlerColor = (theme: Theme) =>
theme === THEME.LIGHT ? oc.white : "#1e1e1e"; theme === THEME.LIGHT ? oc.white : "#1e1e1e";
@@ -88,6 +89,14 @@ export const trash = createIcon(
{ width: 448, height: 512 }, { width: 448, height: 512 },
); );
export const backgroundIcon = createIcon(
<path
fill="currentColor"
d="M512 320s-64 92.65-64 128c0 35.35 28.66 64 64 64s64-28.65 64-64-64-128-64-128zm-9.37-102.94L294.94 9.37C288.69 3.12 280.5 0 272.31 0s-16.38 3.12-22.62 9.37l-81.58 81.58L81.93 4.76c-6.25-6.25-16.38-6.25-22.62 0L36.69 27.38c-6.24 6.25-6.24 16.38 0 22.62l86.19 86.18-94.76 94.76c-37.49 37.48-37.49 98.26 0 135.75l117.19 117.19c18.74 18.74 43.31 28.12 67.87 28.12 24.57 0 49.13-9.37 67.87-28.12l221.57-221.57c12.5-12.5 12.5-32.75.01-45.25zm-116.22 70.97H65.93c1.36-3.84 3.57-7.98 7.43-11.83l13.15-13.15 81.61-81.61 58.6 58.6c12.49 12.49 32.75 12.49 45.24 0s12.49-32.75 0-45.24l-58.6-58.6 58.95-58.95 162.44 162.44-48.34 48.34z"
></path>,
{ width: 576, height: 512 },
);
export const palette = createIcon( export const palette = createIcon(
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z", "M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",
+7
View File
@@ -176,3 +176,10 @@ export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg"; export const SVG_NS = "http://www.w3.org/2000/svg";
export const ENCRYPTION_KEY_BITS = 128; export const ENCRYPTION_KEY_BITS = 128;
export const VERSIONS = {
excalidraw: 2,
excalidrawLibrary: 2,
} as const;
export const PADDING = 30;
+10 -8
View File
@@ -180,7 +180,7 @@
} }
.buttonList label:focus-within, .buttonList label:focus-within,
input:focus { input:focus-visible {
outline: transparent; outline: transparent;
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -190,14 +190,14 @@
user-select: none; user-select: none;
background-color: var(--button-gray-1); background-color: var(--button-gray-1);
border: 0; border: 0;
border-radius: 4px; border-radius: var(--border-radius-md);
margin: 0.125rem 0; margin: 0.125rem 0;
padding: 0.25rem; padding: 0.25rem;
white-space: nowrap; white-space: nowrap;
cursor: pointer; cursor: pointer;
&:focus { &:focus-visible {
outline: transparent; outline: transparent;
box-shadow: 0 0 0 2px var(--focus-highlight-color); box-shadow: 0 0 0 2px var(--focus-highlight-color);
} }
@@ -217,14 +217,16 @@
.active, .active,
.buttonList label.active { .buttonList label.active {
background-color: var(--button-gray-2); background-color: var(--color-primary);
--icon-fill-color: #{$oc-white};
&:hover { &:hover {
background-color: var(--button-gray-2); background-color: var(--color-primary-darker);
} }
&:active { &:active {
background-color: var(--button-gray-3); background-color: var(--color-primary-darkest);
} }
} }
@@ -234,7 +236,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
svg { svg {
width: 36px; width: 35px;
height: 14px; height: 14px;
padding: 2px; padding: 2px;
opacity: 0.6; opacity: 0.6;
@@ -311,7 +313,7 @@
} }
.App-menu_top { .App-menu_top {
grid-template-columns: 1fr auto 1fr; grid-template-columns: auto max-content auto;
grid-gap: 4px; grid-gap: 4px;
align-items: flex-start; align-items: flex-start;
cursor: default; cursor: default;
+19 -3
View File
@@ -12,7 +12,7 @@
--dialog-border-color: #{$oc-gray-6}; --dialog-border-color: #{$oc-gray-6};
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>'); --dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-2}; --focus-highlight-color: #{$oc-blue-2};
--icon-fill-color: #{$oc-black}; --icon-fill-color: #{$oc-gray-9};
--icon-green-fill-color: #{$oc-green-9}; --icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white}; --default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white}; --input-bg-color: #{$oc-white};
@@ -32,10 +32,20 @@
--sar: env(safe-area-inset-right); --sar: env(safe-area-inset-right);
--sat: env(safe-area-inset-top); --sat: env(safe-area-inset-top);
--select-highlight-color: #{$oc-blue-5}; --select-highlight-color: #{$oc-blue-5};
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)}; --shadow-island: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 12%);
--space-factor: 0.25rem; --space-factor: 0.25rem;
--text-primary-color: #{$oc-gray-8}; --text-primary-color: #{$oc-gray-8};
--color-primary: #6965db;
--color-primary-chubb: #625ee0; // to offset Chubb illusion
--color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1;
--color-primary-light: #e2e1fc;
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
&.theme--dark { &.theme--dark {
background: $oc-black; background: $oc-black;
@@ -71,7 +81,13 @@
--popup-text-color: #{$oc-gray-4}; --popup-text-color: #{$oc-gray-4};
--popup-text-inverted-color: #2c2c2c; --popup-text-inverted-color: #2c2c2c;
--select-highlight-color: #{$oc-blue-4}; --select-highlight-color: #{$oc-blue-4};
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)}; --shadow-island: 1px 1px 5px #{transparentize($oc-black, 0.7)};
--text-primary-color: #{$oc-gray-4}; --text-primary-color: #{$oc-gray-4};
--color-primary: #5650f0;
--color-primary-chubb: #726dff; // to offset Chubb illusion
--color-primary-darker: #4b46d8;
--color-primary-darkest: #3e39be;
--color-primary-light: #3f3d64;
} }
} }
+18 -4
View File
@@ -237,7 +237,11 @@ export const dataURLToFile = (dataURL: DataURL, filename = "") => {
export const resizeImageFile = async ( export const resizeImageFile = async (
file: File, file: File,
maxWidthOrHeight: number, opts: {
/** undefined indicates auto */
outputType?: typeof MIME_TYPES["jpg"];
maxWidthOrHeight: number;
},
): Promise<File> => { ): Promise<File> => {
// SVG files shouldn't a can't be resized // SVG files shouldn't a can't be resized
if (file.type === MIME_TYPES.svg) { if (file.type === MIME_TYPES.svg) {
@@ -257,16 +261,26 @@ export const resizeImageFile = async (
pica: pica({ features: ["js", "wasm"] }), pica: pica({ features: ["js", "wasm"] }),
}); });
const fileType = file.type; if (opts.outputType) {
const { outputType } = opts;
reduce._create_blob = function (env) {
return this.pica.toBlob(env.out_canvas, outputType, 0.8).then((blob) => {
env.out_blob = blob;
return env;
});
};
}
if (!isSupportedImageFile(file)) { if (!isSupportedImageFile(file)) {
throw new Error(t("errors.unsupportedFileType")); throw new Error(t("errors.unsupportedFileType"));
} }
return new File( return new File(
[await reduce.toBlob(file, { max: maxWidthOrHeight })], [await reduce.toBlob(file, { max: opts.maxWidthOrHeight })],
file.name, file.name,
{ type: fileType }, {
type: opts.outputType || file.type,
},
); );
}; };
+13 -1
View File
@@ -234,7 +234,19 @@ const splitBuffers = (concatenatedBuffer: Uint8Array) => {
let cursor = 0; let cursor = 0;
// first chunk is the version (ignored for now) // first chunk is the version
const version = dataView(
concatenatedBuffer,
NEXT_CHUNK_SIZE_DATAVIEW_BYTES,
cursor,
);
// If version is outside of the supported versions, throw an error.
// This usually means the buffer wasn't encoded using this API, so we'd only
// waste compute.
if (version > CONCAT_BUFFERS_VERSION) {
throw new Error(`invalid version ${version}`);
}
cursor += VERSION_DATAVIEW_BYTES; cursor += VERSION_DATAVIEW_BYTES;
while (true) { while (true) {
+8 -3
View File
@@ -1,6 +1,11 @@
import { fileOpen, fileSave } from "./filesystem"; import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState"; import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants"; import {
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
} from "../constants";
import { clearElementsForDatabase, clearElementsForExport } from "../element"; import { clearElementsForDatabase, clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState, BinaryFiles, LibraryItems } from "../types"; import { AppState, BinaryFiles, LibraryItems } from "../types";
@@ -42,7 +47,7 @@ export const serializeAsJSON = (
): string => { ): string => {
const data: ExportedDataState = { const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw, type: EXPORT_DATA_TYPES.excalidraw,
version: 2, version: VERSIONS.excalidraw,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
elements: elements:
type === "local" type === "local"
@@ -121,7 +126,7 @@ export const isValidLibrary = (json: any) => {
export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => { export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => {
const data: ExportedLibraryData = { const data: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary, type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 2, version: VERSIONS.excalidrawLibrary,
source: EXPORT_SOURCE, source: EXPORT_SOURCE,
libraryItems, libraryItems,
}; };
+15 -9
View File
@@ -10,11 +10,7 @@ import {
NormalizedZoomValue, NormalizedZoomValue,
} from "../types"; } from "../types";
import { ImportedDataState } from "./types"; import { ImportedDataState } from "./types";
import { import { getNormalizedDimensions, isInvisiblySmallElement } from "../element";
getElementMap,
getNormalizedDimensions,
isInvisiblySmallElement,
} from "../element";
import { isLinearElementType } from "../element/typeChecks"; import { isLinearElementType } from "../element/typeChecks";
import { randomId } from "../random"; import { randomId } from "../random";
import { import {
@@ -26,6 +22,8 @@ import {
import { getDefaultAppState } from "../appState"; import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement"; import { bumpVersion } from "../element/mutateElement";
import { getUpdatedTimestamp } from "../utils";
import { arrayToMap } from "../utils";
type RestoredAppState = Omit< type RestoredAppState = Omit<
AppState, AppState,
@@ -66,7 +64,10 @@ const restoreElementWithProperties = <
T extends ExcalidrawElement, T extends ExcalidrawElement,
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>, K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>( >(
element: Required<T>, element: Required<T> & {
/** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][];
},
extra: Pick< extra: Pick<
T, T,
// This extra Pick<T, keyof K> ensure no excess properties are passed. // This extra Pick<T, keyof K> ensure no excess properties are passed.
@@ -100,7 +101,10 @@ const restoreElementWithProperties = <
strokeSharpness: strokeSharpness:
element.strokeSharpness ?? element.strokeSharpness ??
(isLinearElementType(element.type) ? "round" : "sharp"), (isLinearElementType(element.type) ? "round" : "sharp"),
boundElementIds: element.boundElementIds ?? [], boundElements: element.boundElementIds
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
: element.boundElements ?? [],
updated: element.updated ?? getUpdatedTimestamp(),
}; };
return { return {
@@ -131,6 +135,8 @@ const restoreElement = (
baseline: element.baseline, baseline: element.baseline,
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN, textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN, verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
containerId: element.containerId ?? null,
originalText: element.originalText ?? "",
}); });
case "freedraw": { case "freedraw": {
return restoreElementWithProperties(element, { return restoreElementWithProperties(element, {
@@ -204,14 +210,14 @@ export const restoreElements = (
/** NOTE doesn't serve for reconciliation */ /** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined, localElements: readonly ExcalidrawElement[] | null | undefined,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const localElementsMap = localElements ? getElementMap(localElements) : null; const localElementsMap = localElements ? arrayToMap(localElements) : null;
return (elements || []).reduce((elements, element) => { return (elements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained // and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) { if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement(element); let migratedElement: ExcalidrawElement | null = restoreElement(element);
if (migratedElement) { if (migratedElement) {
const localElement = localElementsMap?.[element.id]; const localElement = localElementsMap?.get(element.id);
if (localElement && localElement.version > migratedElement.version) { if (localElement && localElement.version > migratedElement.version) {
migratedElement = bumpVersion(migratedElement, localElement.version); migratedElement = bumpVersion(migratedElement, localElement.version);
} }
+2 -1
View File
@@ -1,6 +1,7 @@
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types"; import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types";
import type { cleanAppStateForExport } from "../appState"; import type { cleanAppStateForExport } from "../appState";
import { VERSIONS } from "../constants";
export interface ExportedDataState { export interface ExportedDataState {
type: string; type: string;
@@ -24,7 +25,7 @@ export interface ImportedDataState {
export interface ExportedLibraryData { export interface ExportedLibraryData {
type: string; type: string;
version: 2; version: typeof VERSIONS.excalidrawLibrary;
source: string; source: string;
libraryItems: LibraryItems; libraryItems: LibraryItems;
} }
+90 -65
View File
@@ -8,7 +8,11 @@ import {
} from "./types"; } from "./types";
import { getElementAtPosition } from "../scene"; import { getElementAtPosition } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { isBindableElement, isBindingElement } from "./typeChecks"; import {
isBindableElement,
isBindingElement,
isLinearElement,
} from "./typeChecks";
import { import {
bindingBorderTest, bindingBorderTest,
distanceToBindableElement, distanceToBindableElement,
@@ -20,7 +24,7 @@ import {
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { tupleToCoors } from "../utils"; import { arrayToMap, tupleToCoors } from "../utils";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
export type SuggestedBinding = export type SuggestedBinding =
@@ -74,8 +78,9 @@ export const bindOrUnbindLinearElement = (
.getNonDeletedElements(onlyUnbound) .getNonDeletedElements(onlyUnbound)
.forEach((element) => { .forEach((element) => {
mutateElement(element, { mutateElement(element, {
boundElementIds: element.boundElementIds?.filter( boundElements: element.boundElements?.filter(
(id) => id !== linearElement.id, (element) =>
element.type !== "arrow" || element.id !== linearElement.id,
), ),
}); });
}); });
@@ -180,11 +185,16 @@ const bindLinearElement = (
...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd), ...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd),
} as PointBinding, } as PointBinding,
}); });
mutateElement(hoveredElement, {
boundElementIds: Array.from( const boundElementsMap = arrayToMap(hoveredElement.boundElements || []);
new Set([...(hoveredElement.boundElementIds ?? []), linearElement.id]), if (!boundElementsMap.has(linearElement.id)) {
), mutateElement(hoveredElement, {
}); boundElements: (hoveredElement.boundElements || []).concat({
id: linearElement.id,
type: "arrow",
}),
});
}
}; };
// Don't bind both ends of a simple segment // Don't bind both ends of a simple segment
@@ -284,52 +294,56 @@ export const updateBoundElements = (
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
}, },
) => { ) => {
const boundElementIds = changedElement.boundElementIds ?? []; const boundLinearElements = (changedElement.boundElements ?? []).filter(
if (boundElementIds.length === 0) { (el) => el.type === "arrow",
);
if (boundLinearElements.length === 0) {
return; return;
} }
const { newSize, simultaneouslyUpdated } = options ?? {}; const { newSize, simultaneouslyUpdated } = options ?? {};
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds( const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated, simultaneouslyUpdated,
); );
( Scene.getScene(changedElement)!
Scene.getScene(changedElement)!.getNonDeletedElements( .getNonDeletedElements(boundLinearElements.map((el) => el.id))
boundElementIds, .forEach((element) => {
) as NonDeleted<ExcalidrawLinearElement>[] if (!isLinearElement(element)) {
).forEach((linearElement) => { return;
const bindableElement = changedElement as ExcalidrawBindableElement; }
// In case the boundElementIds are stale
if (!doesNeedUpdate(linearElement, bindableElement)) { const bindableElement = changedElement as ExcalidrawBindableElement;
return; // In case the boundElements are stale
} if (!doesNeedUpdate(element, bindableElement)) {
const startBinding = maybeCalculateNewGapWhenScaling( return;
bindableElement, }
linearElement.startBinding, const startBinding = maybeCalculateNewGapWhenScaling(
newSize, bindableElement,
); element.startBinding,
const endBinding = maybeCalculateNewGapWhenScaling( newSize,
bindableElement, );
linearElement.endBinding, const endBinding = maybeCalculateNewGapWhenScaling(
newSize, bindableElement,
); element.endBinding,
// `linearElement` is being moved/scaled already, just update the binding newSize,
if (simultaneouslyUpdatedElementIds.has(linearElement.id)) { );
mutateElement(linearElement, { startBinding, endBinding }); // `linearElement` is being moved/scaled already, just update the binding
return; if (simultaneouslyUpdatedElementIds.has(element.id)) {
} mutateElement(element, { startBinding, endBinding });
updateBoundPoint( return;
linearElement, }
"start", updateBoundPoint(
startBinding, element,
changedElement as ExcalidrawBindableElement, "start",
); startBinding,
updateBoundPoint( changedElement as ExcalidrawBindableElement,
linearElement, );
"end", updateBoundPoint(
endBinding, element,
changedElement as ExcalidrawBindableElement, "end",
); endBinding,
}); changedElement as ExcalidrawBindableElement,
);
});
}; };
const doesNeedUpdate = ( const doesNeedUpdate = (
@@ -401,10 +415,17 @@ const updateBoundPoint = (
newEdgePoint = intersections[0]; newEdgePoint = intersections[0];
} }
} }
LinearElementEditor.movePoint( LinearElementEditor.movePoints(
linearElement, linearElement,
edgePointIndex, [
LinearElementEditor.pointFromAbsoluteCoords(linearElement, newEdgePoint), {
index: edgePointIndex,
point: LinearElementEditor.pointFromAbsoluteCoords(
linearElement,
newEdgePoint,
),
},
],
{ [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding }, { [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding },
); );
}; };
@@ -552,11 +573,11 @@ export const fixBindingsAfterDuplication = (
const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set(); const allBindableElementIds: Set<ExcalidrawElement["id"]> = new Set();
const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld"; const shouldReverseRoles = duplicatesServeAsOld === "duplicatesServeAsOld";
oldElements.forEach((oldElement) => { oldElements.forEach((oldElement) => {
const { boundElementIds } = oldElement; const { boundElements } = oldElement;
if (boundElementIds != null && boundElementIds.length > 0) { if (boundElements != null && boundElements.length > 0) {
boundElementIds.forEach((boundElementId) => { boundElements.forEach((boundElement) => {
if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElementId)) { if (shouldReverseRoles && !oldIdToDuplicatedId.has(boundElement.id)) {
allBoundElementIds.add(boundElementId); allBoundElementIds.add(boundElement.id);
} }
}); });
allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!); allBindableElementIds.add(oldIdToDuplicatedId.get(oldElement.id)!);
@@ -600,12 +621,16 @@ export const fixBindingsAfterDuplication = (
sceneElements sceneElements
.filter(({ id }) => allBindableElementIds.has(id)) .filter(({ id }) => allBindableElementIds.has(id))
.forEach((bindableElement) => { .forEach((bindableElement) => {
const { boundElementIds } = bindableElement; const { boundElements } = bindableElement;
if (boundElementIds != null && boundElementIds.length > 0) { if (boundElements != null && boundElements.length > 0) {
mutateElement(bindableElement, { mutateElement(bindableElement, {
boundElementIds: boundElementIds.map( boundElements: boundElements.map((boundElement) =>
(boundElementId) => oldIdToDuplicatedId.has(boundElement.id)
oldIdToDuplicatedId.get(boundElementId) ?? boundElementId, ? {
id: oldIdToDuplicatedId.get(boundElement.id)!,
type: boundElement.type,
}
: boundElement,
), ),
}); });
} }
@@ -638,9 +663,9 @@ export const fixBindingsAfterDeletion = (
const boundElementIds: Set<ExcalidrawElement["id"]> = new Set(); const boundElementIds: Set<ExcalidrawElement["id"]> = new Set();
deletedElements.forEach((deletedElement) => { deletedElements.forEach((deletedElement) => {
if (isBindableElement(deletedElement)) { if (isBindableElement(deletedElement)) {
deletedElement.boundElementIds?.forEach((id) => { deletedElement.boundElements?.forEach((element) => {
if (!deletedElementIds.has(id)) { if (!deletedElementIds.has(element.id)) {
boundElementIds.add(id); boundElementIds.add(element.id);
} }
}); });
} }
+12 -11
View File
@@ -31,7 +31,9 @@ import { Point } from "../types";
import { Drawable } from "roughjs/bin/core"; import { Drawable } from "roughjs/bin/core";
import { AppState } from "../types"; import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement"; import { getShapeForElement } from "../renderer/renderElement";
import { isImageElement } from "./typeChecks"; import { hasBoundTextElement, isImageElement } from "./typeChecks";
import { isTextElement } from ".";
import { isTransparent } from "../utils";
const isElementDraggableFromInside = ( const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
@@ -43,9 +45,9 @@ const isElementDraggableFromInside = (
if (element.type === "freedraw") { if (element.type === "freedraw") {
return true; return true;
} }
const isDraggableFromInside =
const isDraggableFromInside = element.backgroundColor !== "transparent"; !isTransparent(element.backgroundColor) ||
(isTransparent(element.backgroundColor) && hasBoundTextElement(element));
if (element.type === "line") { if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points); return isDraggableFromInside && isPathALoop(element.points);
} }
@@ -83,19 +85,18 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
); );
}; };
const isHittingElementNotConsideringBoundingBox = ( export const isHittingElementNotConsideringBoundingBox = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
point: Point, point: Point,
): boolean => { ): boolean => {
const threshold = 10 / appState.zoom.value; const threshold = 10 / appState.zoom.value;
const check = const check = isTextElement(element)
element.type === "text" ? isStrictlyInside
? isStrictlyInside : isElementDraggableFromInside(element)
: isElementDraggableFromInside(element) ? isInsideCheck
? isInsideCheck : isNearCheck;
: isNearCheck;
return hitTestPointAgainstElement({ element, point, threshold, check }); return hitTestPointAgainstElement({ element, point, threshold, check });
}; };
+49 -18
View File
@@ -6,13 +6,13 @@ import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types"; import { NonDeletedExcalidrawElement } from "./types";
import { PointerDownState } from "../types"; import { PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement";
export const dragSelectedElements = ( export const dragSelectedElements = (
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
scene: Scene,
lockDirection: boolean = false, lockDirection: boolean = false,
distanceX: number = 0, distanceX: number = 0,
distanceY: number = 0, distanceY: number = 0,
@@ -20,30 +20,61 @@ export const dragSelectedElements = (
const [x1, y1] = getCommonBounds(selectedElements); const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 }; const offset = { x: pointerX - x1, y: pointerY - y1 };
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
let x: number; updateElementCoords(
let y: number; lockDirection,
if (lockDirection) { distanceX,
const lockX = lockDirection && distanceX < distanceY; distanceY,
const lockY = lockDirection && distanceX > distanceY; pointerDownState,
const original = pointerDownState.originalElements.get(element.id); element,
x = lockX && original ? original.x : element.x + offset.x; offset,
y = lockY && original ? original.y : element.y + offset.y; );
} else { if (!element.groupIds.length) {
x = element.x + offset.x; const boundTextElementId = getBoundTextElementId(element);
y = element.y + offset.y; if (boundTextElementId) {
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId);
updateElementCoords(
lockDirection,
distanceX,
distanceY,
pointerDownState,
textElement!,
offset,
);
}
} }
mutateElement(element, {
x,
y,
});
updateBoundElements(element, { updateBoundElements(element, {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
}); });
}); });
}; };
const updateElementCoords = (
lockDirection: boolean,
distanceX: number,
distanceY: number,
pointerDownState: PointerDownState,
element: NonDeletedExcalidrawElement,
offset: { x: number; y: number },
) => {
let x: number;
let y: number;
if (lockDirection) {
const lockX = lockDirection && distanceX < distanceY;
const lockY = lockDirection && distanceX > distanceY;
const original = pointerDownState.originalElements.get(element.id);
x = lockX && original ? original.x : element.x + offset.x;
y = lockY && original ? original.y : element.y + offset.y;
} else {
x = element.x + offset.x;
y = element.y + offset.y;
}
mutateElement(element, {
x,
y,
});
};
export const getDragOffsetXY = ( export const getDragOffsetXY = (
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
x: number, x: number,
+13
View File
@@ -109,3 +109,16 @@ export const normalizeSVG = async (SVGString: string) => {
return svg.outerHTML; return svg.outerHTML;
} }
}; };
export const imageFromImageData = (imagedata: ImageData) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = imagedata.width;
canvas.height = imagedata.height;
ctx.putImageData(imagedata, 0, 0);
const image = new Image();
const dataURL = canvas.toDataURL() as DataURL;
image.src = dataURL;
return { image, dataURL };
};
+112
View File
@@ -0,0 +1,112 @@
import { distance2d } from "../math";
import Scene from "../scene/Scene";
import {
ExcalidrawImageElement,
InitializedExcalidrawImageElement,
} from "./types";
export type EditingImageElement = {
editorType: "alpha";
elementId: ExcalidrawImageElement["id"];
origImageData: Readonly<ImageData>;
imageData: ImageData;
pointerDownState: {
screenX: number;
screenY: number;
sampledPixel: readonly [number, number, number, number] | null;
};
};
const getElement = (id: EditingImageElement["elementId"]) => {
const element = Scene.getScene(id)?.getNonDeletedElement(id);
if (element) {
return element as InitializedExcalidrawImageElement;
}
return null;
};
export class ImageEditor {
static handlePointerDown(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const imageElement = getElement(editingElement.elementId);
if (imageElement) {
if (
scenePointer.x >= imageElement.x &&
scenePointer.x <= imageElement.x + imageElement.width &&
scenePointer.y >= imageElement.y &&
scenePointer.y <= imageElement.y + imageElement.height
) {
editingElement.pointerDownState.screenX = scenePointer.x;
editingElement.pointerDownState.screenY = scenePointer.y;
const { width, height, data } = editingElement.origImageData;
const imageOffsetX = Math.round(
(scenePointer.x - imageElement.x) * (width / imageElement.width),
);
const imageOffsetY = Math.round(
(scenePointer.y - imageElement.y) * (height / imageElement.height),
);
const sampledPixel = [
data[(imageOffsetY * width + imageOffsetX) * 4 + 0],
data[(imageOffsetY * width + imageOffsetX) * 4 + 1],
data[(imageOffsetY * width + imageOffsetX) * 4 + 2],
data[(imageOffsetY * width + imageOffsetX) * 4 + 3],
] as const;
editingElement.pointerDownState.sampledPixel = sampledPixel;
}
}
}
static handlePointerMove(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const { sampledPixel } = editingElement.pointerDownState;
if (sampledPixel) {
const { screenX, screenY } = editingElement.pointerDownState;
const distance = distance2d(
scenePointer.x,
scenePointer.y,
screenX,
screenY,
);
const { width, height, data } = editingElement.origImageData;
const newImageData = new ImageData(width, height);
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
if (
Math.abs(sampledPixel[0] - data[(y * width + x) * 4 + 0]) +
Math.abs(sampledPixel[1] - data[(y * width + x) * 4 + 1]) +
Math.abs(sampledPixel[2] - data[(y * width + x) * 4 + 2]) <
distance
) {
newImageData.data[(y * width + x) * 4 + 0] = 0;
newImageData.data[(y * width + x) * 4 + 1] = 255;
newImageData.data[(y * width + x) * 4 + 2] = 0;
newImageData.data[(y * width + x) * 4 + 3] = 0;
} else {
for (let p = 0; p < 4; ++p) {
newImageData.data[(y * width + x) * 4 + p] =
data[(y * width + x) * 4 + p];
}
}
}
}
return newImageData;
}
}
static handlePointerUp(editingElement: EditingImageElement) {
editingElement.pointerDownState.sampledPixel = null;
editingElement.origImageData = editingElement.imageData;
}
}
-9
View File
@@ -59,15 +59,6 @@ export {
} from "./sizeHelpers"; } from "./sizeHelpers";
export { showSelectedShapeActions } from "./showSelectedShapeActions"; export { showSelectedShapeActions } from "./showSelectedShapeActions";
export const getElementMap = (elements: readonly ExcalidrawElement[]) =>
elements.reduce(
(acc: { [key: string]: ExcalidrawElement }, element: ExcalidrawElement) => {
acc[element.id] = element;
return acc;
},
{},
);
export const getSceneVersion = (elements: readonly ExcalidrawElement[]) => export const getSceneVersion = (elements: readonly ExcalidrawElement[]) =>
elements.reduce((acc, el) => acc + el.version, 0); elements.reduce((acc, el) => acc + el.version, 0);
+417 -102
View File
@@ -25,11 +25,19 @@ export class LinearElementEditor {
public elementId: ExcalidrawElement["id"] & { public elementId: ExcalidrawElement["id"] & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
public activePointIndex: number | null; /** indices */
public selectedPointsIndices: readonly number[] | null;
public pointerDownState: Readonly<{
prevSelectedPointsIndices: readonly number[] | null;
/** index */
lastClickedPoint: number;
}>;
/** whether you're dragging a point */ /** whether you're dragging a point */
public isDragging: boolean; public isDragging: boolean;
public lastUncommittedPoint: Point | null; public lastUncommittedPoint: Point | null;
public pointerOffset: { x: number; y: number }; public pointerOffset: Readonly<{ x: number; y: number }>;
public startBindingElement: ExcalidrawBindableElement | null | "keep"; public startBindingElement: ExcalidrawBindableElement | null | "keep";
public endBindingElement: ExcalidrawBindableElement | null | "keep"; public endBindingElement: ExcalidrawBindableElement | null | "keep";
@@ -40,12 +48,16 @@ export class LinearElementEditor {
Scene.mapElementToScene(this.elementId, scene); Scene.mapElementToScene(this.elementId, scene);
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element);
this.activePointIndex = null; this.selectedPointsIndices = null;
this.lastUncommittedPoint = null; this.lastUncommittedPoint = null;
this.isDragging = false; this.isDragging = false;
this.pointerOffset = { x: 0, y: 0 }; this.pointerOffset = { x: 0, y: 0 };
this.startBindingElement = "keep"; this.startBindingElement = "keep";
this.endBindingElement = "keep"; this.endBindingElement = "keep";
this.pointerDownState = {
prevSelectedPointsIndices: null,
lastClickedPoint: -1,
};
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -66,6 +78,58 @@ export class LinearElementEditor {
return null; return null;
} }
static handleBoxSelection(
event: PointerEvent,
appState: AppState,
setState: React.Component<any, AppState>["setState"],
) {
if (
!appState.editingLinearElement ||
appState.draggingElement?.type !== "selection"
) {
return false;
}
const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return false;
}
const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(appState.draggingElement);
const pointsSceneCoords =
LinearElementEditor.getPointsGlobalCoordinates(element);
const nextSelectedPoints = pointsSceneCoords.reduce(
(acc: number[], point, index) => {
if (
(point[0] >= selectionX1 &&
point[0] <= selectionX2 &&
point[1] >= selectionY1 &&
point[1] <= selectionY2) ||
(event.shiftKey && selectedPointsIndices?.includes(index))
) {
acc.push(index);
}
return acc;
},
[],
);
setState({
editingLinearElement: {
...editingLinearElement,
selectedPointsIndices: nextSelectedPoints.length
? nextSelectedPoints
: null,
},
});
}
/** @returns whether point was dragged */ /** @returns whether point was dragged */
static handlePointDragging( static handlePointDragging(
appState: AppState, appState: AppState,
@@ -74,21 +138,27 @@ export class LinearElementEditor {
scenePointerY: number, scenePointerY: number,
maybeSuggestBinding: ( maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", pointSceneCoords: { x: number; y: number }[],
) => void, ) => void,
): boolean { ): boolean {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return false; return false;
} }
const { editingLinearElement } = appState; const { editingLinearElement } = appState;
const { activePointIndex, elementId, isDragging } = editingLinearElement; const { selectedPointsIndices, elementId, isDragging } =
editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId);
if (!element) { if (!element) {
return false; return false;
} }
if (activePointIndex != null && activePointIndex > -1) { // point that's being dragged (out of all selected points)
const draggingPoint = element.points[
editingLinearElement.pointerDownState.lastClickedPoint
] as [number, number] | undefined;
if (selectedPointsIndices && draggingPoint) {
if (isDragging === false) { if (isDragging === false) {
setState({ setState({
editingLinearElement: { editingLinearElement: {
@@ -98,18 +168,79 @@ export class LinearElementEditor {
}); });
} }
const newPoint = LinearElementEditor.createPointAt( const newDraggingPointPosition = LinearElementEditor.createPointAt(
element, element,
scenePointerX - editingLinearElement.pointerOffset.x, scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y, scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize, appState.gridSize,
); );
LinearElementEditor.movePoint(element, activePointIndex, newPoint);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize,
)
: ([
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
] as const);
return {
index: pointIndex,
point: newPointPosition,
isDragging:
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint,
};
}),
);
// suggest bindings for first and last point if selected
if (isBindingElement(element)) { if (isBindingElement(element)) {
maybeSuggestBinding(element, activePointIndex === 0 ? "start" : "end"); const coords: { x: number; y: number }[] = [];
const firstSelectedIndex = selectedPointsIndices[0];
if (firstSelectedIndex === 0) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[0],
),
),
);
}
const lastSelectedIndex =
selectedPointsIndices[selectedPointsIndices.length - 1];
if (lastSelectedIndex === element.points.length - 1) {
coords.push(
tupleToCoors(
LinearElementEditor.getPointGlobalCoordinates(
element,
element.points[lastSelectedIndex],
),
),
);
}
if (coords.length) {
maybeSuggestBinding(element, coords);
}
} }
return true; return true;
} }
return false; return false;
} }
@@ -118,45 +249,79 @@ export class LinearElementEditor {
editingLinearElement: LinearElementEditor, editingLinearElement: LinearElementEditor,
appState: AppState, appState: AppState,
): LinearElementEditor { ): LinearElementEditor {
const { elementId, activePointIndex, isDragging } = editingLinearElement; const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId);
if (!element) { if (!element) {
return editingLinearElement; return editingLinearElement;
} }
let binding = {}; const bindings: Partial<
if ( Pick<
isDragging && InstanceType<typeof LinearElementEditor>,
(activePointIndex === 0 || activePointIndex === element.points.length - 1) "startBindingElement" | "endBindingElement"
) { >
if (isPathALoop(element.points, appState.zoom.value)) { > = {};
LinearElementEditor.movePoint(
element, if (isDragging && selectedPointsIndices) {
activePointIndex, for (const selectedPoint of selectedPointsIndices) {
activePointIndex === 0 if (
? element.points[element.points.length - 1] selectedPoint === 0 ||
: element.points[0], selectedPoint === element.points.length - 1
); ) {
if (isPathALoop(element.points, appState.zoom.value)) {
LinearElementEditor.movePoints(element, [
{
index: selectedPoint,
point:
selectedPoint === 0
? element.points[element.points.length - 1]
: element.points[0],
},
]);
}
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
selectedPoint!,
),
),
Scene.getScene(element)!,
)
: null;
bindings[
selectedPoint === 0 ? "startBindingElement" : "endBindingElement"
] = bindingElement;
}
} }
const bindingElement = isBindingEnabled(appState)
? getHoveredElementForBinding(
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
activePointIndex!,
),
),
Scene.getScene(element)!,
)
: null;
binding = {
[activePointIndex === 0 ? "startBindingElement" : "endBindingElement"]:
bindingElement,
};
} }
return { return {
...editingLinearElement, ...editingLinearElement,
...binding, ...bindings,
// if clicking without previously dragging a point(s), and not holding
// shift, deselect all points except the one clicked. If holding shift,
// toggle the point.
selectedPointsIndices:
isDragging || event.shiftKey
? !isDragging &&
event.shiftKey &&
pointerDownState.prevSelectedPointsIndices?.includes(
pointerDownState.lastClickedPoint,
)
? selectedPointsIndices &&
selectedPointsIndices.filter(
(pointIndex) =>
pointIndex !== pointerDownState.lastClickedPoint,
)
: selectedPointsIndices
: selectedPointsIndices?.includes(pointerDownState.lastClickedPoint)
? [pointerDownState.lastClickedPoint]
: selectedPointsIndices,
isDragging: false, isDragging: false,
pointerOffset: { x: 0, y: 0 }, pointerOffset: { x: 0, y: 0 },
}; };
@@ -206,7 +371,12 @@ export class LinearElementEditor {
setState({ setState({
editingLinearElement: { editingLinearElement: {
...appState.editingLinearElement, ...appState.editingLinearElement,
activePointIndex: element.points.length - 1, pointerDownState: {
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: -1,
},
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null, lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding( endBindingElement: getHoveredElementForBinding(
scenePointer, scenePointer,
@@ -259,10 +429,28 @@ export class LinearElementEditor {
element.angle, element.angle,
); );
const nextSelectedPointsIndices =
clickedPointIndex > -1 || event.shiftKey
? event.shiftKey ||
appState.editingLinearElement.selectedPointsIndices?.includes(
clickedPointIndex,
)
? normalizeSelectedPoints([
...(appState.editingLinearElement.selectedPointsIndices || []),
clickedPointIndex,
])
: [clickedPointIndex]
: null;
setState({ setState({
editingLinearElement: { editingLinearElement: {
...appState.editingLinearElement, ...appState.editingLinearElement,
activePointIndex: clickedPointIndex > -1 ? clickedPointIndex : null, pointerDownState: {
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint pointerOffset: targetPoint
? { ? {
x: scenePointer.x - targetPoint[0], x: scenePointer.x - targetPoint[0],
@@ -292,7 +480,7 @@ export class LinearElementEditor {
if (!event.altKey) { if (!event.altKey) {
if (lastPoint === lastUncommittedPoint) { if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.movePoint(element, points.length - 1, "delete"); LinearElementEditor.deletePoints(element, [points.length - 1]);
} }
return { ...editingLinearElement, lastUncommittedPoint: null }; return { ...editingLinearElement, lastUncommittedPoint: null };
} }
@@ -305,13 +493,14 @@ export class LinearElementEditor {
); );
if (lastPoint === lastUncommittedPoint) { if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.movePoint( LinearElementEditor.movePoints(element, [
element, {
element.points.length - 1, index: element.points.length - 1,
newPoint, point: newPoint,
); },
]);
} else { } else {
LinearElementEditor.movePoint(element, "new", newPoint); LinearElementEditor.addPoints(element, [{ point: newPoint }]);
} }
return { return {
@@ -320,6 +509,21 @@ export class LinearElementEditor {
}; };
} }
/** scene coords */
static getPointGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>,
point: Point,
) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
let { x, y } = element;
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
return [x, y] as const;
}
/** scene coords */
static getPointsGlobalCoordinates( static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
) { ) {
@@ -439,22 +643,122 @@ export class LinearElementEditor {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
} }
static movePointByOffset( static duplicateSelectedPoints(appState: AppState) {
element: NonDeleted<ExcalidrawLinearElement>, if (!appState.editingLinearElement) {
pointIndex: number, return false;
offset: { x: number; y: number }, }
) {
const [x, y] = element.points[pointIndex]; const { selectedPointsIndices, elementId } = appState.editingLinearElement;
LinearElementEditor.movePoint(element, pointIndex, [
x + offset.x, const element = LinearElementEditor.getElement(elementId);
y + offset.y,
]); if (!element || selectedPointsIndices === null) {
return false;
}
const { points } = element;
const nextSelectedIndices: number[] = [];
let pointAddedToEnd = false;
let indexCursor = -1;
const nextPoints = points.reduce((acc: Point[], point, index) => {
++indexCursor;
acc.push(point);
const isSelected = selectedPointsIndices.includes(index);
if (isSelected) {
const nextPoint = points[index + 1];
if (!nextPoint) {
pointAddedToEnd = true;
}
acc.push(
nextPoint
? [(point[0] + nextPoint[0]) / 2, (point[1] + nextPoint[1]) / 2]
: [point[0], point[1]],
);
nextSelectedIndices.push(indexCursor + 1);
++indexCursor;
}
return acc;
}, []);
mutateElement(element, { points: nextPoints });
// temp hack to ensure the line doesn't move when adding point to the end,
// potentially expanding the bounding box
if (pointAddedToEnd) {
const lastPoint = element.points[element.points.length - 1];
LinearElementEditor.movePoints(element, [
{
index: element.points.length - 1,
point: [lastPoint[0] + 30, lastPoint[1] + 30],
},
]);
}
return {
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
selectedPointsIndices: nextSelectedIndices,
},
},
};
} }
static movePoint( static deletePoints(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
pointIndex: number | "new", pointIndices: readonly number[],
targetPosition: Point | "delete", ) {
let offsetX = 0;
let offsetY = 0;
const isDeletingOriginPoint = pointIndices.includes(0);
// if deleting first point, make the next to be [0,0] and recalculate
// positions of the rest with respect to it
if (isDeletingOriginPoint) {
const firstNonDeletedPoint = element.points.find((point, idx) => {
return !pointIndices.includes(idx);
});
if (firstNonDeletedPoint) {
offsetX = firstNonDeletedPoint[0];
offsetY = firstNonDeletedPoint[1];
}
}
const nextPoints = element.points.reduce((acc: Point[], point, idx) => {
if (!pointIndices.includes(idx)) {
acc.push(
!acc.length ? [0, 0] : [point[0] - offsetX, point[1] - offsetY],
);
}
return acc;
}, []);
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
}
static addPoints(
element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { point: Point }[],
) {
const offsetX = 0;
const offsetY = 0;
const nextPoints = [...element.points, ...targetPoints.map((x) => x.point)];
LinearElementEditor._updatePoints(element, nextPoints, offsetX, offsetY);
}
static movePoints(
element: NonDeleted<ExcalidrawLinearElement>,
targetPoints: { index: number; point: Point; isDragging?: boolean }[],
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding }, otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
) { ) {
const { points } = element; const { points } = element;
@@ -467,49 +771,50 @@ export class LinearElementEditor {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
let nextPoints: (readonly [number, number])[]; const selectedOriginPoint = targetPoints.find(({ index }) => index === 0);
if (targetPosition === "delete") {
// remove point
if (pointIndex === "new") {
throw new Error("invalid args in movePoint");
}
nextPoints = points.slice();
nextPoints.splice(pointIndex, 1);
if (pointIndex === 0) {
// if deleting first point, make the next to be [0,0] and recalculate
// positions of the rest with respect to it
offsetX = nextPoints[0][0];
offsetY = nextPoints[0][1];
nextPoints = nextPoints.map((point, idx) => {
if (idx === 0) {
return [0, 0];
}
return [point[0] - offsetX, point[1] - offsetY];
});
}
} else if (pointIndex === "new") {
nextPoints = [...points, targetPosition];
} else {
const deltaX = targetPosition[0] - points[pointIndex][0];
const deltaY = targetPosition[1] - points[pointIndex][1];
nextPoints = points.map((point, idx) => {
if (idx === pointIndex) {
if (idx === 0) {
offsetX = deltaX;
offsetY = deltaY;
return point;
}
offsetX = 0;
offsetY = 0;
return [point[0] + deltaX, point[1] + deltaY] as const; if (selectedOriginPoint) {
} offsetX =
return offsetX || offsetY selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
? ([point[0] - offsetX, point[1] - offsetY] as const) offsetY =
: point; selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
});
} }
const nextPoints = points.map((point, idx) => {
const selectedPointData = targetPoints.find((p) => p.index === idx);
if (selectedPointData) {
if (selectedOriginPoint) {
return point;
}
const deltaX =
selectedPointData.point[0] - points[selectedPointData.index][0];
const deltaY =
selectedPointData.point[1] - points[selectedPointData.index][1];
return [point[0] + deltaX, point[1] + deltaY] as const;
}
return offsetX || offsetY
? ([point[0] - offsetX, point[1] - offsetY] as const)
: point;
});
LinearElementEditor._updatePoints(
element,
nextPoints,
offsetX,
offsetY,
otherUpdates,
);
}
private static _updatePoints(
element: NonDeleted<ExcalidrawLinearElement>,
nextPoints: readonly Point[],
offsetX: number,
offsetY: number,
otherUpdates?: { startBinding?: PointBinding; endBinding?: PointBinding },
) {
const nextCoords = getElementPointsCoords( const nextCoords = getElementPointsCoords(
element, element,
nextPoints, nextPoints,
@@ -517,7 +822,7 @@ export class LinearElementEditor {
); );
const prevCoords = getElementPointsCoords( const prevCoords = getElementPointsCoords(
element, element,
points, element.points,
element.strokeSharpness || "round", element.strokeSharpness || "round",
); );
const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2; const nextCenterX = (nextCoords[0] + nextCoords[2]) / 2;
@@ -536,3 +841,13 @@ export class LinearElementEditor {
}); });
} }
} }
const normalizeSelectedPoints = (
points: (number | null)[],
): number[] | null => {
let nextPoints = [
...new Set(points.filter((p) => p !== null && p !== -1)),
] as number[];
nextPoints = nextPoints.sort((a, b) => a - b);
return nextPoints.length ? nextPoints : null;
};
+5 -1
View File
@@ -4,6 +4,7 @@ import Scene from "../scene/Scene";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { randomInteger } from "../random"; import { randomInteger } from "../random";
import { Point } from "../types"; import { Point } from "../types";
import { getUpdatedTimestamp } from "../utils";
type ElementUpdate<TElement extends ExcalidrawElement> = Omit< type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>, Partial<TElement>,
@@ -92,6 +93,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
element.version++; element.version++;
element.versionNonce = randomInteger(); element.versionNonce = randomInteger();
element.updated = getUpdatedTimestamp();
if (informMutation) { if (informMutation) {
Scene.getScene(element)?.informMutation(); Scene.getScene(element)?.informMutation();
@@ -126,13 +128,14 @@ export const newElementWith = <TElement extends ExcalidrawElement>(
return { return {
...element, ...element,
...updates, ...updates,
updated: getUpdatedTimestamp(),
version: element.version + 1, version: element.version + 1,
versionNonce: randomInteger(), versionNonce: randomInteger(),
}; };
}; };
/** /**
* Mutates element and updates `version` & `versionNonce`. * Mutates element, bumping `version`, `versionNonce`, and `updated`.
* *
* NOTE: does not trigger re-render. * NOTE: does not trigger re-render.
*/ */
@@ -142,5 +145,6 @@ export const bumpVersion = (
) => { ) => {
element.version = (version ?? element.version) + 1; element.version = (version ?? element.version) + 1;
element.versionNonce = randomInteger(); element.versionNonce = randomInteger();
element.updated = getUpdatedTimestamp();
return element; return element;
}; };
+81 -35
View File
@@ -11,23 +11,28 @@ import {
Arrowhead, Arrowhead,
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
FontFamilyValues, FontFamilyValues,
ExcalidrawRectangleElement,
} from "../element/types"; } from "../element/types";
import { measureText, getFontString } from "../utils"; import { getFontString, getUpdatedTimestamp } from "../utils";
import { randomInteger, randomId } from "../random"; import { randomInteger, randomId } from "../random";
import { newElementWith } from "./mutateElement"; import { mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups"; import { getNewGroupIdsForDuplication } from "../groups";
import { AppState } from "../types"; 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 { measureText } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import Scene from "../scene/Scene";
import { PADDING } from "../constants";
type ElementConstructorOpts = MarkOptional< type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted">, Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
| "width" | "width"
| "height" | "height"
| "angle" | "angle"
| "groupIds" | "groupIds"
| "boundElementIds" | "boundElements"
| "seed" | "seed"
| "version" | "version"
| "versionNonce" | "versionNonce"
@@ -50,32 +55,36 @@ const _newElementBase = <T extends ExcalidrawElement>(
angle = 0, angle = 0,
groupIds = [], groupIds = [],
strokeSharpness, strokeSharpness,
boundElementIds = null, boundElements = null,
...rest ...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">, }: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => ({ ) => {
id: rest.id || randomId(), const element = {
type, id: rest.id || randomId(),
x, type,
y, x,
width, y,
height, width,
angle, height,
strokeColor, angle,
backgroundColor, strokeColor,
fillStyle, backgroundColor,
strokeWidth, fillStyle,
strokeStyle, strokeWidth,
roughness, strokeStyle,
opacity, roughness,
groupIds, opacity,
strokeSharpness, groupIds,
seed: rest.seed ?? randomInteger(), strokeSharpness,
version: rest.version || 1, seed: rest.seed ?? randomInteger(),
versionNonce: rest.versionNonce ?? 0, version: rest.version || 1,
isDeleted: false as false, versionNonce: rest.versionNonce ?? 0,
boundElementIds, isDeleted: false as false,
}); boundElements,
updated: getUpdatedTimestamp(),
};
return element;
};
export const newElement = ( export const newElement = (
opts: { opts: {
@@ -113,6 +122,7 @@ export const newTextElement = (
fontFamily: FontFamilyValues; fontFamily: FontFamilyValues;
textAlign: TextAlign; textAlign: TextAlign;
verticalAlign: VerticalAlign; verticalAlign: VerticalAlign;
containerId?: ExcalidrawRectangleElement["id"];
} & ElementConstructorOpts, } & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => { ): NonDeleted<ExcalidrawTextElement> => {
const metrics = measureText(opts.text, getFontString(opts)); const metrics = measureText(opts.text, getFontString(opts));
@@ -130,6 +140,8 @@ export const newTextElement = (
width: metrics.width, width: metrics.width,
height: metrics.height, height: metrics.height,
baseline: metrics.baseline, baseline: metrics.baseline,
containerId: opts.containerId || null,
originalText: opts.text,
}, },
{}, {},
); );
@@ -146,18 +158,25 @@ const getAdjustedDimensions = (
height: number; height: number;
baseline: number; baseline: number;
} => { } => {
const maxWidth = element.containerId ? element.width : null;
const { const {
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
baseline: nextBaseline, baseline: nextBaseline,
} = measureText(nextText, getFontString(element)); } = measureText(nextText, getFontString(element), maxWidth);
const { textAlign, verticalAlign } = element; const { textAlign, verticalAlign } = element;
let x: number; let x: number;
let y: number; let y: number;
if (
if (textAlign === "center" && verticalAlign === "middle") { textAlign === "center" &&
const prevMetrics = measureText(element.text, getFontString(element)); verticalAlign === "middle" &&
!element.containerId
) {
const prevMetrics = measureText(
element.text,
getFontString(element),
maxWidth,
);
const offsets = getTextElementPositionOffsets(element, { const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width, width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height, height: nextHeight - prevMetrics.height,
@@ -194,6 +213,22 @@ const getAdjustedDimensions = (
); );
} }
// make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions
if (isBoundToContainer(element)) {
const container = Scene.getScene(element)!.getElement(element.containerId)!;
let height = container.height;
let width = container.width;
if (nextHeight > height - PADDING * 2) {
height = nextHeight + PADDING * 2;
}
if (nextWidth > width - PADDING * 2) {
width = nextWidth + PADDING * 2;
}
if (height !== container.height || width !== container.width) {
mutateElement(container, { height, width });
}
}
return { return {
width: nextWidth, width: nextWidth,
height: nextHeight, height: nextHeight,
@@ -205,12 +240,22 @@ const getAdjustedDimensions = (
export const updateTextElement = ( export const updateTextElement = (
element: ExcalidrawTextElement, element: ExcalidrawTextElement,
{ text, isDeleted }: { text: string; isDeleted?: boolean }, {
text,
isDeleted,
originalText,
}: { text: string; isDeleted?: boolean; originalText: string },
updateDimensions: boolean,
): ExcalidrawTextElement => { ): ExcalidrawTextElement => {
const dimensions = updateDimensions
? getAdjustedDimensions(element, text)
: undefined;
return newElementWith(element, { return newElementWith(element, {
text, text,
originalText,
isDeleted: isDeleted ?? element.isDeleted, isDeleted: isDeleted ?? element.isDeleted,
...getAdjustedDimensions(element, text), ...dimensions,
}); });
}; };
@@ -337,6 +382,7 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
} else { } else {
copy.id = randomId(); copy.id = randomId();
} }
copy.updated = getUpdatedTimestamp();
copy.seed = randomInteger(); copy.seed = randomInteger();
copy.groupIds = getNewGroupIdsForDuplication( copy.groupIds = getNewGroupIdsForDuplication(
copy.groupIds, copy.groupIds,
+43 -3
View File
@@ -25,7 +25,7 @@ import {
} from "./typeChecks"; } from "./typeChecks";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers"; import { getPerfectElementSize } from "./sizeHelpers";
import { measureText, getFontString } from "../utils"; import { getFontString } from "../utils";
import { updateBoundElements } from "./binding"; import { updateBoundElements } from "./binding";
import { import {
TransformHandleType, TransformHandleType,
@@ -33,6 +33,13 @@ import {
TransformHandleDirection, TransformHandleDirection,
} from "./transformHandles"; } from "./transformHandles";
import { Point, PointerDownState } from "../types"; import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineWidth,
getBoundTextElementId,
handleBindTextResize,
measureText,
} from "./textElement";
export const normalizeAngle = (angle: number): number => { export const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) { if (angle >= 2 * Math.PI) {
@@ -132,6 +139,7 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
handleBindTextResize(selectedElements, transformHandleType);
return true; return true;
} }
} }
@@ -154,6 +162,11 @@ const rotateSingleElement = (
} }
angle = normalizeAngle(angle); angle = normalizeAngle(angle);
mutateElement(element, { angle }); mutateElement(element, { angle });
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(boundTextElementId);
mutateElement(textElement!, { angle });
}
}; };
// used in DEV only // used in DEV only
@@ -272,6 +285,7 @@ const measureFontSizeFromWH = (
const metrics = measureText( const metrics = measureText(
element.text, element.text,
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }), getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
element.containerId ? element.width : null,
); );
return { return {
size: nextFontSize, size: nextFontSize,
@@ -413,6 +427,9 @@ export const resizeSingleElement = (
element.width, element.width,
element.height, element.height,
); );
const boundTextElementId = getBoundTextElementId(element);
const boundsCurrentWidth = esx2 - esx1; const boundsCurrentWidth = esx2 - esx1;
const boundsCurrentHeight = esy2 - esy1; const boundsCurrentHeight = esy2 - esy1;
@@ -473,6 +490,11 @@ 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)) {
@@ -565,9 +587,16 @@ 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 !== 0 && resizedElement.width > minWidth &&
resizedElement.height !== 0 && resizedElement.height !== 0 &&
Number.isFinite(resizedElement.x) && Number.isFinite(resizedElement.x) &&
Number.isFinite(resizedElement.y) Number.isFinite(resizedElement.y)
@@ -576,6 +605,7 @@ 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);
} }
}; };
@@ -647,7 +677,7 @@ const resizeMultipleElements = (
const width = element.width * scale; const width = element.width * scale;
const height = element.height * scale; const height = element.height * scale;
let font: { fontSize?: number; baseline?: number } = {}; let font: { fontSize?: number; baseline?: number } = {};
if (element.type === "text") { if (isTextElement(element)) {
const nextFont = measureFontSizeFromWH(element, width, height); const nextFont = measureFontSizeFromWH(element, width, height);
if (nextFont === null) { if (nextFont === null) {
return null; return null;
@@ -728,6 +758,16 @@ const rotateMultipleElements = (
y: element.y + (rotatedCY - cy), y: element.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle), angle: normalizeAngle(centerAngle + origAngle),
}); });
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement =
Scene.getScene(element)!.getElement(boundTextElementId)!;
mutateElement(textElement, {
x: textElement.x + (rotatedCX - cx),
y: textElement.y + (rotatedCY - cy),
angle: normalizeAngle(centerAngle + origAngle),
});
}
}); });
}; };
+380 -3
View File
@@ -1,12 +1,389 @@
import { measureText, getFontString } from "../utils"; import { getFontString, arrayToMap } from "../utils";
import { ExcalidrawTextElement } from "./types"; import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
FontString,
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { PADDING } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => { export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
const metrics = measureText(element.text, getFontString(element)); let maxWidth;
if (element.containerId) {
maxWidth = element.width;
}
const metrics = measureText(
element.originalText,
getFontString(element),
maxWidth,
);
mutateElement(element, { mutateElement(element, {
width: metrics.width, width: metrics.width,
height: metrics.height, height: metrics.height,
baseline: metrics.baseline, baseline: metrics.baseline,
}); });
}; };
export const bindTextToShapeAfterDuplication = (
sceneElements: ExcalidrawElement[],
oldElements: ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
): void => {
const sceneElementMap = arrayToMap(sceneElements) as Map<
ExcalidrawElement["id"],
ExcalidrawElement
>;
oldElements.forEach((element) => {
const newElementId = oldIdToDuplicatedId.get(element.id) as string;
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId)!;
mutateElement(
sceneElementMap.get(newElementId) as ExcalidrawBindableElement,
{
boundElements: element.boundElements?.concat({
type: "text",
id: newTextElementId,
}),
},
);
mutateElement(
sceneElementMap.get(newTextElementId) as ExcalidrawTextElement,
{
containerId: newElementId,
},
);
}
});
};
export const handleBindTextResize = (
elements: readonly NonDeletedExcalidrawElement[],
transformHandleType: MaybeTransformHandleType,
) => {
elements.forEach((element) => {
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElement;
if (textElement && textElement.text) {
if (!element) {
return;
}
let text = textElement.text;
let nextHeight = textElement.height;
let containerHeight = element.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
let minCharWidthTillNow = 0;
if (text) {
minCharWidthTillNow = getMinCharWidth(getFontString(textElement));
// check if the diff has exceeded min char width needed
const diff = Math.abs(
element.width - textElement.width + PADDING * 2,
);
if (diff >= minCharWidthTillNow) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
}
}
const dimensions = measureText(
text,
getFontString(textElement),
element.width,
);
nextHeight = dimensions.height;
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > element.height - PADDING * 2) {
containerHeight = nextHeight + PADDING * 2;
const diff = containerHeight - element.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
transformHandleType === "ne" ||
transformHandleType === "nw" ||
transformHandleType === "n"
? element.y - diff
: element.y;
mutateElement(element, {
height: containerHeight,
y: updatedY,
});
}
const updatedY = element.y + containerHeight / 2 - nextHeight / 2;
mutateElement(textElement, {
text,
// preserve padding and set width correctly
width: element.width - PADDING * 2,
height: nextHeight,
x: element.x + PADDING,
y: updatedY,
baseline: nextBaseLine,
});
}
}
});
};
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
export const measureText = (
text: string,
font: FontString,
maxWidth?: number | null,
) => {
text = text
.split("\n")
// replace empty lines with single space because leading/trailing empty
// lines would be stripped from computation
.map((x) => x || " ")
.join("\n");
const container = document.createElement("div");
container.style.position = "absolute";
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
if (maxWidth) {
const lineHeight = getApproxLineHeight(font);
container.style.width = `${String(maxWidth)}px`;
container.style.maxWidth = `${String(maxWidth)}px`;
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.lineHeight = `${String(lineHeight)}px`;
container.style.whiteSpace = "pre-wrap";
}
document.body.appendChild(container);
container.innerText = text;
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.overflow = "hidden";
span.style.width = "1px";
span.style.height = "1px";
container.appendChild(span);
// Baseline is important for positioning text on canvas
const baseline = span.offsetTop + span.offsetHeight;
const width = container.offsetWidth;
const height = container.offsetHeight;
document.body.removeChild(container);
return { width, height, baseline };
};
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
export const getApproxLineHeight = (font: FontString) => {
return measureText(DUMMY_TEXT, font, null).height;
};
let canvas: HTMLCanvasElement | undefined;
const getTextWidth = (text: string, font: FontString) => {
if (!canvas) {
canvas = document.createElement("canvas");
}
const canvas2dContext = canvas.getContext("2d")!;
canvas2dContext.font = font;
const metrics = canvas2dContext.measureText(text);
return metrics.width;
};
export const wrapText = (
text: string,
font: FontString,
containerWidth: number,
) => {
const maxWidth = containerWidth - PADDING * 2;
const lines: Array<string> = [];
const originalLines = text.split("\n");
const spaceWidth = getTextWidth(" ", font);
originalLines.forEach((originalLine) => {
const words = originalLine.split(" ");
// This means its newline so push it
if (words.length === 1 && words[0] === "") {
lines.push(words[0]);
} else {
let currentLine = "";
let currentLineWidthTillNow = 0;
let index = 0;
while (index < words.length) {
const currentWordWidth = getTextWidth(words[index], font);
// Start breaking longer words exceeding max width
if (currentWordWidth > maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
if (currentLine) {
lines.push(currentLine);
}
currentLine = "";
currentLineWidthTillNow = 0;
while (words[index].length > 0) {
const currentChar = words[index][0];
const width = charWidth.calculate(currentChar, font);
currentLineWidthTillNow += width;
words[index] = words[index].slice(1);
if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
lines.push(currentLine);
currentLine = currentChar;
currentLineWidthTillNow = width;
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
} else {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth > maxWidth) {
lines.push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
} else {
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
// to previous line
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
index++;
} else {
// Start appending words in a line till max width reached
while (currentLineWidthTillNow < maxWidth && index < words.length) {
const word = words[index];
currentLineWidthTillNow = getTextWidth(currentLine + word, font);
if (currentLineWidthTillNow >= maxWidth) {
lines.push(currentLine);
currentLineWidthTillNow = 0;
currentLine = "";
break;
}
index++;
currentLine += `${word} `;
}
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
}
}
if (currentLine) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
lines.push(currentLine);
}
}
});
return lines.join("\n");
};
export const charWidth = (() => {
const cachedCharWidth: { [key: FontString]: Array<number> } = {};
const calculate = (char: string, font: FontString) => {
const ascii = char.charCodeAt(0);
if (!cachedCharWidth[font]) {
cachedCharWidth[font] = [];
}
if (!cachedCharWidth[font][ascii]) {
const width = getTextWidth(char, font);
cachedCharWidth[font][ascii] = width;
}
return cachedCharWidth[font][ascii];
};
const updateCache = (char: string, font: FontString) => {
const ascii = char.charCodeAt(0);
if (!cachedCharWidth[font][ascii]) {
cachedCharWidth[font][ascii] = calculate(char, font);
}
};
const clearCacheforFont = (font: FontString) => {
cachedCharWidth[font] = [];
};
const getCache = (font: FontString) => {
return cachedCharWidth[font];
};
return {
calculate,
updateCache,
clearCacheforFont,
getCache,
};
})();
export const getApproxMinLineWidth = (font: FontString) => {
return measureText(DUMMY_TEXT.split("").join("\n"), font).width + PADDING * 2;
};
export const getApproxMinLineHeight = (font: FontString) => {
return getApproxLineHeight(font) + PADDING * 2;
};
export const getMinCharWidth = (font: FontString) => {
const cache = charWidth.getCache(font);
if (!cache) {
return 0;
}
const cacheWithOutEmpty = cache.filter((val) => val !== undefined);
return Math.min(...cacheWithOutEmpty);
};
export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
// Generally lower case is used so converting to lower case
const dummyText = DUMMY_TEXT.toLocaleLowerCase();
const batchLength = 6;
let index = 0;
let widthTillNow = 0;
let str = "";
while (widthTillNow <= width) {
const batch = dummyText.substr(index, index + batchLength);
str += batch;
widthTillNow += getTextWidth(str, font);
if (index === dummyText.length - 1) {
index = 0;
}
index = index + batchLength;
}
while (widthTillNow > width) {
str = str.substr(0, str.length - 1);
widthTillNow = getTextWidth(str, font);
}
return str.length;
};
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
};
+247 -43
View File
@@ -1,10 +1,25 @@
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { isWritableElement, getFontString } from "../utils"; import {
isWritableElement,
getFontString,
viewportCoordsToSceneCoords,
getFontFamilyString,
} from "../utils";
import Scene from "../scene/Scene"; import Scene from "../scene/Scene";
import { isTextElement } from "./typeChecks"; import { isBoundToContainer, isTextElement } from "./typeChecks";
import { CLASSES } from "../constants"; import { CLASSES, PADDING } from "../constants";
import { ExcalidrawElement } from "./types"; import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
} from "./types";
import { AppState } from "../types"; import { AppState } from "../types";
import { mutateElement } from "./mutateElement";
import {
getApproxLineHeight,
getBoundTextElementId,
wrapText,
} from "./textElement";
const normalizeText = (text: string) => { const normalizeText = (text: string) => {
return ( return (
@@ -48,55 +63,154 @@ export const textWysiwyg = ({
id: ExcalidrawElement["id"]; id: ExcalidrawElement["id"];
appState: AppState; appState: AppState;
onChange?: (text: string) => void; onChange?: (text: string) => void;
onSubmit: (data: { text: string; viaKeyboard: boolean }) => void; onSubmit: (data: {
text: string;
viaKeyboard: boolean;
originalText: string;
}) => void;
getViewportCoords: (x: number, y: number) => [number, number]; getViewportCoords: (x: number, y: number) => [number, number];
element: ExcalidrawElement; element: ExcalidrawElement;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
excalidrawContainer: HTMLDivElement | null; excalidrawContainer: HTMLDivElement | null;
}) => { }) => {
const textPropertiesUpdated = (
updatedElement: ExcalidrawTextElement,
editable: HTMLTextAreaElement,
) => {
const currentFont = editable.style.fontFamily.replaceAll('"', "");
if (
getFontFamilyString({ fontFamily: updatedElement.fontFamily }) !==
currentFont
) {
return true;
}
if (`${updatedElement.fontSize}px` !== editable.style.fontSize) {
return true;
}
return false;
};
let originalContainerHeight: number;
let approxLineHeight = isTextElement(element)
? getApproxLineHeight(getFontString(element))
: 0;
const updateWysiwygStyle = () => { const updateWysiwygStyle = () => {
const updatedElement = Scene.getScene(element)?.getElement(id); const updatedElement = Scene.getScene(element)?.getElement(id);
if (updatedElement && isTextElement(updatedElement)) { if (updatedElement && isTextElement(updatedElement)) {
const [viewportX, viewportY] = getViewportCoords( let coordX = updatedElement.x;
updatedElement.x, let coordY = updatedElement.y;
updatedElement.y, let container = updatedElement?.containerId
); ? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId)
: null;
let maxWidth = updatedElement.width;
let maxHeight = updatedElement.height;
let width = updatedElement.width;
let height = updatedElement.height;
if (container && updatedElement.containerId) {
const propertiesUpdated = textPropertiesUpdated(
updatedElement,
editable,
);
if (propertiesUpdated) {
const currentContainer = Scene.getScene(updatedElement)?.getElement(
updatedElement.containerId,
) as ExcalidrawBindableElement;
approxLineHeight = isTextElement(updatedElement)
? getApproxLineHeight(getFontString(updatedElement))
: 0;
if (updatedElement.height > currentContainer.height - PADDING * 2) {
const nextHeight = updatedElement.height + PADDING * 2;
originalContainerHeight = nextHeight;
mutateElement(container, { height: nextHeight });
container = { ...container, height: nextHeight };
}
editable.style.height = `${updatedElement.height}px`;
}
if (!originalContainerHeight) {
originalContainerHeight = container.height;
}
maxWidth = container.width - PADDING * 2;
maxHeight = container.height - PADDING * 2;
width = maxWidth;
height = Math.min(height, maxHeight);
// The coordinates of text box set a distance of
// 30px to preserve padding
coordX = container.x + PADDING;
// autogrow container height if text exceeds
if (editable.clientHeight > maxHeight) {
const diff = Math.min(
editable.clientHeight - maxHeight,
approxLineHeight,
);
mutateElement(container, { height: container.height + diff });
return;
} else if (
// autoshrink container height until original container height
// is reached when text is removed
container.height > originalContainerHeight &&
editable.clientHeight < maxHeight
) {
const diff = Math.min(
maxHeight - editable.clientHeight,
approxLineHeight,
);
mutateElement(container, { height: container.height - diff });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const lines = editable.clientHeight / approxLineHeight;
// For some reason the scrollHeight gets set to twice the lineHeight
// when you start typing for first time and thus line count is 2
// hence this check
if (lines > 2 || propertiesUpdated) {
// vertically center align the text
coordY =
container.y + container.height / 2 - editable.clientHeight / 2;
}
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const { textAlign, angle } = updatedElement; const { textAlign, angle } = updatedElement;
editable.value = updatedElement.text; editable.value = updatedElement.originalText || updatedElement.text;
const lines = updatedElement.originalText.split("\n");
const lines = updatedElement.text.replace(/\r\n?/g, "\n").split("\n"); const lineHeight = updatedElement.containerId
const lineHeight = updatedElement.height / lines.length; ? approxLineHeight
const maxWidth = : updatedElement.height / lines.length;
(appState.offsetLeft + appState.width - viewportX - 8) / if (!container) {
appState.zoom.value - maxWidth =
// margin-right of parent if any (appState.offsetLeft + appState.width - viewportX - 8) /
Number( appState.zoom.value -
getComputedStyle( // margin-right of parent if any
excalidrawContainer?.parentNode as Element, Number(
).marginRight.slice(0, -2), getComputedStyle(
); excalidrawContainer?.parentNode as Element,
).marginRight.slice(0, -2),
);
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.offsetTop + appState.height - viewportY) /
appState.zoom.value;
Object.assign(editable.style, { Object.assign(editable.style, {
font: getFontString(updatedElement), font: getFontString(updatedElement),
// must be defined *after* font ¯\_(ツ)_/¯ // must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`, lineHeight: `${lineHeight}px`,
width: `${updatedElement.width}px`, width: `${width}px`,
height: `${updatedElement.height}px`, height: `${Math.max(editable.clientHeight, updatedElement.height)}px`,
left: `${viewportX}px`, left: `${viewportX}px`,
top: `${viewportY}px`, top: `${viewportY}px`,
transform: getTransform( transform: getTransform(width, height, angle, appState, maxWidth),
updatedElement.width,
updatedElement.height,
angle,
appState,
maxWidth,
),
textAlign, textAlign,
color: updatedElement.strokeColor, color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100, opacity: updatedElement.opacity / 100,
filter: "var(--theme-filter)", filter: "var(--theme-filter)",
maxWidth: `${maxWidth}px`, maxWidth: `${maxWidth}px`,
maxHeight: `${editorMaxHeight}px`,
}); });
} }
}; };
@@ -110,6 +224,13 @@ export const textWysiwyg = ({
editable.wrap = "off"; editable.wrap = "off";
editable.classList.add("excalidraw-wysiwyg"); editable.classList.add("excalidraw-wysiwyg");
let whiteSpace = "pre";
let wordBreak = "normal";
if (isBoundToContainer(element)) {
whiteSpace = "pre-wrap";
wordBreak = "break-word";
}
Object.assign(editable.style, { Object.assign(editable.style, {
position: "absolute", position: "absolute",
display: "inline-block", display: "inline-block",
@@ -122,16 +243,21 @@ export const textWysiwyg = ({
resize: "none", resize: "none",
background: "transparent", background: "transparent",
overflow: "hidden", overflow: "hidden",
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
whiteSpace: "pre",
// must be specified because in dark mode canvas creates a stacking context // must be specified because in dark mode canvas creates a stacking context
zIndex: "var(--zIndex-wysiwyg)", zIndex: "var(--zIndex-wysiwyg)",
wordBreak,
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
whiteSpace,
overflowWrap: "break-word",
}); });
updateWysiwygStyle(); updateWysiwygStyle();
if (onChange) { if (onChange) {
editable.oninput = () => { editable.oninput = () => {
if (isBoundToContainer(element)) {
editable.style.height = "auto";
editable.style.height = `${editable.scrollHeight}px`;
}
onChange(normalizeText(editable.value)); onChange(normalizeText(editable.value));
}; };
} }
@@ -174,7 +300,7 @@ export const textWysiwyg = ({
const linesStartIndices = getSelectedLinesStartIndices(); const linesStartIndices = getSelectedLinesStartIndices();
let value = editable.value; let value = editable.value;
linesStartIndices.forEach((startIndex) => { linesStartIndices.forEach((startIndex: number) => {
const startValue = value.slice(0, startIndex); const startValue = value.slice(0, startIndex);
const endValue = value.slice(startIndex); const endValue = value.slice(startIndex);
@@ -274,9 +400,63 @@ 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);
if (!updateElement) {
return;
}
let wrappedText = "";
if (isTextElement(updateElement) && updateElement?.containerId) {
const container = Scene.getScene(updateElement)!.getElement(
updateElement.containerId,
) as ExcalidrawBindableElement;
if (container) {
wrappedText = wrapText(
editable.value,
getFontString(updateElement),
container.width,
);
const { x, y } = viewportCoordsToSceneCoords(
{
clientX: Number(editable.style.left.slice(0, -2)),
clientY: Number(editable.style.top.slice(0, -2)),
},
appState,
);
if (isTextElement(updateElement) && updateElement.containerId) {
if (editable.value) {
mutateElement(updateElement, {
y: y + appState.offsetTop,
height: Number(editable.style.height.slice(0, -2)),
width: Number(editable.style.width.slice(0, -2)),
x: x + appState.offsetLeft,
});
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 {
wrappedText = editable.value;
}
onSubmit({ onSubmit({
text: normalizeText(editable.value), text: normalizeText(wrappedText),
viaKeyboard: submittedViaKeyboard, viaKeyboard: submittedViaKeyboard,
originalText: editable.value,
}); });
}; };
@@ -305,26 +485,45 @@ export const textWysiwyg = ({
editable.remove(); editable.remove();
}; };
const bindBlurEvent = () => { const bindBlurEvent = (event?: MouseEvent) => {
window.removeEventListener("pointerup", bindBlurEvent); window.removeEventListener("pointerup", bindBlurEvent);
// Deferred so that the pointerdown that initiates the wysiwyg doesn't // Deferred so that the pointerdown that initiates the wysiwyg doesn't
// trigger the blur on ensuing pointerup. // trigger the blur on ensuing pointerup.
// Also to handle cases such as picking a color which would trigger a blur // Also to handle cases such as picking a color which would trigger a blur
// in that same tick. // in that same tick.
const target = event?.target;
const isTargetColorPicker =
target instanceof HTMLInputElement &&
target.closest(".color-picker-input") &&
isWritableElement(target);
setTimeout(() => { setTimeout(() => {
editable.onblur = handleSubmit; editable.onblur = handleSubmit;
if (target && isTargetColorPicker) {
target.onblur = () => {
editable.focus();
};
}
// case: clicking on the same property → no change → no update → no focus // case: clicking on the same property → no change → no update → no focus
editable.focus(); if (!isTargetColorPicker) {
editable.focus();
}
}); });
}; };
// prevent blur when changing properties from the menu // prevent blur when changing properties from the menu
const onPointerDown = (event: MouseEvent) => { const onPointerDown = (event: MouseEvent) => {
const isTargetColorPicker =
event.target instanceof HTMLInputElement &&
event.target.closest(".color-picker-input") &&
isWritableElement(event.target);
if ( if (
(event.target instanceof HTMLElement || ((event.target instanceof HTMLElement ||
event.target instanceof SVGElement) && event.target instanceof SVGElement) &&
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) && event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
!isWritableElement(event.target) !isWritableElement(event.target)) ||
isTargetColorPicker
) { ) {
editable.onblur = null; editable.onblur = null;
window.addEventListener("pointerup", bindBlurEvent); window.addEventListener("pointerup", bindBlurEvent);
@@ -337,7 +536,12 @@ export const textWysiwyg = ({
// handle updates of textElement properties of editing element // handle updates of textElement properties of editing element
const unbindUpdate = Scene.getScene(element)!.addCallback(() => { const unbindUpdate = Scene.getScene(element)!.addCallback(() => {
updateWysiwygStyle(); updateWysiwygStyle();
editable.focus(); const isColorPickerActive = !!document.activeElement?.closest(
".color-picker-input",
);
if (!isColorPickerActive) {
editable.focus();
}
}); });
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+2 -1
View File
@@ -3,6 +3,7 @@ import { ExcalidrawElement, PointerType } from "./types";
import { getElementAbsoluteCoords, Bounds } from "./bounds"; import { getElementAbsoluteCoords, Bounds } from "./bounds";
import { rotate } from "../math"; import { rotate } from "../math";
import { Zoom } from "../types"; import { Zoom } from "../types";
import { isTextElement } from ".";
export type TransformHandleDirection = export type TransformHandleDirection =
| "n" | "n"
@@ -242,7 +243,7 @@ export const getTransformHandles = (
omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH; omitSides = OMIT_SIDES_FOR_LINE_BACKSLASH;
} }
} }
} else if (element.type === "text") { } else if (isTextElement(element)) {
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT; omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
} }
+30 -1
View File
@@ -7,6 +7,7 @@ import {
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
InitializedExcalidrawImageElement, InitializedExcalidrawImageElement,
ExcalidrawImageElement, ExcalidrawImageElement,
ExcalidrawTextElementWithContainer,
} from "./types"; } from "./types";
export const isGenericElement = ( export const isGenericElement = (
@@ -85,7 +86,18 @@ export const isBindableElement = (
(element.type === "rectangle" || (element.type === "rectangle" ||
element.type === "diamond" || element.type === "diamond" ||
element.type === "ellipse" || element.type === "ellipse" ||
element.type === "text") element.type === "image" ||
(element.type === "text" && !element.containerId))
);
};
export const isTextBindableContainer = (element: ExcalidrawElement | null) => {
return (
element != null &&
(element.type === "rectangle" ||
element.type === "diamond" ||
element.type === "ellipse" ||
element.type === "image")
); );
}; };
@@ -100,3 +112,20 @@ export const isExcalidrawElement = (element: any): boolean => {
element?.type === "line" element?.type === "line"
); );
}; };
export const hasBoundTextElement = (
element: ExcalidrawElement | null,
): element is ExcalidrawBindableElement => {
return (
isBindableElement(element) &&
!!element.boundElements?.some(({ type }) => type === "text")
);
};
export const isBoundToContainer = (
element: ExcalidrawElement | null,
): element is ExcalidrawTextElementWithContainer => {
return (
element !== null && isTextElement(element) && element.containerId !== null
);
};
+15 -2
View File
@@ -43,8 +43,15 @@ type _ExcalidrawElementBase = Readonly<{
/** List of groups the element belongs to. /** List of groups the element belongs to.
Ordered from deepest to shallowest. */ Ordered from deepest to shallowest. */
groupIds: readonly GroupId[]; groupIds: readonly GroupId[];
/** Ids of (linear) elements that are bound to this element. */ /** other elements that are bound to this element */
boundElementIds: readonly ExcalidrawLinearElement["id"][] | null; boundElements:
| readonly Readonly<{
id: ExcalidrawLinearElement["id"];
type: "arrow" | "text";
}>[]
| null;
/** epoch (ms) timestamp of last element update */
updated: number;
}>; }>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & { export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
@@ -114,6 +121,8 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
baseline: number; baseline: number;
textAlign: TextAlign; textAlign: TextAlign;
verticalAlign: VerticalAlign; verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;
originalText: string;
}>; }>;
export type ExcalidrawBindableElement = export type ExcalidrawBindableElement =
@@ -123,6 +132,10 @@ export type ExcalidrawBindableElement =
| ExcalidrawTextElement | ExcalidrawTextElement
| ExcalidrawImageElement; | ExcalidrawImageElement;
export type ExcalidrawTextElementWithContainer = {
containerId: ExcalidrawGenericElement["id"];
} & ExcalidrawTextElement;
export type PointBinding = { export type PointBinding = {
elementId: ExcalidrawBindableElement["id"]; elementId: ExcalidrawBindableElement["id"];
focus: number; focus: number;
@@ -78,7 +78,7 @@ export const ExportToExcalidrawPlus: React.FC<{
onError: (error: Error) => void; onError: (error: Error) => void;
}> = ({ elements, appState, files, onError }) => { }> = ({ elements, appState, files, onError }) => {
return ( return (
<Card color="indigo"> <Card color="primary">
<div className="Card-icon">{excalidrawPlusIcon}</div> <div className="Card-icon">{excalidrawPlusIcon}</div>
<h2>Excalidraw+</h2> <h2>Excalidraw+</h2>
<div className="Card-details"> <div className="Card-details">
+9 -1
View File
@@ -11,7 +11,15 @@ import { MIME_TYPES } from "../../constants";
// private // private
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG); let FIREBASE_CONFIG: Record<string, any>;
try {
FIREBASE_CONFIG = JSON.parse(process.env.REACT_APP_FIREBASE_CONFIG);
} catch (error: any) {
console.warn(
`Error JSON parsing firebase config. Supplied value: ${process.env.REACT_APP_FIREBASE_CONFIG}`,
);
FIREBASE_CONFIG = {};
}
let firebasePromise: Promise<typeof import("firebase/app").default> | null = let firebasePromise: Promise<typeof import("firebase/app").default> | null =
null; null;
+65 -37
View File
@@ -1,6 +1,6 @@
import { compressData, decompressData } from "../../data/encode";
import { import {
decryptData, decryptData,
encryptData,
generateEncryptionKey, generateEncryptionKey,
IV_LENGTH_BYTES, IV_LENGTH_BYTES,
} from "../../data/encryption"; } from "../../data/encryption";
@@ -109,9 +109,45 @@ export const getCollaborationLink = (data: {
return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`; return `${window.location.origin}${window.location.pathname}#room=${data.roomId},${data.roomKey}`;
}; };
/**
* Decodes shareLink data using the legacy buffer format.
* @deprecated
*/
const legacy_decodeFromBackend = async ({
buffer,
decryptionKey,
}: {
buffer: ArrayBuffer;
decryptionKey: string;
}) => {
let decrypted: ArrayBuffer;
try {
// Buffer should contain both the IV (fixed length) and encrypted data
const iv = buffer.slice(0, IV_LENGTH_BYTES);
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength);
decrypted = await decryptData(new Uint8Array(iv), encrypted, decryptionKey);
} catch (error: any) {
// Fixed IV (old format, backward compatibility)
const fixedIv = new Uint8Array(IV_LENGTH_BYTES);
decrypted = await decryptData(fixedIv, buffer, decryptionKey);
}
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted),
);
const data: ImportedDataState = JSON.parse(string);
return {
elements: data.elements || null,
appState: data.appState || null,
};
};
const importFromBackend = async ( const importFromBackend = async (
id: string, id: string,
privateKey: string, decryptionKey: string,
): Promise<ImportedDataState> => { ): Promise<ImportedDataState> => {
try { try {
const response = await fetch(`${BACKEND_V2_GET}${id}`); const response = await fetch(`${BACKEND_V2_GET}${id}`);
@@ -122,28 +158,28 @@ const importFromBackend = async (
} }
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
let decrypted: ArrayBuffer;
try { try {
// Buffer should contain both the IV (fixed length) and encrypted data const { data: decodedBuffer } = await decompressData(
const iv = buffer.slice(0, IV_LENGTH_BYTES); new Uint8Array(buffer),
const encrypted = buffer.slice(IV_LENGTH_BYTES, buffer.byteLength); {
decrypted = await decryptData(new Uint8Array(iv), encrypted, privateKey); decryptionKey,
},
);
const data: ImportedDataState = JSON.parse(
new TextDecoder().decode(decodedBuffer),
);
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error: any) { } catch (error: any) {
// Fixed IV (old format, backward compatibility) console.warn(
const fixedIv = new Uint8Array(IV_LENGTH_BYTES); "error when decoding shareLink data using the new format:",
decrypted = await decryptData(fixedIv, buffer, privateKey); error,
);
return legacy_decodeFromBackend({ buffer, decryptionKey });
} }
// We need to convert the decrypted array buffer to a string
const string = new window.TextDecoder("utf-8").decode(
new Uint8Array(decrypted),
);
const data: ImportedDataState = JSON.parse(string);
return {
elements: data.elements || null,
appState: data.appState || null,
};
} catch (error: any) { } catch (error: any) {
window.alert(t("alerts.importBackendFailed")); window.alert(t("alerts.importBackendFailed"));
console.error(error); console.error(error);
@@ -188,20 +224,14 @@ export const exportToBackend = async (
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
) => { ) => {
const json = serializeAsJSON(elements, appState, files, "database"); const encryptionKey = await generateEncryptionKey("string");
const encoded = new TextEncoder().encode(json);
const cryptoKey = await generateEncryptionKey("cryptoKey"); const payload = await compressData(
new TextEncoder().encode(
const { encryptedBuffer, iv } = await encryptData(cryptoKey, encoded); serializeAsJSON(elements, appState, files, "database"),
),
// Concatenate IV with encrypted data (IV does not have to be secret). { encryptionKey },
const payloadBlob = new Blob([iv.buffer, encryptedBuffer]); );
const payload = await new Response(payloadBlob).arrayBuffer();
// We use jwk encoding to be able to extract just the base64 encoded key.
// We will hardcode the rest of the attributes when importing back the key.
const exportedKey = await window.crypto.subtle.exportKey("jwk", cryptoKey);
try { try {
const filesMap = new Map<FileId, BinaryFileData>(); const filesMap = new Map<FileId, BinaryFileData>();
@@ -211,8 +241,6 @@ export const exportToBackend = async (
} }
} }
const encryptionKey = exportedKey.k!;
const filesToUpload = await encodeFilesForUpload({ const filesToUpload = await encodeFilesForUpload({
files: filesMap, files: filesMap,
encryptionKey, encryptionKey,
@@ -221,7 +249,7 @@ export const exportToBackend = async (
const response = await fetch(BACKEND_V2_POST, { const response = await fetch(BACKEND_V2_POST, {
method: "POST", method: "POST",
body: payload, body: payload.buffer,
}); });
const json = await response.json(); const json = await response.json();
if (json.id) { if (json.id) {
+1 -1
View File
@@ -9,7 +9,7 @@
.encrypted-icon { .encrypted-icon {
border-radius: var(--space-factor); border-radius: var(--space-factor);
color: var(--icon-green-fill-color); color: var(--color-primary);
margin-top: auto; margin-top: auto;
margin-bottom: auto; margin-bottom: auto;
margin-inline-start: auto; margin-inline-start: auto;
+8 -1
View File
@@ -111,10 +111,17 @@ interface Uint8Array {
// https://github.com/nodeca/image-blob-reduce/issues/23#issuecomment-783271848 // https://github.com/nodeca/image-blob-reduce/issues/23#issuecomment-783271848
declare module "image-blob-reduce" { declare module "image-blob-reduce" {
import { PicaResizeOptions } from "pica"; import { PicaResizeOptions, Pica } from "pica";
namespace ImageBlobReduce { namespace ImageBlobReduce {
interface ImageBlobReduce { interface ImageBlobReduce {
toBlob(file: File, options: ImageBlobReduceOptions): Promise<Blob>; toBlob(file: File, options: ImageBlobReduceOptions): Promise<Blob>;
_create_blob(
this: { pica: Pica },
env: {
out_canvas: HTMLCanvasElement;
out_blob: Blob;
},
): Promise<any>;
} }
interface ImageBlobReduceStatic { interface ImageBlobReduceStatic {
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "مشاركة", "share": "مشاركة",
"showStroke": "إظهار منتقي لون الخط", "showStroke": "إظهار منتقي لون الخط",
"showBackground": "إظهار منتقي لون الخلفية", "showBackground": "إظهار منتقي لون الخلفية",
"toggleTheme": "غير النمط" "toggleTheme": "غير النمط",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "إعادة تعيين اللوحة", "clearReset": "إعادة تعيين اللوحة",
@@ -135,7 +137,11 @@
"zenMode": "وضع التأمل", "zenMode": "وضع التأمل",
"exitZenMode": "إلغاء الوضع الليلى", "exitZenMode": "إلغاء الوضع الليلى",
"cancel": "إلغاء", "cancel": "إلغاء",
"clear": "مسح" "clear": "مسح",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟", "clearReset": "هذا سيُزيل كامل اللوحة. هل أنت متأكد؟",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة", "cannotRestoreFromImage": "تعذر استعادة المشهد من ملف الصورة",
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.", "invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟", "resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل." "invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "انقر نقراً مزدوجاً أو اضغط Enter لتعديل النقاط", "lineEditor_info": "انقر نقراً مزدوجاً أو اضغط Enter لتعديل النقاط",
"lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة، Ctrl Or Cmd+D للتكرار، أو اسحب للانتقال", "lineEditor_pointSelected": "اضغط على حذف لإزالة النقطة، Ctrl Or Cmd+D للتكرار، أو اسحب للانتقال",
"lineEditor_nothingSelected": "حدد نقطة لتحريك أو إزالتها، أو اضغط Alt ثم انقر لإضافة نقاط جديدة", "lineEditor_nothingSelected": "حدد نقطة لتحريك أو إزالتها، أو اضغط Alt ثم انقر لإضافة نقاط جديدة",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "تعذر عرض المعاينة", "cannotShowPreview": "تعذر عرض المعاينة",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.", "tooltip": "رسوماتك مشفرة من النهاية إلى النهاية حتى أن خوادم Excalidraw لن تراها أبدا.",
"link": "مشاركة المدونة في التشفير من النهاية إلى النهاية في Excalidraw" "link": "مشاركة المدونة في التشفير من النهاية إلى النهاية في Excalidraw"
@@ -289,6 +345,7 @@
"width": "العرض" "width": "العرض"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "نسخت الانماط.", "copyStyles": "نسخت الانماط.",
"copyToClipboard": "نسخ إلى الحافظة.", "copyToClipboard": "نسخ إلى الحافظة.",
"copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "تم نسخ {{exportSelection}} إلى الحافظة بصيغة PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Нулиране на платно", "clearReset": "Нулиране на платно",
@@ -135,7 +137,11 @@
"zenMode": "Режим Zen", "zenMode": "Режим Zen",
"exitZenMode": "Спиране на Zen режим", "exitZenMode": "Спиране на Zen режим",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?", "clearReset": "Това ще изчисти цялото платно. Сигурни ли сте?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл", "cannotRestoreFromImage": "Не може да бъде възстановена сцена от този файл",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "Кликнете два пъти или натиснете Enter за да промените точките", "lineEditor_info": "Кликнете два пъти или натиснете Enter за да промените точките",
"lineEditor_pointSelected": "Натиснете Delete за да изтриете точка, CtrlOrCmd+D за дуплициране, или извлачете за да преместите", "lineEditor_pointSelected": "Натиснете Delete за да изтриете точка, CtrlOrCmd+D за дуплициране, или извлачете за да преместите",
"lineEditor_nothingSelected": "Изберете точка за местене или изтриване, или пък задръжте Alt и натиснете за да добавите нови точки", "lineEditor_nothingSelected": "Изберете точка за местене или изтриване, или пък задръжте Alt и натиснете за да добавите нови точки",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Невъзможност за показване на preview", "cannotShowPreview": "Невъзможност за показване на preview",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.", "tooltip": "Вашите рисунки са криптирани от край до край, така че сървърите на Excalidraw няма да могат да ги виждат.",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "Широчина" "width": "Широчина"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Копирани стилове.", "copyStyles": "Копирани стилове.",
"copyToClipboard": "Копирано в клипборда.", "copyToClipboard": "Копирано в клипборда.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -135,7 +137,11 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "", "exitZenMode": "",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "", "tooltip": "",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "" "width": ""
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Comparteix", "share": "Comparteix",
"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": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Neteja el llenç", "clearReset": "Neteja el llenç",
@@ -135,7 +137,11 @@
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Surt de mode zen", "exitZenMode": "Surt de mode zen",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "S'esborrarà tot el llenç. N'esteu segur?", "clearReset": "S'esborrarà tot el llenç. N'esteu segur?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Lescena no sha pogut restaurar des daquest fitxer dimatge", "cannotRestoreFromImage": "Lescena no sha pogut restaurar des daquest fitxer dimatge",
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.", "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": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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 punt, 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": "Selecciona un punt per moure o eliminar, o manté premut Alt i fes clic per afegir punts nous",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització", "cannotShowPreview": "No es pot mostrar la previsualització",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai.", "tooltip": "Els vostres dibuixos estan xifrats de punta a punta de manera que els servidors dExcalidraw no els veuran mai.",
"link": "Article del blog sobre encriptació d'extrem a extrem a Excalidraw" "link": "Article del blog sobre encriptació d'extrem a extrem a Excalidraw"
@@ -289,6 +345,7 @@
"width": "Amplada" "width": "Amplada"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"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}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Sdílet", "share": "Sdílet",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "Přepnout tmavý řežim" "toggleTheme": "Přepnout tmavý řežim",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -135,7 +137,11 @@
"zenMode": "Zen mód", "zenMode": "Zen mód",
"exitZenMode": "Opustit zen mód", "exitZenMode": "Opustit zen mód",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "", "tooltip": "",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "" "width": ""
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Del", "share": "Del",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -135,7 +137,11 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "", "exitZenMode": "",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "", "tooltip": "",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "Bredde" "width": "Bredde"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Kopieret stilarter.", "copyStyles": "Kopieret stilarter.",
"copyToClipboard": "Kopieret til klippebord.", "copyToClipboard": "Kopieret til klippebord.",
"copyToClipboardAsPng": "Kopieret {{exportSelection}} til klippebord som PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopieret {{exportSelection}} til klippebord som PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"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": "Design umschalten",
"personalLib": "Persönliche Bibliothek",
"excalidrawLib": "Excalidraw-Bibliothek"
}, },
"buttons": { "buttons": {
"clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen", "clearReset": "Zeichenfläche löschen & Hintergrundfarbe zurücksetzen",
@@ -135,7 +137,11 @@
"zenMode": "Zen-Modus", "zenMode": "Zen-Modus",
"exitZenMode": "Zen-Modus verlassen", "exitZenMode": "Zen-Modus verlassen",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"clear": "Löschen" "clear": "Löschen",
"remove": "Entfernen",
"publishLibrary": "Veröffentlichen",
"submit": "Absenden",
"confirm": "Bestätigen"
}, },
"alerts": { "alerts": {
"clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?", "clearReset": "Dies wird die ganze Zeichenfläche löschen. Bist du dir sicher?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden", "cannotRestoreFromImage": "Die Zeichnung konnte aus dieser Bilddatei nicht wiederhergestellt werden",
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.", "invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?", "resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert." "invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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 zu entfernen, Strg+D oder Cmd+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 Punkt zum Verschieben oder Löschen oder halte die Alt-Taste 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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Vorschau kann nicht angezeigt werden", "cannotShowPreview": "Vorschau kann nicht angezeigt werden",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Zeichenfläche löschen" "title": "Zeichenfläche löschen"
}, },
"publishDialog": {
"title": "Bibliothek veröffentlichen",
"itemName": "Elementname",
"authorName": "Name des Autors",
"githubUsername": "GitHub-Benutzername",
"twitterUsername": "Twitter-Benutzername",
"libraryName": "Name der Bibliothek",
"libraryDesc": "Beschreibung der Bibliothek",
"website": "Webseite",
"placeholder": {
"authorName": "Dein Name oder Benutzername",
"libraryName": "Name deiner Bibliothek",
"libraryDesc": "Beschreibung deiner Bibliothek, um anderen Nutzern bei der Verwendung zu helfen",
"githubHandle": "GitHub-Handle (optional), damit du die Bibliothek bearbeiten kannst, wenn sie zur Überprüfung eingereicht wurde",
"twitterHandle": "Twitter-Benutzername (optional), damit wir wissen, wen wir bei Werbung über Twitter nennen können",
"website": "Link zu deiner persönlichen Webseite oder zu anderer Seite (optional)"
},
"errors": {
"required": "Erforderlich",
"website": "Gültige URL eingeben"
},
"noteDescription": {
"pre": "Sende deine Bibliothek ein, um in die ",
"link": "öffentliche Bibliotheks-Repository aufgenommen zu werden",
"post": "damit andere Nutzer sie in ihren Zeichnungen verwenden können."
},
"noteGuidelines": {
"pre": "Die Bibliothek muss zuerst manuell freigegeben werden. Bitte lies die ",
"link": "Richtlinien",
"post": " vor dem Absenden. Du benötigst ein GitHub-Konto, um zu kommunizieren und Änderungen vorzunehmen, falls erforderlich, aber es ist nicht unbedingt erforderlich."
},
"noteLicense": {
"pre": "Mit dem Absenden stimmst du zu, dass die Bibliothek unter der ",
"link": "MIT-Lizenz, ",
"post": "die zusammengefasst beinhaltet, dass jeder sie ohne Einschränkungen nutzen kann."
},
"noteItems": "Jedes Bibliothekselement muss einen eigenen Namen haben, damit es gefiltert werden kann. Die folgenden Bibliothekselemente werden hinzugefügt:",
"atleastOneLibItem": "Bitte wähle mindestens ein Bibliothekselement aus, um zu beginnen"
},
"publishSuccessDialog": {
"title": "Bibliothek übermittelt",
"content": "Vielen Dank {{authorName}}. Deine Bibliothek wurde zur Überprüfung eingereicht. Du kannst den Status verfolgen",
"link": "hier"
},
"confirmDialog": {
"resetLibrary": "Bibliothek zurücksetzen",
"removeItemsFromLib": "Ausgewählte Elemente aus der Bibliothek entfernen"
},
"encrypted": { "encrypted": {
"tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.", "tooltip": "Da deine Zeichnungen Ende-zu-Ende verschlüsselt werden, sehen auch unsere Excalidraw-Server sie niemals.",
"link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw" "link": "Blogbeitrag über Ende-zu-Ende-Verschlüsselung in Excalidraw"
@@ -289,6 +345,7 @@
"width": "Breite" "width": "Breite"
}, },
"toast": { "toast": {
"addedToLibrary": "Zur Bibliothek hinzugefügt",
"copyStyles": "Formatierungen kopiert.", "copyStyles": "Formatierungen kopiert.",
"copyToClipboard": "In die Zwischenablage kopiert.", "copyToClipboard": "In die Zwischenablage kopiert.",
"copyToClipboardAsPng": "{{exportSelection}} als PNG in die Zwischenablage kopiert\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} als PNG in die Zwischenablage kopiert\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Κοινοποίηση", "share": "Κοινοποίηση",
"showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς", "showStroke": "Εμφάνιση επιλογέα χρωμάτων πινελιάς",
"showBackground": "Εμφάνιση επιλογέα χρώματος φόντου", "showBackground": "Εμφάνιση επιλογέα χρώματος φόντου",
"toggleTheme": "Εναλλαγή θέματος" "toggleTheme": "Εναλλαγή θέματος",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Επαναφορά του καμβά", "clearReset": "Επαναφορά του καμβά",
@@ -135,7 +137,11 @@
"zenMode": "Λειτουργία Zεν", "zenMode": "Λειτουργία Zεν",
"exitZenMode": "Έξοδος από την λειτουργία Zen", "exitZenMode": "Έξοδος από την λειτουργία Zen",
"cancel": "Ακύρωση", "cancel": "Ακύρωση",
"clear": "Καθαρισμός" "clear": "Καθαρισμός",
"remove": "Κατάργηση",
"publishLibrary": "Δημοσίευση",
"submit": "Υποβολή",
"confirm": "Επιβεβαίωση"
}, },
"alerts": { "alerts": {
"clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;", "clearReset": "Αυτό θα σβήσει ολόκληρο τον καμβά. Είσαι σίγουρος;",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας", "cannotRestoreFromImage": "Η σκηνή δεν ήταν δυνατό να αποκατασταθεί από αυτό το αρχείο εικόνας",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;", "resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη." "invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία", "lineEditor_info": "Διπλό-κλικ ή πιέστε Enter για να επεξεργαστείτε τα σημεία",
"lineEditor_pointSelected": "Πιέστε Διαγραφή για να αφαιρέσετε το σημείου, CtrlOrCmd+D για να το αντιγράψετε ή σύρτε το για να το μετακινήσετε", "lineEditor_pointSelected": "Πιέστε Διαγραφή για να αφαιρέσετε το σημείου, CtrlOrCmd+D για να το αντιγράψετε ή σύρτε το για να το μετακινήσετε",
"lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για μετακίνηση ή αφαίρεση, ή κρατήστε παρατεταμένα το Alt και κάντε κλικ για να προσθέσετε νέα σημεία", "lineEditor_nothingSelected": "Επιλέξτε ένα σημείο για μετακίνηση ή αφαίρεση, ή κρατήστε παρατεταμένα το Alt και κάντε κλικ για να προσθέσετε νέα σημεία",
"placeImage": "" "placeImage": "",
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης", "cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Καθαρισμός καμβά" "title": "Καθαρισμός καμβά"
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "Όνομα δημιουργού",
"githubUsername": "GitHub username",
"twitterUsername": "Twitter username",
"libraryName": "Όνομα βιβλιοθήκης",
"libraryDesc": "",
"website": "Ιστοσελίδα",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": "Εισάγετε μια έγκυρη διεύθυνση URL"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.", "tooltip": "Τα σχέδιά σου είναι κρυπτογραφημένα από άκρο σε άκρο, έτσι δεν θα είναι ποτέ ορατά μέσα από τους διακομιστές του Excalidraw.",
"link": "Blog post στην κρυπτογράφηση end-to-end στο Excalidraw" "link": "Blog post στην κρυπτογράφηση end-to-end στο Excalidraw"
@@ -289,6 +345,7 @@
"width": "Πλάτος" "width": "Πλάτος"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Αντιγράφηκαν στυλ.", "copyStyles": "Αντιγράφηκαν στυλ.",
"copyToClipboard": "Αντιγράφηκε στο πρόχειρο.", "copyToClipboard": "Αντιγράφηκε στο πρόχειρο.",
"copyToClipboardAsPng": "Αντιγράφηκε {{exportSelection}} στο πρόχειρο ως PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Αντιγράφηκε {{exportSelection}} στο πρόχειρο ως PNG\n({{exportColorScheme}})",
+4 -3
View File
@@ -204,10 +204,11 @@
"resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center", "resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center",
"rotate": "You can constrain angles by holding SHIFT while rotating", "rotate": "You can constrain angles by holding SHIFT while rotating",
"lineEditor_info": "Double-click or press Enter to edit points", "lineEditor_info": "Double-click or press Enter to edit points",
"lineEditor_pointSelected": "Press Delete to remove point, CtrlOrCmd+D to duplicate, or drag to move", "lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move",
"lineEditor_nothingSelected": "Select a point to move or remove, or hold Alt and click to add new points", "lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
"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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Cannot show preview", "cannotShowPreview": "Cannot show preview",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Compartir", "share": "Compartir",
"showStroke": "Mostrar selector de color de trazo", "showStroke": "Mostrar selector de color de trazo",
"showBackground": "Mostrar el selector de color de fondo", "showBackground": "Mostrar el selector de color de fondo",
"toggleTheme": "Alternar tema" "toggleTheme": "Alternar tema",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Limpiar lienzo y reiniciar el color de fondo", "clearReset": "Limpiar lienzo y reiniciar el color de fondo",
@@ -135,7 +137,11 @@
"zenMode": "Modo Zen", "zenMode": "Modo Zen",
"exitZenMode": "Salir del modo Zen", "exitZenMode": "Salir del modo Zen",
"cancel": "Cancelar", "cancel": "Cancelar",
"clear": "Borrar" "clear": "Borrar",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Esto limpiará todo el lienzo. Estás seguro?", "clearReset": "Esto limpiará todo el lienzo. Estás seguro?",
@@ -157,6 +163,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": "",
"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": {
@@ -199,7 +206,8 @@
"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 punto, 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": "Selecciona un punto sea para mover o eliminar, o mantén pulsado Alt y haz 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": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa", "cannotShowPreview": "No se puede mostrar la vista previa",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Borrar lienzo" "title": "Borrar lienzo"
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "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.",
"link": "Entrada en el blog sobre cifrado de extremo a extremo" "link": "Entrada en el blog sobre cifrado de extremo a extremo"
@@ -289,6 +345,7 @@
"width": "Ancho" "width": "Ancho"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"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}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "اشتراک‌گذاری", "share": "اشتراک‌گذاری",
"showStroke": "نمایش انتخاب کننده رنگ حاشیه", "showStroke": "نمایش انتخاب کننده رنگ حاشیه",
"showBackground": "نمایش انتخاب کننده رنگ پس زمینه", "showBackground": "نمایش انتخاب کننده رنگ پس زمینه",
"toggleTheme": "تغییر تم" "toggleTheme": "تغییر تم",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "پاکسازی بوم نقاشی", "clearReset": "پاکسازی بوم نقاشی",
@@ -135,7 +137,11 @@
"zenMode": "حالت ذن", "zenMode": "حالت ذن",
"exitZenMode": "خروج از حالت تمرکز", "exitZenMode": "خروج از حالت تمرکز",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟", "clearReset": "این کار کل صفحه را پاک میکند. آیا مطمئنید؟",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد", "cannotRestoreFromImage": "صحنه را نمی توان از این فایل تصویری بازیابی کرد",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید", "lineEditor_info": "دوبار کلیک کنید یا Enter را فشار دهید تا نقاط را ویرایش کنید",
"lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید.", "lineEditor_pointSelected": "برای حذف نقطه Delete برای کپی زدن Ctrl یا Cmd+D را بزنید و یا برای جابجایی بکشید.",
"lineEditor_nothingSelected": "یک نقطه را برای جابجایی یا حذف انتخاب کنید یا کلید Alt بگیرید و کلیک کنید تا بتوانید یک نقطه جدید اضافه کنید", "lineEditor_nothingSelected": "یک نقطه را برای جابجایی یا حذف انتخاب کنید یا کلید Alt بگیرید و کلیک کنید تا بتوانید یک نقطه جدید اضافه کنید",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود", "cannotShowPreview": "پیش نمایش نشان داده نمی شود",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.", "tooltip": "شما در یک محیط رمزگزاری شده دو طرفه در حال طراحی هستید پس Excalidraw هرگز طرح های شما را نمیبند.",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "عرض" "width": "عرض"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "کپی سبک.", "copyStyles": "کپی سبک.",
"copyToClipboard": "در کلیپ‌بورد کپی شد.", "copyToClipboard": "در کلیپ‌بورد کپی شد.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Jaa", "share": "Jaa",
"showStroke": "Näytä viivan värin valitsin", "showStroke": "Näytä viivan värin valitsin",
"showBackground": "Näytä taustavärin valitsin", "showBackground": "Näytä taustavärin valitsin",
"toggleTheme": "Vaihda teema" "toggleTheme": "Vaihda teema",
"personalLib": "Oma kirjasto",
"excalidrawLib": "Excalidraw kirjasto"
}, },
"buttons": { "buttons": {
"clearReset": "Tyhjennä piirtoalue", "clearReset": "Tyhjennä piirtoalue",
@@ -135,7 +137,11 @@
"zenMode": "Zen-tila", "zenMode": "Zen-tila",
"exitZenMode": "Poistu zen-tilasta", "exitZenMode": "Poistu zen-tilasta",
"cancel": "Peruuta", "cancel": "Peruuta",
"clear": "Pyyhi" "clear": "Pyyhi",
"remove": "Poista",
"publishLibrary": "Julkaise",
"submit": "Lähetä",
"confirm": "Vahvista"
}, },
"alerts": { "alerts": {
"clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?", "clearReset": "Tämä tyhjentää koko piirtoalueen. Jatketaanko?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta", "cannotRestoreFromImage": "Teosta ei voitu palauttaa tästä kuvatiedostosta",
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.", "invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?", "resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä." "invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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": "Paina Delete poistaaksesi pisteen, Ctrl tai Cmd+D monistaaksesi, tai raahaa liikuttaaksesi",
"lineEditor_nothingSelected": "Valitse liikutettava tai poistettava piste, tai pidä ALT-näppäintä alaspainettuna ja napsauta lisätäksesi uusia pisteitä", "lineEditor_nothingSelected": "Valitse liikutettava tai poistettava piste, tai pidä ALT-näppäintä alaspainettuna ja napsauta 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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Esikatselua ei voitu näyttää", "cannotShowPreview": "Esikatselua ei voitu näyttää",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Pyyhi piirtoalue" "title": "Pyyhi piirtoalue"
}, },
"publishDialog": {
"title": "Julkaise kirjasto",
"itemName": "Kohteen nimi",
"authorName": "Tekijän nimi",
"githubUsername": "GitHub-käyttäjätunnus",
"twitterUsername": "Twitter-käyttäjätunnus",
"libraryName": "Kirjaston nimi",
"libraryDesc": "Kirjaston kuvaus",
"website": "Verkkosivu",
"placeholder": {
"authorName": "Nimesi tai käyttäjänimesi",
"libraryName": "Kirjastosi nimi",
"libraryDesc": "Kirjaston kuvaus, joka auttaa ihmisiä ymmärtämään sen käyttötarkoitukset",
"githubHandle": "GitHub-tunnuksesi (valinnainen), jotta voit muokata kirjastoa sen jälkeen kun se on lähetetty tarkastettavaksi",
"twitterHandle": "Twitter-tunnus (valinnainen), jotta tiedämme ketä kiittää kun viestimme Twitterissä",
"website": "Linkki henkilökohtaiselle verkkosivustollesi tai muualle (valinnainen)"
},
"errors": {
"required": "Pakollinen",
"website": "Syötä oikeamuotoinen URL-osoite"
},
"noteDescription": {
"pre": "Lähetä kirjastosi, jotta se voidaan sisällyttää ",
"link": "julkisessa kirjastolistauksessa",
"post": "muiden käyttöön omissa piirrustuksissaan."
},
"noteGuidelines": {
"pre": "Kirjasto on ensin hyväksyttävä manuaalisesti. Ole hyvä ja lue ",
"link": "ohjeet",
"post": " ennen lähettämistä. Tarvitset GitHub-tilin, jotta voit viestiä ja tehdä muutoksia pyydettäessä, mutta se ei ole ehdottoman välttämätöntä."
},
"noteLicense": {
"pre": "Lähettämällä hyväksyt että kirjasto julkaistaan ",
"link": "MIT-lisenssin ",
"post": "alla, mikä lyhyesti antaa muiden käyttää sitä ilman rajoituksia."
},
"noteItems": "Jokaisella kirjaston kohteella on oltava oma nimensä suodatusta varten. Seuraavat kirjaston kohteet sisältyvät:",
"atleastOneLibItem": "Valitse vähintään yksi kirjaston kohde aloittaaksesi"
},
"publishSuccessDialog": {
"title": "Kirjasto lähetetty",
"content": "Kiitos {{authorName}}. Kirjastosi on lähetetty tarkistettavaksi. Voit seurata sen tilaa",
"link": "täällä"
},
"confirmDialog": {
"resetLibrary": "Tyhjennä kirjasto",
"removeItemsFromLib": "Poista valitut kohteet kirjastosta"
},
"encrypted": { "encrypted": {
"tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.", "tooltip": "Piirroksesi ovat päästä-päähän-salattuja, joten Excalidrawin palvelimet eivät koskaan näe niitä.",
"link": "Blogiartikkeli päästä päähän -salauksesta Excalidraw:ssa" "link": "Blogiartikkeli päästä päähän -salauksesta Excalidraw:ssa"
@@ -289,6 +345,7 @@
"width": "Leveys" "width": "Leveys"
}, },
"toast": { "toast": {
"addedToLibrary": "Lisätty kirjastoon",
"copyStyles": "Tyylit kopioitiin.", "copyStyles": "Tyylit kopioitiin.",
"copyToClipboard": "Kopioitiin leikepöydälle.", "copyToClipboard": "Kopioitiin leikepöydälle.",
"copyToClipboardAsPng": "Kopioitiin {{exportSelection}} leikepöydälle PNG:nä\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopioitiin {{exportSelection}} leikepöydälle PNG:nä\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Partager", "share": "Partager",
"showStroke": "Afficher le sélecteur de couleur de trait", "showStroke": "Afficher le sélecteur de couleur de trait",
"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",
"excalidrawLib": "Bibliothèque Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Réinitialiser le canevas", "clearReset": "Réinitialiser le canevas",
@@ -135,7 +137,11 @@
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Quitter le mode zen", "exitZenMode": "Quitter le mode zen",
"cancel": "Annuler", "cancel": "Annuler",
"clear": "Effacer" "clear": "Effacer",
"remove": "Supprimer",
"publishLibrary": "Publier",
"submit": "Envoyer",
"confirm": "Confirmer"
}, },
"alerts": { "alerts": {
"clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?", "clearReset": "L'intégralité du canevas va être effacée. Êtes-vous sûr ?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image", "cannotRestoreFromImage": "Impossible de restaurer la scène depuis ce fichier image",
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.", "invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?", "resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée." "invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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": "Appuyez sur Supprimer pour supprimer le point, Ctrl ou Cmd+D pour le dupliquer, ou faites-le glisser pour le 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électionnez un point à déplacer ou supprimer, ou maintenez Alt et cliquez 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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossible dafficher laperçu", "cannotShowPreview": "Impossible dafficher laperçu",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Effacer la zone de dessin" "title": "Effacer la zone de dessin"
}, },
"publishDialog": {
"title": "Publier la bibliothèque",
"itemName": "Nom de l’élément",
"authorName": "Nom de l'auteur",
"githubUsername": "Nom d'utilisateur GitHub",
"twitterUsername": "Nom d'utilisateur Twitter",
"libraryName": "Nom de la bibliothèque",
"libraryDesc": "Description de la bibliothèque",
"website": "Site web",
"placeholder": {
"authorName": "Votre nom ou nom d'utilisateur",
"libraryName": "Nom de votre bibliothèque",
"libraryDesc": "Description de votre bibliothèque pour aider les gens à comprendre son usage",
"githubHandle": "Nom d'utilisateur GitHub (optionnel), pour que tu puisses modifier la bibliothèque une fois soumise pour vérification",
"twitterHandle": "Nom d'utilisateur Twitter (optionnel), pour savoir qui créditer lors de la promotion sur Twitter",
"website": "Lien vers votre site web personnel ou autre (optionnel)"
},
"errors": {
"required": "Requis",
"website": "Entrer une URL valide"
},
"noteDescription": {
"pre": "Soumets ta bibliothèque pour l'inclure au ",
"link": "dépôt de bibliothèque publique",
"post": "pour permettre son utilisation par autrui dans leurs dessins."
},
"noteGuidelines": {
"pre": "La bibliothèque doit d'abord être approuvée manuellement. Veuillez lire les ",
"link": "lignes directrices",
"post": " avant de la soumettre. Vous aurez besoin d'un compte GitHub pour communiquer et apporter des modifications si demandé, mais ce n'est pas obligatoire."
},
"noteLicense": {
"pre": "En soumettant, vous acceptez que la bibliothèque soit publiée sous la ",
"link": "Licence MIT, ",
"post": "ce qui en gros signifie que tout le monde peut l'utiliser sans restrictions."
},
"noteItems": "Chaque élément de la bibliothèque doit avoir son propre nom afin qu'il soit filtrable. Les éléments de bibliothèque suivants seront inclus :",
"atleastOneLibItem": "Veuillez sélectionner au moins un élément de bibliothèque pour commencer"
},
"publishSuccessDialog": {
"title": "Bibliothèque soumise",
"content": "Merci {{authorName}}. Votre bibliothèque a été soumise pour examen. Vous pouvez suivre le statut",
"link": "ici"
},
"confirmDialog": {
"resetLibrary": "Réinitialiser la bibliothèque",
"removeItemsFromLib": "Enlever les éléments sélectionnés de la bibliothèque"
},
"encrypted": { "encrypted": {
"tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.", "tooltip": "Vos dessins sont chiffrés de bout en bout, les serveurs d'Excalidraw ne les verront jamais.",
"link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw" "link": "Article de blog sur le chiffrement de bout en bout dans Excalidraw"
@@ -289,6 +345,7 @@
"width": "Largeur" "width": "Largeur"
}, },
"toast": { "toast": {
"addedToLibrary": "Ajouté à la bibliothèque",
"copyStyles": "Styles copiés.", "copyStyles": "Styles copiés.",
"copyToClipboard": "Copié dans le presse-papier.", "copyToClipboard": "Copié dans le presse-papier.",
"copyToClipboardAsPng": "{{exportSelection}} copié dans le presse-papier en PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} copié dans le presse-papier en PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "שתף", "share": "שתף",
"showStroke": "הצג צבעי קו מתאר", "showStroke": "הצג צבעי קו מתאר",
"showBackground": "הצג צבעי רקע", "showBackground": "הצג צבעי רקע",
"toggleTheme": "שינוי ערכת העיצוב" "toggleTheme": "שינוי ערכת העיצוב",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "אפס את הלוח", "clearReset": "אפס את הלוח",
@@ -135,7 +137,11 @@
"zenMode": "מצב זן", "zenMode": "מצב זן",
"exitZenMode": "צא ממצב תפריט מרחף", "exitZenMode": "צא ממצב תפריט מרחף",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "clearReset": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה", "cannotRestoreFromImage": "לא הצלחנו לשחזר את התצוגה מקובץ התמונה",
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.", "invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?", "resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות", "lineEditor_info": "לחץ לחיצה כפולה או אנטר לעריכת הנקודות",
"lineEditor_pointSelected": "לחץ על Delete להסרת נקודה, CtrlOrCmd+D לשכפל, או גרור להזזה", "lineEditor_pointSelected": "לחץ על Delete להסרת נקודה, CtrlOrCmd+D לשכפל, או גרור להזזה",
"lineEditor_nothingSelected": "בחר נקודה להזזה או הסרה, או החזק את כפתור Alt והקלק להוספת נקודות חדשות", "lineEditor_nothingSelected": "בחר נקודה להזזה או הסרה, או החזק את כפתור Alt והקלק להוספת נקודות חדשות",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה", "cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.", "tooltip": "הרישומים שלך מוצפנים מקצה לקצה כך שהשרתים של Excalidraw לא יראו אותם לעולם.",
"link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw" "link": "פוסט בבלוג על הצפנה מקצה לקצב ב-Excalidraw"
@@ -289,6 +345,7 @@
"width": "רוחב" "width": "רוחב"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "העתק סגנונות.", "copyStyles": "העתק סגנונות.",
"copyToClipboard": "הועתק אל הלוח.", "copyToClipboard": "הועתק אל הלוח.",
"copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} הועתקה ללוח כ-PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "कैनवास रीसेट करें", "clearReset": "कैनवास रीसेट करें",
@@ -135,7 +137,11 @@
"zenMode": "ज़ेन मोड", "zenMode": "ज़ेन मोड",
"exitZenMode": "जेन मोड से बाहर निकलें", "exitZenMode": "जेन मोड से बाहर निकलें",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "इससे पूरा कैनवास साफ हो जाएगा। क्या आपको यकीन है?", "clearReset": "इससे पूरा कैनवास साफ हो जाएगा। क्या आपको यकीन है?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है", "cannotRestoreFromImage": "छवि फ़ाइल बहाल दृश्य नहीं है",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "बिंदुओं को संपादित करने के लिए Enter पर डबल-क्लिक करें या दबाएँ", "lineEditor_info": "बिंदुओं को संपादित करने के लिए Enter पर डबल-क्लिक करें या दबाएँ",
"lineEditor_pointSelected": "बिंदु हटाने के लिए डिलीट दबाएं, प्रतिरूपित करने के लिए कण्ट्रोल या कमांड डी दबाएं या स्थानांतरित करने के लिए खींचे", "lineEditor_pointSelected": "बिंदु हटाने के लिए डिलीट दबाएं, प्रतिरूपित करने के लिए कण्ट्रोल या कमांड डी दबाएं या स्थानांतरित करने के लिए खींचे",
"lineEditor_nothingSelected": "स्थानांतरित करने या हटाने के लिए एक बिंदु का चयन करें, या Alt दबाए रखें और नए बिंदुओं को जोड़ने के लिए क्लिक करें", "lineEditor_nothingSelected": "स्थानांतरित करने या हटाने के लिए एक बिंदु का चयन करें, या Alt दबाए रखें और नए बिंदुओं को जोड़ने के लिए क्लिक करें",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं", "cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।", "tooltip": "आपके चित्र अंत-से-अंत एन्क्रिप्टेड हैं, इसलिए एक्सक्लूसिव्रॉव के सर्वर उन्हें कभी नहीं देखेंगे।",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "चौड़ाई" "width": "चौड़ाई"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "काॅपी कीए स्टाइल", "copyStyles": "काॅपी कीए स्टाइल",
"copyToClipboard": "क्लिपबोर्ड में कॉपी कीए", "copyToClipboard": "क्लिपबोर्ड में कॉपी कीए",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Vászon törlése", "clearReset": "Vászon törlése",
@@ -135,7 +137,11 @@
"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": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"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?",
@@ -157,6 +163,7 @@
"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": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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 delete gombot a pont eltávolításához, Ctrl vagy Cmd + D-t a duplikáláshoz, vagy húzva mozgasd",
"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álassz ki egy pontot a mozgatáshoz vagy törtléshez, vagy az Alt lenyomása mellett kattintva hozz létre új pontokat",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Előnézet nem jeleníthető meg", "cannotShowPreview": "Előnézet nem jeleníthető meg",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "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": ""
@@ -289,6 +345,7 @@
"width": "Szélesség" "width": "Szélesség"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Bagikan", "share": "Bagikan",
"showStroke": "Tampilkan garis pengambil warna", "showStroke": "Tampilkan garis pengambil warna",
"showBackground": "Tampilkan latar pengambil warna", "showBackground": "Tampilkan latar pengambil warna",
"toggleTheme": "Ubah tema" "toggleTheme": "Ubah tema",
"personalLib": "Pustaka Pribadi",
"excalidrawLib": "Pustaka Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Setel Ulang Kanvas", "clearReset": "Setel Ulang Kanvas",
@@ -135,7 +137,11 @@
"zenMode": "Mode zen", "zenMode": "Mode zen",
"exitZenMode": "Keluar dari mode zen", "exitZenMode": "Keluar dari mode zen",
"cancel": "Batal", "cancel": "Batal",
"clear": "Hapus" "clear": "Hapus",
"remove": "Hapus",
"publishLibrary": "Terbitkan",
"submit": "Kirimkan",
"confirm": "Konfirmasi"
}, },
"alerts": { "alerts": {
"clearReset": "Ini akan menghapus semua yang ada dikanvas. Apakah kamu yakin ?", "clearReset": "Ini akan menghapus semua yang ada dikanvas. Apakah kamu yakin ?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini", "cannotRestoreFromImage": "Pemandangan tidak dapat dipulihkan dari file gambar ini",
"invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.", "invalidSceneUrl": "Tidak dapat impor pemandangan dari URL. Kemungkinan URL itu rusak atau tidak berisi data JSON Excalidraw yang valid.",
"resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?", "resetLibrary": "Ini akan menghapus pustaka Anda. Anda yakin?",
"removeItemsFromsLibrary": "Hapus {{count}} item dari pustaka?",
"invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan." "invalidEncryptionKey": "Sandi enkripsi harus 22 karakter. Kolaborasi langsung dinonaktifkan."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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 sebuah titik untuk memindah atau menghapus, atau tekan Alt dan klik untuk menambahkan 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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Tidak dapat menampilkan pratinjau", "cannotShowPreview": "Tidak dapat menampilkan pratinjau",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Hapus kanvas" "title": "Hapus kanvas"
}, },
"publishDialog": {
"title": "Terbitkan pustaka",
"itemName": "Nama item",
"authorName": "Nama pembuat",
"githubUsername": "Nama pengguna github",
"twitterUsername": "Nama pengguna Twitter",
"libraryName": "Nama Pustaka",
"libraryDesc": "Deskripsi pustaka",
"website": "Situs Web",
"placeholder": {
"authorName": "Nama atau nama pengguna Anda",
"libraryName": "Nama dari pustaka Anda",
"libraryDesc": "Deskripsi pustaka Anda untuk membantu orang mengerti penggunaannya",
"githubHandle": "Akun GitHub (opsional), jadi Anda dapat mengubah pustaka ketika diserahkan untuk review",
"twitterHandle": "Nama pengguna Twitter (opsional), jadi kami tahu siapa dipuji ketika mempromosikannya melalui Twitter",
"website": "Hubungkan ke situs personal Anda atau lainnya (opsional)"
},
"errors": {
"required": "Dibutuhkan",
"website": "Masukkan URL valid"
},
"noteDescription": {
"pre": "Kirimkan pustaka Anda untuk disertakan di ",
"link": "repositori pustaka publik",
"post": "untuk orang lain menggunakannya dalam gambar mereka."
},
"noteGuidelines": {
"pre": "Pustaka butuh disetujui secara manual terlebih dahulu. Baca ",
"link": "pedoman",
"post": " sebelum mengirim. Anda butuh akun GitHub untuk berkomunikasi dan membuat perubahan jika dibutuhkan, tetapi tidak wajib dibutukan."
},
"noteLicense": {
"pre": "Dengan mengkirimkannya, Anda setuju pustaka akan diterbitkan dibawah ",
"link": "Lisensi MIT, ",
"post": "yang artinya siapa pun dapat menggunakannya tanpa batasan."
},
"noteItems": "Setiap item pustaka harus memiliki nama, sehingga bisa disortir. Item pustaka di bawah ini akan dimasukan:",
"atleastOneLibItem": "Pilih setidaknya satu item pustaka untuk mulai"
},
"publishSuccessDialog": {
"title": "Pustaka telah dikirm",
"content": "Terima kasih {{authorName}}. pustaka Anda telah diserahkan untuk ditinjau ulang. Anda dapat cek statusnya",
"link": "di sini"
},
"confirmDialog": {
"resetLibrary": "Reset pustaka",
"removeItemsFromLib": "Hapus item yang dipilih dari pustaka"
},
"encrypted": { "encrypted": {
"tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.", "tooltip": "Gambar anda terenkripsi end-to-end sehingga server Excalidraw tidak akan pernah dapat melihatnya.",
"link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw" "link": "Pos blog tentang enkripsi ujung ke ujung di Excalidraw"
@@ -289,6 +345,7 @@
"width": "Lebar" "width": "Lebar"
}, },
"toast": { "toast": {
"addedToLibrary": "Tambahkan ke pustaka",
"copyStyles": "Gaya tersalin.", "copyStyles": "Gaya tersalin.",
"copyToClipboard": "Tersalin ke papan klip.", "copyToClipboard": "Tersalin ke papan klip.",
"copyToClipboardAsPng": "Tersalin {{exportSelection}} ke clipboard sebagai PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Tersalin {{exportSelection}} ke clipboard sebagai PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Condividi", "share": "Condividi",
"showStroke": "Mostra selettore colore del tratto", "showStroke": "Mostra selettore colore del tratto",
"showBackground": "Mostra selettore colore di sfondo", "showBackground": "Mostra selettore colore di sfondo",
"toggleTheme": "Cambia tema" "toggleTheme": "Cambia tema",
"personalLib": "Libreria Personale",
"excalidrawLib": "Libreria di Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Svuota la tela", "clearReset": "Svuota la tela",
@@ -135,7 +137,11 @@
"zenMode": "Modalità Zen", "zenMode": "Modalità Zen",
"exitZenMode": "Uscire dalla modalità zen", "exitZenMode": "Uscire dalla modalità zen",
"cancel": "Annulla", "cancel": "Annulla",
"clear": "Cancella" "clear": "Cancella",
"remove": "Rimuovi",
"publishLibrary": "Pubblica",
"submit": "Invia",
"confirm": "Conferma"
}, },
"alerts": { "alerts": {
"clearReset": "Questa azione cancellerà l'intera tela. Sei sicuro?", "clearReset": "Questa azione cancellerà l'intera tela. Sei sicuro?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine", "cannotRestoreFromImage": "Impossibile ripristinare la scena da questo file immagine",
"invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.", "invalidSceneUrl": "Impossibile importare la scena dall'URL fornito. Potrebbe essere malformato o non contenere dati JSON Excalidraw validi.",
"resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?", "resetLibrary": "Questa azione cancellerà l'intera libreria. Sei sicuro?",
"removeItemsFromsLibrary": "Eliminare {{count}} elementi dalla libreria?",
"invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata." "invalidEncryptionKey": "La chiave di cifratura deve essere composta da 22 caratteri. La collaborazione live è disabilitata."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"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": "Premere Elimina per rimuovere il punto, CtrlOrCmd+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 per spostare o rimuovere, oppure 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"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Impossibile visualizzare l'anteprima", "cannotShowPreview": "Impossibile visualizzare l'anteprima",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Svuota la tela" "title": "Svuota la tela"
}, },
"publishDialog": {
"title": "Pubblica la libreria",
"itemName": "Nome dell'elemento",
"authorName": "Nome dell'autore",
"githubUsername": "Nome utente di GitHub",
"twitterUsername": "Nome utente di Twitter",
"libraryName": "Nome della libreria",
"libraryDesc": "Descrizione della libreria",
"website": "Sito Web",
"placeholder": {
"authorName": "Il tuo nome o nome utente",
"libraryName": "Nome della tua libreria",
"libraryDesc": "Descrizione della tua libreria per aiutare le persone a comprenderne lo scopo",
"githubHandle": "Handle di GitHub (opzionale), così che tu possa modificare la libreria una volta inviata per la revisione",
"twitterHandle": "Nome utente di Twitter (opzionale), così che sappiamo chi accreditare promuovendo su Twitter",
"website": "Link al tuo sito web personale o altro (opzionale)"
},
"errors": {
"required": "Obbligatorio",
"website": "Inserisci un URL valido"
},
"noteDescription": {
"pre": "Invia la tua libreria da includere nella ",
"link": "repository della libreria pubblica",
"post": "perché sia usata da altri nei loro disegni."
},
"noteGuidelines": {
"pre": "La libreria dev'esser prima approvata manualmente. Sei pregato di leggere le ",
"link": "linee guida",
"post": " prima di inviarla. Necessiterai di un profilo di GitHub per comunicare ed effettuare modifiche se richiesto, ma non è strettamente necessario."
},
"noteLicense": {
"pre": "Inviando, acconsenti che la libreria sarà pubblicata sotto la ",
"link": "Licenza MIT, ",
"post": "che in breve significa che chiunque possa usarla senza restrizioni."
},
"noteItems": "Ogni elemento della libreria deve avere il proprio nome, così che sia filtrabile. Gli elementi della seguente libreria saranno inclusi:",
"atleastOneLibItem": "Sei pregato di selezionare almeno un elemento della libreria per iniziare"
},
"publishSuccessDialog": {
"title": "Libreria inviata",
"content": "Grazie {{authorName}}. La tua libreria è stata inviata per la revisione. Puoi monitorarne lo stato",
"link": "qui"
},
"confirmDialog": {
"resetLibrary": "Ripristina la libreria",
"removeItemsFromLib": "Rimuovi gli elementi selezionati dalla libreria"
},
"encrypted": { "encrypted": {
"tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.", "tooltip": "I tuoi disegni sono crittografati end-to-end in modo che i server di Excalidraw non li possano mai vedere.",
"link": "Articolo del blog sulla crittografia end-to-end di Excalidraw" "link": "Articolo del blog sulla crittografia end-to-end di Excalidraw"
@@ -289,6 +345,7 @@
"width": "Larghezza" "width": "Larghezza"
}, },
"toast": { "toast": {
"addedToLibrary": "Aggiunto alla libreria",
"copyStyles": "Stili copiati.", "copyStyles": "Stili copiati.",
"copyToClipboard": "Copiato negli appunti.", "copyToClipboard": "Copiato negli appunti.",
"copyToClipboardAsPng": "{{exportSelection}} copiato negli appunti come PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} copiato negli appunti come PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "共有", "share": "共有",
"showStroke": "ストロークカラーピッカーを表示", "showStroke": "ストロークカラーピッカーを表示",
"showBackground": "背景色ピッカーを表示", "showBackground": "背景色ピッカーを表示",
"toggleTheme": "テーマの切り替え" "toggleTheme": "テーマの切り替え",
"personalLib": "個人ライブラリ",
"excalidrawLib": "Excalidrawライブラリ"
}, },
"buttons": { "buttons": {
"clearReset": "キャンバスのリセット", "clearReset": "キャンバスのリセット",
@@ -135,7 +137,11 @@
"zenMode": "Zenモード", "zenMode": "Zenモード",
"exitZenMode": "集中モードをやめる", "exitZenMode": "集中モードをやめる",
"cancel": "キャンセル", "cancel": "キャンセル",
"clear": "消去" "clear": "消去",
"remove": "削除",
"publishLibrary": "公開",
"submit": "送信",
"confirm": "確認"
}, },
"alerts": { "alerts": {
"clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?", "clearReset": "この操作によってキャンバス全体が消えます。よろしいですか?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした", "cannotRestoreFromImage": "このイメージファイルからシーンを復元できませんでした",
"invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。", "invalidSceneUrl": "指定された URL からシーンをインポートできませんでした。不正な形式であるか、有効な Excalidraw JSON データが含まれていません。",
"resetLibrary": "ライブラリを消去します。本当によろしいですか?", "resetLibrary": "ライブラリを消去します。本当によろしいですか?",
"removeItemsFromsLibrary": "{{count}} 個のアイテムをライブラリから削除しますか?",
"invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。" "invalidEncryptionKey": "暗号化キーは22文字でなければなりません。ライブコラボレーションは無効化されています。"
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します", "lineEditor_info": "ポイントを編集するには、ダブルクリックまたはEnterキーを押します",
"lineEditor_pointSelected": "削除ボタンを押して点を削除します。Ctrl+D または Cmd+D で複製します。またはドラッグして移動します", "lineEditor_pointSelected": "削除ボタンを押して点を削除します。Ctrl+D または Cmd+D で複製します。またはドラッグして移動します",
"lineEditor_nothingSelected": "移動または削除する点を選択するか、Altキーを押しながらクリックして新しい点を追加します", "lineEditor_nothingSelected": "移動または削除する点を選択するか、Altキーを押しながらクリックして新しい点を追加します",
"placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します" "placeImage": "クリックして画像を配置するか、クリックしてドラッグしてサイズを手動で設定します",
"publishLibrary": "自分のライブラリを公開"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "プレビューを表示できません", "cannotShowPreview": "プレビューを表示できません",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "キャンバスを消去" "title": "キャンバスを消去"
}, },
"publishDialog": {
"title": "ライブラリを公開",
"itemName": "アイテム名",
"authorName": "作成者名",
"githubUsername": "GitHub ユーザ名",
"twitterUsername": "Twitter ユーザ名",
"libraryName": "ライブラリ名",
"libraryDesc": "ライブラリの説明",
"website": "Webサイト",
"placeholder": {
"authorName": "お名前またはユーザー名",
"libraryName": "あなたのライブラリ名",
"libraryDesc": "ライブラリの使い方を理解するための説明",
"githubHandle": "GitHubハンドル(任意)。一度レビューのために送信されると、ライブラリを編集できます",
"twitterHandle": "Twitterのユーザー名 (任意)。Twitterでプロモーションする際にクレジットする人を知っておくためのものです",
"website": "個人のウェブサイトまたは他のサイトへのリンク (オプション)"
},
"errors": {
"required": "必須項目",
"website": "有効な URL を入力してください"
},
"noteDescription": {
"pre": "以下に含めるライブラリを提出してください ",
"link": "公開ライブラリのリポジトリ",
"post": "他の人が作図に使えるようにするためです"
},
"noteGuidelines": {
"pre": "最初にライブラリを手動で承認する必要があります。次をお読みください ",
"link": "ガイドライン",
"post": " 送信する前に、GitHubアカウントが必要になりますが、必須ではありません。"
},
"noteLicense": {
"pre": "提出することにより、ライブラリが次の下で公開されることに同意します: ",
"link": "MIT ライセンス",
"post": "つまり誰でも制限なく使えるということです"
},
"noteItems": "",
"atleastOneLibItem": "開始するには少なくとも1つのライブラリ項目を選択してください"
},
"publishSuccessDialog": {
"title": "ライブラリを送信しました",
"content": "{{authorName}} さん、ありがとうございます。あなたのライブラリはレビューのために提出されました。状況を追跡できます。",
"link": "こちら"
},
"confirmDialog": {
"resetLibrary": "ライブラリをリセット",
"removeItemsFromLib": "選択したアイテムをライブラリから削除"
},
"encrypted": { "encrypted": {
"tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。", "tooltip": "描画内容はエンドツーエンド暗号化が施されており、Excalidrawサーバーが内容を見ることはできません。",
"link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事" "link": "Excalidrawのエンドツーエンド暗号化に関するブログ記事"
@@ -289,6 +345,7 @@
"width": "幅" "width": "幅"
}, },
"toast": { "toast": {
"addedToLibrary": "ライブラリに追加しました",
"copyStyles": "スタイルをコピーしました。", "copyStyles": "スタイルをコピーしました。",
"copyToClipboard": "クリップボードにコピー", "copyToClipboard": "クリップボードにコピー",
"copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} を PNG 形式でクリップボードにコピーしました\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Bḍu", "share": "Bḍu",
"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": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Ales awennez n teɣzut n usuneɣ", "clearReset": "Ales awennez n teɣzut n usuneɣ",
@@ -135,7 +137,11 @@
"zenMode": "Askar Zen", "zenMode": "Askar Zen",
"exitZenMode": "Ffeɣ seg uskar Zen", "exitZenMode": "Ffeɣ seg uskar Zen",
"cancel": "Sefsex", "cancel": "Sefsex",
"clear": "Sfeḍ" "clear": "Sfeḍ",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?", "clearReset": "Ayagi ad isfeḍ akk taɣzut n usuneɣ. Tetḥeqqeḍ?",
@@ -157,6 +163,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": "",
"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": {
@@ -199,7 +206,8 @@
"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ḍt, CtrlOrCmd+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 ara tesmuttiḍ neɣ ara tekkseḍ, neɣ ṭṭef taqeffalt 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": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Ulamek abeqqeḍ n teskant", "cannotShowPreview": "Ulamek abeqqeḍ n teskant",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "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. ",
"link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw" "link": "Amagrad ɣef uwgelhen ixef s ixef di Excalidraw"
@@ -289,6 +345,7 @@
"width": "Tehri" "width": "Tehri"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"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}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "", "clearReset": "",
@@ -135,7 +137,11 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "", "exitZenMode": "",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "", "clearReset": "",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "", "cannotRestoreFromImage": "",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "", "lineEditor_info": "",
"lineEditor_pointSelected": "", "lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "", "lineEditor_nothingSelected": "",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "", "cannotShowPreview": "",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Сіздің сызбаларыңыз өтпелі шифрлеу арқылы шифрланған, сондықтан Excalidraw серверлері оларды ешқашан көрмейді.", "tooltip": "Сіздің сызбаларыңыз өтпелі шифрлеу арқылы шифрланған, сондықтан Excalidraw серверлері оларды ешқашан көрмейді.",
"link": "Excalidraw қолданатын өтпелі шифрлеу туралы блог жазбасы" "link": "Excalidraw қолданатын өтпелі шифрлеу туралы блог жазбасы"
@@ -289,6 +345,7 @@
"width": "Ені" "width": "Ені"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Стильдер көшірілді.", "copyStyles": "Стильдер көшірілді.",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "캔버스 초기화", "clearReset": "캔버스 초기화",
@@ -135,7 +137,11 @@
"zenMode": "젠 모드", "zenMode": "젠 모드",
"exitZenMode": "젠 모드 종료하기", "exitZenMode": "젠 모드 종료하기",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "모든 작업 내용이 초기화됩니다. 계속하시겠습니까?", "clearReset": "모든 작업 내용이 초기화됩니다. 계속하시겠습니까?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다", "cannotRestoreFromImage": "이미지 파일에서 화면을 복구할 수 없었습니다",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "지점을 수정하려면 두 번 클릭하거나 Enter 키를 누르세요.", "lineEditor_info": "지점을 수정하려면 두 번 클릭하거나 Enter 키를 누르세요.",
"lineEditor_pointSelected": "제거하려면 Delete 키, 복제하려면 CtrlOrCmd+D, 이동하려면 드래그하세요.", "lineEditor_pointSelected": "제거하려면 Delete 키, 복제하려면 CtrlOrCmd+D, 이동하려면 드래그하세요.",
"lineEditor_nothingSelected": "옮기거나 지울 지점을 선택하거나, Alt를 누른 상태로 클릭해 새 지점을 만드세요", "lineEditor_nothingSelected": "옮기거나 지울 지점을 선택하거나, Alt를 누른 상태로 클릭해 새 지점을 만드세요",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "미리보기를 볼 수 없습니다", "cannotShowPreview": "미리보기를 볼 수 없습니다",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.", "tooltip": "그림은 종단 간 암호화되므로 Excalidraw의 서버는 절대로 내용을 알 수 없습니다.",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "너비" "width": "너비"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "스타일 복사.", "copyStyles": "스타일 복사.",
"copyToClipboard": "클립보드로 복사.", "copyToClipboard": "클립보드로 복사.",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+61 -4
View File
@@ -43,7 +43,7 @@
"exportEmbedScene": "Iegult ainu", "exportEmbedScene": "Iegult ainu",
"exportEmbedScene_details": "Ainas dati tiks iekļauti saglabātajā PNG/SVG datnē, lai no tās būtu iespējams ainu atgūt. Tas palielinās datnes izmēru.", "exportEmbedScene_details": "Ainas dati tiks iekļauti saglabātajā PNG/SVG datnē, lai no tās būtu iespējams ainu atgūt. Tas palielinās datnes izmēru.",
"addWatermark": "Pievienot \"Radīts ar Excalidraw\"", "addWatermark": "Pievienot \"Radīts ar Excalidraw\"",
"handDrawn": "Ar roku zīmēts", "handDrawn": "Rokraksts",
"normal": "Parasts", "normal": "Parasts",
"code": "Kods", "code": "Kods",
"small": "Mazs", "small": "Mazs",
@@ -100,7 +100,9 @@
"share": "Kopīgot", "share": "Kopīgot",
"showStroke": "Rādīt svītras krāsas atlasītāju", "showStroke": "Rādīt svītras krāsas atlasītāju",
"showBackground": "Rādīt fona krāsas atlasītāju", "showBackground": "Rādīt fona krāsas atlasītāju",
"toggleTheme": "Pārslēgt krāsu tēmu" "toggleTheme": "Pārslēgt krāsu tēmu",
"personalLib": "Personīgā bibliotēka",
"excalidrawLib": "Excalidraw bibliotēka"
}, },
"buttons": { "buttons": {
"clearReset": "Atiestatīt tāfeli", "clearReset": "Atiestatīt tāfeli",
@@ -135,7 +137,11 @@
"zenMode": "Zen režīms", "zenMode": "Zen režīms",
"exitZenMode": "Pamest Zen režīmu", "exitZenMode": "Pamest Zen režīmu",
"cancel": "Atcelt", "cancel": "Atcelt",
"clear": "Notīrīt" "clear": "Notīrīt",
"remove": "Noņemt",
"publishLibrary": "Publicēt",
"submit": "Iesniegt",
"confirm": "Apstiprināt"
}, },
"alerts": { "alerts": {
"clearReset": "Šī funkcija notīrīs visu tāfeli. Vai turpināt?", "clearReset": "Šī funkcija notīrīs visu tāfeli. Vai turpināt?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Ainu nevarēja atgūt no attēla faila", "cannotRestoreFromImage": "Ainu nevarēja atgūt no attēla faila",
"invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.", "invalidSceneUrl": "Nevarēja importēt ainu no norādītā URL. Vai nu tas ir nederīgs, vai nesatur derīgus Excalidraw JSON datus.",
"resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?", "resetLibrary": "Šī funkcija iztukšos bibliotēku. Vai turpināt?",
"removeItemsFromsLibrary": "Vai izņemt {{count}} vienumu(s) no bibliotēkas?",
"invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta." "invalidEncryptionKey": "Šifrēšanas atslēgai jābūt 22 simbolus garai. Tiešsaistes sadarbība ir izslēgta."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "Dubultklikšķiniet vai spiediet ievades taustiņu, lai rediģētu punktus", "lineEditor_info": "Dubultklikšķiniet vai spiediet ievades taustiņu, lai rediģētu punktus",
"lineEditor_pointSelected": "Spiediet dzēšanas taustiņu, lai noņemtu punktu, CtrlOrCmd+D, lai to kopētu, vai velciet, lai pārvietotu", "lineEditor_pointSelected": "Spiediet dzēšanas taustiņu, lai noņemtu punktu, CtrlOrCmd+D, lai to kopētu, vai velciet, lai pārvietotu",
"lineEditor_nothingSelected": "Atlasiet punktu, lai to pārvietotu vai noņemtu; lai pievienotu jaunus punktus, turiet nospiestu Alt taustiņu", "lineEditor_nothingSelected": "Atlasiet punktu, lai to pārvietotu vai noņemtu; lai pievienotu jaunus punktus, turiet nospiestu Alt taustiņu",
"placeImage": "Klikšķiniet, lai novietotu attēlu, vai spiediet un velciet, lai iestatītu tā izmēru" "placeImage": "Klikšķiniet, lai novietotu attēlu, vai spiediet un velciet, lai iestatītu tā izmēru",
"publishLibrary": "Publicēt savu bibliotēku"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Nevar rādīt priekšskatījumu", "cannotShowPreview": "Nevar rādīt priekšskatījumu",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Notīrīt tāfeli" "title": "Notīrīt tāfeli"
}, },
"publishDialog": {
"title": "Publicēt bibliotēku",
"itemName": "Vienuma nosaukums",
"authorName": "Autora vārds",
"githubUsername": "GitHub lietotājvārds",
"twitterUsername": "Twitter lietotājvārds",
"libraryName": "Bibliotēkas nosaukums",
"libraryDesc": "Bibliotēkas apraksts",
"website": "Mājaslapa",
"placeholder": {
"authorName": "Jūsu vārds vai lietotājvārds",
"libraryName": "Jūsu bibliotēkas nosaukums",
"libraryDesc": "Bibliotēkas apraksts, kas palīdzēs citiem saprast tās pielietojumu",
"githubHandle": "GitHub lietotājvārds (neobligāts), lai jūs varētu rediģēt bibliotēku pēc tās iesniegšanas izskatīšanai",
"twitterHandle": "Twitter lietotājvārds (neobligāts), lai mēs varētu jūs pieminēt kā autoru, kad reklamēsim bibliotēku platformā Twitter",
"website": "Saikne uz jūsu personīgo mājaslapu vai kādu citu lapu (neobligāta)"
},
"errors": {
"required": "Obligāts",
"website": "Ievadiet derīgu URL"
},
"noteDescription": {
"pre": "Iesniegt savu bibliotēku iekļaušanai ",
"link": "publiskajā bibliotēku datubāzē",
"post": ", lai citi to varētu izmantot savos zīmējumos."
},
"noteGuidelines": {
"pre": "Šai bibliotēkai vispirms jātiek manuāli apstiprinātai. Lūdzu, izlasiet ",
"link": "norādījumus",
"post": " pirms iesniegšanas. Jums vajadzēs GitHub kontu, lai sazinātos un veiktu izmaiņas, ja tādas būs pieprasītas, bet tas nav absolūti nepieciešams."
},
"noteLicense": {
"pre": "Iesniedzot bibliotēku, jūs piekrītat tās publicēšanai saskaņā ar ",
"link": "MIT Licenci, ",
"post": "kas īsumā nozīmē, ka jebkurš to varēs izmantot bez ierobežojumiem."
},
"noteItems": "Katram bibliotēkas vienumam jābūt savam nosaukumam, lai to varētu atrast filtrējot. Tiks iekļauti sekojošie bibliotēkas vienumi:",
"atleastOneLibItem": "Lūdzu, atlasiet vismaz vienu bibliotēkas vienumu, lai sāktu darbu"
},
"publishSuccessDialog": {
"title": "Bibliotēka iesniegta",
"content": "Paldies, {{authorName}}! Jūsu bibliotēka iesniegta izskatīšanai. Jūs varat izsekot iesnieguma statusam",
"link": "šeit"
},
"confirmDialog": {
"resetLibrary": "Atiestatīt bibliotēku",
"removeItemsFromLib": "Noņemt atlasītos vienumus no bibliotēkas"
},
"encrypted": { "encrypted": {
"tooltip": "Jūsu zīmējumi ir šifrēti no gala līdz galam; līdz ar to Excalidraw serveri tos nekad neredzēs.", "tooltip": "Jūsu zīmējumi ir šifrēti no gala līdz galam; līdz ar to Excalidraw serveri tos nekad neredzēs.",
"link": "Ieraksts par šifrēšanu no gala līdz galam Excalidraw blogā" "link": "Ieraksts par šifrēšanu no gala līdz galam Excalidraw blogā"
@@ -289,6 +345,7 @@
"width": "Platums" "width": "Platums"
}, },
"toast": { "toast": {
"addedToLibrary": "Pievienots bibliotēkai",
"copyStyles": "Nokopēja stilus.", "copyStyles": "Nokopēja stilus.",
"copyToClipboard": "Nokopēja starpliktuvē.", "copyToClipboard": "Nokopēja starpliktuvē.",
"copyToClipboardAsPng": "Nokopēja {{exportSelection}} starpliktuvē kā PNG ({{exportColorScheme}})", "copyToClipboardAsPng": "Nokopēja {{exportSelection}} starpliktuvē kā PNG ({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "", "share": "",
"showStroke": "", "showStroke": "",
"showBackground": "", "showBackground": "",
"toggleTheme": "" "toggleTheme": "",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "ကားချပ်ရှင်းလင်း", "clearReset": "ကားချပ်ရှင်းလင်း",
@@ -135,7 +137,11 @@
"zenMode": "", "zenMode": "",
"exitZenMode": "ဇင်မြင်ကွင်းမှထွက်", "exitZenMode": "ဇင်မြင်ကွင်းမှထွက်",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "ကားချပ်တစ်ခုလုံးရှင်းလင်းပါတော့မည်။ အတည်ပြုပါ။", "clearReset": "ကားချပ်တစ်ခုလုံးရှင်းလင်းပါတော့မည်။ အတည်ပြုပါ။",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "ဤပုံဖြင့်မြင်ကွင်းပြန်လည်မရယူနိုင်ပါ။", "cannotRestoreFromImage": "ဤပုံဖြင့်မြင်ကွင်းပြန်လည်မရယူနိုင်ပါ။",
"invalidSceneUrl": "", "invalidSceneUrl": "",
"resetLibrary": "", "resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "အမှတ်များပြင်ဆင်သတ်မှတ်ရင် ကလစ်နှစ်ချက် (သို့) Enter ကိုနှိပ်ပါ", "lineEditor_info": "အမှတ်များပြင်ဆင်သတ်မှတ်ရင် ကလစ်နှစ်ချက် (သို့) Enter ကိုနှိပ်ပါ",
"lineEditor_pointSelected": "အမှတ်များအား ဖျက်ရန် Delete နှင့် ပွားရန် Ctrl/Cmd + D သုံးပါ၊ ရွှေ့လိုပါက တရွတ်ဆွဲပါ", "lineEditor_pointSelected": "အမှတ်များအား ဖျက်ရန် Delete နှင့် ပွားရန် Ctrl/Cmd + D သုံးပါ၊ ရွှေ့လိုပါက တရွတ်ဆွဲပါ",
"lineEditor_nothingSelected": "ရွှေ့လို (သို့) ဖယ်ရှားလိုသောအမှတ်ကိုရွေးပါ၊ Alt နှင့် ကလစ်တွဲနှိပ်၍လည်းအမှတ်အသစ်ထပ်ထည့်နိုင်သည်", "lineEditor_nothingSelected": "ရွှေ့လို (သို့) ဖယ်ရှားလိုသောအမှတ်ကိုရွေးပါ၊ Alt နှင့် ကလစ်တွဲနှိပ်၍လည်းအမှတ်အသစ်ထပ်ထည့်နိုင်သည်",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "နမူနာမပြသနိုင်ပါ", "cannotShowPreview": "နမူနာမပြသနိုင်ပါ",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။", "tooltip": "ရေးဆွဲထားသောပုံများအား နှစ်ဘက်စွန်းတိုင်လျှို့ဝှက်ထားသဖြင့် Excalidraw ၏ဆာဗာများပင်လျှင်မြင်တွေ့ရမည်မဟုတ်ပါ။",
"link": "" "link": ""
@@ -289,6 +345,7 @@
"width": "အကျယ်" "width": "အကျယ်"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "", "copyStyles": "",
"copyToClipboard": "", "copyToClipboard": "",
"copyToClipboardAsPng": "", "copyToClipboardAsPng": "",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Del", "share": "Del",
"showStroke": "Vis fargevelger for kantfarge", "showStroke": "Vis fargevelger for kantfarge",
"showBackground": "Vis fargevelger for bakgrunnsfarge", "showBackground": "Vis fargevelger for bakgrunnsfarge",
"toggleTheme": "Veksle tema" "toggleTheme": "Veksle tema",
"personalLib": "Personlig bibliotek",
"excalidrawLib": "Excalidraw-bibliotek"
}, },
"buttons": { "buttons": {
"clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen", "clearReset": "Tøm lerretet og tilbakestill bakgrunnsfargen",
@@ -135,7 +137,11 @@
"zenMode": "Zen-modus", "zenMode": "Zen-modus",
"exitZenMode": "Avslutt zen-modus", "exitZenMode": "Avslutt zen-modus",
"cancel": "Avbryt", "cancel": "Avbryt",
"clear": "Tøm" "clear": "Tøm",
"remove": "Fjern",
"publishLibrary": "Publiser",
"submit": "Send inn",
"confirm": "Bekreft"
}, },
"alerts": { "alerts": {
"clearReset": "Dette vil tømme lerretet. Er du sikker?", "clearReset": "Dette vil tømme lerretet. Er du sikker?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Scenen kunne ikke gjenopprettes fra denne bildefilen", "cannotRestoreFromImage": "Scenen kunne ikke gjenopprettes fra denne bildefilen",
"invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.", "invalidSceneUrl": "Kunne ikke importere scene fra den oppgitte URL-en. Den er enten ødelagt, eller inneholder ikke gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?", "resetLibrary": "Dette vil tømme biblioteket ditt. Er du sikker?",
"removeItemsFromsLibrary": "Slett {{count}} element(er) fra biblioteket?",
"invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert." "invalidEncryptionKey": "Krypteringsnøkkel må ha 22 tegn. Live-samarbeid er deaktivert."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkter", "lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkter",
"lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, Ctrl / Cmd+D for å duplisere, eller dra for å flytte", "lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, Ctrl / Cmd+D for å duplisere, eller dra for å flytte",
"lineEditor_nothingSelected": "Velg et punkt å flytte eller fjerne, eller hold Alt og klikk for å legge til nye punkter", "lineEditor_nothingSelected": "Velg et punkt å flytte eller fjerne, eller hold Alt og klikk for å legge til nye punkter",
"placeImage": "Klikk for å plassere bildet, eller klikk og dra for å angi størrelsen manuelt" "placeImage": "Klikk for å plassere bildet, eller klikk og dra for å angi størrelsen manuelt",
"publishLibrary": "Publiser ditt eget bibliotek"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Kan ikke vise forhåndsvisning", "cannotShowPreview": "Kan ikke vise forhåndsvisning",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "Tøm lerret" "title": "Tøm lerret"
}, },
"publishDialog": {
"title": "Publiser bibliotek",
"itemName": "Elementnavn",
"authorName": "Forfatterens navn",
"githubUsername": "GitHub-brukernavnet",
"twitterUsername": "Twitter-brukernavn",
"libraryName": "Biblioteknavn",
"libraryDesc": "Beskrivelse av bibliotek",
"website": "Nettsted",
"placeholder": {
"authorName": "Ditt navn eller brukernavn",
"libraryName": "Navnet på biblioteket ditt",
"libraryDesc": "Beskrivelse av biblioteket ditt for å hjelpe folk med å forstå bruken",
"githubHandle": "Github-brukernavn (valgfritt), slik at du kan redigere biblioteket når du har sendt inn for gjennomgang",
"twitterHandle": "Twitter-brukernavn (valgfritt), slik at vi vet hvem vi skal kreditere når promotert på Twitter",
"website": "Lenke til din personlige nettside eller et annet sted (valgfritt)"
},
"errors": {
"required": "Påkrevd",
"website": "Angi en gyldig nettadresse"
},
"noteDescription": {
"pre": "Send inn biblioteket ditt som skal inkluderes i ",
"link": "kildekode for offentlig bibliotek",
"post": "for andre å bruke dem i tegninger."
},
"noteGuidelines": {
"pre": "Biblioteket må godkjennes manuelt først. Les ",
"link": "retningslinjene",
"post": " før innsending. Du vil trenge en GitHub-konto for å kommunisere og gjøre endringer hvis ønsket, men det er ikke påkrevd."
},
"noteLicense": {
"pre": "Ved å sende inn godtar du at biblioteket blir publisert under ",
"link": "MIT-lisens, ",
"post": "som kortfattet betyr at andre kan bruke dem uten begrensninger."
},
"noteItems": "Hvert bibliotek må ha sitt eget navn, så det er filtrerbart. Følgende bibliotekselementer vil bli inkludert:",
"atleastOneLibItem": "Vennligst velg minst ett bibliotek for å komme i gang"
},
"publishSuccessDialog": {
"title": "Bibliotek innsendt",
"content": "Takk {{authorName}}. Ditt bibliotek har blitt sendt inn for gjennomgang. Du kan spore statusen",
"link": "her"
},
"confirmDialog": {
"resetLibrary": "Nullstill bibliotek",
"removeItemsFromLib": "Fjern valgte elementer fra bibliotek"
},
"encrypted": { "encrypted": {
"tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.", "tooltip": "Dine tegninger er ende-til-ende-krypterte slik at Excalidraw sine servere aldri vil se dem.",
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw" "link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
@@ -289,6 +345,7 @@
"width": "Bredde" "width": "Bredde"
}, },
"toast": { "toast": {
"addedToLibrary": "Lagt til i biblioteket",
"copyStyles": "Kopierte stiler.", "copyStyles": "Kopierte stiler.",
"copyToClipboard": "Kopiert til utklippstavlen.", "copyToClipboard": "Kopiert til utklippstavlen.",
"copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavlen som PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavlen som PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Deel", "share": "Deel",
"showStroke": "Toon lijn kleur kiezer", "showStroke": "Toon lijn kleur kiezer",
"showBackground": "Toon achtergrondkleur kiezer", "showBackground": "Toon achtergrondkleur kiezer",
"toggleTheme": "Thema aan/uit" "toggleTheme": "Thema aan/uit",
"personalLib": "Persoonlijke bibliotheek",
"excalidrawLib": "Excalidraw bibliotheek"
}, },
"buttons": { "buttons": {
"clearReset": "Canvas opnieuw instellen", "clearReset": "Canvas opnieuw instellen",
@@ -135,7 +137,11 @@
"zenMode": "Zen modus", "zenMode": "Zen modus",
"exitZenMode": "Verlaat zen modus", "exitZenMode": "Verlaat zen modus",
"cancel": "Annuleren", "cancel": "Annuleren",
"clear": "Wissen" "clear": "Wissen",
"remove": "Verwijderen",
"publishLibrary": "Publiceren",
"submit": "Versturen",
"confirm": "Bevestigen"
}, },
"alerts": { "alerts": {
"clearReset": "Dit zal het hele canvas verwijderen. Weet je het zeker?", "clearReset": "Dit zal het hele canvas verwijderen. Weet je het zeker?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Scène kan niet worden hersteld vanuit dit afbeeldingsbestand", "cannotRestoreFromImage": "Scène kan niet worden hersteld vanuit dit afbeeldingsbestand",
"invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.", "invalidSceneUrl": "Kan scène niet importeren vanuit de opgegeven URL. Het is onjuist of bevat geen geldige Excalidraw JSON-gegevens.",
"resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?", "resetLibrary": "Dit zal je bibliotheek wissen. Weet je het zeker?",
"removeItemsFromsLibrary": "Verwijder {{count}} item(s) uit bibliotheek?",
"invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld." "invalidEncryptionKey": "Encryptiesleutel moet 22 tekens zijn. Live samenwerking is uitgeschakeld."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "Dubbelklik of druk op Enter om punten te bewerken", "lineEditor_info": "Dubbelklik of druk op Enter om punten te bewerken",
"lineEditor_pointSelected": "Druk op Delete om een punt te verwijderen, op CtrlOrCmd+D om te kopiëren, of sleeg om te verplaatsen", "lineEditor_pointSelected": "Druk op Delete om een punt te verwijderen, op CtrlOrCmd+D om te kopiëren, of sleeg om te verplaatsen",
"lineEditor_nothingSelected": "Selecteer een punt om te verplaatsen of te verwijderen, of houd Alt ingedrukt en klik om nieuwe punten toe te voegen", "lineEditor_nothingSelected": "Selecteer een punt om te verplaatsen of te verwijderen, of houd Alt ingedrukt en klik om nieuwe punten toe te voegen",
"placeImage": "" "placeImage": "",
"publishLibrary": "Publiceer je eigen bibliotheek"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Kan voorbeeld niet tonen", "cannotShowPreview": "Kan voorbeeld niet tonen",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "Publiceer bibliotheek",
"itemName": "Itemnaam",
"authorName": "Naam auteur",
"githubUsername": "GitHub gebruikersnaam",
"twitterUsername": "Twitter gebruikersnaam",
"libraryName": "Bibliotheek naam",
"libraryDesc": "Bibliotheek beschrijving",
"website": "Website",
"placeholder": {
"authorName": "Uw naam of gebruikersnaam:",
"libraryName": "Naam van je bibliotheek",
"libraryDesc": "Beschrijving van uw bibliotheek om mensen te helpen het gebruik ervan te begrijpen",
"githubHandle": "",
"twitterHandle": "",
"website": "Link naar je persoonlijke website of elders (optioneel)"
},
"errors": {
"required": "Vereist",
"website": "Vul een geldige URL in"
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": "Hier"
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.", "tooltip": "Je tekeningen zijn beveiligd met end-to-end encryptie, dus Excalidraw's servers zullen nooit zien wat je tekent.",
"link": "Blog post over end-to-end versleuteling in Excalidraw" "link": "Blog post over end-to-end versleuteling in Excalidraw"
@@ -289,6 +345,7 @@
"width": "Breedte" "width": "Breedte"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Stijlen gekopieerd.", "copyStyles": "Stijlen gekopieerd.",
"copyToClipboard": "Gekopieerd naar het klembord.", "copyToClipboard": "Gekopieerd naar het klembord.",
"copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} naar klembord gekopieerd als PNG\n({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "Del", "share": "Del",
"showStroke": "Vis fargeveljar for linjer", "showStroke": "Vis fargeveljar for linjer",
"showBackground": "Vis fargeveljar for bakgrunn", "showBackground": "Vis fargeveljar for bakgrunn",
"toggleTheme": "Veksle tema" "toggleTheme": "Veksle tema",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "Tilbakestill lerretet", "clearReset": "Tilbakestill lerretet",
@@ -135,7 +137,11 @@
"zenMode": "Zen-modus", "zenMode": "Zen-modus",
"exitZenMode": "Avslutt zen-modus", "exitZenMode": "Avslutt zen-modus",
"cancel": "Avbryt", "cancel": "Avbryt",
"clear": "Tøm" "clear": "Tøm",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "Dette vil tømme lerretet. Er du sikker?", "clearReset": "Dette vil tømme lerretet. Er du sikker?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Scena kunne ikkje gjenopprettast frå denne biletfila", "cannotRestoreFromImage": "Scena kunne ikkje gjenopprettast frå denne biletfila",
"invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.", "invalidSceneUrl": "Kunne ikkje hente noko scene frå den URL-en. Ho er anten øydelagd eller inneheld ikkje gyldig Excalidraw JSON-data.",
"resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?", "resetLibrary": "Dette vil fjerne alt innhald frå biblioteket. Er du sikker?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert." "invalidEncryptionKey": "Krypteringsnøkkelen må ha 22 teikn. Sanntidssamarbeid er deaktivert."
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkt", "lineEditor_info": "Dobbeltklikk eller trykk Enter for å redigere punkt",
"lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, CtrlOrCmd+D for å duplisere, eller dra for å flytte", "lineEditor_pointSelected": "Trykk på Slett for å fjerne punktet, CtrlOrCmd+D for å duplisere, eller dra for å flytte",
"lineEditor_nothingSelected": "Vel eit punkt å flytte eller fjerne, eller hald Alt og klikk for å legge til nye punkt", "lineEditor_nothingSelected": "Vel eit punkt å flytte eller fjerne, eller hald Alt og klikk for å legge til nye punkt",
"placeImage": "Klikk for å plassere biletet, eller klikk og drag for å velje storleik manuelt" "placeImage": "Klikk for å plassere biletet, eller klikk og drag for å velje storleik manuelt",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Kan ikkje vise førehandsvising", "cannotShowPreview": "Kan ikkje vise førehandsvising",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.", "tooltip": "Teikningane dine er ende-til-ende-krypterte slik at Excalidraw sine serverar aldri får sjå dei.",
"link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw" "link": "Blogginnlegg om ende-til-ende-kryptering i Excalidraw"
@@ -289,6 +345,7 @@
"width": "Breidde" "width": "Breidde"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "Kopierte stilane.", "copyStyles": "Kopierte stilane.",
"copyToClipboard": "Kopiert til utklippstavla.", "copyToClipboard": "Kopiert til utklippstavla.",
"copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavla som PNG\n({{exportColorScheme}})", "copyToClipboardAsPng": "Kopierte {{exportSelection}} til utklippstavla som PNG\n({{exportColorScheme}})",
+64 -7
View File
@@ -35,7 +35,7 @@
"arrowhead_arrow": "Sageta", "arrowhead_arrow": "Sageta",
"arrowhead_bar": "Barra", "arrowhead_bar": "Barra",
"arrowhead_dot": "Ponch", "arrowhead_dot": "Ponch",
"arrowhead_triangle": "", "arrowhead_triangle": "Triangle",
"fontSize": "Talha poliça", "fontSize": "Talha poliça",
"fontFamily": "Familha de poliça", "fontFamily": "Familha de poliça",
"onlySelected": "Seleccion sonque", "onlySelected": "Seleccion sonque",
@@ -100,7 +100,9 @@
"share": "Partejar", "share": "Partejar",
"showStroke": "Mostrar lo selector de color de contorn", "showStroke": "Mostrar lo selector de color de contorn",
"showBackground": "Mostrar lo selector de color de fons", "showBackground": "Mostrar lo selector de color de fons",
"toggleTheme": "Alternar tèma" "toggleTheme": "Alternar tèma",
"personalLib": "Bibliotèca personala",
"excalidrawLib": "Bibliotèca Excalidraw"
}, },
"buttons": { "buttons": {
"clearReset": "Reïnicializar lo canabàs", "clearReset": "Reïnicializar lo canabàs",
@@ -135,7 +137,11 @@
"zenMode": "Mòde escur", "zenMode": "Mòde escur",
"exitZenMode": "Sortir del mòde zen", "exitZenMode": "Sortir del mòde zen",
"cancel": "Anullar", "cancel": "Anullar",
"clear": "Escafar" "clear": "Escafar",
"remove": "Tirar",
"publishLibrary": "Publicar",
"submit": "Enviar",
"confirm": "Confirmar"
}, },
"alerts": { "alerts": {
"clearReset": "Aquò suprimirà lo canabàs complèt. O volètz vertadièrament?", "clearReset": "Aquò suprimirà lo canabàs complèt. O volètz vertadièrament?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "Restauracion impossibla de la scèna a partir del fichièr imatge", "cannotRestoreFromImage": "Restauracion impossibla de la scèna a partir del fichièr imatge",
"invalidSceneUrl": "Importacion impossibla de la scèna a partir de lURL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.", "invalidSceneUrl": "Importacion impossibla de la scèna a partir de lURL provesida. Es siá mal formatada o siá conten pas cap de donada JSON Excalidraw valida.",
"resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament?", "resetLibrary": "Aquò suprimirà vòstra bibliotèca. O volètz vertadièrament?",
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la bibliotèca?",
"invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada." "invalidEncryptionKey": "La clau de chiframent deu conténer 22 caractèrs. La collaboracion en dirèct es desactivada."
}, },
"errors": { "errors": {
@@ -164,7 +171,7 @@
"imageInsertError": "Insercion dimatge impossibla. Tornatz ensajar mai tard...", "imageInsertError": "Insercion dimatge impossibla. Tornatz ensajar mai tard...",
"fileTooBig": "Fichièr tròp pesuc. La talha maximala autorizada es {{maxSize}}.", "fileTooBig": "Fichièr tròp pesuc. La talha maximala autorizada es {{maxSize}}.",
"svgImageInsertError": "Insercion dimatge SVG impossibla. Las balisas SVG semblan invalidas.", "svgImageInsertError": "Insercion dimatge SVG impossibla. Las balisas SVG semblan invalidas.",
"invalidSVGString": "" "invalidSVGString": "SVG invalid."
}, },
"toolBar": { "toolBar": {
"selection": "Seleccion", "selection": "Seleccion",
@@ -185,7 +192,7 @@
"shapes": "Formas" "shapes": "Formas"
}, },
"hints": { "hints": {
"canvasPanning": "", "canvasPanning": "Per desplaçar los canabasses, tenètz la rodeta de la mirga o la barra despaci pendent lo desplaçament",
"linearElement": "Clicatz per començar mantun punt, lisatz per una sola linha", "linearElement": "Clicatz per començar mantun punt, lisatz per una sola linha",
"freeDraw": "Clicatz e lisatz, relargatz un còp acabat", "freeDraw": "Clicatz e lisatz, relargatz un còp acabat",
"text": "Astúcia: podètz tanben apondre de tèxt en doble clicant ont que siá amb laisina de seleccion", "text": "Astúcia: podètz tanben apondre de tèxt en doble clicant ont que siá amb laisina de seleccion",
@@ -199,7 +206,8 @@
"lineEditor_info": "Doble-clicatz o quichatz Entrada per modificar los punts", "lineEditor_info": "Doble-clicatz o quichatz Entrada per modificar los punts",
"lineEditor_pointSelected": "Quichatz Suprimir per suprimir lo punt, Ctrl o Cmd+D per lo duplicar, o fasètz lisar per lo desplaçar", "lineEditor_pointSelected": "Quichatz Suprimir per suprimir lo punt, Ctrl o Cmd+D per lo duplicar, o fasètz lisar per lo desplaçar",
"lineEditor_nothingSelected": "Seleccionatz un punt de desplaçar o suprimir, o mantenètz Alt e clicatz per apondre punts novèls", "lineEditor_nothingSelected": "Seleccionatz un punt de desplaçar o suprimir, o mantenètz Alt e clicatz per apondre punts novèls",
"placeImage": "Clicatz per plaçar limatge, o clicatz e lisatz per definir sa talha manualament" "placeImage": "Clicatz per plaçar limatge, o clicatz e lisatz per definir sa talha manualament",
"publishLibrary": "Publicar vòstra pròpria bibliotèca"
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "Afichatge impossible de lapercebut", "cannotShowPreview": "Afichatge impossible de lapercebut",
@@ -267,7 +275,55 @@
"zoomToSelection": "Zoomar la seleccion" "zoomToSelection": "Zoomar la seleccion"
}, },
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": "Escafar canabàs"
},
"publishDialog": {
"title": "Publicar la bibliotèca",
"itemName": "Nom de lelement",
"authorName": "Nom de lautor",
"githubUsername": "Nom dutilizaire GitHub",
"twitterUsername": "Nom dutilizaire Twitter",
"libraryName": "Nom de la bibliotèca",
"libraryDesc": "Descripcion de la bibliotèca",
"website": "Site web",
"placeholder": {
"authorName": "Vòstre nom o nom dutilizaire",
"libraryName": "Nom de vòstra bibliotèca",
"libraryDesc": "Descripcion de vòstra bibliotèca per ajudar lo monde a comprendre son utilizacion",
"githubHandle": "GitHub handle(opcional), per poder modificar la bibliotèca un còp enviada per repassa",
"twitterHandle": "Nom dutilizaire Twitter (opcional), per saber qual mercejar quand ne parlam sus Twitter",
"website": "Ligam cap a vòstre site web personal o endacòm mai (opcional)"
},
"errors": {
"required": "Requerit",
"website": ""
},
"noteDescription": {
"pre": "Enviatz vòstra bibliotèca per èsser compresa al ",
"link": "repertòri public de bibliotèca",
"post": "per que los autres lutilizen dins lor dessenhs."
},
"noteGuidelines": {
"pre": "Qualquun deu aprovar la bibliotèca manualament per començar. Volgatz legir las ",
"link": "linhas directrises",
"post": " abans de sometre. Vos farà mestièr un compte GitHub per comunicar e realizar de modificacions se demandadas, mas es pas complètament obligatòri."
},
"noteLicense": {
"pre": "En sometent, acceptatz que la bibliotèca siá publicada sota la ",
"link": "Licéncia MIT, ",
"post": "que significa en brèu que qual que siá pòt lutilizar sens cap de restriccion."
},
"noteItems": "Cada element de bibliotèca deu aver un nom pròpri per èsser filtrable. Los elements de bibliotèca seguentas seràn incluses:",
"atleastOneLibItem": "Volgatz seleccionar almens un element de bibliotèca per començar"
},
"publishSuccessDialog": {
"title": "Bibliotèca somesa",
"content": "Mercés {{authorName}}. Vòstre bibliotèca es estada somesa per repassa. Podètz seguir lavançament",
"link": "aquí"
},
"confirmDialog": {
"resetLibrary": "Reïnicializar la bibliotèca",
"removeItemsFromLib": "Tirar los elements seleccionats de la bibliotèca"
}, },
"encrypted": { "encrypted": {
"tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors dExcalidraw los veiràn pas jamai.", "tooltip": "Vòstres dessenhs son chifrats del cap a la fin en consequéncia los servidors dExcalidraw los veiràn pas jamai.",
@@ -289,6 +345,7 @@
"width": "Largor" "width": "Largor"
}, },
"toast": { "toast": {
"addedToLibrary": "Apondut a la bibliotèca",
"copyStyles": "Estiles copiats.", "copyStyles": "Estiles copiats.",
"copyToClipboard": "Copiats al quichapapièrs.", "copyToClipboard": "Copiats al quichapapièrs.",
"copyToClipboardAsPng": "{{exportSelection}} copiat coma PNG ({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} copiat coma PNG ({{exportColorScheme}})",
+60 -3
View File
@@ -100,7 +100,9 @@
"share": "ਸਾਂਝਾ ਕਰੋ", "share": "ਸਾਂਝਾ ਕਰੋ",
"showStroke": "ਰੇਖਾ ਦਾ ਰੰਗ ਚੋਣਕਾਰ ਦਿਖਾਓ", "showStroke": "ਰੇਖਾ ਦਾ ਰੰਗ ਚੋਣਕਾਰ ਦਿਖਾਓ",
"showBackground": "ਬੈਕਗਰਾਉਂਡ ਦਾ ਰੰਗ ਚੋਣਕਾਰ ਦਿਖਾਓ", "showBackground": "ਬੈਕਗਰਾਉਂਡ ਦਾ ਰੰਗ ਚੋਣਕਾਰ ਦਿਖਾਓ",
"toggleTheme": "ਥੀਮ ਬਦਲੋ" "toggleTheme": "ਥੀਮ ਬਦਲੋ",
"personalLib": "",
"excalidrawLib": ""
}, },
"buttons": { "buttons": {
"clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ", "clearReset": "ਕੈਨਵਸ ਰੀਸੈੱਟ ਕਰੋ",
@@ -135,7 +137,11 @@
"zenMode": "ਜ਼ੈੱਨ ਮੋਡ", "zenMode": "ਜ਼ੈੱਨ ਮੋਡ",
"exitZenMode": "ਜ਼ੈੱਨ ਮੋਡ 'ਚੋਂ ਬਾਹਰ ਨਿਕਲੋ", "exitZenMode": "ਜ਼ੈੱਨ ਮੋਡ 'ਚੋਂ ਬਾਹਰ ਨਿਕਲੋ",
"cancel": "", "cancel": "",
"clear": "" "clear": "",
"remove": "",
"publishLibrary": "",
"submit": "",
"confirm": ""
}, },
"alerts": { "alerts": {
"clearReset": "ਇਹ ਸਾਰਾ ਕੈਨਵਸ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?", "clearReset": "ਇਹ ਸਾਰਾ ਕੈਨਵਸ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
@@ -157,6 +163,7 @@
"cannotRestoreFromImage": "ਇਸ ਤਸਵੀਰ ਫਾਈਲ ਤੋਂ ਦ੍ਰਿਸ਼ ਬਹਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ", "cannotRestoreFromImage": "ਇਸ ਤਸਵੀਰ ਫਾਈਲ ਤੋਂ ਦ੍ਰਿਸ਼ ਬਹਾਲ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਿਆ",
"invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।", "invalidSceneUrl": "ਦਿੱਤੀ ਗਈ URL 'ਚੋਂ ਦ੍ਰਿਸ਼ ਨੂੰ ਆਯਾਤ ਨਹੀਂ ਕਰ ਸਕੇ। ਇਹ ਜਾਂ ਤਾਂ ਖਰਾਬ ਹੈ, ਜਾਂ ਇਸ ਵਿੱਚ ਜਾਇਜ਼ Excalidraw JSON ਡਾਟਾ ਸ਼ਾਮਲ ਨਹੀਂ ਹੈ।",
"resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?", "resetLibrary": "ਇਹ ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਨੂੰ ਸਾਫ ਕਰ ਦੇਵੇਗਾ। ਕੀ ਤੁਸੀਂ ਪੱਕਾ ਇੰਝ ਕਰਨਾ ਚਾਹੁੰਦੇ ਹੋ?",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "" "invalidEncryptionKey": ""
}, },
"errors": { "errors": {
@@ -199,7 +206,8 @@
"lineEditor_info": "ਬਿੰਦੂਆਂ ਨੂੰ ਸੋਧਣ ਲਈ ਡਬਲ-ਕਲਿੱਕ ਜਾਂ ਐਂਟਰ ਦਬਾਓ", "lineEditor_info": "ਬਿੰਦੂਆਂ ਨੂੰ ਸੋਧਣ ਲਈ ਡਬਲ-ਕਲਿੱਕ ਜਾਂ ਐਂਟਰ ਦਬਾਓ",
"lineEditor_pointSelected": "ਬਿੰਦੀ ਹਟਾਉਣ ਲਈ ਡਲੀਟ ਦਬਾਓ, ਡੁਪਲੀਕੇਟ ਬਣਾਉਣ ਲਈ CtrlOrCmd+D, ਜਾਂ ਹਿਲਾਉਣ ਲਈ ਘਸੀਟੋ", "lineEditor_pointSelected": "ਬਿੰਦੀ ਹਟਾਉਣ ਲਈ ਡਲੀਟ ਦਬਾਓ, ਡੁਪਲੀਕੇਟ ਬਣਾਉਣ ਲਈ CtrlOrCmd+D, ਜਾਂ ਹਿਲਾਉਣ ਲਈ ਘਸੀਟੋ",
"lineEditor_nothingSelected": "ਹਿਲਾਉਣ ਜਾਂ ਹਟਾਉਣ ਲਈ ਬਿੰਦੂ ਚੁਣੋ, ਜਾਂ ਨਵਾਂ ਬਿੰਦੂ ਜੋੜਨ ਲਈ Alt ਦਬਾਕੇ ਕਲਿੱਕ ਕਰੋ", "lineEditor_nothingSelected": "ਹਿਲਾਉਣ ਜਾਂ ਹਟਾਉਣ ਲਈ ਬਿੰਦੂ ਚੁਣੋ, ਜਾਂ ਨਵਾਂ ਬਿੰਦੂ ਜੋੜਨ ਲਈ Alt ਦਬਾਕੇ ਕਲਿੱਕ ਕਰੋ",
"placeImage": "" "placeImage": "",
"publishLibrary": ""
}, },
"canvasError": { "canvasError": {
"cannotShowPreview": "ਝਲਕ ਨਹੀਂ ਦਿਖਾ ਸਕਦੇ", "cannotShowPreview": "ਝਲਕ ਨਹੀਂ ਦਿਖਾ ਸਕਦੇ",
@@ -269,6 +277,54 @@
"clearCanvasDialog": { "clearCanvasDialog": {
"title": "" "title": ""
}, },
"publishDialog": {
"title": "",
"itemName": "",
"authorName": "",
"githubUsername": "",
"twitterUsername": "",
"libraryName": "",
"libraryDesc": "",
"website": "",
"placeholder": {
"authorName": "",
"libraryName": "",
"libraryDesc": "",
"githubHandle": "",
"twitterHandle": "",
"website": ""
},
"errors": {
"required": "",
"website": ""
},
"noteDescription": {
"pre": "",
"link": "",
"post": ""
},
"noteGuidelines": {
"pre": "",
"link": "",
"post": ""
},
"noteLicense": {
"pre": "",
"link": "",
"post": ""
},
"noteItems": "",
"atleastOneLibItem": ""
},
"publishSuccessDialog": {
"title": "",
"content": "",
"link": ""
},
"confirmDialog": {
"resetLibrary": "",
"removeItemsFromLib": ""
},
"encrypted": { "encrypted": {
"tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।", "tooltip": "ਤੁਹਾਡੀ ਡਰਾਇੰਗਾਂ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕਰਿਪਟ ਕੀਤੀਆਂ ਹੋਈਆਂ ਹਨ, ਇਸ ਲਈ Excalidraw ਦੇ ਸਰਵਰ ਉਹਨਾਂ ਨੂੰ ਕਦੇ ਵੀ ਨਹੀਂ ਦੇਖਣਗੇ।",
"link": "Excalidraw ਵਿੱਚ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕ੍ਰਿਪਸ਼ਨ 'ਤੇ ਬਲੌਗ ਸੰਪਾਦਨਾ" "link": "Excalidraw ਵਿੱਚ ਸਿਰੇ-ਤੋਂ-ਸਿਰੇ ਤੱਕ ਇਨਕ੍ਰਿਪਸ਼ਨ 'ਤੇ ਬਲੌਗ ਸੰਪਾਦਨਾ"
@@ -289,6 +345,7 @@
"width": "ਚੌੜਾਈ" "width": "ਚੌੜਾਈ"
}, },
"toast": { "toast": {
"addedToLibrary": "",
"copyStyles": "ਕਾਪੀ ਕੀਤੇ ਸਟਾਇਲ।", "copyStyles": "ਕਾਪੀ ਕੀਤੇ ਸਟਾਇਲ।",
"copyToClipboard": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕੀਤਾ।", "copyToClipboard": "ਕਲਿੱਪਬੋਰਡ 'ਤੇ ਕਾਪੀ ਕੀਤਾ।",
"copyToClipboardAsPng": "{{exportSelection}} ਨੂੰ ਕਲਿੱਪਬੋਰਡ 'ਤੇ PNG ਵਜੋਂ ਕਾਪੀ ਕੀਤਾ ({{exportColorScheme}})", "copyToClipboardAsPng": "{{exportSelection}} ਨੂੰ ਕਲਿੱਪਬੋਰਡ 'ਤੇ PNG ਵਜੋਂ ਕਾਪੀ ਕੀਤਾ ({{exportColorScheme}})",

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