Compare commits

..

121 Commits

Author SHA1 Message Date
dwelle
1d65536360 improve logger 2022-08-20 18:01:26 +02:00
dwelle
d731a6463c move debug helpers to app.tsx 2022-08-20 13:58:08 +02:00
dwelle
9f325a626e support serving local production builds 2022-08-20 12:47:12 +02:00
dwelle
41200ea28d [PERF DEBUG] 2022-08-20 11:57:36 +02:00
Aakansha Doshi
46a61ad4df feat: enable midpoint inside linear element editor (#5564)
* feat: enable midpoint inside linear element editor

* fix

* fix

* hack to set pointerDownState.hit.hasHitElementInside when mid point added

* remove hacks as not needed :)

* remove newline

* fix

* add doc
2022-08-18 19:56:26 +05:30
Excalidraw Bot
f4b1a30bef chore: Update translations from Crowdin (#5552) 2022-08-18 15:42:40 +02:00
Aakansha Doshi
32aa79164b refactor: remove unused attribute hasHitElementInside from pointerDownState (#5591) 2022-08-18 19:11:18 +05:30
Aakansha Doshi
b5fd904808 fix: allow box selection of points when inside editor (#5594) 2022-08-18 19:07:14 +05:30
David Luzar
8f8dd1105f docs: changelog fixes (#5593) 2022-08-18 14:16:06 +02:00
David Luzar
b914ad41fc feat: support ExcalidrawElement.customData (#5592)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-08-18 17:32:46 +05:30
Aakansha Doshi
551c38f60b fix: remove unnecessary conditions in pointerup for linear elements (#5575)
* fix: remove unnecessary conditions in pointerup for linear elements

* reset editingLinearElement when clicked inside bounding box
2022-08-18 13:58:46 +05:30
Aakansha Doshi
38e8ae46c9 fix: check if hitting link in handleSelectionOnPointerDown (#5589)
fix: check if hitting link in handleSelectionOnPoiinterDown
2022-08-18 13:40:26 +05:30
David Luzar
ad0c4c4c78 fix: points not being normalized on single-elem resize (#5581) 2022-08-16 21:51:43 +02:00
Aakansha Doshi
27cf5ed17e fix: deselect linear element when clicked inside bounding box outside editor (#5579) 2022-08-16 23:05:38 +05:30
Aakansha Doshi
fd946adbae refactor: cleanup renderScene (#5573)
* refactor: cleanup renderScene

* pass object instead of individual params
2022-08-16 16:09:53 +05:30
Caleb OLeary
c37977af4b docs: correct readme type typo (#5574) 2022-08-16 13:55:55 +05:30
Alex Kim
a0d413ab4e fix: resize multiple elements from center (#5560)
Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-08-13 19:53:10 +02:00
Aakansha Doshi
b67a2b4f65 fix: call static methods via class instead of instance in linearElementEditor (#5561)
* fix: call getMidPoint via class instead of instance

* fix

* fix
2022-08-11 20:33:07 +05:30
Aakansha Doshi
5a8dbe8030 feat: show a mid point for linear elements (#5534)
* feat: Add a mid point for linear elements

* fix tests

* show mid point only on hover

* hacky fix :(

* don't add mid points if present and only add outside editor

* improve styling and always show phantom point instead of just on hover

* fix tests

* fix

* only add polyfill for test

* add hover state for phantom point

* fix tests

* fix

* Add Array.at polyfill

* reuse `centerPoint()` helper

* reuse `distance2d`

* use `Point` type

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-08-11 20:16:25 +05:30
Aakansha Doshi
731093f631 fix: show bounding box for 3 or more linear point elements (#5554)
* fix: show bounding box for 3+ linear point elements

* refactor

* show bounding box for 3 points as well

* fix dragging bounding box for linear elements

* Increase margin/padding for linear elements

* fix cursor and keep bounding box same but offset resize handles

* introduce slight padding for selection border

* better

* add constant for spacing
2022-08-10 21:42:28 +05:30
Aakansha Doshi
fe56975f19 fix: cleanup the condition for dragging elements (#5555) 2022-08-10 15:32:40 +05:30
David Luzar
2d800feeeb fix: shareable links being merged with current scene data (#5547) 2022-08-08 17:51:19 +02:00
David Luzar
93cccd596a fix: Scene lookup failing when looking up by id (#5542) 2022-08-08 17:01:17 +02:00
David Luzar
45b592227d fix: remove rounding to fix jitter when shift-editing (#5543)
Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
2022-08-05 20:22:46 +05:30
Ryan Di
b818df1098 feat: lock angle when editing linear elements with shift pressed (#5527)
Co-authored-by: Ryan <diweihao@bytedance.com>
2022-08-04 22:42:31 +00:00
David Luzar
4359e2935d fix: line deselected when shift-dragging point outside editor (#5540) 2022-08-05 00:01:56 +05:30
Aakansha Doshi
3d9d398378 fix: flip linear elements after redesign (#5538) 2022-08-04 18:41:31 +05:30
Aakansha Doshi
0a5da0269f docs: migrate the example to React 18 (#5533) 2022-08-04 12:24:13 +05:30
Aakansha Doshi
08ce7c7fc3 feat: redesign linear elements 🎉 (#5501)
* feat: redesign arrows and lines

* set selectedLinearElement on pointerup

* fix tests

* fix lint

* set selectionLinearElement to null when element is not selected

* fix

* don't set selectedElementIds to empty object when linear element selected

* don't move arrows when clicked on bounding box

* don't consider bounding box when linear element selected

* better hitbox

* show pointer when over the points in linear elements

* highlight points when hovered

* tweak design whene editing linear element points

* tweak

* fix test

* fix multi point editing

* cleanup

* fix

* fix

* remove stroke when hovered

* account for zoom when hover

* review fix

* set selectedLinearElement to null when selectedElementIds doesn't contain the linear element

* remove hover affect when moved away from linear element

* don't set selectedLinearAElement if already set

* fix selection

* render reduced in test :p

* fix box selection for single linear element

* set selectedLinearElement when deselecting selected elements and linear element is selected

* don't show linear element handles when element locked

* selected linear element when only linear present and selected with selectAll

* don't set selectedLinearElement if already set

* store selectedLinearElement in browser to persist

* remove redundant checks

* test fix

* select linear element handles when user has finished multipoint editing

* fix snap

* add comments

* show bounding box for locked linear elements

* add stroke param to fillCircle and remove stroke when linear element point hovered

* set selectedLinearElement when thats the only element left when deselcting others

* skip tests instead of removing for rotation

* (un)bind on pointerUp when moving linear element points outside editor

* render bounding box for linear elements as a fallback on state mismatch

* simplify and remove type assertion

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-08-03 20:58:17 +05:30
Excalidraw Bot
fe7fbff7f6 chore: Update translations from Crowdin (#5507)
* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage
2022-08-03 11:43:00 +05:30
David Luzar
501397cb61 fix: disable locking aspect ratio for box-selection (#5525) 2022-08-02 19:10:17 +05:30
Ryan Di
865d29388c feat: cursor alignment when creating linear elements with shift pressed (#5518)
* feat: cursor alignment when creating linear elements

* feat: apply cursor alignment to multi-point linear elements

* refactor: rename size helper function
2022-08-02 15:43:19 +05:30
José dBruxelles
54c7ec416a fix: Add title attribute to the modal close button (#5521)
Add `title` attribute to the modal close button
2022-08-02 11:42:47 +05:30
zsviczian
aca284057d fix: Context menu positioning when component has offsets (#5520)
Update Popover.tsx
2022-08-02 11:38:55 +05:30
Ryan Di
2820cd112e feat: shift-clamp when creating multi-point lines/arrows (#5500)
Co-authored-by: Ryan <diweihao@bytedance.com>
2022-08-01 15:41:50 +02:00
Ryan Di
426b5d9537 feat: cursor alignment when creating generic elements (#5516)
Co-authored-by: Ryan <diweihao@bytedance.com>
2022-08-01 13:24:46 +02:00
David Luzar
e7d34677c6 fix: resolve paths in prebuild.js script (#5498) 2022-07-30 21:56:46 +02:00
Aakansha Doshi
3d5356cb8e fix: use flushSync when moving line editor since we need to read previous value after setting state (#5508)
* fix: use flushSync when moving line editor since we need to read previous value after setting state

* add comment
2022-07-29 19:27:37 +05:30
Aakansha Doshi
46f5ce5ce0 fix: useLayout effect cleanup in dev mode for charts (#5505) 2022-07-29 17:25:26 +05:30
Excalidraw Bot
b00bd3d6c0 chore: Update translations from Crowdin (#5476)
* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (Tamil)

* New translations en.json (Swedish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Bengali)

* New translations en.json (Slovak)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

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

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Slovenian)

* New translations en.json (Russian)

* New translations en.json (Turkish)

* New translations en.json (German)

* New translations en.json (Marathi)

* New translations en.json (Basque)

* New translations en.json (Romanian)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (Greek)

* New translations en.json (Portuguese)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-07-29 11:14:09 +05:30
Aakansha Doshi
91fc22182c fix: revert browser toast for high/low zoom (#5495) 2022-07-27 20:49:29 +05:30
Aakansha Doshi
966ca2ffa6 refactor: rename docs to dev-docs (#5487) 2022-07-26 16:55:25 +05:30
Aakansha Doshi
2b049b4a65 docs: Integrate docusaraus for docs (#5482)
* feat:Integrate docusaraus for docs

* Update docs for Excalidraw

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

* remove blogs

* remove blog authors

* get started docs

* typo

* add static assets

* change port number

* Add script to build docs only if docs updated

* dummy

* update script to be compatible with ignoreBuild in vercel

* remove script and dummy log

Co-authored-by: David Luzar <luzar.david@gmail.com>
2022-07-26 16:34:12 +05:30
Aakansha Doshi
339212e563 refactor: remove unnecessary if condition for linear element onKeyDown (#5486)
* refactor: remove unnecessary if condition for linear element onKeyDown

* fix
2022-07-26 16:33:13 +05:30
DanielJGeiger
f8b4bb66b4 chore: Update peer dependencies to React 18 in @excalidraw/excalidraw (#5483)
* chore: Update peer dependencies to React 18 in `@excalidraw/excalidraw`

* Update src/packages/excalidraw/package.json

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

Co-authored-by: David Luzar <luzar.david@gmail.com>
2022-07-26 16:23:30 +05:30
Andrew
f4312bba5e fix: Fixing push to DockerHub (#5468)
* fix: Fixing push to DockerHub

* Update .github/workflows/publish-docker.yml

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-07-26 16:13:09 +05:30
David Luzar
ac66665b64 fix: incorrectly rendering freedraw elements (#5481) 2022-07-22 16:18:41 +02:00
Aakansha Doshi
2b71a1f0bd fix: generate types when building example (#5480) 2022-07-22 18:53:21 +05:30
Aakansha Doshi
58845e450a fix: Use React.FC as react-dom is not able to infer types of Modal (#5479) 2022-07-22 13:09:15 +05:30
Aakansha Doshi
15d79f8fee chore: upgrade to React 18 (#5450)
* chore: upgrade to React 18

* better type

* use React.FC to fix type

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-07-22 11:20:36 +05:30
Shubham Shah
958ebeae61 feat: make context menu scrollable (#4030)
* Make context menu scrollable

* Fix color picker not showing up

* Fix overflow cuts shadow

* style fixes

* fix

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-07-21 14:34:49 +02:00
Excalidraw Bot
31f51ca53b chore: Update translations from Crowdin (#5428)
* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (French)

* New translations en.json (French)

* add marathi and vietnamese

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-07-21 13:25:30 +05:30
dependabot[bot]
5abbf73050 chore(deps-dev): bump sass-loader from 12.4.0 to 13.0.2 in /src/packages/excalidraw (#5400)
chore(deps-dev): bump sass-loader in /src/packages/excalidraw

Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.4.0 to 13.0.2.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.4.0...v13.0.2)

---
updated-dependencies:
- dependency-name: sass-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>
2022-07-20 12:51:57 +00:00
dependabot[bot]
7cf766630d chore(deps-dev): bump rewire from 5.0.0 to 6.0.0 (#4440)
Bumps [rewire](https://github.com/jhnns/rewire) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/jhnns/rewire/releases)
- [Changelog](https://github.com/jhnns/rewire/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jhnns/rewire/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: rewire
  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>
2022-07-20 18:20:03 +05:30
dependabot[bot]
59fccafeac chore(deps-dev): bump sass-loader from 12.6.0 to 13.0.2 in /src/packages/utils (#5396)
chore(deps-dev): bump sass-loader in /src/packages/utils

Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.6.0 to 13.0.2.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.6.0...v13.0.2)

---
updated-dependencies:
- dependency-name: sass-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>
2022-07-20 18:17:09 +05:30
dependabot[bot]
19a6996e6b chore(deps-dev): bump typescript from 4.6.4 to 4.7.4 in /src/packages/excalidraw (#5329)
chore(deps-dev): bump typescript in /src/packages/excalidraw

Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.6.4 to 4.7.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.6.4...v4.7.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 18:16:09 +05:30
dependabot[bot]
86c4f90910 chore(deps-dev): bump postcss-loader from 6.2.1 to 7.0.0 in /src/packages/excalidraw (#5234)
chore(deps-dev): bump postcss-loader in /src/packages/excalidraw

Bumps [postcss-loader](https://github.com/webpack-contrib/postcss-loader) from 6.2.1 to 7.0.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/v6.2.1...v7.0.0)

---
updated-dependencies:
- dependency-name: postcss-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>
2022-07-20 18:15:50 +05:30
dependabot[bot]
4d88112021 chore(deps-dev): bump @babel/plugin-transform-runtime from 7.17.10 to 7.18.6 in /src/packages/excalidraw (#5390)
chore(deps-dev): bump @babel/plugin-transform-runtime

Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.17.10 to 7.18.6.
- [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.18.6/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 18:12:21 +05:30
dependabot[bot]
de5c63e299 chore(deps-dev): bump @babel/plugin-transform-arrow-functions from 7.16.7 to 7.18.6 in /src/packages/excalidraw (#5392)
chore(deps-dev): bump @babel/plugin-transform-arrow-functions

Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.16.7 to 7.18.6.
- [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.18.6/packages/babel-plugin-transform-arrow-functions)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-arrow-functions"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 18:11:40 +05:30
dependabot[bot]
da0853a121 chore(deps-dev): bump @babel/plugin-transform-async-to-generator from 7.16.5 to 7.18.6 in /src/packages/utils (#5391)
chore(deps-dev): bump @babel/plugin-transform-async-to-generator

Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.16.5 to 7.18.6.
- [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.18.6/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>
2022-07-20 11:11:25 +00:00
dependabot[bot]
57cc4b6a29 chore(deps-dev): bump terser-webpack-plugin from 5.3.1 to 5.3.3 in /src/packages/excalidraw (#5272)
chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw

Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.3.1 to 5.3.3.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.3.1...v5.3.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:43:47 +00:00
dependabot[bot]
e2ddd7b27a chore(deps-dev): bump @babel/plugin-transform-typescript from 7.16.1 to 7.18.6 in /src/packages/excalidraw (#5397)
chore(deps-dev): bump @babel/plugin-transform-typescript

Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.16.1 to 7.18.6.
- [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.18.6/packages/babel-plugin-transform-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:43:37 +00:00
dependabot[bot]
693de8501e chore(deps-dev): bump webpack-cli from 4.9.2 to 4.10.0 in src/packages/excalidraw (#5327)
chore(deps-dev): bump webpack-cli in /src/packages/excalidraw

Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.9.2 to 4.10.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.9.2...webpack-cli@4.10.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:42:51 +00:00
dependabot[bot]
c6df6d444e chore(deps-dev): bump @babel/plugin-transform-arrow-functions from 7.16.0 to 7.18.6 in /src/packages/utils (#5398)
chore(deps-dev): bump @babel/plugin-transform-arrow-functions

Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.16.0 to 7.18.6.
- [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.18.6/packages/babel-plugin-transform-arrow-functions)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-arrow-functions"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:39:23 +00:00
dependabot[bot]
ad5692c5f8 chore(deps-dev): bump webpack-cli from 4.9.2 to 4.10.0 in /src/packages/utils (#5324)
chore(deps-dev): bump webpack-cli in /src/packages/utils

Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.9.2 to 4.10.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.9.2...webpack-cli@4.10.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:25:19 +00:00
dependabot[bot]
60ab3337af chore(deps-dev): bump dotenv from 10.0.0 to 16.0.1 (#5197)
Bumps [dotenv](https://github.com/motdotla/dotenv) from 10.0.0 to 16.0.1.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v10.0.0...v16.0.1)

---
updated-dependencies:
- dependency-name: dotenv
  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>
2022-07-20 15:50:17 +05:30
dependabot[bot]
dd847793d2 chore(deps): bump dotenv from 10.0.0 to 16.0.1 in /src/packages/excalidraw (#5195)
chore(deps): bump dotenv in /src/packages/excalidraw

Bumps [dotenv](https://github.com/motdotla/dotenv) from 10.0.0 to 16.0.1.
- [Release notes](https://github.com/motdotla/dotenv/releases)
- [Changelog](https://github.com/motdotla/dotenv/blob/master/CHANGELOG.md)
- [Commits](https://github.com/motdotla/dotenv/compare/v10.0.0...v16.0.1)

---
updated-dependencies:
- dependency-name: dotenv
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:48:57 +05:30
dependabot[bot]
6d6e9f0dd3 chore(deps-dev): bump @babel/plugin-transform-typescript from 7.16.1 to 7.18.6 in /src/packages/utils (#5393)
chore(deps-dev): bump @babel/plugin-transform-typescript

Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.16.1 to 7.18.6.
- [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.18.6/packages/babel-plugin-transform-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:15:12 +00:00
dependabot[bot]
0fe0d7ca6b chore(deps-dev): bump webpack from 5.72.0 to 5.73.0 in /src/packages/excalidraw (#5268)
chore(deps-dev): bump webpack in /src/packages/excalidraw

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 10:12:12 +00:00
dependabot[bot]
abcf1f1bae chore(deps-dev): bump @types/lodash.throttle from 4.1.6 to 4.1.7 (#5172)
Bumps [@types/lodash.throttle](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash.throttle) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash.throttle)

---
updated-dependencies:
- dependency-name: "@types/lodash.throttle"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:41:20 +05:30
dependabot[bot]
7d0b03f754 chore(deps-dev): bump ts-loader from 9.3.0 to 9.3.1 in /src/packages/utils (#5356)
chore(deps-dev): bump ts-loader in /src/packages/utils

Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.3.0 to 9.3.1.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.3.0...v9.3.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:40:25 +05:30
dependabot[bot]
bd8931d3d1 chore(deps): bump i18next-browser-languagedetector from 6.1.2 to 6.1.4 (#4977)
Bumps [i18next-browser-languagedetector](https://github.com/i18next/i18next-browser-languageDetector) from 6.1.2 to 6.1.4.
- [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.1.2...v6.1.4)

---
updated-dependencies:
- dependency-name: i18next-browser-languagedetector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:39:41 +05:30
dependabot[bot]
0d86c04939 chore(deps-dev): bump @babel/plugin-transform-async-to-generator from 7.16.0 to 7.18.6 in /src/packages/excalidraw (#5402)
chore(deps-dev): bump @babel/plugin-transform-async-to-generator

Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.16.0 to 7.18.6.
- [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.18.6/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>
2022-07-20 15:37:40 +05:30
dependabot[bot]
8436ebbf68 chore(deps-dev): bump ts-loader from 9.3.0 to 9.3.1 in /src/packages/excalidraw (#5355)
chore(deps-dev): bump ts-loader in /src/packages/excalidraw

Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.3.0 to 9.3.1.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.3.0...v9.3.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:29:47 +05:30
dependabot[bot]
824f94b3df chore(deps-dev): bump @babel/preset-typescript from 7.16.7 to 7.18.6 in /src/packages/utils (#5389)
chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils

Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.16.7 to 7.18.6.
- [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.18.6/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:29:17 +05:30
dependabot[bot]
f9a8e686b2 chore(deps-dev): bump @babel/core from 7.17.2 to 7.18.6 in /src/packages/utils (#5395)
chore(deps-dev): bump @babel/core in /src/packages/utils

Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.2 to 7.18.6.
- [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.18.6/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:28:54 +05:30
dependabot[bot]
e442a44ba8 chore(deps-dev): bump @babel/preset-react from 7.16.7 to 7.18.6 in /src/packages/excalidraw (#5394)
chore(deps-dev): bump @babel/preset-react in /src/packages/excalidraw

Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.16.7 to 7.18.6.
- [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.18.6/packages/babel-preset-react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:28:34 +05:30
dependabot[bot]
f1fd29571a chore(deps): bump @tldraw/vec from 1.4.3 to 1.7.1 (#5360)
Bumps [@tldraw/vec](https://github.com/tldraw/tldraw) from 1.4.3 to 1.7.1.
- [Release notes](https://github.com/tldraw/tldraw/releases)
- [Commits](https://github.com/tldraw/tldraw/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 15:04:56 +05:30
dependabot[bot]
6a482a7ba2 chore(deps): bump url-parse from 1.5.7 to 1.5.10 (#4851)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.7 to 1.5.10.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.7...1.5.10)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 14:59:43 +05:30
dependabot[bot]
bfea434a55 chore(deps-dev): bump @types/resize-observer-browser from 0.1.6 to 0.1.7 (#4759)
Bumps [@types/resize-observer-browser](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/resize-observer-browser) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/resize-observer-browser)

---
updated-dependencies:
- dependency-name: "@types/resize-observer-browser"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 14:58:42 +05:30
dependabot[bot]
acb22c5a64 chore(deps-dev): bump webpack from 5.72.1 to 5.73.0 in /src/packages/utils (#5273)
chore(deps-dev): bump webpack in /src/packages/utils

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 14:56:24 +05:30
dependabot[bot]
7cd1b621d1 chore(deps-dev): bump mini-css-extract-plugin from 2.6.0 to 2.6.1 in /src/packages/excalidraw (#5331)
chore(deps-dev): bump mini-css-extract-plugin

Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.6.0 to 2.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/v2.6.0...v2.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>
2022-07-20 14:55:53 +05:30
dependabot[bot]
9c37b25bab chore(deps-dev): bump @babel/preset-typescript from 7.16.7 to 7.18.6 in /src/packages/excalidraw (#5399)
chore(deps-dev): bump @babel/preset-typescript

Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.16.7 to 7.18.6.
- [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.18.6/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 14:55:15 +05:30
dependabot[bot]
a8bb9a78ef chore(deps-dev): bump @babel/preset-env from 7.16.7 to 7.18.6 in /src/packages/utils (#5401)
chore(deps-dev): bump @babel/preset-env in /src/packages/utils

Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.7 to 7.18.6.
- [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.18.6/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 14:54:51 +05:30
dependabot[bot]
e4aff04061 chore(deps-dev): bump @babel/core from 7.17.0 to 7.18.6 in /src/packages/excalidraw (#5403)
chore(deps-dev): bump @babel/core in /src/packages/excalidraw

Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.17.0 to 7.18.6.
- [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.18.6/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 14:54:30 +05:30
Gwenaël Gallon
c5cadc7de3 fix: missing translation for "Scale" to Export Dialog (#5456)
fix: missing translation for "Scale"
2022-07-20 14:52:04 +05:30
dependabot[bot]
7dc0c0d96a chore(deps): bump terser from 5.7.0 to 5.14.2 in /src/packages/utils (#5469)
Bumps [terser](https://github.com/terser/terser) from 5.7.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 12:33:27 +05:30
dependabot[bot]
2c9c8c8e05 chore(deps): bump terser from 5.9.0 to 5.14.2 in /src/packages/excalidraw (#5470)
chore(deps): bump terser in /src/packages/excalidraw

Bumps [terser](https://github.com/terser/terser) from 5.9.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 12:32:56 +05:30
dependabot[bot]
b5d7ae57e5 chore(deps): bump terser from 4.8.0 to 4.8.1 (#5471)
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-20 12:32:36 +05:30
Aakansha Doshi
0f66ee3a41 build: move dotenv to dev deps (#5472) 2022-07-20 12:32:12 +05:30
Aakansha Doshi
771372c66b fix: add display name for Excalidraw component so it doesn't show as anonymous (#5464)
fix: add display name for Excalidraw component
2022-07-19 21:04:05 +05:30
David Luzar
a7937681e9 fix: account for safe area for floating buttons on mobile (#5420) 2022-07-19 15:44:14 +02:00
David Luzar
792f238d16 refactor: improve typing & check (#5415) 2022-07-19 15:44:04 +02:00
Aakansha Doshi
ba16416c75 fix: attribute warnings in comment svg example (#5465)
fix the attributes in comment svg example
2022-07-19 17:53:21 +05:30
Aakansha Doshi
6e0ac52a64 fix: check for ctrl key when wheel event triggered to only disable zooming (#5459)
* fix: check for ctrl key when wheel event triggered to only disable zooming

* remove newline
2022-07-18 14:39:55 +05:30
David Luzar
5bc40402a6 fix: disable render throttling by default & during resize (#5451) 2022-07-16 11:36:55 +02:00
Aakansha Doshi
df14c69977 refactor: don't pass zenModeEnable, viewModeEnabled and toggleZenMode props to LayerUI (#5444)
refactor: don't pass zenModeEnabled and viewModeEnabled props to LayerUI
2022-07-14 16:13:10 +05:30
Aakansha Doshi
1ea67ba93d fix: attach wheel event to exscalidraw container only (#5443) 2022-07-14 11:08:20 +05:30
Aakansha Doshi
a7153d9d1d feat: update toast api to account for duration and closable (#5427)
* feat: update toast api to account for duration and closable

* fix

* update snaps

* update docs

* male toast nullable

* fix snaps

* remove clearToast from App.tsx and replace clearToast prop in Toast comp with onClose
2022-07-11 18:11:13 +05:30
Excalidraw Bot
e885057a71 chore: Update translations from Crowdin (#5186)
* Auto commit: Calculate translation coverage

* New translations en.json (Persian)

* Auto commit: Calculate translation coverage

* New translations en.json (Persian)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* New translations en.json (Catalan)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

* New translations en.json (Kabyle)

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Basque)

* Auto commit: Calculate translation coverage

* New translations en.json (Catalan)

* New translations en.json (Catalan)

* New translations en.json (Bulgarian)

* New translations en.json (Hebrew)

* New translations en.json (Greek)

* New translations en.json (German)

* New translations en.json (Danish)

* New translations en.json (Czech)

* New translations en.json (Finnish)

* New translations en.json (Arabic)

* New translations en.json (French)

* New translations en.json (Romanian)

* New translations en.json (Basque)

* New translations en.json (Spanish)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Burmese)

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

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Ukrainian)

* New translations en.json (Sinhala)

* New translations en.json (Chinese Traditional)

* New translations en.json (Polish)

* New translations en.json (Turkish)

* New translations en.json (Lithuanian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Dutch)

* New translations en.json (Swedish)

* New translations en.json (Punjabi)

* New translations en.json (Kabyle)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Finnish)

* New translations en.json (Latvian)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Catalan)

* New translations en.json (Greek)

* New translations en.json (Punjabi)

* New translations en.json (Dutch)

* New translations en.json (Lithuanian)

* New translations en.json (Korean)

* New translations en.json (Japanese)

* New translations en.json (Italian)

* New translations en.json (Hungarian)

* New translations en.json (Hebrew)

* New translations en.json (Finnish)

* New translations en.json (Polish)

* New translations en.json (German)

* New translations en.json (Czech)

* New translations en.json (Bulgarian)

* New translations en.json (Arabic)

* New translations en.json (Spanish)

* New translations en.json (French)

* New translations en.json (Romanian)

* New translations en.json (Basque)

* New translations en.json (Danish)

* New translations en.json (Kazakh)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Sinhala)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

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

* New translations en.json (Persian)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Tamil)

* New translations en.json (Turkish)

* New translations en.json (Indonesian)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Ukrainian)

* New translations en.json (Swedish)

* New translations en.json (Slovenian)

* New translations en.json (Slovak)

* New translations en.json (Russian)

* New translations en.json (Portuguese)

* New translations en.json (Kabyle)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* New translations en.json (Swedish)

* New translations en.json (Chinese Traditional)

* New translations en.json (Latvian)

* New translations en.json (Norwegian Bokmal)

* 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 (Slovenian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage

* New translations en.json (Finnish)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Turkish)

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Czech)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Catalan)

* New translations en.json (Bengali)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Marathi)

* New translations en.json (Russian)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

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

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Slovak)

* New translations en.json (Portuguese)

* New translations en.json (Kabyle)

* New translations en.json (Danish)

* New translations en.json (Chinese Simplified)

* New translations en.json (Vietnamese)

* New translations en.json (Basque)

* 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 (Czech)

* New translations en.json (German)

* New translations en.json (Polish)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* Add Slovenščina

* New translations en.json (Slovenian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Italian)

* New translations en.json (Latvian)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* fix code for Slovenščina

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-07-09 11:28:09 +05:30
Aakansha Doshi
7efa081976 fix: show toast when browser zoom is not 100% (#5304)
* fix: show toast when browser zoom is not 100%

* update threshold for detecting zoom

* Make toast permanent for browser zoom

* check if browser zoomed on mount

* retrieve toast params from function getToastParams

* Revert "retrieve toast params from function getToastParams"

This reverts commit cfca580d74.
2022-07-08 19:19:00 +05:30
Aakansha Doshi
5deb93a083 fix: prevent browser zoom inside Excalidraw (#5426)
* fix: prevent browser zoom inside Excalidraw

* prevent default only for =/-
2022-07-08 17:53:40 +05:30
Aakansha Doshi
e3908e6fe3 fix: typo in changelog (#5425) 2022-07-07 18:45:17 +05:30
Aakansha Doshi
fe3d0b5e8b docs: release @excalidraw/excalidraw@0.12.0 🎉 (#5421) 2022-07-07 18:26:19 +05:30
David Luzar
b6bb74d08d feat: throttle scene rendering to animation framerate (#5422) 2022-07-07 11:47:37 +02:00
Aakansha Doshi
c725f84334 build: extract all i18n files into locales folder (#5419) 2022-07-06 15:21:05 +05:30
Aakansha Doshi
11a3380d83 build: automate release step fully (#5414)
* build: automate release step fully

* exit process when error

* Add npm scripts for release and prerelease

* update docs with release setps
2022-07-06 15:20:52 +05:30
Aakansha Doshi
76a5bb060e feat: make toast closable and allow custom duration (#5308)
* feat: make toast closable and allow custom duration

* use Infinity to keep prevent auto close

* rename to DEFAULT_TOAST_TIMEOUT and move to toast.tsx

* fix

* set closable as false by default and fix design

* tweak css

* reuse variables

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-07-05 21:43:59 +05:30
David Luzar
dac8dda4d4 feat: collab component state handling rewrite & fixes (#5046) 2022-07-05 16:03:40 +02:00
Aakansha Doshi
a1a62468a6 docs: fix command to trigger release (#5413) 2022-07-05 12:24:50 +05:30
Aakansha Doshi
ba3a723e99 fix: autorelease job name (#5412) 2022-07-04 22:32:22 +05:30
Aakansha Doshi
c5355c08cf fix: action name for autorelease (#5411) 2022-07-04 22:25:24 +05:30
Aakansha Doshi
6102380051 build: use next and preview tags instead of separate packages for next and preview release (#5346)
* build: use next and preview tags instead of separate packages for next and preview release

* add tag

* dummy diff

* revert dummy tweak

* don't use readme_next

* add note for latest release readme

* Add warning emoji for note

* move note to top

* remove readme next

* fix

* dummy change for release

* remove double note

* Revert "dummy change for release"

This reverts commit d3655cdee4.
2022-07-04 22:19:08 +05:30
Aakansha Doshi
655e59a707 chore: bye bye dependabot :) (#5409) 2022-07-04 20:11:05 +05:30
Aakansha Doshi
d05745070b fix: typecast file to fix the build (#5410)
* fix: typecast file to fix the build

* update type for fileOpen

* fix
2022-07-04 17:44:20 +05:30
dependabot[bot]
88c313bf86 chore(deps): bump jsdom from 16.4.0 to 16.7.0 (#5350)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 16.4.0 to 16.7.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Changelog](https://github.com/jsdom/jsdom/blob/master/Changelog.md)
- [Commits](https://github.com/jsdom/jsdom/compare/16.4.0...16.7.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-04 14:06:27 +05:30
dependabot[bot]
a7705848ec chore(deps-dev): bump webpack-dev-server from 4.9.0 to 4.9.3 in /src/packages/excalidraw (#5404)
chore(deps-dev): bump webpack-dev-server in /src/packages/excalidraw

Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.9.0 to 4.9.3.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.9.0...v4.9.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-04 11:36:08 +05:30
dependabot[bot]
69e1bae8dd chore(deps-dev): bump @babel/preset-env from 7.17.10 to 7.18.6 in /src/packages/excalidraw (#5405)
chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw

Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.17.10 to 7.18.6.
- [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.18.6/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-04 11:35:25 +05:30
dependabot[bot]
d361757e4a chore(deps-dev): bump @babel/plugin-transform-runtime from 7.17.10 to 7.18.6 in /src/packages/utils (#5388)
chore(deps-dev): bump @babel/plugin-transform-runtime

Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.17.10 to 7.18.6.
- [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.18.6/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-04 11:35:06 +05:30
David Luzar
0ef202f2df feat: support debugging PWA in dev (#4853)
* feat: support enabling pwa in dev

* enable workbox debug

* add prebuild script

* fix lint
2022-07-02 17:59:03 +02:00
David Luzar
bbfd2b3cd3 fix: file handle not persisted when importing excalidraw files (#5372) 2022-06-28 14:44:59 +02:00
185 changed files with 15569 additions and 6438 deletions

View File

@@ -11,3 +11,12 @@ REACT_APP_WS_SERVER_URL=http://localhost:3002
REACT_APP_PORTAL_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'
# put these in your .env.local, or make sure you don't commit!
# must be lowercase `true` when turned on
#
# whether to enable Service Workers in development
REACT_APP_DEV_ENABLE_SW=
# whether to disable live reload / HMR. Usuaully what you want to do when
# debugging Service Workers.
REACT_APP_DEV_DISABLE_LIVE_RELOAD=

View File

@@ -1,37 +0,0 @@
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- lipis
assignees:
- lipis
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/excalidraw/
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/utils/
schedule:
interval: weekly
day: sunday
time: "01:00"
reviewers:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20

View File

@@ -1,4 +1,4 @@
name: Auto release @excalidraw/excalidraw-next
name: Auto release excalidraw next
on:
push:
branches:

View File

@@ -1,4 +1,4 @@
name: Auto release preview @excalidraw/excalidraw-preview
name: Auto release excalidraw preview
on:
issue_comment:
types: [created, edited]

View File

@@ -10,11 +10,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/build-push-action@v2
- name: Checkout repository
uses: actions/checkout@v3
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: excalidraw/excalidraw
tag_with_ref: true
tag_with_sha: true
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: excalidraw/excalidraw:latest

1
.gitignore vendored
View File

@@ -19,7 +19,6 @@ logs
node_modules
npm-debug.log*
package-lock.json
static
yarn-debug.log*
yarn-error.log*
src/packages/excalidraw/types

20
dev-docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

41
dev-docs/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Website
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
### Installation
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
### Build
```
$ yarn build
```
This command generates static content into the `build` directory and can be served using any static contents hosting service.
### Deployment
Using SSH:
```
$ USE_SSH=true yarn deploy
```
Not using SSH:
```
$ GIT_USER=<Your GitHub username> yarn deploy
```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.

3
dev-docs/babel.config.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
};

View File

@@ -0,0 +1,6 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).

View File

@@ -0,0 +1,8 @@
---
sidebar_position: 1
title: Introduction
---
Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview).
If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview).

View File

@@ -0,0 +1,6 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).

View File

@@ -0,0 +1,121 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require("prism-react-renderer/themes/github");
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "Excalidraw developer docs",
tagline:
"For Excalidraw contributors or those integrating the Excalidraw editor",
url: "https://docs.excalidraw.com.com",
baseUrl: "/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
favicon: "img/favicon.ico",
organizationName: "Excalidraw", // Usually your GitHub org/user name.
projectName: "excalidraw", // Usually your repo name.
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
editUrl: "https://github.com/excalidraw/docs/tree/master/",
},
theme: {
customCss: require.resolve("./src/css/custom.css"),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: "Excalidraw Docs",
logo: {
alt: "Excalidraw Logo",
src: "img/logo.svg",
},
items: [
{
type: "doc",
docId: "get-started",
position: "left",
label: "Get started",
},
{
to: "https://blog.excalidraw.com",
label: "Blog",
position: "left",
},
{
to: "https://github.com/excalidraw/excalidraw",
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
title: "Docs",
items: [
{
label: "Get Started",
to: "/docs/get-started",
},
],
},
{
title: "Community",
items: [
{
label: "Discord",
href: "https://discord.gg/UexuTaE",
},
{
label: "Twitter",
href: "https://twitter.com/excalidraw",
},
],
},
{
title: "More",
items: [
{
label: "Blog",
to: "https://blog.excalidraw.com",
},
{
label: "GitHub",
to: "https://github.com/excalidraw/excalidraw",
},
],
},
],
copyright: `Made with ❤️ Built with Docusaurus`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;

46
dev-docs/package.json Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start --port 3003",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.0.0-rc.1",
"@docusaurus/preset-classic": "2.0.0-rc.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-rc.1",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=16.14"
}
}

31
dev-docs/sidebars.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
// But you can create a sidebar manually
/*
tutorialSidebar: [
{
type: 'category',
label: 'Tutorial',
items: ['hello'],
},
],
*/
};
module.exports = sidebars;

View File

@@ -0,0 +1,62 @@
import React from "react";
import clsx from "clsx";
import styles from "./styles.module.css";
const FeatureList = [
{
title: "Learn how Excalidraw works",
Svg: require("@site/static/img/undraw_innovative.svg").default,
description: (
<>Want to contribute to Excalidraw but got lost in the codebase?</>
),
},
{
title: "Integrate Excalidraw",
Svg: require("@site/static/img/undraw_blank_canvas.svg").default,
description: (
<>
Want to build your own app powered by Excalidraw by don't know where to
start?
</>
),
},
{
title: "Help us improve",
Svg: require("@site/static/img/undraw_add_files.svg").default,
description: (
<>
Are the docs missing something? Anything you had trouble understanding
or needs an explanation? Come contribute to the docs to make them even
better!
</>
),
},
];
function Feature({ Svg, title, description }) {
return (
<div className={clsx("col col--4")}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures() {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,70 @@
import React from "react";
import clsx from "clsx";
import styles from "./styles.module.css";
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<"svg">>;
description: JSX.Element;
};
const FeatureList: FeatureItem[] = [
{
title: "Easy to Use",
Svg: require("@site/static/img/undraw_docusaurus_mountain.svg").default,
description: (
<>
Docusaurus was designed from the ground up to be easily installed and
used to get your website up and running quickly.
</>
),
},
{
title: "Focus on What Matters",
Svg: require("@site/static/img/undraw_docusaurus_tree.svg").default,
description: (
<>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go
ahead and move your docs into the <code>docs</code> directory.
</>
),
},
{
title: "Powered by React",
Svg: require("@site/static/img/undraw_docusaurus_react.svg").default,
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can
be extended while reusing the same header and footer.
</>
),
},
];
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx("col col--4")}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

View File

@@ -0,0 +1,43 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #6965db;
--ifm-color-primary-dark: #5b57d1;
--ifm-color-primary-darker: #5b57d1;
--ifm-color-primary-darkest: #4a47b1;
--ifm-color-primary-light: #5b57d1;
--ifm-color-primary-lighter: #5b57d1;
--ifm-color-primary-lightest: #5b57d1;
--ifm-code-font-size: 95%;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
--ifm-color-primary: #5650f0;
--ifm-color-primary-dark: #4b46d8;
--ifm-color-primary-darker: #4b46d8;
--ifm-color-primary-darkest: #3e39be;
--ifm-color-primary-light: #3f3d64;
--ifm-color-primary-lighter: #3f3d64;
--ifm-color-primary-lightest: #3f3d64;
}
.docusaurus-highlight-code-line {
background-color: rgba(0, 0, 0, 0.1);
display: block;
margin: 0 calc(-1 * var(--ifm-pre-padding));
padding: 0 var(--ifm-pre-padding);
}
[data-theme="dark"] .docusaurus-highlight-code-line {
background-color: rgba(0, 0, 0, 0.3);
}
[data-theme="dark"] .navbar__logo {
filter: invert(93%) hue-rotate(180deg);
}

View File

@@ -0,0 +1,42 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/get-started"
>
Get started
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}

View File

@@ -0,0 +1,27 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
[data-theme="dark"] .heroBanner {
color: #fff;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,42 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/get-started"
>
Get started
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}

View File

@@ -0,0 +1,7 @@
---
title: Markdown page example
---
# Markdown page example
You don't need React to write simple standalone pages.

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 80 180" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2">
<path d="M22.197 150.382c-4.179-3.359-10.618-9.051-15.702-13.946l-4.01-3.813.734-5.009c.396-2.732 1.13-8.083 1.582-11.839.508-3.757 1.017-7.286 1.186-7.798.226-.683 0-1.025-.621-1.025-1.073 0-1.13.285 1.807-9.107a617.602 617.602 0 0 1 2.203-7.229c.113-.398.565-.569 1.073-.398.508.227.791.683.621 1.081-.169.455.113.911.565 1.082.621.227.565.683-.395 2.333-1.525 2.562-5.422 24.419-5.648 31.477-.17 5.009-.17 5.066 1.92 7.912 2.033 2.789 6.721 7.001 13.951 12.351 2.033 1.537 4.067 3.245 4.631 3.814.848 1.024 1.243.74 8.36-6.887 4.123-4.383 8.698-8.88 10.166-10.018l2.711-2.049-2.089-4.44c-1.13-2.391-5.705-11.612-10.223-20.377-9.433-18.442-7.513-16.678-18.47-16.849l-7.117-.056-2.372-2.733c-2.485-2.903-2.824-3.984-1.638-5.805.452-.627.791-1.651.791-2.277 0-1.025.395-1.196 2.655-1.309 1.412-.057 2.711-.228 2.88-.399.17-.171.396-3.7.565-7.855l.226-7.513-3.784-8.197C2.485 39.844 0 33.583 0 31.533c0-1.081.226-1.992.452-1.992.565 0 .565.057 23.553 48.382 10.675 22.426 20.785 43.544 22.479 47.016 1.695 3.472 3.22 6.659 3.333 7.115.113.512-3.785 4.439-9.998 9.961-5.591 5.008-10.505 9.562-10.957 10.074-1.299 1.594-3.219 1.082-6.665-1.707Zm1.921-65.458c-2.599-5.066-2.712-5.123-9.828-5.464-6.27-.342-6.383-.285-6.383.911 0 .683-.226 1.593-.508 2.049-.339.512-.113 1.423.678 2.675l1.242 1.935h5.649c3.106.057 6.664.285 7.907.512 1.243.228 2.316.342 2.429.285.113-.057-.452-1.366-1.186-2.903Zm-4.745-9.107c-.452-1.195-1.638-3.7-2.598-5.578-1.581-3.188-1.751-3.301-2.146-1.992-.226.797-.396 3.13-.452 5.236-.057 4.155-.17 4.098 4.575 4.383l1.525.057-.904-2.106Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
<path d="M23.892 136.835c-1.017-.74-1.299-1.48-1.299-3.358 0-2.22.169-2.562 1.694-3.188 1.525-.626 1.92-.569 3.671.626 2.316 1.594 2.373 1.992.678 4.554-1.468 2.22-2.937 2.618-4.744 1.366Zm3.219-2.049c.904-1.594.339-2.789-1.355-2.789-1.525 0-2.203 1.536-1.356 3.073.678 1.253 1.977 1.139 2.711-.284ZM59.306 124.028c0-.285-.339-.569-.735-.569-.339 0-1.299-1.594-2.033-3.529-2.259-5.92-24.852-50.943-24.908-49.52 0 .74-.339 1.252-.904 1.252-.791 0-.904-.456-.565-2.675.339-2.562.113-3.131-7.907-18.841-4.519-8.936-9.376-18.271-10.788-20.775-1.469-2.619-2.598-5.465-2.711-6.66-.17-2.049.056-2.334 4.97-6.603 2.824-2.504 6.439-5.635 8.02-7.058C28.862 2.504 32.194-.114 33.098.057c1.356.228 22.31 22.369 22.367 23.622 0 .569-1.017 9.221-2.259 19.238-2.147 17.076-4.18 37.055-3.954 38.99.169 1.196-.678 7.229-1.299 9.847-.509 2.05-.283 2.903 3.784 12.238 2.372 5.521 5.479 12.295 6.834 15.027 1.299 2.732 2.429 5.123 2.429 5.294 0 .17-.395.284-.847.284-.452 0-.847-.228-.847-.569ZM46.315 81.509c.621-3.984 1.864-13.547 2.767-21.231 1.751-14.116 3.785-29.769 4.349-33.753.339-1.993.113-2.391-3.558-6.489-6.382-7.229-13.16-14.344-15.476-16.165l-2.146-1.708-11.014 10.359C11.07 21.971 10.223 22.939 10.844 24.077c.339.626 3.22 5.92 6.383 11.725 3.163 5.806 7.342 13.547 9.263 17.19 1.977 3.7 3.784 6.887 4.123 7.058.395.228.508-5.521.395-17.759-.226-18.271-.169-18.328 1.638-17.929.226 0 .396 9.221.396 20.434v20.377l5.93 11.953c3.276 6.603 5.987 11.896 6.1 11.84.113-.058.678-3.416 1.243-7.457Z" style="fill-rule:nonzero;stroke:#000;stroke-width:2px" transform="matrix(1.01351 0 0 -1 9.088 166.517)" />
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

7
dev-docs/tsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
// This file is not used in compilation. It is here just for a nice editor experience.
"extends": "@tsconfig/docusaurus/tsconfig.json",
"compilerOptions": {
"baseUrl": "."
}
}

7489
dev-docs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -23,17 +23,19 @@
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5",
"@tldraw/vec": "1.4.3",
"@tldraw/vec": "1.7.1",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "17.0.39",
"@types/react-dom": "17.0.11",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1",
"clsx": "1.1.1",
"cross-env": "7.0.3",
"fake-indexeddb": "3.1.7",
"firebase": "8.3.3",
"i18next-browser-languagedetector": "6.1.2",
"http-server": "14.1.1",
"i18next-browser-languagedetector": "6.1.4",
"idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1",
"jotai": "1.6.4",
@@ -47,8 +49,8 @@
"png-chunks-extract": "1.0.0",
"points-on-curve": "0.2.0",
"pwacompat": "2.0.17",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-scripts": "4.0.3",
"roughjs": "4.5.2",
"sass": "1.51.0",
@@ -59,11 +61,11 @@
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/lodash.throttle": "4.1.6",
"@types/lodash.throttle": "4.1.7",
"@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.6",
"@types/resize-observer-browser": "0.1.7",
"chai": "4.3.6",
"dotenv": "10.0.0",
"dotenv": "16.0.1",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "3.3.1",
"husky": "7.0.4",
@@ -71,7 +73,7 @@
"lint-staged": "12.3.7",
"pepjs": "0.5.3",
"prettier": "2.6.2",
"rewire": "5.0.0"
"rewire": "6.0.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.10.2"
@@ -91,10 +93,11 @@
"private": true,
"scripts": {
"build-node": "node ./scripts/build-node.js",
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:app:docker": "cross-env REACT_APP_DISABLE_SENTRY=true react-scripts build",
"build:app": "cross-env REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js",
"build": "yarn build:app && yarn build:version",
"build:prebuild": "node ./scripts/prebuild.js",
"build": "yarn build:prebuild && yarn build:app && yarn build:version",
"eject": "react-scripts eject",
"fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write",
@@ -104,6 +107,7 @@
"prepare": "husky install",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "react-scripts start",
"start:build": "npm run build && npx http-server build -a localhost -p 3001 -o",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",
"test:app": "react-scripts test --passWithNoTests",
"test:code": "eslint --max-warnings=0 --ext .js,.ts,.tsx .",
@@ -112,6 +116,8 @@
"test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app",
"autorelease": "node scripts/autorelease.js"
"autorelease": "node scripts/autorelease.js",
"prerelease": "node scripts/prerelease.js",
"release": "node scripts/release.js"
}
}

View File

@@ -98,6 +98,22 @@
/>
<link rel="stylesheet" href="fonts.css" type="text/css" />
<% if (process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD === "true") { %>
<script>
{
const _WebSocket = window.WebSocket;
window.WebSocket = function (url) {
if (/ws:\/\/localhost:.+?\/sockjs-node/.test(url)) {
console.info(
"[!!!] Live reload is disabled via process.env.REACT_APP_DEV_DISABLE_LIVE_RELOAD [!!!]",
);
} else {
return new _WebSocket(url);
}
};
}
</script>
<% } %>
<script>
window.EXCALIDRAW_ASSET_PATH = "/";
// setting this so that libraries installation reuses this window tab.

View File

@@ -17,11 +17,23 @@
* See https://goo.gl/2aRDsh
*/
importScripts("/workbox/workbox-sw.js");
// in dev, `process` is undefined because this file is not compiled until build
const IS_DEVELOPMENT =
typeof process === "undefined" || process.env.NODE_ENV !== "production";
workbox.setConfig({
modulePathPrefix: "/workbox/",
});
if (IS_DEVELOPMENT) {
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js",
);
workbox.setConfig({
debug: true,
});
} else {
importScripts("/workbox/workbox-sw.js");
workbox.setConfig({
modulePathPrefix: "/workbox/",
});
}
self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") {
@@ -30,14 +42,17 @@ self.addEventListener("message", (event) => {
});
workbox.core.clientsClaim();
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL("./index.html"),
{
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
},
);
if (!IS_DEVELOPMENT) {
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
workbox.routing.registerNavigationRoute(
workbox.precaching.getCacheKeyForURL("./index.html"),
{
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
},
);
}
// Cache relevant font files
workbox.routing.registerRoute(

View File

@@ -5,22 +5,25 @@ const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const isPreview = process.argv.slice(2)[0] === "preview";
const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim();
};
const publish = () => {
const tag = isPreview ? "preview" : "next";
try {
execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
console.info("Published 🎉");
execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
console.info(`Published ${pkg.name}@${tag}🎉`);
core.setOutput(
"result",
`**Preview version has been shipped** :rocket:
You can use [@excalidraw/excalidraw-preview@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw-preview/v/${pkg.version}) for testing!`,
You can use [@excalidraw/excalidraw@${pkg.version}](https://www.npmjs.com/package/@excalidraw/excalidraw/v/${pkg.version}) for testing!`,
);
} catch (error) {
core.setOutput("result", "package couldn't be published :warning:!");
@@ -51,27 +54,19 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
}
// update package.json
pkg.name = "@excalidraw/excalidraw-next";
let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
const isPreview = process.argv.slice(2)[0] === "preview";
if (isPreview) {
// use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`;
// replace "excalidraw-next" with "excalidraw-preview"
pkg.name = "@excalidraw/excalidraw-preview";
data = data.replace(/excalidraw-next/g, "excalidraw-preview");
data = data.trim();
}
pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
console.info("Publish in progress...");
publish();
});

21
scripts/buildDocs.js Normal file
View File

@@ -0,0 +1,21 @@
const { exec } = require("child_process");
// 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 docFiles = changedFiles.filter((file) => {
return file.indexOf("docs") >= 0;
});
if (!docFiles.length) {
console.info("Skipping building docs as no valid diff found");
process.exit(0);
}
// Exit code 1 to build the docs in ignoredBuildStep
process.exit(1);
});

View File

@@ -36,6 +36,7 @@ const crowdinMap = {
"ru-RU": "en-ru",
"si-LK": "en-silk",
"sk-SK": "en-sk",
"sl-SI": "en-sl",
"sv-SE": "en-sv",
"ta-IN": "en-ta",
"tr-TR": "en-tr",
@@ -47,6 +48,8 @@ const crowdinMap = {
"lv-LV": "en-lv",
"cs-CZ": "en-cs",
"kk-KZ": "en-kk",
"vi-vn": "en-vi",
"mr-in": "en-mr",
};
const flags = {
@@ -86,6 +89,7 @@ const flags = {
"ru-RU": "🇷🇺",
"si-LK": "🇱🇰",
"sk-SK": "🇸🇰",
"sl-SI": "🇸🇮",
"sv-SE": "🇸🇪",
"ta-IN": "🇮🇳",
"tr-TR": "🇹🇷",
@@ -93,6 +97,9 @@ const flags = {
"zh-CN": "🇨🇳",
"zh-HK": "🇭🇰",
"zh-TW": "🇹🇼",
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
};
const languages = {
@@ -133,6 +140,7 @@ const languages = {
"ru-RU": "Русский",
"si-LK": "සිංහල",
"sk-SK": "Slovenčina",
"sl-SI": "Slovenščina",
"sv-SE": "Svenska",
"ta-IN": "Tamil",
"tr-TR": "Türkçe",
@@ -140,6 +148,8 @@ const languages = {
"zh-CN": "简体中文",
"zh-HK": "繁體中文 (香港)",
"zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
};
const percentages = fs.readFileSync(

23
scripts/prebuild.js Normal file
View File

@@ -0,0 +1,23 @@
const fs = require("fs");
const path = require("path");
// for development purposes we want to have the service-worker.js file
// accessible from the public folder. On build though, we need to compile it
// and CRA expects that file to be in src/ folder.
const moveServiceWorkerScript = () => {
const oldPath = path.resolve(__dirname, "../public/service-worker.js");
const newPath = path.resolve(__dirname, "../src/service-worker.js");
fs.rename(oldPath, newPath, (error) => {
if (error) {
throw error;
}
console.info("public/service-worker.js moved to src/");
});
};
// -----------------------------------------------------------------------------
if (process.env.CI) {
moveServiceWorkerScript();
}

37
scripts/prerelease.js Normal file
View File

@@ -0,0 +1,37 @@
const fs = require("fs");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
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 prerelease = async (nextVersion) => {
try {
await updateChangelog(nextVersion);
updatePackageVersion(nextVersion);
await exec(`git add -u`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
console.info("Done!");
} catch (error) {
console.error(error);
process.exit(1);
}
};
const nextVersion = process.argv.slice(2)[0];
if (!nextVersion) {
console.error("Pass the next version to release!");
process.exit(1);
}
prerelease(nextVersion);

View File

@@ -1,39 +1,44 @@
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 { execSync } = require("child_process");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
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 originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
const updateReadme = () => {
const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
// remove note for stable readme
const data = originalReadMe.slice(excalidrawIndex);
// update readme
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
};
const release = async (nextVersion) => {
const publish = () => {
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!");
execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
} catch (error) {
console.error(error);
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);
const release = () => {
updateReadme();
console.info("Note for stable readme removed");
publish();
console.info(`Published ${pkg.version}!`);
// revert readme after release
fs.writeFileSync(`${excalidrawDir}/README.md`, originalReadMe, "utf8");
console.info("Readme reverted");
};
release();

View File

@@ -1,27 +0,0 @@
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

@@ -42,7 +42,7 @@ export const actionAddToLibrary = register({
commitToHistory: false,
appState: {
...appState,
toastMessage: t("toast.addedToLibrary"),
toast: { message: t("toast.addedToLibrary") },
},
};
})

View File

@@ -107,14 +107,16 @@ export const actionCopyAsPng = register({
return {
appState: {
...appState,
toastMessage: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
toast: {
message: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length
? t("toast.selection")
: t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode
? t("buttons.darkMode")
: t("buttons.lightMode"),
}),
},
},
commitToHistory: false,
};

View File

@@ -128,12 +128,15 @@ const duplicateElements = (
{
...appState,
selectedGroupIds: {},
selectedElementIds: newElements.reduce((acc, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
}, {} as any),
selectedElementIds: newElements.reduce(
(acc: Record<ExcalidrawElement["id"], true>, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{},
),
},
getNonDeletedElements(finalElements),
),

View File

@@ -144,13 +144,15 @@ export const actionSaveToActiveFile = register({
appState: {
...appState,
fileHandle,
toastMessage: fileHandleExists
? fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved")
toast: fileHandleExists
? {
message: fileHandle?.name
? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`,
)
: t("toast.fileSaved"),
}
: null,
},
};

View File

@@ -13,7 +13,7 @@ import {
maybeBindLinearElement,
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement } from "../element/typeChecks";
import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { AppState } from "../types";
export const actionFinalize = register({
@@ -181,6 +181,11 @@ export const actionFinalize = register({
[multiPointElement.id]: true,
}
: appState.selectedElementIds,
// To select the linear element when user has finished mutipoint editing
selectedLinearElement:
multiPointElement && isLinearElement(multiPointElement)
? new LinearElementEditor(multiPointElement, scene)
: appState.selectedLinearElement,
pendingImageElementId: null,
},
commitToHistory: appState.activeTool.type === "freedraw",

View File

@@ -2,29 +2,43 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements, isTextElement } from "../element";
import { ExcalidrawElement } from "../element/types";
import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor";
export const actionSelectAll = register({
name: "selectAll",
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
perform: (elements, appState, value, app) => {
if (appState.editingLinearElement) {
return false;
}
const selectedElementIds = elements.reduce(
(map: Record<ExcalidrawElement["id"], true>, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
!element.locked
) {
map[element.id] = true;
}
return map;
},
{},
);
return {
appState: selectGroupsForSelectedElements(
{
...appState,
selectedLinearElement:
// single linear element selected
Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene)
: null,
editingGroupId: null,
selectedElementIds: elements.reduce((map, element) => {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId) &&
element.locked === false
) {
map[element.id] = true;
}
return map;
}, {} as any),
selectedElementIds,
},
getNonDeletedElements(elements),
),

View File

@@ -36,7 +36,7 @@ export const actionCopyStyles = register({
return {
appState: {
...appState,
toastMessage: t("toast.copyStyles"),
toast: { message: t("toast.copyStyles") },
},
commitToHistory: false,
};

View File

@@ -17,16 +17,19 @@ export const actionToggleLock = register({
const operation = getOperation(selectedElements);
const selectedElementsMap = arrayToMap(selectedElements);
const lock = operation === "lock";
return {
elements: elements.map((element) => {
if (!selectedElementsMap.has(element.id)) {
return element;
}
return newElementWith(element, { locked: operation === "lock" });
return newElementWith(element, { locked: lock });
}),
appState,
appState: {
...appState,
selectedLinearElement: lock ? null : appState.selectedLinearElement,
},
commitToHistory: true,
};
},

View File

@@ -81,7 +81,7 @@ export const getDefaultAppState = (): Omit<
showStats: false,
startBoundElement: null,
suggestedBindings: [],
toastMessage: null,
toast: null,
viewBackgroundColor: oc.white,
zenModeEnabled: false,
zoom: {
@@ -90,6 +90,7 @@ export const getDefaultAppState = (): Omit<
viewModeEnabled: false,
pendingImageElementId: null,
showHyperlinkPopup: false,
selectedLinearElement: null,
};
};
@@ -101,228 +102,91 @@ const APP_STATE_STORAGE_CONF = (<
Values extends {
/** whether to keep when storing to browser storage (localStorage/IDB) */
browser: boolean;
/** whether to keep when exporting to a text file */
text: boolean;
/** whether to keep when exporting to an image file */
image: boolean;
/** whether to keep when exporting to file/database */
export: boolean;
/** server (shareLink/collab/...) */
server: boolean;
},
T extends Record<keyof AppState, Values>,
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
config)({
theme: { browser: true, text: false, image: false, server: false },
collaborators: { browser: false, text: false, image: false, server: false },
currentChartType: { browser: true, text: false, image: false, server: false },
currentItemBackgroundColor: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemEndArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFillStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontFamily: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemFontSize: {
browser: true,
text: false,
image: false,
server: false,
},
theme: { browser: true, export: false, server: false },
collaborators: { browser: false, export: false, server: false },
currentChartType: { browser: true, export: false, server: false },
currentItemBackgroundColor: { browser: true, export: false, server: false },
currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false },
currentItemFontFamily: { browser: true, export: false, server: false },
currentItemFontSize: { browser: true, export: false, server: false },
currentItemLinearStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemOpacity: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemRoughness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStartArrowhead: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeColor: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeSharpness: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeStyle: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemStrokeWidth: {
browser: true,
text: false,
image: false,
server: false,
},
currentItemTextAlign: {
browser: true,
text: false,
image: false,
server: false,
},
cursorButton: { browser: true, text: false, image: false, server: false },
draggingElement: { browser: false, text: false, image: false, server: false },
editingElement: { browser: false, text: false, image: false, server: false },
editingGroupId: { browser: true, text: false, image: false, server: false },
editingLinearElement: {
browser: false,
text: false,
image: false,
server: false,
},
activeTool: { browser: true, text: false, image: false, server: false },
penMode: { browser: true, text: false, image: false, server: false },
penDetected: { browser: true, text: false, image: false, server: false },
errorMessage: { browser: false, text: false, image: false, server: false },
exportBackground: { browser: true, text: false, image: true, server: false },
exportEmbedScene: { browser: true, text: false, image: true, server: false },
exportScale: { browser: true, text: false, image: true, server: false },
exportWithDarkMode: {
browser: true,
text: false,
image: true,
server: false,
},
fileHandle: { browser: false, text: false, image: false, server: false },
gridSize: { browser: true, text: true, image: true, server: true },
height: { browser: false, text: false, image: false, server: false },
isBindingEnabled: {
browser: false,
text: false,
image: false,
server: false,
},
isLibraryOpen: { browser: true, text: false, image: false, server: false },
isLibraryMenuDocked: {
browser: true,
text: false,
image: false,
server: false,
},
isLoading: { browser: false, text: false, image: false, server: false },
isResizing: { browser: false, text: false, image: false, server: false },
isRotating: { browser: false, text: false, image: false, server: false },
lastPointerDownWith: {
browser: true,
text: false,
image: false,
server: false,
},
multiElement: { browser: false, text: false, image: false, server: false },
name: { browser: true, text: false, image: false, server: false },
offsetLeft: { browser: false, text: false, image: false, server: false },
offsetTop: { browser: false, text: false, image: false, server: false },
openMenu: { browser: true, text: false, image: false, server: false },
openPopup: { browser: false, text: false, image: false, server: false },
pasteDialog: { browser: false, text: false, image: false, server: false },
previousSelectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
resizingElement: { browser: false, text: false, image: false, server: false },
scrolledOutside: { browser: true, text: false, image: false, server: false },
scrollX: { browser: true, text: false, image: false, server: false },
scrollY: { browser: true, text: false, image: false, server: false },
selectedElementIds: {
browser: true,
text: false,
image: false,
server: false,
},
selectedGroupIds: { browser: true, text: false, image: false, server: false },
selectionElement: {
browser: false,
text: false,
image: false,
server: false,
},
shouldCacheIgnoreZoom: {
browser: true,
text: false,
image: false,
server: false,
},
showHelpDialog: { browser: false, text: false, image: false, server: false },
showStats: { browser: true, text: false, image: false, server: false },
startBoundElement: {
browser: false,
text: false,
image: false,
server: false,
},
suggestedBindings: {
browser: false,
text: false,
image: false,
server: false,
},
toastMessage: { browser: false, text: false, image: false, server: false },
viewBackgroundColor: {
browser: true,
text: true,
image: true,
server: true,
},
width: { browser: false, text: false, image: false, server: false },
zenModeEnabled: { browser: true, text: false, image: false, server: false },
zoom: { browser: true, text: false, image: false, server: false },
viewModeEnabled: { browser: false, text: false, image: false, server: false },
pendingImageElementId: {
browser: false,
text: false,
image: false,
server: false,
},
showHyperlinkPopup: {
browser: false,
text: false,
image: false,
export: false,
server: false,
},
currentItemOpacity: { browser: true, export: false, server: false },
currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false },
currentItemStrokeSharpness: { browser: true, export: false, server: false },
currentItemStrokeStyle: { browser: true, export: false, server: false },
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
activeTool: { browser: true, export: false, server: false },
penMode: { browser: true, export: false, server: false },
penDetected: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { browser: true, export: false, server: false },
exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false },
isLibraryOpen: { browser: true, export: false, server: false },
isLibraryMenuDocked: { browser: true, export: false, server: false },
isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false },
lastPointerDownWith: { browser: true, export: false, server: false },
multiElement: { browser: false, export: false, server: false },
name: { browser: true, export: false, server: false },
offsetLeft: { browser: false, export: false, server: false },
offsetTop: { browser: false, export: false, server: false },
openMenu: { browser: true, export: false, server: false },
openPopup: { browser: false, export: false, server: false },
pasteDialog: { browser: false, export: false, server: false },
previousSelectedElementIds: { browser: true, export: false, server: false },
resizingElement: { browser: false, export: false, server: false },
scrolledOutside: { browser: true, export: false, server: false },
scrollX: { browser: true, export: false, server: false },
scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false },
selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
showHelpDialog: { browser: false, export: false, server: false },
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
toast: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false },
selectedLinearElement: { browser: true, export: false, server: false },
});
const _clearAppStateForStorage = <
ExportType extends "image" | "text" | "browser" | "server",
ExportType extends "export" | "browser" | "server",
>(
appState: Partial<AppState>,
exportType: ExportType,
@@ -349,12 +213,8 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "browser");
};
export const cleanAppStateForTextExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "text");
};
export const cleanAppStateForImageExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "image");
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export");
};
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,7 @@ import "./Card.scss";
export const Card: React.FC<{
color: keyof OpenColor | "primary";
children?: React.ReactNode;
}> = ({ children, color }) => {
return (
<div

View File

@@ -8,6 +8,7 @@ export const CheckboxItem: React.FC<{
checked: boolean;
onChange: (checked: boolean, event: React.MouseEvent) => void;
className?: string;
children?: React.ReactNode;
}> = ({ children, checked, onChange, className }) => {
return (
<div

View File

@@ -18,13 +18,15 @@
left: -5px;
}
min-width: 1em;
min-height: 1em;
line-height: 1;
position: absolute;
bottom: -5px;
padding: 3px;
border-radius: 50%;
background-color: $oc-green-6;
color: $oc-white;
font-size: 0.7em;
font-family: var(--ui-font);
font-size: 0.6em;
font-family: "Cascadia";
}
}

View File

@@ -28,7 +28,7 @@ const CollabButton = ({
aria-label={t("labels.liveCollaboration")}
showAriaLabel={useDevice().isMobile}
>
{collaboratorCount > 0 && (
{isCollaborating && (
<div className="CollabButton-collaborators">{collaboratorCount}</div>
)}
</ToolButton>

View File

@@ -85,6 +85,7 @@ export const Dialog = (props: DialogProps) => {
<button
className="Modal__close"
onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")}
>
{useDevice().isMobile ? back : close}

View File

@@ -58,6 +58,7 @@ const ExportButton: React.FC<{
onClick: () => void;
title: string;
shade?: number;
children?: React.ReactNode;
}> = ({ children, title, onClick, color, shade = 6 }) => {
return (
<button
@@ -170,7 +171,9 @@ const ImageExportModal = ({
<Stack.Row gap={2}>
{actionManager.renderAction("changeExportScale")}
</Stack.Row>
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p>
<p style={{ marginLeft: "1em", userSelect: "none" }}>
{t("buttons.scale")}
</p>
</div>
<div
style={{

View File

@@ -39,6 +39,7 @@ import { trackEvent } from "../analytics";
import { useDevice } from "../components/App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import { actionToggleZenMode } from "../actions";
interface LayerUIProps {
actionManager: ActionManager;
@@ -51,16 +52,13 @@ interface LayerUIProps {
onLockToggle: () => void;
onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
zenModeEnabled: boolean;
showExitZenModeBtn: boolean;
showThemeBtn: boolean;
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
renderCustomFooter?: ExcalidrawProps["renderFooter"];
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"];
focusContainer: () => void;
@@ -79,15 +77,12 @@ const LayerUI = ({
onLockToggle,
onPenModeToggle,
onInsertElements,
zenModeEnabled,
showExitZenModeBtn,
showThemeBtn,
toggleZenMode,
isCollaborating,
renderTopRightUI,
renderCustomFooter,
renderCustomStats,
viewModeEnabled,
libraryReturnUrl,
UIOptions,
focusContainer,
@@ -171,7 +166,7 @@ const LayerUI = ({
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled,
"transition-left": appState.zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
@@ -192,7 +187,7 @@ const LayerUI = ({
<Section
heading="canvasActions"
className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled,
"transition-left": appState.zenModeEnabled,
})}
>
{/* the zIndex ensures this menu has higher stacking order,
@@ -232,7 +227,7 @@ const LayerUI = ({
<Section
heading="selectedShapeActions"
className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled,
"transition-left": appState.zenModeEnabled,
})}
>
<Island
@@ -302,32 +297,34 @@ const LayerUI = ({
<div className="App-menu App-menu_top">
<Stack.Col
gap={4}
className={clsx({ "disable-pointerEvents": zenModeEnabled })}
className={clsx({
"disable-pointerEvents": appState.zenModeEnabled,
})}
>
{viewModeEnabled
{appState.viewModeEnabled
? renderViewModeCanvasActions()
: renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col>
{!viewModeEnabled && (
{!appState.viewModeEnabled && (
<Section heading="shapes">
{(heading) => (
{(heading: React.ReactNode) => (
<Stack.Col gap={4} align="start">
<Stack.Row
gap={1}
className={clsx("App-toolbar-container", {
"zen-mode": zenModeEnabled,
"zen-mode": appState.zenModeEnabled,
})}
>
<PenModeButton
zenModeEnabled={zenModeEnabled}
zenModeEnabled={appState.zenModeEnabled}
checked={appState.penMode}
onChange={onPenModeToggle}
title={t("toolBar.penMode")}
penDetected={appState.penDetected}
/>
<LockButton
zenModeEnabled={zenModeEnabled}
zenModeEnabled={appState.zenModeEnabled}
checked={appState.activeTool.locked}
onChange={() => onLockToggle()}
title={t("toolBar.lock")}
@@ -335,7 +332,7 @@ const LayerUI = ({
<Island
padding={1}
className={clsx("App-toolbar", {
"zen-mode": zenModeEnabled,
"zen-mode": appState.zenModeEnabled,
})}
>
<HintViewer
@@ -371,7 +368,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__top-right zen-mode-transition",
{
"transition-right": zenModeEnabled,
"transition-right": appState.zenModeEnabled,
},
)}
>
@@ -396,7 +393,8 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-left zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
},
)}
>
@@ -408,12 +406,12 @@ const LayerUI = ({
zoom={appState.zoom}
/>
</Island>
{!viewModeEnabled && (
{!appState.viewModeEnabled && (
<>
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
@@ -423,20 +421,20 @@ const LayerUI = ({
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{!viewModeEnabled &&
{!appState.viewModeEnabled &&
appState.multiElement &&
device.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("finalize", { size: "small" })}
@@ -450,7 +448,7 @@ const LayerUI = ({
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
appState.zenModeEnabled,
},
)}
>
@@ -460,7 +458,7 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-right zen-mode-transition",
{
"transition-right disable-pointerEvents": zenModeEnabled,
"transition-right disable-pointerEvents": appState.zenModeEnabled,
},
)}
>
@@ -470,7 +468,7 @@ const LayerUI = ({
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={toggleZenMode}
onClick={() => actionManager.executeAction(actionToggleZenMode)}
>
{t("buttons.exitZenMode")}
</button>
@@ -543,7 +541,6 @@ const LayerUI = ({
canvas={canvas}
isCollaborating={isCollaborating}
renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}

View File

@@ -224,7 +224,7 @@ export const LibraryMenu = ({
}, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = useCallback(
(data, libraryItems: LibraryItems) => {
(data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice();

View File

@@ -36,7 +36,6 @@ type MobileMenuProps = {
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
viewModeEnabled: boolean;
showThemeBtn: boolean;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
@@ -60,7 +59,6 @@ export const MobileMenu = ({
canvas,
isCollaborating,
renderCustomFooter,
viewModeEnabled,
showThemeBtn,
onImageAction,
renderTopRightUI,
@@ -70,7 +68,7 @@ export const MobileMenu = ({
return (
<FixedSideContainer side="top" className="App-top-bar">
<Section heading="shapes">
{(heading) => (
{(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1} className="App-toolbar-container">
<Island padding={1} className="App-toolbar">
@@ -125,7 +123,7 @@ export const MobileMenu = ({
!appState.editingElement &&
getSelectedElements(elements, appState).length === 0;
if (viewModeEnabled) {
if (appState.viewModeEnabled) {
return (
<div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")}
@@ -151,7 +149,7 @@ export const MobileMenu = ({
};
const renderCanvasActions = () => {
if (viewModeEnabled) {
if (appState.viewModeEnabled) {
return (
<>
{renderJSONExportDialog()}
@@ -185,7 +183,7 @@ export const MobileMenu = ({
};
return (
<>
{!viewModeEnabled && renderToolbar()}
{!appState.viewModeEnabled && renderToolbar()}
{renderStats()}
<div
className="App-bottom-bar"
@@ -216,7 +214,7 @@ export const MobileMenu = ({
</div>
</Section>
) : appState.openMenu === "shape" &&
!viewModeEnabled &&
!appState.viewModeEnabled &&
showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions
@@ -229,18 +227,20 @@ export const MobileMenu = ({
) : null}
<footer className="App-toolbar">
{renderAppToolbar()}
{appState.scrolledOutside && !appState.openMenu && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
{appState.scrolledOutside &&
!appState.openMenu &&
!appState.isLibraryOpen && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</footer>
</Island>
</div>

View File

@@ -8,7 +8,7 @@ import { useExcalidrawContainer, useDevice } from "./App";
import { AppState } from "../types";
import { THEME } from "../constants";
export const Modal = (props: {
export const Modal: React.FC<{
className?: string;
children: React.ReactNode;
maxWidth?: number;
@@ -16,7 +16,7 @@ export const Modal = (props: {
labelledBy: string;
theme?: AppState["theme"];
closeOnClickOutside?: boolean;
}) => {
}> = (props) => {
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
const modalRoot = useBodyRoot(theme);

View File

@@ -46,7 +46,7 @@ const ChartPreviewBtn = (props: {
},
null, // files
);
previewNode.replaceChildren();
previewNode.appendChild(svg);
if (props.selected) {
@@ -55,7 +55,7 @@ const ChartPreviewBtn = (props: {
})();
return () => {
previewNode.removeChild(svg);
previewNode.replaceChildren();
};
}, [props.spreadsheet, props.chartType, props.selected]);

View File

@@ -2,5 +2,6 @@
.popover {
position: absolute;
z-index: 10;
padding: 5px 0 5px;
}
}

View File

@@ -69,12 +69,26 @@ export const Popover = ({
if (fitInViewport && popoverRef.current) {
const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect();
//Position correctly when clicked on rightmost part or the bottom part of viewport
if (x + width - offsetLeft > viewportWidth) {
element.style.left = `${viewportWidth - width}px`;
element.style.left = `${viewportWidth - width - 10}px`;
}
if (y + height - offsetTop > viewportHeight) {
element.style.top = `${viewportHeight - height}px`;
}
//Resize to fit viewport on smaller screens
if (height >= viewportHeight) {
element.style.height = `${viewportHeight - 20}px`;
element.style.top = "10px";
element.style.overflowY = "scroll";
}
if (width >= viewportWidth) {
element.style.width = `${viewportWidth}px`;
element.style.left = "0px";
element.style.overflowX = "scroll";
}
}
}, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);

View File

@@ -2,12 +2,11 @@ import React from "react";
import { t } from "../i18n";
import { useExcalidrawContainer } from "./App";
interface SectionProps extends React.HTMLProps<HTMLElement> {
export const Section: React.FC<{
heading: string;
children: React.ReactNode | ((header: React.ReactNode) => React.ReactNode);
}
export const Section = ({ heading, children, ...props }: SectionProps) => {
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
className?: string;
}> = ({ heading, children, ...props }) => {
const { id } = useExcalidrawContainer();
const header = (
<h2 className="visually-hidden" id={`${id}-${heading}-title`}>

View File

@@ -2,6 +2,9 @@
.excalidraw {
.Toast {
$closeButtonSize: 1.2rem;
$closeButtonPadding: 0.4rem;
animation: fade-in 0.5s;
background-color: var(--button-gray-1);
border-radius: 4px;
@@ -15,11 +18,24 @@
text-align: center;
width: 300px;
z-index: 999999;
}
.Toast__message {
color: var(--popup-text-color);
white-space: pre-wrap;
.Toast__message {
padding: 0 $closeButtonSize + ($closeButtonPadding);
color: var(--popup-text-color);
white-space: pre-wrap;
}
.close {
position: absolute;
top: 0;
right: 0;
padding: $closeButtonPadding;
.ToolIcon__icon {
width: $closeButtonSize;
height: $closeButtonSize;
}
}
}
@keyframes fade-in {

View File

@@ -1,34 +1,59 @@
import { useCallback, useEffect, useRef } from "react";
import { TOAST_TIMEOUT } from "../constants";
import { close } from "./icons";
import "./Toast.scss";
import { ToolButton } from "./ToolButton";
const DEFAULT_TOAST_TIMEOUT = 5000;
export const Toast = ({
message,
clearToast,
onClose,
closable = false,
// To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT,
}: {
message: string;
clearToast: () => void;
onClose: () => void;
closable?: boolean;
duration?: number;
}) => {
const timerRef = useRef<number>(0);
const scheduleTimeout = useCallback(
() =>
(timerRef.current = window.setTimeout(() => clearToast(), TOAST_TIMEOUT)),
[clearToast],
);
const shouldAutoClose = duration !== Infinity;
const scheduleTimeout = useCallback(() => {
if (!shouldAutoClose) {
return;
}
timerRef.current = window.setTimeout(() => onClose(), duration);
}, [onClose, duration, shouldAutoClose]);
useEffect(() => {
if (!shouldAutoClose) {
return;
}
scheduleTimeout();
return () => clearTimeout(timerRef.current);
}, [scheduleTimeout, message]);
}, [scheduleTimeout, message, duration, shouldAutoClose]);
const onMouseEnter = shouldAutoClose
? () => clearTimeout(timerRef?.current)
: undefined;
const onMouseLeave = shouldAutoClose ? scheduleTimeout : undefined;
return (
<div
className="Toast"
onMouseEnter={() => clearTimeout(timerRef?.current)}
onMouseLeave={scheduleTimeout}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<p className="Toast__message">{message}</p>
{closable && (
<ToolButton
icon={close}
aria-label="close"
type="icon"
onClick={onClose}
className="close"
/>
)}
</div>
);
};

View File

@@ -212,16 +212,14 @@
}
}
.ToolIcon.ToolIcon__library {
top: 100px;
top: calc(var(--sat) + 100px);
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: 0;
top: 60px;
top: calc(var(--sat) + 60px);
}
.ToolIcon.ToolIcon__penMode {
margin-inline-end: 0;
top: 140px;
top: calc(var(--sat) + 140px);
}
}

View File

@@ -32,7 +32,6 @@
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}

View File

@@ -116,7 +116,6 @@ export const IMAGE_RENDER_TIMEOUT = 500;
export const TAP_TWICE_TIMEOUT = 300;
export const TOUCH_CTX_MENU_TIMEOUT = 500;
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;

View File

@@ -1,42 +0,0 @@
import React from "react";
export const createInverseContext = <T extends unknown = null>(
initialValue: T,
) => {
const Context = React.createContext(initialValue) as React.Context<T> & {
_updateProviderValue?: (value: T) => void;
};
class InverseConsumer extends React.Component {
state = { value: initialValue };
constructor(props: any) {
super(props);
Context._updateProviderValue = (value: T) => this.setState({ value });
}
render() {
return (
<Context.Provider value={this.state.value}>
{this.props.children}
</Context.Provider>
);
}
}
class InverseProvider extends React.Component<{ value: T }> {
componentDidMount() {
Context._updateProviderValue?.(this.props.value);
}
componentDidUpdate() {
Context._updateProviderValue?.(this.props.value);
}
render() {
return <Context.Consumer>{() => this.props.children}</Context.Consumer>;
}
}
return {
Context,
Consumer: InverseConsumer,
Provider: InverseProvider,
};
};

View File

@@ -1,5 +1,5 @@
import { nanoid } from "nanoid";
import { cleanAppStateForImageExport } from "../appState";
import { cleanAppStateForExport } from "../appState";
import { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement, FileId } from "../element/types";
@@ -143,7 +143,7 @@ export const loadSceneOrLibraryFromBlob = async (
appState: {
theme: localAppState?.theme,
fileHandle: fileHandle || blob.handle || null,
...cleanAppStateForImageExport(data.appState || {}),
...cleanAppStateForExport(data.appState || {}),
...(localAppState
? calculateScrollCenter(
data.elements || [],

View File

@@ -1,5 +1,4 @@
import {
FileWithHandle,
fileOpen as _fileOpen,
fileSave as _fileSave,
FileSystemHandle,
@@ -26,13 +25,9 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
description: string;
multiple?: M;
}): Promise<
M extends false | undefined ? FileWithHandle : FileWithHandle[]
> => {
}): Promise<M extends false | undefined ? File : File[]> => {
// an unsafe TS hack, alas not much we can do AFAIK
type RetType = M extends false | undefined
? FileWithHandle
: FileWithHandle[];
type RetType = M extends false | undefined ? File : File[];
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
mimeTypes.push(MIME_TYPES[type]);

View File

@@ -82,7 +82,7 @@ export const exportCanvas = async (
await import(/* webpackChunkName: "image" */ "./image")
).encodePngMetadata({
blob,
metadata: serializeAsJSON(elements, appState, files, "image"),
metadata: serializeAsJSON(elements, appState, files, "local"),
});
}

View File

@@ -1,9 +1,5 @@
import { fileOpen, fileSave } from "./filesystem";
import {
cleanAppStateForImageExport,
cleanAppStateForTextExport,
clearAppStateForDatabase,
} from "../appState";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import {
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
@@ -47,32 +43,25 @@ export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
appState: Partial<AppState>,
files: BinaryFiles,
destination: "text" | "image" | "database",
type: "local" | "database",
): string => {
const cleanAppState = () => {
switch (destination) {
case "database":
return clearAppStateForDatabase(appState);
case "text":
return cleanAppStateForTextExport(appState);
case "image":
return cleanAppStateForImageExport(appState);
}
};
const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw,
version: VERSIONS.excalidraw,
source: EXPORT_SOURCE,
elements:
destination === "database"
? clearElementsForDatabase(elements)
: clearElementsForExport(elements),
appState: cleanAppState(),
type === "local"
? clearElementsForExport(elements)
: clearElementsForDatabase(elements),
appState:
type === "local"
? cleanAppStateForExport(appState)
: clearAppStateForDatabase(appState),
files:
destination === "database"
? // will be stripped from JSON
undefined
: filterOutDeletedFiles(elements, files),
type === "local"
? filterOutDeletedFiles(elements, files)
: // will be stripped from JSON
undefined,
};
return JSON.stringify(data, null, 2);
@@ -83,7 +72,7 @@ export const saveAsJSON = async (
appState: AppState,
files: BinaryFiles,
) => {
const serialized = serializeAsJSON(elements, appState, files, "text");
const serialized = serializeAsJSON(elements, appState, files, "local");
const blob = new Blob([serialized], {
type: MIME_TYPES.excalidraw,
});
@@ -109,7 +98,12 @@ export const loadFromJSON = async (
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
// extensions: ["json", "excalidraw", "png", "svg"],
});
return loadFromBlob(await normalizeFile(file), localAppState, localElements);
return loadFromBlob(
await normalizeFile(file),
localAppState,
localElements,
file.handle,
);
};
export const isValidExcalidrawData = (data?: {

View File

@@ -67,13 +67,14 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
};
const restoreElementWithProperties = <
T extends ExcalidrawElement,
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>(
element: Required<T> & {
T extends Required<Omit<ExcalidrawElement, "customData">> & {
customData?: ExcalidrawElement["customData"];
/** @deprecated */
boundElementIds?: readonly ExcalidrawElement["id"][];
},
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>(
element: T,
extra: Pick<
T,
// This extra Pick<T, keyof K> ensure no excess properties are passed.
@@ -115,6 +116,10 @@ const restoreElementWithProperties = <
locked: element.locked ?? false,
};
if ("customData" in element) {
base.customData = element.customData;
}
return {
...base,
...getNormalizedDimensions(base),

View File

@@ -5,7 +5,7 @@ import {
LibraryItems,
LibraryItems_anyVersion,
} from "../types";
import type { cleanAppStateForTextExport } from "../appState";
import type { cleanAppStateForExport } from "../appState";
import { VERSIONS } from "../constants";
export interface ExportedDataState {
@@ -13,7 +13,7 @@ export interface ExportedDataState {
version: number;
source: string;
elements: readonly ExcalidrawElement[];
appState: ReturnType<typeof cleanAppStateForTextExport>;
appState: ReturnType<typeof cleanAppStateForExport>;
files: BinaryFiles | undefined;
}

View File

@@ -18,6 +18,7 @@ import { rescalePoints } from "../points";
// x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [number, number, number, number];
type MaybeQuadraticSolution = [number | null, number | null] | false;
// If the element is created from right to left, the width is going to be negative
// This set of functions retrieves the absolute position of the 4 points.
@@ -68,11 +69,95 @@ export const getCurvePathOps = (shape: Drawable): Op[] => {
return shape.sets[0].ops;
};
// reference: https://eliot-jones.com/2019/12/cubic-bezier-curve-bounding-boxes
const getBezierValueForT = (
t: number,
p0: number,
p1: number,
p2: number,
p3: number,
) => {
const oneMinusT = 1 - t;
return (
Math.pow(oneMinusT, 3) * p0 +
3 * Math.pow(oneMinusT, 2) * t * p1 +
3 * oneMinusT * Math.pow(t, 2) * p2 +
Math.pow(t, 3) * p3
);
};
const solveQuadratic = (
p0: number,
p1: number,
p2: number,
p3: number,
): MaybeQuadraticSolution => {
const i = p1 - p0;
const j = p2 - p1;
const k = p3 - p2;
const a = 3 * i - 6 * j + 3 * k;
const b = 6 * j - 6 * i;
const c = 3 * i;
const sqrtPart = b * b - 4 * a * c;
const hasSolution = sqrtPart >= 0;
if (!hasSolution) {
return false;
}
const t1 = (-b + Math.sqrt(sqrtPart)) / (2 * a);
const t2 = (-b - Math.sqrt(sqrtPart)) / (2 * a);
let s1 = null;
let s2 = null;
if (t1 >= 0 && t1 <= 1) {
s1 = getBezierValueForT(t1, p0, p1, p2, p3);
}
if (t2 >= 0 && t2 <= 1) {
s2 = getBezierValueForT(t2, p0, p1, p2, p3);
}
return [s1, s2];
};
const getCubicBezierCurveBound = (
p0: Point,
p1: Point,
p2: Point,
p3: Point,
): Bounds => {
const solX = solveQuadratic(p0[0], p1[0], p2[0], p3[0]);
const solY = solveQuadratic(p0[1], p1[1], p2[1], p3[1]);
let minX = Math.min(p0[0], p3[0]);
let maxX = Math.max(p0[0], p3[0]);
if (solX) {
const xs = solX.filter((x) => x !== null) as number[];
minX = Math.min(minX, ...xs);
maxX = Math.max(maxX, ...xs);
}
let minY = Math.min(p0[1], p3[1]);
let maxY = Math.max(p0[1], p3[1]);
if (solY) {
const ys = solY.filter((y) => y !== null) as number[];
minY = Math.min(minY, ...ys);
maxY = Math.max(maxY, ...ys);
}
return [minX, minY, maxX, maxY];
};
const getMinMaxXYFromCurvePathOps = (
ops: Op[],
transformXY?: (x: number, y: number) => [number, number],
): [number, number, number, number] => {
let currentP: Point = [0, 0];
const { minX, minY, maxX, maxY } = ops.reduce(
(limits, { op, data }) => {
// There are only four operation types:
@@ -83,38 +168,29 @@ const getMinMaxXYFromCurvePathOps = (
// move operation does not draw anything; so, it always
// returns false
} else if (op === "bcurveTo") {
// create points from bezier curve
// bezier curve stores data as a flattened array of three positions
// [x1, y1, x2, y2, x3, y3]
const p1 = [data[0], data[1]] as Point;
const p2 = [data[2], data[3]] as Point;
const p3 = [data[4], data[5]] as Point;
const _p1 = [data[0], data[1]] as Point;
const _p2 = [data[2], data[3]] as Point;
const _p3 = [data[4], data[5]] as Point;
const p0 = currentP;
currentP = p3;
const p1 = transformXY ? transformXY(..._p1) : _p1;
const p2 = transformXY ? transformXY(..._p2) : _p2;
const p3 = transformXY ? transformXY(..._p3) : _p3;
const equation = (t: number, idx: number) =>
Math.pow(1 - t, 3) * p3[idx] +
3 * t * Math.pow(1 - t, 2) * p2[idx] +
3 * Math.pow(t, 2) * (1 - t) * p1[idx] +
p0[idx] * Math.pow(t, 3);
const p0 = transformXY ? transformXY(...currentP) : currentP;
currentP = _p3;
let t = 0;
while (t <= 1.0) {
let x = equation(t, 0);
let y = equation(t, 1);
if (transformXY) {
[x, y] = transformXY(x, y);
}
const [minX, minY, maxX, maxY] = getCubicBezierCurveBound(
p0,
p1,
p2,
p3,
);
limits.minY = Math.min(limits.minY, y);
limits.minX = Math.min(limits.minX, x);
limits.minX = Math.min(limits.minX, minX);
limits.minY = Math.min(limits.minY, minY);
limits.maxX = Math.max(limits.maxX, x);
limits.maxY = Math.max(limits.maxY, y);
t += 0.1;
}
limits.maxX = Math.max(limits.maxX, maxX);
limits.maxY = Math.max(limits.maxY, maxY);
} else if (op === "lineTo") {
// TODO: Implement this
} else if (op === "qcurveTo") {
@@ -124,7 +200,6 @@ const getMinMaxXYFromCurvePathOps = (
},
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity },
);
return [minX, minY, maxX, maxY];
};
@@ -420,6 +495,7 @@ export const getResizedElementAbsoluteCoords = (
element: ExcalidrawElement,
nextWidth: number,
nextHeight: number,
normalizePoints: boolean,
): [number, number, number, number] => {
if (!(isLinearElement(element) || isFreeDrawElement(element))) {
return [
@@ -433,7 +509,8 @@ export const getResizedElementAbsoluteCoords = (
const points = rescalePoints(
0,
nextWidth,
rescalePoints(1, nextHeight, element.points),
rescalePoints(1, nextHeight, element.points, normalizePoints),
normalizePoints,
);
let bounds: [number, number, number, number];

View File

@@ -35,6 +35,7 @@ import { getShapeForElement } from "../renderer/renderElement";
import { hasBoundTextElement, isImageElement } from "./typeChecks";
import { isTextElement } from ".";
import { isTransparent } from "../utils";
import { shouldShowBoundingBox } from "./transformHandles";
const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement,
@@ -64,7 +65,10 @@ export const hitTest = (
const threshold = 10 / appState.zoom.value;
const point: Point = [x, y];
if (isElementSelected(appState, element)) {
if (
isElementSelected(appState, element) &&
shouldShowBoundingBox([element], appState)
) {
return isPointHittingElementBoundingBox(element, point, threshold);
}

View File

@@ -105,15 +105,26 @@ export const dragNewElement = (
true */
widthAspectRatio?: number | null,
) => {
if (shouldMaintainAspectRatio) {
if (shouldMaintainAspectRatio && draggingElement.type !== "selection") {
if (widthAspectRatio) {
height = width / widthAspectRatio;
} else {
({ width, height } = getPerfectElementSize(
elementType,
width,
y < originY ? -height : height,
));
// Depending on where the cursor is at (x, y) relative to where the starting point is
// (originX, originY), we use ONLY width or height to control size increase.
// This allows the cursor to always "stick" to one of the sides of the bounding box.
if (Math.abs(y - originY) > Math.abs(x - originX)) {
({ width, height } = getPerfectElementSize(
elementType,
height,
x < originX ? -width : width,
));
} else {
({ width, height } = getPerfectElementSize(
elementType,
width,
y < originY ? -height : height,
));
}
if (height < 0) {
height = -height;

View File

@@ -53,6 +53,7 @@ export { textWysiwyg } from "./textWysiwyg";
export { redrawTextBoundingBox } from "./textElement";
export {
getPerfectElementSize,
getLockedLinearCursorAlignSize,
isInvisiblySmallElement,
resizePerfectLineForNWHandler,
getNormalizedDimensions,

View File

@@ -5,8 +5,15 @@ import {
PointBinding,
ExcalidrawBindableElement,
} from "./types";
import { distance2d, rotate, isPathALoop, getGridPoint } from "../math";
import { getElementAbsoluteCoords } from ".";
import {
distance2d,
rotate,
isPathALoop,
getGridPoint,
rotatePoint,
centerPoint,
} from "../math";
import { getElementAbsoluteCoords, getLockedLinearCursorAlignSize } from ".";
import { getElementPointsCoords } from "./bounds";
import { Point, AppState } from "../types";
import { mutateElement } from "./mutateElement";
@@ -20,26 +27,32 @@ import {
} from "./binding";
import { tupleToCoors } from "../utils";
import { isBindingElement } from "./typeChecks";
import { shouldRotateWithDiscreteAngle } from "../keys";
export class LinearElementEditor {
public elementId: ExcalidrawElement["id"] & {
public readonly elementId: ExcalidrawElement["id"] & {
_brand: "excalidrawLinearElementId";
};
/** indices */
public selectedPointsIndices: readonly number[] | null;
public readonly selectedPointsIndices: readonly number[] | null;
public pointerDownState: Readonly<{
public readonly pointerDownState: Readonly<{
prevSelectedPointsIndices: readonly number[] | null;
/** index */
lastClickedPoint: number;
}>;
/** whether you're dragging a point */
public isDragging: boolean;
public lastUncommittedPoint: Point | null;
public pointerOffset: Readonly<{ x: number; y: number }>;
public startBindingElement: ExcalidrawBindableElement | null | "keep";
public endBindingElement: ExcalidrawBindableElement | null | "keep";
public readonly isDragging: boolean;
public readonly lastUncommittedPoint: Point | null;
public readonly pointerOffset: Readonly<{ x: number; y: number }>;
public readonly startBindingElement:
| ExcalidrawBindableElement
| null
| "keep";
public readonly endBindingElement: ExcalidrawBindableElement | null | "keep";
public readonly hoverPointIndex: number;
public readonly midPointHovered: boolean;
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) {
this.elementId = element.id as string & {
@@ -58,13 +71,15 @@ export class LinearElementEditor {
prevSelectedPointsIndices: null,
lastClickedPoint: -1,
};
this.hoverPointIndex = -1;
this.midPointHovered = false;
}
// ---------------------------------------------------------------------------
// static methods
// ---------------------------------------------------------------------------
static POINT_HANDLE_SIZE = 20;
static POINT_HANDLE_SIZE = 10;
/**
* @param id the `elementId` from the instance of this class (so that we can
@@ -132,22 +147,20 @@ export class LinearElementEditor {
/** @returns whether point was dragged */
static handlePointDragging(
event: PointerEvent,
appState: AppState,
setState: React.Component<any, AppState>["setState"],
scenePointerX: number,
scenePointerY: number,
maybeSuggestBinding: (
element: NonDeleted<ExcalidrawLinearElement>,
pointSceneCoords: { x: number; y: number }[],
) => void,
linearElementEditor: LinearElementEditor,
): boolean {
if (!appState.editingLinearElement) {
if (!linearElementEditor) {
return false;
}
const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId, isDragging } =
editingLinearElement;
const { selectedPointsIndices, elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return false;
@@ -155,54 +168,71 @@ export class LinearElementEditor {
// point that's being dragged (out of all selected points)
const draggingPoint = element.points[
editingLinearElement.pointerDownState.lastClickedPoint
linearElementEditor.pointerDownState.lastClickedPoint
] as [number, number] | undefined;
if (selectedPointsIndices && draggingPoint) {
if (isDragging === false) {
setState({
editingLinearElement: {
...editingLinearElement,
isDragging: true,
},
});
}
if (
shouldRotateWithDiscreteAngle(event) &&
selectedPointsIndices.length === 1 &&
element.points.length > 1
) {
const selectedIndex = selectedPointsIndices[0];
const referencePoint =
element.points[selectedIndex === 0 ? 1 : selectedIndex - 1];
const newDraggingPointPosition = LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize,
);
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
referencePoint,
[scenePointerX, scenePointerY],
appState.gridSize,
);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
appState.gridSize,
)
: ([
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
] as const);
return {
index: pointIndex,
point: newPointPosition,
LinearElementEditor.movePoints(element, [
{
index: selectedIndex,
point: [width + referencePoint[0], height + referencePoint[1]],
isDragging:
selectedIndex ===
linearElementEditor.pointerDownState.lastClickedPoint,
},
]);
} else {
const newDraggingPointPosition = LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
appState.gridSize,
);
const deltaX = newDraggingPointPosition[0] - draggingPoint[0];
const deltaY = newDraggingPointPosition[1] - draggingPoint[1];
LinearElementEditor.movePoints(
element,
selectedPointsIndices.map((pointIndex) => {
const newPointPosition =
pointIndex ===
editingLinearElement.pointerDownState.lastClickedPoint,
};
}),
);
linearElementEditor.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
appState.gridSize,
)
: ([
element.points[pointIndex][0] + deltaX,
element.points[pointIndex][1] + deltaY,
] as const);
return {
index: pointIndex,
point: newPointPosition,
isDragging:
pointIndex ===
linearElementEditor.pointerDownState.lastClickedPoint,
};
}),
);
}
// suggest bindings for first and last point if selected
if (isBindingElement(element, false)) {
@@ -256,10 +286,12 @@ export class LinearElementEditor {
return editingLinearElement;
}
const bindings: Partial<
Pick<
InstanceType<typeof LinearElementEditor>,
"startBindingElement" | "endBindingElement"
const bindings: Mutable<
Partial<
Pick<
InstanceType<typeof LinearElementEditor>,
"startBindingElement" | "endBindingElement"
>
>
> = {};
@@ -327,34 +359,126 @@ export class LinearElementEditor {
};
}
static isHittingMidPoint = (
linearElementEditor: LinearElementEditor,
scenePointer: { x: number; y: number },
appState: AppState,
) => {
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return false;
}
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element,
appState.zoom,
scenePointer.x,
scenePointer.y,
);
if (clickedPointIndex >= 0) {
return false;
}
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
if (points.length >= 3) {
return false;
}
const midPoint = LinearElementEditor.getMidPoint(linearElementEditor);
if (midPoint) {
const threshold =
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value;
const distance = distance2d(
midPoint[0],
midPoint[1],
scenePointer.x,
scenePointer.y,
);
return distance <= threshold;
}
return false;
};
static getMidPoint(linearElementEditor: LinearElementEditor) {
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return null;
}
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
return centerPoint(points[0], points.at(-1)!);
}
static handlePointerDown(
event: React.PointerEvent<HTMLCanvasElement>,
appState: AppState,
setState: React.Component<any, AppState>["setState"],
history: History,
scenePointer: { x: number; y: number },
linearElementEditor: LinearElementEditor,
): {
didAddPoint: boolean;
hitElement: NonDeleted<ExcalidrawElement> | null;
linearElementEditor: LinearElementEditor | null;
isMidPoint: boolean;
} {
const ret: ReturnType<typeof LinearElementEditor["handlePointerDown"]> = {
didAddPoint: false,
hitElement: null,
linearElementEditor: null,
isMidPoint: false,
};
if (!appState.editingLinearElement) {
if (!linearElementEditor) {
return ret;
}
const { elementId } = appState.editingLinearElement;
const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return ret;
}
if (event.altKey) {
if (appState.editingLinearElement.lastUncommittedPoint == null) {
const hittingMidPoint = LinearElementEditor.isHittingMidPoint(
linearElementEditor,
scenePointer,
appState,
);
if (
LinearElementEditor.isHittingMidPoint(
linearElementEditor,
scenePointer,
appState,
)
) {
const midPoint = LinearElementEditor.getMidPoint(linearElementEditor);
if (midPoint) {
mutateElement(element, {
points: [
element.points[0],
LinearElementEditor.createPointAt(
element,
midPoint[0],
midPoint[1],
appState.gridSize,
),
...element.points.slice(1),
],
});
}
ret.didAddPoint = true;
ret.isMidPoint = true;
ret.linearElementEditor = {
...linearElementEditor,
selectedPointsIndices: element.points[1],
pointerDownState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1,
},
lastUncommittedPoint: null,
};
}
if (event.altKey && appState.editingLinearElement) {
if (linearElementEditor.lastUncommittedPoint == null) {
mutateElement(element, {
points: [
...element.points,
@@ -366,24 +490,23 @@ export class LinearElementEditor {
),
],
});
ret.didAddPoint = true;
}
history.resumeRecording();
setState({
editingLinearElement: {
...appState.editingLinearElement,
pointerDownState: {
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: -1,
},
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding(
scenePointer,
Scene.getScene(element)!,
),
ret.linearElementEditor = {
...linearElementEditor,
pointerDownState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: -1,
},
});
selectedPointsIndices: [element.points.length - 1],
lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding(
scenePointer,
Scene.getScene(element)!,
),
};
ret.didAddPoint = true;
return ret;
}
@@ -397,7 +520,7 @@ export class LinearElementEditor {
// if we clicked on a point, set the element as hitElement otherwise
// it would get deselected if the point is outside the hitbox area
if (clickedPointIndex > -1) {
if (clickedPointIndex >= 0 || hittingMidPoint) {
ret.hitElement = element;
} else {
// You might be wandering why we are storing the binding elements on
@@ -405,8 +528,7 @@ export class LinearElementEditor {
// from the end points of the `linearElement` - this is to allow disabling
// binding (which needs to happen at the point the user finishes moving
// the point).
const { startBindingElement, endBindingElement } =
appState.editingLinearElement;
const { startBindingElement, endBindingElement } = linearElementEditor;
if (isBindingEnabled(appState) && isBindingElement(element)) {
bindOrUnbindLinearElement(
element,
@@ -432,33 +554,28 @@ export class LinearElementEditor {
const nextSelectedPointsIndices =
clickedPointIndex > -1 || event.shiftKey
? event.shiftKey ||
appState.editingLinearElement.selectedPointsIndices?.includes(
clickedPointIndex,
)
linearElementEditor.selectedPointsIndices?.includes(clickedPointIndex)
? normalizeSelectedPoints([
...(appState.editingLinearElement.selectedPointsIndices || []),
...(linearElementEditor.selectedPointsIndices || []),
clickedPointIndex,
])
: [clickedPointIndex]
: null;
setState({
editingLinearElement: {
...appState.editingLinearElement,
pointerDownState: {
prevSelectedPointsIndices:
appState.editingLinearElement.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
},
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint
? {
x: scenePointer.x - targetPoint[0],
y: scenePointer.y - targetPoint[1],
}
: { x: 0, y: 0 },
ret.linearElementEditor = {
...linearElementEditor,
pointerDownState: {
prevSelectedPointsIndices: linearElementEditor.selectedPointsIndices,
lastClickedPoint: clickedPointIndex,
},
});
selectedPointsIndices: nextSelectedPointsIndices,
pointerOffset: targetPoint
? {
x: scenePointer.x - targetPoint[0],
y: scenePointer.y - targetPoint[1],
}
: { x: 0, y: 0 },
};
return ret;
}
@@ -466,13 +583,13 @@ export class LinearElementEditor {
event: React.PointerEvent<HTMLCanvasElement>,
scenePointerX: number,
scenePointerY: number,
editingLinearElement: LinearElementEditor,
linearElementEditor: LinearElementEditor,
gridSize: number | null,
): LinearElementEditor {
const { elementId, lastUncommittedPoint } = editingLinearElement;
const { elementId, lastUncommittedPoint } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId);
if (!element) {
return editingLinearElement;
return linearElementEditor;
}
const { points } = element;
@@ -482,15 +599,33 @@ export class LinearElementEditor {
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.deletePoints(element, [points.length - 1]);
}
return { ...editingLinearElement, lastUncommittedPoint: null };
return { ...linearElementEditor, lastUncommittedPoint: null };
}
const newPoint = LinearElementEditor.createPointAt(
element,
scenePointerX - editingLinearElement.pointerOffset.x,
scenePointerY - editingLinearElement.pointerOffset.y,
gridSize,
);
let newPoint: Point;
if (shouldRotateWithDiscreteAngle(event) && points.length >= 2) {
const lastCommittedPoint = points[points.length - 2];
const [width, height] = LinearElementEditor._getShiftLockedDelta(
element,
lastCommittedPoint,
[scenePointerX, scenePointerY],
gridSize,
);
newPoint = [
width + lastCommittedPoint[0],
height + lastCommittedPoint[1],
];
} else {
newPoint = LinearElementEditor.createPointAt(
element,
scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y,
gridSize,
);
}
if (lastPoint === lastUncommittedPoint) {
LinearElementEditor.movePoints(element, [
@@ -504,7 +639,7 @@ export class LinearElementEditor {
}
return {
...editingLinearElement,
...linearElementEditor,
lastUncommittedPoint: element.points[element.points.length - 1],
};
}
@@ -526,14 +661,14 @@ export class LinearElementEditor {
/** scene coords */
static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>,
) {
): Point[] {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
return element.points.map((point) => {
let { x, y } = element;
[x, y] = rotate(x + point[0], y + point[1], cx, cy, element.angle);
return [x, y];
return [x, y] as const;
});
}
@@ -577,7 +712,8 @@ export class LinearElementEditor {
x: number,
y: number,
) {
const pointHandles = this.getPointsGlobalCoordinates(element);
const pointHandles =
LinearElementEditor.getPointsGlobalCoordinates(element);
let idx = pointHandles.length;
// loop from right to left because points on the right are rendered over
// points on the left, thus should take precedence when clicking, if they
@@ -587,7 +723,7 @@ export class LinearElementEditor {
if (
distance2d(x, y, point[0], point[1]) * zoom.value <
// +1px to account for outline stroke
this.POINT_HANDLE_SIZE / 2 + 1
LinearElementEditor.POINT_HANDLE_SIZE + 1
) {
return idx;
}
@@ -775,9 +911,9 @@ export class LinearElementEditor {
if (selectedOriginPoint) {
offsetX =
selectedOriginPoint.point[0] - points[selectedOriginPoint.index][0];
selectedOriginPoint.point[0] + points[selectedOriginPoint.index][0];
offsetY =
selectedOriginPoint.point[1] - points[selectedOriginPoint.index][1];
selectedOriginPoint.point[1] + points[selectedOriginPoint.index][1];
}
const nextPoints = points.map((point, idx) => {
@@ -840,6 +976,33 @@ export class LinearElementEditor {
y: element.y + rotated[1],
});
}
private static _getShiftLockedDelta(
element: NonDeleted<ExcalidrawLinearElement>,
referencePoint: Point,
scenePointer: Point,
gridSize: number | null,
) {
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
element,
referencePoint,
);
const [gridX, gridY] = getGridPoint(
scenePointer[0],
scenePointer[1],
gridSize,
);
const { width, height } = getLockedLinearCursorAlignSize(
referencePointCoords[0],
referencePointCoords[1],
gridX,
gridY,
);
return rotatePoint([width, height], [0, 0], -element.angle);
}
}
const normalizeSelectedPoints = (

View File

@@ -198,6 +198,7 @@ const getAdjustedDimensions = (
element,
nextWidth,
nextHeight,
false,
);
const deltaX1 = (x1 - nextX1) / 2;
const deltaY1 = (y1 - nextY1) / 2;

View File

@@ -18,6 +18,7 @@ import {
getElementAbsoluteCoords,
getCommonBounds,
getResizedElementAbsoluteCoords,
getCommonBoundingBox,
} from "./bounds";
import {
isFreeDrawElement,
@@ -137,8 +138,10 @@ export const transformElements = (
transformHandleType === "se"
) {
resizeMultipleElements(
pointerDownState,
selectedElements,
transformHandleType,
shouldResizeFromCenter,
pointerX,
pointerY,
);
@@ -261,13 +264,15 @@ const rescalePointsInElement = (
element: NonDeletedExcalidrawElement,
width: number,
height: number,
normalizePoints: boolean,
) =>
isLinearElement(element) || isFreeDrawElement(element)
? {
points: rescalePoints(
0,
width,
rescalePoints(1, height, element.points),
rescalePoints(1, height, element.points, normalizePoints),
normalizePoints,
),
}
: {};
@@ -371,6 +376,7 @@ const resizeSingleTextElement = (
element,
nextWidth,
nextHeight,
false,
);
const deltaX1 = (x1 - nextX1) / 2;
const deltaY1 = (y1 - nextY1) / 2;
@@ -412,6 +418,7 @@ export const resizeSingleElement = (
stateAtResizeStart,
stateAtResizeStart.width,
stateAtResizeStart.height,
true,
);
const startTopLeft: Point = [x1, y1];
const startBottomRight: Point = [x2, y2];
@@ -429,6 +436,7 @@ export const resizeSingleElement = (
element,
element.width,
element.height,
true,
);
const boundsCurrentWidth = esx2 - esx1;
@@ -522,6 +530,7 @@ export const resizeSingleElement = (
stateAtResizeStart,
eleNewWidth,
eleNewHeight,
true,
);
const newBoundsWidth = newBoundsX2 - newBoundsX1;
const newBoundsHeight = newBoundsY2 - newBoundsY1;
@@ -592,6 +601,7 @@ export const resizeSingleElement = (
stateAtResizeStart,
eleNewWidth,
eleNewHeight,
true,
);
// For linear elements (x,y) are the coordinates of the first drawn point not the top-left corner
// So we need to readjust (x,y) to be where the first point should be
@@ -637,146 +647,147 @@ export const resizeSingleElement = (
};
const resizeMultipleElements = (
elements: readonly NonDeletedExcalidrawElement[],
pointerDownState: PointerDownState,
selectedElements: readonly NonDeletedExcalidrawElement[],
transformHandleType: "nw" | "ne" | "sw" | "se",
shouldResizeFromCenter: boolean,
pointerX: number,
pointerY: number,
) => {
const [x1, y1, x2, y2] = getCommonBounds(elements);
let scale: number;
let getNextXY: (
element: NonDeletedExcalidrawElement,
origCoords: readonly [number, number, number, number],
finalCoords: readonly [number, number, number, number],
) => { x: number; y: number };
switch (transformHandleType) {
case "se":
scale = Math.max(
(pointerX - x1) / (x2 - x1),
(pointerY - y1) / (y2 - y1),
);
getNextXY = (element, [origX1, origY1], [finalX1, finalY1]) => {
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
return { x, y };
};
break;
case "nw":
scale = Math.max(
(x2 - pointerX) / (x2 - x1),
(y2 - pointerY) / (y2 - y1),
);
getNextXY = (element, [, , origX2, origY2], [, , finalX2, finalY2]) => {
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
return { x, y };
};
break;
case "ne":
scale = Math.max(
(pointerX - x1) / (x2 - x1),
(y2 - pointerY) / (y2 - y1),
);
getNextXY = (element, [origX1, , , origY2], [finalX1, , , finalY2]) => {
const x = element.x + (origX1 - x1) * (scale - 1) + origX1 - finalX1;
const y = element.y - (y2 - origY2) * (scale - 1) + origY2 - finalY2;
return { x, y };
};
break;
case "sw":
scale = Math.max(
(x2 - pointerX) / (x2 - x1),
(pointerY - y1) / (y2 - y1),
);
getNextXY = (element, [, origY1, origX2], [, finalY1, finalX2]) => {
const x = element.x - (x2 - origX2) * (scale - 1) + origX2 - finalX2;
const y = element.y + (origY1 - y1) * (scale - 1) + origY1 - finalY1;
return { x, y };
};
break;
// map selected elements to the original elements. While it never should
// happen that pointerDownState.originalElements won't contain the selected
// elements during resize, this coupling isn't guaranteed, so to ensure
// type safety we need to transform only those elements we filter.
const targetElements = selectedElements.reduce(
(
acc: {
/** element at resize start */
orig: NonDeletedExcalidrawElement;
/** latest element */
latest: NonDeletedExcalidrawElement;
}[],
element,
) => {
const origElement = pointerDownState.originalElements.get(element.id);
if (origElement) {
acc.push({ orig: origElement, latest: element });
}
return acc;
},
[],
);
const { minX, minY, maxX, maxY, midX, midY } = getCommonBoundingBox(
targetElements.map(({ orig }) => orig),
);
const direction = transformHandleType;
const mapDirectionsToAnchors: Record<typeof direction, Point> = {
ne: [minX, maxY],
se: [minX, minY],
sw: [maxX, minY],
nw: [maxX, maxY],
};
// anchor point must be on the opposite side of the dragged selection handle
// or be the center of the selection if alt is pressed
const [anchorX, anchorY]: Point = shouldResizeFromCenter
? [midX, midY]
: mapDirectionsToAnchors[direction];
const mapDirectionsToPointerSides: Record<
typeof direction,
[x: boolean, y: boolean]
> = {
ne: [pointerX >= anchorX, pointerY <= anchorY],
se: [pointerX >= anchorX, pointerY >= anchorY],
sw: [pointerX <= anchorX, pointerY >= anchorY],
nw: [pointerX <= anchorX, pointerY <= anchorY],
};
// pointer side relative to anchor
const [pointerSideX, pointerSideY] = mapDirectionsToPointerSides[
direction
].map((condition) => (condition ? 1 : -1));
// stop resizing if a pointer is on the other side of selection
if (pointerSideX < 0 && pointerSideY < 0) {
return;
}
if (scale > 0) {
const updates = elements.reduce(
(prev, element) => {
if (!prev) {
return prev;
const scale =
Math.max(
(pointerSideX * Math.abs(pointerX - anchorX)) / (maxX - minX),
(pointerSideY * Math.abs(pointerY - anchorY)) / (maxY - minY),
) * (shouldResizeFromCenter ? 2 : 1);
if (scale === 1) {
return;
}
targetElements.forEach((element) => {
const width = element.orig.width * scale;
const height = element.orig.height * scale;
const x = anchorX + (element.orig.x - anchorX) * scale;
const y = anchorY + (element.orig.y - anchorY) * scale;
// readjust points for linear & free draw elements
const rescaledPoints = rescalePointsInElement(
element.orig,
width,
height,
false,
);
const update: {
width: number;
height: number;
x: number;
y: number;
points?: Point[];
fontSize?: number;
baseline?: number;
} = {
width,
height,
x,
y,
...rescaledPoints,
};
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
const boundTextElement = getBoundTextElement(element.latest);
if (boundTextElement || isTextElement(element.orig)) {
const optionalPadding = boundTextElement ? BOUND_TEXT_PADDING * 2 : 0;
const textMeasurements = measureFontSizeFromWH(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
width - optionalPadding,
height - optionalPadding,
);
if (textMeasurements) {
if (isTextElement(element.orig)) {
update.fontSize = textMeasurements.size;
update.baseline = textMeasurements.baseline;
}
const width = element.width * scale;
const height = element.height * scale;
const boundTextElement = getBoundTextElement(element);
let font: { fontSize?: number; baseline?: number } = {};
if (boundTextElement) {
const nextFont = measureFontSizeFromWH(
boundTextElement,
width - BOUND_TEXT_PADDING * 2,
height - BOUND_TEXT_PADDING * 2,
);
if (nextFont === null) {
return null;
}
font = {
fontSize: nextFont.size,
baseline: nextFont.baseline,
boundTextUpdates = {
fontSize: textMeasurements.size,
baseline: textMeasurements.baseline,
};
}
if (isTextElement(element)) {
const nextFont = measureFontSizeFromWH(element, width, height);
if (nextFont === null) {
return null;
}
font = { fontSize: nextFont.size, baseline: nextFont.baseline };
}
const origCoords = getElementAbsoluteCoords(element);
const rescaledPoints = rescalePointsInElement(element, width, height);
updateBoundElements(element, {
newSize: { width, height },
simultaneouslyUpdated: elements,
});
const finalCoords = getResizedElementAbsoluteCoords(
{
...element,
...rescaledPoints,
},
width,
height,
);
const { x, y } = getNextXY(element, origCoords, finalCoords);
return [...prev, { width, height, x, y, ...rescaledPoints, ...font }];
},
[] as
| {
width: number;
height: number;
x: number;
y: number;
points?: (readonly [number, number])[];
fontSize?: number;
baseline?: number;
}[]
| null,
);
if (updates) {
elements.forEach((element, index) => {
mutateElement(element, updates[index]);
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
mutateElement(boundTextElement, {
fontSize: updates[index].fontSize,
baseline: updates[index].baseline,
});
handleBindTextResize(element, transformHandleType);
}
});
}
}
}
mutateElement(element.latest, update);
if (boundTextElement && boundTextUpdates) {
mutateElement(boundTextElement, boundTextUpdates);
handleBindTextResize(element.latest, transformHandleType);
}
});
};
const rotateMultipleElements = (

View File

@@ -1,49 +1,51 @@
import { getPerfectElementSize } from "./sizeHelpers";
import * as constants from "../constants";
const EPSILON_DIGITS = 3;
describe("getPerfectElementSize", () => {
it("should return height:0 if `elementType` is line and locked angle is 0", () => {
const { height, width } = getPerfectElementSize("line", 149, 10);
expect(width).toEqual(149);
expect(height).toEqual(0);
expect(width).toBeCloseTo(149, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
});
it("should return width:0 if `elementType` is line and locked angle is 90 deg (Math.PI/2)", () => {
const { height, width } = getPerfectElementSize("line", 10, 140);
expect(width).toEqual(0);
expect(height).toEqual(140);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(140, EPSILON_DIGITS);
});
it("should return height:0 if `elementType` is arrow and locked angle is 0", () => {
const { height, width } = getPerfectElementSize("arrow", 200, 20);
expect(width).toEqual(200);
expect(height).toEqual(0);
expect(width).toBeCloseTo(200, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
});
it("should return width:0 if `elementType` is arrow and locked angle is 90 deg (Math.PI/2)", () => {
const { height, width } = getPerfectElementSize("arrow", 10, 100);
expect(width).toEqual(0);
expect(height).toEqual(100);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(100, EPSILON_DIGITS);
});
it("should return adjust height to be width * tan(locked angle)", () => {
const { height, width } = getPerfectElementSize("arrow", 120, 185);
expect(width).toEqual(120);
expect(height).toEqual(208);
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
expect(height).toBeCloseTo(207.846, EPSILON_DIGITS);
});
it("should return height equals to width if locked angle is 45 deg", () => {
const { height, width } = getPerfectElementSize("arrow", 135, 145);
expect(width).toEqual(135);
expect(height).toEqual(135);
expect(width).toBeCloseTo(135, EPSILON_DIGITS);
expect(height).toBeCloseTo(135, EPSILON_DIGITS);
});
it("should return height:0 and width:0 when width and height are 0", () => {
const { height, width } = getPerfectElementSize("arrow", 0, 0);
expect(width).toEqual(0);
expect(height).toEqual(0);
expect(width).toBeCloseTo(0, EPSILON_DIGITS);
expect(height).toBeCloseTo(0, EPSILON_DIGITS);
});
describe("should respond to SHIFT_LOCKING_ANGLE constant", () => {
it("should have only 2 locking angles per section if SHIFT_LOCKING_ANGLE = 45 deg (Math.PI/4)", () => {
(constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4;
const { height, width } = getPerfectElementSize("arrow", 120, 185);
expect(width).toEqual(120);
expect(height).toEqual(120);
expect(width).toBeCloseTo(120, EPSILON_DIGITS);
expect(height).toBeCloseTo(120, EPSILON_DIGITS);
});
});
});

View File

@@ -37,9 +37,7 @@ export const getPerfectElementSize = (
} else if (lockedAngle === Math.PI / 2) {
width = 0;
} else {
height =
Math.round(absWidth * Math.tan(lockedAngle)) * Math.sign(height) ||
height;
height = absWidth * Math.tan(lockedAngle) * Math.sign(height) || height;
}
} else if (elementType !== "selection") {
height = absWidth * Math.sign(height);
@@ -47,6 +45,46 @@ export const getPerfectElementSize = (
return { width, height };
};
export const getLockedLinearCursorAlignSize = (
originX: number,
originY: number,
x: number,
y: number,
) => {
let width = x - originX;
let height = y - originY;
const lockedAngle =
Math.round(Math.atan(height / width) / SHIFT_LOCKING_ANGLE) *
SHIFT_LOCKING_ANGLE;
if (lockedAngle === 0) {
height = 0;
} else if (lockedAngle === Math.PI / 2) {
width = 0;
} else {
// locked angle line, y = mx + b => mx - y + b = 0
const a1 = Math.tan(lockedAngle);
const b1 = -1;
const c1 = originY - a1 * originX;
// line through cursor, perpendicular to locked angle line
const a2 = -1 / a1;
const b2 = -1;
const c2 = y - a2 * x;
// intersection of the two lines above
const intersectX = (b1 * c2 - b2 * c1) / (a1 * b2 - a2 * b1);
const intersectY = (c1 * a2 - c2 * a1) / (a1 * b2 - a2 * b1);
// delta
width = intersectX - originX;
height = intersectY - originY;
}
return { width, height };
};
export const resizePerfectLineForNWHandler = (
element: ExcalidrawElement,
x: number,

View File

@@ -1,9 +1,15 @@
import { ExcalidrawElement, PointerType } from "./types";
import {
ExcalidrawElement,
NonDeletedExcalidrawElement,
PointerType,
} from "./types";
import { getElementAbsoluteCoords, Bounds } from "./bounds";
import { rotate } from "../math";
import { Zoom } from "../types";
import { AppState, Zoom } from "../types";
import { isTextElement } from ".";
import { isLinearElement } from "./typeChecks";
import { DEFAULT_SPACING } from "../renderer/renderScene";
export type TransformHandleDirection =
| "n"
@@ -59,8 +65,6 @@ const OMIT_SIDES_FOR_LINE_BACKSLASH = {
s: true,
n: true,
w: true,
ne: true,
sw: true,
};
const generateTransformHandle = (
@@ -82,6 +86,7 @@ export const getTransformHandlesFromCoords = (
zoom: Zoom,
pointerType: PointerType,
omitSides: { [T in TransformHandleType]?: boolean } = {},
margin = 4,
): TransformHandles => {
const size = transformHandleSizes[pointerType];
const handleWidth = size / zoom.value;
@@ -94,9 +99,7 @@ export const getTransformHandlesFromCoords = (
const height = y2 - y1;
const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2;
const dashedLineMargin = 4 / zoom.value;
const dashedLineMargin = margin / zoom.value;
const centeringOffset = (size - 8) / (2 * zoom.value);
const transformHandles: TransformHandles = {
@@ -230,11 +233,7 @@ export const getTransformHandles = (
}
let omitSides: { [T in TransformHandleType]?: boolean } = {};
if (
element.type === "arrow" ||
element.type === "line" ||
element.type === "freedraw"
) {
if (element.type === "freedraw" || isLinearElement(element)) {
if (element.points.length === 2) {
// only check the last point because starting point is always (0,0)
const [, p1] = element.points;
@@ -253,12 +252,33 @@ export const getTransformHandles = (
} else if (isTextElement(element)) {
omitSides = OMIT_SIDES_FOR_TEXT_ELEMENT;
}
const dashedLineMargin = isLinearElement(element)
? DEFAULT_SPACING * 3
: DEFAULT_SPACING;
return getTransformHandlesFromCoords(
getElementAbsoluteCoords(element),
element.angle,
zoom,
pointerType,
omitSides,
dashedLineMargin,
);
};
export const shouldShowBoundingBox = (
elements: NonDeletedExcalidrawElement[],
appState: AppState,
) => {
if (appState.editingLinearElement) {
return false;
}
if (elements.length > 1) {
return true;
}
const element = elements[0];
if (!isLinearElement(element)) {
return true;
}
return element.points.length > 2;
};

View File

@@ -56,6 +56,7 @@ type _ExcalidrawElementBase = Readonly<{
updated: number;
link: string | null;
locked: boolean;
customData?: Record<string, any>;
}>;
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {

View File

@@ -19,7 +19,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
}, STORAGE_SIZE_TIMEOUT);
type Props = {
setToastMessage: (message: string) => void;
setToast: (message: string) => void;
};
const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({
@@ -68,7 +68,7 @@ const CustomStats = (props: Props) => {
onClick={async () => {
try {
await copyTextToSystemClipboard(getVersion());
props.setToastMessage(t("toast.copyToClipboard"));
props.setToast(t("toast.copyToClipboard"));
} catch {}
}}
title={t("stats.versionCopy")}

View File

@@ -8,10 +8,12 @@ import {
ExcalidrawElement,
InitializedExcalidrawImageElement,
} from "../../element/types";
import { getSceneVersion } from "../../packages/excalidraw/index";
import {
getSceneVersion,
restoreElements,
} from "../../packages/excalidraw/index";
import { Collaborator, Gesture } from "../../types";
import {
getFrame,
preventUnload,
resolvablePromise,
withBatchedUpdates,
@@ -47,11 +49,9 @@ import {
} from "../data/localStorage";
import Portal from "./Portal";
import RoomDialog from "./RoomDialog";
import { createInverseContext } from "../../createInverseContext";
import { t } from "../../i18n";
import { UserIdleState } from "../../types";
import { IDLE_THRESHOLD, ACTIVE_THRESHOLD } from "../../constants";
import { trackEvent } from "../../analytics";
import {
encodeFilesForUpload,
FileManager,
@@ -70,52 +70,45 @@ import {
import { decryptData } from "../../data/encryption";
import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai";
import { jotaiStore } from "../../jotai";
export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false);
export const isCollaboratingAtom = atom(false);
interface CollabState {
modalIsShown: boolean;
errorMessage: string;
username: string;
userState: UserIdleState;
activeRoomLink: string;
}
type CollabInstance = InstanceType<typeof CollabWrapper>;
type CollabInstance = InstanceType<typeof Collab>;
export interface CollabAPI {
/** function so that we can access the latest value from stale callbacks */
isCollaborating: () => boolean;
username: CollabState["username"];
userState: CollabState["userState"];
onPointerUpdate: CollabInstance["onPointerUpdate"];
initializeSocketClient: CollabInstance["initializeSocketClient"];
onCollabButtonClick: CollabInstance["onCollabButtonClick"];
startCollaboration: CollabInstance["startCollaboration"];
stopCollaboration: CollabInstance["stopCollaboration"];
syncElements: CollabInstance["syncElements"];
fetchImageFilesFromFirebase: CollabInstance["fetchImageFilesFromFirebase"];
setUsername: (username: string) => void;
}
interface Props {
interface PublicProps {
excalidrawAPI: ExcalidrawImperativeAPI;
onRoomClose?: () => void;
}
const {
Context: CollabContext,
Consumer: CollabContextConsumer,
Provider: CollabContextProvider,
} = createInverseContext<{ api: CollabAPI | null }>({ api: null });
type Props = PublicProps & { modalIsShown: boolean };
export { CollabContext, CollabContextConsumer };
class CollabWrapper extends PureComponent<Props, CollabState> {
class Collab extends PureComponent<Props, CollabState> {
portal: Portal;
fileManager: FileManager;
excalidrawAPI: Props["excalidrawAPI"];
activeIntervalId: number | null;
idleTimeoutId: number | null;
// marked as private to ensure we don't change it outside this class
private _isCollaborating: boolean = false;
private socketInitializationTimer?: number;
private lastBroadcastedOrReceivedSceneVersion: number = -1;
private collaborators = new Map<string, Collaborator>();
@@ -123,10 +116,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
constructor(props: Props) {
super(props);
this.state = {
modalIsShown: false,
errorMessage: "",
username: importUsernameFromLocalStorage() || "",
userState: UserIdleState.ACTIVE,
activeRoomLink: "",
};
this.portal = new Portal(this);
@@ -164,9 +155,22 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.addEventListener(EVENT.UNLOAD, this.onUnload);
const collabAPI: CollabAPI = {
isCollaborating: this.isCollaborating,
onPointerUpdate: this.onPointerUpdate,
startCollaboration: this.startCollaboration,
syncElements: this.syncElements,
fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase,
stopCollaboration: this.stopCollaboration,
setUsername: this.setUsername,
};
jotaiStore.set(collabAPIAtom, collabAPI);
if (
process.env.NODE_ENV === ENV.TEST ||
process.env.NODE_ENV === ENV.DEVELOPMENT
process.env.NODE_ENV === ENV.DEVELOPMENT ||
process.env.REACT_APP_VERCEL_ENV === "preview"
) {
window.collab = window.collab || ({} as Window["collab"]);
Object.defineProperties(window, {
@@ -196,7 +200,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}
}
isCollaborating = () => this._isCollaborating;
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
private setIsCollaborating = (isCollaborating: boolean) => {
jotaiStore.set(isCollaboratingAtom, isCollaborating);
};
private onUnload = () => {
this.destroySocketClient({ isUnload: true });
@@ -208,7 +216,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
);
if (
this._isCollaborating &&
this.isCollaborating() &&
(this.fileManager.shouldPreventUnload(syncableElements) ||
!isSavedToFirebase(this.portal, syncableElements))
) {
@@ -252,12 +260,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}
};
openPortal = async () => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
return this.initializeSocketClient(null);
};
closePortal = () => {
stopCollaboration = (keepRemoteState = true) => {
this.queueBroadcastAllElements.cancel();
this.queueSaveToFirebase.cancel();
this.loadImageFiles.cancel();
@@ -267,16 +270,26 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
if (this.portal.socket && this.fallbackInitializationHandler) {
this.portal.socket.off(
"connect_error",
this.fallbackInitializationHandler,
);
}
if (!keepRemoteState) {
LocalData.fileStorage.reset();
this.destroySocketClient();
} else if (window.confirm(t("alerts.collabStopOverridePrompt"))) {
// hack to ensure that we prefer we disregard any new browser state
// that could have been saved in other tabs while we were collaborating
resetBrowserStateVersions();
window.history.pushState({}, APP_NAME, window.location.origin);
this.destroySocketClient();
trackEvent("share", "room closed");
this.props.onRoomClose?.();
LocalData.fileStorage.reset();
const elements = this.excalidrawAPI
.getSceneElementsIncludingDeleted()
@@ -295,20 +308,20 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
};
private destroySocketClient = (opts?: { isUnload: boolean }) => {
this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close();
this.fileManager.reset();
if (!opts?.isUnload) {
this.setIsCollaborating(false);
this.setState({
activeRoomLink: "",
});
this.collaborators = new Map();
this.excalidrawAPI.updateScene({
collaborators: this.collaborators,
});
this.setState({
activeRoomLink: "",
});
this._isCollaborating = false;
LocalData.resumeSave("collaboration");
}
this.lastBroadcastedOrReceivedSceneVersion = -1;
this.portal.close();
this.fileManager.reset();
};
private fetchImageFilesFromFirebase = async (scene: {
@@ -349,7 +362,9 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}
};
private initializeSocketClient = async (
private fallbackInitializationHandler: null | (() => any) = null;
startCollaboration = async (
existingRoomLinkData: null | { roomId: string; roomKey: string },
): Promise<ImportedDataState | null> => {
if (this.portal.socket) {
@@ -372,13 +387,23 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
const scenePromise = resolvablePromise<ImportedDataState | null>();
this._isCollaborating = true;
this.setIsCollaborating(true);
LocalData.pauseSave("collaboration");
const { default: socketIOClient } = await import(
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
);
const fallbackInitializationHandler = () => {
this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
}).then((scene) => {
scenePromise.resolve(scene);
});
};
this.fallbackInitializationHandler = fallbackInitializationHandler;
try {
const socketServerData = await getCollabServer();
@@ -391,6 +416,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
roomId,
roomKey,
);
this.portal.socket.once("connect_error", fallbackInitializationHandler);
} catch (error: any) {
console.error(error);
this.setState({ errorMessage: error.message });
@@ -419,13 +446,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
// fallback in case you're not alone in the room but still don't receive
// initial SCENE_INIT message
this.socketInitializationTimer = window.setTimeout(() => {
this.initializeRoom({
roomLinkData: existingRoomLinkData,
fetchScene: true,
});
scenePromise.resolve(null);
}, INITIAL_SCENE_UPDATE_TIMEOUT);
this.socketInitializationTimer = window.setTimeout(
fallbackInitializationHandler,
INITIAL_SCENE_UPDATE_TIMEOUT,
);
// All socket listeners are moving to Portal
this.portal.socket.on(
@@ -530,6 +554,12 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
}
| { fetchScene: false; roomLinkData?: null }) => {
clearTimeout(this.socketInitializationTimer!);
if (this.portal.socket && this.fallbackInitializationHandler) {
this.portal.socket.off(
"connect_error",
this.fallbackInitializationHandler,
);
}
if (fetchScene && roomLinkData && this.portal.socket) {
this.excalidrawAPI.resetScene();
@@ -567,6 +597,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
remoteElements = restoreElements(remoteElements, null);
const reconciledElements = _reconcileElements(
localElements,
remoteElements,
@@ -672,19 +704,17 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
};
setCollaborators(sockets: string[]) {
this.setState((state) => {
const collaborators: InstanceType<typeof CollabWrapper>["collaborators"] =
new Map();
for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
collaborators.set(socketId, this.collaborators.get(socketId)!);
} else {
collaborators.set(socketId, {});
}
const collaborators: InstanceType<typeof Collab>["collaborators"] =
new Map();
for (const socketId of sockets) {
if (this.collaborators.has(socketId)) {
collaborators.set(socketId, this.collaborators.get(socketId)!);
} else {
collaborators.set(socketId, {});
}
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
});
}
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
}
public setLastBroadcastedOrReceivedSceneVersion = (version: number) => {
@@ -713,7 +743,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
);
onIdleStateChange = (userState: UserIdleState) => {
this.setState({ userState });
this.portal.broadcastIdleChange(userState);
};
@@ -747,18 +776,22 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
this.setLastBroadcastedOrReceivedSceneVersion(newVersion);
}, SYNC_FULL_SCENE_INTERVAL_MS);
queueSaveToFirebase = throttle(() => {
if (this.portal.socketInitialized) {
this.saveCollabRoomToFirebase(
getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
}
}, SYNC_FULL_SCENE_INTERVAL_MS);
queueSaveToFirebase = throttle(
() => {
if (this.portal.socketInitialized) {
this.saveCollabRoomToFirebase(
getSyncableElements(
this.excalidrawAPI.getSceneElementsIncludingDeleted(),
),
);
}
},
SYNC_FULL_SCENE_INTERVAL_MS,
{ leading: false },
);
handleClose = () => {
this.setState({ modalIsShown: false });
jotaiStore.set(collabDialogShownAtom, false);
};
setUsername = (username: string) => {
@@ -770,35 +803,10 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
saveUsernameToLocalStorage(username);
};
onCollabButtonClick = () => {
this.setState({
modalIsShown: true,
});
};
/** PRIVATE. Use `this.getContextValue()` instead. */
private contextValue: CollabAPI | null = null;
/** Getter of context value. Returned object is stable. */
getContextValue = (): CollabAPI => {
if (!this.contextValue) {
this.contextValue = {} as CollabAPI;
}
this.contextValue.isCollaborating = this.isCollaborating;
this.contextValue.username = this.state.username;
this.contextValue.onPointerUpdate = this.onPointerUpdate;
this.contextValue.initializeSocketClient = this.initializeSocketClient;
this.contextValue.onCollabButtonClick = this.onCollabButtonClick;
this.contextValue.syncElements = this.syncElements;
this.contextValue.fetchImageFilesFromFirebase =
this.fetchImageFilesFromFirebase;
this.contextValue.setUsername = this.setUsername;
return this.contextValue;
};
render() {
const { modalIsShown, username, errorMessage, activeRoomLink } = this.state;
const { username, errorMessage, activeRoomLink } = this.state;
const { modalIsShown } = this.props;
return (
<>
@@ -808,8 +816,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
activeRoomLink={activeRoomLink}
username={username}
onUsernameChange={this.onUsernameChange}
onRoomCreate={this.openPortal}
onRoomDestroy={this.closePortal}
onRoomCreate={() => this.startCollaboration(null)}
onRoomDestroy={this.stopCollaboration}
setErrorMessage={(errorMessage) => {
this.setState({ errorMessage });
}}
@@ -822,11 +830,6 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
onClose={() => this.setState({ errorMessage: "" })}
/>
)}
<CollabContextProvider
value={{
api: this.getContextValue(),
}}
/>
</>
);
}
@@ -834,7 +837,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
declare global {
interface Window {
collab: InstanceType<typeof CollabWrapper>;
collab: InstanceType<typeof Collab>;
}
}
@@ -845,4 +848,11 @@ if (
window.collab = window.collab || ({} as Window["collab"]);
}
export default CollabWrapper;
const _Collab: React.FC<PublicProps> = (props) => {
const [collabDialogShown] = useAtom(collabDialogShownAtom);
return <Collab {...props} modalIsShown={collabDialogShown} />;
};
export default _Collab;
export type TCollabClass = Collab;

View File

@@ -4,7 +4,7 @@ import {
SocketUpdateDataSource,
} from "../data";
import CollabWrapper from "./CollabWrapper";
import { TCollabClass } from "./Collab";
import { ExcalidrawElement } from "../../element/types";
import {
@@ -20,14 +20,14 @@ import { BroadcastedExcalidrawElement } from "./reconciliation";
import { encryptData } from "../../data/encryption";
class Portal {
collab: CollabWrapper;
collab: TCollabClass;
socket: SocketIOClient.Socket | null = null;
socketInitialized: boolean = false; // we don't want the socket to emit any updates until it is fully initialized
roomId: string | null = null;
roomKey: string | null = null;
broadcastedElementVersions: Map<string, number> = new Map();
constructor(collab: CollabWrapper) {
constructor(collab: TCollabClass) {
this.collab = collab;
}

View File

@@ -14,6 +14,8 @@ import { t } from "../../i18n";
import "./RoomDialog.scss";
import Stack from "../../components/Stack";
import { AppState } from "../../types";
import { trackEvent } from "../../analytics";
import { getFrame } from "../../utils";
const getShareIcon = () => {
const navigator = window.navigator as any;
@@ -95,7 +97,10 @@ const RoomDialog = ({
title={t("roomDialog.button_startSession")}
aria-label={t("roomDialog.button_startSession")}
showAriaLabel={true}
onClick={onRoomCreate}
onClick={() => {
trackEvent("share", "room creation", `ui (${getFrame()})`);
onRoomCreate();
}}
/>
</div>
</>
@@ -160,7 +165,10 @@ const RoomDialog = ({
title={t("roomDialog.button_stopSession")}
aria-label={t("roomDialog.button_stopSession")}
showAriaLabel={true}
onClick={onRoomDestroy}
onClick={() => {
trackEvent("share", "room closed");
onRoomDestroy();
}}
/>
</div>
</>

View File

@@ -134,9 +134,16 @@ export type SocketUpdateData =
_brand: "socketUpdateData";
};
const RE_COLLAB_LINK = /^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/;
export const isCollaborationLink = (link: string) => {
const hash = new URL(link).hash;
return RE_COLLAB_LINK.test(hash);
};
export const getCollaborationLinkData = (link: string) => {
const hash = new URL(link).hash;
const match = hash.match(/^#room=([a-zA-Z0-9_-]+),([a-zA-Z0-9_-]+)$/);
const match = hash.match(RE_COLLAB_LINK);
if (match && match[2].length !== 22) {
window.alert(t("alerts.invalidEncryptionKey"));
return null;

View File

@@ -1,5 +1,6 @@
import polyfill from "../polyfill";
import LanguageDetector from "i18next-browser-languagedetector";
import { useCallback, useContext, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { trackEvent } from "../analytics";
import { getDefaultAppState } from "../appState";
import { ErrorDialog } from "../components/ErrorDialog";
@@ -45,20 +46,26 @@ import {
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
import CollabWrapper, {
import Collab, {
CollabAPI,
CollabContext,
CollabContextConsumer,
} from "./collab/CollabWrapper";
collabAPIAtom,
collabDialogShownAtom,
isCollaboratingAtom,
} from "./collab/Collab";
import { LanguageList } from "./components/LanguageList";
import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
import {
exportToBackend,
getCollaborationLinkData,
isCollaborationLink,
loadScene,
} from "./data";
import {
getLibraryItemsFromStorage,
importFromLocalStorage,
importUsernameFromLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restoreAppState, RestoredDataState } from "../data/restore";
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
import { Tooltip } from "../components/Tooltip";
import { shield } from "../components/icons";
@@ -72,8 +79,14 @@ import { loadFilesFromFirebase } from "./data/firebase";
import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx";
import { Provider, useAtom } from "jotai";
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
polyfill();
window.EXCALIDRAW_THROTTLE_RENDER = true;
const isExcalidrawPlusSignedUser = document.cookie.includes(
COOKIES.AUTH_STATE_COOKIE,
);
@@ -85,6 +98,7 @@ languageDetector.init({
const initializeScene = async (opts: {
collabAPI: CollabAPI;
excalidrawAPI: ExcalidrawImperativeAPI;
}): Promise<
{ scene: ExcalidrawInitialDataState | null } & (
| { isExternalScene: true; id: string; key: string }
@@ -169,8 +183,28 @@ const initializeScene = async (opts: {
}
if (roomLinkData) {
const { excalidrawAPI } = opts;
const scene = await opts.collabAPI.startCollaboration(roomLinkData);
return {
scene: await opts.collabAPI.initializeSocketClient(roomLinkData),
// when collaborating, the state may have already been updated at this
// point (we may have received updates from other clients), so reconcile
// elements and appState with existing state
scene: {
...scene,
appState: {
...restoreAppState(scene?.appState, excalidrawAPI.getAppState()),
// necessary if we're invoking from a hashchange handler which doesn't
// go through App.initializeScene() that resets this flag
isLoading: false,
},
elements: reconcileElements(
scene?.elements || [],
excalidrawAPI.getSceneElementsIncludingDeleted(),
excalidrawAPI.getAppState(),
),
},
isExternalScene: true,
id: roomLinkData.roomId,
key: roomLinkData.roomKey,
@@ -242,7 +276,11 @@ const ExcalidrawWrapper = () => {
const [excalidrawAPI, excalidrawRefCallback] =
useCallbackRefState<ExcalidrawImperativeAPI>();
const collabAPI = useContext(CollabContext)?.api;
const [collabAPI] = useAtom(collabAPIAtom);
const [, setCollabDialogShown] = useAtom(collabDialogShownAtom);
const [isCollaborating] = useAtomWithInitialValue(isCollaboratingAtom, () => {
return isCollaborationLink(window.location.href);
});
useHandleLibrary({
excalidrawAPI,
@@ -320,7 +358,7 @@ const ExcalidrawWrapper = () => {
}
};
initializeScene({ collabAPI }).then((data) => {
initializeScene({ collabAPI, excalidrawAPI }).then(async (data) => {
loadImages(data, /* isInitialLoad */ true);
initialStatePromiseRef.current.promise.resolve(data.scene);
});
@@ -329,12 +367,21 @@ const ExcalidrawWrapper = () => {
event.preventDefault();
const libraryUrlTokens = parseLibraryTokensFromUrl();
if (!libraryUrlTokens) {
initializeScene({ collabAPI }).then((data) => {
if (
collabAPI.isCollaborating() &&
!isCollaborationLink(window.location.href)
) {
collabAPI.stopCollaboration(false);
}
excalidrawAPI.updateScene({ appState: { isLoading: true } });
initializeScene({ collabAPI, excalidrawAPI }).then((data) => {
loadImages(data);
if (data.scene) {
excalidrawAPI.updateScene({
...data.scene,
appState: restoreAppState(data.scene.appState, null),
...restore(data.scene, null, null),
commitToHistory: true,
});
}
});
@@ -622,7 +669,7 @@ const ExcalidrawWrapper = () => {
const renderCustomStats = () => {
return (
<CustomStats
setToastMessage={(message) => excalidrawAPI!.setToastMessage(message)}
setToast={(message) => excalidrawAPI!.setToast({ message })}
/>
);
};
@@ -636,23 +683,19 @@ const ExcalidrawWrapper = () => {
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
};
const onRoomClose = useCallback(() => {
LocalData.fileStorage.reset();
}, []);
return (
<div
style={{ height: "100%" }}
className={clsx("excalidraw-app", {
"is-collaborating": collabAPI?.isCollaborating(),
"is-collaborating": isCollaborating,
})}
>
<Excalidraw
ref={excalidrawRefCallback}
onChange={onChange}
initialData={initialStatePromiseRef.current.promise}
onCollabButtonClick={collabAPI?.onCollabButtonClick}
isCollaborating={collabAPI?.isCollaborating()}
onCollabButtonClick={() => setCollabDialogShown(true)}
isCollaborating={isCollaborating}
onPointerUpdate={collabAPI?.onPointerUpdate}
UIOptions={{
canvasActions: {
@@ -686,12 +729,7 @@ const ExcalidrawWrapper = () => {
onLibraryChange={onLibraryChange}
autoFocus={true}
/>
{excalidrawAPI && (
<CollabWrapper
excalidrawAPI={excalidrawAPI}
onRoomClose={onRoomClose}
/>
)}
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && (
<ErrorDialog
message={errorMessage}
@@ -705,9 +743,9 @@ const ExcalidrawWrapper = () => {
const ExcalidrawApp = () => {
return (
<TopErrorBoundary>
<CollabContextConsumer>
<Provider unstable_createStore={() => jotaiStore}>
<ExcalidrawWrapper />
</CollabContextConsumer>
</Provider>
</TopErrorBoundary>
);
};

4
src/global.d.ts vendored
View File

@@ -14,7 +14,11 @@ interface Window {
__EXCALIDRAW_SHA__: string | undefined;
EXCALIDRAW_ASSET_PATH: string | undefined;
EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function;
logTime: (name: string, time?: number) => void;
logTimeAverage: (name: string, time?: number) => void;
DEBUG_LOG_TIMES: boolean;
}
// https://github.com/facebook/create-react-app/blob/ddcb7d5/packages/react-scripts/lib/react-app.d.ts

View File

@@ -48,10 +48,13 @@ const allLanguages: Language[] = [
{ code: "ru-RU", label: "Русский" },
{ code: "sk-SK", label: "Slovenčina" },
{ code: "sv-SE", label: "Svenska" },
{ code: "sl-SI", label: "Slovenščina" },
{ code: "tr-TR", label: "Türkçe" },
{ code: "uk-UA", label: "Українська" },
{ code: "zh-CN", label: "简体中文" },
{ code: "zh-TW", label: "繁體中文" },
{ code: "vi-VN", label: "Tiếng Việt" },
{ code: "mr-IN", label: "मराठी" },
].concat([defaultLang]);
export const languages: Language[] = allLanguages
@@ -86,7 +89,7 @@ export const setLanguage = async (lang: Language) => {
currentLangData = {};
} else {
currentLangData = await import(
/* webpackChunkName: "i18n-[request]" */ `./locales/${currentLang.code}.json`
/* webpackChunkName: "locales/[request]" */ `./locales/${currentLang.code}.json`
);
}
};

View File

@@ -1,8 +1,14 @@
import ReactDOM from "react-dom";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import ExcalidrawApp from "./excalidraw-app";
import "./excalidraw-app/pwa";
import "./excalidraw-app/sentry";
window.__EXCALIDRAW_SHA__ = process.env.REACT_APP_GIT_SHA;
ReactDOM.render(<ExcalidrawApp />, document.getElementById("root"));
const rootElement = document.getElementById("root")!;
const root = createRoot(rootElement);
root.render(
<StrictMode>
<ExcalidrawApp />
</StrictMode>,
);

View File

@@ -1,4 +1,27 @@
import { unstable_createStore } from "jotai";
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
import { useLayoutEffect } from "react";
export const jotaiScope = Symbol();
export const jotaiStore = unstable_createStore();
export const useAtomWithInitialValue = <
T extends unknown,
A extends WritableAtom<T, T>,
>(
atom: A,
initialValue: T | (() => T),
) => {
const [value, setValue] = useAtom(atom);
useLayoutEffect(() => {
if (typeof initialValue === "function") {
// @ts-ignore
setValue(initialValue());
} else {
setValue(initialValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return [value, setValue] as const;
};

View File

@@ -80,5 +80,5 @@ export const shouldMaintainAspectRatio = (event: MouseEvent | KeyboardEvent) =>
event.shiftKey;
export const shouldRotateWithDiscreteAngle = (
event: MouseEvent | KeyboardEvent,
event: MouseEvent | KeyboardEvent | React.PointerEvent<HTMLCanvasElement>,
) => event.shiftKey;

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