Compare commits

...

254 Commits

Author SHA1 Message Date
add75b8c93 fix: reset canvas transformation to not accumulate error on non-zero dPR 2021-07-14 11:01:34 +02:00
0749d2c1f3 fix: color picker shortcuts not working when elements selected (#3817) 2021-07-10 21:05:00 +00:00
8787f3dc60 docs: release @excalidraw/excalidraw@0.9.0 🎉 (#3807)
* docs: release @excalidraw/excalidraw@0.9.0  🎉

* remove

* update changelog
2021-07-10 18:52:19 +05:30
5fabc57277 fix: view mode cursor adjustments (#3809) 2021-07-10 00:00:13 +02:00
e7cbb859f0 chore: Bump nginx version to newest (#3811)
Bump nginx version to newest.
2021-07-09 17:07:34 +02:00
aa860251c7 chore: Update translations from Crowdin (#3718)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-07-08 13:09:13 +02:00
380aaa30e6 fix: pass next release to updatePackageVersion & replace ## unreleased with new version (#3806)
* fix: pass next version to updatePackageVersion

* replace unreleased with next version in changelog

* fix

* fix
2021-07-06 00:13:56 +05:30
2e61fec7a6 build: Add release script to update relevant files and commit for next release (#3805)
* build: Add script to update package.json and commit for next release

* fix
2021-07-05 22:29:35 +05:30
3c295559c7 docs: specify to use yarn v1 not v2 (#3799)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-07-05 18:01:52 +02:00
55d3287abf fix: include deleted elements when passing to restore (#3802) 2021-07-05 13:43:53 +02:00
e3e967421e fix: import React before using jsx (#3804) 2021-07-05 15:59:09 +05:30
77aae63006 docs: tweak changelog and readme (#3796)
* docs: tweak changelog and readme

* moving to discussions :)

* Apply suggestions from code review

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

* Add about  attributes passed to updateScene

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-07-05 14:21:01 +05:30
ee64a7e264 fix: ensure s and g shortcuts work on no selection (#3800)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-07-04 22:27:33 +02:00
097362662d feat: pass localElements to restore and restoreElement API's and bump versions of duplicate elements on import (#3797) 2021-07-04 22:23:35 +02:00
038e9c13dd build: Add script to update changelog before a stable release (#3784)
* build: Add script to update changelog before a stable release

* fix

* fix

* fix

* Add note for lib updates

* Update scripts/updateChangelog.js

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-07-04 18:40:25 +05:30
bc8ba08ad0 build: Add script to update readme before stable release (#3781)
* build: Add script to update readme before stable release

* fix

* fix
2021-07-03 18:50:22 +05:30
f861a9fdd0 feat: support appState.exportEmbedScene to embed scene data in exportToSvg util (#3777)
* feat: add embedScene attribute to exportToSvg util

* fix

* return promise

* add docs and remove

* fix

* fix tests

* use

* fix

* fix

* remove metadata and use exportEmbedScene

* fix

* fix

* fix

* fix

* IIFE
2021-07-03 02:07:01 +05:30
62303b8a22 chore: bump browser-fs-access (#3780) 2021-07-02 14:55:16 +00:00
9cc741ab3a chore(deps): bump ssri from 6.0.1 to 6.0.2 (#3711)
Bumps [ssri](https://github.com/npm/ssri) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/npm/ssri/releases)
- [Changelog](https://github.com/npm/ssri/blob/v6.0.2/CHANGELOG.md)
- [Commits](https://github.com/npm/ssri/compare/v6.0.1...v6.0.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-30 01:29:46 +05:30
2d279cbb02 chore(deps-dev): bump mini-css-extract-plugin (#3767)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.6.0 to 1.6.1.
- [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/v1.6.0...v1.6.1)

---
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-06-29 02:13:49 +05:30
57ea4fdf9a chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#3768)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.1.3 to 5.1.4.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.1.3...v5.1.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-29 02:13:27 +05:30
44402f42bf feat: switch to selection tool on library item insert (#3773)
* switch to selection tool on library item insert

* add test

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-06-28 12:00:33 +02:00
bdead4d164 feat: expose getFreeDrawSvg, loadFromBlob and loadLibraryFromBlob from excalidraw package (#3764)
* feat: expose getFreeDrawSvg, loadFromBlob and loadLibraryFromBlob from excalidraw package

* Add docs

* fix
2021-06-26 02:12:58 +05:30
bfc0656475 chore(deps): bump color-string from 1.5.4 to 1.5.5 (#3761)
Bumps [color-string](https://github.com/Qix-/color-string) from 1.5.4 to 1.5.5.
- [Release notes](https://github.com/Qix-/color-string/releases)
- [Changelog](https://github.com/Qix-/color-string/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Qix-/color-string/compare/1.5.4...1.5.5)

---
updated-dependencies:
- dependency-name: color-string
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-26 01:37:59 +05:30
a33a3334f7 chore: upgrade deps in packages (#3760)
* chore: upgrade deps

* upgrade deps in utils
2021-06-24 01:26:44 +05:30
969d3c694a fix: keep binding for attached arrows after changing text (#3754)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-06-21 14:31:49 +02:00
5cd921549a fix: deselect elements on viewMode toggle (#3741) 2021-06-16 19:01:16 +02:00
437afcbea4 fix: allow pointer events for disable zen mode button (#3743) 2021-06-16 22:25:22 +05:30
6dee02e320 feat: expose fontfamily and refactor FONT_FAMILY (#3710)
* feat: expose fontfamily and refactor FONT_FAMILY for better readability

* fix

* fix

* fix

* docs

* fix
2021-06-13 21:26:55 +05:30
74a2f16501 feat: Show active file name when saving to current file (#3733)
* feat: Show active file name when saving to current file

* Make requested changes

* More changes
2021-06-13 21:11:07 +05:30
fd4460be37 feat: add hint around text editing (#3708) 2021-06-12 22:58:34 +02:00
e82d0493cf chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#3716)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.1.0 to 9.2.3.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.1.0...v9.2.3)

---
updated-dependencies:
- dependency-name: ts-loader
  dependency-type: direct:development
  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-06-12 20:03:02 +00:00
083cb4c656 chore(deps-dev): bump ts-loader in /src/packages/utils (#3712)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.1.0 to 9.2.3.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.1.0...v9.2.3)

---
updated-dependencies:
- dependency-name: ts-loader
  dependency-type: direct:development
  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-06-13 01:27:54 +05:30
d067365c1d chore(deps-dev): bump typescript in /src/packages/excalidraw (#3671)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.2.4 to 4.3.2.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.2.4...v4.3.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-13 01:26:27 +05:30
273cac6b60 chore(deps-dev): bump @babel/plugin-transform-typescript (#3713)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.14.3 to 7.14.5.
- [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.14.5/packages/babel-plugin-transform-typescript)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-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-06-12 21:48:03 +05:30
b9337b8a36 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3714)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.14.4 to 7.14.5.
- [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.14.5/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-06-12 15:41:26 +00:00
0e0025921b chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#3715)
Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.13.0 to 7.14.5.
- [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.14.5/packages/babel-plugin-transform-async-to-generator)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-async-to-generator"
  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-06-12 15:36:46 +00:00
efc01ddab1 chore(deps): bump ws from 7.4.3 to 7.4.6 in /src/packages/excalidraw (#3665)
Bumps [ws](https://github.com/websockets/ws) from 7.4.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.3...7.4.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-12 15:34:00 +00:00
7bce22b114 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3670)
Bumps [webpack](https://github.com/webpack/webpack) from 5.37.1 to 5.38.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.37.1...v5.38.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-12 15:31:49 +00:00
aab4965bbb chore(deps): bump ws from 7.4.3 to 7.4.6 in /src/packages/utils (#3664)
Bumps [ws](https://github.com/websockets/ws) from 7.4.3 to 7.4.6.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.4.3...7.4.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-12 15:30:49 +00:00
486a9a3da8 chore(deps-dev): bump autoprefixer in /src/packages/excalidraw (#3672)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.2.5 to 10.2.6.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.2.5...10.2.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-12 20:56:01 +05:30
2425c06082 chore(deps-dev): bump webpack in /src/packages/utils (#3673)
Bumps [webpack](https://github.com/webpack/webpack) from 5.37.1 to 5.38.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.37.1...v5.38.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-12 20:55:40 +05:30
79ea844901 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3675)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.14.1 to 7.14.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.14.4/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-12 20:54:40 +05:30
6690215cd1 feat: change library icon to be more clear (#3583) 2021-06-11 23:13:05 +02:00
7f5e783fe8 chore: Update translations from Crowdin (#3659) 2021-06-11 19:15:44 +02:00
9325109836 fix: use excal id so every element has unique id (#3696)
* fix: use excal id so every element has unique id

* fix

* fix

* fix

* add docs

* fix
2021-06-10 02:46:56 +05:30
69b6fbb3f4 feat: pass current theme when installing libraries (#3701)
* pass current `theme` when installing libraries

* pass `theme` instead of `appState`
2021-06-08 23:14:02 +02:00
6b6002baae refactor: Delete React SyntheticEvent persist (#3700) 2021-06-07 10:32:30 +02:00
54dcb73701 docs: improve grammar and example (#3699)
* Improve grammar, render only once

* Update README_NEXT.md
2021-06-07 01:38:05 +05:30
b595d3fcba test: add unit tests for restore.ts file (#3679)
* Add unit tests for restore.ts file

* Improving describe blocks in restore tests

* Move restore.tests.ts folder and remove depedency of UI in the tests

* Adding snapshots to restore.ts tests

* Using snapshot in freedraw test

* Updating description of test for line and draw elements

* Updating description of test for arrow element

* Improving restoreAppState tests

* specs cleanup

* fix

* fix
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-06-06 21:53:12 +05:30
d0867d1c3b refatcor: make ProjectName a functional component (#3695) 2021-06-04 21:22:09 +05:30
0d19e9210c feat: update virgil font (#3692)
Co-authored-by: tjangy <youri.tjang@rabobank.nl>
2021-06-02 21:41:14 +02:00
4249de41d4 feat: Add prop autoFocus to set focus on the Excalidraw component (#3691)
* feat: Add prop autofocus to set focus on Excalidraw component

* Update PR number

* Make requested changes

* Add note

* Update src/packages/excalidraw/CHANGELOG.md

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

* Update src/tests/excalidrawPackage.test.tsx

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

* Remove duplicate sentence

* Indent note

* autofocus -> autoFocus

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-06-02 19:54:21 +05:30
15f02ba191 refactor: code clean up (#3681)
* refactor: code clean up
Move types from App.tsx to types.ts
Move excalidrawPackage.test.tsx inside src/tests/package

* import type
2021-06-01 23:52:13 +05:30
a2e1199907 feat: support exporting json to excalidraw plus (#3678)
* feat: support exporting json to excalidraw plus

* add Firebase Storage rules to codebase

* factor the onClick handler out

* move excal icon to icons.tsx

* handle export error
2021-06-01 14:05:09 +02:00
c08e9c4172 fix: use rgba instead of shorthand alpha (#3688) 2021-05-31 14:29:40 +05:30
abfc58eb24 feat: save exportScale in AppState (#3580)
Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-30 16:31:12 +02:00
035c7affff fix: color pickers not opening on mobile (#3676) 2021-05-30 12:05:07 +02:00
c819b653bf fix: on contextMenu, use selected element regardless of z-index (#3668) 2021-05-29 22:33:53 +02:00
60cea7a0c2 fix: selectedGroupIds not being stored in history (#3630)
thanks!
2021-05-29 21:35:03 +02:00
d63b6a3469 feat: support custom UI rendering inside export dialog (#3666)
* feat: support custom UI rendering inside export dialog

* docs

* add

* remove assertion

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-30 00:37:38 +05:30
0912fe1c93 fix: overscroll on touch devices (#3663) 2021-05-29 11:54:36 -04:00
360310de31 feat: Add prop UIOptions.canvasActions.saveAsImage to show/hide save image button (#3662)
* feat: Add prop UIOptions.canvasActions.saveAsImage which implies whether the save as image dialog should be shown

* Add docs

* fix specs
2021-05-29 19:41:50 +05:30
716c78e930 chore(deps): bump dns-packet from 1.3.1 to 1.3.4 (#3652)
Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-29 16:06:20 +02:00
ba48974351 feat: customise export dialog with UIOptions.canvasActions.export prop (#3658)
* refactor: update UIOptions.canvasActions.export to be a an object

* fix

* fix

* dnt show export icon when false

* fix

* inline

* memoize UIOptions

* update docs

* fix

* tweak readme

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-05-29 02:56:25 +05:30
6c3e4417e1 feat: Add shortcuts for stroke and background color picker (#3318)
* feat: Add shortcuts for opening stroke and background color picker

* Use App.tsx keydown handler

* only get selectedElements if applicable (perf)

* fix tests and snaps

* reuse `appState.openMenu`

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-28 13:52:42 +02:00
bc0b6e1888 refactor: rename UIOptions.canvasActions.saveScene to UIOptions.canvasActions.saveToActiveFile (#3657)
* refactor rename action saveScene to saveFileToDisk

* docs

* fix

* fix
2021-05-28 02:10:33 +05:30
99a22e8445 chore: Update translations from Crowdin (#3542)
* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Bulgarian)

* New translations en.json (Italian)

* New translations en.json (Catalan)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (Japanese)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* New translations en.json (Catalan)

* Auto commit: Calculate translation coverage

* New translations en.json (Finnish)

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* New translations en.json (Chinese Simplified)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Korean)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Dutch)

* New translations en.json (Japanese)

* New translations en.json (Turkish)

* New translations en.json (Arabic)

* New translations en.json (Indonesian)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Latvian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Bulgarian)

* New translations en.json (Italian)

* New translations en.json (Catalan)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* New translations en.json (German)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* New translations en.json (Romanian)

* New translations en.json (Finnish)

* New translations en.json (Italian)

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* New translations en.json (Slovak)

* New translations en.json (Chinese Simplified)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Korean)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Persian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Dutch)

* New translations en.json (Japanese)

* New translations en.json (Turkish)

* New translations en.json (Arabic)

* New translations en.json (Indonesian)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Latvian)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Bulgarian)

* New translations en.json (Italian)

* New translations en.json (Catalan)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Kabyle)

* New translations en.json (Dutch)

* New translations en.json (Swedish)

* New translations en.json (Dutch)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Romanian)

* New translations en.json (Finnish)

* New translations en.json (Occitan)

* New translations en.json (Slovak)

* New translations en.json (German)

* New translations en.json (Italian)

* New translations en.json (Slovak)

* New translations en.json (French)

* New translations en.json (Portuguese)

* New translations en.json (Indonesian)

* New translations en.json (Indonesian)

* New translations en.json (French)

* New translations en.json (Chinese Traditional)

* New translations en.json (Kabyle)

* New translations en.json (Ukrainian)

* New translations en.json (Slovak)

* New translations en.json (Slovak)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Simplified)

* New translations en.json (Japanese)

* New translations en.json (Occitan)

* New translations en.json (Latvian)

* New translations en.json (Latvian)

* New translations en.json (Latvian)

* New translations en.json (Turkish)

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* update language picker & coverage descriptions

* New translations en.json (Punjabi)

* Auto commit: Calculate translation coverage

* New translations en.json (Punjabi)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* New translations en.json (Russian)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Swedish)

* New translations en.json (Finnish)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Hebrew)

* New translations en.json (Greek)

* New translations en.json (Turkish)

* New translations en.json (Occitan)

* New translations en.json (Latvian)

* New translations en.json (Japanese)

* New translations en.json (Punjabi)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Kabyle)

* New translations en.json (German)

* New translations en.json (Czech)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

* New translations en.json (Dutch)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Ukrainian)

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (Arabic)

* Auto commit: Calculate translation coverage

* New translations en.json (Arabic)

* New translations en.json (Swedish)

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-27 23:39:19 +05:30
e6d9797167 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3620)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.2 to 7.14.3.
- [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.14.3/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:22:44 +05:30
a1e8fdfb1b chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3653)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.4 to 5.2.6.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.4...v5.2.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:17:33 +05:30
1cce63b07b chore(deps-dev): bump css-loader in /src/packages/utils (#3654)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.4 to 5.2.6.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.4...v5.2.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:17:15 +05:30
e9c2a09c21 chore(deps-dev): bump @babel/core in /src/packages/utils (#3619)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.2 to 7.14.3.
- [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.14.3/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:15:10 +05:30
55e0812680 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3618)
Bumps [webpack](https://github.com/webpack/webpack) from 5.37.0 to 5.37.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.37.0...v5.37.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:14:46 +05:30
0f32278a7e chore(deps-dev): bump webpack-bundle-analyzer in /src/packages/utils (#3617)
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.4.1...v4.4.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:14:30 +05:30
1bdb8da1c3 chore(deps-dev): bump @babel/plugin-transform-typescript (#3625)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.13.0 to 7.14.3.
- [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.14.3/packages/babel-plugin-transform-typescript)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:13:37 +05:30
9c9787e0a0 chore(deps-dev): bump @babel/plugin-transform-typescript (#3624)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.13.0 to 7.14.3.
- [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.14.3/packages/babel-plugin-transform-typescript)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:13:12 +05:30
c2fe24c562 chore(deps): bump browserslist in /src/packages/utils (#3647)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.16.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:12:30 +05:30
52faa52091 chore(deps): bump browserslist in /src/packages/excalidraw (#3648)
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.16.3 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.16.3...4.16.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-27 18:12:08 +05:30
dd12abc583 refactor: remove watermark code (#3639)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-05-26 21:44:54 +02:00
abebf9aff8 fix: small UI issues around image export dialog (#3642) 2021-05-26 14:44:03 +02:00
790c9fd02e feat: exporting redesign (#3613)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-05-25 21:37:14 +02:00
357266e9ab feat: auto-position tooltip and suport overflowing container (#3631) 2021-05-25 13:52:04 +02:00
0bbb4535cf fix: normalize linear element points on restore (#3633) 2021-05-24 20:35:53 +02:00
d201d0be1b fix: disable pointer-events on footer-center container (#3629) 2021-05-23 17:09:39 +02:00
5662c5141d feat: Auto release @excalidraw/excalidraw-next on every change (#3614)
* feat: Auto release @excalidraw/excalidraw-next on every change

* fix

* fix name

* fix

* add logs

* use commithash

* yarn installå

* fix

* catch

* log

* fix

* uncomment

* remove console

* add logs

* list files changed between prev and current commit

* fetch last two commits

* remove logs

* fix

* update readme_next

* update readme before release

* temp commit to trigger release

* update package name to excalidraw-next

* bold

* remove temp branch

* add note about next

* fix

* fix

* fix
2021-05-22 19:43:28 +05:30
044614dcf3 perf: Improve arrow head sizing (#3480)
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2021-05-22 12:30:02 +02:00
9ec15989ab chore(deps-dev): bump sass-loader in /src/packages/utils (#3589)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 11.0.1 to 11.1.1.
- [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/v11.0.1...v11.1.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 09:19:54 +00:00
08aafcd248 chore(deps-dev): bump sass-loader in /src/packages/excalidraw (#3593)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 11.0.1 to 11.1.1.
- [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/v11.0.1...v11.1.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 09:18:51 +00:00
ea5602457f chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#3594)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.1.1...v5.1.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 14:45:16 +05:30
3c58d19d45 chore(deps-dev): bump @babel/plugin-transform-runtime (#3609)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.13.15 to 7.14.3.
- [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.14.3/packages/babel-plugin-transform-runtime)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 14:44:54 +05:30
fcfcdebc99 chore(deps-dev): bump webpack in /src/packages/utils (#3610)
Bumps [webpack](https://github.com/webpack/webpack) from 5.36.2 to 5.37.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.36.2...v5.37.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 14:44:24 +05:30
aa97c074a7 chore(deps-dev): bump webpack-cli in /src/packages/excalidraw (#3586)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.6.0...webpack-cli@4.7.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 12:39:45 +05:30
d65d2c5279 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3595)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.14.1 to 7.14.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.2/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-21 12:38:34 +05:30
6d40039f08 feat: allow inner-drag-selecting with cmd/ctrl (#3603)
* feat: allow inner-drag-selecting with cmd/ctrl

* don't use  cursor when pressing cmd/ctrl

* ensure we reset deselected groups

* add tests

* add docs

* couple fixes around group selection
2021-05-20 22:28:34 +02:00
f4e10c93e1 chore(deps-dev): bump eslint-config-prettier from 8.2.0 to 8.3.0 (#3494)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.2.0 to 8.3.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.2.0...v8.3.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-20 14:53:07 +05:30
82c6df0e1f chore: Make deploy source and logs public (#3596) 2021-05-17 23:03:01 +02:00
c37bd59ddd chore(deps-dev): bump @babel/core in /src/packages/utils (#3588)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.0 to 7.14.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.2/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-16 18:40:49 +05:30
198a5e3b53 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3587)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.0 to 7.14.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.2/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-16 18:40:21 +05:30
a78e1fa99b chore(deps-dev): bump postcss-loader in /src/packages/excalidraw (#3585)
Bumps [postcss-loader](https://github.com/webpack-contrib/postcss-loader) from 5.2.0 to 5.3.0.
- [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/v5.2.0...v5.3.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-16 18:39:45 +05:30
fc5db9248c chore(deps-dev): bump webpack in /src/packages/excalidraw (#3584)
Bumps [webpack](https://github.com/webpack/webpack) from 5.36.2 to 5.37.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.36.2...v5.37.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-16 18:39:03 +05:30
ebf64036fd docs: release @excalidraw/excalidraw@0.8.0 🎉 (#3581)
* docs: release @excalidraw/excalidraw@0.8.0

* remove

* remove

* add info for each section

* add .

* update
2021-05-15 18:17:44 +05:30
6271a031a3 fix: move encrypted icon to excalidraw-app add separate animation for renderFooter prop (#3577)
* fix: move encrypted icon to excalidraw-app

* use grid & separate animation for custom footer

* update docs

* fix
2021-05-15 14:49:58 +05:30
78da4c075e feat: support updating appState in updateScene API (#3576)
* feat: support updating appState in updateScene API

* make `updateScene.data.appState` more type-safe

* restore `appState` when passing to `updateScene`

* fix

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-14 17:52:56 +05:30
f1cf28a84e refactor: reduce passing-around of canvas in export code (#3571) 2021-05-13 19:21:15 +02:00
3b9290831a refactor: rename renderTopRight prop to renderTopRightUI (#3572)
* refactor: rename renderTopRight prop to renderTopRightUI

* update

* fix

* update
2021-05-13 21:02:59 +05:30
bec34f2d57 feat: Shortcut key for nerd stats (#3552)
* added alt+/ as the shortcut key for nerd stats

Signed-off-by: gurkiran_singh <gurkiransinghk@gmail.com>

* added shortcut info in HelpDialog.ts

Signed-off-by: gurkiran_singh <gurkiransinghk@gmail.com>

* resolved conflicts

Signed-off-by: gurkiran_singh <gurkiransinghk@gmail.com>

* added shortcut info in HelpDialog.ts

Signed-off-by: gurkiran_singh <gurkiransinghk@gmail.com>
2021-05-12 14:27:35 +05:30
07839f8d20 perf: Reduce SVG export size by more than half by reducing precision to 2 decimal places (#3567)
* render svg with a specified precision

* moved precision to a constant

* fix test case  to use rounded values
2021-05-11 19:35:35 -07:00
8068d1f853 feat: export serializeAsJSON from package (#3538)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-05-12 00:24:41 +02:00
92c7d3257f fix: Exporting freedraw with color to svg (#3565) 2021-05-11 10:44:26 +02:00
a8a5e7b6ff fix: no migrating draw lines correctly 2021-05-10 16:19:31 +02:00
45a4a00b69 chore(deps): bump hosted-git-info from 2.8.8 to 2.8.9 (#3555)
Bumps [hosted-git-info](https://github.com/npm/hosted-git-info) from 2.8.8 to 2.8.9.
- [Release notes](https://github.com/npm/hosted-git-info/releases)
- [Changelog](https://github.com/npm/hosted-git-info/blob/v2.8.9/CHANGELOG.md)
- [Commits](https://github.com/npm/hosted-git-info/compare/v2.8.8...v2.8.9)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 18:12:10 +05:30
436e539d3a chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3549)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.15 to 7.14.1.
- [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.14.1/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 18:08:35 +05:30
ff19167063 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3547)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.15 to 7.14.1.
- [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.14.1/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 18:03:50 +05:30
3fc531ed6e chore(deps-dev): bump webpack in /src/packages/utils (#3528)
Bumps [webpack](https://github.com/webpack/webpack) from 5.35.1 to 5.36.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.35.1...v5.36.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 18:02:06 +05:30
6f55c00814 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3523)
Bumps [webpack](https://github.com/webpack/webpack) from 5.35.1 to 5.36.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.35.1...v5.36.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 18:01:29 +05:30
a7eb6e1168 chore(deps): bump lodash from 4.17.20 to 4.17.21 in /src/packages/utils (#3556)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 18:00:54 +05:30
641bbdd2da chore(deps): bump lodash in /src/packages/excalidraw (#3554)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.20 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.20...4.17.21)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 17:53:27 +05:30
42b0f7a614 chore(deps-dev): bump @babel/core in /src/packages/utils (#3526)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.16 to 7.14.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.0/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 17:33:06 +05:30
c11e3818ac chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3525)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.16 to 7.14.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.14.0/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 17:32:46 +05:30
4b6aa5c53b chore(deps-dev): bump mini-css-extract-plugin (#3522)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.5.0 to 1.6.0.
- [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/v1.5.0...v1.6.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 17:32:01 +05:30
ebd0408d7d chore(deps-dev): bump webpack-cli in /src/packages/utils (#3550)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.6.0...webpack-cli@4.7.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-05-10 17:31:30 +05:30
f4fefbcee8 feat: Better rendering of curved rectangles (#3562) 2021-05-10 12:41:17 +02:00
11b8cc2caa fix: remove draw element from codebase (#3559) 2021-05-10 11:01:10 +02:00
6bebfe63be fix: handle render errors (#3557) 2021-05-09 21:43:36 +02:00
91ab7f36e2 fix: restore on paste or lib import (#3558) 2021-05-09 21:42:12 +02:00
5ee8e8249c log instead of throw on unimplemented render type 2021-05-09 17:47:52 +02:00
49c6bdd520 feat: improved freedraw (#3512)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-09 17:42:10 +02:00
198800136e feat: Add shortcut for dark mode (#3543)
* Create and move toggle into an action

* Add shortcut on help dialog
2021-05-08 11:47:30 +02:00
178ee04d82 feat: Adds rounded icons, joins and caps. (#3521)
Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2021-05-07 18:03:23 +02:00
18cdafbcbe chore: Update translations from Crowdin (#3472)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-05-06 22:13:35 +02:00
286e9a1524 feat: add temporary Excalidraw+ promo (#3540)
* feat: add temporary Excalidraw+ promo

* add seemingly required query params
2021-05-06 21:29:05 +02:00
bac76778ce feat: add renderTopRight prop & remove GH corner from core (#3539)
* feat: add `renderTopRight` prop & remove GH corner from core

* reuse `--space-factor` var

* update readme & changelog
2021-05-06 21:00:17 +02:00
f28f7ffb6e fix: improve mobile user experience (#3508) 2021-04-27 12:46:30 +02:00
12e8cc853f feat: remove backdrop-filter to improve perf (#3506)
* feat: remove `backdrop-filter` to improve perf

* remove `backdrop-filter` from Modal
2021-04-27 10:55:59 +02:00
81108bf580 fix: prevent selecting .visually-hidden elements (#3501) 2021-04-26 00:03:53 +02:00
23030a15f2 docs: release @excalidraw/excalidraw 0.7.0 🎉 (#3497)
* docs: release version 0.7.0

* update

* fix

* fix

* fix

* fix

* fix

* fix

* version bump

* fix readme

* tweaks

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

* update link

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-04-25 19:02:06 +05:30
4ef7cb7365 feat: bump element version on z-index change (#3483)
* feat: bump element version on z-index change

* update snaps

* update changelog
2021-04-25 14:09:38 +02:00
5cc3f7db80 feat: support scroll to center to single element and rename setScrollToContent (#3482)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-04-25 12:28:41 +02:00
5c42cb5be4 fix: only handle cut/paste events inside excalidraw (#3484)
* fix: only hand cut/paste events inside excalidraw

* changelog

* check if excalidraw is active for copy event

* check if active element is part of excalidraw
2021-04-25 15:13:42 +05:30
004d3180b5 chore(deps-dev): bump webpack in /src/packages/utils (#3491)
Bumps [webpack](https://github.com/webpack/webpack) from 5.33.2 to 5.35.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.33.2...v5.35.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 12:59:13 +05:30
c12119278a chore(deps-dev): bump webpack in /src/packages/excalidraw (#3492)
Bumps [webpack](https://github.com/webpack/webpack) from 5.34.0 to 5.35.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.34.0...v5.35.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 12:58:51 +05:30
4d628844de chore(deps-dev): bump css-loader in /src/packages/utils (#3489)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.2 to 5.2.4.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.2...v5.2.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 12:58:02 +05:30
946a209927 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3487)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.15 to 7.13.16.
- [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.13.16/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 12:57:11 +05:30
811437724b chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3486)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.3 to 5.2.4.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.3...v5.2.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 12:56:40 +05:30
9dcde502aa chore(deps-dev): bump @babel/core in /src/packages/utils (#3493)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.15 to 7.13.16.
- [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.13.16/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-25 12:56:18 +05:30
d3106495b2 fix: make history local to a given excalidraw instance (#3481)
* fix: make history local to a given excalidraw instance

* changelog

* Update src/packages/excalidraw/CHANGELOG.md
2021-04-24 18:21:02 +05:30
891ac82447 fix: use active Excalidraw component when editing text (#3478)
* fix: use active excalidraw component when editing text

* changelog

* tweak
2021-04-23 21:11:18 +05:30
354976e08e build: Add vendor prefixes to css rules (#3476)
* build: Add vendor prefixes to css files

* changelog

* fix
2021-04-23 11:31:38 +05:30
5c73c5813c chore: fix CHANGELOG links 2021-04-21 23:40:51 +02:00
3a0b6fb41b refactor: move getSyncableElements to CollabWrapper & expose isInvisiblySmallElement helper (#3471)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-04-21 23:37:44 +02:00
37d513ad59 feat: Make library local to given excalidraw instance and allow consumer to control it (#3451)
* feat: dnt share library & attach to the excalidraw instance

* fix

* Add addToLibrary, resetLibrary and libraryItems in initialData

* remove comment

* handle errors & improve types

* remove resetLibrary and addToLibrary and add onLibraryChange prop

* set library cache to empty arrary on reset

* Add i18n for remove/add library

* Update src/locales/en.json

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

* update docs

* Assign unique ID to
 each excalidraw component and remove csrfToken from library as its not needed

* tweaks

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

* update

* tweak

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-04-21 23:38:24 +05:30
46624cc953 docs: update local installation instructions in readme (#3452)
* docs: tweak readme

* remove editors

* Update README.md

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

* Update README.md

Co-authored-by: Lipis <lipiridis@gmail.com>
Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-04-21 14:52:38 +05:30
0d23c8dd76 chore(deps): bump sass from 1.32.8 to 1.32.10 (#3460)
Bumps [sass](https://github.com/sass/dart-sass) from 1.32.8 to 1.32.10.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.32.8...1.32.10)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-21 00:32:37 +03:00
51ef4cd97b chore: Update translations from Crowdin (#3377) 2021-04-20 23:48:57 +03:00
b558d19d37 chore(deps-dev): bump eslint-config-prettier from 8.1.0 to 8.2.0 (#3461)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.1.0 to 8.2.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.1.0...v8.2.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 23:48:30 +03:00
b8fb6580ef chore(deps-dev): bump @babel/plugin-transform-runtime (#3434)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.13.10 to 7.13.15.
- [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.13.15/packages/babel-plugin-transform-runtime)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-20 15:55:13 +05:30
6730eb41c2 fix: scrollToContent only on visible elements (#3466) 2021-04-19 17:29:13 +02:00
87c42cb327 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3469)
Bumps [webpack](https://github.com/webpack/webpack) from 5.31.2 to 5.34.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.31.2...v5.34.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 15:15:46 +00:00
8cfd05aa95 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3470)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.1 to 5.2.3.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.1...v5.2.3)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 15:14:24 +00:00
3ed8271344 chore(deps-dev): bump webpack-bundle-analyzer in /src/packages/utils (#3458)
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.4.0...v4.4.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 20:41:14 +05:30
73515b5a83 chore(deps-dev): bump webpack-bundle-analyzer (#3455)
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.4.0...v4.4.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 20:40:27 +05:30
63d3da9a54 chore(deps-dev): bump mini-css-extract-plugin (#3456)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.4.1 to 1.5.0.
- [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/v1.4.1...v1.5.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 20:39:53 +05:30
215fb5e357 chore(deps-dev): bump css-loader in /src/packages/utils (#3457)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.1...v5.2.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 20:39:18 +05:30
886177816b chore(deps-dev): bump webpack in /src/packages/utils (#3459)
Bumps [webpack](https://github.com/webpack/webpack) from 5.31.2 to 5.33.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.31.2...v5.33.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-19 20:35:56 +05:30
7d29351d66 fix: library onClick paste off-center (#3462) 2021-04-18 13:33:05 +02:00
c0047269c1 fix: focus on last active element when dialog closes (#3447)
* fix: focus on last active element when dialog closes

* useState instead of ref
2021-04-15 20:29:00 +05:30
793b69e592 fix: Apply theme to only to active excalidraw component (#3446)
* feat: Apply theme to only current instance of excalidraw

* fix

* fix

* fix

* fix

* fix

* update changelog

* fix
2021-04-13 23:02:57 +05:30
e0a449aa40 feat: support tab in text Wyswig (#3411)
* fix: support tab in text Wyswig

* Refactor tab handling

Tab now indent the whole line, instead of inserting at the cursor
position.

Shift+Tab now deindent the whole line.

* Add multi-line tabulation support

* rename

* simplify algo for selected lines start indices & naming tweaks

* add cmd-bracket shortcuts as alias to indent/outdent

* support outdenting partial tabs

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-04-13 16:23:46 +02:00
d5a270f643 fix: tweak readme for syncable elements (#3444)
* fix: tweak readme for syncable elements

* fix

* tweak

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-04-13 15:04:02 +05:30
d126d04d17 feat: Bind keyboard events to the current excalidraw container and add handleKeyboardGlobally prop to allow host to bind to document (#3430)
* fix: Bind keyboard events to excalidraw container

* fix cases around blurring

* fix modal rendering so keyboard shortcuts work on modal as well

* Revert "fix modal rendering so keyboard shortcuts work on modal as well"

This reverts commit 2c8ec6be8e.

* Attach keyboard event in react way so we need not handle portals separately (modals)

* dnt propagate esc event when modal shown

* focus the container when help dialog closed with shift+?

* focus the help icon when help dialog on close triggered

* move focusNearestTabbableParent to util

* rename util to focusNearestParent and remove outline from excal and modal

* Add prop bindKeyGlobally to decide if keyboard events should be binded to document and allow it in excal app, revert tests

* fix

* focus container after installing library, reset library and closing error dialog

* fix tests and create util to focus container

* Add excalidraw-container class to focus on the container

* pass focus container to library to focus current instance of excal

* update docs

* remove util as it wont be used anywhere

* fix propagation not being stopped for React keyboard handling

* tweak reamde

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

* tweak changelog

* rename prop to handleKeyboardGlobally

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-04-13 01:29:25 +05:30
153ca6a7c6 chore(deps-dev): bump typescript in /src/packages/excalidraw (#3438)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.2.3 to 4.2.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.2.3...v4.2.4)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 19:06:44 +00:00
2618ac9f6e chore(deps-dev): bump @babel/core in /src/packages/utils (#3432)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.14 to 7.13.15.
- [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.13.15/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 19:05:54 +00:00
f64fd80493 chore(deps-dev): bump @babel/plugin-transform-runtime (#3435)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.13.10 to 7.13.15.
- [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.13.15/packages/babel-plugin-transform-runtime)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 19:00:35 +00:00
a884351137 chore(deps-dev): bump mini-css-extract-plugin (#3443)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.4.0 to 1.4.1.
- [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/v1.4.0...v1.4.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 18:58:53 +00:00
e546a85a8d chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3431)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.12 to 7.13.15.
- [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.13.15/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 18:57:23 +00:00
29e630086c chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3442)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.14 to 7.13.15.
- [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.13.15/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 00:24:47 +05:30
a82165cb50 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3433)
Bumps [webpack](https://github.com/webpack/webpack) from 5.30.0 to 5.31.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.30.0...v5.31.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 00:22:34 +05:30
4dc0159a05 chore(deps-dev): bump webpack in /src/packages/utils (#3436)
Bumps [webpack](https://github.com/webpack/webpack) from 5.30.0 to 5.31.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.30.0...v5.31.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 00:22:07 +05:30
458787d1d7 chore(deps-dev): bump css-loader in /src/packages/utils (#3437)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.0...v5.2.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 00:20:50 +05:30
815977296e chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3439)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.0...v5.2.1)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 00:20:23 +05:30
58f840aa93 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3440)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.12 to 7.13.15.
- [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.13.15/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-13 00:19:49 +05:30
422149c249 chore(deps): bump firebase from 8.3.2 to 8.3.3 (#3441)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-12 10:48:31 +02:00
a7cbe68ae8 refactor: improve types around dataState and libraryData (#3427) 2021-04-10 19:17:49 +02:00
c19c8ecd27 feat: Add scroll listener to the nearest scrollable container and allow consumer to disable it (#3408)
* fix: Add scroll listener to the nearest scrollable container

* fix

* use loop instead of recursion

* fix

* return document

* calculate nearest scrollable container in settimeout to unblock main thread

* Add prop detectNearestScroll and clear timeout on unmount

* disable scroll listener on excal app

* update prop name to detectScroll

* update docs

* remove settimeout

* tweak docs

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

* tweak changelog

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

* lint

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-04-09 20:44:54 +05:30
d91950bd03 feat: Add onPaste prop to customise clipboard paste event (#3420)
* Add Awaited type util

* Expose onPasteFromClipboard props

* Add `event` as second param for advanced usages

* Add support for async flows

* Extract ClipboardData type

* Rename `onPasteFromClipboard` to `onPaste`

* Remove unused type helper

* Add `onPaste` documentation

* tweak docs

* fix

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-04-09 20:19:58 +05:30
89472c14ed chore(deps): bump typescript from 4.2.3 to 4.2.4 (#3422)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.2.3 to 4.2.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/commits)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 19:55:25 +02:00
09dfd16b17 feat: use component dimensions to break to mobile (#3414)
Co-authored-by: Jed Fox <git@jedfox.com>
2021-04-08 19:54:50 +02:00
016e69b9f2 chore(deps): bump @sentry/integrations from 6.2.1 to 6.2.5 (#3423)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 17:46:04 +02:00
bb1f979718 chore(deps): bump @types/react-dom from 17.0.2 to 17.0.3 (#3419)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 17:19:17 +02:00
5fda8400f3 chore(deps): bump @testing-library/react from 11.2.5 to 11.2.6 (#3421)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.5 to 11.2.6.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v11.2.5...v11.2.6)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 17:03:31 +02:00
96beaa4354 chore(deps): bump browser-fs-access from 0.16.2 to 0.16.4 (#3418)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 16:51:50 +02:00
7183f1c83e chore(deps): bump i18next-browser-languagedetector from 6.0.1 to 6.1.0 (#3417)
Bumps [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/i18next/i18next-browser-languageDetector/releases)
- [Changelog](https://github.com/i18next/i18next-browser-languageDetector/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-browser-languageDetector/compare/v6.0.1...v6.1.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 16:32:37 +02:00
24ae9dca2e chore(deps-dev): bump firebase-tools from 9.6.1 to 9.9.0 (#3416)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.6.1 to 9.9.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.6.1...v9.9.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 16:14:16 +02:00
f6ac3ea7c6 chore(deps): bump firebase from 8.2.10 to 8.3.2 (#3391)
Bumps [firebase](https://github.com/firebase/firebase-js-sdk) from 8.2.10 to 8.3.2.
- [Release notes](https://github.com/firebase/firebase-js-sdk/releases)
- [Changelog](https://github.com/firebase/firebase-js-sdk/blob/master/CHANGELOG.md)
- [Commits](https://github.com/firebase/firebase-js-sdk/compare/firebase@8.2.10...firebase@8.3.2)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 16:00:29 +02:00
b88e0253cc chore(deps): bump @sentry/browser from 6.2.2 to 6.2.5 (#3403)
Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 6.2.2 to 6.2.5.
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/6.2.2...6.2.5)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-08 15:57:46 +02:00
1e48aafb9c fix: incorrectly caching png file handle (#3407) 2021-04-06 21:27:15 +02:00
34761200bf feat: Add screenshots to manifest.json (#3369)
* feat: Add screenshots to manifest.json

* rename screenshots
2021-04-06 23:02:58 +05:30
a0899966ff feat: enable drop event on the whole component (#3406)
Co-authored-by: Thang Vu <thang.huu.vu@mgm-tp.com>
2021-04-06 17:17:00 +02:00
c2b40dff92 docs: changelog tweaks and add Library updates for 0.6.0 (#3404)
* docs: changelog tweaks and Library updates for 0.6.0
* update readme_next to be same as readme
2021-04-06 00:38:14 +05:30
9733ecb3df fix: popover positioning (#3399) 2021-04-05 17:26:37 +02:00
189b721eed chore(deps): bump @testing-library/jest-dom from 5.11.9 to 5.11.10 (#3393)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.11.9 to 5.11.10.
- [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.11.9...v5.11.10)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-05 20:47:02 +05:30
90fd4a95df refactor: rename setCanvasOffsets to refresh and release @excalidraw/excalidraw v0.6.0 🎉 (#3398)
* docs: Release @excalidraw/excalidraw v0.6.0

* update

* fix

* Update src/packages/excalidraw/README.md

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

* rename setCanvasOffsets to refresh

* fix

* fix

* typo fix

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-04-04 22:05:02 +05:30
5d3e98fa04 chore: bump browser-fs-access to 0.16.2 (#3396) 2021-04-04 14:45:02 +02:00
422c25449f fix: export dialog canvas positioning (#3397) 2021-04-04 14:41:22 +02:00
67289ef4ce feat: reopen library menu on import from file (#3383)
Co-authored-by: Thang Vu <thang.huu.vu@mgm-tp.com>
2021-04-04 14:06:10 +02:00
233576628c feat: Support customising canvas actions 🎉 (#3364)
* feat: Support hiding save, save as, clear & export

* Remove canvasActions from state & minor changes

* Rename prop to UIOptions & pass default value

* Make requested changes

* better type checking so that optional check not needed at every point

* remove optional checks

* Add few tests

* Add describe block for canvasActions & use snapshot tests

* Add support for hiding canvas background picker

* Take snapshot of canvasActions instead of the whole app

* Add support for hiding dark mode toggle

* Update README.md

* Rename table heading

* Update changelog

* Make requested changes

* Update test name

* tweaks

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-04-04 15:57:14 +05:30
c54a099010 feat: Calculate width/height of canvas based on container dimensions (".excalidraw" selector) & remove props width & height (#3379)
* Remove width/height from the ".excalidraw" container so it will sized automatically.
* updated all ref calculation to ".excalidraw" instead of parent since now ".excalidraw" will get resized
* Remove props width/height as its not needed anymore.
* Resize handler is also not needed anymore.
* Position absolute canvas due to #3379 (comment)

* move css to style and remove one extra rerendering

* factor out mock logic for test

* set height, width so as to avoid unnecessary updates of regression snap

* better mock

* better type checking and omit width,height from getDefaultAppState and also restore

* revert

* default to window dimensions in constructor

* update docs

* update

* update

* tweaks
2021-04-04 15:05:16 +05:30
3b976613ba chore(deps-dev): bump ts-loader in /src/packages/utils (#3388)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.18 to 8.1.0.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.0.18...v8.1.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-04 09:12:34 +00:00
bee59747d1 chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#3385)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.18 to 8.1.0.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.0.18...v8.1.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-04 09:11:09 +00:00
2e1352f3fa chore(deps-dev): bump @babel/core in /src/packages/utils (#3386)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.13 to 7.13.14.
- [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.13.14/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-04 14:39:06 +05:30
6b65db7b68 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3387)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.13 to 7.13.14.
- [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.13.14/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-04 14:38:14 +05:30
e4c5ebf867 chore(deps-dev): bump webpack in /src/packages/excalidraw (#3389)
Bumps [webpack](https://github.com/webpack/webpack) from 5.28.0 to 5.30.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.28.0...v5.30.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-04 14:37:16 +05:30
0602f3cfe4 chore(deps-dev): bump webpack in /src/packages/utils (#3390)
Bumps [webpack](https://github.com/webpack/webpack) from 5.28.0 to 5.30.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.28.0...v5.30.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-04 14:36:59 +05:30
eade72b744 chore(deps): bump @types/jest from 26.0.21 to 26.0.22 (#3349)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.21 to 26.0.22.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-02 01:24:24 +03:00
ef5c9002ad chore(deps): bump react and react-dom (#3348)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom). These dependencies needed to be updated together.

Updates `react` from 17.0.1 to 17.0.2
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v17.0.2/packages/react)

Updates `react-dom` from 17.0.1 to 17.0.2
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v17.0.2/packages/react-dom)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-02 01:24:04 +03:00
aa9e1c4566 chore: Update translations from Crowdin (#3371) 2021-04-01 18:47:48 +03:00
edc7f7bf47 feat: calculate offsets when excalidraw container resizes (#3374)
* feat: calculate offsets when excalidraw container resizes

* fix

* rename

* update docs

* Update src/packages/excalidraw/README_NEXT.md

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-04-01 21:10:11 +05:30
1310256dcc fix: remove JSON.stringify when calculating storage as its not needed (#3373) 2021-04-01 11:19:31 +02:00
4ac1841d92 test: Add unit tests for package/utils (#3357)
* Update return type to reflect actual signature

* add tests

* Set getDimensions as optional

* add newlines between specs

* remove redundant assertion

* move fixtures to separate files

* Add spacing

* Move tests, add cases

* Add unit tests for package/utils exportToSvg

* extract default object in test

* Move test suite to new file
2021-03-31 17:58:25 +05:30
bdf6e53289 fix: Add aria-label to end-to-end encryption blog link (#3367) 2021-03-31 17:02:54 +05:30
a6706cff20 feat: export types for package @excalidraw/excalidraw 🎉 (#3337)
* feat: export types for package @excalidraw/excalidraw

* update

* remove

* Add lib in tsconfig-types and Add global.d.ts, and errors down to 39 :)

* Add declaration for scss so typescript allows scss imports, errors down to 37 :)

* Add css.d.ts, errors down to 32 yay

* set target to es6, all errors resolved yay

* move types outside dist

* update docs

* fix
2021-03-30 23:51:55 +05:30
c739ac5c61 chore: Update translations from Crowdin (#3335)
* New translations en.json (Greek)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Finnish)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage
2021-03-30 12:22:15 +03:00
0d818f3810 feat: Add renderCustomStats prop and expose setToastMessage API via refs to update toast (#3360)
* feat: Add renderCustomStats prop to render extra stats & move storage and version to renderCustomStats

* expose Api to update toast message so single instance of toast is used

* rename to setToastMessage

* update docs
2021-03-29 20:06:34 +05:30
58a7568c9f chore(deps): bump nanoid from 3.1.21 to 3.1.22 (#3350)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.21 to 3.1.22.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.21...3.1.22)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-29 17:09:44 +03:00
722e5ca845 refactor: Use arrow function where possible (#3315) 2021-03-29 17:09:20 +03:00
bb568a9670 chore: Remove duplicate Twitter og:image (#3359)
* removed-duplicate-twitter-ogtags

* put favicon back

* fix lint
2021-03-29 13:18:21 +05:30
0f5b0d1d1d docs: revert README to last version and add README_NEXT with changes for next version (#3355) 2021-03-29 01:12:27 +05:30
25fd275158 fix: Don't share collab types with core (#3353)
* fix: Don't share collab types with core

* fix

* remove

* fix
2021-03-28 19:26:03 +05:30
3d047d57a7 build: Add separate dev and prod builds and add source-maps to dev build 🎉 (#3330)
* build: Add separate dev and prod builds and add sourcemaps to dev build

* update

* add

* update changelog
2021-03-28 13:48:26 +05:30
26a6f9e76d chore(deps-dev): bump webpack-cli in /src/packages/utils (#3342)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.5.0...webpack-cli@4.6.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 08:13:04 +00:00
1c11bb5b41 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#3351)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.10 to 7.13.12.
- [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.13.12/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:42:20 +05:30
aced1cc6f5 chore(deps-dev): bump css-loader in /src/packages/utils (#3341)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.3 to 5.2.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.3...v5.2.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 08:11:35 +00:00
f3f85b4c90 chore(deps-dev): bump @babel/preset-react in /src/packages/excalidraw (#3343)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.12.13 to 7.13.13.
- [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.13.13/packages/babel-preset-react)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:36:35 +05:30
86781f09dd chore(deps-dev): bump webpack in /src/packages/utils (#3344)
Bumps [webpack](https://github.com/webpack/webpack) from 5.27.1 to 5.28.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.27.1...v5.28.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:35:59 +05:30
a94b44440e chore(deps-dev): bump webpack in /src/packages/excalidraw (#3339)
Bumps [webpack](https://github.com/webpack/webpack) from 5.27.1 to 5.28.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.27.1...v5.28.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:35:28 +05:30
77bf553ed8 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#3346)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.13.10 to 7.13.12.
- [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.13.12/packages/babel-preset-env)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:34:56 +05:30
fce7047199 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#3352)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.1.3 to 5.2.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.1.3...v5.2.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:34:22 +05:30
9905deb4b4 chore(deps-dev): bump webpack-cli in /src/packages/excalidraw (#3338)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.5.0 to 4.6.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.5.0...webpack-cli@4.6.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:08:57 +05:30
fee84f3807 chore(deps-dev): bump mini-css-extract-plugin (#3340)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.3.9 to 1.4.0.
- [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/v1.3.9...v1.4.0)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:08:14 +05:30
9020ab3761 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#3345)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.10 to 7.13.13.
- [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.13.13/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:07:31 +05:30
136f8b2279 chore(deps-dev): bump @babel/core in /src/packages/utils (#3347)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.13.10 to 7.13.13.
- [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.13.13/packages/babel-core)

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-03-28 13:06:56 +05:30
8670b2d587 fix: support d&d of files without extension (#3168) 2021-03-26 22:12:02 +01:00
b081a09962 fix: positions stats for linear elements (#3331) 2021-03-26 22:56:27 +02:00
10a23a10a5 chore: Update translations from Crowdin (#3313) 2021-03-26 22:54:57 +02:00
30ae4b8bf2 feat: don't unnecessarily prompt when installing libraries (#3329) 2021-03-26 18:32:38 +01:00
cf9e29834d feat: prefer hash when importing libraries & expose importLibrary (#3320) 2021-03-26 18:10:43 +01:00
5d26c15daf fix: debounce.flush invokes func even if never queued before (#3326)
* fix: `debounce.flush` invokes func even if never queued before

* reset after debounced invocation

* account for fn throwing
2021-03-26 17:12:32 +01:00
b0d7ff290f feat: Add option to flip single element on the context menu (#2520)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-03-26 16:45:08 +01:00
458e6d6c24 fix: state selection state on opening contextMenu (#3333) 2021-03-26 16:41:57 +01:00
a21db08cae Update to browser-fs-access v0.16.0 (#3323) 2021-03-26 11:37:27 +02:00
1b626175de feat: use origin + pathname as libraryReturnUrl default (#3325) 2021-03-25 18:27:40 +01:00
5ffdd3f32d docs: Readme tweaks :) (#3319)
* docs: Readme tweaks :)

* update

* fix

* Add info about collab
2021-03-25 21:38:15 +05:30
77b873251a fix: Add unique key for library header to resolve dev warnings (#3316) 2021-03-23 22:25:54 +05:30
b50b8f7b0d fix: disallow create text in viewMode on mobile (#3219) 2021-03-23 19:36:16 +05:30
240 changed files with 20745 additions and 10537 deletions

View File

@ -0,0 +1,26 @@
name: Auto release @excalidraw/excalidraw-next
on:
push:
branches:
- master
jobs:
Auto-release-excalidraw-next:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Set up publish access
run: |
npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Auto release
run: |
yarn autorelease

3
.gitignore vendored
View File

@ -5,9 +5,11 @@
.env.test.local
.envrc
.eslintcache
.history
.idea
.vercel
.vscode
.yarn
*.log
*.tgz
build
@ -20,3 +22,4 @@ package-lock.json
static
yarn-debug.log*
yarn-error.log*
src/packages/excalidraw/types

View File

@ -10,7 +10,7 @@ ARG NODE_ENV=production
COPY . .
RUN yarn build:app:docker
FROM nginx:1.17-alpine
FROM nginx:1.21-alpine
COPY --from=build /opt/node_app/build /usr/share/nginx/html

View File

@ -93,7 +93,7 @@ These instructions will get you a copy of the project up and running on your loc
#### Requirements
- [Node.js](https://nodejs.org/en/)
- [Yarn](https://yarnpkg.com/getting-started/install)
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
- [Git](https://git-scm.com/downloads)
#### Clone the repo
@ -102,6 +102,20 @@ These instructions will get you a copy of the project up and running on your loc
git clone https://github.com/excalidraw/excalidraw.git
```
#### Install the dependencies
```bash
yarn
```
#### Start the server
```bash
yarn start
```
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
#### Commands
| Command | Description |

View File

@ -2,5 +2,8 @@
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"storage": {
"rules": "storage.rules"
}
}

View File

@ -0,0 +1,12 @@
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /{migrations} {
match /{scenes}/{scene} {
allow get, write: if true;
// redundant, but let's be explicit'
allow list: if false;
}
}
}
}

View File

@ -19,34 +19,35 @@
]
},
"dependencies": {
"@sentry/browser": "6.2.2",
"@sentry/integrations": "6.2.1",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5",
"@types/jest": "26.0.21",
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.11.10",
"@testing-library/react": "11.2.6",
"@types/jest": "26.0.22",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.2",
"@types/react-dom": "17.0.3",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.15.3",
"browser-fs-access": "0.18.0",
"clsx": "1.1.1",
"firebase": "8.2.10",
"i18next-browser-languagedetector": "6.0.1",
"firebase": "8.3.3",
"i18next-browser-languagedetector": "6.1.0",
"lodash.throttle": "4.1.1",
"nanoid": "3.1.21",
"nanoid": "3.1.22",
"open-color": "1.8.0",
"pako": "1.0.11",
"perfect-freehand": "0.4.7",
"png-chunk-text": "1.0.0",
"png-chunks-encode": "1.0.0",
"png-chunks-extract": "1.0.0",
"points-on-curve": "0.2.0",
"pwacompat": "2.0.17",
"react": "17.0.1",
"react-dom": "17.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "4.0.3",
"roughjs": "4.3.1",
"sass": "1.32.8",
"roughjs": "4.4.1",
"sass": "1.32.10",
"socket.io-client": "2.3.1",
"typescript": "4.2.3"
"typescript": "4.2.4"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
@ -54,9 +55,9 @@
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.1",
"@types/resize-observer-browser": "0.1.5",
"eslint-config-prettier": "8.1.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.6.1",
"firebase-tools": "9.9.0",
"husky": "4.3.8",
"jest-canvas-mock": "2.3.1",
"lint-staged": "10.5.4",
@ -103,6 +104,7 @@
"test:other": "yarn prettier --list-different",
"test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app"
"test": "yarn test:app",
"autorelease": "node scripts/autorelease.js"
}
}

Binary file not shown.

View File

@ -51,8 +51,7 @@
name="twitter:description"
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/>
<!-- OG tags require absolute url for images -->
<meta name="twitter:image" content="https://excalidraw.com/og-image.png" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<!-- Excalidraw version -->
@ -108,15 +107,17 @@
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
<style>
body {
body,
html {
margin: 0;
--ui-font: system-ui, BlinkMacSystemFont, -apple-system, Segoe UI,
Roboto, Helvetica, Arial, sans-serif;
font-family: var(--ui-font);
-webkit-text-size-adjust: 100%;
width: 100vw;
height: 100vh;
width: 100%;
height: 100%;
overflow: hidden;
}
.visually-hidden {
@ -126,6 +127,7 @@
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
user-select: none;
}
.LoadingMessage {
@ -148,6 +150,24 @@
color: var(--popup-text-color);
font-size: 1.3em;
}
#root {
height: 100%;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
@media screen and (min-width: 1200px) {
-webkit-touch-callout: default;
-webkit-user-select: auto;
-khtml-user-select: auto;
-moz-user-select: auto;
-ms-user-select: auto;
user-select: auto;
}
}
</style>
</head>

View File

@ -39,5 +39,37 @@
}
]
}
}
},
"screenshots": [
{
"src": "/screenshots/virtual-whiteboard.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/wireframe.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/illustration.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/shapes.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/collaboration.png",
"type": "image/png",
"sizes": "462x945"
},
{
"src": "/screenshots/export.png",
"type": "image/png",
"sizes": "462x945"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

51
scripts/autorelease.js Normal file
View File

@ -0,0 +1,51 @@
const fs = require("fs");
const { exec, execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const publish = () => {
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
} catch (e) {
console.error(e);
}
};
// get files changed between prev and head commit
exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
if (error || stderr) {
console.error(error);
process.exit(1);
}
const changedFiles = stdout.trim().split("\n");
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/;
const excalidrawPackageFiles = changedFiles.filter((file) => {
return file.indexOf("src") >= 0 && !filesToIgnoreRegex.test(file);
});
if (!excalidrawPackageFiles.length) {
process.exit(0);
}
// update package.json
pkg.version = `${pkg.version}-${getShortCommitHash()}`;
pkg.name = "@excalidraw/excalidraw-next";
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
// update readme
const data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
publish();
});

View File

@ -37,6 +37,9 @@ const crowdinMap = {
"uk-UA": "en-uk",
"zh-CN": "en-zhcn",
"zh-TW": "en-zhtw",
"lv-LV": "en-lv",
"cs-CZ": "en-cs",
"kk-KZ": "en-kk",
};
const flags = {
@ -74,6 +77,9 @@ const flags = {
"uk-UA": "🇺🇦",
"zh-CN": "🇨🇳",
"zh-TW": "🇹🇼",
"lv-LV": "🇱🇻",
"cs-CZ": "🇨🇿",
"kk-KZ": "🇰🇿",
};
const languages = {
@ -111,6 +117,9 @@ const languages = {
"uk-UA": "Українська",
"zh-CN": "简体中文",
"zh-TW": "繁體中文",
"lv-LV": "Latviešu",
"cs-CZ": "Česky",
"kk-KZ": "Қазақ тілі",
};
const percentages = fs.readFileSync(

39
scripts/release.js Normal file
View File

@ -0,0 +1,39 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const updateReadme = require("./updateReadme");
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const updatePackageVersion = (nextVersion) => {
const pkg = require(excalidrawPackage);
pkg.version = nextVersion;
const content = `${JSON.stringify(pkg, null, 2)}\n`;
fs.writeFileSync(excalidrawPackage, content, "utf-8");
};
const release = async (nextVersion) => {
try {
updateReadme();
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release excalidraw@excalidraw@${nextVersion} 🎉"`,
);
/* eslint-disable no-console */
console.log("Done!");
} catch (e) {
console.error(e);
process.exit(1);
}
};
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
release(nextVersion);

View File

@ -0,0 +1,97 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const lastVersion = pkg.version;
const existingChangeLog = fs.readFileSync(
`${excalidrawDir}/CHANGELOG.md`,
"utf8",
);
const supportedTypes = ["feat", "fix", "style", "refactor", "perf", "build"];
const headerForType = {
feat: "Features",
fix: "Fixes",
style: "Styles",
refactor: " Refactor",
perf: "Performance",
build: "Build",
};
const getCommitHashForLastVersion = async () => {
try {
const commitMessage = `"release @excalidraw/excalidraw@${lastVersion}"`;
const { stdout } = await exec(
`git log --format=format:"%H" --grep=${commitMessage}`,
);
return stdout;
} catch (e) {
console.error(e);
}
};
const getLibraryCommitsSinceLastRelease = async () => {
const commitHash = await getCommitHashForLastVersion();
const { stdout } = await exec(
`git log --pretty=format:%s ${commitHash}...master`,
);
const commitsSinceLastRelease = stdout.split("\n");
const commitList = {};
supportedTypes.forEach((type) => {
commitList[type] = [];
});
commitsSinceLastRelease.forEach((commit) => {
const indexOfColon = commit.indexOf(":");
const type = commit.slice(0, indexOfColon);
if (!supportedTypes.includes(type)) {
return;
}
const messageWithoutType = commit.slice(indexOfColon + 1).trim();
const messageWithCapitalizeFirst =
messageWithoutType.charAt(0).toUpperCase() + messageWithoutType.slice(1);
const prNumber = commit.match(/\(#([0-9]*)\)/)[1];
// return if the changelog already contains the pr number which would happen for package updates
if (existingChangeLog.includes(prNumber)) {
return;
}
const prMarkdown = `[#${prNumber}](https://github.com/excalidraw/excalidraw/pull/${prNumber})`;
const messageWithPRLink = messageWithCapitalizeFirst.replace(
/\(#[0-9]*\)/,
prMarkdown,
);
commitList[type].push(messageWithPRLink);
});
return commitList;
};
const updateChangelog = async (nextVersion) => {
const commitList = await getLibraryCommitsSinceLastRelease();
let changelogForLibrary =
"## Excalidraw Library\n\n**_This section lists the updates made to the excalidraw library and will not affect the integration._**\n\n";
supportedTypes.forEach((type) => {
if (commitList[type].length) {
changelogForLibrary += `### ${headerForType[type]}\n\n`;
const commits = commitList[type];
commits.forEach((commit) => {
changelogForLibrary += `- ${commit}\n\n`;
});
}
});
changelogForLibrary += "---\n";
const lastVersionIndex = existingChangeLog.indexOf(`## ${lastVersion}`);
let updatedContent =
existingChangeLog.slice(0, lastVersionIndex) +
changelogForLibrary +
existingChangeLog.slice(lastVersionIndex);
const currentDate = new Date().toISOString().slice(0, 10);
const newVersion = `## ${nextVersion} (${currentDate})`;
updatedContent = updatedContent.replace(`## Unreleased`, newVersion);
fs.writeFileSync(`${excalidrawDir}/CHANGELOG.md`, updatedContent, "utf8");
};
module.exports = updateChangelog;

27
scripts/updateReadme.js Normal file
View File

@ -0,0 +1,27 @@
const fs = require("fs");
const updateReadme = () => {
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
// remove note for unstable release
data = data.replace(
/<!-- unstable-readme-start-->[\s\S]*?<!-- unstable-readme-end-->/,
"",
);
// replace "excalidraw-next" with "excalidraw"
data = data.replace(/excalidraw-next/g, "excalidraw");
data = data.trim();
const demoIndex = data.indexOf("### Demo");
const excalidrawNextNote =
"#### Note\n\n**If you don't want to wait for the next stable release and try out the unreleased changes you can use [@excalidraw/excalidraw-next](https://www.npmjs.com/package/@excalidraw/excalidraw-next).**\n\n";
// Add excalidraw next note to try out for unreleased changes
data = data.slice(0, demoIndex) + excalidrawNextNote + data.slice(demoIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
module.exports = updateReadme;

View File

@ -2,18 +2,20 @@ import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { deepCopyElement } from "../element/newElement";
import { Library } from "../data/library";
export const actionAddToLibrary = register({
name: "addToLibrary",
perform: (elements, appState) => {
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
Library.loadLibrary().then((items) => {
Library.saveLibrary([...items, selectedElements.map(deepCopyElement)]);
app.library.loadLibrary().then((items) => {
app.library.saveLibrary([
...items,
selectedElements.map(deepCopyElement),
]);
});
return false;
},

View File

@ -3,12 +3,13 @@ import { getDefaultAppState } from "../appState";
import { ColorPicker } from "../components/ColorPicker";
import { resetZoom, trash, zoomIn, zoomOut } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { ZOOM_STEP } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element";
import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll";
@ -21,8 +22,8 @@ export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
perform: (_, appState, value) => {
return {
appState: { ...appState, viewBackgroundColor: value },
commitToHistory: true,
appState: { ...appState, ...value },
commitToHistory: !!value.viewBackgroundColor,
};
},
PanelComponent: ({ appState, updateData }) => {
@ -32,7 +33,12 @@ export const actionChangeViewBackgroundColor = register({
label={t("labels.canvasBackground")}
type="canvasBackground"
color={appState.viewBackgroundColor}
onChange={(color) => updateData(color)}
onChange={(color) => updateData({ viewBackgroundColor: color })}
isActive={appState.openPopup === "canvasColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "canvasColorPicker" : null })
}
data-testid="canvas-background-picker"
/>
</div>
);
@ -53,7 +59,6 @@ export const actionClearCanvas = register({
exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene,
gridSize: appState.gridSize,
shouldAddWatermark: appState.shouldAddWatermark,
showStats: appState.showStats,
pasteDialog: appState.pasteDialog,
},
@ -72,6 +77,7 @@ export const actionClearCanvas = register({
updateData(null);
}
}}
data-testid="clear-canvas-button"
/>
),
});
@ -258,3 +264,27 @@ export const actionZoomToFit = register({
!event.altKey &&
!event[KEYS.CTRL_OR_CMD],
});
export const actionToggleTheme = register({
name: "toggleTheme",
perform: (_, appState, value) => {
return {
appState: {
...appState,
theme: value || (appState.theme === "light" ? "dark" : "light"),
},
commitToHistory: false,
};
},
PanelComponent: ({ appState, updateData }) => (
<div style={{ marginInlineStart: "0.25rem" }}>
<DarkModeToggle
value={appState.theme}
onChange={(theme) => {
updateData(theme);
}}
/>
</div>
),
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
});

View File

@ -50,7 +50,6 @@ export const actionCopyAsSvg = register({
? selectedElements
: getNonDeletedElements(elements),
appState,
app.canvas,
appState,
);
return {
@ -89,7 +88,6 @@ export const actionCopyAsPng = register({
? selectedElements
: getNonDeletedElements(elements),
appState,
app.canvas,
appState,
);
return {

View File

@ -1,6 +1,6 @@
import React from "react";
import { trackEvent } from "../analytics";
import { load, questionCircle, save, saveAs } from "../components/icons";
import { load, questionCircle, saveAs } from "../components/icons";
import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import "../components/ToolIcon.scss";
@ -8,10 +8,16 @@ import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import { KEYS } from "../keys";
import { register } from "./register";
import { supported } from "browser-fs-access";
import { supported as fsSupported } from "browser-fs-access";
import { CheckboxItem } from "../components/CheckboxItem";
import { getExportSize } from "../scene/export";
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element";
import { ActiveFile } from "../components/ActiveFile";
export const actionChangeProjectName = register({
name: "changeProjectName",
@ -31,6 +37,54 @@ export const actionChangeProjectName = register({
),
});
export const actionChangeExportScale = register({
name: "changeExportScale",
perform: (_elements, appState, value) => {
return {
appState: { ...appState, exportScale: value },
commitToHistory: false,
};
},
PanelComponent: ({ elements: allElements, appState, updateData }) => {
const elements = getNonDeletedElements(allElements);
const exportSelected = isSomeElementSelected(elements, appState);
const exportedElements = exportSelected
? getSelectedElements(elements, appState)
: elements;
return (
<>
{EXPORT_SCALES.map((s) => {
const [width, height] = getExportSize(
exportedElements,
DEFAULT_EXPORT_PADDING,
s,
);
const scaleButtonTitle = `${t(
"buttons.scale",
)} ${s}x (${width}x${height})`;
return (
<ToolButton
key={s}
size="s"
type="radio"
icon={`${s}x`}
name="export-canvas-scale"
title={scaleButtonTitle}
aria-label={scaleButtonTitle}
id="export-canvas-scale"
checked={s === appState.exportScale}
onChange={() => updateData(s)}
/>
);
})}
</>
);
},
});
export const actionChangeExportBackground = register({
name: "changeExportBackground",
perform: (_elements, appState, value) => {
@ -40,14 +94,12 @@ export const actionChangeExportBackground = register({
};
},
PanelComponent: ({ appState, updateData }) => (
<label>
<input
type="checkbox"
checked={appState.exportBackground}
onChange={(event) => updateData(event.target.checked)}
/>{" "}
<CheckboxItem
checked={appState.exportBackground}
onChange={(checked) => updateData(checked)}
>
{t("labels.withBackground")}
</label>
</CheckboxItem>
),
});
@ -60,46 +112,20 @@ export const actionChangeExportEmbedScene = register({
};
},
PanelComponent: ({ appState, updateData }) => (
<label style={{ display: "flex" }}>
<input
type="checkbox"
checked={appState.exportEmbedScene}
onChange={(event) => updateData(event.target.checked)}
/>{" "}
<CheckboxItem
checked={appState.exportEmbedScene}
onChange={(checked) => updateData(checked)}
>
{t("labels.exportEmbedScene")}
<Tooltip
label={t("labels.exportEmbedScene_details")}
position="above"
long={true}
>
<div className="TooltipIcon">{questionCircle}</div>
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}>
<div className="Tooltip-icon">{questionCircle}</div>
</Tooltip>
</label>
</CheckboxItem>
),
});
export const actionChangeShouldAddWatermark = register({
name: "changeShouldAddWatermark",
perform: (_elements, appState, value) => {
return {
appState: { ...appState, shouldAddWatermark: value },
commitToHistory: false,
};
},
PanelComponent: ({ appState, updateData }) => (
<label>
<input
type="checkbox"
checked={appState.shouldAddWatermark}
onChange={(event) => updateData(event.target.checked)}
/>{" "}
{t("labels.addWatermark")}
</label>
),
});
export const actionSaveScene = register({
name: "saveScene",
export const actionSaveToActiveFile = register({
name: "saveToActiveFile",
perform: async (elements, appState, value) => {
const fileHandleExists = !!appState.fileHandle;
try {
@ -128,20 +154,16 @@ export const actionSaveScene = register({
},
keyTest: (event) =>
event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey,
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={save}
title={t("buttons.save")}
aria-label={t("buttons.save")}
showAriaLabel={useIsMobile()}
onClick={() => updateData(null)}
PanelComponent: ({ updateData, appState }) => (
<ActiveFile
onSave={() => updateData(null)}
fileName={appState.fileHandle?.name}
/>
),
});
export const actionSaveAsScene = register({
name: "saveAsScene",
export const actionSaveFileToDisk = register({
name: "saveFileToDisk",
perform: async (elements, appState, value) => {
try {
const { fileHandle } = await saveAsJSON(elements, {
@ -165,8 +187,9 @@ export const actionSaveAsScene = register({
title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")}
showAriaLabel={useIsMobile()}
hidden={!supported}
hidden={!fsSupported}
onClick={() => updateData(null)}
data-testid="save-as-button"
/>
),
});
@ -178,7 +201,7 @@ export const actionLoadScene = register({
const {
elements: loadedElements,
appState: loadedAppState,
} = await loadFromJSON(appState);
} = await loadFromJSON(appState, elements);
return {
elements: loadedElements,
appState: loadedAppState,
@ -204,6 +227,7 @@ export const actionLoadScene = register({
aria-label={t("buttons.load")}
showAriaLabel={useIsMobile()}
onClick={updateData}
data-testid="load-button"
/>
),
});

View File

@ -18,7 +18,7 @@ import { isBindingElement } from "../element/typeChecks";
export const actionFinalize = register({
name: "finalize",
perform: (elements, appState, _, { canvas }) => {
perform: (elements, appState, _, { canvas, focusContainer }) => {
if (appState.editingLinearElement) {
const {
elementId,
@ -51,19 +51,19 @@ export const actionFinalize = register({
let newElements = elements;
if (window.document.activeElement instanceof HTMLElement) {
window.document.activeElement.blur();
focusContainer();
}
const multiPointElement = appState.multiElement
? appState.multiElement
: appState.editingElement?.type === "draw"
: appState.editingElement?.type === "freedraw"
? appState.editingElement
: null;
if (multiPointElement) {
// pen and mouse have hover
if (
multiPointElement.type !== "draw" &&
multiPointElement.type !== "freedraw" &&
appState.lastPointerDownWith !== "touch"
) {
const { points, lastCommittedPoint } = multiPointElement;
@ -86,7 +86,7 @@ export const actionFinalize = register({
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
if (
multiPointElement.type === "line" ||
multiPointElement.type === "draw"
multiPointElement.type === "freedraw"
) {
if (isLoop) {
const linePoints = multiPointElement.points;
@ -118,22 +118,24 @@ export const actionFinalize = register({
);
}
if (!appState.elementLocked && appState.elementType !== "draw") {
if (!appState.elementLocked && appState.elementType !== "freedraw") {
appState.selectedElementIds[multiPointElement.id] = true;
}
}
if (
(!appState.elementLocked && appState.elementType !== "draw") ||
(!appState.elementLocked && appState.elementType !== "freedraw") ||
!multiPointElement
) {
resetCursor(canvas);
}
return {
elements: newElements,
appState: {
...appState,
elementType:
(appState.elementLocked || appState.elementType === "draw") &&
(appState.elementLocked || appState.elementType === "freedraw") &&
multiPointElement
? appState.elementType
: "selection",
@ -145,14 +147,14 @@ export const actionFinalize = register({
selectedElementIds:
multiPointElement &&
!appState.elementLocked &&
appState.elementType !== "draw"
appState.elementType !== "freedraw"
? {
...appState.selectedElementIds,
[multiPointElement.id]: true,
}
: appState.selectedElementIds,
},
commitToHistory: appState.elementType === "draw",
commitToHistory: appState.elementType === "freedraw",
};
},
keyTest: (event, appState) =>

207
src/actions/actionFlip.ts Normal file
View File

@ -0,0 +1,207 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getElementMap, getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
import { AppState } from "../types";
import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1 && eligibleElements[0].type !== "text";
};
const enableActionFlipVertical = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const eligibleElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return eligibleElements.length === 1;
};
export const actionFlipHorizontal = register({
name: "flipHorizontal",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "horizontal"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyH",
contextItemLabel: "labels.flipHorizontal",
contextItemPredicate: (elements, appState) =>
enableActionFlipHorizontal(elements, appState),
});
export const actionFlipVertical = register({
name: "flipVertical",
perform: (elements, appState) => {
return {
elements: flipSelectedElements(elements, appState, "vertical"),
appState,
commitToHistory: true,
};
},
keyTest: (event) => event.shiftKey && event.code === "KeyV",
contextItemLabel: "labels.flipVertical",
contextItemPredicate: (elements, appState) =>
enableActionFlipVertical(elements, appState),
});
const flipSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical",
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
// remove once we allow for groups of elements to be flipped
if (selectedElements.length > 1) {
return elements;
}
const updatedElements = flipElements(
selectedElements,
appState,
flipDirection,
);
const updatedElementsMap = getElementMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
};
const flipElements = (
elements: NonDeleted<ExcalidrawElement>[],
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
for (let i = 0; i < elements.length; i++) {
flipElement(elements[i], appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(elements[i], Math.PI);
}
}
return elements;
};
const flipElement = (
element: NonDeleted<ExcalidrawElement>,
appState: AppState,
) => {
const originalX = element.x;
const originalY = element.y;
const width = element.width;
const height = element.height;
const originalAngle = normalizeAngle(element.angle);
let finalOffsetX = 0;
if (isLinearElement(element) || isFreeDrawElement(element)) {
finalOffsetX =
element.points.reduce((max, point) => Math.max(max, point[0]), 0) * 2 -
element.width;
}
// Rotate back to zero, if necessary
mutateElement(element, {
angle: normalizeAngle(0),
});
// Flip unrotated by pulling TransformHandle to opposite side
const transformHandles = getTransformHandles(element, appState.zoom);
let usingNWHandle = true;
let newNCoordsX = 0;
let nHandle = transformHandles.nw;
if (!nHandle) {
// Use ne handle instead
usingNWHandle = false;
nHandle = transformHandles.ne;
if (!nHandle) {
mutateElement(element, {
angle: originalAngle,
});
return;
}
}
if (isLinearElement(element)) {
for (let i = 1; i < element.points.length; i++) {
LinearElementEditor.movePoint(element, i, [
-element.points[i][0],
element.points[i][1],
]);
}
LinearElementEditor.normalizePoints(element);
} else {
// calculate new x-coord for transformation
newNCoordsX = usingNWHandle ? element.x + 2 * width : element.x - 2 * width;
resizeSingleElement(
element,
true,
element,
usingNWHandle ? "nw" : "ne",
false,
newNCoordsX,
nHandle[1],
);
// fix the size to account for handle sizes
mutateElement(element, {
width,
height,
});
}
// Rotate by (360 degrees - original angle)
let angle = normalizeAngle(2 * Math.PI - originalAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(angle + 2 * Math.PI);
}
mutateElement(element, {
angle,
});
// Move back to original spot to appear "flipped in place"
mutateElement(element, {
x: originalX + finalOffsetX,
y: originalY,
});
updateBoundElements(element);
};
const rotateElement = (element: ExcalidrawElement, rotationAngle: number) => {
const originalX = element.x;
const originalY = element.y;
let angle = normalizeAngle(element.angle + rotationAngle);
if (angle < 0) {
// check, probably unnecessary
angle = normalizeAngle(2 * Math.PI + angle);
}
mutateElement(element, {
angle,
});
// Move back to original spot
mutateElement(element, {
x: originalX,
y: originalY,
});
};

View File

@ -3,7 +3,7 @@ import React from "react";
import { undo, redo } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
import { SceneHistory, HistoryEntry } from "../history";
import History, { HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { isWindows, KEYS } from "../keys";
@ -59,7 +59,7 @@ const writeData = (
return { commitToHistory };
};
type ActionCreator = (history: SceneHistory) => Action;
type ActionCreator = (history: History) => Action;
export const createUndoAction: ActionCreator = (history) => ({
name: "undo",

View File

@ -70,7 +70,10 @@ export const actionFullScreen = register({
export const actionShortcuts = register({
name: "toggleShortcuts",
perform: (_elements, appState) => {
perform: (_elements, appState, _, { focusContainer }) => {
if (appState.showHelpDialog) {
focusContainer();
}
return {
appState: {
...appState,

View File

@ -13,6 +13,13 @@ import {
FillCrossHatchIcon,
FillHachureIcon,
FillSolidIcon,
FontFamilyCodeIcon,
FontFamilyHandDrawnIcon,
FontFamilyNormalIcon,
FontSizeExtraLargeIcon,
FontSizeLargeIcon,
FontSizeMediumIcon,
FontSizeSmallIcon,
SloppinessArchitectIcon,
SloppinessArtistIcon,
SloppinessCartoonistIcon,
@ -20,18 +27,15 @@ import {
StrokeStyleDottedIcon,
StrokeStyleSolidIcon,
StrokeWidthIcon,
FontSizeSmallIcon,
FontSizeMediumIcon,
FontSizeLargeIcon,
FontSizeExtraLargeIcon,
FontFamilyHandDrawnIcon,
FontFamilyNormalIcon,
FontFamilyCodeIcon,
TextAlignLeftIcon,
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
} from "../components/icons";
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
FONT_FAMILY,
} from "../constants";
import {
getNonDeletedElements,
isTextElement,
@ -44,7 +48,7 @@ import {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
FontFamily,
FontFamilyValues,
TextAlign,
} from "../element/types";
import { getLanguage, t } from "../i18n";
@ -99,13 +103,18 @@ export const actionChangeStrokeColor = register({
name: "changeStrokeColor",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
strokeColor: value,
}),
),
appState: { ...appState, currentItemStrokeColor: value },
commitToHistory: true,
...(value.currentItemStrokeColor && {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
}),
),
}),
appState: {
...appState,
...value,
},
commitToHistory: !!value.currentItemStrokeColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
@ -120,7 +129,11 @@ export const actionChangeStrokeColor = register({
(element) => element.strokeColor,
appState.currentItemStrokeColor,
)}
onChange={updateData}
onChange={(color) => updateData({ currentItemStrokeColor: color })}
isActive={appState.openPopup === "strokeColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "strokeColorPicker" : null })
}
/>
</>
),
@ -130,13 +143,18 @@ export const actionChangeBackgroundColor = register({
name: "changeBackgroundColor",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
backgroundColor: value,
}),
),
appState: { ...appState, currentItemBackgroundColor: value },
commitToHistory: true,
...(value.currentItemBackgroundColor && {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
backgroundColor: value.currentItemBackgroundColor,
}),
),
}),
appState: {
...appState,
...value,
},
commitToHistory: !!value.currentItemBackgroundColor,
};
},
PanelComponent: ({ elements, appState, updateData }) => (
@ -151,7 +169,11 @@ export const actionChangeBackgroundColor = register({
(element) => element.backgroundColor,
appState.currentItemBackgroundColor,
)}
onChange={updateData}
onChange={(color) => updateData({ currentItemBackgroundColor: color })}
isActive={appState.openPopup === "backgroundColorPicker"}
setActive={(active) =>
updateData({ openPopup: active ? "backgroundColorPicker" : null })
}
/>
</>
),
@ -481,19 +503,23 @@ export const actionChangeFontFamily = register({
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const options: { value: FontFamily; text: string; icon: JSX.Element }[] = [
const options: {
value: FontFamilyValues;
text: string;
icon: JSX.Element;
}[] = [
{
value: 1,
value: FONT_FAMILY.Virgil,
text: t("labels.handDrawn"),
icon: <FontFamilyHandDrawnIcon theme={appState.theme} />,
},
{
value: 2,
value: FONT_FAMILY.Helvetica,
text: t("labels.normal"),
icon: <FontFamilyNormalIcon theme={appState.theme} />,
},
{
value: 3,
value: FONT_FAMILY.Cascadia,
text: t("labels.code"),
icon: <FontFamilyCodeIcon theme={appState.theme} />,
},
@ -502,7 +528,7 @@ export const actionChangeFontFamily = register({
return (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
<ButtonIconSelect<FontFamily | false>
<ButtonIconSelect<FontFamilyValues | false>
group="font-family"
options={options}
value={getFormValue(

View File

@ -1,4 +1,5 @@
import { register } from "./register";
import { CODES, KEYS } from "../keys";
export const actionToggleStats = register({
name: "stats",
@ -13,4 +14,6 @@ export const actionToggleStats = register({
},
checked: (appState) => appState.showStats,
contextItemLabel: "stats.title",
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH,
});

View File

@ -10,7 +10,6 @@ export const actionToggleViewMode = register({
appState: {
...appState,
viewModeEnabled: !this.checked!(appState),
selectedElementIds: {},
},
commitToHistory: false,
};

View File

@ -26,6 +26,7 @@ export {
actionZoomOut,
actionResetZoom,
actionZoomToFit,
actionToggleTheme,
} from "./actionCanvas";
export { actionFinalize } from "./actionFinalize";
@ -33,8 +34,8 @@ export { actionFinalize } from "./actionFinalize";
export {
actionChangeProjectName,
actionChangeExportBackground,
actionSaveScene,
actionSaveAsScene,
actionSaveToActiveFile,
actionSaveFileToDisk,
actionLoadScene,
} from "./actionExport";
@ -66,6 +67,8 @@ export {
distributeVertically,
} from "./actionDistribute";
export { actionFlipHorizontal, actionFlipVertical } from "./actionFlip";
export {
actionCopy,
actionCut,

View File

@ -7,12 +7,18 @@ import {
ActionResult,
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppState, ExcalidrawProps } from "../types";
import { AppProps, AppState } from "../types";
import { MODES } from "../constants";
import Library from "../data/library";
// This is the <App> component, but for now we don't care about anything but its
// `canvas` state.
type App = { canvas: HTMLCanvasElement | null; props: ExcalidrawProps };
type App = {
canvas: HTMLCanvasElement | null;
focusContainer: () => void;
props: AppProps;
library: Library;
};
export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"];
@ -51,11 +57,15 @@ export class ActionManager implements ActionsManagerInterface {
actions.forEach((action) => this.registerAction(action));
}
handleKeyDown(event: KeyboardEvent) {
handleKeyDown(event: React.KeyboardEvent | KeyboardEvent) {
const canvasActions = this.app.props.UIOptions.canvasActions;
const data = Object.values(this.actions)
.sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0))
.filter(
(action) =>
(action.name in canvasActions
? canvasActions[action.name as keyof typeof canvasActions]
: true) &&
action.keyTest &&
action.keyTest(
event,
@ -102,7 +112,15 @@ export class ActionManager implements ActionsManagerInterface {
// like the user list. We can use this key to extract more
// data from app state. This is an alternative to generic prop hell!
renderAction = (name: ActionName, id?: string) => {
if (this.actions[name] && "PanelComponent" in this.actions[name]) {
const canvasActions = this.app.props.UIOptions.canvasActions;
if (
this.actions[name] &&
"PanelComponent" in this.actions[name] &&
(name in canvasActions
? canvasActions[name as keyof typeof canvasActions]
: true)
) {
const action = this.actions[name];
const PanelComponent = action.PanelComponent!;
const updateData = (formState?: any) => {

View File

@ -23,7 +23,9 @@ export type ShortcutName =
| "zenMode"
| "stats"
| "addToLibrary"
| "viewMode";
| "viewMode"
| "flipHorizontal"
| "flipVertical";
const shortcutMap: Record<ShortcutName, string[]> = {
cut: [getShortcutKey("CtrlOrCmd+X")],
@ -55,8 +57,10 @@ const shortcutMap: Record<ShortcutName, string[]> = {
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
gridMode: [getShortcutKey("CtrlOrCmd+'")],
zenMode: [getShortcutKey("Alt+Z")],
stats: [],
stats: [getShortcutKey("Alt+/")],
addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
};

View File

@ -1,22 +1,32 @@
import React from "react";
import { ExcalidrawElement } from "../element/types";
import { AppState, ExcalidrawProps } from "../types";
import Library from "../data/library";
/** if false, the action should be prevented */
export type ActionResult =
| {
elements?: readonly ExcalidrawElement[] | null;
appState?: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
appState?: MarkOptional<
AppState,
"offsetTop" | "offsetLeft" | "width" | "height"
> | null;
commitToHistory: boolean;
syncHistory?: boolean;
}
| false;
type AppAPI = {
canvas: HTMLCanvasElement | null;
focusContainer(): void;
library: Library;
};
type ActionFn = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
formData: any,
app: { canvas: HTMLCanvasElement | null },
app: AppAPI,
) => ActionResult | Promise<ActionResult>;
export type UpdaterFn = (res: ActionResult) => void;
@ -42,6 +52,7 @@ export type ActionName =
| "changeBackgroundColor"
| "changeFillStyle"
| "changeStrokeWidth"
| "changeStrokeShape"
| "changeSloppiness"
| "changeStrokeStyle"
| "changeArrowhead"
@ -55,9 +66,9 @@ export type ActionName =
| "changeProjectName"
| "changeExportBackground"
| "changeExportEmbedScene"
| "changeShouldAddWatermark"
| "saveScene"
| "saveAsScene"
| "changeExportScale"
| "saveToActiveFile"
| "saveFileToDisk"
| "loadScene"
| "duplicateSelection"
| "deleteSelectedElements"
@ -85,8 +96,11 @@ export type ActionName =
| "alignHorizontallyCentered"
| "distributeHorizontally"
| "distributeVertically"
| "flipHorizontal"
| "flipVertical"
| "viewMode"
| "exportWithDarkMode";
| "exportWithDarkMode"
| "toggleTheme";
export interface Action {
name: ActionName;
@ -100,7 +114,7 @@ export interface Action {
perform: ActionFn;
keyPriority?: number;
keyTest?: (
event: KeyboardEvent,
event: React.KeyboardEvent | KeyboardEvent,
appState: AppState,
elements: readonly ExcalidrawElement[],
) => boolean;
@ -115,6 +129,7 @@ export interface Action {
export interface ActionsManagerInterface {
actions: Record<ActionName, Action>;
registerAction: (action: Action) => void;
handleKeyDown: (event: KeyboardEvent) => boolean;
handleKeyDown: (event: React.KeyboardEvent | KeyboardEvent) => boolean;
renderAction: (name: ActionName) => React.ReactElement | null;
executeAction: (action: Action) => void;
}

View File

@ -3,14 +3,19 @@ import {
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
EXPORT_SCALES,
} from "./constants";
import { t } from "./i18n";
import { AppState, NormalizedZoomValue } from "./types";
import { getDateTime } from "./utils";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
? devicePixelRatio
: 1;
export const getDefaultAppState = (): Omit<
AppState,
"offsetTop" | "offsetLeft"
"offsetTop" | "offsetLeft" | "width" | "height"
> => {
return {
theme: "light",
@ -39,11 +44,11 @@ export const getDefaultAppState = (): Omit<
elementType: "selection",
errorMessage: null,
exportBackground: true,
exportScale: defaultExportScale,
exportEmbedScene: false,
exportWithDarkMode: false,
fileHandle: null,
gridSize: null,
height: window.innerHeight,
isBindingEnabled: true,
isLibraryOpen: false,
isLoading: false,
@ -53,6 +58,7 @@ export const getDefaultAppState = (): Omit<
multiElement: null,
name: `${t("labels.untitled")}-${getDateTime()}`,
openMenu: null,
openPopup: null,
pasteDialog: { shown: false, data: null },
previousSelectedElementIds: {},
resizingElement: null,
@ -62,7 +68,6 @@ export const getDefaultAppState = (): Omit<
selectedElementIds: {},
selectedGroupIds: {},
selectionElement: null,
shouldAddWatermark: false,
shouldCacheIgnoreZoom: false,
showHelpDialog: false,
showStats: false,
@ -70,7 +75,6 @@ export const getDefaultAppState = (): Omit<
suggestedBindings: [],
toastMessage: null,
viewBackgroundColor: oc.white,
width: window.innerWidth,
zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
viewModeEnabled: false,
@ -119,6 +123,7 @@ const APP_STATE_STORAGE_CONF = (<
errorMessage: { browser: false, export: false },
exportBackground: { browser: true, export: false },
exportEmbedScene: { browser: true, export: false },
exportScale: { browser: true, export: false },
exportWithDarkMode: { browser: true, export: false },
fileHandle: { browser: false, export: false },
gridSize: { browser: true, export: true },
@ -134,6 +139,7 @@ const APP_STATE_STORAGE_CONF = (<
offsetLeft: { browser: false, export: false },
offsetTop: { browser: false, export: false },
openMenu: { browser: true, export: false },
openPopup: { browser: false, export: false },
pasteDialog: { browser: false, export: false },
previousSelectedElementIds: { browser: true, export: false },
resizingElement: { browser: false, export: false },
@ -143,7 +149,6 @@ const APP_STATE_STORAGE_CONF = (<
selectedElementIds: { browser: true, export: false },
selectedGroupIds: { browser: true, export: false },
selectionElement: { browser: false, export: false },
shouldAddWatermark: { browser: true, export: false },
shouldCacheIgnoreZoom: { browser: true, export: false },
showHelpDialog: { browser: false, export: false },
showStats: { browser: true, export: false },

View File

@ -6,7 +6,6 @@ import { getSelectedElements } from "./scene";
import { AppState } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { canvasToBlob } from "./data/blob";
import { EXPORT_DATA_TYPES } from "./constants";
type ElementsClipboard = {
@ -14,6 +13,13 @@ type ElementsClipboard = {
elements: ExcalidrawElement[];
};
export interface ClipboardData {
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[];
text?: string;
errorMessage?: string;
}
let CLIPBOARD = "";
let PREFER_APP_CLIPBOARD = false;
@ -110,12 +116,7 @@ const getSystemClipboard = async (
*/
export const parseClipboard = async (
event: ClipboardEvent | null,
): Promise<{
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[];
text?: string;
errorMessage?: string;
}> => {
): Promise<ClipboardData> => {
const systemClipboard = await getSystemClipboard(event);
// if system clipboard empty, couldn't be resolved, or contains previously
@ -150,8 +151,7 @@ export const parseClipboard = async (
}
};
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
const blob = await canvasToBlob(canvas);
export const copyBlobToClipboardAsPng = async (blob: Blob) => {
await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blob }),
]);

View File

@ -3,13 +3,14 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import {
canChangeSharpness,
canHaveArrowheads,
getTargetElements,
hasBackground,
hasStroke,
hasStrokeStyle,
hasStrokeWidth,
hasText,
} from "../scene";
import { SHAPES } from "../shapes";
@ -53,10 +54,17 @@ export const SelectedShapeActions = ({
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
{showFillIcons && renderAction("changeFillStyle")}
{(hasStroke(elementType) ||
targetElements.some((element) => hasStroke(element.type))) && (
{(hasStrokeWidth(elementType) ||
targetElements.some((element) => hasStrokeWidth(element.type))) &&
renderAction("changeStrokeWidth")}
{(elementType === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")}
{(hasStrokeStyle(elementType) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && (
<>
{renderAction("changeStrokeWidth")}
{renderAction("changeStrokeStyle")}
{renderAction("changeSloppiness")}
</>
@ -143,23 +151,14 @@ export const SelectedShapeActions = ({
);
};
const LIBRARY_ICON = (
// fa-th-large
<svg viewBox="0 0 512 512">
<path d="M296 32h192c13.255 0 24 10.745 24 24v160c0 13.255-10.745 24-24 24H296c-13.255 0-24-10.745-24-24V56c0-13.255 10.745-24 24-24zm-80 0H24C10.745 32 0 42.745 0 56v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V56c0-13.255-10.745-24-24-24zM0 296v160c0 13.255 10.745 24 24 24h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H24c-13.255 0-24 10.745-24 24zm296 184h192c13.255 0 24-10.745 24-24V296c0-13.255-10.745-24-24-24H296c-13.255 0-24 10.745-24 24v160c0 13.255 10.745 24 24 24z" />
</svg>
);
export const ShapesSwitcher = ({
canvas,
elementType,
setAppState,
isLibraryOpen,
}: {
canvas: HTMLCanvasElement | null;
elementType: ExcalidrawElement["type"];
setAppState: React.Component<any, AppState>["setState"];
isLibraryOpen: boolean;
}) => (
<>
{SHAPES.map(({ value, icon, key }, index) => {
@ -193,19 +192,6 @@ export const ShapesSwitcher = ({
/>
);
})}
<ToolButton
className="Shape ToolIcon_type_button__library"
type="button"
icon={LIBRARY_ICON}
name="editor-library"
keyBindingLabel="9"
aria-keyshortcuts="9"
title={`${capitalizeString(t("toolBar.library"))} — 9`}
aria-label={capitalizeString(t("toolBar.library"))}
onClick={() => {
setAppState({ isLibraryOpen: !isLibraryOpen });
}}
/>
</>
);

View File

@ -0,0 +1,21 @@
.excalidraw {
.ActiveFile {
.ActiveFile__fileName {
display: flex;
align-items: center;
span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 9.3em;
}
svg {
width: 1.15em;
margin-inline-end: 0.3em;
transform: scaleY(0.9);
}
}
}
}

View File

@ -0,0 +1,29 @@
import React from "react";
import Stack from "../components/Stack";
import { ToolButton } from "../components/ToolButton";
import { save, file } from "../components/icons";
import { t } from "../i18n";
import "./ActiveFile.scss";
type ActiveFileProps = {
fileName?: string;
onSave: () => void;
};
export const ActiveFile = ({ fileName, onSave }: ActiveFileProps) => (
<Stack.Row className="ActiveFile" gap={1} align="center">
<span className="ActiveFile__fileName">
{file}
<span>{fileName}</span>
</span>
<ToolButton
type="icon"
icon={save}
title={t("buttons.save")}
aria-label={t("buttons.save")}
onClick={onSave}
data-testid="save-button"
/>
</Stack.Row>
);

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,6 @@
import React from "react";
import { ActionManager } from "../actions/manager";
import { AppState } from "../types";
import { DarkModeToggle } from "./DarkModeToggle";
export const BackgroundPickerAndDarkModeToggle = ({
appState,
@ -16,15 +15,6 @@ export const BackgroundPickerAndDarkModeToggle = ({
}) => (
<div style={{ display: "flex" }}>
{actionManager.renderAction("changeViewBackgroundColor")}
{showThemeBtn && (
<div style={{ marginInlineStart: "0.25rem" }}>
<DarkModeToggle
value={appState.theme}
onChange={(theme) => {
setAppState({ theme });
}}
/>
</div>
)}
{showThemeBtn && actionManager.renderAction("toggleTheme")}
</div>
);

View File

@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx";
export const ButtonIconCycle = <T extends any>({
@ -14,11 +13,11 @@ export const ButtonIconCycle = <T extends any>({
}) => {
const current = options.find((op) => op.value === value);
function cycle() {
const cycle = () => {
const index = options.indexOf(current!);
const next = (index + 1) % options.length;
onChange(options[next].value);
}
};
return (
<label key={group} className={clsx({ active: current!.value !== null })}>

53
src/components/Card.scss Normal file
View File

@ -0,0 +1,53 @@
@import "../css/variables.module";
.excalidraw {
.Card {
display: flex;
flex-direction: column;
align-items: center;
max-width: 290px;
margin: 1em;
text-align: center;
.Card-icon {
font-size: 2.6em;
display: flex;
flex: 0 0 auto;
padding: 1.4rem;
border-radius: 50%;
background: var(--card-color);
color: $oc-white;
svg {
width: 2.8rem;
height: 2.8rem;
}
}
.Card-details {
font-size: 0.96em;
min-height: 90px;
padding: 0 1em;
margin-bottom: auto;
}
& .Card-button.ToolIcon_type_button {
height: 2.5rem;
margin-top: 1em;
margin-bottom: 0.3em;
background-color: var(--card-color);
&:hover {
background-color: var(--card-color-darker);
}
&:active {
background-color: var(--card-color-darkest);
}
.ToolIcon__label {
color: $oc-white;
}
}
}
}

20
src/components/Card.tsx Normal file
View File

@ -0,0 +1,20 @@
import OpenColor from "open-color";
import "./Card.scss";
export const Card: React.FC<{
color: keyof OpenColor;
}> = ({ children, color }) => {
return (
<div
className="Card"
style={{
["--card-color" as any]: OpenColor[color][7],
["--card-color-darker" as any]: OpenColor[color][8],
["--card-color-darkest" as any]: OpenColor[color][9],
}}
>
{children}
</div>
);
};

View File

@ -0,0 +1,89 @@
@import "../css/variables.module";
.excalidraw {
.Checkbox {
margin: 4px 0.3em;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
&:hover:not(.is-checked) .Checkbox-box:not(:focus) {
box-shadow: 0 0 0 2px #{$oc-blue-4};
}
&:hover:not(.is-checked) .Checkbox-box:not(:focus) {
svg {
display: block;
opacity: 0.3;
}
}
&:active {
.Checkbox-box {
box-shadow: 0 0 2px 1px inset #{$oc-blue-7} !important;
}
}
&:hover {
.Checkbox-box {
background-color: fade-out($oc-blue-1, 0.8);
}
}
&.is-checked {
.Checkbox-box {
background-color: #{$oc-blue-1};
svg {
display: block;
}
}
&:hover .Checkbox-box {
background-color: #{$oc-blue-2};
}
}
.Checkbox-box {
width: 22px;
height: 22px;
padding: 0;
flex: 0 0 auto;
margin: 0 1em;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 0 2px #{$oc-blue-7};
background-color: transparent;
border-radius: 4px;
color: #{$oc-blue-7};
&:focus {
box-shadow: 0 0 0 3px #{$oc-blue-7};
}
svg {
display: none;
width: 16px;
height: 16px;
stroke-width: 3px;
}
}
.Checkbox-label {
display: flex;
align-items: center;
}
.Tooltip-icon {
width: 1em;
height: 1em;
}
}
}

View File

@ -0,0 +1,27 @@
import React from "react";
import clsx from "clsx";
import { checkIcon } from "./icons";
import "./CheckboxItem.scss";
export const CheckboxItem: React.FC<{
checked: boolean;
onChange: (checked: boolean) => void;
}> = ({ children, checked, onChange }) => {
return (
<div
className={clsx("Checkbox", { "is-checked": checked })}
onClick={(event) => {
onChange(!checked);
((event.currentTarget as HTMLDivElement).querySelector(
".Checkbox-box",
) as HTMLButtonElement).focus();
}}
>
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>
{checkIcon}
</button>
<div className="Checkbox-label">{children}</div>
</div>
);
};

View File

@ -2,7 +2,7 @@ import React from "react";
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import { users } from "./icons";
import "./CollabButton.scss";

View File

@ -160,7 +160,7 @@
}
.color-picker-input {
width: 12ch; /* length of `transparent` + 1 */
width: 11ch; /* length of `transparent` */
margin: 0;
font-size: 1rem;
background-color: var(--input-bg-color);
@ -218,7 +218,7 @@
left: 2px;
}
@media #{$is-mobile-query} {
@include isMobile {
display: none;
}
}

View File

@ -115,6 +115,7 @@ const Picker = ({
onClose();
}
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
};
return (
@ -237,13 +238,16 @@ export const ColorPicker = ({
color,
onChange,
label,
isActive,
setActive,
}: {
type: "canvasBackground" | "elementBackground" | "elementStroke";
color: string | null;
onChange: (color: string) => void;
label: string;
isActive: boolean;
setActive: (active: boolean) => void;
}) => {
const [isActive, setActive] = React.useState(false);
const pickerButton = React.useRef<HTMLButtonElement>(null);
return (

View File

@ -76,7 +76,7 @@
z-index: 1;
}
@media #{$is-mobile-query} {
@include isMobile {
.context-menu-option {
display: block;

View File

@ -32,67 +32,63 @@ const ContextMenu = ({
actionManager,
appState,
}: ContextMenuProps) => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
?.classList.contains("theme--dark");
return (
<div
className={clsx("excalidraw", {
"theme--dark theme--dark-background-none": isDarkTheme,
})}
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
>
<Popover
onCloseRequest={onCloseRequest}
top={top}
left={left}
fitInViewport={true}
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
<ul
className="context-menu"
onContextMenu={(event) => event.preventDefault()}
>
{options.map((option, idx) => {
if (option === "separator") {
return <hr key={idx} className="context-menu-option-separator" />;
}
{options.map((option, idx) => {
if (option === "separator") {
return <hr key={idx} className="context-menu-option-separator" />;
}
const actionName = option.name;
const label = option.contextItemLabel
? t(option.contextItemLabel)
: "";
return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button
className={clsx("context-menu-option", {
dangerous: actionName === "deleteSelectedElements",
checkmark: option.checked?.(appState),
})}
onClick={() => actionManager.executeAction(option)}
>
<div className="context-menu-option__label">{label}</div>
<kbd className="context-menu-option__shortcut">
{actionName
? getShortcutFromShortcutName(actionName as ShortcutName)
: ""}
</kbd>
</button>
</li>
);
})}
</ul>
</Popover>
</div>
const actionName = option.name;
const label = option.contextItemLabel
? t(option.contextItemLabel)
: "";
return (
<li key={idx} data-testid={actionName} onClick={onCloseRequest}>
<button
className={clsx("context-menu-option", {
dangerous: actionName === "deleteSelectedElements",
checkmark: option.checked?.(appState),
})}
onClick={() => actionManager.executeAction(option)}
>
<div className="context-menu-option__label">{label}</div>
<kbd className="context-menu-option__shortcut">
{actionName
? getShortcutFromShortcutName(actionName as ShortcutName)
: ""}
</kbd>
</button>
</li>
);
})}
</ul>
</Popover>
);
};
let contextMenuNode: HTMLDivElement;
const getContextMenuNode = (): HTMLDivElement => {
const contextMenuNodeByContainer = new WeakMap<HTMLElement, HTMLDivElement>();
const getContextMenuNode = (container: HTMLElement): HTMLDivElement => {
let contextMenuNode = contextMenuNodeByContainer.get(container);
if (contextMenuNode) {
return contextMenuNode;
}
const div = document.createElement("div");
document.body.appendChild(div);
return (contextMenuNode = div);
contextMenuNode = document.createElement("div");
container
.querySelector(".excalidraw-contextMenuContainer")!
.appendChild(contextMenuNode);
contextMenuNodeByContainer.set(container, contextMenuNode);
return contextMenuNode;
};
type ContextMenuParams = {
@ -101,10 +97,16 @@ type ContextMenuParams = {
left: ContextMenuProps["left"];
actionManager: ContextMenuProps["actionManager"];
appState: Readonly<AppState>;
container: HTMLElement;
};
const handleClose = () => {
unmountComponentAtNode(getContextMenuNode());
const handleClose = (container: HTMLElement) => {
const contextMenuNode = contextMenuNodeByContainer.get(container);
if (contextMenuNode) {
unmountComponentAtNode(contextMenuNode);
contextMenuNode.remove();
contextMenuNodeByContainer.delete(container);
}
};
export default {
@ -121,11 +123,11 @@ export default {
top={params.top}
left={params.left}
options={options}
onCloseRequest={handleClose}
onCloseRequest={() => handleClose(params.container)}
actionManager={params.actionManager}
appState={params.appState}
/>,
getContextMenuNode(),
getContextMenuNode(params.container),
);
}
},

View File

@ -2,6 +2,7 @@ import "./ToolIcon.scss";
import React from "react";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
export type Appearence = "light" | "dark";
@ -12,31 +13,19 @@ export const DarkModeToggle = (props: {
onChange: (value: Appearence) => void;
title?: string;
}) => {
const title = props.title
? props.title
: props.value === "dark"
? t("buttons.lightMode")
: t("buttons.darkMode");
const title =
props.title ||
(props.value === "dark" ? t("buttons.lightMode") : t("buttons.darkMode"));
return (
<label
className="ToolIcon ToolIcon_type_floating ToolIcon_size_M"
data-testid="toggle-dark-mode"
<ToolButton
type="icon"
icon={props.value === "light" ? ICONS.MOON : ICONS.SUN}
title={title}
>
<input
className="ToolIcon_type_checkbox ToolIcon_toggle_opaque"
type="checkbox"
onChange={(event) =>
props.onChange(event.target.checked ? "dark" : "light")
}
checked={props.value === "dark"}
aria-label={title}
/>
<div className="ToolIcon__icon">
{props.value === "light" ? ICONS.MOON : ICONS.SUN}
</div>
</label>
aria-label={title}
onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
data-testid="toggle-dark-mode"
/>
);
};

View File

@ -31,7 +31,7 @@
padding: 0 16px 16px;
}
@media #{$is-mobile-query} {
@include isMobile {
.Dialog {
--metric: calc(var(--space-factor) * 4);
--inset-left: #{"max(var(--metric), var(--sal))"};

View File

@ -1,13 +1,14 @@
import clsx from "clsx";
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useExcalidrawContainer, useIsMobile } from "../components/App";
import { KEYS } from "../keys";
import "./Dialog.scss";
import { back, close } from "./icons";
import { Island } from "./Island";
import { Modal } from "./Modal";
import { AppState } from "../types";
export const Dialog = (props: {
children: React.ReactNode;
@ -16,8 +17,11 @@ export const Dialog = (props: {
onCloseRequest(): void;
title: React.ReactNode;
autofocus?: boolean;
theme?: AppState["theme"];
}) => {
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
const [lastActiveElement] = useState(document.activeElement);
const { id } = useExcalidrawContainer();
useEffect(() => {
if (!islandNode) {
@ -65,19 +69,25 @@ export const Dialog = (props: {
return focusableElements ? Array.from(focusableElements) : [];
};
const onClose = () => {
(lastActiveElement as HTMLElement).focus();
props.onCloseRequest();
};
return (
<Modal
className={clsx("Dialog", props.className)}
labelledBy="dialog-title"
maxWidth={props.small ? 550 : 800}
onCloseRequest={props.onCloseRequest}
onCloseRequest={onClose}
theme={props.theme}
>
<Island ref={setIslandNode}>
<h2 id="dialog-title" className="Dialog__title">
<h2 id={`${id}-dialog-title`} className="Dialog__title">
<span className="Dialog__titleContent">{props.title}</span>
<button
className="Modal__close"
onClick={props.onCloseRequest}
onClick={onClose}
aria-label={t("buttons.close")}
>
{useIsMobile() ? back : close}

View File

@ -2,6 +2,7 @@ import React, { useState } from "react";
import { t } from "../i18n";
import { Dialog } from "./Dialog";
import { useExcalidrawContainer } from "./App";
export const ErrorDialog = ({
message,
@ -11,6 +12,7 @@ export const ErrorDialog = ({
onClose?: () => void;
}) => {
const [modalIsShown, setModalIsShown] = useState(!!message);
const { container: excalidrawContainer } = useExcalidrawContainer();
const handleClose = React.useCallback(() => {
setModalIsShown(false);
@ -18,7 +20,9 @@ export const ErrorDialog = ({
if (onClose) {
onClose();
}
}, [onClose]);
// TODO: Fix the A11y issues so this is never needed since we should always focus on last active element
excalidrawContainer?.focus();
}, [onClose, excalidrawContainer]);
return (
<>
@ -28,14 +32,7 @@ export const ErrorDialog = ({
onCloseRequest={handleClose}
title={t("errorDialog.title")}
>
<div>
{message.split("\n").map((line) => (
<>
{line}
<br />
</>
))}
</div>
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div>
</Dialog>
)}
</>

View File

@ -28,34 +28,7 @@
justify-content: space-between;
}
.ExportDialog__name {
grid-column: project-name;
margin: auto;
display: flex;
align-items: center;
.TextInput {
height: calc(1rem - 3px);
width: 200px;
overflow: hidden;
text-align: center;
margin-left: 8px;
text-overflow: ellipsis;
&--readonly {
background: none;
border: none;
&:hover {
background: none;
}
width: auto;
max-width: 200px;
padding-left: 2px;
}
}
}
@media #{$is-mobile-query} {
@include isMobile {
.ExportDialog {
display: flex;
flex-direction: column;
@ -84,4 +57,63 @@
overflow-y: auto;
}
}
.ExportDialog--json {
.ExportDialog-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
justify-items: center;
row-gap: 2em;
@media (max-width: 460px) {
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
.Card-details {
min-height: 40px;
}
}
.ProjectName {
width: fit-content;
margin: 1em auto;
align-items: flex-start;
flex-direction: column;
.TextInput {
width: auto;
}
}
.ProjectName-label {
margin: 0.625em 0;
font-weight: bold;
}
}
}
button.ExportDialog-imageExportButton {
width: 5rem;
height: 5rem;
margin: 0 0.2em;
border-radius: 1rem;
background-color: var(--button-color);
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.28),
0 6px 10px 0 rgba(0, 0, 0, 0.14);
font-family: Cascadia;
font-size: 1.8em;
color: $oc-white;
&:hover {
background-color: var(--button-color-darker);
}
&:active {
background-color: var(--button-color-darkest);
box-shadow: none;
}
svg {
width: 0.9em;
}
}
}

View File

@ -1,6 +1,5 @@
.excalidraw {
.FixedSideContainer {
--margin: 0.25rem;
position: absolute;
pointer-events: none;
}
@ -10,9 +9,9 @@
}
.FixedSideContainer_side_top {
left: var(--margin);
top: var(--margin);
right: var(--margin);
left: var(--space-factor);
top: var(--space-factor);
right: var(--space-factor);
z-index: 2;
}
@ -23,16 +22,16 @@
/* TODO: if these are used, make sure to implement RTL support
.FixedSideContainer_side_left {
left: var(--margin);
top: var(--margin);
bottom: var(--margin);
left: var(--space-factor);
top: var(--space-factor);
bottom: var(--space-factor);
z-index: 1;
}
.FixedSideContainer_side_right {
right: var(--margin);
top: var(--margin);
bottom: var(--margin);
right: var(--space-factor);
top: var(--space-factor);
bottom: var(--space-factor);
z-index: 3;
}
*/

View File

@ -153,10 +153,17 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
<Shortcut label={t("toolBar.arrow")} shortcuts={["A", "5"]} />
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
<Shortcut
label={t("toolBar.draw")}
label={t("toolBar.freedraw")}
shortcuts={["Shift+P", "7"]}
/>
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
<Shortcut
label={t("helpDialog.editSelectedShape")}
shortcuts={[
getShortcutKey("Enter"),
t("helpDialog.doubleClick"),
]}
/>
<Shortcut
label={t("helpDialog.textNewLine")}
shortcuts={[
@ -231,6 +238,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.viewMode")}
shortcuts={[getShortcutKey("Alt+R")]}
/>
<Shortcut
label={t("labels.toggleTheme")}
shortcuts={[getShortcutKey("Alt+Shift+D")]}
/>
<Shortcut
label={t("stats.title")}
shortcuts={[getShortcutKey("Alt+/")]}
/>
</ShortcutIsland>
</Column>
<Column>
@ -349,6 +364,22 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.ungroup")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+G")]}
/>
<Shortcut
label={t("labels.flipHorizontal")}
shortcuts={[getShortcutKey("Shift+H")]}
/>
<Shortcut
label={t("labels.flipVertical")}
shortcuts={[getShortcutKey("Shift+V")]}
/>
<Shortcut
label={t("labels.showStroke")}
shortcuts={[getShortcutKey("S")]}
/>
<Shortcut
label={t("labels.showBackground")}
shortcuts={[getShortcutKey("G")]}
/>
</ShortcutIsland>
</Column>
</Columns>

View File

@ -19,7 +19,7 @@ $wide-viewport-width: 1000px;
color: $oc-gray-6;
font-size: 0.8rem;
@media #{$is-mobile-query} {
@include isMobile {
position: static;
padding-right: 2em;
}

View File

@ -5,7 +5,7 @@ import { getSelectedElements } from "../scene";
import "./HintViewer.scss";
import { AppState } from "../types";
import { isLinearElement } from "../element/typeChecks";
import { isLinearElement, isTextElement } from "../element/typeChecks";
import { getShortcutKey } from "../utils";
interface Hint {
@ -23,7 +23,7 @@ const getHints = ({ appState, elements }: Hint) => {
return t("hints.linearElementMulti");
}
if (elementType === "draw") {
if (elementType === "freedraw") {
return t("hints.freeDraw");
}
@ -57,6 +57,14 @@ const getHints = ({ appState, elements }: Hint) => {
return t("hints.lineEditor_info");
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
if (appState.editingElement && isTextElement(appState.editingElement)) {
return t("hints.text_editing");
}
return null;
};

View File

@ -111,7 +111,7 @@
:root[dir="rtl"] & {
left: 2px;
}
@media #{$is-mobile-query} {
@include isMobile {
display: none;
}
}

View File

@ -88,6 +88,7 @@ function Picker<T>({
onClose();
}
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
};
return (

View File

@ -6,18 +6,19 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "./App";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas, getExportSize } from "../scene/export";
import { exportToCanvas } from "../scene/export";
import { AppState } from "../types";
import { Dialog } from "./Dialog";
import "./ExportDialog.scss";
import { clipboard, exportFile, link } from "./icons";
import { clipboard, exportImage } from "./icons";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
const scales = [1, 2, 3];
const defaultScale = scales.includes(devicePixelRatio) ? devicePixelRatio : 1;
import "./ExportDialog.scss";
import { supported as fsSupported } from "browser-fs-access";
import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING } from "../constants";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
@ -52,15 +53,37 @@ export type ExportCB = (
scale?: number,
) => void;
const ExportModal = ({
const ExportButton: React.FC<{
color: keyof OpenColor;
onClick: () => void;
title: string;
shade?: number;
}> = ({ children, title, onClick, color, shade = 6 }) => {
return (
<button
className="ExportDialog-imageExportButton"
style={{
["--button-color" as any]: OpenColor[color][shade],
["--button-color-darker" as any]: OpenColor[color][shade + 1],
["--button-color-darkest" as any]: OpenColor[color][shade + 2],
}}
title={title}
aria-label={title}
onClick={onClick}
>
{children}
</button>
);
};
const ImageExportModal = ({
elements,
appState,
exportPadding = 10,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
onExportToBackend,
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
@ -69,18 +92,12 @@ const ExportModal = ({
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend?: ExportCB;
onCloseRequest: () => void;
}) => {
const someElementIsSelected = isSomeElementSelected(elements, appState);
const [scale, setScale] = useState(defaultScale);
const [exportSelected, setExportSelected] = useState(someElementIsSelected);
const previewRef = useRef<HTMLDivElement>(null);
const {
exportBackground,
viewBackgroundColor,
shouldAddWatermark,
} = appState;
const { exportBackground, viewBackgroundColor } = appState;
const exportedElements = exportSelected
? getSelectedElements(elements, appState)
@ -100,8 +117,6 @@ const ExportModal = ({
exportBackground,
viewBackgroundColor,
exportPadding,
scale,
shouldAddWatermark,
});
// if converting to blob fails, there's some problem that will
@ -124,8 +139,6 @@ const ExportModal = ({
exportBackground,
exportPadding,
viewBackgroundColor,
scale,
shouldAddWatermark,
]);
return (
@ -133,106 +146,84 @@ const ExportModal = ({
<div className="ExportDialog__preview" ref={previewRef} />
{supportsContextFilters &&
actionManager.renderAction("exportWithDarkMode")}
<Stack.Col gap={2} align="center">
<div className="ExportDialog__actions">
<Stack.Row gap={2}>
<ToolButton
type="button"
label="PNG"
title={t("buttons.exportToPng")}
aria-label={t("buttons.exportToPng")}
onClick={() => onExportToPng(exportedElements, scale)}
/>
<ToolButton
type="button"
label="SVG"
title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements, scale)}
/>
{probablySupportsClipboardBlob && (
<ToolButton
type="button"
icon={clipboard}
title={t("buttons.copyPngToClipboard")}
aria-label={t("buttons.copyPngToClipboard")}
onClick={() => onExportToClipboard(exportedElements, scale)}
/>
)}
{onExportToBackend && (
<ToolButton
type="button"
icon={link}
title={t("buttons.getShareableLink")}
aria-label={t("buttons.getShareableLink")}
onClick={() => onExportToBackend(exportedElements)}
/>
)}
</Stack.Row>
<div className="ExportDialog__name">
{actionManager.renderAction("changeProjectName")}
</div>
<Stack.Row gap={2}>
{scales.map((s) => {
const [width, height] = getExportSize(
exportedElements,
exportPadding,
shouldAddWatermark,
s,
);
const scaleButtonTitle = `${t(
"buttons.scale",
)} ${s}x (${width}x${height})`;
return (
<ToolButton
key={s}
size="s"
type="radio"
icon={`${s}x`}
name="export-canvas-scale"
title={scaleButtonTitle}
aria-label={scaleButtonTitle}
id="export-canvas-scale"
checked={s === scale}
onChange={() => setScale(s)}
/>
);
})}
</Stack.Row>
</div>
{actionManager.renderAction("changeExportBackground")}
{someElementIsSelected && (
<div>
<label>
<input
type="checkbox"
checked={exportSelected}
onChange={(event) =>
setExportSelected(event.currentTarget.checked)
}
/>{" "}
<div style={{ display: "grid", gridTemplateColumns: "1fr" }}>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(190px, 1fr))",
// dunno why this is needed, but when the items wrap it creates
// an overflow
overflow: "hidden",
}}
>
{actionManager.renderAction("changeExportBackground")}
{someElementIsSelected && (
<CheckboxItem
checked={exportSelected}
onChange={(checked) => setExportSelected(checked)}
>
{t("labels.onlySelected")}
</label>
</div>
</CheckboxItem>
)}
{actionManager.renderAction("changeExportEmbedScene")}
</div>
</div>
<div style={{ display: "flex", alignItems: "center", marginTop: ".6em" }}>
<Stack.Row gap={2}>
{actionManager.renderAction("changeExportScale")}
</Stack.Row>
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
</div>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
margin: ".6em 0",
}}
>
{!fsSupported && actionManager.renderAction("changeProjectName")}
</div>
<Stack.Row gap={2} justifyContent="center" style={{ margin: "2em 0" }}>
<ExportButton
color="indigo"
title={t("buttons.exportToPng")}
aria-label={t("buttons.exportToPng")}
onClick={() => onExportToPng(exportedElements)}
>
PNG
</ExportButton>
<ExportButton
color="red"
title={t("buttons.exportToSvg")}
aria-label={t("buttons.exportToSvg")}
onClick={() => onExportToSvg(exportedElements)}
>
SVG
</ExportButton>
{probablySupportsClipboardBlob && (
<ExportButton
title={t("buttons.copyPngToClipboard")}
onClick={() => onExportToClipboard(exportedElements)}
color="gray"
shade={7}
>
{clipboard}
</ExportButton>
)}
{actionManager.renderAction("changeExportEmbedScene")}
{actionManager.renderAction("changeShouldAddWatermark")}
</Stack.Col>
</Stack.Row>
</div>
);
};
export const ExportDialog = ({
export const ImageExportDialog = ({
elements,
appState,
exportPadding = 10,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager,
onExportToPng,
onExportToSvg,
onExportToClipboard,
onExportToBackend,
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
@ -241,14 +232,11 @@ export const ExportDialog = ({
onExportToPng: ExportCB;
onExportToSvg: ExportCB;
onExportToClipboard: ExportCB;
onExportToBackend?: ExportCB;
}) => {
const [modalIsShown, setModalIsShown] = useState(false);
const triggerButton = useRef<HTMLButtonElement>(null);
const handleClose = React.useCallback(() => {
setModalIsShown(false);
triggerButton.current?.focus();
}, []);
return (
@ -257,17 +245,16 @@ export const ExportDialog = ({
onClick={() => {
setModalIsShown(true);
}}
data-testid="export-button"
icon={exportFile}
data-testid="image-export-button"
icon={exportImage}
type="button"
aria-label={t("buttons.export")}
aria-label={t("buttons.exportImage")}
showAriaLabel={useIsMobile()}
title={t("buttons.export")}
ref={triggerButton}
title={t("buttons.exportImage")}
/>
{modalIsShown && (
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
<ExportModal
<Dialog onCloseRequest={handleClose} title={t("buttons.exportImage")}>
<ImageExportModal
elements={elements}
appState={appState}
exportPadding={exportPadding}
@ -275,7 +262,6 @@ export const ExportDialog = ({
onExportToPng={onExportToPng}
onExportToSvg={onExportToSvg}
onExportToClipboard={onExportToClipboard}
onExportToBackend={onExportToBackend}
onCloseRequest={handleClose}
/>
</Dialog>

View File

@ -2,7 +2,6 @@
.Island {
--padding: 0;
background-color: var(--island-bg-color);
backdrop-filter: saturate(100%) blur(10px);
box-shadow: var(--shadow-island);
border-radius: 4px;
padding: calc(var(--padding) * var(--space-factor));

View File

@ -0,0 +1,127 @@
import React, { useState } from "react";
import { ActionsManagerInterface } from "../actions/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "./App";
import { AppState, ExportOpts } from "../types";
import { Dialog } from "./Dialog";
import { exportFile, exportToFileIcon, link } from "./icons";
import { ToolButton } from "./ToolButton";
import { actionSaveFileToDisk } from "../actions/actionExport";
import { Card } from "./Card";
import "./ExportDialog.scss";
import { supported as fsSupported } from "browser-fs-access";
export type ExportCB = (
elements: readonly NonDeletedExcalidrawElement[],
scale?: number,
) => void;
const JSONExportModal = ({
elements,
appState,
actionManager,
exportOpts,
canvas,
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionsManagerInterface;
onCloseRequest: () => void;
exportOpts: ExportOpts;
canvas: HTMLCanvasElement | null;
}) => {
const { onExportToBackend } = exportOpts;
return (
<div className="ExportDialog ExportDialog--json">
<div className="ExportDialog-cards">
{exportOpts.saveFileToDisk && (
<Card color="lime">
<div className="Card-icon">{exportToFileIcon}</div>
<h2>{t("exportDialog.disk_title")}</h2>
<div className="Card-details">
{t("exportDialog.disk_details")}
{!fsSupported && actionManager.renderAction("changeProjectName")}
</div>
<ToolButton
className="Card-button"
type="button"
title={t("exportDialog.disk_button")}
aria-label={t("exportDialog.disk_button")}
showAriaLabel={true}
onClick={() => {
actionManager.executeAction(actionSaveFileToDisk);
}}
/>
</Card>
)}
{onExportToBackend && (
<Card color="pink">
<div className="Card-icon">{link}</div>
<h2>{t("exportDialog.link_title")}</h2>
<div className="Card-details">{t("exportDialog.link_details")}</div>
<ToolButton
className="Card-button"
type="button"
title={t("exportDialog.link_button")}
aria-label={t("exportDialog.link_button")}
showAriaLabel={true}
onClick={() => onExportToBackend(elements, appState, canvas)}
/>
</Card>
)}
{exportOpts.renderCustomUI &&
exportOpts.renderCustomUI(elements, appState, canvas)}
</div>
</div>
);
};
export const JSONExportDialog = ({
elements,
appState,
actionManager,
exportOpts,
canvas,
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionsManagerInterface;
exportOpts: ExportOpts;
canvas: HTMLCanvasElement | null;
}) => {
const [modalIsShown, setModalIsShown] = useState(false);
const handleClose = React.useCallback(() => {
setModalIsShown(false);
}, []);
return (
<>
<ToolButton
onClick={() => {
setModalIsShown(true);
}}
data-testid="json-export-button"
icon={exportFile}
type="button"
aria-label={t("buttons.export")}
showAriaLabel={useIsMobile()}
title={t("buttons.export")}
/>
{modalIsShown && (
<Dialog onCloseRequest={handleClose} title={t("buttons.export")}>
<JSONExportModal
elements={elements}
appState={appState}
actionManager={actionManager}
onCloseRequest={handleClose}
exportOpts={exportOpts}
canvas={canvas}
/>
</Dialog>
)}
</>
);
};

View File

@ -40,50 +40,17 @@
.layer-ui__wrapper {
z-index: var(--zIndex-layerUI);
.encrypted-icon {
position: relative;
margin-inline-start: 15px;
&__top-right {
display: flex;
justify-content: center;
align-items: center;
border-radius: var(--space-factor);
color: $oc-green-9;
svg {
width: 1.2rem;
height: 1.2rem;
}
}
&__github-corner {
top: 0;
:root[dir="ltr"] & {
right: 0;
}
:root[dir="rtl"] & {
left: 0;
}
position: absolute;
width: 40px;
}
&__footer {
position: absolute;
z-index: 100;
bottom: 0;
width: 100%;
:root[dir="ltr"] & {
right: 0;
&-right {
z-index: 100;
display: flex;
}
:root[dir="rtl"] & {
left: 0;
}
width: 190px;
}
.zen-mode-transition {
@ -105,12 +72,16 @@
transform: translate(-999px, 0);
}
:root[dir="ltr"] &.App-menu_bottom--transition-left {
:root[dir="ltr"] &.layer-ui__wrapper__footer-left--transition-left {
transform: translate(-92px, 0);
}
:root[dir="rtl"] &.App-menu_bottom--transition-left {
:root[dir="rtl"] &.layer-ui__wrapper__footer-left--transition-left {
transform: translate(92px, 0);
}
&.layer-ui__wrapper__footer-left--transition-bottom {
transform: translate(0, 92px);
}
}
.disable-zen-mode {
@ -137,5 +108,17 @@
transition-delay: 0.8s;
}
}
.layer-ui__wrapper__footer-center {
pointer-events: none;
& > * {
pointer-events: all;
}
}
.layer-ui__wrapper__footer-left,
.layer-ui__wrapper__footer-right,
.disable-zen-mode--visible {
pointer-events: all;
}
}
}

View File

@ -10,29 +10,33 @@ import { ActionManager } from "../actions/manager";
import { CLASSES } from "../constants";
import { exportCanvas } from "../data";
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
import { Library } from "../data/library";
import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types";
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
import {
AppProps,
AppState,
ExcalidrawProps,
LibraryItem,
LibraryItems,
} from "../types";
import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import CollabButton from "./CollabButton";
import { ErrorDialog } from "./ErrorDialog";
import { ExportCB, ExportDialog } from "./ExportDialog";
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
import { FixedSideContainer } from "./FixedSideContainer";
import { GitHubCorner } from "./GitHubCorner";
import { HintViewer } from "./HintViewer";
import { exportFile, load, shield, trash } from "./icons";
import { exportFile, load, trash } from "./icons";
import { Island } from "./Island";
import "./LayerUI.scss";
import { LibraryUnit } from "./LibraryUnit";
import { LoadingMessage } from "./LoadingMessage";
import { LockIcon } from "./LockIcon";
import { LockButton } from "./LockButton";
import { MobileMenu } from "./MobileMenu";
import { PasteChartDialog } from "./PasteChartDialog";
import { Section } from "./Section";
@ -41,6 +45,9 @@ import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import { UserList } from "./UserList";
import Library from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton";
interface LayerUIProps {
actionManager: ActionManager;
@ -57,14 +64,14 @@ interface LayerUIProps {
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
onExportToBackend?: (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
canvas: HTMLCanvasElement | null,
) => void;
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
renderTopRightUI?: (isMobile: boolean, appState: AppState) => JSX.Element;
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
focusContainer: () => void;
library: Library;
id: string;
}
const useOnClickOutside = (
@ -96,35 +103,44 @@ const useOnClickOutside = (
};
const LibraryMenuItems = ({
library,
libraryItems,
onRemoveFromLibrary,
onAddToLibrary,
onInsertShape,
pendingElements,
theme,
setAppState,
setLibraryItems,
libraryReturnUrl,
focusContainer,
library,
id,
}: {
library: LibraryItems;
libraryItems: LibraryItems;
pendingElements: LibraryItem;
onRemoveFromLibrary: (index: number) => void;
onInsertShape: (elements: LibraryItem) => void;
onAddToLibrary: (elements: LibraryItem) => void;
theme: AppState["theme"];
setAppState: React.Component<any, AppState>["setState"];
setLibraryItems: (library: LibraryItems) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}) => {
const isMobile = useIsMobile();
const numCells = library.length + (pendingElements.length > 0 ? 1 : 0);
const numCells = libraryItems.length + (pendingElements.length > 0 ? 1 : 0);
const CELLS_PER_ROW = isMobile ? 4 : 6;
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
const rows = [];
let addedPendingElements = false;
const referrer = libraryReturnUrl || window.location.origin;
const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
rows.push(
<div className="layer-ui__library-header">
<div className="layer-ui__library-header" key="library-header">
<ToolButton
key="import"
type="button"
@ -132,11 +148,11 @@ const LibraryMenuItems = ({
aria-label={t("buttons.load")}
icon={load}
onClick={() => {
importLibraryFromJSON()
importLibraryFromJSON(library)
.then(() => {
// Maybe we should close and open the menu so that the items get updated.
// But for now we just close the menu.
// Close and then open to get the libraries updated
setAppState({ isLibraryOpen: false });
setAppState({ isLibraryOpen: true });
})
.catch(muteFSAbortError)
.catch((error) => {
@ -144,7 +160,7 @@ const LibraryMenuItems = ({
});
}}
/>
{!!library.length && (
{!!libraryItems.length && (
<>
<ToolButton
key="export"
@ -153,7 +169,7 @@ const LibraryMenuItems = ({
aria-label={t("buttons.export")}
icon={exportFile}
onClick={() => {
saveLibraryAsJSON()
saveLibraryAsJSON(library)
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
@ -168,8 +184,9 @@ const LibraryMenuItems = ({
icon={trash}
onClick={() => {
if (window.confirm(t("alerts.resetLibrary"))) {
Library.resetLibrary();
library.resetLibrary();
setLibraryItems([]);
focusContainer();
}
}}
/>
@ -178,7 +195,7 @@ const LibraryMenuItems = ({
<a
href={`https://libraries.excalidraw.com?target=${
window.name || "_blank"
}&referrer=${referrer}`}
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
@ -193,13 +210,13 @@ const LibraryMenuItems = ({
const shouldAddPendingElements: boolean =
pendingElements.length > 0 &&
!addedPendingElements &&
y + x >= library.length;
y + x >= libraryItems.length;
addedPendingElements = addedPendingElements || shouldAddPendingElements;
children.push(
<Stack.Col key={x}>
<LibraryUnit
elements={library[y + x]}
elements={libraryItems[y + x]}
pendingElements={
shouldAddPendingElements ? pendingElements : undefined
}
@ -207,7 +224,7 @@ const LibraryMenuItems = ({
onClick={
shouldAddPendingElements
? onAddToLibrary.bind(null, pendingElements)
: onInsertShape.bind(null, library[y + x])
: onInsertShape.bind(null, libraryItems[y + x])
}
/>
</Stack.Col>,
@ -232,15 +249,23 @@ const LibraryMenu = ({
onInsertShape,
pendingElements,
onAddToLibrary,
theme,
setAppState,
libraryReturnUrl,
focusContainer,
library,
id,
}: {
pendingElements: LibraryItem;
onClickOutside: (event: MouseEvent) => void;
onInsertShape: (elements: LibraryItem) => void;
onAddToLibrary: () => void;
theme: AppState["theme"];
setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => {
@ -266,7 +291,7 @@ const LibraryMenu = ({
resolve("loading");
}, 100);
}),
Library.loadLibrary().then((items) => {
library.loadLibrary().then((items) => {
setLibraryItems(items);
setIsLoading("ready");
}),
@ -278,24 +303,33 @@ const LibraryMenu = ({
return () => {
clearTimeout(loadingTimerRef.current!);
};
}, []);
}, [library]);
const removeFromLibrary = useCallback(async (indexToRemove) => {
const items = await Library.loadLibrary();
const nextItems = items.filter((_, index) => index !== indexToRemove);
Library.saveLibrary(nextItems);
setLibraryItems(nextItems);
}, []);
const removeFromLibrary = useCallback(
async (indexToRemove) => {
const items = await library.loadLibrary();
const nextItems = items.filter((_, index) => index !== indexToRemove);
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setLibraryItems(nextItems);
},
[library, setAppState],
);
const addToLibrary = useCallback(
async (elements: LibraryItem) => {
const items = await Library.loadLibrary();
const items = await library.loadLibrary();
const nextItems = [...items, elements];
onAddToLibrary();
Library.saveLibrary(nextItems);
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
setLibraryItems(nextItems);
},
[onAddToLibrary],
[onAddToLibrary, library, setAppState],
);
return loadingState === "preloading" ? null : (
@ -306,7 +340,7 @@ const LibraryMenu = ({
</div>
) : (
<LibraryMenuItems
library={libraryItems}
libraryItems={libraryItems}
onRemoveFromLibrary={removeFromLibrary}
onAddToLibrary={addToLibrary}
onInsertShape={onInsertShape}
@ -314,6 +348,10 @@ const LibraryMenu = ({
setAppState={setAppState}
setLibraryItems={setLibraryItems}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
theme={theme}
id={id}
/>
)}
</Island>
@ -334,69 +372,69 @@ const LayerUI = ({
showThemeBtn,
toggleZenMode,
isCollaborating,
onExportToBackend,
renderTopRightUI,
renderCustomFooter,
viewModeEnabled,
libraryReturnUrl,
UIOptions,
focusContainer,
library,
id,
}: LayerUIProps) => {
const isMobile = useIsMobile();
const renderEncryptedIcon = () => (
<a
className={clsx("encrypted-icon tooltip zen-mode-visibility", {
"zen-mode-visibility--hidden": zenModeEnabled,
})}
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
>
<Tooltip label={t("encrypted.tooltip")} position="above" long={true}>
{shield}
</Tooltip>
</a>
);
const renderJSONExportDialog = () => {
if (!UIOptions.canvasActions.export) {
return null;
}
return (
<JSONExportDialog
elements={elements}
appState={appState}
actionManager={actionManager}
exportOpts={UIOptions.canvasActions.export}
canvas={canvas}
/>
);
};
const renderImageExportDialog = () => {
if (!UIOptions.canvasActions.saveAsImage) {
return null;
}
const renderExportDialog = () => {
const createExporter = (type: ExportType): ExportCB => async (
exportedElements,
scale,
) => {
if (canvas) {
await exportCanvas(type, exportedElements, appState, canvas, {
exportBackground: appState.exportBackground,
name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor,
scale,
shouldAddWatermark: appState.shouldAddWatermark,
})
.catch(muteFSAbortError)
.catch((error) => {
console.error(error);
setAppState({ errorMessage: error.message });
});
}
await exportCanvas(type, exportedElements, appState, {
exportBackground: appState.exportBackground,
name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor,
})
.catch(muteFSAbortError)
.catch((error) => {
console.error(error);
setAppState({ errorMessage: error.message });
});
};
return (
<ExportDialog
<ImageExportDialog
elements={elements}
appState={appState}
actionManager={actionManager}
onExportToPng={createExporter("png")}
onExportToSvg={createExporter("svg")}
onExportToClipboard={createExporter("clipboard")}
onExportToBackend={
onExportToBackend
? (elements) => {
onExportToBackend &&
onExportToBackend(elements, appState, canvas);
}
: undefined
}
/>
);
};
const Separator = () => {
return <div style={{ width: ".625em" }} />;
};
const renderViewModeCanvasActions = () => {
return (
<Section
@ -410,9 +448,8 @@ const LayerUI = ({
<Island padding={2} style={{ zIndex: 1 }}>
<Stack.Col gap={4}>
<Stack.Row gap={1} justifyContent="space-between">
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{renderExportDialog()}
{renderJSONExportDialog()}
{renderImageExportDialog()}
</Stack.Row>
</Stack.Col>
</Island>
@ -431,11 +468,12 @@ const LayerUI = ({
<Island padding={2} style={{ zIndex: 1 }}>
<Stack.Col gap={4}>
<Stack.Row gap={1} justifyContent="space-between">
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{renderExportDialog()}
{actionManager.renderAction("clearCanvas")}
<Separator />
{actionManager.renderAction("loadScene")}
{renderJSONExportDialog()}
{renderImageExportDialog()}
<Separator />
{onCollabButtonClick && (
<CollabButton
isCollaborating={isCollaborating}
@ -450,6 +488,9 @@ const LayerUI = ({
setAppState={setAppState}
showThemeBtn={showThemeBtn}
/>
{appState.fileHandle && (
<>{actionManager.renderAction("saveToActiveFile")}</>
)}
</Stack.Col>
</Island>
</Section>
@ -468,7 +509,8 @@ const LayerUI = ({
style={{
// we want to make sure this doesn't overflow so substracting 200
// which is approximately height of zoom footer and top left menu items with some buffer
maxHeight: `${appState.height - 200}px`,
// if active file name is displayed, subtracting 248 to account for its height
maxHeight: `${appState.height - (appState.fileHandle ? 248 : 200)}px`,
}}
>
<SelectedShapeActions
@ -503,6 +545,10 @@ const LayerUI = ({
onAddToLibrary={deselectItems}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
theme={appState.theme}
id={id}
/>
) : null;
@ -529,6 +575,12 @@ const LayerUI = ({
{(heading) => (
<Stack.Col gap={4} align="start">
<Stack.Row gap={1}>
<LockButton
zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
<Island
padding={1}
className={clsx({ "zen-mode": zenModeEnabled })}
@ -540,15 +592,12 @@ const LayerUI = ({
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
/>
</Stack.Row>
</Island>
<LockIcon
zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
<LibraryButton
appState={appState}
setAppState={setAppState}
/>
</Stack.Row>
{libraryMenu}
@ -556,24 +605,30 @@ const LayerUI = ({
)}
</Section>
)}
<UserList
className={clsx("zen-mode-transition", {
"transition-right": zenModeEnabled,
})}
<div
className={clsx(
"layer-ui__wrapper__top-right zen-mode-transition",
{
"transition-right": zenModeEnabled,
},
)}
>
{appState.collaborators.size > 0 &&
Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user.
.filter(([_, client]) => Object.keys(client).length !== 0)
.map(([clientId, client]) => (
<Tooltip
label={client.username || "Unknown user"}
key={clientId}
>
{actionManager.renderAction("goToCollaborator", clientId)}
</Tooltip>
))}
</UserList>
<UserList>
{appState.collaborators.size > 0 &&
Array.from(appState.collaborators)
// Collaborator is either not initialized or is actually the current user.
.filter(([_, client]) => Object.keys(client).length !== 0)
.map(([clientId, client]) => (
<Tooltip
label={client.username || "Unknown user"}
key={clientId}
>
{actionManager.renderAction("goToCollaborator", clientId)}
</Tooltip>
))}
</UserList>
{renderTopRightUI?.(isMobile, appState)}
</div>
</div>
</FixedSideContainer>
);
@ -581,61 +636,61 @@ const LayerUI = ({
const renderBottomAppMenu = () => {
return (
<div
className={clsx("App-menu App-menu_bottom zen-mode-transition", {
"App-menu_bottom--transition-left": zenModeEnabled,
})}
<footer
role="contentinfo"
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
>
<Stack.Col gap={2}>
<Section heading="canvasActions">
<Island padding={1}>
<ZoomActions
renderAction={actionManager.renderAction}
zoom={appState.zoom}
/>
</Island>
{renderEncryptedIcon()}
</Section>
</Stack.Col>
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-left zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
},
)}
>
<Stack.Col gap={2}>
<Section heading="canvasActions">
<Island padding={1}>
<ZoomActions
renderAction={actionManager.renderAction}
zoom={appState.zoom}
/>
</Island>
</Section>
</Stack.Col>
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
},
)}
>
{renderCustomFooter?.(false, appState)}
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-right zen-mode-transition",
{
"transition-right disable-pointerEvents": zenModeEnabled,
},
)}
>
{actionManager.renderAction("toggleShortcuts")}
</div>
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={toggleZenMode}
>
{t("buttons.exitZenMode")}
</button>
</footer>
);
};
const renderGitHubCorner = () => {
return (
<aside
className={clsx(
"layer-ui__wrapper__github-corner zen-mode-transition",
{
"transition-right": zenModeEnabled,
},
)}
>
<GitHubCorner theme={appState.theme} />
</aside>
);
};
const renderFooter = () => (
<footer role="contentinfo" className="layer-ui__wrapper__footer">
<div
className={clsx("zen-mode-transition", {
"transition-right disable-pointerEvents": zenModeEnabled,
})}
>
{renderCustomFooter?.(false)}
{actionManager.renderAction("toggleShortcuts")}
</div>
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={toggleZenMode}
>
{t("buttons.exitZenMode")}
</button>
</footer>
);
const dialogs = (
<>
{appState.isLoading && <LoadingMessage />}
@ -646,7 +701,11 @@ const LayerUI = ({
/>
)}
{appState.showHelpDialog && (
<HelpDialog onClose={() => setAppState({ showHelpDialog: false })} />
<HelpDialog
onClose={() => {
setAppState({ showHelpDialog: false });
}}
/>
)}
{appState.pasteDialog.shown && (
<PasteChartDialog
@ -671,7 +730,8 @@ const LayerUI = ({
elements={elements}
actionManager={actionManager}
libraryMenu={libraryMenu}
exportButton={renderExportDialog()}
renderJSONExportDialog={renderJSONExportDialog}
renderImageExportDialog={renderImageExportDialog}
setAppState={setAppState}
onCollabButtonClick={onCollabButtonClick}
onLockToggle={onLockToggle}
@ -694,8 +754,6 @@ const LayerUI = ({
{dialogs}
{renderFixedSideContainer()}
{renderBottomAppMenu()}
{renderGitHubCorner()}
{renderFooter()}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"

View File

@ -0,0 +1,46 @@
import React from "react";
import clsx from "clsx";
import { t } from "../i18n";
import { AppState } from "../types";
import { capitalizeString } from "../utils";
const LIBRARY_ICON = (
<svg viewBox="0 0 576 512">
<path
fill="currentColor"
d="M542.22 32.05c-54.8 3.11-163.72 14.43-230.96 55.59-4.64 2.84-7.27 7.89-7.27 13.17v363.87c0 11.55 12.63 18.85 23.28 13.49 69.18-34.82 169.23-44.32 218.7-46.92 16.89-.89 30.02-14.43 30.02-30.66V62.75c.01-17.71-15.35-31.74-33.77-30.7zM264.73 87.64C197.5 46.48 88.58 35.17 33.78 32.05 15.36 31.01 0 45.04 0 62.75V400.6c0 16.24 13.13 29.78 30.02 30.66 49.49 2.6 149.59 12.11 218.77 46.95 10.62 5.35 23.21-1.94 23.21-13.46V100.63c0-5.29-2.62-10.14-7.27-12.99z"
></path>
</svg>
);
export const LibraryButton: React.FC<{
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
}> = ({ appState, setAppState }) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon_type_floating ToolIcon__library zen-mode-visibility",
`ToolIcon_size_m`,
{
"zen-mode-visibility--hidden": appState.zenModeEnabled,
},
)}
title={`${capitalizeString(t("toolBar.library"))} — 9`}
style={{ marginInlineStart: "var(--space-factor)" }}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
name="editor-library"
onChange={(event) => {
setAppState({ isLibraryOpen: event.target.checked });
}}
checked={appState.isLibraryOpen}
aria-label={capitalizeString(t("toolBar.library"))}
aria-keyshortcuts="9"
/>
<div className="ToolIcon__icon">{LIBRARY_ICON}</div>
</label>
);
};

View File

@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
import { close } from "../components/icons";
import { MIME_TYPES } from "../constants";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import { exportToSvg } from "../scene/export";
import { LibraryItem } from "../types";
import "./LibraryUnit.scss";
@ -36,22 +36,27 @@ export const LibraryUnit = ({
if (!elementsToRender) {
return;
}
const svg = exportToSvg(elementsToRender, {
exportBackground: false,
viewBackgroundColor: oc.white,
shouldAddWatermark: false,
});
for (const child of ref.current!.children) {
if (child.tagName !== "svg") {
continue;
}
ref.current!.removeChild(child);
}
ref.current!.appendChild(svg);
let svg: SVGSVGElement;
const current = ref.current!;
(async () => {
svg = await exportToSvg(elementsToRender, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
for (const child of ref.current!.children) {
if (child.tagName !== "svg") {
continue;
}
current!.removeChild(child);
}
current!.appendChild(svg);
})();
return () => {
current.removeChild(svg);
if (svg) {
current.removeChild(svg);
}
};
}, [elements, pendingElements]);

View File

@ -8,10 +8,8 @@ type LockIconSize = "s" | "m";
type LockIconProps = {
title?: string;
name?: string;
id?: string;
checked: boolean;
onChange?(): void;
size?: LockIconSize;
zenModeEnabled?: boolean;
};
@ -41,12 +39,12 @@ const ICONS = {
),
};
export const LockIcon = (props: LockIconProps) => {
export const LockButton = (props: LockIconProps) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
`ToolIcon_size_${props.size || DEFAULT_SIZE}`,
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"zen-mode-visibility--hidden": props.zenModeEnabled,
},
@ -57,7 +55,6 @@ export const LockIcon = (props: LockIconProps) => {
className="ToolIcon_type_checkbox"
type="checkbox"
name={props.name}
id={props.id}
onChange={props.onChange}
checked={props.checked}
aria-label={props.title}

View File

@ -13,14 +13,16 @@ import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { Section } from "./Section";
import CollabButton from "./CollabButton";
import { SCROLLBAR_WIDTH, SCROLLBAR_MARGIN } from "../scene/scrollbars";
import { LockIcon } from "./LockIcon";
import { LockButton } from "./LockButton";
import { UserList } from "./UserList";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import { LibraryButton } from "./LibraryButton";
type MobileMenuProps = {
appState: AppState;
actionManager: ActionManager;
exportButton: React.ReactNode;
renderJSONExportDialog: () => React.ReactNode;
renderImageExportDialog: () => React.ReactNode;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
libraryMenu: JSX.Element | null;
@ -28,7 +30,7 @@ type MobileMenuProps = {
onLockToggle: () => void;
canvas: HTMLCanvasElement | null;
isCollaborating: boolean;
renderCustomFooter?: (isMobile: boolean) => JSX.Element;
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
viewModeEnabled: boolean;
showThemeBtn: boolean;
};
@ -38,7 +40,8 @@ export const MobileMenu = ({
elements,
libraryMenu,
actionManager,
exportButton,
renderJSONExportDialog,
renderImageExportDialog,
setAppState,
onCollabButtonClick,
onLockToggle,
@ -62,15 +65,15 @@ export const MobileMenu = ({
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
isLibraryOpen={appState.isLibraryOpen}
/>
</Stack.Row>
</Island>
<LockIcon
<LockButton
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
/>
<LibraryButton appState={appState} setAppState={setAppState} />
</Stack.Row>
{libraryMenu}
</Stack.Col>
@ -107,19 +110,17 @@ export const MobileMenu = ({
if (viewModeEnabled) {
return (
<>
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{exportButton}
{renderJSONExportDialog()}
{renderImageExportDialog()}
</>
);
}
return (
<>
{actionManager.renderAction("loadScene")}
{actionManager.renderAction("saveScene")}
{actionManager.renderAction("saveAsScene")}
{exportButton}
{actionManager.renderAction("clearCanvas")}
{actionManager.renderAction("loadScene")}
{renderJSONExportDialog()}
{renderImageExportDialog()}
{onCollabButtonClick && (
<CollabButton
isCollaborating={isCollaborating}
@ -155,7 +156,7 @@ export const MobileMenu = ({
<div className="panelColumn">
<Stack.Col gap={4}>
{renderCanvasActions()}
{renderCustomFooter?.(true)}
{renderCustomFooter?.(true, appState)}
{appState.collaborators.size > 0 && (
<fieldset>
<legend>{t("labels.collaborators")}</legend>

View File

@ -26,8 +26,7 @@
right: 0;
bottom: 0;
z-index: 1;
background-color: transparentize($oc-black, 0.7);
backdrop-filter: blur(2px);
background-color: transparentize($oc-black, 0.3);
}
.Modal__content {
@ -45,14 +44,17 @@
// for modals, reset blurry bg
background: var(--island-bg-color);
backdrop-filter: none;
border: 1px solid var(--dialog-border-color);
box-shadow: 0 2px 10px transparentize($oc-black, 0.75);
border-radius: 6px;
box-sizing: border-box;
@media #{$is-mobile-query} {
&:focus {
outline: none;
}
@include isMobile {
max-width: 100%;
border: 0;
border-radius: 0;
@ -82,7 +84,7 @@
}
}
@media #{$is-mobile-query} {
@include isMobile {
.Modal {
padding: 0;
}

View File

@ -1,9 +1,11 @@
import "./Modal.scss";
import React, { useState, useLayoutEffect } from "react";
import React, { useState, useLayoutEffect, useRef } from "react";
import { createPortal } from "react-dom";
import clsx from "clsx";
import { KEYS } from "../keys";
import { useExcalidrawContainer, useIsMobile } from "./App";
import { AppState } from "../types";
export const Modal = (props: {
className?: string;
@ -11,8 +13,10 @@ export const Modal = (props: {
maxWidth?: number;
onCloseRequest(): void;
labelledBy: string;
theme?: AppState["theme"];
}) => {
const modalRoot = useBodyRoot();
const { theme = "light" } = props;
const modalRoot = useBodyRoot(theme);
if (!modalRoot) {
return null;
@ -21,6 +25,7 @@ export const Modal = (props: {
const handleKeydown = (event: React.KeyboardEvent) => {
if (event.key === KEYS.ESCAPE) {
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
props.onCloseRequest();
}
};
@ -37,6 +42,7 @@ export const Modal = (props: {
<div
className="Modal__content"
style={{ "--max-width": `${props.maxWidth}px` }}
tabIndex={0}
>
{props.children}
</div>
@ -45,16 +51,29 @@ export const Modal = (props: {
);
};
const useBodyRoot = () => {
const useBodyRoot = (theme: AppState["theme"]) => {
const [div, setDiv] = useState<HTMLDivElement | null>(null);
const isMobile = useIsMobile();
const isMobileRef = useRef(isMobile);
isMobileRef.current = isMobile;
const { container: excalidrawContainer } = useExcalidrawContainer();
useLayoutEffect(() => {
const isDarkTheme = !!document
.querySelector(".excalidraw")
?.classList.contains("theme--dark");
if (div) {
div.classList.toggle("excalidraw--mobile", isMobile);
}
}, [div, isMobile]);
useLayoutEffect(() => {
const isDarkTheme =
!!excalidrawContainer?.classList.contains("theme--dark") ||
theme === "dark";
const div = document.createElement("div");
div.classList.add("excalidraw", "excalidraw-modal-container");
div.classList.toggle("excalidraw--mobile", isMobileRef.current);
if (isDarkTheme) {
div.classList.add("theme--dark");
@ -67,7 +86,7 @@ const useBodyRoot = () => {
return () => {
document.body.removeChild(div);
};
}, []);
}, [excalidrawContainer, theme]);
return div;
};

View File

@ -2,7 +2,7 @@
.excalidraw {
.PasteChartDialog {
@media #{$is-mobile-query} {
@include isMobile {
.Island {
display: flex;
flex-direction: column;
@ -13,7 +13,7 @@
align-items: center;
justify-content: space-around;
flex-wrap: wrap;
@media #{$is-mobile-query} {
@include isMobile {
flex-direction: column;
justify-content: center;
}

View File

@ -34,20 +34,21 @@ const ChartPreviewBtn = (props: {
0,
);
setChartElements(elements);
const svg = exportToSvg(elements, {
exportBackground: false,
viewBackgroundColor: oc.white,
shouldAddWatermark: false,
});
let svg: SVGSVGElement;
const previewNode = previewRef.current!;
previewNode.appendChild(svg);
(async () => {
svg = await exportToSvg(elements, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
if (props.selected) {
(previewNode.parentNode as HTMLDivElement).focus();
}
previewNode.appendChild(svg);
if (props.selected) {
(previewNode.parentNode as HTMLDivElement).focus();
}
})();
return () => {
previewNode.removeChild(svg);

View File

@ -1,6 +1,6 @@
.excalidraw {
.popover {
position: fixed;
position: absolute;
z-index: 10;
}
}

View File

@ -0,0 +1,25 @@
.ProjectName {
margin: auto;
display: flex;
align-items: center;
.TextInput {
height: calc(1rem - 3px);
width: 200px;
overflow: hidden;
text-align: center;
margin-left: 8px;
text-overflow: ellipsis;
&--readonly {
background: none;
border: none;
&:hover {
background: none;
}
width: auto;
max-width: 200px;
padding-left: 2px;
}
}
}

View File

@ -1,6 +1,10 @@
import "./TextInput.scss";
import React, { Component } from "react";
import React, { useState } from "react";
import { focusNearestParent } from "../utils";
import "./ProjectName.scss";
import { useExcalidrawContainer } from "./App";
type Props = {
value: string;
@ -9,21 +13,19 @@ type Props = {
isNameEditable: boolean;
};
type State = {
fileName: string;
};
export class ProjectName extends Component<Props, State> {
state = {
fileName: this.props.value,
};
private handleBlur = (event: any) => {
export const ProjectName = (props: Props) => {
const { id } = useExcalidrawContainer();
const [fileName, setFileName] = useState<string>(props.value);
const handleBlur = (event: any) => {
focusNearestParent(event.target);
const value = event.target.value;
if (value !== this.props.value) {
this.props.onChange(value);
if (value !== props.value) {
props.onChange(value);
}
};
private handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
const handleKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
if (event.key === "Enter") {
event.preventDefault();
if (event.nativeEvent.isComposing || event.keyCode === 229) {
@ -33,29 +35,25 @@ export class ProjectName extends Component<Props, State> {
}
};
public render() {
return (
<>
<label htmlFor="file-name">
{`${this.props.label}${this.props.isNameEditable ? "" : ":"}`}
</label>
{this.props.isNameEditable ? (
<input
className="TextInput"
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
id="file-name"
value={this.state.fileName}
onChange={(event) =>
this.setState({ fileName: event.target.value })
}
/>
) : (
<span className="TextInput TextInput--readonly" id="file-name">
{this.props.value}
</span>
)}
</>
);
}
}
return (
<div className="ProjectName">
<label className="ProjectName-label" htmlFor="filename">
{`${props.label}${props.isNameEditable ? "" : ":"}`}
</label>
{props.isNameEditable ? (
<input
className="TextInput"
onBlur={handleBlur}
onKeyDown={handleKeyDown}
id={`${id}-filename`}
value={fileName}
onChange={(event) => setFileName(event.target.value)}
/>
) : (
<span className="TextInput TextInput--readonly" id={`${id}-filename`}>
{props.value}
</span>
)}
</div>
);
};

View File

@ -1,5 +1,6 @@
import React from "react";
import { t } from "../i18n";
import { useExcalidrawContainer } from "./App";
interface SectionProps extends React.HTMLProps<HTMLElement> {
heading: string;
@ -7,13 +8,14 @@ interface SectionProps extends React.HTMLProps<HTMLElement> {
}
export const Section = ({ heading, children, ...props }: SectionProps) => {
const { id } = useExcalidrawContainer();
const header = (
<h2 className="visually-hidden" id={`${heading}-title`}>
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>
{t(`headings.${heading}`)}
</h2>
);
return (
<section {...props} aria-labelledby={`${heading}-title`}>
<section {...props} aria-labelledby={`${id}-${heading}-title`}>
{typeof children === "function" ? (
children(header)
) : (

View File

@ -6,7 +6,7 @@
top: 64px;
right: 12px;
font-size: 12px;
z-index: 999;
z-index: 10;
h3 {
margin: 0 24px 8px 0;

View File

@ -1,49 +1,22 @@
import React, { useEffect, useState } from "react";
import { copyTextToSystemClipboard } from "../clipboard";
import { DEFAULT_VERSION } from "../constants";
import React from "react";
import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types";
import {
getElementsStorageSize,
getTotalStorageSize,
} from "../excalidraw-app/data/localStorage";
import { t } from "../i18n";
import useIsMobile from "../is-mobile";
import { useIsMobile } from "../components/App";
import { getTargetElements } from "../scene";
import { AppState } from "../types";
import { debounce, getVersion, nFormatter } from "../utils";
import { AppState, ExcalidrawProps } from "../types";
import { close } from "./icons";
import { Island } from "./Island";
import "./Stats.scss";
type StorageSizes = { scene: number; total: number };
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
cb({
scene: getElementsStorageSize(),
total: getTotalStorageSize(),
});
}, 500);
export const Stats = (props: {
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];
}) => {
const isMobile = useIsMobile();
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
scene: 0,
total: 0,
});
useEffect(() => {
getStorageSizes((sizes) => {
setStorageSizes(sizes);
});
});
useEffect(() => () => getStorageSizes.cancel(), []);
const boundingBox = getCommonBounds(props.elements);
const selectedElements = getTargetElements(props.elements, props.appState);
@ -53,17 +26,6 @@ export const Stats = (props: {
return null;
}
const version = getVersion();
let hash;
let timestamp;
if (version !== DEFAULT_VERSION) {
timestamp = version.slice(0, 16).replace("T", " ");
hash = version.slice(21);
} else {
timestamp = t("stats.versionNotAvailable");
}
return (
<div className="Stats">
<Island padding={2}>
@ -88,17 +50,7 @@ export const Stats = (props: {
<td>{t("stats.height")}</td>
<td>{Math.round(boundingBox[3]) - Math.round(boundingBox[1])}</td>
</tr>
<tr>
<th colSpan={2}>{t("stats.storage")}</th>
</tr>
<tr>
<td>{t("stats.scene")}</td>
<td>{nFormatter(storageSizes.scene, 1)}</td>
</tr>
<tr>
<td>{t("stats.total")}</td>
<td>{nFormatter(storageSizes.total, 1)}</td>
</tr>
{selectedElements.length === 1 && (
<tr>
<th colSpan={2}>{t("stats.element")}</th>
@ -120,31 +72,17 @@ export const Stats = (props: {
<>
<tr>
<td>{"x"}</td>
<td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].x
: selectedBoundingBox[0],
)}
</td>
<td>{Math.round(selectedBoundingBox[0])}</td>
</tr>
<tr>
<td>{"y"}</td>
<td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].y
: selectedBoundingBox[1],
)}
</td>
<td>{Math.round(selectedBoundingBox[1])}</td>
</tr>
<tr>
<td>{t("stats.width")}</td>
<td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].width
: selectedBoundingBox[2] - selectedBoundingBox[0],
selectedBoundingBox[2] - selectedBoundingBox[0],
)}
</td>
</tr>
@ -152,9 +90,7 @@ export const Stats = (props: {
<td>{t("stats.height")}</td>
<td>
{Math.round(
selectedElements.length === 1
? selectedElements[0].height
: selectedBoundingBox[3] - selectedBoundingBox[1],
selectedBoundingBox[3] - selectedBoundingBox[1],
)}
</td>
</tr>
@ -170,28 +106,7 @@ export const Stats = (props: {
</td>
</tr>
)}
<tr>
<th colSpan={2}>{t("stats.version")}</th>
</tr>
<tr>
<td
colSpan={2}
style={{ textAlign: "center", cursor: "pointer" }}
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setAppState({
toastMessage: t("toast.copyToClipboard"),
});
} catch {}
}}
title={t("stats.versionCopy")}
>
{timestamp}
<br />
{hash}
</td>
</tr>
{props.renderCustomStats?.(props.elements, props.appState)}
</tbody>
</table>
</Island>

View File

@ -2,6 +2,7 @@ import "./ToolIcon.scss";
import React from "react";
import clsx from "clsx";
import { useExcalidrawContainer } from "./App";
type ToolIconSize = "s" | "m";
@ -29,9 +30,13 @@ type ToolButtonProps =
children?: React.ReactNode;
onClick?(): void;
})
| (ToolButtonBaseProps & {
type: "icon";
children?: React.ReactNode;
onClick?(): void;
})
| (ToolButtonBaseProps & {
type: "radio";
checked: boolean;
onChange?(): void;
});
@ -39,11 +44,12 @@ type ToolButtonProps =
const DEFAULT_SIZE: ToolIconSize = "m";
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
const { id: excalId } = useExcalidrawContainer();
const innerRef = React.useRef(null);
React.useImperativeHandle(ref, () => innerRef.current);
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
if (props.type === "button") {
if (props.type === "button" || props.type === "icon") {
return (
<button
className={clsx(
@ -56,6 +62,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
{
ToolIcon: !props.hidden,
"ToolIcon--selected": props.selected,
"ToolIcon--plain": props.type === "icon",
},
)}
data-testid={props["data-testid"]}
@ -66,14 +73,16 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
onClick={props.onClick}
ref={innerRef}
>
<div className="ToolIcon__icon" aria-hidden="true">
{props.icon || props.label}
{props.keyBindingLabel && (
<span className="ToolIcon__keybinding">
{props.keyBindingLabel}
</span>
)}
</div>
{(props.icon || props.label) && (
<div className="ToolIcon__icon" aria-hidden="true">
{props.icon || props.label}
{props.keyBindingLabel && (
<span className="ToolIcon__keybinding">
{props.keyBindingLabel}
</span>
)}
</div>
)}
{props.showAriaLabel && (
<div className="ToolIcon__label">{props["aria-label"]}</div>
)}
@ -91,7 +100,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
aria-label={props["aria-label"]}
aria-keyshortcuts={props["aria-keyshortcuts"]}
data-testid={props["data-testid"]}
id={props.id}
id={`${excalId}-${props.id}`}
onChange={props.onChange}
checked={props.checked}
ref={innerRef}

View File

@ -8,9 +8,26 @@
position: relative;
font-family: Cascadia;
cursor: pointer;
background-color: var(--button-gray-1);
-webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor);
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 {
background-color: transparent;
.ToolIcon__icon {
width: 2rem;
height: 2rem;
}
}
.ToolIcon__icon {
@ -57,14 +74,6 @@
margin: 0;
font-size: inherit;
&:hover {
background-color: var(--button-gray-1);
}
&:active {
background-color: var(--button-gray-2);
}
&:focus {
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
@ -77,6 +86,14 @@
}
}
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
&--show {
visibility: visible;
}
@ -94,6 +111,9 @@
&:not(.ToolIcon_toggle_opaque):checked + .ToolIcon__icon {
background-color: var(--button-gray-2);
&:active {
background-color: var(--button-gray-3);
}
}
&:focus + .ToolIcon__icon {
@ -121,12 +141,21 @@
}
.ToolIcon__icon {
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
width: 2rem;
height: 2em;
}
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
@ -157,10 +186,9 @@
// move the lock button out of the way on small viewports
// it begins to collide with the GitHub icon before we switch to mobile mode
@media (max-width: 760px) {
.ToolIcon.ToolIcon__lock {
.ToolIcon.ToolIcon_type_floating {
display: inline-block;
position: absolute;
top: 60px;
right: -8px;
margin-left: 0;
@ -185,16 +213,13 @@
position: static;
}
}
}
.ToolIcon.ToolIcon__library {
top: 100px;
}
.TooltipIcon {
width: 0.9em;
height: 0.9em;
margin-left: 5px;
margin-top: 1px;
@media #{$is-mobile-query} {
display: none;
.ToolIcon.ToolIcon__lock {
margin-inline-end: 0;
top: 60px;
}
}

View File

@ -1,58 +1,39 @@
@import "../css/variables.module";
.excalidraw {
.Tooltip {
position: relative;
}
.excalidraw-tooltip {
position: absolute;
z-index: 1000;
.Tooltip__label {
--arrow-size: 4px;
visibility: hidden;
background: $oc-black;
color: $oc-white;
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 10;
font-size: 13px;
line-height: 1.5;
font-weight: 500;
// extra pixel offset for unknown reasons
left: calc(50% + var(--arrow-size) / 2 - 1px);
transform: translateX(-50%);
word-wrap: break-word;
padding: 8px;
border-radius: 6px;
box-sizing: border-box;
pointer-events: none;
word-wrap: break-word;
&::after {
content: "";
border: var(--arrow-size) solid transparent;
position: absolute;
left: calc(50% - var(--arrow-size));
}
background: $oc-black;
&--above {
bottom: calc(100% + var(--arrow-size) + 3px);
line-height: 1.5;
text-align: center;
font-size: 13px;
font-weight: 500;
color: $oc-white;
&::after {
border-top-color: $oc-black;
top: 100%;
}
}
display: none;
&--below {
top: calc(100% + var(--arrow-size) + 3px);
&::after {
border-bottom-color: $oc-black;
bottom: 100%;
}
}
}
.Tooltip:hover .Tooltip__label {
visibility: visible;
}
.Tooltip__label:hover {
visibility: visible;
&.excalidraw-tooltip--visible {
display: block;
}
}
.excalidraw {
.Tooltip-icon {
width: 0.9em;
height: 0.9em;
margin-left: 5px;
margin-top: 1px;
display: flex;
@include isMobile {
display: none;
}
}
}

View File

@ -1,31 +1,92 @@
import "./Tooltip.scss";
import React from "react";
import React, { useEffect } from "react";
const getTooltipDiv = () => {
const existingDiv = document.querySelector<HTMLDivElement>(
".excalidraw-tooltip",
);
if (existingDiv) {
return existingDiv;
}
const div = document.createElement("div");
document.body.appendChild(div);
div.classList.add("excalidraw-tooltip");
return div;
};
const updateTooltip = (
item: HTMLDivElement,
tooltip: HTMLDivElement,
label: string,
long: boolean,
) => {
tooltip.classList.add("excalidraw-tooltip--visible");
tooltip.style.minWidth = long ? "50ch" : "10ch";
tooltip.style.maxWidth = long ? "50ch" : "15ch";
tooltip.textContent = label;
const {
x: itemX,
bottom: itemBottom,
top: itemTop,
width: itemWidth,
} = item.getBoundingClientRect();
const {
width: labelWidth,
height: labelHeight,
} = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const margin = 5;
const left = itemX + itemWidth / 2 - labelWidth / 2;
const offsetLeft =
left + labelWidth >= viewportWidth ? left + labelWidth - viewportWidth : 0;
const top = itemBottom + margin;
const offsetTop =
top + labelHeight >= viewportHeight
? itemBottom - itemTop + labelHeight + margin * 2
: 0;
Object.assign(tooltip.style, {
top: `${top - offsetTop}px`,
left: `${left - offsetLeft}px`,
});
};
type TooltipProps = {
children: React.ReactNode;
label: string;
position?: "above" | "below";
long?: boolean;
};
export const Tooltip = ({
children,
label,
position = "below",
long = false,
}: TooltipProps) => (
<div className="Tooltip">
<span
className={
position === "above"
? "Tooltip__label Tooltip__label--above"
: "Tooltip__label Tooltip__label--below"
export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
useEffect(() => {
return () =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
}, []);
return (
<div
onPointerEnter={(event) =>
updateTooltip(
event.currentTarget as HTMLDivElement,
getTooltipDiv(),
label,
long,
)
}
onPointerLeave={() =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
}
style={{ width: long ? "50ch" : "10ch" }}
>
{label}
</span>
{children}
</div>
);
{children}
</div>
);
};

View File

@ -2,7 +2,8 @@
.UserList {
pointer-events: none;
/*github corner*/
padding: var(--space-factor) 40px var(--space-factor) var(--space-factor);
padding: var(--space-factor) var(--space-factor) var(--space-factor)
var(--space-factor);
display: flex;
flex-wrap: wrap;
justify-content: flex-end;

View File

@ -24,7 +24,10 @@ type Opts = {
mirror?: true;
} & React.SVGProps<SVGSVGElement>;
const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
export const createIcon = (
d: string | React.ReactNode,
opts: number | Opts = 512,
) => {
const { width = 512, height = width, mirror, style } =
typeof opts === "number" ? ({ width: opts } as Opts) : opts;
return (
@ -41,6 +44,14 @@ const createIcon = (d: string | React.ReactNode, opts: number | Opts = 512) => {
);
};
export const checkIcon = createIcon(
<polyline fill="none" stroke="currentColor" points="20 6 9 17 4 12" />,
{
width: 24,
height: 24,
},
);
export const link = createIcon(
"M326.612 185.391c59.747 59.809 58.927 155.698.36 214.59-.11.12-.24.25-.36.37l-67.2 67.2c-59.27 59.27-155.699 59.262-214.96 0-59.27-59.26-59.27-155.7 0-214.96l37.106-37.106c9.84-9.84 26.786-3.3 27.294 10.606.648 17.722 3.826 35.527 9.69 52.721 1.986 5.822.567 12.262-3.783 16.612l-13.087 13.087c-28.026 28.026-28.905 73.66-1.155 101.96 28.024 28.579 74.086 28.749 102.325.51l67.2-67.19c28.191-28.191 28.073-73.757 0-101.83-3.701-3.694-7.429-6.564-10.341-8.569a16.037 16.037 0 0 1-6.947-12.606c-.396-10.567 3.348-21.456 11.698-29.806l21.054-21.055c5.521-5.521 14.182-6.199 20.584-1.731a152.482 152.482 0 0 1 20.522 17.197zM467.547 44.449c-59.261-59.262-155.69-59.27-214.96 0l-67.2 67.2c-.12.12-.25.25-.36.37-58.566 58.892-59.387 154.781.36 214.59a152.454 152.454 0 0 0 20.521 17.196c6.402 4.468 15.064 3.789 20.584-1.731l21.054-21.055c8.35-8.35 12.094-19.239 11.698-29.806a16.037 16.037 0 0 0-6.947-12.606c-2.912-2.005-6.64-4.875-10.341-8.569-28.073-28.073-28.191-73.639 0-101.83l67.2-67.19c28.239-28.239 74.3-28.069 102.325.51 27.75 28.3 26.872 73.934-1.155 101.96l-13.087 13.087c-4.35 4.35-5.769 10.79-3.783 16.612 5.864 17.194 9.042 34.999 9.69 52.721.509 13.906 17.454 20.446 27.294 10.606l37.106-37.106c59.271-59.259 59.271-155.699.001-214.959z",
{ mirror: true },
@ -80,6 +91,19 @@ export const exportFile = createIcon(
{ width: 576, height: 512, mirror: true },
);
export const exportImage = createIcon(
<>
<path d="M571 308l-95.7-96.4c-10.1-10.1-27.4-3-27.4 11.3V288h-64v64h64v65.2c0 14.3 17.3 21.4 27.4 11.3L571 332c6.6-6.6 6.6-17.4 0-24zm-187 44v-64 64z" />
<path d="M384 121.941V128H256V0h6.059c6.362 0 12.471 2.53 16.97 7.029l97.941 97.941a24.01 24.01 0 017.03 16.971zM248 160c-13.2 0-24-10.8-24-24V0H24C10.745 0 0 10.745 0 24v464c0 13.255 10.745 24 24 24h336c13.255 0 24-10.745 24-24V160H248zm-135.455 16c26.51 0 48 21.49 48 48s-21.49 48-48 48-48-21.49-48-48 21.491-48 48-48zm208 240h-256l.485-48.485L104.545 328c4.686-4.686 11.799-4.201 16.485.485L160.545 368 264.06 264.485c4.686-4.686 12.284-4.686 16.971 0L320.545 304v112z" />
</>,
{ width: 576, height: 512, mirror: true },
);
export const exportToFileIcon = createIcon(
"M216 0h80c13.3 0 24 10.7 24 24v168h87.7c17.8 0 26.7 21.5 14.1 34.1L269.7 378.3c-7.5 7.5-19.8 7.5-27.3 0L90.1 226.1c-12.6-12.6-3.7-34.1 14.1-34.1H192V24c0-13.3 10.7-24 24-24zm296 376v112c0 13.3-10.7 24-24 24H24c-13.3 0-24-10.7-24-24V376c0-13.3 10.7-24 24-24h146.7l49 49c20.1 20.1 52.5 20.1 72.6 0l49-49H488c13.3 0 24 10.7 24 24zm-124 88c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20zm64 0c0-11-9-20-20-20s-20 9-20 20 9 20 20 20 20-9 20-20z",
{ width: 512, height: 512 },
);
export const zoomIn = createIcon(
"M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z",
{ width: 448, height: 512 },
@ -222,14 +246,12 @@ export const SendToBackIcon = React.memo(
d="M18 7.333C18 6.597 17.403 6 16.667 6H7.333C6.597 6 6 6.597 6 7.333v9.334C6 17.403 6.597 18 7.333 18h9.334c.736 0 1.333-.597 1.333-1.333V7.333z"
fill={activeElementColor(theme)}
stroke={activeElementColor(theme)}
strokeLinejoin="round"
strokeWidth="2"
/>
<path
d="M11 3a1 1 0 00-1-1H3a1 1 0 00-1 1v7a1 1 0 001 1h8V3zM22 14a1 1 0 00-1-1h-7a1 1 0 00-1 1v7a1 1 0 001 1h8v-8z"
fill={iconFillColor(theme)}
stroke={iconFillColor(theme)}
strokeLinejoin="round"
strokeWidth="2"
/>
</>,
@ -335,7 +357,6 @@ export const DistributeHorizontallyIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
<path d="M5 5V19Z" fill="black" />
<path
d="M19 5V19M5 5V19"
stroke={iconFillColor(theme)}
@ -353,14 +374,6 @@ export const DistributeHorizontallyIcon = React.memo(
),
);
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
></svg>;
export const DistributeVerticallyIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
@ -464,6 +477,11 @@ export const shield = createIcon(
{ width: 24 },
);
export const file = createIcon(
"M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm32-48h224V288l-23.5-23.5c-4.7-4.7-12.3-4.7-17 0L176 352l-39.5-39.5c-4.7-4.7-12.3-4.7-17 0L80 352v64zm48-240c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z",
{ width: 384, height: 512 },
);
export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<>
@ -479,42 +497,16 @@ export const GroupIcon = React.memo(({ theme }: { theme: "light" | "dark" }) =>
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<rect
x="2.5"
y="2.5"
width="30"
height="30"
<g
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="2.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="147.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="147.5"
y="2.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
>
<rect x="2.5" y="2.5" width="30" height="30" />
<rect x="2.5" y="149.5" width="30" height="30" />
<rect x="147.5" y="149.5" width="30" height="30" />
<rect x="147.5" y="2.5" width="30" height="30" />
</g>
</>,
{ width: 182, height: 182, mirror: true },
),
@ -536,60 +528,18 @@ export const UngroupIcon = React.memo(
stroke={iconFillColor(theme)}
strokeWidth="2"
/>
<rect
x="2.5"
y="2.5"
width="30"
height="30"
<g
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="78.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="147.5"
y="149.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="147.5"
y="78.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="105.5"
y="2.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
<rect
x="2.5"
y="102.5"
width="30"
height="30"
fill={handlerColor(theme)}
stroke={iconFillColor(theme)}
strokeWidth="6"
/>
>
<rect x="2.5" y="2.5" width="30" height="30" />
<rect x="78.5" y="149.5" width="30" height="30" />
<rect x="147.5" y="149.5" width="30" height="30" />
<rect x="147.5" y="78.5" width="30" height="30" />
<rect x="105.5" y="2.5" width="30" height="30" />
<rect x="2.5" y="102.5" width="30" height="30" />
</g>
</>,
{ width: 182, height: 182, mirror: true },
),
@ -631,9 +581,10 @@ export const StrokeWidthIcon = React.memo(
({ theme, strokeWidth }: { theme: "light" | "dark"; strokeWidth: number }) =>
createIcon(
<path
d="M6 10H34"
d="M6 10H32"
stroke={iconFillColor(theme)}
strokeWidth={strokeWidth}
strokeLinecap="round"
fill="none"
/>,
{ width: 40, height: 20 },
@ -648,6 +599,7 @@ export const StrokeStyleSolidIcon = React.memo(
stroke={iconFillColor(theme)}
strokeWidth={2}
fill="none"
strokeLinecap="round"
/>,
{
width: 40,
@ -665,6 +617,7 @@ export const StrokeStyleDashedIcon = React.memo(
strokeWidth={2.5}
strokeDasharray={"10, 8"}
fill="none"
strokeLinecap="round"
/>,
{ width: 40, height: 20 },
),
@ -674,11 +627,12 @@ export const StrokeStyleDottedIcon = React.memo(
({ theme }: { theme: "light" | "dark" }) =>
createIcon(
<path
d="M6 10H34"
d="M6 10H36"
stroke={iconFillColor(theme)}
strokeWidth={2.5}
strokeDasharray={"4, 4"}
strokeDasharray={"2, 4.5"}
fill="none"
strokeLinecap="round"
/>,
{ width: 40, height: 20 },
),
@ -691,6 +645,7 @@ export const SloppinessArchitectIcon = React.memo(
d="M3.00098 16.1691C6.28774 13.9744 19.6399 2.8905 22.7215 3.00082C25.8041 3.11113 19.1158 15.5488 21.4962 16.8309C23.8757 18.1131 34.4155 11.7148 37.0001 10.6919"
stroke={iconFillColor(theme)}
strokeWidth={2}
strokeLinecap="round"
fill="none"
/>,
{ width: 40, height: 20, mirror: true },
@ -704,6 +659,7 @@ export const SloppinessArtistIcon = React.memo(
d="M3 17C6.68158 14.8752 16.1296 9.09849 22.0648 6.54922C28 3.99995 22.2896 13.3209 25 14C27.7104 14.6791 36.3757 9.6471 36.3757 9.6471M6.40706 15C13 11.1918 20.0468 1.51045 23.0234 3.0052C26 4.49995 20.457 12.8659 22.7285 16.4329C25 20 36.3757 13 36.3757 13"
stroke={iconFillColor(theme)}
strokeWidth={2}
strokeLinecap="round"
fill="none"
/>,
{ width: 40, height: 20, mirror: true },
@ -717,6 +673,7 @@ export const SloppinessCartoonistIcon = React.memo(
d="M3 15.6468C6.93692 13.5378 22.5544 2.81528 26.6206 3.00242C30.6877 3.18956 25.6708 15.3346 27.4009 16.7705C29.1309 18.2055 35.4001 12.4762 37 11.6177M3.97143 10.4917C6.61158 9.24563 16.3706 2.61886 19.8104 3.01724C23.2522 3.41472 22.0773 12.2013 24.6181 12.8783C27.1598 13.5536 33.3179 8.04068 35.0571 7.07244"
stroke={iconFillColor(theme)}
strokeWidth={2}
strokeLinecap="round"
fill="none"
/>,
{ width: 40, height: 20, mirror: true },
@ -730,6 +687,7 @@ export const EdgeSharpIcon = React.memo(
d="M10 17L10 5L35 5"
stroke={iconFillColor(theme)}
strokeWidth={2}
strokeLinecap="round"
fill="none"
/>,
{ width: 40, height: 20, mirror: true },
@ -743,6 +701,7 @@ export const EdgeRoundIcon = React.memo(
d="M10 17V15C10 8 13 5 21 5L33.5 5"
stroke={iconFillColor(theme)}
strokeWidth={2}
strokeLinecap="round"
fill="none"
/>,
{ width: 40, height: 20, mirror: true },
@ -902,6 +861,7 @@ export const TextAlignLeftIcon = React.memo(
<path
d="M12.83 352h262.34A12.82 12.82 0 00288 339.17v-38.34A12.82 12.82 0 00275.17 288H12.83A12.82 12.82 0 000 300.83v38.34A12.82 12.82 0 0012.83 352zm0-256h262.34A12.82 12.82 0 00288 83.17V44.83A12.82 12.82 0 00275.17 32H12.83A12.82 12.82 0 000 44.83v38.34A12.82 12.82 0 0012.83 96zM432 160H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm0 256H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16z"
fill={iconFillColor(theme)}
strokeLinecap="round"
/>,
{ width: 448, height: 512 },
),
@ -924,6 +884,7 @@ export const TextAlignRightIcon = React.memo(
<path
d="M16 224h416a16 16 0 0016-16v-32a16 16 0 00-16-16H16a16 16 0 00-16 16v32a16 16 0 0016 16zm416 192H16a16 16 0 00-16 16v32a16 16 0 0016 16h416a16 16 0 0016-16v-32a16 16 0 00-16-16zm3.17-384H172.83A12.82 12.82 0 00160 44.83v38.34A12.82 12.82 0 00172.83 96h262.34A12.82 12.82 0 00448 83.17V44.83A12.82 12.82 0 00435.17 32zm0 256H172.83A12.82 12.82 0 00160 300.83v38.34A12.82 12.82 0 00172.83 352h262.34A12.82 12.82 0 00448 339.17v-38.34A12.82 12.82 0 00435.17 288z"
fill={iconFillColor(theme)}
strokeLinecap="round"
/>,
{ width: 448, height: 512 },
),

View File

@ -1,5 +1,6 @@
import { FontFamily } from "./element/types";
import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types";
import { FontFamilyValues } from "./element/types";
export const APP_NAME = "Excalidraw";
@ -13,6 +14,7 @@ export const CURSOR_TYPE = {
TEXT: "text",
CROSSHAIR: "crosshair",
GRABBING: "grabbing",
GRAB: "grab",
POINTER: "pointer",
MOVE: "move",
AUTO: "",
@ -62,15 +64,15 @@ export const CLASSES = {
// 1-based in case we ever do `if(element.fontFamily)`
export const FONT_FAMILY = {
1: "Virgil",
2: "Helvetica",
3: "Cascadia",
} as const;
Virgil: 1,
Helvetica: 2,
Cascadia: 3,
};
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const DEFAULT_FONT_SIZE = 20;
export const DEFAULT_FONT_FAMILY: FontFamily = 1;
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
export const DEFAULT_TEXT_ALIGN = "left";
export const DEFAULT_VERTICAL_ALIGN = "top";
export const DEFAULT_VERSION = "{version}";
@ -90,6 +92,8 @@ export const EXPORT_DATA_TYPES = {
excalidrawLibrary: "excalidrawlib",
} as const;
export const EXPORT_SOURCE = window.location.origin;
export const STORAGE_KEYS = {
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
} as const;
@ -101,7 +105,6 @@ export const TITLE_TIMEOUT = 10000;
export const TOAST_TIMEOUT = 5000;
export const VERSION_TIMEOUT = 30000;
export const SCROLL_TIMEOUT = 100;
export const ZOOM_STEP = 0.1;
// Report a user inactive after IDLE_THRESHOLD milliseconds
@ -116,3 +119,32 @@ export const MODES = {
};
export const THEME_FILTER = cssVariables.themeFilter;
export const URL_QUERY_KEYS = {
addLibrary: "addLibrary",
} as const;
export const URL_HASH_KEYS = {
addLibrary: "addLibrary",
} as const;
export const DEFAULT_UI_OPTIONS: AppProps["UIOptions"] = {
canvasActions: {
changeViewBackgroundColor: true,
clearCanvas: true,
export: { saveFileToDisk: true },
loadScene: true,
saveToActiveFile: true,
theme: true,
saveAsImage: true,
},
};
export const MQ_MAX_WIDTH_PORTRAIT = 730;
export const MQ_MAX_WIDTH_LANDSCAPE = 1000;
export const MQ_MAX_HEIGHT_LANDSCAPE = 500;
export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
export const EXPORT_SCALES = [1, 2, 3];
export const DEFAULT_EXPORT_PADDING = 10; // px

View File

@ -5,6 +5,7 @@
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap; /* added line */
user-select: none;
}
.LoadingMessage {

View File

@ -16,6 +16,12 @@
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
&:focus {
outline: none;
}
// serves 2 purposes:
// 1. prevent selecting text outside the component when double-clicking or
@ -45,6 +51,13 @@
image-rendering: -moz-crisp-edges; // FF
z-index: var(--zIndex-canvas);
// Remove the main canvas from document flow to avoid resizeObserver
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
}
&__canvas {
position: absolute;
}
&.theme--dark {
@ -320,8 +333,8 @@
.App-menu_bottom {
position: absolute;
bottom: 0;
grid-template-columns: 1fr auto 1fr;
grid-gap: 4px;
grid-template-columns: min-content auto min-content;
grid-gap: 15px;
align-items: flex-start;
cursor: default;
pointer-events: none !important;
@ -346,10 +359,6 @@
}
}
.layer-ui__wrapper:not(.disable-pointerEvents) .App-menu_bottom > * {
pointer-events: all;
}
.App-menu_bottom > *:first-child {
justify-self: flex-start;
}
@ -407,13 +416,11 @@
}
&.dropdown-select--floating {
position: absolute;
margin: 0.5em;
}
}
.dropdown-select__language.dropdown-select--floating {
position: absolute;
bottom: 10px;
:root[dir="ltr"] & {
@ -449,13 +456,12 @@
}
.help-icon {
position: absolute;
cursor: pointer;
fill: $oc-gray-6;
bottom: 14px;
width: 1.5rem;
padding: 0;
margin: 0;
margin-top: 5px;
background: none;
color: var(--icon-fill-color);
@ -464,15 +470,15 @@
}
:root[dir="ltr"] & {
right: 14px;
margin-right: 14px;
}
:root[dir="rtl"] & {
left: 14px;
margin-left: 14px;
}
}
@media #{$is-mobile-query} {
@include isMobile {
aside {
display: none;
}
@ -488,20 +494,6 @@
}
}
.github-corner {
position: absolute;
top: 0;
z-index: 2;
:root[dir="ltr"] & {
right: 0;
}
:root[dir="rtl"] & {
left: 0;
}
}
.zen-mode-visibility {
visibility: visible;
opacity: 1;

View File

@ -14,11 +14,12 @@
--focus-highlight-color: #{$oc-blue-2};
--icon-fill-color: #{$oc-black};
--icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white};
--input-border-color: #{$oc-gray-3};
--input-hover-bg-color: #{$oc-gray-1};
--input-label-color: #{$oc-gray-7};
--island-bg-color: rgba(255, 255, 255, 0.9);
--island-bg-color: rgba(255, 255, 255, 0.96);
--keybinding-color: #{$oc-gray-5};
--link-color: #{$oc-blue-7};
--overlay-bg-color: #{transparentize($oc-white, 0.12)};
@ -56,11 +57,12 @@
--focus-highlight-color: #{$oc-blue-6};
--icon-fill-color: #{$oc-gray-4};
--icon-green-fill-color: #{$oc-green-4};
--default-bg-color: #121212;
--input-bg-color: #121212;
--input-border-color: #2e2e2e;
--input-hover-bg-color: #181818;
--input-label-color: #{$oc-gray-2};
--island-bg-color: #1e1e1e;
--island-bg-color: rgba(30, 30, 30, 0.98);
--keybinding-color: #{$oc-gray-6};
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
--popup-bg-color: #2c2c2c;

View File

@ -1,10 +1,13 @@
@import "open-color/open-color.scss";
// keep up to date with is-mobile.tsx
$is-mobile-query: "(max-width: 600px), (max-height: 500px) and (max-width: 1000px)";
@mixin isMobile() {
@at-root .excalidraw--mobile#{&} {
@content;
}
}
$theme-filter: "invert(93%) hue-rotate(180deg)";
:export {
isMobileQuery: unquote($is-mobile-query);
themeFilter: unquote($theme-filter);
}

View File

@ -1,13 +1,14 @@
import { cleanAppStateForExport } from "../appState";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState } from "../types";
import { isValidExcalidrawData } from "./json";
import { restore } from "./restore";
import { LibraryData } from "./types";
import { ImportedLibraryData } from "./types";
const parseFileContents = async (blob: Blob | File) => {
let contents: string;
@ -83,6 +84,7 @@ export const loadFromBlob = async (
blob: Blob,
/** @see restore.localAppState */
localAppState: AppState | null,
localElements: readonly ExcalidrawElement[] | null,
) => {
const contents = await parseFileContents(blob);
try {
@ -95,13 +97,7 @@ export const loadFromBlob = async (
elements: clearElementsForExport(data.elements || []),
appState: {
theme: localAppState?.theme,
fileHandle:
blob.handle &&
["application/json", MIME_TYPES.excalidraw].includes(
getMimeType(blob),
)
? blob.handle
: null,
fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null,
...cleanAppStateForExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(data.elements || [], localAppState, null)
@ -109,6 +105,7 @@ export const loadFromBlob = async (
},
},
localAppState,
localElements,
);
return result;
@ -120,7 +117,7 @@ export const loadFromBlob = async (
export const loadLibraryFromBlob = async (blob: Blob) => {
const contents = await parseFileContents(blob);
const data: LibraryData = JSON.parse(contents);
const data: ImportedLibraryData = JSON.parse(contents);
if (data.type !== EXPORT_DATA_TYPES.excalidrawLibrary) {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}

View File

@ -1,8 +1,9 @@
import { fileSave } from "browser-fs-access";
import {
copyCanvasToClipboardAsPng,
copyBlobToClipboardAsPng,
copyTextToSystemClipboard,
} from "../clipboard";
import { DEFAULT_EXPORT_PADDING } from "../constants";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { exportToCanvas, exportToSvg } from "../scene/export";
@ -18,42 +19,29 @@ export const exportCanvas = async (
type: ExportType,
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
canvas: HTMLCanvasElement,
{
exportBackground,
exportPadding = 10,
exportPadding = DEFAULT_EXPORT_PADDING,
viewBackgroundColor,
name,
scale = 1,
shouldAddWatermark,
}: {
exportBackground: boolean;
exportPadding?: number;
viewBackgroundColor: string;
name: string;
scale?: number;
shouldAddWatermark: boolean;
},
) => {
if (elements.length === 0) {
throw new Error(t("alerts.cannotExportEmptyCanvas"));
}
if (type === "svg" || type === "clipboard-svg") {
const tempSvg = exportToSvg(elements, {
const tempSvg = await exportToSvg(elements, {
exportBackground,
exportWithDarkMode: appState.exportWithDarkMode,
viewBackgroundColor,
exportPadding,
scale,
shouldAddWatermark,
metadata:
appState.exportEmbedScene && type === "svg"
? await (
await import(/* webpackChunkName: "image" */ "./image")
).encodeSvgMetadata({
text: serializeAsJSON(elements, appState),
})
: undefined,
exportScale: appState.exportScale,
exportEmbedScene: appState.exportEmbedScene && type === "svg",
});
if (type === "svg") {
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), {
@ -62,7 +50,7 @@ export const exportCanvas = async (
});
return;
} else if (type === "clipboard-svg") {
copyTextToSystemClipboard(tempSvg.outerHTML);
await copyTextToSystemClipboard(tempSvg.outerHTML);
return;
}
}
@ -71,15 +59,14 @@ export const exportCanvas = async (
exportBackground,
viewBackgroundColor,
exportPadding,
scale,
shouldAddWatermark,
});
tempCanvas.style.display = "none";
document.body.appendChild(tempCanvas);
let blob = await canvasToBlob(tempCanvas);
tempCanvas.remove();
if (type === "png") {
const fileName = `${name}.png`;
let blob = await canvasToBlob(tempCanvas);
if (appState.exportEmbedScene) {
blob = await (
await import(/* webpackChunkName: "image" */ "./image")
@ -95,7 +82,7 @@ export const exportCanvas = async (
});
} else if (type === "clipboard") {
try {
await copyCanvasToClipboardAsPng(tempCanvas);
await copyBlobToClipboardAsPng(blob);
} catch (error) {
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw error;
@ -103,9 +90,4 @@ export const exportCanvas = async (
throw new Error(t("alerts.couldNotCopyToClipboard"));
}
}
// clean up the DOM
if (tempCanvas !== canvas) {
tempCanvas.remove();
}
};

View File

@ -1,28 +1,32 @@
import { fileOpen, fileSave } from "browser-fs-access";
import { cleanAppStateForExport } from "../appState";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { loadFromBlob } from "./blob";
import { Library } from "./library";
import { ImportedDataState } from "./types";
import {
ExportedDataState,
ImportedDataState,
ExportedLibraryData,
} from "./types";
import Library from "./library";
export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
appState: AppState,
): string =>
JSON.stringify(
{
type: EXPORT_DATA_TYPES.excalidraw,
version: 2,
source: window.location.origin,
elements: clearElementsForExport(elements),
appState: cleanAppStateForExport(appState),
},
null,
2,
);
appState: Partial<AppState>,
): string => {
const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw,
version: 2,
source: EXPORT_SOURCE,
elements: clearElementsForExport(elements),
appState: cleanAppStateForExport(appState),
};
return JSON.stringify(data, null, 2);
};
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
@ -45,7 +49,10 @@ export const saveAsJSON = async (
return { fileHandle };
};
export const loadFromJSON = async (localAppState: AppState) => {
export const loadFromJSON = async (
localAppState: AppState,
localElements: readonly ExcalidrawElement[] | null,
) => {
const blob = await fileOpen({
description: "Excalidraw files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
@ -60,7 +67,7 @@ export const loadFromJSON = async (localAppState: AppState) => {
],
*/
});
return loadFromBlob(blob, localAppState);
return loadFromBlob(blob, localAppState, localElements);
};
export const isValidExcalidrawData = (data?: {
@ -85,17 +92,15 @@ export const isValidLibrary = (json: any) => {
);
};
export const saveLibraryAsJSON = async () => {
const library = await Library.loadLibrary();
const serialized = JSON.stringify(
{
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 1,
library,
},
null,
2,
);
export const saveLibraryAsJSON = async (library: Library) => {
const libraryItems = await library.loadLibrary();
const data: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 1,
source: EXPORT_SOURCE,
library: libraryItems,
};
const serialized = JSON.stringify(data, null, 2);
const fileName = "library.excalidrawlib";
const blob = new Blob([serialized], {
type: MIME_TYPES.excalidrawlib,
@ -107,7 +112,7 @@ export const saveLibraryAsJSON = async () => {
});
};
export const importLibraryFromJSON = async () => {
export const importLibraryFromJSON = async (library: Library) => {
const blob = await fileOpen({
description: "Excalidraw library files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
@ -116,5 +121,5 @@ export const importLibraryFromJSON = async () => {
extensions: [".json", ".excalidrawlib"],
*/
});
Library.importLibrary(blob);
await library.importLibrary(blob);
};

View File

@ -1,20 +1,29 @@
import { loadLibraryFromBlob } from "./blob";
import { LibraryItems, LibraryItem } from "../types";
import { restoreElements } from "./restore";
import { STORAGE_KEYS } from "../constants";
import { getNonDeletedElements } from "../element";
import { NonDeleted, ExcalidrawElement } from "../element/types";
import type App from "../components/App";
export class Library {
private static libraryCache: LibraryItems | null = null;
class Library {
private libraryCache: LibraryItems | null = null;
private app: App;
static resetLibrary = () => {
Library.libraryCache = null;
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
constructor(app: App) {
this.app = app;
}
resetLibrary = async () => {
await this.app.props.onLibraryChange?.([]);
this.libraryCache = [];
};
restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
const elements = getNonDeletedElements(restoreElements(libraryItem, null));
return elements.length ? elements : null;
};
/** imports library (currently merges, removing duplicates) */
static async importLibrary(blob: Blob) {
async importLibrary(blob: Blob) {
const libraryFile = await loadLibraryFromBlob(blob);
if (!libraryFile || !libraryFile.library) {
return;
@ -44,37 +53,41 @@ export class Library {
});
};
const existingLibraryItems = await Library.loadLibrary();
const existingLibraryItems = await this.loadLibrary();
const filtered = libraryFile.library!.reduce((acc, libraryItem) => {
const restored = getNonDeletedElements(restoreElements(libraryItem));
if (isUniqueitem(existingLibraryItems, restored)) {
acc.push(restored);
const restoredItem = this.restoreLibraryItem(libraryItem);
if (restoredItem && isUniqueitem(existingLibraryItems, restoredItem)) {
acc.push(restoredItem);
}
return acc;
}, [] as (readonly NonDeleted<ExcalidrawElement>[])[]);
}, [] as Mutable<LibraryItems>);
Library.saveLibrary([...existingLibraryItems, ...filtered]);
await this.saveLibrary([...existingLibraryItems, ...filtered]);
}
static loadLibrary = (): Promise<LibraryItems> => {
loadLibrary = (): Promise<LibraryItems> => {
return new Promise(async (resolve) => {
if (Library.libraryCache) {
return resolve(JSON.parse(JSON.stringify(Library.libraryCache)));
if (this.libraryCache) {
return resolve(JSON.parse(JSON.stringify(this.libraryCache)));
}
try {
const data = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
if (!data) {
const libraryItems = this.app.libraryItemsFromStorage;
if (!libraryItems) {
return resolve([]);
}
const items = (JSON.parse(data) as LibraryItems).map((elements) =>
restoreElements(elements),
) as Mutable<LibraryItems>;
const items = libraryItems.reduce((acc, item) => {
const restoredItem = this.restoreLibraryItem(item);
if (restoredItem) {
acc.push(item);
}
return acc;
}, [] as Mutable<LibraryItems>);
// clone to ensure we don't mutate the cached library elements in the app
Library.libraryCache = JSON.parse(JSON.stringify(items));
this.libraryCache = JSON.parse(JSON.stringify(items));
resolve(items);
} catch (error) {
@ -84,17 +97,19 @@ export class Library {
});
};
static saveLibrary = (items: LibraryItems) => {
const prevLibraryItems = Library.libraryCache;
saveLibrary = async (items: LibraryItems) => {
const prevLibraryItems = this.libraryCache;
try {
const serializedItems = JSON.stringify(items);
// cache optimistically so that consumers have access to the latest
// cache optimistically so that the app has access to the latest
// immediately
Library.libraryCache = JSON.parse(serializedItems);
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
this.libraryCache = JSON.parse(serializedItems);
await this.app.props.onLibraryChange?.(items);
} catch (error) {
Library.libraryCache = prevLibraryItems;
console.error(error);
this.libraryCache = prevLibraryItems;
throw error;
}
};
}
export default Library;

View File

@ -1,36 +1,72 @@
import {
ExcalidrawElement,
FontFamily,
ExcalidrawSelectionElement,
FontFamilyValues,
} from "../element/types";
import { AppState, NormalizedZoomValue } from "../types";
import { DataState, ImportedDataState } from "./types";
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
import { ImportedDataState } from "./types";
import {
getElementMap,
getNormalizedDimensions,
isInvisiblySmallElement,
} from "../element";
import { isLinearElementType } from "../element/typeChecks";
import { randomId } from "../random";
import {
FONT_FAMILY,
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
FONT_FAMILY,
} from "../constants";
import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement";
const getFontFamilyByName = (fontFamilyName: string): FontFamily => {
for (const [id, fontFamilyString] of Object.entries(FONT_FAMILY)) {
if (fontFamilyString.includes(fontFamilyName)) {
return parseInt(id) as FontFamily;
}
type RestoredAppState = Omit<
AppState,
"offsetTop" | "offsetLeft" | "width" | "height"
>;
export const AllowedExcalidrawElementTypes: Record<
ExcalidrawElement["type"],
true
> = {
selection: true,
text: true,
rectangle: true,
diamond: true,
ellipse: true,
line: true,
arrow: true,
freedraw: true,
};
export type RestoredDataState = {
elements: ExcalidrawElement[];
appState: RestoredAppState;
};
const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
if (Object.keys(FONT_FAMILY).includes(fontFamilyName)) {
return FONT_FAMILY[
fontFamilyName as keyof typeof FONT_FAMILY
] as FontFamilyValues;
}
return DEFAULT_FONT_FAMILY;
};
const restoreElementWithProperties = <T extends ExcalidrawElement>(
const restoreElementWithProperties = <
T extends ExcalidrawElement,
K extends keyof Omit<
Required<T>,
Exclude<keyof ExcalidrawElement, "type" | "x" | "y">
>
>(
element: Required<T>,
extra: Omit<Required<T>, keyof ExcalidrawElement>,
extra: Pick<T, K>,
): T => {
const base: Pick<T, keyof ExcalidrawElement> = {
type: element.type,
type: (extra as Partial<T>).type || element.type,
// all elements must have version > 0 so getSceneVersion() will pick up
// newly added elements
version: element.version || 1,
@ -43,8 +79,8 @@ const restoreElementWithProperties = <T extends ExcalidrawElement>(
roughness: element.roughness ?? 1,
opacity: element.opacity == null ? 100 : element.opacity,
angle: element.angle || 0,
x: element.x || 0,
y: element.y || 0,
x: (extra as Partial<T>).x ?? element.x ?? 0,
y: (extra as Partial<T>).y ?? element.y ?? 0,
strokeColor: element.strokeColor,
backgroundColor: element.backgroundColor,
width: element.width || 0,
@ -87,28 +123,51 @@ const restoreElement = (
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
});
case "draw":
case "freedraw": {
return restoreElementWithProperties(element, {
points: element.points,
lastCommittedPoint: null,
simulatePressure: element.simulatePressure,
pressures: element.pressures,
});
}
case "line":
// @ts-ignore LEGACY type
// eslint-disable-next-line no-fallthrough
case "draw":
case "arrow": {
const {
startArrowhead = null,
endArrowhead = element.type === "arrow" ? "arrow" : null,
} = element;
let x = element.x;
let y = element.y;
let points = // migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2
? [
[0, 0],
[element.width, element.height],
]
: element.points;
if (points[0][0] !== 0 || points[0][1] !== 0) {
({ points, x, y } = LinearElementEditor.getNormalizedPoints(element));
}
return restoreElementWithProperties(element, {
type:
(element.type as ExcalidrawElement["type"] | "draw") === "draw"
? "line"
: element.type,
startBinding: element.startBinding,
endBinding: element.endBinding,
points:
// migrate old arrow model to new one
!Array.isArray(element.points) || element.points.length < 2
? [
[0, 0],
[element.width, element.height],
]
: element.points,
lastCommittedPoint: null,
startArrowhead,
endArrowhead,
points,
x,
y,
});
}
// generic elements
@ -127,13 +186,20 @@ const restoreElement = (
export const restoreElements = (
elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined,
): ExcalidrawElement[] => {
const localElementsMap = localElements ? getElementMap(localElements) : null;
return (elements || []).reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
const migratedElement = restoreElement(element);
let migratedElement: ExcalidrawElement = restoreElement(element);
if (migratedElement) {
const localElement = localElementsMap?.[element.id];
if (localElement && localElement.version > migratedElement.version) {
migratedElement = bumpVersion(migratedElement, localElement.version);
}
elements.push(migratedElement);
}
}
@ -143,31 +209,32 @@ export const restoreElements = (
export const restoreAppState = (
appState: ImportedDataState["appState"],
localAppState: Partial<AppState> | null,
): AppState => {
localAppState: Partial<AppState> | null | undefined,
): RestoredAppState => {
appState = appState || {};
const defaultAppState = getDefaultAppState();
const nextAppState = {} as typeof defaultAppState;
for (const [key, val] of Object.entries(defaultAppState) as [
for (const [key, defaultValue] of Object.entries(defaultAppState) as [
keyof typeof defaultAppState,
any,
][]) {
const restoredValue = appState[key];
const suppliedValue = appState[key];
const localValue = localAppState ? localAppState[key] : undefined;
(nextAppState as any)[key] =
restoredValue !== undefined
? restoredValue
suppliedValue !== undefined
? suppliedValue
: localValue !== undefined
? localValue
: val;
: defaultValue;
}
return {
...nextAppState,
offsetLeft: appState.offsetLeft || 0,
offsetTop: appState.offsetTop || 0,
elementType: AllowedExcalidrawElementTypes[nextAppState.elementType]
? nextAppState.elementType
: "selection",
// Migrates from previous version where appState.zoom was a number
zoom:
typeof appState.zoom === "number"
@ -188,9 +255,10 @@ export const restore = (
* Supply `null` if you can't get access to it.
*/
localAppState: Partial<AppState> | null | undefined,
): DataState => {
localElements: readonly ExcalidrawElement[] | null | undefined,
): RestoredDataState => {
return {
elements: restoreElements(data?.elements),
elements: restoreElements(data?.elements, localElements),
appState: restoreAppState(data?.appState, localAppState || null),
};
};

View File

@ -1,26 +1,30 @@
import { ExcalidrawElement } from "../element/types";
import { AppState, LibraryItems } from "../types";
import type { cleanAppStateForExport } from "../appState";
export interface DataState {
type?: string;
version?: string;
source?: string;
export interface ExportedDataState {
type: string;
version: number;
source: string;
elements: readonly ExcalidrawElement[];
appState: MarkOptional<AppState, "offsetTop" | "offsetLeft">;
appState: ReturnType<typeof cleanAppStateForExport>;
}
export interface ImportedDataState {
type?: string;
version?: string;
source?: string;
elements?: DataState["elements"] | null;
appState?: Partial<DataState["appState"]> | null;
scrollToContent?: boolean;
}
export interface LibraryData {
type?: string;
version?: number;
source?: string;
library?: LibraryItems;
elements?: readonly ExcalidrawElement[] | null;
appState?: Readonly<Partial<AppState>> | null;
scrollToContent?: boolean;
libraryItems?: LibraryItems;
}
export interface ExportedLibraryData {
type: string;
version: number;
source: string;
library: LibraryItems;
}
export interface ImportedLibraryData extends Partial<ExportedLibraryData> {}

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