Compare commits

..

192 Commits

Author SHA1 Message Date
Zsolt Viczian
e68e11ff9f update tests 2022-08-27 16:27:01 +02:00
zsviczian
b4750f4485 Update App.tsx 2022-08-27 16:13:36 +02:00
zsviczian
4426275184 exit updateDOMRect if width or height is zero 2022-08-27 15:54:39 +02:00
Aakansha Doshi
2b4462c941 refactor: reuse common ui dialogs and message for mobile and LayerUI (#5611)
* refactor: Move common UI dialogs to component

* refactor

* fix
2022-08-26 11:46:34 +05:30
Aakansha Doshi
43b13d8e3a fix: Add display name to components so it doesn't show as anonymous (#5616) 2022-08-26 11:46:19 +05:30
Ryan Di
720f468f39 fix: improve solveQuadratic when a = 0 (#5618) 2022-08-24 14:44:59 +08:00
Ryan Di
33300d19f6 fix: add random tiny offsets to avoid linear elements from being clipped (#5615)
Co-authored-by: Ryan Di <ryandi@Ryans-MacBook-Pro.local>
2022-08-23 15:52:15 +02:00
Aakansha Doshi
5aed159991 docs: fix refs table (#5614)
* docs: fix refs table

* fix

* fix

* fix

* fix
2022-08-23 13:55:43 +05:30
Aakansha Doshi
de1d221d1c docs: add PR link (#5613)
docs:add PR link
2022-08-23 11:51:45 +05:30
Aakansha Doshi
9a68dbffe2 docs: update docs for param defaultStatus in loadLibraryFromBlob (#5612) 2022-08-23 11:32:53 +05:30
Aakansha Doshi
32d82219b1 refactor: Stats component (#5610)
refactor: stats component
2022-08-22 17:18:25 +05:30
Aakansha Doshi
ba2c86fe1b refactor: Move footer to its own component (#5609) 2022-08-22 16:09:24 +05:30
zsviczian
f1ae37c84b fix: Crash when adding a new point in the line editor #5602 (#5606)
Update linearElementEditor.ts
2022-08-22 10:39:27 +05:30
Aakansha Doshi
ec350ba8b2 feat: Introduce ExcalidrawElements and ExcalidrawAppState provider (#5463)
* feat: Introduce ExcalidrawData provider so that app state and elements need not be passed to children

* fix

* fix zen mode

* Separate providers for data and elements

* pass appState and elements to layerUI

* pass appState and elements to selectedShapeActions

* pass appState and elements to MobileMenu

* pass appState to librarymenu

* rename

* rename to ExcalidrawAppState
2022-08-20 22:49:44 +05:30
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
David Luzar
120c8f373c fix: library not scrollable when no published items installed (#5352)
* fix: library not scrollable when no published items installed

* show empty lib message in one case & fix i18n
2022-06-25 20:10:53 +02:00
David Luzar
9135ebf2e2 feat: redirect vscode.excalidraw.com to vscode marketplace (#5285) 2022-06-23 17:42:50 +02:00
David Luzar
af31e9dcc2 fix: focus traps inside popovers (#5317) 2022-06-23 17:33:50 +02:00
David Luzar
50bc7e099a fix: unable to use cmd/ctrl-delete/backspace in inputs (#5348) 2022-06-23 17:27:15 +02:00
Aakansha Doshi
39d17c4a3c fix: delay loading until language imported (#5344) 2022-06-22 22:06:29 +05:30
Aakansha Doshi
d34c2a75db fix: command to trigger release (#5347) 2022-06-22 18:10:57 +05:30
Aakansha Doshi
de95c68d75 fix: remove unnecessary options passed to language detector (#5336) 2022-06-22 01:33:08 +05:30
Ishtiaq Bhatti
cdf352d4c3 feat: add sidebar for libraries panel (#5274)
Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-06-21 17:03:23 +02:00
David Luzar
4712393b62 fix: stale appState.pendingImageElement (#5322)
* fix: stale `appState.pendingImageElement`

* unrelated fix for devTools race conditions

* snap fix
2022-06-19 14:13:43 +02:00
David Luzar
fd48c2cf79 fix: non-letter shortcuts being swallowed by color picker (#5316) 2022-06-17 12:37:11 +02:00
David Luzar
5feacd9a3b feat: deduplicate collab avatars based on id (#5309) 2022-06-15 15:35:57 +02:00
Aakansha Doshi
ec35d5db51 fix: bind text to correct container when nested (#5307)
* fix: bind text to correct container when nested

* fix tests
2022-06-15 16:09:12 +05:30
dependabot[bot]
ddf088e428 chore(deps): bump eventsource from 1.1.0 to 1.1.1 (#5262)
Bumps [eventsource](https://github.com/EventSource/eventsource) from 1.1.0 to 1.1.1.
- [Release notes](https://github.com/EventSource/eventsource/releases)
- [Changelog](https://github.com/EventSource/eventsource/blob/master/HISTORY.md)
- [Commits](https://github.com/EventSource/eventsource/compare/v1.1.0...v1.1.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-14 22:04:35 +05:30
dependabot[bot]
adc1e585ff chore(deps): bump protobufjs from 6.10.2 to 6.11.3 (#5266)
Bumps [protobufjs](https://github.com/protobufjs/protobuf.js) from 6.10.2 to 6.11.3.
- [Release notes](https://github.com/protobufjs/protobuf.js/releases)
- [Changelog](https://github.com/protobufjs/protobuf.js/blob/v6.11.3/CHANGELOG.md)
- [Commits](https://github.com/protobufjs/protobuf.js/compare/v6.10.2...v6.11.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-14 22:04:01 +05:30
Aakansha Doshi
84b47a2ed5 fix: copy bound text style when copying element having bound text (#5305)
* fix: copy bound text style when copying element having bound text

* fix

* fix tests
2022-06-14 19:42:49 +05:30
Aakansha Doshi
6196fba286 docs: migrate example to typescript (#5243)
* docs: migrate example to typescript

* fix

* fix sidebar

* fix
2022-06-14 17:56:05 +05:30
Aakansha Doshi
5daff2d3cd fix: copy arrow head when using copy styles (#5303)
* fix: copy arrow head when using copy styles

* remove mutations & check against `arrow` type

* fix lint

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-06-14 16:27:41 +05:30
Aakansha Doshi
f1bc90e08a fix: Allow null in renderFooter prop (#5282)
* fix: Allow null in render props

* update docs
2022-06-06 20:45:06 +05:30
Aakansha Doshi
aabcdc20fd docs: prevent touch so that pointermove works in touch devices (#5241) 2022-05-25 11:39:16 +05:30
Aakansha Doshi
269fbcc2f3 docs: remove dragging threshold when interacting with custom elements (#5240) 2022-05-24 20:56:01 +05:30
jasonphillips
d08179c215 fix: transpile browser-fs-access in package build (#5041) 2022-05-24 15:07:05 +02:00
Aakansha Doshi
90e739d444 docs: fix offsets in the example when dragging custom elements (#5238) 2022-05-24 16:27:23 +05:30
David Luzar
4a9fac2d1e fix: unsafely accessing draggingElement (#5216) 2022-05-20 17:48:26 +02:00
Aakansha Doshi
07ebd7c68c feat: support setting/resetting cursor from host (#5215)
* Support setting/resetting cursor type from host

* add docs

* minor
2022-05-20 18:43:38 +05:30
David Luzar
92f30f7ed6 test: add tests for loading library from file picker (#5206) 2022-05-19 00:37:36 +02:00
zsviczian
605aa554d0 fix: Library load button does not work (#5205)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-18 19:46:08 +00:00
Milos Vetesnik
bed9fca4a5 feat: go-to-excalidrawplus button (#5202)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-18 18:30:34 +02:00
Milos Vetesnik
b9968e2e72 feat: Autoredirect to Excalidraw+ if special cookie is present (#5183)
Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-18 12:04:26 +02:00
Aakansha Doshi
ab1a30073c feat: expose utilitis to convert scene coords to viewport coords and vice versa (#5187)
* feat: expose utilitis to convert scene coords to viewport coords and vice versa

* add return value
2022-05-12 17:14:30 +05:30
Excalidraw Bot
31049d06e8 chore: Update translations from Crowdin (#5061) 2022-05-11 23:44:02 +02:00
Johannes
ef8559d060 fix: do not deselect when not zooming using touchscreen pinch (#5181)
* Do not deselect when zooming

* do not deselect when trackpad zooming on Safari

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-11 23:30:50 +02:00
Johannes
33bb23d2f3 fix: wheel zoom normalization (#5165)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2022-05-11 22:01:20 +02:00
David Luzar
b27ac257e7 feat: support resubmitting published library items (#5174) 2022-05-11 15:56:11 +02:00
David Luzar
d2cc76e52e feat: support adding multiple library items on canvas (#5116) 2022-05-11 15:51:02 +02:00
David Luzar
cad6097d60 feat: factor out url library init & switch to updateLibrary API (#5115)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-05-11 15:08:54 +02:00
David Luzar
2537b225ac fix: hide sidebar when custom tool active (#5179) 2022-05-11 12:42:52 +00:00
Aakansha Doshi
4ee48d2729 fix: rename src to avatarUrl in collaborator (#5177)
* fix: rename  to  in collaborator

* update pr link

* fallback to intials on image error
2022-05-11 15:54:10 +05:30
Aakansha Doshi
68f23d652f feat: support custom elements in @excalidraw/excalidraw (#5164)
* feat: support custom elements in @excalidraw/excalidraw

* revert

* fix css

* fix offsets

* fix overflow of custom elements in example

* fix overflow in comments input

* make sure comment input never overflows the viewport

* remove offsetschange

* expose setActiveTool

* rename to onPointerDown

* update docs

* fix
2022-05-11 13:30:15 +05:30
dependabot[bot]
a078508c05 chore(deps-dev): bump sass-loader in /src/packages/utils (#4813)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.4.0 to 12.6.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.4.0...v12.6.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 13:02:20 +00:00
dependabot[bot]
abf4dc9256 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#5121)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.7 to 7.17.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.10/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-05-10 12:51:53 +00:00
dependabot[bot]
ba8f12d588 chore(deps-dev): bump webpack-dev-server in /src/packages/excalidraw (#5158)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.7.4 to 4.9.0.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.7.4...v4.9.0)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  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-05-10 12:49:37 +00:00
dependabot[bot]
d57560db06 chore(deps-dev): bump typescript in /src/packages/excalidraw (#5119)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.4 to 4.6.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.5.4...v4.6.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-05-10 12:48:59 +00:00
dependabot[bot]
0d26049b4e chore(deps-dev): bump babel-loader in /src/packages/excalidraw (#5086)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.3 to 8.2.5.
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v8.2.3...v8.2.5)

---
updated-dependencies:
- dependency-name: babel-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-05-10 12:48:27 +00:00
dependabot[bot]
f72e9b6ea5 chore(deps-dev): bump ts-loader in /src/packages/utils (#5123)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.8 to 9.3.0.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.8...v9.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 12:42:13 +00:00
dependabot[bot]
029cfb31b0 chore(deps-dev): bump prettier from 2.5.1 to 2.6.2 (#5168)
Bumps [prettier](https://github.com/prettier/prettier) from 2.5.1 to 2.6.2.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.5.1...2.6.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 12:23:34 +00:00
dependabot[bot]
3a288eb09c chore(deps-dev): bump eslint-config-prettier from 8.3.0 to 8.5.0 (#5098)
Bumps [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) from 8.3.0 to 8.5.0.
- [Release notes](https://github.com/prettier/eslint-config-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-config-prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-config-prettier/compare/v8.3.0...v8.5.0)

---
updated-dependencies:
- dependency-name: eslint-config-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 12:22:58 +00:00
dependabot[bot]
803909abb6 chore(deps-dev): bump webpack in /src/packages/utils (#5167)
Bumps [webpack](https://github.com/webpack/webpack) from 5.66.0 to 5.72.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.66.0...v5.72.1)

---
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-05-10 12:19:48 +00:00
dependabot[bot]
56c75b769c chore(deps): bump sass from 1.49.7 to 1.51.0 (#5169)
Bumps [sass](https://github.com/sass/dart-sass) from 1.49.7 to 1.51.0.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.49.7...1.51.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 17:49:02 +05:30
dependabot[bot]
eea48d94d3 chore(deps): bump @testing-library/react from 12.1.2 to 12.1.5 (#5055)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 12.1.2 to 12.1.5.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v12.1.2...v12.1.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 17:42:29 +05:30
dependabot[bot]
e29152ab30 chore(deps-dev): bump @babel/plugin-transform-runtime (#5118)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.17.0 to 7.17.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.17.10/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 17:42:13 +05:30
dependabot[bot]
f4aa36b35d chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#5117)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.8 to 9.3.0.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.8...v9.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-10 17:40:30 +05:30
dependabot[bot]
2903a763a7 chore(deps-dev): bump jest-canvas-mock from 2.3.1 to 2.4.0 (#5127)
Bumps [jest-canvas-mock](https://github.com/hustcc/jest-canvas-mock) from 2.3.1 to 2.4.0.
- [Release notes](https://github.com/hustcc/jest-canvas-mock/releases)
- [Changelog](https://github.com/hustcc/jest-canvas-mock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hustcc/jest-canvas-mock/commits/v2.4.0)

---
updated-dependencies:
- dependency-name: jest-canvas-mock
  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-05-10 17:39:46 +05:30
dependabot[bot]
4a980ed5db chore(deps-dev): bump autoprefixer in /src/packages/excalidraw (#5159)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.4.5 to 10.4.7.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.5...10.4.7)

---
updated-dependencies:
- dependency-name: autoprefixer
  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-05-10 17:39:09 +05:30
David Luzar
d2e687ed0a feat: make file handling more robust (#5057) 2022-05-09 15:53:04 +02:00
DanielJGeiger
0d70690ec8 fix: Don't save deleted ExcalidrawElements to Firebase (#5108)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-09 15:38:44 +02:00
David Luzar
a524eeb66e fix: eraser removed deleted elements (#5155)
* fix: eraser removed deleted elements

* rename `getElements` API

* fix one more case of not including deleted elements
2022-05-07 21:01:37 +02:00
Tom Sherman
3d56ceb794 fix: Handle ColorPicker parentSelector being undefined (#5152)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-07 18:16:14 +00:00
David Luzar
65c32b3319 fix: library multiselect not accounting for published state (#5132) 2022-05-07 19:13:10 +02:00
Nicholas Fitton
9e8e047aae fix: Chart display fix (#5154) 2022-05-07 19:12:31 +02:00
Aakansha Doshi
64d330a332 feat: support customType in activeTool (#5144)
* feat: support customType in activeTool

* fix

* rewrite types and handling

* tweaks

* fix

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-05-06 18:21:22 +05:30
Aakansha Doshi
1ed1529f96 fix: update opacity of bound text when opacity of container updated (#5142) 2022-05-04 14:29:05 +05:30
252 changed files with 20118 additions and 9256 deletions

View File

@@ -11,3 +11,12 @@ REACT_APP_WS_SERVER_URL=http://localhost:3002
REACT_APP_PORTAL_URL= 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"}' 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

@@ -13,3 +13,5 @@ REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","
# production-only vars # production-only vars
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13 REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
REACT_APP_PLUS_APP=https://app.excalidraw.com

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: on:
push: push:
branches: branches:

View File

@@ -1,4 +1,4 @@
name: Auto release preview @excalidraw/excalidraw-preview name: Auto release excalidraw preview
on: on:
issue_comment: issue_comment:
types: [created, edited] types: [created, edited]
@@ -6,7 +6,7 @@ on:
jobs: jobs:
Auto-release-excalidraw-preview: Auto-release-excalidraw-preview:
name: Auto release preview name: Auto release preview
if: github.event.comment.body == '@excalibot release package' && github.event.issue.pull_request if: github.event.comment.body == '@excalibot trigger release' && github.event.issue.pull_request
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: React to release comment - name: React to release comment

View File

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

1
.gitignore vendored
View File

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

@@ -22,18 +22,18 @@
"@sentry/browser": "6.2.5", "@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5", "@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.16.2", "@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.2", "@testing-library/react": "12.1.5",
"@tldraw/vec": "1.4.3", "@tldraw/vec": "1.7.1",
"@types/jest": "27.4.0", "@types/jest": "27.4.0",
"@types/pica": "5.1.3", "@types/pica": "5.1.3",
"@types/react": "17.0.39", "@types/react": "18.0.15",
"@types/react-dom": "17.0.11", "@types/react-dom": "18.0.6",
"@types/socket.io-client": "1.4.36", "@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1", "browser-fs-access": "0.29.1",
"clsx": "1.1.1", "clsx": "1.1.1",
"fake-indexeddb": "3.1.7", "fake-indexeddb": "3.1.7",
"firebase": "8.3.3", "firebase": "8.3.3",
"i18next-browser-languagedetector": "6.1.2", "i18next-browser-languagedetector": "6.1.4",
"idb-keyval": "6.0.3", "idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1", "image-blob-reduce": "3.0.1",
"jotai": "1.6.4", "jotai": "1.6.4",
@@ -47,11 +47,11 @@
"png-chunks-extract": "1.0.0", "png-chunks-extract": "1.0.0",
"points-on-curve": "0.2.0", "points-on-curve": "0.2.0",
"pwacompat": "2.0.17", "pwacompat": "2.0.17",
"react": "17.0.2", "react": "18.2.0",
"react-dom": "17.0.2", "react-dom": "18.2.0",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"roughjs": "4.5.2", "roughjs": "4.5.2",
"sass": "1.49.7", "sass": "1.51.0",
"socket.io-client": "2.3.1", "socket.io-client": "2.3.1",
"typescript": "4.5.5" "typescript": "4.5.5"
}, },
@@ -59,19 +59,19 @@
"@excalidraw/eslint-config": "1.0.0", "@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2", "@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0", "@types/chai": "4.3.0",
"@types/lodash.throttle": "4.1.6", "@types/lodash.throttle": "4.1.7",
"@types/pako": "1.0.3", "@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.6", "@types/resize-observer-browser": "0.1.7",
"chai": "4.3.6", "chai": "4.3.6",
"dotenv": "10.0.0", "dotenv": "16.0.1",
"eslint-config-prettier": "8.3.0", "eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "3.3.1", "eslint-plugin-prettier": "3.3.1",
"husky": "7.0.4", "husky": "7.0.4",
"jest-canvas-mock": "2.3.1", "jest-canvas-mock": "2.4.0",
"lint-staged": "12.3.7", "lint-staged": "12.3.7",
"pepjs": "0.5.3", "pepjs": "0.5.3",
"prettier": "2.5.1", "prettier": "2.6.2",
"rewire": "5.0.0" "rewire": "6.0.0"
}, },
"resolutions": { "resolutions": {
"@typescript-eslint/typescript-estree": "5.10.2" "@typescript-eslint/typescript-estree": "5.10.2"
@@ -94,7 +94,8 @@
"build:app:docker": "REACT_APP_DISABLE_SENTRY=true react-scripts build", "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": "REACT_APP_GIT_SHA=$VERCEL_GIT_COMMIT_SHA react-scripts build",
"build:version": "node ./scripts/build-version.js", "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", "eject": "react-scripts eject",
"fix:code": "yarn test:code --fix", "fix:code": "yarn test:code --fix",
"fix:other": "yarn prettier --write", "fix:other": "yarn prettier --write",
@@ -112,6 +113,8 @@
"test:typecheck": "tsc", "test:typecheck": "tsc",
"test:update": "yarn test:app --updateSnapshot --watchAll=false", "test:update": "yarn test:app --updateSnapshot --watchAll=false",
"test": "yarn test:app", "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

@@ -52,6 +52,25 @@
content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them." content="Excalidraw is a whiteboard tool that lets you easily sketch diagrams that have a hand-drawn feel to them."
/> />
<script>
// Redirect Excalidraw+ users which have auto-redirect enabled.
//
// Redirect only the bare root path, so link/room/library urls are not
// redirected.
//
// Putting into index.html for best performance (can't redirect on server
// due to location.hash checks).
if (
window.location.pathname === "/" &&
!window.location.hash &&
!window.location.search &&
// if its present redirect
document.cookie.includes("excplus-autoredirect=true")
) {
window.location.href = "https://app.excalidraw.com";
}
</script>
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<!-- Excalidraw version --> <!-- Excalidraw version -->
@@ -79,6 +98,22 @@
/> />
<link rel="stylesheet" href="fonts.css" type="text/css" /> <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> <script>
window.EXCALIDRAW_ASSET_PATH = "/"; window.EXCALIDRAW_ASSET_PATH = "/";
// setting this so that libraries installation reuses this window tab. // setting this so that libraries installation reuses this window tab.

View File

@@ -17,11 +17,23 @@
* See https://goo.gl/2aRDsh * 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({ if (IS_DEVELOPMENT) {
modulePathPrefix: "/workbox/", 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) => { self.addEventListener("message", (event) => {
if (event.data && event.data.type === "SKIP_WAITING") { if (event.data && event.data.type === "SKIP_WAITING") {
@@ -30,14 +42,17 @@ self.addEventListener("message", (event) => {
}); });
workbox.core.clientsClaim(); workbox.core.clientsClaim();
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
workbox.routing.registerNavigationRoute( if (!IS_DEVELOPMENT) {
workbox.precaching.getCacheKeyForURL("./index.html"), workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
{
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/], workbox.routing.registerNavigationRoute(
}, workbox.precaching.getCacheKeyForURL("./index.html"),
); {
blacklist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
},
);
}
// Cache relevant font files // Cache relevant font files
workbox.routing.registerRoute( workbox.routing.registerRoute(

View File

@@ -5,22 +5,25 @@ const core = require("@actions/core");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`; const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage); const pkg = require(excalidrawPackage);
const isPreview = process.argv.slice(2)[0] === "preview";
const getShortCommitHash = () => { const getShortCommitHash = () => {
return execSync("git rev-parse --short HEAD").toString().trim(); return execSync("git rev-parse --short HEAD").toString().trim();
}; };
const publish = () => { const publish = () => {
const tag = isPreview ? "preview" : "next";
try { try {
execSync(`yarn --frozen-lockfile`); execSync(`yarn --frozen-lockfile`);
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir }); execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir }); execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`); execSync(`yarn --cwd ${excalidrawDir} publish --tag ${tag}`);
console.info("Published 🎉"); console.info(`Published ${pkg.name}@${tag}🎉`);
core.setOutput( core.setOutput(
"result", "result",
`**Preview version has been shipped** :rocket: `**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) { } catch (error) {
core.setOutput("result", "package couldn't be published :warning:!"); 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 // update package.json
pkg.name = "@excalidraw/excalidraw-next";
let version = `${pkg.version}-${getShortCommitHash()}`; let version = `${pkg.version}-${getShortCommitHash()}`;
// update readme // update readme
let data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
const isPreview = process.argv.slice(2)[0] === "preview";
if (isPreview) { if (isPreview) {
// use pullNumber-commithash as the version for preview // use pullNumber-commithash as the version for preview
const pullRequestNumber = process.argv.slice(3)[0]; const pullRequestNumber = process.argv.slice(3)[0];
version = `${pkg.version}-${pullRequestNumber}-${getShortCommitHash()}`; 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; pkg.version = version;
fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8"); fs.writeFileSync(excalidrawPackage, JSON.stringify(pkg, null, 2), "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
console.info("Publish in progress..."); console.info("Publish in progress...");
publish(); 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", "ru-RU": "en-ru",
"si-LK": "en-silk", "si-LK": "en-silk",
"sk-SK": "en-sk", "sk-SK": "en-sk",
"sl-SI": "en-sl",
"sv-SE": "en-sv", "sv-SE": "en-sv",
"ta-IN": "en-ta", "ta-IN": "en-ta",
"tr-TR": "en-tr", "tr-TR": "en-tr",
@@ -47,6 +48,8 @@ const crowdinMap = {
"lv-LV": "en-lv", "lv-LV": "en-lv",
"cs-CZ": "en-cs", "cs-CZ": "en-cs",
"kk-KZ": "en-kk", "kk-KZ": "en-kk",
"vi-vn": "en-vi",
"mr-in": "en-mr",
}; };
const flags = { const flags = {
@@ -86,6 +89,7 @@ const flags = {
"ru-RU": "🇷🇺", "ru-RU": "🇷🇺",
"si-LK": "🇱🇰", "si-LK": "🇱🇰",
"sk-SK": "🇸🇰", "sk-SK": "🇸🇰",
"sl-SI": "🇸🇮",
"sv-SE": "🇸🇪", "sv-SE": "🇸🇪",
"ta-IN": "🇮🇳", "ta-IN": "🇮🇳",
"tr-TR": "🇹🇷", "tr-TR": "🇹🇷",
@@ -93,6 +97,9 @@ const flags = {
"zh-CN": "🇨🇳", "zh-CN": "🇨🇳",
"zh-HK": "🇭🇰", "zh-HK": "🇭🇰",
"zh-TW": "🇹🇼", "zh-TW": "🇹🇼",
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
}; };
const languages = { const languages = {
@@ -133,6 +140,7 @@ const languages = {
"ru-RU": "Русский", "ru-RU": "Русский",
"si-LK": "සිංහල", "si-LK": "සිංහල",
"sk-SK": "Slovenčina", "sk-SK": "Slovenčina",
"sl-SI": "Slovenščina",
"sv-SE": "Svenska", "sv-SE": "Svenska",
"ta-IN": "Tamil", "ta-IN": "Tamil",
"tr-TR": "Türkçe", "tr-TR": "Türkçe",
@@ -140,6 +148,8 @@ const languages = {
"zh-CN": "简体中文", "zh-CN": "简体中文",
"zh-HK": "繁體中文 (香港)", "zh-HK": "繁體中文 (香港)",
"zh-TW": "繁體中文", "zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
}; };
const percentages = fs.readFileSync( const percentages = fs.readFileSync(

21
scripts/prebuild.js Normal file
View File

@@ -0,0 +1,21 @@
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/");
});
};
// -----------------------------------------------------------------------------
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 fs = require("fs");
const util = require("util"); const { execSync } = require("child_process");
const exec = util.promisify(require("child_process").exec);
const updateReadme = require("./updateReadme");
const updateChangelog = require("./updateChangelog");
const excalidrawDir = `${__dirname}/../src/packages/excalidraw`; const excalidrawDir = `${__dirname}/../src/packages/excalidraw`;
const excalidrawPackage = `${excalidrawDir}/package.json`; const excalidrawPackage = `${excalidrawDir}/package.json`;
const pkg = require(excalidrawPackage);
const updatePackageVersion = (nextVersion) => { const originalReadMe = fs.readFileSync(`${excalidrawDir}/README.md`, "utf8");
const pkg = require(excalidrawPackage);
pkg.version = nextVersion; const updateReadme = () => {
const content = `${JSON.stringify(pkg, null, 2)}\n`; const excalidrawIndex = originalReadMe.indexOf("### Excalidraw");
fs.writeFileSync(excalidrawPackage, content, "utf-8");
// 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 { try {
updateReadme(); execSync(`yarn --frozen-lockfile`);
await updateChangelog(nextVersion); execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
updatePackageVersion(nextVersion); execSync(`yarn run build:umd`, { cwd: excalidrawDir });
await exec(`git add -u`); execSync(`yarn --cwd ${excalidrawDir} publish`);
await exec(
`git commit -m "docs: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
);
/* eslint-disable no-console */
console.log("Done!");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
process.exit(1); process.exit(1);
} }
}; };
const nextVersion = process.argv.slice(2)[0]; const release = () => {
if (!nextVersion) { updateReadme();
console.error("Pass the next version to release!"); console.info("Note for stable readme removed");
process.exit(1);
} publish();
release(nextVersion); 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, commitToHistory: false,
appState: { appState: {
...appState, ...appState,
toastMessage: t("toast.addedToLibrary"), toast: { message: t("toast.addedToLibrary") },
}, },
}; };
}) })

View File

@@ -11,7 +11,7 @@ import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey } from "../utils"; import { getShortcutKey, updateActiveTool } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { Tooltip } from "../components/Tooltip"; import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
@@ -304,36 +304,22 @@ export const actionErase = register({
name: "eraser", name: "eraser",
trackEvent: { category: "toolbar" }, trackEvent: { category: "toolbar" },
perform: (elements, appState) => { perform: (elements, appState) => {
const activeTool: any = { ...appState.activeTool }; let activeTool: AppState["activeTool"];
if (appState.activeTool.type !== "eraser") {
if (appState.activeTool.type === "custom") {
activeTool.lastActiveToolBeforeEraser = {
type: "custom",
customType: appState.activeTool.customType,
};
} else {
activeTool.lastActiveToolBeforeEraser = appState.activeTool.type;
}
}
if (isEraserActive(appState)) { if (isEraserActive(appState)) {
if (appState.activeTool.lastActiveToolBeforeEraser) { activeTool = updateActiveTool(appState, {
if ( ...(appState.activeTool.lastActiveToolBeforeEraser || {
typeof appState.activeTool.lastActiveToolBeforeEraser === "object" && type: "selection",
appState.activeTool.lastActiveToolBeforeEraser?.type === "custom" }),
) { lastActiveToolBeforeEraser: null,
activeTool.type = "custom"; });
activeTool.customType =
appState.activeTool.lastActiveToolBeforeEraser.customType;
} else {
activeTool.type = appState.activeTool.lastActiveToolBeforeEraser;
}
} else {
activeTool.type = "selection";
}
} else { } else {
activeTool.type = "eraser"; activeTool = updateActiveTool(appState, {
type: "eraser",
lastActiveToolBeforeEraser: appState.activeTool,
});
} }
return { return {
appState: { appState: {
...appState, ...appState,

View File

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

View File

@@ -12,6 +12,7 @@ import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { fixBindingsAfterDeletion } from "../element/binding"; import { fixBindingsAfterDeletion } from "../element/binding";
import { isBoundToContainer } from "../element/typeChecks"; import { isBoundToContainer } from "../element/typeChecks";
import { updateActiveTool } from "../utils";
const deleteSelectedElements = ( const deleteSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
@@ -134,7 +135,7 @@ export const actionDeleteSelected = register({
elements: nextElements, elements: nextElements,
appState: { appState: {
...nextAppState, ...nextAppState,
activeTool: { ...appState.activeTool, type: "selection" }, activeTool: updateActiveTool(appState, { type: "selection" }),
multiElement: null, multiElement: null,
}, },
commitToHistory: isSomeElementSelected( commitToHistory: isSomeElementSelected(

View File

@@ -3,7 +3,7 @@ import {
DistributeVerticallyIcon, DistributeVerticallyIcon,
} from "../components/icons"; } from "../components/icons";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../disitrubte"; import { distributeElements, Distribution } from "../distribute";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";

View File

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

View File

@@ -7,7 +7,7 @@ import { DarkModeToggle } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data"; import { loadFromJSON, saveAsJSON } from "../data";
import { resaveAsImageWithScene } from "../data/resave"; import { resaveAsImageWithScene } from "../data/resave";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "../components/App"; import { useDevice } from "../components/App";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { register } from "./register"; import { register } from "./register";
import { CheckboxItem } from "../components/CheckboxItem"; import { CheckboxItem } from "../components/CheckboxItem";
@@ -144,13 +144,15 @@ export const actionSaveToActiveFile = register({
appState: { appState: {
...appState, ...appState,
fileHandle, fileHandle,
toastMessage: fileHandleExists toast: fileHandleExists
? fileHandle?.name ? {
? t("toast.fileSavedToFilename").replace( message: fileHandle?.name
"{filename}", ? t("toast.fileSavedToFilename").replace(
`"${fileHandle.name}"`, "{filename}",
) `"${fileHandle.name}"`,
: t("toast.fileSaved") )
: t("toast.fileSaved"),
}
: null, : null,
}, },
}; };
@@ -204,7 +206,7 @@ export const actionSaveFileToDisk = register({
icon={saveAs} icon={saveAs}
title={t("buttons.saveAs")} title={t("buttons.saveAs")}
aria-label={t("buttons.saveAs")} aria-label={t("buttons.saveAs")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
hidden={!nativeFileSystemSupported} hidden={!nativeFileSystemSupported}
onClick={() => updateData(null)} onClick={() => updateData(null)}
data-testid="save-as-button" data-testid="save-as-button"
@@ -242,13 +244,13 @@ export const actionLoadScene = register({
} }
}, },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O,
PanelComponent: ({ updateData, appState }) => ( PanelComponent: ({ updateData }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={load} icon={load}
title={t("buttons.load")} title={t("buttons.load")}
aria-label={t("buttons.load")} aria-label={t("buttons.load")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
onClick={updateData} onClick={updateData}
data-testid="load-button" data-testid="load-button"
/> />

View File

@@ -1,6 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element"; import { isInvisiblySmallElement } from "../element";
import { resetCursor } from "../utils"; import { updateActiveTool, resetCursor } from "../utils";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons"; import { done } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -13,12 +13,13 @@ import {
maybeBindLinearElement, maybeBindLinearElement,
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
} from "../element/binding"; } from "../element/binding";
import { isBindingElement } from "../element/typeChecks"; import { isBindingElement, isLinearElement } from "../element/typeChecks";
import { AppState } from "../types";
export const actionFinalize = register({ export const actionFinalize = register({
name: "finalize", name: "finalize",
trackEvent: false, trackEvent: false,
perform: (elements, appState, _, { canvas, focusContainer }) => { perform: (elements, appState, _, { canvas, focusContainer, scene }) => {
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } = const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement; appState.editingLinearElement;
@@ -49,8 +50,12 @@ export const actionFinalize = register({
let newElements = elements; let newElements = elements;
if (appState.pendingImageElement) { const pendingImageElement =
mutateElement(appState.pendingImageElement, { isDeleted: true }, false); appState.pendingImageElementId &&
scene.getElement(appState.pendingImageElementId);
if (pendingImageElement) {
mutateElement(pendingImageElement, { isDeleted: true }, false);
} }
if (window.document.activeElement instanceof HTMLElement) { if (window.document.activeElement instanceof HTMLElement) {
@@ -136,21 +141,21 @@ export const actionFinalize = register({
) { ) {
resetCursor(canvas); resetCursor(canvas);
} }
const activeTool: any = { ...appState.activeTool };
if (appState.activeTool.lastActiveToolBeforeEraser) { let activeTool: AppState["activeTool"];
if ( if (appState.activeTool.type === "eraser") {
typeof appState.activeTool.lastActiveToolBeforeEraser === "object" && activeTool = updateActiveTool(appState, {
appState.activeTool.lastActiveToolBeforeEraser.type === "custom" ...(appState.activeTool.lastActiveToolBeforeEraser || {
) { type: "selection",
activeTool.type = appState.activeTool.lastActiveToolBeforeEraser.type; }),
activeTool.customType = lastActiveToolBeforeEraser: null,
appState.activeTool.lastActiveToolBeforeEraser.customType; });
} else {
activeTool.type = appState.activeTool.lastActiveToolBeforeEraser;
}
} else { } else {
activeTool.type = "selection"; activeTool = updateActiveTool(appState, {
type: "selection",
});
} }
return { return {
elements: newElements, elements: newElements,
appState: { appState: {
@@ -176,7 +181,12 @@ export const actionFinalize = register({
[multiPointElement.id]: true, [multiPointElement.id]: true,
} }
: appState.selectedElementIds, : appState.selectedElementIds,
pendingImageElement: null, // 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", commitToHistory: appState.activeTool.type === "freedraw",
}; };

View File

@@ -31,16 +31,7 @@ export const actionGoToCollaborator = register({
}; };
}, },
PanelComponent: ({ appState, updateData, data }) => { PanelComponent: ({ appState, updateData, data }) => {
const clientId: string | undefined = data?.id; const [clientId, collaborator] = data as [string, Collaborator];
if (!clientId) {
return null;
}
const collaborator = appState.collaborators.get(clientId);
if (!collaborator) {
return null;
}
const { background, stroke } = getClientColors(clientId, appState); const { background, stroke } = getClientColors(clientId, appState);
@@ -50,7 +41,7 @@ export const actionGoToCollaborator = register({
border={stroke} border={stroke}
onClick={() => updateData(collaborator.pointer)} onClick={() => updateData(collaborator.pointer)}
name={collaborator.username || ""} name={collaborator.username || ""}
src={collaborator.src} src={collaborator.avatarUrl}
/> />
); );
}, },

View File

@@ -485,10 +485,14 @@ export const actionChangeOpacity = register({
trackEvent: false, trackEvent: false,
perform: (elements, appState, value) => { perform: (elements, appState, value) => {
return { return {
elements: changeProperty(elements, appState, (el) => elements: changeProperty(
newElementWith(el, { elements,
opacity: value, appState,
}), (el) =>
newElementWith(el, {
opacity: value,
}),
true,
), ),
appState: { ...appState, currentItemOpacity: value }, appState: { ...appState, currentItemOpacity: value },
commitToHistory: true, commitToHistory: true,

View File

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

View File

@@ -48,7 +48,7 @@ describe("actionStyles", () => {
Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => { Keyboard.withModifierKeys({ ctrl: true, alt: true }, () => {
Keyboard.codeDown(CODES.C); Keyboard.codeDown(CODES.C);
}); });
const secondRect = JSON.parse(copiedStyles); const secondRect = JSON.parse(copiedStyles)[0];
expect(secondRect.id).toBe(h.elements[1].id); expect(secondRect.id).toBe(h.elements[1].id);
mouse.reset(); mouse.reset();

View File

@@ -6,13 +6,15 @@ import {
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { t } from "../i18n"; import { t } from "../i18n";
import { register } from "./register"; import { register } from "./register";
import { mutateElement, newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { import {
DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
DEFAULT_FONT_FAMILY, DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
} from "../constants"; } from "../constants";
import { getContainerElement } from "../element/textElement"; import { getBoundTextElement } from "../element/textElement";
import { hasBoundTextElement } from "../element/typeChecks";
import { getSelectedElements } from "../scene";
// `copiedStyles` is exported only for tests. // `copiedStyles` is exported only for tests.
export let copiedStyles: string = "{}"; export let copiedStyles: string = "{}";
@@ -21,14 +23,20 @@ export const actionCopyStyles = register({
name: "copyStyles", name: "copyStyles",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState) => {
const elementsCopied = [];
const element = elements.find((el) => appState.selectedElementIds[el.id]); const element = elements.find((el) => appState.selectedElementIds[el.id]);
elementsCopied.push(element);
if (element && hasBoundTextElement(element)) {
const boundTextElement = getBoundTextElement(element);
elementsCopied.push(boundTextElement);
}
if (element) { if (element) {
copiedStyles = JSON.stringify(element); copiedStyles = JSON.stringify(elementsCopied);
} }
return { return {
appState: { appState: {
...appState, ...appState,
toastMessage: t("toast.copyStyles"), toast: { message: t("toast.copyStyles") },
}, },
commitToHistory: false, commitToHistory: false,
}; };
@@ -42,31 +50,62 @@ export const actionPasteStyles = register({
name: "pasteStyles", name: "pasteStyles",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState) => {
const pastedElement = JSON.parse(copiedStyles); const elementsCopied = JSON.parse(copiedStyles);
const pastedElement = elementsCopied[0];
const boundTextElement = elementsCopied[1];
if (!isExcalidrawElement(pastedElement)) { if (!isExcalidrawElement(pastedElement)) {
return { elements, commitToHistory: false }; return { elements, commitToHistory: false };
} }
const selectedElements = getSelectedElements(elements, appState, true);
const selectedElementIds = selectedElements.map((element) => element.id);
return { return {
elements: elements.map((element) => { elements: elements.map((element) => {
if (appState.selectedElementIds[element.id]) { if (selectedElementIds.includes(element.id)) {
const newElement = newElementWith(element, { let elementStylesToCopyFrom = pastedElement;
backgroundColor: pastedElement?.backgroundColor, if (isTextElement(element) && element.containerId) {
strokeWidth: pastedElement?.strokeWidth, elementStylesToCopyFrom = boundTextElement;
strokeColor: pastedElement?.strokeColor,
strokeStyle: pastedElement?.strokeStyle,
fillStyle: pastedElement?.fillStyle,
opacity: pastedElement?.opacity,
roughness: pastedElement?.roughness,
});
if (isTextElement(newElement) && isTextElement(element)) {
mutateElement(newElement, {
fontSize: pastedElement?.fontSize || DEFAULT_FONT_SIZE,
fontFamily: pastedElement?.fontFamily || DEFAULT_FONT_FAMILY,
textAlign: pastedElement?.textAlign || DEFAULT_TEXT_ALIGN,
});
redrawTextBoundingBox(newElement, getContainerElement(newElement));
} }
if (!elementStylesToCopyFrom) {
return element;
}
let newElement = newElementWith(element, {
backgroundColor: elementStylesToCopyFrom?.backgroundColor,
strokeWidth: elementStylesToCopyFrom?.strokeWidth,
strokeColor: elementStylesToCopyFrom?.strokeColor,
strokeStyle: elementStylesToCopyFrom?.strokeStyle,
fillStyle: elementStylesToCopyFrom?.fillStyle,
opacity: elementStylesToCopyFrom?.opacity,
roughness: elementStylesToCopyFrom?.roughness,
});
if (isTextElement(newElement)) {
newElement = newElementWith(newElement, {
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
fontFamily:
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
textAlign:
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
});
let container = null;
if (newElement.containerId) {
container =
selectedElements.find(
(element) =>
isTextElement(newElement) &&
element.id === newElement.containerId,
) || null;
}
redrawTextBoundingBox(newElement, container);
}
if (newElement.type === "arrow") {
newElement = newElementWith(newElement, {
startArrowhead: elementStylesToCopyFrom.startArrowhead,
endArrowhead: elementStylesToCopyFrom.endArrowhead,
});
}
return newElement; return newElement;
} }
return element; return element;

View File

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

View File

@@ -30,7 +30,7 @@ const trackAction = (
trackEvent( trackEvent(
action.trackEvent.category, action.trackEvent.category,
action.trackEvent.action || action.name, action.trackEvent.action || action.name,
`${source} (${app.deviceType.isMobile ? "mobile" : "desktop"})`, `${source} (${app.device.isMobile ? "mobile" : "desktop"})`,
); );
} }
} }
@@ -147,6 +147,7 @@ export class ActionManager {
) { ) {
const action = this.actions[name]; const action = this.actions[name];
const PanelComponent = action.PanelComponent!; const PanelComponent = action.PanelComponent!;
PanelComponent.displayName = "PanelComponent";
const elements = this.getElementsIncludingDeleted(); const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState(); const appState = this.getAppState();
const updateData = (formState?: any) => { const updateData = (formState?: any) => {

View File

@@ -6,7 +6,6 @@ import {
ExcalidrawProps, ExcalidrawProps,
BinaryFiles, BinaryFiles,
} from "../types"; } from "../types";
import { ToolButtonSize } from "../components/ToolButton";
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api"; export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
@@ -119,7 +118,7 @@ export type PanelComponentProps = {
appState: AppState; appState: AppState;
updateData: (formData?: any) => void; updateData: (formData?: any) => void;
appProps: ExcalidrawProps; appProps: ExcalidrawProps;
data?: Partial<{ id: string; size: ToolButtonSize }>; data?: Record<string, any>;
}; };
export interface Action { export interface Action {

View File

@@ -43,6 +43,7 @@ export const getDefaultAppState = (): Omit<
editingLinearElement: null, editingLinearElement: null,
activeTool: { activeTool: {
type: "selection", type: "selection",
customType: null,
locked: false, locked: false,
lastActiveToolBeforeEraser: null, lastActiveToolBeforeEraser: null,
}, },
@@ -57,6 +58,7 @@ export const getDefaultAppState = (): Omit<
gridSize: null, gridSize: null,
isBindingEnabled: true, isBindingEnabled: true,
isLibraryOpen: false, isLibraryOpen: false,
isLibraryMenuDocked: false,
isLoading: false, isLoading: false,
isResizing: false, isResizing: false,
isRotating: false, isRotating: false,
@@ -79,15 +81,16 @@ export const getDefaultAppState = (): Omit<
showStats: false, showStats: false,
startBoundElement: null, startBoundElement: null,
suggestedBindings: [], suggestedBindings: [],
toastMessage: null, toast: null,
viewBackgroundColor: oc.white, viewBackgroundColor: oc.white,
zenModeEnabled: false, zenModeEnabled: false,
zoom: { zoom: {
value: 1 as NormalizedZoomValue, value: 1 as NormalizedZoomValue,
}, },
viewModeEnabled: false, viewModeEnabled: false,
pendingImageElement: null, pendingImageElementId: null,
showHyperlinkPopup: false, showHyperlinkPopup: false,
selectedLinearElement: null,
}; };
}; };
@@ -145,7 +148,8 @@ const APP_STATE_STORAGE_CONF = (<
gridSize: { browser: true, export: true, server: true }, gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false }, height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false }, isBindingEnabled: { browser: false, export: false, server: false },
isLibraryOpen: { 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 }, isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false }, isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false }, isRotating: { browser: false, export: false, server: false },
@@ -170,14 +174,15 @@ const APP_STATE_STORAGE_CONF = (<
showStats: { browser: true, export: false, server: false }, showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false }, startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false }, suggestedBindings: { browser: false, export: false, server: false },
toastMessage: { browser: false, export: false, server: false }, toast: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true }, viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false }, width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false }, zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false }, zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false }, viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElement: { browser: false, export: false, server: false }, pendingImageElementId: { browser: false, export: false, server: false },
showHyperlinkPopup: { browser: false, export: false, server: false }, showHyperlinkPopup: { browser: false, export: false, server: false },
selectedLinearElement: { browser: true, export: false, server: false },
}); });
const _clearAppStateForStorage = < const _clearAppStateForStorage = <

121
src/charts.test.ts Normal file
View File

@@ -0,0 +1,121 @@
import {
Spreadsheet,
tryParseCells,
tryParseNumber,
VALID_SPREADSHEET,
} from "./charts";
describe("charts", () => {
describe("tryParseNumber", () => {
it.each<[string, number]>([
["1", 1],
["0", 0],
["-1", -1],
["0.1", 0.1],
[".1", 0.1],
["1.", 1],
["424.", 424],
["$1", 1],
["-.1", -0.1],
["-$1", -1],
["$-1", -1],
])("should correctly identify %s as numbers", (given, expected) => {
expect(tryParseNumber(given)).toEqual(expected);
});
it.each<[string]>([["a"], ["$"], ["$a"], ["-$a"]])(
"should correctly identify %s as not a number",
(given) => {
expect(tryParseNumber(given)).toBeNull();
},
);
});
describe("tryParseCells", () => {
it("Successfully parses a spreadsheet", () => {
const spreadsheet = [
["time", "value"],
["01:00", "61"],
["02:00", "-60"],
["03:00", "85"],
["04:00", "-67"],
["05:00", "54"],
["06:00", "95"],
];
const result = tryParseCells(spreadsheet);
expect(result.type).toBe(VALID_SPREADSHEET);
const { title, labels, values } = (
result as { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet }
).spreadsheet;
expect(title).toEqual("value");
expect(labels).toEqual([
"01:00",
"02:00",
"03:00",
"04:00",
"05:00",
"06:00",
]);
expect(values).toEqual([61, -60, 85, -67, 54, 95]);
});
it("Uses the second column as the label if it is not a number", () => {
const spreadsheet = [
["time", "value"],
["01:00", "61"],
["02:00", "-60"],
["03:00", "85"],
["04:00", "-67"],
["05:00", "54"],
["06:00", "95"],
];
const result = tryParseCells(spreadsheet);
expect(result.type).toBe(VALID_SPREADSHEET);
const { title, labels, values } = (
result as { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet }
).spreadsheet;
expect(title).toEqual("value");
expect(labels).toEqual([
"01:00",
"02:00",
"03:00",
"04:00",
"05:00",
"06:00",
]);
expect(values).toEqual([61, -60, 85, -67, 54, 95]);
});
it("treats the first column as labels if both columns are numbers", () => {
const spreadsheet = [
["time", "value"],
["01", "61"],
["02", "-60"],
["03", "85"],
["04", "-67"],
["05", "54"],
["06", "95"],
];
const result = tryParseCells(spreadsheet);
expect(result.type).toBe(VALID_SPREADSHEET);
const { title, labels, values } = (
result as { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet }
).spreadsheet;
expect(title).toEqual("value");
expect(labels).toEqual(["01", "02", "03", "04", "05", "06"]);
expect(values).toEqual([61, -60, 85, -67, 54, 95]);
});
});
});

View File

@@ -29,18 +29,24 @@ type ParseSpreadsheetResult =
| { type: typeof NOT_SPREADSHEET; reason: string } | { type: typeof NOT_SPREADSHEET; reason: string }
| { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet }; | { type: typeof VALID_SPREADSHEET; spreadsheet: Spreadsheet };
const tryParseNumber = (s: string): number | null => { /**
const match = /^[$€£¥₩]?([0-9,]+(\.[0-9]+)?)$/.exec(s); * @private exported for testing
*/
export const tryParseNumber = (s: string): number | null => {
const match = /^([-+]?)[$€£¥₩]?([-+]?)([\d.,]+)[%]?$/.exec(s);
if (!match) { if (!match) {
return null; return null;
} }
return parseFloat(match[1].replace(/,/g, "")); return parseFloat(`${(match[1] || match[2]) + match[3]}`.replace(/,/g, ""));
}; };
const isNumericColumn = (lines: string[][], columnIndex: number) => const isNumericColumn = (lines: string[][], columnIndex: number) =>
lines.slice(1).every((line) => tryParseNumber(line[columnIndex]) !== null); lines.slice(1).every((line) => tryParseNumber(line[columnIndex]) !== null);
const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => { /**
* @private exported for testing
*/
export const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
const numCols = cells[0].length; const numCols = cells[0].length;
if (numCols > 2) { if (numCols > 2) {
@@ -71,13 +77,16 @@ const tryParseCells = (cells: string[][]): ParseSpreadsheetResult => {
}; };
} }
const valueColumnIndex = isNumericColumn(cells, 0) ? 0 : 1; const labelColumnNumeric = isNumericColumn(cells, 0);
const valueColumnNumeric = isNumericColumn(cells, 1);
if (!isNumericColumn(cells, valueColumnIndex)) { if (!labelColumnNumeric && !valueColumnNumeric) {
return { type: NOT_SPREADSHEET, reason: "Value is not numeric" }; return { type: NOT_SPREADSHEET, reason: "Value is not numeric" };
} }
const labelColumnIndex = (valueColumnIndex + 1) % 2; const [labelColumnIndex, valueColumnIndex] = valueColumnNumeric
? [0, 1]
: [1, 0];
const hasHeader = tryParseNumber(cells[0][valueColumnIndex]) === null; const hasHeader = tryParseNumber(cells[0][valueColumnIndex]) === null;
const rows = hasHeader ? cells.slice(1) : cells; const rows = hasHeader ? cells.slice(1) : cells;

View File

@@ -3,7 +3,7 @@ import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ExcalidrawElement, PointerType } from "../element/types"; import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "../components/App"; import { useDevice } from "../components/App";
import { import {
canChangeSharpness, canChangeSharpness,
canHaveArrowheads, canHaveArrowheads,
@@ -15,23 +15,28 @@ import {
} from "../scene"; } from "../scene";
import { SHAPES } from "../shapes"; import { SHAPES } from "../shapes";
import { AppState, Zoom } from "../types"; import { AppState, Zoom } from "../types";
import { capitalizeString, isTransparent, setCursorForShape } from "../utils"; import {
capitalizeString,
isTransparent,
updateActiveTool,
setCursorForShape,
} from "../utils";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons"; import { hasStrokeColor } from "../scene/comparisons";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks"; import { hasBoundTextElement, isBoundToContainer } from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
export const SelectedShapeActions = ({ export const SelectedShapeActions = ({
appState, appState,
elements, elements,
renderAction, renderAction,
activeTool,
}: { }: {
appState: AppState; appState: AppState;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"]; renderAction: ActionManager["renderAction"];
activeTool: AppState["activeTool"]["type"];
}) => { }) => {
const targetElements = getTargetElements( const targetElements = getTargetElements(
getNonDeletedElements(elements), getNonDeletedElements(elements),
@@ -47,17 +52,17 @@ export const SelectedShapeActions = ({
isSingleElementBoundContainer = true; isSingleElementBoundContainer = true;
} }
const isEditing = Boolean(appState.editingElement); const isEditing = Boolean(appState.editingElement);
const deviceType = useDeviceType(); const device = useDevice();
const isRTL = document.documentElement.getAttribute("dir") === "rtl"; const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons = const showFillIcons =
hasBackground(activeTool) || hasBackground(appState.activeTool.type) ||
targetElements.some( targetElements.some(
(element) => (element) =>
hasBackground(element.type) && !isTransparent(element.backgroundColor), hasBackground(element.type) && !isTransparent(element.backgroundColor),
); );
const showChangeBackgroundIcons = const showChangeBackgroundIcons =
hasBackground(activeTool) || hasBackground(appState.activeTool.type) ||
targetElements.some((element) => hasBackground(element.type)); targetElements.some((element) => hasBackground(element.type));
const showLinkIcon = const showLinkIcon =
@@ -74,23 +79,23 @@ export const SelectedShapeActions = ({
return ( return (
<div className="panelColumn"> <div className="panelColumn">
{((hasStrokeColor(activeTool) && {((hasStrokeColor(appState.activeTool.type) &&
activeTool !== "image" && appState.activeTool.type !== "image" &&
commonSelectedType !== "image") || commonSelectedType !== "image") ||
targetElements.some((element) => hasStrokeColor(element.type))) && targetElements.some((element) => hasStrokeColor(element.type))) &&
renderAction("changeStrokeColor")} renderAction("changeStrokeColor")}
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")} {showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
{showFillIcons && renderAction("changeFillStyle")} {showFillIcons && renderAction("changeFillStyle")}
{(hasStrokeWidth(activeTool) || {(hasStrokeWidth(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeWidth(element.type))) && targetElements.some((element) => hasStrokeWidth(element.type))) &&
renderAction("changeStrokeWidth")} renderAction("changeStrokeWidth")}
{(activeTool === "freedraw" || {(appState.activeTool.type === "freedraw" ||
targetElements.some((element) => element.type === "freedraw")) && targetElements.some((element) => element.type === "freedraw")) &&
renderAction("changeStrokeShape")} renderAction("changeStrokeShape")}
{(hasStrokeStyle(activeTool) || {(hasStrokeStyle(appState.activeTool.type) ||
targetElements.some((element) => hasStrokeStyle(element.type))) && ( targetElements.some((element) => hasStrokeStyle(element.type))) && (
<> <>
{renderAction("changeStrokeStyle")} {renderAction("changeStrokeStyle")}
@@ -98,12 +103,12 @@ export const SelectedShapeActions = ({
</> </>
)} )}
{(canChangeSharpness(activeTool) || {(canChangeSharpness(appState.activeTool.type) ||
targetElements.some((element) => canChangeSharpness(element.type))) && ( targetElements.some((element) => canChangeSharpness(element.type))) && (
<>{renderAction("changeSharpness")}</> <>{renderAction("changeSharpness")}</>
)} )}
{(hasText(activeTool) || {(hasText(appState.activeTool.type) ||
targetElements.some((element) => hasText(element.type))) && ( targetElements.some((element) => hasText(element.type))) && (
<> <>
{renderAction("changeFontSize")} {renderAction("changeFontSize")}
@@ -118,7 +123,7 @@ export const SelectedShapeActions = ({
(element) => (element) =>
hasBoundTextElement(element) || isBoundToContainer(element), hasBoundTextElement(element) || isBoundToContainer(element),
) && renderAction("changeVerticalAlign")} ) && renderAction("changeVerticalAlign")}
{(canHaveArrowheads(activeTool) || {(canHaveArrowheads(appState.activeTool.type) ||
targetElements.some((element) => canHaveArrowheads(element.type))) && ( targetElements.some((element) => canHaveArrowheads(element.type))) && (
<>{renderAction("changeArrowhead")}</> <>{renderAction("changeArrowhead")}</>
)} )}
@@ -172,8 +177,8 @@ export const SelectedShapeActions = ({
<fieldset> <fieldset>
<legend>{t("labels.actions")}</legend> <legend>{t("labels.actions")}</legend>
<div className="buttonList"> <div className="buttonList">
{!deviceType.isMobile && renderAction("duplicateSelection")} {!device.isMobile && renderAction("duplicateSelection")}
{!deviceType.isMobile && renderAction("deleteSelectedElements")} {!device.isMobile && renderAction("deleteSelectedElements")}
{renderAction("group")} {renderAction("group")}
{renderAction("ungroup")} {renderAction("ungroup")}
{showLinkIcon && renderAction("hyperlink")} {showLinkIcon && renderAction("hyperlink")}
@@ -229,7 +234,9 @@ export const ShapesSwitcher = ({
if (appState.activeTool.type !== value) { if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui"); trackEvent("toolbar", value, "ui");
} }
const nextActiveTool = { ...activeTool, type: value }; const nextActiveTool = updateActiveTool(appState, {
type: value,
});
setAppState({ setAppState({
activeTool: nextActiveTool, activeTool: nextActiveTool,
multiElement: null, multiElement: null,
@@ -264,3 +271,45 @@ export const ZoomActions = ({
</Stack.Row> </Stack.Row>
</Stack.Col> </Stack.Col>
); );
export const UndoRedoActions = ({
renderAction,
className,
}: {
renderAction: ActionManager["renderAction"];
className?: string;
}) => (
<div className={`undo-redo-buttons ${className}`}>
{renderAction("undo", { size: "small" })}
{renderAction("redo", { size: "small" })}
</div>
);
export const ExitZenModeAction = ({
executeAction,
showExitZenModeBtn,
}: {
executeAction: ActionManager["executeAction"];
showExitZenModeBtn: boolean;
}) => (
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={() => executeAction(actionToggleZenMode)}
>
{t("buttons.exitZenMode")}
</button>
);
export const FinalizeAction = ({
renderAction,
className,
}: {
renderAction: ActionManager["renderAction"];
className?: string;
}) => (
<div className={`finalize-button ${className}`}>
{renderAction("finalize", { size: "small" })}
</div>
);

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import "./Avatar.scss"; import "./Avatar.scss";
import React from "react"; import React, { useState } from "react";
import { getClientInitials } from "../clients"; import { getClientInitials } from "../clients";
type AvatarProps = { type AvatarProps = {
@@ -13,13 +13,21 @@ type AvatarProps = {
export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => { export const Avatar = ({ color, border, onClick, name, src }: AvatarProps) => {
const shortName = getClientInitials(name); const shortName = getClientInitials(name);
const style = src const [error, setError] = useState(false);
const loadImg = !error && src;
const style = loadImg
? undefined ? undefined
: { background: color, border: `1px solid ${border}` }; : { background: color, border: `1px solid ${border}` };
return ( return (
<div className="Avatar" style={style} onClick={onClick}> <div className="Avatar" style={style} onClick={onClick}>
{src ? ( {loadImg ? (
<img className="Avatar-img" src={src} alt={shortName} /> <img
className="Avatar-img"
src={src}
alt={shortName}
referrerPolicy="no-referrer"
onError={() => setError(true)}
/>
) : ( ) : (
shortName shortName
)} )}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "./App"; import { useDevice } from "./App";
import { trash } from "./icons"; import { trash } from "./icons";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
@@ -19,7 +19,7 @@ const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
icon={trash} icon={trash}
title={t("buttons.clearReset")} title={t("buttons.clearReset")}
aria-label={t("buttons.clearReset")} aria-label={t("buttons.clearReset")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
onClick={toggleDialog} onClick={toggleDialog}
data-testid="clear-canvas-button" data-testid="clear-canvas-button"
/> />

View File

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

View File

@@ -1,7 +1,7 @@
import clsx from "clsx"; import clsx from "clsx";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "../components/App"; import { useDevice } from "../components/App";
import { users } from "./icons"; import { users } from "./icons";
import "./CollabButton.scss"; import "./CollabButton.scss";
@@ -26,9 +26,9 @@ const CollabButton = ({
type="button" type="button"
title={t("labels.liveCollaboration")} title={t("labels.liveCollaboration")}
aria-label={t("labels.liveCollaboration")} aria-label={t("labels.liveCollaboration")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
> >
{collaboratorCount > 0 && ( {isCollaborating && (
<div className="CollabButton-collaborators">{collaboratorCount}</div> <div className="CollabButton-collaborators">{collaboratorCount}</div>
)} )}
</ToolButton> </ToolButton>

View File

@@ -128,45 +128,33 @@ const Picker = ({
}, []); }, []);
const handleKeyDown = (event: React.KeyboardEvent) => { const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === KEYS.TAB) { let handled = false;
const { activeElement } = document; if (isArrowKey(event.key)) {
if (event.shiftKey) { handled = true;
if (activeElement === firstItem.current) {
colorInput.current?.focus();
event.preventDefault();
}
} else if (activeElement === colorInput.current) {
firstItem.current?.focus();
event.preventDefault();
}
} else if (isArrowKey(event.key)) {
const { activeElement } = document; const { activeElement } = document;
const isRTL = getLanguage().rtl; const isRTL = getLanguage().rtl;
let isCustom = false; let isCustom = false;
let index = Array.prototype.indexOf.call( let index = Array.prototype.indexOf.call(
gallery!.current!.querySelector(".color-picker-content--default")! gallery.current!.querySelector(".color-picker-content--default")
.children, ?.children,
activeElement, activeElement,
); );
if (index === -1) { if (index === -1) {
index = Array.prototype.indexOf.call( index = Array.prototype.indexOf.call(
gallery!.current!.querySelector( gallery.current!.querySelector(".color-picker-content--canvas-colors")
".color-picker-content--canvas-colors", ?.children,
)!.children,
activeElement, activeElement,
); );
if (index !== -1) { if (index !== -1) {
isCustom = true; isCustom = true;
} }
} }
const parentSelector = isCustom const parentElement = isCustom
? gallery!.current!.querySelector( ? gallery.current?.querySelector(".color-picker-content--canvas-colors")
".color-picker-content--canvas-colors", : gallery.current?.querySelector(".color-picker-content--default");
)!
: gallery!.current!.querySelector(".color-picker-content--default")!;
if (index !== -1) { if (parentElement && index !== -1) {
const length = parentSelector!.children.length - (showInput ? 1 : 0); const length = parentElement.children.length - (showInput ? 1 : 0);
const nextIndex = const nextIndex =
event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT) event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
? (index + 1) % length ? (index + 1) % length
@@ -177,30 +165,38 @@ const Picker = ({
: !isCustom && event.key === KEYS.ARROW_UP : !isCustom && event.key === KEYS.ARROW_UP
? (length + index - 5) % length ? (length + index - 5) % length
: index; : index;
(parentSelector!.children![nextIndex] as HTMLElement)?.focus(); (parentElement.children[nextIndex] as HTMLElement | undefined)?.focus();
} }
event.preventDefault(); event.preventDefault();
} else if ( } else if (
keyBindings.includes(event.key.toLowerCase()) && keyBindings.includes(event.key.toLowerCase()) &&
!event[KEYS.CTRL_OR_CMD] &&
!event.altKey &&
!isWritableElement(event.target) !isWritableElement(event.target)
) { ) {
handled = true;
const index = keyBindings.indexOf(event.key.toLowerCase()); const index = keyBindings.indexOf(event.key.toLowerCase());
const isCustom = index >= MAX_DEFAULT_COLORS; const isCustom = index >= MAX_DEFAULT_COLORS;
const parentSelector = isCustom const parentElement = isCustom
? gallery!.current!.querySelector( ? gallery?.current?.querySelector(
".color-picker-content--canvas-colors", ".color-picker-content--canvas-colors",
)! )
: gallery!.current!.querySelector(".color-picker-content--default")!; : gallery?.current?.querySelector(".color-picker-content--default");
const actualIndex = isCustom ? index - MAX_DEFAULT_COLORS : index; const actualIndex = isCustom ? index - MAX_DEFAULT_COLORS : index;
(parentSelector!.children![actualIndex] as HTMLElement)?.focus(); (
parentElement?.children[actualIndex] as HTMLElement | undefined
)?.focus();
event.preventDefault(); event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) { } else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
handled = true;
event.preventDefault(); event.preventDefault();
onClose(); onClose();
} }
event.nativeEvent.stopImmediatePropagation(); if (handled) {
event.stopPropagation(); event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
}
}; };
const renderColors = (colors: Array<string>, custom: boolean = false) => { const renderColors = (colors: Array<string>, custom: boolean = false) => {
@@ -264,7 +260,8 @@ const Picker = ({
gallery.current = el; gallery.current = el;
} }
}} }}
tabIndex={0} // to allow focusing by clicking but not by tabbing
tabIndex={-1}
> >
<div className="color-picker-content--default"> <div className="color-picker-content--default">
{renderColors(colors)} {renderColors(colors)}
@@ -346,6 +343,8 @@ const ColorInput = React.forwardRef(
}, },
); );
ColorInput.displayName = "ColorInput";
export const ColorPicker = ({ export const ColorPicker = ({
type, type,
color, color,

View File

@@ -2,13 +2,14 @@ import clsx from "clsx";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useCallbackRefState } from "../hooks/useCallbackRefState"; import { useCallbackRefState } from "../hooks/useCallbackRefState";
import { t } from "../i18n"; import { t } from "../i18n";
import { useExcalidrawContainer, useDeviceType } from "../components/App"; import { useExcalidrawContainer, useDevice } from "../components/App";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import "./Dialog.scss"; import "./Dialog.scss";
import { back, close } from "./icons"; import { back, close } from "./icons";
import { Island } from "./Island"; import { Island } from "./Island";
import { Modal } from "./Modal"; import { Modal } from "./Modal";
import { AppState } from "../types"; import { AppState } from "../types";
import { queryFocusableElements } from "../utils";
export interface DialogProps { export interface DialogProps {
children: React.ReactNode; children: React.ReactNode;
@@ -64,14 +65,6 @@ export const Dialog = (props: DialogProps) => {
return () => islandNode.removeEventListener("keydown", handleKeyDown); return () => islandNode.removeEventListener("keydown", handleKeyDown);
}, [islandNode, props.autofocus]); }, [islandNode, props.autofocus]);
const queryFocusableElements = (node: HTMLElement) => {
const focusableElements = node.querySelectorAll<HTMLElement>(
"button, a, input, select, textarea, div[tabindex]",
);
return focusableElements ? Array.from(focusableElements) : [];
};
const onClose = () => { const onClose = () => {
(lastActiveElement as HTMLElement).focus(); (lastActiveElement as HTMLElement).focus();
props.onCloseRequest(); props.onCloseRequest();
@@ -92,9 +85,10 @@ export const Dialog = (props: DialogProps) => {
<button <button
className="Modal__close" className="Modal__close"
onClick={onClose} onClick={onClose}
title={t("buttons.close")}
aria-label={t("buttons.close")} aria-label={t("buttons.close")}
> >
{useDeviceType().isMobile ? back : close} {useDevice().isMobile ? back : close}
</button> </button>
</h2> </h2>
<div className="Dialog__content">{props.children}</div> <div className="Dialog__content">{props.children}</div>

99
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,99 @@
import clsx from "clsx";
import { ActionManager } from "../actions/manager";
import { AppState, ExcalidrawProps } from "../types";
import {
ExitZenModeAction,
FinalizeAction,
UndoRedoActions,
ZoomActions,
} from "./Actions";
import { useDevice } from "./App";
import { Island } from "./Island";
import { Section } from "./Section";
import Stack from "./Stack";
const Footer = ({
appState,
actionManager,
renderCustomFooter,
showExitZenModeBtn,
}: {
appState: AppState;
actionManager: ActionManager;
renderCustomFooter?: ExcalidrawProps["renderFooter"];
showExitZenModeBtn: boolean;
}) => {
const device = useDevice();
const showFinalize =
!appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
return (
<footer
role="contentinfo"
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
>
<div
className={clsx("layer-ui__wrapper__footer-left zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
>
<Stack.Col gap={2}>
<Section heading="canvasActions">
<Island padding={1}>
<ZoomActions
renderAction={actionManager.renderAction}
zoom={appState.zoom}
/>
</Island>
{!appState.viewModeEnabled && (
<>
<UndoRedoActions
renderAction={actionManager.renderAction}
className={clsx("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
})}
/>
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
</div>
</>
)}
{showFinalize && (
<FinalizeAction
renderAction={actionManager.renderAction}
className={clsx("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
appState.zenModeEnabled,
})}
/>
)}
</Section>
</Stack.Col>
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
appState.zenModeEnabled,
},
)}
>
{renderCustomFooter?.(false, appState)}
</div>
<ExitZenModeAction
executeAction={actionManager.executeAction}
showExitZenModeBtn={showExitZenModeBtn}
/>
</footer>
);
};
export default Footer;

View File

@@ -45,7 +45,7 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
return t("hints.text"); return t("hints.text");
} }
if (appState.activeTool.type === "image" && appState.pendingImageElement) { if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
return t("hints.placeImage"); return t("hints.placeImage");
} }

View File

@@ -5,7 +5,7 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { CanvasError } from "../errors"; import { CanvasError } from "../errors";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "./App"; import { useDevice } from "./App";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export"; import { exportToCanvas } from "../scene/export";
import { AppState, BinaryFiles } from "../types"; import { AppState, BinaryFiles } from "../types";
@@ -58,6 +58,7 @@ const ExportButton: React.FC<{
onClick: () => void; onClick: () => void;
title: string; title: string;
shade?: number; shade?: number;
children?: React.ReactNode;
}> = ({ children, title, onClick, color, shade = 6 }) => { }> = ({ children, title, onClick, color, shade = 6 }) => {
return ( return (
<button <button
@@ -170,7 +171,9 @@ const ImageExportModal = ({
<Stack.Row gap={2}> <Stack.Row gap={2}>
{actionManager.renderAction("changeExportScale")} {actionManager.renderAction("changeExportScale")}
</Stack.Row> </Stack.Row>
<p style={{ marginLeft: "1em", userSelect: "none" }}>Scale</p> <p style={{ marginLeft: "1em", userSelect: "none" }}>
{t("buttons.scale")}
</p>
</div> </div>
<div <div
style={{ style={{
@@ -250,7 +253,7 @@ export const ImageExportDialog = ({
icon={exportImage} icon={exportImage}
type="button" type="button"
aria-label={t("buttons.exportImage")} aria-label={t("buttons.exportImage")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
title={t("buttons.exportImage")} title={t("buttons.exportImage")}
/> />
{modalIsShown && ( {modalIsShown && (

View File

@@ -14,11 +14,11 @@ export const InitializeApp = (props: Props) => {
useEffect(() => { useEffect(() => {
const updateLang = async () => { const updateLang = async () => {
await setLanguage(currentLang); await setLanguage(currentLang);
setLoading(false);
}; };
const currentLang = const currentLang =
languages.find((lang) => lang.code === props.langCode) || defaultLang; languages.find((lang) => lang.code === props.langCode) || defaultLang;
updateLang(); updateLang();
setLoading(false);
}, [props.langCode]); }, [props.langCode]);
return loading ? <LoadingMessage /> : props.children; return loading ? <LoadingMessage /> : props.children;

View File

@@ -1,7 +1,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { useDeviceType } from "./App"; import { useDevice } from "./App";
import { AppState, ExportOpts, BinaryFiles } from "../types"; import { AppState, ExportOpts, BinaryFiles } from "../types";
import { Dialog } from "./Dialog"; import { Dialog } from "./Dialog";
import { exportFile, exportToFileIcon, link } from "./icons"; import { exportFile, exportToFileIcon, link } from "./icons";
@@ -117,7 +117,7 @@ export const JSONExportDialog = ({
icon={exportFile} icon={exportFile}
type="button" type="button"
aria-label={t("buttons.export")} aria-label={t("buttons.export")}
showAriaLabel={useDeviceType().isMobile} showAriaLabel={useDevice().isMobile}
title={t("buttons.export")} title={t("buttons.export")}
/> />
{modalIsShown && ( {modalIsShown && (

View File

@@ -1,9 +1,63 @@
@import "open-color/open-color"; @import "open-color/open-color";
@import "../css/variables.module";
.layer-ui__sidebar {
position: absolute;
top: var(--sat);
bottom: var(--sab);
right: var(--sar);
z-index: 5;
box-shadow: var(--shadow-island);
overflow: hidden;
border-radius: var(--border-radius-lg);
margin: var(--space-factor);
width: calc(#{$right-sidebar-width} - var(--space-factor) * 2);
.Island {
box-shadow: none;
}
.ToolIcon__icon {
border-radius: var(--border-radius-md);
}
.ToolIcon__icon__close {
.Modal__close {
width: calc(var(--space-factor) * 7);
height: calc(var(--space-factor) * 7);
display: flex;
justify-content: center;
align-items: center;
color: var(--color-text);
}
}
.Island {
--padding: 0;
background-color: var(--island-bg-color);
border-radius: var(--border-radius-lg);
padding: calc(var(--padding) * var(--space-factor));
position: relative;
transition: box-shadow 0.5s ease-in-out;
}
}
.excalidraw { .excalidraw {
.layer-ui__wrapper.animate {
transition: width 0.1s ease-in-out;
}
.layer-ui__wrapper { .layer-ui__wrapper {
// when the rightside sidebar is docked, we need to resize the UI by its
// width, making the nested UI content shift to the left. To do this,
// we need the UI container to actually have dimensions set, but
// then we also need to disable pointer events else the canvas below
// wouldn't be interactive.
position: absolute;
width: 100%;
height: 100%;
pointer-events: none;
z-index: var(--zIndex-layerUI); z-index: var(--zIndex-layerUI);
&__top-right { &__top-right {
display: flex; display: flex;
} }

View File

@@ -1,7 +1,7 @@
import clsx from "clsx"; import clsx from "clsx";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { CLASSES } from "../constants"; import { CLASSES, LIBRARY_SIDEBAR_WIDTH } from "../constants";
import { exportCanvas } from "../data"; import { exportCanvas } from "../data";
import { isTextElement, showSelectedShapeActions } from "../element"; import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types"; import { NonDeletedExcalidrawElement } from "../element/types";
@@ -10,7 +10,7 @@ import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types"; import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types"; import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { muteFSAbortError } from "../utils"; import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions"; import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import CollabButton from "./CollabButton"; import CollabButton from "./CollabButton";
import { ErrorDialog } from "./ErrorDialog"; import { ErrorDialog } from "./ErrorDialog";
@@ -25,9 +25,8 @@ import { PasteChartDialog } from "./PasteChartDialog";
import { Section } from "./Section"; import { Section } from "./Section";
import { HelpDialog } from "./HelpDialog"; import { HelpDialog } from "./HelpDialog";
import Stack from "./Stack"; import Stack from "./Stack";
import { Tooltip } from "./Tooltip";
import { UserList } from "./UserList"; import { UserList } from "./UserList";
import Library from "../data/library"; import Library, { distributeLibraryItemsOnSquareGrid } from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog"; import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton"; import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob"; import { isImageFileHandle } from "../data/blob";
@@ -37,7 +36,10 @@ import "./LayerUI.scss";
import "./Toolbar.scss"; import "./Toolbar.scss";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useDeviceType } from "../components/App"; import { useDevice } from "../components/App";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions/actionToggleStats";
import Footer from "./Footer";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@@ -50,55 +52,45 @@ interface LayerUIProps {
onLockToggle: () => void; onLockToggle: () => void;
onPenModeToggle: () => void; onPenModeToggle: () => void;
onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void; onInsertElements: (elements: readonly NonDeletedExcalidrawElement[]) => void;
zenModeEnabled: boolean;
showExitZenModeBtn: boolean; showExitZenModeBtn: boolean;
showThemeBtn: boolean; showThemeBtn: boolean;
toggleZenMode: () => void;
langCode: Language["code"]; langCode: Language["code"];
isCollaborating: boolean; isCollaborating: boolean;
renderTopRightUI?: ( renderTopRightUI?: ExcalidrawProps["renderTopRightUI"];
isMobile: boolean, renderCustomFooter?: ExcalidrawProps["renderFooter"];
appState: AppState, renderCustomStats?: ExcalidrawProps["renderCustomStats"];
) => JSX.Element | null;
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
UIOptions: AppProps["UIOptions"]; UIOptions: AppProps["UIOptions"];
focusContainer: () => void; focusContainer: () => void;
library: Library; library: Library;
id: string; id: string;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderCustomElementWidget?: (appState: AppState) => void;
} }
const LayerUI = ({ const LayerUI = ({
actionManager, actionManager,
appState, appState,
files, files,
setAppState, setAppState,
canvas,
elements, elements,
canvas,
onCollabButtonClick, onCollabButtonClick,
onLockToggle, onLockToggle,
onPenModeToggle, onPenModeToggle,
onInsertElements, onInsertElements,
zenModeEnabled,
showExitZenModeBtn, showExitZenModeBtn,
showThemeBtn, showThemeBtn,
toggleZenMode,
isCollaborating, isCollaborating,
renderTopRightUI, renderTopRightUI,
renderCustomFooter, renderCustomFooter,
viewModeEnabled, renderCustomStats,
libraryReturnUrl, libraryReturnUrl,
UIOptions, UIOptions,
focusContainer, focusContainer,
library, library,
id, id,
onImageAction, onImageAction,
renderCustomElementWidget,
}: LayerUIProps) => { }: LayerUIProps) => {
const deviceType = useDeviceType(); const device = useDevice();
const renderJSONExportDialog = () => { const renderJSONExportDialog = () => {
if (!UIOptions.canvasActions.export) { if (!UIOptions.canvasActions.export) {
@@ -174,7 +166,7 @@ const LayerUI = ({
<Section <Section
heading="canvasActions" heading="canvasActions"
className={clsx("zen-mode-transition", { className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled, "transition-left": appState.zenModeEnabled,
})} })}
> >
{/* the zIndex ensures this menu has higher stacking order, {/* the zIndex ensures this menu has higher stacking order,
@@ -195,7 +187,7 @@ const LayerUI = ({
<Section <Section
heading="canvasActions" heading="canvasActions"
className={clsx("zen-mode-transition", { className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled, "transition-left": appState.zenModeEnabled,
})} })}
> >
{/* the zIndex ensures this menu has higher stacking order, {/* the zIndex ensures this menu has higher stacking order,
@@ -218,8 +210,8 @@ const LayerUI = ({
)} )}
</Stack.Row> </Stack.Row>
<BackgroundPickerAndDarkModeToggle <BackgroundPickerAndDarkModeToggle
actionManager={actionManager}
appState={appState} appState={appState}
actionManager={actionManager}
setAppState={setAppState} setAppState={setAppState}
showThemeBtn={showThemeBtn} showThemeBtn={showThemeBtn}
/> />
@@ -235,7 +227,7 @@ const LayerUI = ({
<Section <Section
heading="selectedShapeActions" heading="selectedShapeActions"
className={clsx("zen-mode-transition", { className={clsx("zen-mode-transition", {
"transition-left": zenModeEnabled, "transition-left": appState.zenModeEnabled,
})} })}
> >
<Island <Island
@@ -252,7 +244,6 @@ const LayerUI = ({
appState={appState} appState={appState}
elements={elements} elements={elements}
renderAction={actionManager.renderAction} renderAction={actionManager.renderAction}
activeTool={appState.activeTool.type}
/> />
</Island> </Island>
</Section> </Section>
@@ -279,13 +270,14 @@ const LayerUI = ({
<LibraryMenu <LibraryMenu
pendingElements={getSelectedElements(elements, appState, true)} pendingElements={getSelectedElements(elements, appState, true)}
onClose={closeLibrary} onClose={closeLibrary}
onInsertShape={onInsertElements} onInsertLibraryItems={(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
}}
onAddToLibrary={deselectItems} onAddToLibrary={deselectItems}
setAppState={setAppState} setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer} focusContainer={focusContainer}
library={library} library={library}
theme={appState.theme}
files={files} files={files}
id={id} id={id}
appState={appState} appState={appState}
@@ -303,32 +295,34 @@ const LayerUI = ({
<div className="App-menu App-menu_top"> <div className="App-menu App-menu_top">
<Stack.Col <Stack.Col
gap={4} gap={4}
className={clsx({ "disable-pointerEvents": zenModeEnabled })} className={clsx({
"disable-pointerEvents": appState.zenModeEnabled,
})}
> >
{viewModeEnabled {appState.viewModeEnabled
? renderViewModeCanvasActions() ? renderViewModeCanvasActions()
: renderCanvasActions()} : renderCanvasActions()}
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()} {shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
</Stack.Col> </Stack.Col>
{!viewModeEnabled && ( {!appState.viewModeEnabled && (
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading: React.ReactNode) => (
<Stack.Col gap={4} align="start"> <Stack.Col gap={4} align="start">
<Stack.Row <Stack.Row
gap={1} gap={1}
className={clsx("App-toolbar-container", { className={clsx("App-toolbar-container", {
"zen-mode": zenModeEnabled, "zen-mode": appState.zenModeEnabled,
})} })}
> >
<PenModeButton <PenModeButton
zenModeEnabled={zenModeEnabled} zenModeEnabled={appState.zenModeEnabled}
checked={appState.penMode} checked={appState.penMode}
onChange={onPenModeToggle} onChange={onPenModeToggle}
title={t("toolBar.penMode")} title={t("toolBar.penMode")}
penDetected={appState.penDetected} penDetected={appState.penDetected}
/> />
<LockButton <LockButton
zenModeEnabled={zenModeEnabled} zenModeEnabled={appState.zenModeEnabled}
checked={appState.activeTool.locked} checked={appState.activeTool.locked}
onChange={() => onLockToggle()} onChange={() => onLockToggle()}
title={t("toolBar.lock")} title={t("toolBar.lock")}
@@ -336,13 +330,13 @@ const LayerUI = ({
<Island <Island
padding={1} padding={1}
className={clsx("App-toolbar", { className={clsx("App-toolbar", {
"zen-mode": zenModeEnabled, "zen-mode": appState.zenModeEnabled,
})} })}
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
elements={elements} elements={elements}
isMobile={deviceType.isMobile} isMobile={device.isMobile}
/> />
{heading} {heading}
<Stack.Row gap={1}> <Stack.Row gap={1}>
@@ -364,7 +358,6 @@ const LayerUI = ({
setAppState={setAppState} setAppState={setAppState}
/> />
</Stack.Row> </Stack.Row>
{libraryMenu}
</Stack.Col> </Stack.Col>
)} )}
</Section> </Section>
@@ -373,128 +366,22 @@ const LayerUI = ({
className={clsx( className={clsx(
"layer-ui__wrapper__top-right zen-mode-transition", "layer-ui__wrapper__top-right zen-mode-transition",
{ {
"transition-right": zenModeEnabled, "transition-right": appState.zenModeEnabled,
}, },
)} )}
> >
<UserList> <UserList
{appState.collaborators.size > 0 && collaborators={appState.collaborators}
Array.from(appState.collaborators) actionManager={actionManager}
// Collaborator is either not initialized or is actually the current user. />
.filter(([_, client]) => Object.keys(client).length !== 0) {renderTopRightUI?.(device.isMobile, appState)}
.map(([clientId, client]) => (
<Tooltip
label={client.username || "Unknown user"}
key={clientId}
>
{actionManager.renderAction("goToCollaborator", {
id: clientId,
})}
</Tooltip>
))}
</UserList>
{renderTopRightUI?.(deviceType.isMobile, appState)}
</div> </div>
</div> </div>
</FixedSideContainer> </FixedSideContainer>
); );
}; };
const renderBottomAppMenu = () => { return (
return (
<footer
role="contentinfo"
className="layer-ui__wrapper__footer App-menu App-menu_bottom"
>
<div
className={clsx(
"layer-ui__wrapper__footer-left zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-left": zenModeEnabled,
},
)}
>
<Stack.Col gap={2}>
<Section heading="canvasActions">
<Island padding={1}>
<ZoomActions
renderAction={actionManager.renderAction}
zoom={appState.zoom}
/>
</Island>
{!viewModeEnabled && (
<>
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
{actionManager.renderAction("redo", { size: "small" })}
</div>
<div
className={clsx("eraser-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
})}
>
{actionManager.renderAction("eraser", { size: "small" })}
{renderCustomElementWidget &&
renderCustomElementWidget(appState)}
</div>
</>
)}
{!viewModeEnabled &&
appState.multiElement &&
deviceType.isTouchScreen && (
<div
className={clsx("finalize-button zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left":
zenModeEnabled,
})}
>
{actionManager.renderAction("finalize", { size: "small" })}
</div>
)}
</Section>
</Stack.Col>
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
},
)}
>
{renderCustomFooter?.(false, appState)}
</div>
<div
className={clsx(
"layer-ui__wrapper__footer-right zen-mode-transition",
{
"transition-right disable-pointerEvents": zenModeEnabled,
},
)}
>
{actionManager.renderAction("toggleShortcuts")}
</div>
<button
className={clsx("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn,
})}
onClick={toggleZenMode}
>
{t("buttons.exitZenMode")}
</button>
</footer>
);
};
const dialogs = (
<> <>
{appState.isLoading && <LoadingMessage delay={250} />} {appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && ( {appState.errorMessage && (
@@ -522,57 +409,81 @@ const LayerUI = ({
} }
/> />
)} )}
</> {device.isMobile && (
); <MobileMenu
appState={appState}
return deviceType.isMobile ? ( elements={elements}
<> actionManager={actionManager}
{dialogs} libraryMenu={libraryMenu}
<MobileMenu renderJSONExportDialog={renderJSONExportDialog}
appState={appState} renderImageExportDialog={renderImageExportDialog}
elements={elements} setAppState={setAppState}
actionManager={actionManager} onCollabButtonClick={onCollabButtonClick}
libraryMenu={libraryMenu} onLockToggle={() => onLockToggle()}
renderJSONExportDialog={renderJSONExportDialog} onPenModeToggle={onPenModeToggle}
renderImageExportDialog={renderImageExportDialog} canvas={canvas}
setAppState={setAppState} isCollaborating={isCollaborating}
onCollabButtonClick={onCollabButtonClick} renderCustomFooter={renderCustomFooter}
onLockToggle={() => onLockToggle()} showThemeBtn={showThemeBtn}
onPenModeToggle={onPenModeToggle} onImageAction={onImageAction}
canvas={canvas} renderTopRightUI={renderTopRightUI}
isCollaborating={isCollaborating} renderCustomStats={renderCustomStats}
renderCustomFooter={renderCustomFooter} />
viewModeEnabled={viewModeEnabled}
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
/>
</>
) : (
<div
className={clsx("layer-ui__wrapper", {
"disable-pointerEvents":
appState.draggingElement ||
appState.resizingElement ||
(appState.editingElement && !isTextElement(appState.editingElement)),
})}
>
{dialogs}
{renderFixedSideContainer()}
{renderBottomAppMenu()}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)} )}
</div>
{!device.isMobile && (
<div
className={clsx("layer-ui__wrapper", {
"disable-pointerEvents":
appState.draggingElement ||
appState.resizingElement ||
(appState.editingElement &&
!isTextElement(appState.editingElement)),
})}
style={
appState.isLibraryOpen &&
appState.isLibraryMenuDocked &&
device.canDeviceFitSidebar
? { width: `calc(100% - ${LIBRARY_SIDEBAR_WIDTH}px)` }
: {}
}
>
{renderFixedSideContainer()}
<Footer
appState={appState}
actionManager={actionManager}
renderCustomFooter={renderCustomFooter}
showExitZenModeBtn={showExitZenModeBtn}
/>
{appState.showStats && (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
{appState.scrolledOutside && (
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
...calculateScrollCenter(elements, appState, canvas),
});
}}
>
{t("buttons.scrollBackToContent")}
</button>
)}
</div>
)}
{appState.isLibraryOpen && (
<div className="layer-ui__sidebar">{libraryMenu}</div>
)}
</>
); );
}; };

View File

@@ -3,6 +3,8 @@ import clsx from "clsx";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState } from "../types"; import { AppState } from "../types";
import { capitalizeString } from "../utils"; import { capitalizeString } from "../utils";
import { trackEvent } from "../analytics";
import { useDevice } from "./App";
const LIBRARY_ICON = ( const LIBRARY_ICON = (
<svg viewBox="0 0 576 512"> <svg viewBox="0 0 576 512">
@@ -18,6 +20,7 @@ export const LibraryButton: React.FC<{
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
isMobile?: boolean; isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => { }> = ({ appState, setAppState, isMobile }) => {
const device = useDevice();
return ( return (
<label <label
className={clsx( className={clsx(
@@ -34,7 +37,19 @@ export const LibraryButton: React.FC<{
type="checkbox" type="checkbox"
name="editor-library" name="editor-library"
onChange={(event) => { onChange={(event) => {
setAppState({ isLibraryOpen: event.target.checked }); document
.querySelector(".layer-ui__wrapper")
?.classList.remove("animate");
const nextState = event.target.checked;
setAppState({ isLibraryOpen: nextState });
// track only openings
if (nextState) {
trackEvent(
"library",
"toggleLibrary (open)",
`toolbar (${device.isMobile ? "mobile" : "desktop"})`,
);
}
}} }}
checked={appState.isLibraryOpen} checked={appState.isLibraryOpen}
aria-label={capitalizeString(t("toolBar.library"))} aria-label={capitalizeString(t("toolBar.library"))}

View File

@@ -2,7 +2,6 @@
.excalidraw { .excalidraw {
.layer-ui__library { .layer-ui__library {
margin: auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -11,8 +10,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
margin: 2px 0; margin: 2px 0 15px 0;
.Spinner { .Spinner {
margin-right: 1rem; margin-right: 1rem;
} }
@@ -21,13 +19,17 @@
// 2px from the left to account for focus border of left-most button // 2px from the left to account for focus border of left-most button
margin: 0 2px; margin: 0 2px;
} }
}
}
a { .layer-ui__sidebar {
margin-inline-start: auto; .layer-ui__library {
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra padding: 0;
padding-inline-end: 18px; height: 100%;
white-space: nowrap; }
} .library-menu-items-container {
height: 100%;
width: 100%;
} }
} }
@@ -65,4 +67,38 @@
} }
} }
} }
.library-menu-browse-button {
width: 80%;
min-height: 22px;
margin: 0 auto;
margin-top: 1rem;
padding: 10px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
border-radius: var(--border-radius-lg);
background-color: var(--color-primary);
color: $oc-white;
text-align: center;
white-space: nowrap;
text-decoration: none !important;
&:hover {
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--color-primary-darkest);
}
}
.library-menu-browse-button--mobile {
min-height: 22px;
margin-left: auto;
a {
padding-right: 0;
}
}
} }

View File

@@ -25,11 +25,11 @@ import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems"; import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants"; import { EVENT } from "../constants";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
import { trackEvent } from "../analytics"; import { trackEvent } from "../analytics";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { jotaiScope } from "../jotai"; import { jotaiScope } from "../jotai";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { useDevice } from "./App";
const useOnClickOutside = ( const useOnClickOutside = (
ref: RefObject<HTMLElement>, ref: RefObject<HTMLElement>,
@@ -77,10 +77,9 @@ const LibraryMenuWrapper = forwardRef<
export const LibraryMenu = ({ export const LibraryMenu = ({
onClose, onClose,
onInsertShape, onInsertLibraryItems,
pendingElements, pendingElements,
onAddToLibrary, onAddToLibrary,
theme,
setAppState, setAppState,
files, files,
libraryReturnUrl, libraryReturnUrl,
@@ -91,9 +90,8 @@ export const LibraryMenu = ({
}: { }: {
pendingElements: LibraryItem["elements"]; pendingElements: LibraryItem["elements"];
onClose: () => void; onClose: () => void;
onInsertShape: (elements: LibraryItem["elements"]) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: () => void; onAddToLibrary: () => void;
theme: AppState["theme"];
files: BinaryFiles; files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
@@ -104,17 +102,29 @@ export const LibraryMenu = ({
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => { const device = useDevice();
// If click on the library icon, do nothing. useOnClickOutside(
if ((event.target as Element).closest(".ToolIcon__library")) { ref,
return; useCallback(
} (event) => {
onClose(); // If click on the library icon, do nothing.
}); if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
if (!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar) {
onClose();
}
},
[onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar],
),
);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.ESCAPE) { if (
event.key === KEYS.ESCAPE &&
(!appState.isLibraryMenuDocked || !device.canDeviceFitSidebar)
) {
onClose(); onClose();
} }
}; };
@@ -122,7 +132,7 @@ export const LibraryMenu = ({
return () => { return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown); document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
}; };
}, [onClose]); }, [onClose, appState.isLibraryMenuDocked, device.canDeviceFitSidebar]);
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]); const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [showPublishLibraryDialog, setShowPublishLibraryDialog] = const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
@@ -211,7 +221,7 @@ export const LibraryMenu = ({
}, [setPublishLibSuccess, publishLibSuccess]); }, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = useCallback( const onPublishLibSuccess = useCallback(
(data, libraryItems: LibraryItems) => { (data: { url: string; authorName: string }, libraryItems: LibraryItems) => {
setShowPublishLibraryDialog(false); setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName }); setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice(); const nextLibItems = libraryItems.slice();
@@ -225,10 +235,6 @@ export const LibraryMenu = ({
[setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library], [setShowPublishLibraryDialog, setPublishLibSuccess, selectedItems, library],
); );
const [lastSelectedItem, setLastSelectedItem] = useState<
LibraryItem["id"] | null
>(null);
if ( if (
libraryItemsData.status === "loading" && libraryItemsData.status === "loading" &&
!libraryItemsData.isInitialized !libraryItemsData.isInitialized
@@ -275,56 +281,17 @@ export const LibraryMenu = ({
onAddToLibrary={(elements) => onAddToLibrary={(elements) =>
addToLibrary(elements, libraryItemsData.libraryItems) addToLibrary(elements, libraryItemsData.libraryItems)
} }
onInsertShape={onInsertShape} onInsertLibraryItems={onInsertLibraryItems}
pendingElements={pendingElements} pendingElements={pendingElements}
setAppState={setAppState} setAppState={setAppState}
appState={appState}
libraryReturnUrl={libraryReturnUrl} libraryReturnUrl={libraryReturnUrl}
library={library} library={library}
theme={theme} theme={appState.theme}
files={files} files={files}
id={id} id={id}
selectedItems={selectedItems} selectedItems={selectedItems}
onToggle={(id, event) => { onSelectItems={(ids) => setSelectedItems(ids)}
const shouldSelect = !selectedItems.includes(id);
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = libraryItemsData.libraryItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = libraryItemsData.libraryItems.findIndex(
(item) => item.id === id,
);
if (rangeStart === -1 || rangeEnd === -1) {
setSelectedItems([...selectedItems, id]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = libraryItemsData.libraryItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
setSelectedItems(nextSelectedIds);
} else {
setSelectedItems([...selectedItems, id]);
}
setLastSelectedItem(id);
} else {
setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id));
}
}}
onPublish={() => setShowPublishLibraryDialog(true)} onPublish={() => setShowPublishLibraryDialog(true)}
resetLibrary={resetLibrary} resetLibrary={resetLibrary}
/> />

View File

@@ -2,8 +2,17 @@
.excalidraw { .excalidraw {
.library-menu-items-container { .library-menu-items-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 0.5rem;
box-sizing: border-box;
.library-actions { .library-actions {
width: 100%;
display: flex; display: flex;
margin-right: auto;
align-items: center;
button .library-actions-counter { button .library-actions-counter {
position: absolute; position: absolute;
@@ -87,12 +96,16 @@
} }
} }
&__items { &__items {
max-height: 50vh; flex: 1;
overflow: auto; overflow-y: auto;
margin-top: 0.5rem; overflow-x: hidden;
margin-bottom: 1rem;
} }
.separator { .separator {
width: 100%;
display: flex;
align-items: center;
font-weight: 500; font-weight: 500;
font-size: 0.9rem; font-size: 0.9rem;
margin: 0.6em 0.2em; margin: 0.6em 0.2em;

View File

@@ -1,6 +1,6 @@
import { chunk } from "lodash"; import { chunk } from "lodash";
import { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json"; import { saveLibraryAsJSON, serializeLibraryAsJSON } from "../data/json";
import Library from "../data/library"; import Library from "../data/library";
import { ExcalidrawElement, NonDeleted } from "../element/types"; import { ExcalidrawElement, NonDeleted } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -11,34 +11,39 @@ import {
LibraryItem, LibraryItem,
LibraryItems, LibraryItems,
} from "../types"; } from "../types";
import { muteFSAbortError } from "../utils"; import { arrayToMap, muteFSAbortError } from "../utils";
import { useDeviceType } from "./App"; import { useDevice } from "./App";
import ConfirmDialog from "./ConfirmDialog"; import ConfirmDialog from "./ConfirmDialog";
import { exportToFileIcon, load, publishIcon, trash } from "./icons"; import { close, exportToFileIcon, load, publishIcon, trash } from "./icons";
import { LibraryUnit } from "./LibraryUnit"; import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack"; import Stack from "./Stack";
import { ToolButton } from "./ToolButton"; import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import "./LibraryMenuItems.scss"; import "./LibraryMenuItems.scss";
import { VERSIONS } from "../constants"; import { MIME_TYPES, VERSIONS } from "../constants";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { fileOpen } from "../data/filesystem";
import { SidebarLockButton } from "./SidebarLockButton";
import { trackEvent } from "../analytics";
const LibraryMenuItems = ({ const LibraryMenuItems = ({
isLoading, isLoading,
libraryItems, libraryItems,
onRemoveFromLibrary, onRemoveFromLibrary,
onAddToLibrary, onAddToLibrary,
onInsertShape, onInsertLibraryItems,
pendingElements, pendingElements,
theme, theme,
setAppState, setAppState,
appState,
libraryReturnUrl, libraryReturnUrl,
library, library,
files, files,
id, id,
selectedItems, selectedItems,
onToggle, onSelectItems,
onPublish, onPublish,
resetLibrary, resetLibrary,
}: { }: {
@@ -46,16 +51,17 @@ const LibraryMenuItems = ({
libraryItems: LibraryItems; libraryItems: LibraryItems;
pendingElements: LibraryItem["elements"]; pendingElements: LibraryItem["elements"];
onRemoveFromLibrary: () => void; onRemoveFromLibrary: () => void;
onInsertShape: (elements: LibraryItem["elements"]) => void; onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: (elements: LibraryItem["elements"]) => void; onAddToLibrary: (elements: LibraryItem["elements"]) => void;
theme: AppState["theme"]; theme: AppState["theme"];
files: BinaryFiles; files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
appState: AppState;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"]; libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library; library: Library;
id: string; id: string;
selectedItems: LibraryItem["id"][]; selectedItems: LibraryItem["id"][];
onToggle: (id: LibraryItem["id"], event: React.MouseEvent) => void; onSelectItems: (id: LibraryItem["id"][]) => void;
onPublish: () => void; onPublish: () => void;
resetLibrary: () => void; resetLibrary: () => void;
}) => { }) => {
@@ -87,9 +93,7 @@ const LibraryMenuItems = ({
}, [selectedItems, onRemoveFromLibrary, resetLibrary]); }, [selectedItems, onRemoveFromLibrary, resetLibrary]);
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false); const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const device = useDevice();
const isMobile = useDeviceType().isMobile;
const renderLibraryActions = () => { const renderLibraryActions = () => {
const itemsSelected = !!selectedItems.length; const itemsSelected = !!selectedItems.length;
const items = itemsSelected const items = itemsSelected
@@ -100,20 +104,34 @@ const LibraryMenuItems = ({
: t("buttons.resetLibrary"); : t("buttons.resetLibrary");
return ( return (
<div className="library-actions"> <div className="library-actions">
{(!itemsSelected || !isMobile) && ( {!itemsSelected && (
<ToolButton <ToolButton
key="import" key="import"
type="button" type="button"
title={t("buttons.load")} title={t("buttons.load")}
aria-label={t("buttons.load")} aria-label={t("buttons.load")}
icon={load} icon={load}
onClick={() => { onClick={async () => {
importLibraryFromJSON(library) try {
.catch(muteFSAbortError) await library.updateLibrary({
.catch((error) => { libraryItems: fileOpen({
console.error(error); description: "Excalidraw library files",
setAppState({ errorMessage: t("errors.importLibraryError") }); // ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
*/
}),
merge: true,
openLibraryMenu: true,
}); });
} catch (error: any) {
if (error?.name === "AbortError") {
console.warn(error);
return;
}
setAppState({ errorMessage: t("errors.importLibraryError") });
}
}} }}
className="library-actions--load" className="library-actions--load"
/> />
@@ -161,7 +179,7 @@ const LibraryMenuItems = ({
</ToolButton> </ToolButton>
</> </>
)} )}
{itemsSelected && !isPublished && ( {itemsSelected && (
<Tooltip label={t("hints.publishLibrary")}> <Tooltip label={t("hints.publishLibrary")}>
<ToolButton <ToolButton
type="button" type="button"
@@ -171,7 +189,7 @@ const LibraryMenuItems = ({
className="library-actions--publish" className="library-actions--publish"
onClick={onPublish} onClick={onPublish}
> >
{!isMobile && <label>{t("buttons.publishLibrary")}</label>} {!device.isMobile && <label>{t("buttons.publishLibrary")}</label>}
{selectedItems.length > 0 && ( {selectedItems.length > 0 && (
<span className="library-actions-counter"> <span className="library-actions-counter">
{selectedItems.length} {selectedItems.length}
@@ -180,17 +198,89 @@ const LibraryMenuItems = ({
</ToolButton> </ToolButton>
</Tooltip> </Tooltip>
)} )}
{device.isMobile && (
<div className="library-menu-browse-button--mobile">
<a
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary
}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
</div>
)}
</div> </div>
); );
}; };
const CELLS_PER_ROW = isMobile ? 4 : 6; const CELLS_PER_ROW = device.isMobile && !device.isSmScreen ? 6 : 4;
const referrer = const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname; libraryReturnUrl || window.location.origin + window.location.pathname;
const isPublished = selectedItems.some(
(id) => libraryItems.find((item) => item.id === id)?.status === "published", const [lastSelectedItem, setLastSelectedItem] = useState<
); LibraryItem["id"] | null
>(null);
const onItemSelectToggle = (
id: LibraryItem["id"],
event: React.MouseEvent,
) => {
const shouldSelect = !selectedItems.includes(id);
const orderedItems = [...unpublishedItems, ...publishedItems];
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = orderedItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = orderedItems.findIndex((item) => item.id === id);
if (rangeStart === -1 || rangeEnd === -1) {
onSelectItems([...selectedItems, id]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = orderedItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
onSelectItems(nextSelectedIds);
} else {
onSelectItems([...selectedItems, id]);
}
setLastSelectedItem(id);
} else {
setLastSelectedItem(null);
onSelectItems(selectedItems.filter((_id) => _id !== id));
}
};
const getInsertedElements = (id: string) => {
let targetElements;
if (selectedItems.includes(id)) {
targetElements = libraryItems.filter((item) =>
selectedItems.includes(item.id),
);
} else {
targetElements = libraryItems.filter((item) => item.id === id);
}
return targetElements;
};
const createLibraryItemCompo = (params: { const createLibraryItemCompo = (params: {
item: item:
@@ -212,8 +302,12 @@ const LibraryMenuItems = ({
onClick={params.onClick || (() => {})} onClick={params.onClick || (() => {})}
id={params.item?.id || null} id={params.item?.id || null}
selected={!!params.item?.id && selectedItems.includes(params.item.id)} selected={!!params.item?.id && selectedItems.includes(params.item.id)}
onToggle={(id, event) => { onToggle={onItemSelectToggle}
onToggle(id, event); onDrag={(id, event) => {
event.dataTransfer.setData(
MIME_TYPES.excalidrawlib,
serializeLibraryAsJSON(getInsertedElements(id)),
);
}} }}
/> />
</Stack.Col> </Stack.Col>
@@ -233,7 +327,7 @@ const LibraryMenuItems = ({
if (item.id) { if (item.id) {
return createLibraryItemCompo({ return createLibraryItemCompo({
item, item,
onClick: () => onInsertShape(item.elements), onClick: () => onInsertLibraryItems(getInsertedElements(item.id)),
key: item.id, key: item.id,
}); });
} }
@@ -272,53 +366,192 @@ const LibraryMenuItems = ({
}); });
}; };
const unpublishedItems = libraryItems.filter(
(item) => item.status !== "published",
);
const publishedItems = libraryItems.filter( const publishedItems = libraryItems.filter(
(item) => item.status === "published", (item) => item.status === "published",
); );
const unpublishedItems = [
// append pending library item
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...libraryItems.filter((item) => item.status !== "published"),
];
return ( const renderLibraryHeader = () => {
<div className="library-menu-items-container"> return (
{showRemoveLibAlert && renderRemoveLibAlert()} <>
<div className="layer-ui__library-header" key="library-header"> <div className="layer-ui__library-header" key="library-header">
{renderLibraryActions()} {renderLibraryActions()}
{isLoading ? ( {device.canDeviceFitSidebar && (
<Spinner /> <>
) : ( <div className="layer-ui__sidebar-lock-button">
<a <SidebarLockButton
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${ checked={appState.isLibraryMenuDocked}
window.name || "_blank" onChange={() => {
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${ document
VERSIONS.excalidrawLibrary .querySelector(".layer-ui__wrapper")
}`} ?.classList.add("animate");
target="_excalidraw_libraries" const nextState = !appState.isLibraryMenuDocked;
> setAppState({
{t("labels.libraries")} isLibraryMenuDocked: nextState,
</a> });
)} trackEvent(
</div> "library",
`toggleLibraryDock (${nextState ? "dock" : "undock"})`,
`sidebar (${device.isMobile ? "mobile" : "desktop"})`,
);
}}
/>
</div>
</>
)}
{!device.isMobile && (
<div className="ToolIcon__icon__close">
<button
className="Modal__close"
onClick={() =>
setAppState({
isLibraryOpen: false,
})
}
aria-label={t("buttons.close")}
>
{close}
</button>
</div>
)}
</div>
</>
);
};
const renderLibraryMenuItems = () => {
return (
<Stack.Col <Stack.Col
className="library-menu-items-container__items" className="library-menu-items-container__items"
align="start" align="start"
gap={1} gap={1}
style={{
flex: publishedItems.length > 0 ? 1 : "0 1 auto",
marginBottom: 0,
}}
> >
<> <>
<div className="separator">{t("labels.personalLib")}</div> <div className="separator">
{renderLibrarySection(unpublishedItems)} {(pendingElements.length > 0 ||
unpublishedItems.length > 0 ||
publishedItems.length > 0) && (
<div>{t("labels.personalLib")}</div>
)}
{isLoading && (
<div
style={{
marginLeft: "auto",
marginRight: "1rem",
display: "flex",
alignItems: "center",
fontWeight: "normal",
}}
>
<div style={{ transform: "translateY(2px)" }}>
<Spinner />
</div>
</div>
)}
</div>
{!pendingElements.length && !unpublishedItems.length ? (
<div
style={{
height: 65,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontSize: ".9rem",
}}
>
{t("library.noItems")}
<div
style={{
margin: ".6rem 0",
fontSize: ".8em",
width: "70%",
textAlign: "center",
}}
>
{publishedItems.length > 0
? t("library.hint_emptyPrivateLibrary")
: t("library.hint_emptyLibrary")}
</div>
</div>
) : (
renderLibrarySection([
// append pending library item
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...unpublishedItems,
])
)}
</> </>
<> <>
<div className="separator">{t("labels.excalidrawLib")} </div> {(publishedItems.length > 0 ||
(!device.isMobile &&
{renderLibrarySection(publishedItems)} (pendingElements.length > 0 || unpublishedItems.length > 0))) && (
<div className="separator">{t("labels.excalidrawLib")}</div>
)}
{publishedItems.length > 0 ? (
renderLibrarySection(publishedItems)
) : unpublishedItems.length > 0 ? (
<div
style={{
margin: "1rem 0",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontSize: ".9rem",
}}
>
{t("library.noItems")}
</div>
) : null}
</> </>
</Stack.Col> </Stack.Col>
);
};
const renderLibraryFooter = () => {
return (
<a
className="library-menu-browse-button"
href={`${process.env.REACT_APP_LIBRARY_URL}?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${
VERSIONS.excalidrawLibrary
}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
);
};
return (
<div
className="library-menu-items-container"
style={
device.isMobile
? {
minHeight: "200px",
maxHeight: "70vh",
}
: undefined
}
>
{showRemoveLibAlert && renderRemoveLibAlert()}
{renderLibraryHeader()}
{renderLibraryMenuItems()}
{!device.isMobile && renderLibraryFooter()}
</div> </div>
); );
}; };

View File

@@ -3,7 +3,7 @@
.excalidraw { .excalidraw {
.library-unit { .library-unit {
align-items: center; align-items: center;
border: 1px solid var(--button-gray-2); border: 1px solid transparent;
display: flex; display: flex;
justify-content: center; justify-content: center;
position: relative; position: relative;
@@ -21,10 +21,6 @@
} }
} }
&.theme--dark .library-unit {
border-color: rgb(48, 48, 48);
}
.library-unit__dragger { .library-unit__dragger {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -1,8 +1,7 @@
import clsx from "clsx"; import clsx from "clsx";
import oc from "open-color"; import oc from "open-color";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { MIME_TYPES } from "../constants"; import { useDevice } from "../components/App";
import { useDeviceType } from "../components/App";
import { exportToSvg } from "../scene/export"; import { exportToSvg } from "../scene/export";
import { BinaryFiles, LibraryItem } from "../types"; import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss"; import "./LibraryUnit.scss";
@@ -29,6 +28,7 @@ export const LibraryUnit = ({
onClick, onClick,
selected, selected,
onToggle, onToggle,
onDrag,
}: { }: {
id: LibraryItem["id"] | /** for pending item */ null; id: LibraryItem["id"] | /** for pending item */ null;
elements?: LibraryItem["elements"]; elements?: LibraryItem["elements"];
@@ -37,6 +37,7 @@ export const LibraryUnit = ({
onClick: () => void; onClick: () => void;
selected: boolean; selected: boolean;
onToggle: (id: string, event: React.MouseEvent) => void; onToggle: (id: string, event: React.MouseEvent) => void;
onDrag: (id: string, event: React.DragEvent) => void;
}) => { }) => {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => { useEffect(() => {
@@ -66,7 +67,7 @@ export const LibraryUnit = ({
}, [elements, files]); }, [elements, files]);
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const isMobile = useDeviceType().isMobile; const isMobile = useDevice().isMobile;
const adder = isPending && ( const adder = isPending && (
<div className="library-unit__adder">{PLUS_ICON}</div> <div className="library-unit__adder">{PLUS_ICON}</div>
); );
@@ -99,11 +100,12 @@ export const LibraryUnit = ({
: undefined : undefined
} }
onDragStart={(event) => { onDragStart={(event) => {
if (!id) {
event.preventDefault();
return;
}
setIsHovered(false); setIsHovered(false);
event.dataTransfer.setData( onDrag(id, event);
MIME_TYPES.excalidrawlib,
JSON.stringify(elements),
);
}} }}
/> />
{adder} {adder}

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { AppState } from "../types"; import { AppState, ExcalidrawProps } from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@@ -18,6 +18,8 @@ import { UserList } from "./UserList";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle"; import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
import { LibraryButton } from "./LibraryButton"; import { LibraryButton } from "./LibraryButton";
import { PenModeButton } from "./PenModeButton"; import { PenModeButton } from "./PenModeButton";
import { Stats } from "./Stats";
import { actionToggleStats } from "../actions";
type MobileMenuProps = { type MobileMenuProps = {
appState: AppState; appState: AppState;
@@ -32,14 +34,17 @@ type MobileMenuProps = {
onPenModeToggle: () => void; onPenModeToggle: () => void;
canvas: HTMLCanvasElement | null; canvas: HTMLCanvasElement | null;
isCollaborating: boolean; isCollaborating: boolean;
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element; renderCustomFooter?: (
viewModeEnabled: boolean; isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
showThemeBtn: boolean; showThemeBtn: boolean;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: ( renderTopRightUI?: (
isMobile: boolean, isMobile: boolean,
appState: AppState, appState: AppState,
) => JSX.Element | null; ) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@@ -56,16 +61,16 @@ export const MobileMenu = ({
canvas, canvas,
isCollaborating, isCollaborating,
renderCustomFooter, renderCustomFooter,
viewModeEnabled,
showThemeBtn, showThemeBtn,
onImageAction, onImageAction,
renderTopRightUI, renderTopRightUI,
renderCustomStats,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const renderToolbar = () => { const renderToolbar = () => {
return ( return (
<FixedSideContainer side="top" className="App-top-bar"> <FixedSideContainer side="top" className="App-top-bar">
<Section heading="shapes"> <Section heading="shapes">
{(heading) => ( {(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center"> <Stack.Col gap={4} align="center">
<Stack.Row gap={1} className="App-toolbar-container"> <Stack.Row gap={1} className="App-toolbar-container">
<Island padding={1} className="App-toolbar"> <Island padding={1} className="App-toolbar">
@@ -120,7 +125,7 @@ export const MobileMenu = ({
!appState.editingElement && !appState.editingElement &&
getSelectedElements(elements, appState).length === 0; getSelectedElements(elements, appState).length === 0;
if (viewModeEnabled) { if (appState.viewModeEnabled) {
return ( return (
<div className="App-toolbar-content"> <div className="App-toolbar-content">
{actionManager.renderAction("toggleCanvasMenu")} {actionManager.renderAction("toggleCanvasMenu")}
@@ -146,7 +151,7 @@ export const MobileMenu = ({
}; };
const renderCanvasActions = () => { const renderCanvasActions = () => {
if (viewModeEnabled) { if (appState.viewModeEnabled) {
return ( return (
<> <>
{renderJSONExportDialog()} {renderJSONExportDialog()}
@@ -180,7 +185,18 @@ export const MobileMenu = ({
}; };
return ( return (
<> <>
{!viewModeEnabled && renderToolbar()} {!appState.viewModeEnabled && renderToolbar()}
{!appState.openMenu && appState.showStats && (
<Stats
appState={appState}
setAppState={setAppState}
elements={elements}
onClose={() => {
actionManager.executeAction(actionToggleStats);
}}
renderCustomStats={renderCustomStats}
/>
)}
<div <div
className="App-bottom-bar" className="App-bottom-bar"
style={{ style={{
@@ -199,51 +215,43 @@ export const MobileMenu = ({
{appState.collaborators.size > 0 && ( {appState.collaborators.size > 0 && (
<fieldset> <fieldset>
<legend>{t("labels.collaborators")}</legend> <legend>{t("labels.collaborators")}</legend>
<UserList mobile> <UserList
{Array.from(appState.collaborators) mobile
// Collaborator is either not initialized or is actually the current user. collaborators={appState.collaborators}
.filter( actionManager={actionManager}
([_, client]) => Object.keys(client).length !== 0, />
)
.map(([clientId, client]) => (
<React.Fragment key={clientId}>
{actionManager.renderAction("goToCollaborator", {
id: clientId,
})}
</React.Fragment>
))}
</UserList>
</fieldset> </fieldset>
)} )}
</Stack.Col> </Stack.Col>
</div> </div>
</Section> </Section>
) : appState.openMenu === "shape" && ) : appState.openMenu === "shape" &&
!viewModeEnabled && !appState.viewModeEnabled &&
showSelectedShapeActions(appState, elements) ? ( showSelectedShapeActions(appState, elements) ? (
<Section className="App-mobile-menu" heading="selectedShapeActions"> <Section className="App-mobile-menu" heading="selectedShapeActions">
<SelectedShapeActions <SelectedShapeActions
appState={appState} appState={appState}
elements={elements} elements={elements}
renderAction={actionManager.renderAction} renderAction={actionManager.renderAction}
activeTool={appState.activeTool.type}
/> />
</Section> </Section>
) : null} ) : null}
<footer className="App-toolbar"> <footer className="App-toolbar">
{renderAppToolbar()} {renderAppToolbar()}
{appState.scrolledOutside && !appState.openMenu && ( {appState.scrolledOutside &&
<button !appState.openMenu &&
className="scroll-back-to-content" !appState.isLibraryOpen && (
onClick={() => { <button
setAppState({ className="scroll-back-to-content"
...calculateScrollCenter(elements, appState, canvas), onClick={() => {
}); setAppState({
}} ...calculateScrollCenter(elements, appState, canvas),
> });
{t("buttons.scrollBackToContent")} }}
</button> >
)} {t("buttons.scrollBackToContent")}
</button>
)}
</footer> </footer>
</Island> </Island>
</div> </div>

View File

@@ -4,11 +4,11 @@ import React, { useState, useLayoutEffect, useRef } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import clsx from "clsx"; import clsx from "clsx";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { useExcalidrawContainer, useDeviceType } from "./App"; import { useExcalidrawContainer, useDevice } from "./App";
import { AppState } from "../types"; import { AppState } from "../types";
import { THEME } from "../constants"; import { THEME } from "../constants";
export const Modal = (props: { export const Modal: React.FC<{
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
maxWidth?: number; maxWidth?: number;
@@ -16,7 +16,7 @@ export const Modal = (props: {
labelledBy: string; labelledBy: string;
theme?: AppState["theme"]; theme?: AppState["theme"];
closeOnClickOutside?: boolean; closeOnClickOutside?: boolean;
}) => { }> = (props) => {
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props; const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
const modalRoot = useBodyRoot(theme); const modalRoot = useBodyRoot(theme);
@@ -59,17 +59,17 @@ export const Modal = (props: {
const useBodyRoot = (theme: AppState["theme"]) => { const useBodyRoot = (theme: AppState["theme"]) => {
const [div, setDiv] = useState<HTMLDivElement | null>(null); const [div, setDiv] = useState<HTMLDivElement | null>(null);
const deviceType = useDeviceType(); const device = useDevice();
const isMobileRef = useRef(deviceType.isMobile); const isMobileRef = useRef(device.isMobile);
isMobileRef.current = deviceType.isMobile; isMobileRef.current = device.isMobile;
const { container: excalidrawContainer } = useExcalidrawContainer(); const { container: excalidrawContainer } = useExcalidrawContainer();
useLayoutEffect(() => { useLayoutEffect(() => {
if (div) { if (div) {
div.classList.toggle("excalidraw--mobile", deviceType.isMobile); div.classList.toggle("excalidraw--mobile", device.isMobile);
} }
}, [div, deviceType.isMobile]); }, [div, device.isMobile]);
useLayoutEffect(() => { useLayoutEffect(() => {
const isDarkTheme = const isDarkTheme =

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import React, { useLayoutEffect, useRef, useEffect } from "react"; import React, { useLayoutEffect, useRef, useEffect } from "react";
import "./Popover.scss"; import "./Popover.scss";
import { unstable_batchedUpdates } from "react-dom"; import { unstable_batchedUpdates } from "react-dom";
import { queryFocusableElements } from "../utils";
import { KEYS } from "../keys";
type Props = { type Props = {
top?: number; top?: number;
@@ -27,17 +29,66 @@ export const Popover = ({
}: Props) => { }: Props) => {
const popoverRef = useRef<HTMLDivElement>(null); const popoverRef = useRef<HTMLDivElement>(null);
const container = popoverRef.current;
useEffect(() => {
if (!container) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.TAB) {
const focusableElements = queryFocusableElements(container);
const { activeElement } = document;
const currentIndex = focusableElements.findIndex(
(element) => element === activeElement,
);
if (currentIndex === 0 && event.shiftKey) {
focusableElements[focusableElements.length - 1].focus();
event.preventDefault();
event.stopImmediatePropagation();
} else if (
currentIndex === focusableElements.length - 1 &&
!event.shiftKey
) {
focusableElements[0].focus();
event.preventDefault();
event.stopImmediatePropagation();
}
}
};
container.addEventListener("keydown", handleKeyDown);
return () => container.removeEventListener("keydown", handleKeyDown);
}, [container]);
// ensure the popover doesn't overflow the viewport // ensure the popover doesn't overflow the viewport
useLayoutEffect(() => { useLayoutEffect(() => {
if (fitInViewport && popoverRef.current) { if (fitInViewport && popoverRef.current) {
const element = popoverRef.current; const element = popoverRef.current;
const { x, y, width, height } = element.getBoundingClientRect(); const { x, y, width, height } = element.getBoundingClientRect();
//Position correctly when clicked on rightmost part or the bottom part of viewport
if (x + width - offsetLeft > viewportWidth) { if (x + width - offsetLeft > viewportWidth) {
element.style.left = `${viewportWidth - width}px`; element.style.left = `${viewportWidth - width - 10}px`;
} }
if (y + height - offsetTop > viewportHeight) { if (y + height - offsetTop > viewportHeight) {
element.style.top = `${viewportHeight - height}px`; 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]); }, [fitInViewport, viewportWidth, viewportHeight, offsetLeft, offsetTop]);

View File

@@ -82,6 +82,10 @@
} }
} }
&-warning {
color: $oc-red-6;
}
&-note { &-note {
padding: 1em; padding: 1em;
font-style: italic; font-style: italic;

View File

@@ -295,6 +295,11 @@ const PublishLibrary = ({
}, [clonedLibItems, onClose, updateItemsInStorage, libraryData]); }, [clonedLibItems, onClose, updateItemsInStorage, libraryData]);
const shouldRenderForm = !!libraryItems.length; const shouldRenderForm = !!libraryItems.length;
const containsPublishedItems = libraryItems.some(
(item) => item.status === "published",
);
return ( return (
<Dialog <Dialog
onCloseRequest={onDialogClose} onCloseRequest={onDialogClose}
@@ -329,6 +334,11 @@ const PublishLibrary = ({
<div className="publish-library-note"> <div className="publish-library-note">
{t("publishDialog.noteItems")} {t("publishDialog.noteItems")}
</div> </div>
{containsPublishedItems && (
<span className="publish-library-note publish-library-warning">
{t("publishDialog.republishWarning")}
</span>
)}
{renderLibraryItems()} {renderLibraryItems()}
<div className="publish-library__fields"> <div className="publish-library__fields">
<label> <label>

View File

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

View File

@@ -0,0 +1,22 @@
@import "../css/variables.module";
.excalidraw {
.layer-ui__sidebar-lock-button {
@include toolbarButtonColorStates;
margin-right: 0.2rem;
}
.ToolIcon_type_floating .side_lock_icon {
width: calc(var(--space-factor) * 7);
height: calc(var(--space-factor) * 7);
svg {
// mirror
transform: scale(-1, 1);
}
}
.ToolIcon_type_checkbox {
&:not(.ToolIcon_toggle_opaque):checked + .side_lock_icon {
background-color: var(--color-primary);
}
}
}

View File

@@ -0,0 +1,46 @@
import "./ToolIcon.scss";
import React from "react";
import clsx from "clsx";
import { ToolButtonSize } from "./ToolButton";
import { t } from "../i18n";
import { Tooltip } from "./Tooltip";
import "./SidebarLockButton.scss";
type SidebarLockIconProps = {
checked: boolean;
onChange?(): void;
};
const DEFAULT_SIZE: ToolButtonSize = "medium";
const SIDE_LIBRARY_TOGGLE_ICON = (
<svg viewBox="0 0 24 24" fill="#ffffff">
<path d="M19 22H5a3 3 0 01-3-3V5a3 3 0 013-3h14a3 3 0 013 3v14a3 3 0 01-3 3zm0-18h-9v16h9a1.01 1.01 0 001-1V5a1.01 1.01 0 00-1-1z"></path>
</svg>
);
export const SidebarLockButton = (props: SidebarLockIconProps) => {
return (
<Tooltip label={t("labels.sidebarLock")}>
<label
className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
`ToolIcon_size_${DEFAULT_SIZE}`,
)}
>
<input
className="ToolIcon_type_checkbox"
type="checkbox"
onChange={props.onChange}
checked={props.checked}
aria-label={t("labels.sidebarLock")}
/>{" "}
<div className="ToolIcon__icon side_lock_icon" tabIndex={0}>
{SIDE_LIBRARY_TOGGLE_ICON}
</div>{" "}
</label>{" "}
</Tooltip>
);
};

View File

@@ -3,11 +3,24 @@
.excalidraw { .excalidraw {
.single-library-item { .single-library-item {
position: relative; position: relative;
&-status {
position: absolute;
top: 0.3rem;
left: 0.3rem;
font-size: 0.7rem;
color: $oc-red-7;
background: rgba(255, 255, 255, 0.9);
padding: 0.1rem 0.2rem;
border-radius: 0.2rem;
}
&__svg { &__svg {
background-color: $oc-white;
padding: 0.3rem;
width: 7.5rem; width: 7.5rem;
height: 7.5rem; height: 7.5rem;
border: 1px solid var(--button-gray-2); border: 1px solid var(--button-gray-2);
margin: 0.3rem;
svg { svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -40,7 +53,7 @@
&--remove { &--remove {
position: absolute; position: absolute;
top: 0.2rem; top: 0.2rem;
right: 1.3rem; right: 1rem;
.ToolIcon__icon { .ToolIcon__icon {
margin: 0; margin: 0;

View File

@@ -45,6 +45,11 @@ const SingleLibraryItem = ({
return ( return (
<div className="single-library-item"> <div className="single-library-item">
{libItem.status === "published" && (
<span className="single-library-item-status">
{t("labels.statusPublished")}
</span>
)}
<div ref={svgRef} className="single-library-item__svg" /> <div ref={svgRef} className="single-library-item__svg" />
<ToolButton <ToolButton
aria-label={t("buttons.remove")} aria-label={t("buttons.remove")}

View File

@@ -41,6 +41,7 @@ const ColStack = ({
align, align,
justifyContent, justifyContent,
className, className,
style,
}: StackProps) => { }: StackProps) => {
return ( return (
<div <div
@@ -49,6 +50,7 @@ const ColStack = ({
"--gap": gap, "--gap": gap,
justifyItems: align, justifyItems: align,
justifyContent, justifyContent,
...style,
}} }}
> >
{children} {children}

View File

@@ -7,6 +7,7 @@
right: 12px; right: 12px;
font-size: 12px; font-size: 12px;
z-index: 10; z-index: 10;
pointer-events: all;
h3 { h3 {
margin: 0 24px 8px 0; margin: 0 24px 8px 0;

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