Compare commits

...

260 Commits

Author SHA1 Message Date
6d5813e9d2 fix: restore elements and app state in updateScene 2022-01-13 12:57:08 +05:30
1ef287027b fix: support collaboration in bounded text (#4580) 2022-01-12 23:21:45 +01:00
a51ed9ced6 feat: support decreasing/increasing fontSize via keyboard (#4553)
Co-authored-by: david <dw@dw.local>
2022-01-12 15:21:36 +01:00
4501d6d630 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#4510)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.4 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-12 15:33:05 +05:30
92a5936c7f fix: port for collab server and update docs (#4569) 2022-01-11 18:40:09 +05:30
50bd5fbae1 fix: don't mutate the bounded text if not updated when submitted (#4543)
* fix: don't mutate the bounded text if not updated when submitted

* dont update text for bounded text unless submitted

* add specs

* use node 16

* fix

* Update text when editing and cache prev text

* update prev text when props updated

* remove only

* type properly and remove unnecessary type checks

* cache original text and compare with editor value to fix alignement issue after editing and add specs

* naming tweak

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-01-11 16:36:08 +05:30
62bead66d7 chore(deps-dev): bump typescript in /src/packages/excalidraw (#4432)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.3 to 4.5.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.5.3...v4.5.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-10 12:40:14 +05:30
b3073984b3 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#4513)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.16.0 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-10 12:40:01 +05:30
3c9ee13979 chore(deps-dev): bump autoprefixer in /src/packages/excalidraw (#4554)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.4.0 to 10.4.2.
- [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.0...10.4.2)

---
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-01-10 12:39:42 +05:30
228c8136cf chore(deps-dev): bump mini-css-extract-plugin (#4555)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.4.5 to 2.4.6.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.4.5...v2.4.6)

---
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-01-10 12:39:29 +05:30
324dd460c8 chore(deps-dev): bump lint-staged from 12.1.4 to 12.1.7 (#4557)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.1.4 to 12.1.7.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.1.4...v12.1.7)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-10 12:38:48 +05:30
d8ea085a94 chore(deps): bump sass from 1.45.2 to 1.47.0 (#4561)
Bumps [sass](https://github.com/sass/dart-sass) from 1.45.2 to 1.47.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.45.2...1.47.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-01-10 12:38:20 +05:30
adbd486f32 chore(deps): bump @tldraw/vec from 1.4.0 to 1.4.3 (#4562)
Bumps [@tldraw/vec](https://github.com/tldraw/tldraw) from 1.4.0 to 1.4.3.
- [Release notes](https://github.com/tldraw/tldraw/releases)
- [Commits](https://github.com/tldraw/tldraw/compare/v1.4.0...v1.4.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-10 12:38:08 +05:30
0a89c4b0c8 fix: prevent canvas drag while editing text (#4552) 2022-01-08 23:50:25 +01:00
c03845bac3 fix: support shift+P for freedraw (#4550)
* fix: support shift+P for freedraw

* newline

* show shift+p first
2022-01-08 18:01:22 +05:30
d5a6014076 fix: prefer spreadsheet data over image (#4533) 2022-01-07 23:18:04 +01:00
74861b1398 Fix shortcut for Draw tool in help dialog, it's not SHIFT+P anymore but just X (#4548) 2022-01-07 23:50:42 +05:30
ac71ee7278 feat: link to new LP for excalidraw plus (#4549) 2022-01-07 16:32:35 +01:00
9088df8f5a chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#4526)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.16.5 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-07 12:42:48 +05:30
c5fe0cd446 chore(deps-dev): bump webpack-dev-server in /src/packages/excalidraw (#4525)
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.7.1 to 4.7.2.
- [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.1...v4.7.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-07 12:00:08 +05:30
9f8783c2dd chore(deps-dev): bump @babel/plugin-transform-typescript (#4527)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.16.1 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-plugin-transform-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-07 11:59:46 +05:30
b475412199 feat: support updating library in updateScene API (#4546)
* feat: support updating library in updateScene API

* fix

* update docs

* Update src/packages/excalidraw/CHANGELOG.md
2022-01-06 21:37:33 +05:30
5f1616f2c5 fix: show text properties button states correctly for bounded text (#4542)
* fix: show text properties button states correctly for bounded text

* rename
2022-01-05 17:58:03 +05:30
cec92c1d17 feat: update stroke color of bounded text along with container (#4541) 2022-01-05 16:27:48 +05:30
5f476e09d4 chore(deps-dev): bump @babel/core in /src/packages/utils (#4528)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.16.5 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-05 15:54:34 +05:30
9aa6a27252 chore(deps): bump @types/jest from 27.0.3 to 27.4.0 (#4529)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 27.0.3 to 27.4.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-05 15:54:04 +05:30
a2e8806f57 fix: rotate bounded text when container is rotated before typing (#4535) 2022-01-04 18:03:24 +05:30
b71e702991 fix: undo should work when selecting bounded textr (#4537) 2022-01-04 18:02:16 +05:30
5c67329be6 fix: Reduce padding to 5px for bounded text (#4530)
* fix: Reduce padding to 5px

* reduce width by 50 to fix tests

* Push the word if appending space exceeds max width when breaking words

* fix spec
2022-01-03 17:59:26 +05:30
28546fbb55 fix: bound text doesn't inherit container (#4521) 2021-12-31 14:55:02 +01:00
b0cccbb9e8 chore(deps): bump @tldraw/vec from 1.2.9 to 1.4.0 (#4517)
Bumps [@tldraw/vec](https://github.com/tldraw/tldraw) from 1.2.9 to 1.4.0.
- [Release notes](https://github.com/tldraw/tldraw/releases)
- [Commits](https://github.com/tldraw/tldraw/compare/v1.2.9...v1.4.0)

---
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>
2021-12-31 14:44:44 +02:00
b621d065de feat: hints and shortcuts help around deep selection (#4502) 2021-12-31 11:00:20 +01:00
96580c92a5 chore(deps-dev): bump @babel/plugin-transform-arrow-functions (#4515)
Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.16.5 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-plugin-transform-arrow-functions)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 12:42:29 +05:30
975441549b chore(deps): bump sass from 1.43.5 to 1.45.2 (#4518)
Bumps [sass](https://github.com/sass/dart-sass) from 1.43.5 to 1.45.2.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.43.5...1.45.2)

---
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>
2021-12-31 12:42:04 +05:30
4be701416a chore(deps-dev): bump @babel/preset-react in /src/packages/excalidraw (#4519)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.16.0 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-preset-react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 12:41:31 +05:30
1acb1e33f1 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#4516)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.4 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 01:11:22 +00:00
986e1e40d3 chore(deps-dev): bump @babel/preset-typescript (#4514)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.16.0 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 01:00:57 +00:00
fab4a0e060 chore(deps-dev): bump @babel/plugin-transform-runtime (#4508)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.16.4 to 7.16.7.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.7/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:59:42 +02:00
b265ebf88f chore(deps): bump @tldraw/vec from 1.1.5 to 1.2.9 (#4479)
Bumps [@tldraw/vec](https://github.com/tldraw/tldraw) from 1.1.5 to 1.2.9.
- [Release notes](https://github.com/tldraw/tldraw/releases)
- [Commits](https://github.com/tldraw/tldraw/compare/v1.1.5...v1.2.9)

---
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>
2021-12-31 02:52:36 +02:00
351845019e chore(deps-dev): bump @types/pako from 1.0.2 to 1.0.3 (#4481)
Bumps [@types/pako](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pako) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pako)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:51:20 +02:00
c0fcce6f27 chore(deps): bump @testing-library/jest-dom from 5.15.1 to 5.16.1 (#4395)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.15.1 to 5.16.1.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.15.1...v5.16.1)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  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>
2021-12-31 02:50:30 +02:00
b093d2d2b6 chore(deps-dev): bump prettier from 2.5.0 to 2.5.1 (#4360)
Bumps [prettier](https://github.com/prettier/prettier) from 2.5.0 to 2.5.1.
- [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.0...2.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:49:43 +02:00
69548c5502 chore(deps): bump @types/react from 17.0.37 to 17.0.38 (#4483)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.37 to 17.0.38.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:49:15 +02:00
6ca0afa6e5 chore(deps-dev): bump @types/chai from 4.2.22 to 4.3.0 (#4394)
Bumps [@types/chai](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chai) from 4.2.22 to 4.3.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chai)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:48:59 +02:00
c50f81b829 chore(deps-dev): bump @babel/plugin-transform-arrow-functions (#4426)
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.16.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.5/packages/babel-plugin-transform-arrow-functions)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:47:42 +02:00
b122c8c4eb chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#4437)
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.16.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.5/packages/babel-plugin-transform-async-to-generator)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:47:16 +02:00
9a7216fe94 chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#4423)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.2.5 to 5.3.0.
- [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.2.5...v5.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:46:52 +02:00
8eee749076 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#4430)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.16.0 to 7.16.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.5/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:46:36 +02:00
2158ad0656 chore(deps-dev): bump @babel/core in /src/packages/utils (#4435)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.16.0 to 7.16.5.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.5/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:45:49 +02:00
74c3fea7f5 chore(deps): bump typescript from 4.5.2 to 4.5.4 (#4442)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.2 to 4.5.4.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.5.2...v4.5.4)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:45:31 +02:00
5e456e6d05 chore(deps-dev): bump lint-staged from 12.1.2 to 12.1.4 (#4478)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.1.2 to 12.1.4.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.1.2...v12.1.4)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-31 02:45:09 +02:00
477cce2ed6 fix: Text wrapping with grid (#4505) (#4506) 2021-12-30 19:10:31 +01:00
dd8e465304 feat: Support updating text properties by clicking on container (#4499) 2021-12-29 16:49:52 +05:30
11396a21de fix: check if process is defined before using so it works in browser (#4497)
* refactor: use isTestEnv() utils where applicable

* check if process is defined
2021-12-28 17:17:41 +05:30
38236bc5e0 tests: Add tests for wrapText util (#4495) 2021-12-28 16:52:57 +05:30
63ce5b82d7 fix: pending review fixes for sticky notes (#4493) 2021-12-28 16:24:44 +05:30
bae0e985b2 fix: prevent browser from scrolling when panning (#4489) 2021-12-27 14:18:11 +01:00
04f852a40a build: Added example folder for testing @excalidraw/excalidraw in local (#4488)
* build: Added example folder for testing @excalidraw/excalidraw in local

* remove unnecessary files

* use scss

* update docs

* newline

* remove index

* remove yarn

* use the bundled excalidraw.development.js for better testing and font will also be available

* remove src folder from example
2021-12-27 18:01:33 +05:30
f463c047c0 fix: pasted elements except binded text once paste action is complete (#4472) 2021-12-23 22:07:16 +05:30
1fd347cade fix: don't select binded text when ungrouping (#4470) 2021-12-23 21:36:29 +05:30
ef62390841 fix: set height correctly when text properties updated while editing in container until first submit (#4469)
* fix: set height correctly when text properties updated while editing in container

* rename PADDING to BOUND_TEXT_PADDING
2021-12-23 17:02:35 +05:30
bf2bca221e fix: align and distribute binded text in container and cleanup (#4468) 2021-12-23 17:02:13 +05:30
d0733b1960 fix: move binded text when moving container using keyboard (#4466) 2021-12-23 01:47:14 +05:30
64c2d76cfa fix: support dragging binded text in container selected in a group (#4462)
* fix: support moving binded text when container selected via group

* update coords of bounded text only when element doesn't belong to any group or element in group is selected

* dnt drag binded text when nested group selected

* Update src/element/dragElements.ts

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

Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-12-22 19:16:49 +05:30
c76784b774 fix: Scope drag and drop events to Excalidraw container to prevent overriding the host drag and drop events (#4445)
* cross-env

* reverting lib

https://github.com/excalidraw/excalidraw/issues/4282

* Revert "reverting lib"

This reverts commit 840726806a.

* Update package.json

* Update App.tsx

* Update App.tsx

* lint

* updated changelog

* Update src/packages/excalidraw/CHANGELOG.md

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

* Update src/packages/excalidraw/CHANGELOG.md

* Move fixes above build header

* Update src/packages/excalidraw/CHANGELOG.md

* lint

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Co-authored-by: David Luzar <luzar.david@gmail.com>
2021-12-22 18:55:34 +05:30
25e54e5999 fix: vertically align single line when deleting text in bounded container (#4460) 2021-12-22 15:32:21 +05:30
55b7a7d554 fix: update height correctly when updating text properties in binded text (#4459)
* fix: update height correctly when updating text properties in binded text

* read height from editor style so its accurate

* fix
2021-12-22 12:08:51 +05:30
c1c37a6ee7 fix: align library item previews to center (#4447) 2021-12-21 19:59:36 +01:00
25b529f519 fix: vertically center align text when text deleted (#4457) 2021-12-22 00:07:55 +05:30
8e6a747873 fix: vertically center the first line as user starts typing in container (#4454)
* fix: vertically center the first line as user starts typing in container

* fix
2021-12-21 23:08:36 +05:30
089b05db1b fix: switch cursor to center of container when adding text when dimensions are too small (#4452) 2021-12-21 19:00:01 +05:30
081e097cef fix: vertically center align the bounded text correctly when zoomed (#4444)
* fix: vertically center align the bounded text correctly when zoomed

* dnt add offsets since its calculated correctly

* set editor max width better when offsets present

* Update src/element/textWysiwyg.tsx

* const

* revert
2021-12-21 17:13:11 +05:30
8b5657e1ce fix: support updating stroke color for text by typing in color picker input (#4415)
* fix: support updating stroke color for text by typing in color picker input

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

* focus editor on color picker blur

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

* fixes for auto increase in height

* fix height

* respect newlines when wrapping text

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

* select bound text if present when rect selected

* mutate y coord after text submit

* Add padding of 30px and update dimensions acordingly

* Don't allow selecting bound text element directly

* support deletion of bound text element when rect deleted

* trim text

* Support autoshrink and improve algo

* calculate approx line height instead of hardcoding

* use textContainerId instead of storing textContainer element itself

* rename boundTextElement -> boundTextElementId

* fix text properties not getting reflected after edit inside rect

* Support resizing

* remove ts ignore

* increase height of container when text height increases while resizing

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

* fix tests

* add util isRectangleElement

* use isTextElement util everywhere

* disable selecting text inside rect when selectAll

* Bind text to circle and diamond as well

* fix tests

* vertically center align the text always

* better vertical align

* Disable binding arrows for text inside shapes

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

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

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

* fix test

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

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

* use console.info so eslint doesnt warn :p

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

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

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

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

* Add hint when container selected

* Select container when escape key is pressed after submitting text

* fix copy/paste when using copy/paste action

* fix copy when dragged with alt pressed

* fix export to svg/png

* fix add to library

* Fix copy as png/svg

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

* fix rotation jump

* moove all text utils to textElement.ts

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

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

* fix typo, use string for character type

* renaming

* fix bugs in word wrap algo

* make grouping work

* set boundTextElementId only when text present else unset it

* rename textContainerId to containerId

* fix

* fix snap

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

* use boundElementIds and also support binding text in images 🎉

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

* fix y coord when resizing from north

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

* bind when clicked on center of container

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

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

* review fixes

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

* fix

* don't duplicate boundElementIds when text submitted

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

* set width correctly when resizing to fix alignment issues

* make duplication work using cmd/ctrl+d

* set X coord correctly during resize

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

* fix, check last char is space

* remove logs

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-13 11:42:29 +05:30
e15d73d94c chore(deps-dev): bump typescript in /src/packages/excalidraw (#4392)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v4.5.2...v4.5.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-13 11:42:18 +05:30
80ee097b85 chore(deps-dev): bump webpack in /src/packages/utils (#4391)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.4 to 5.65.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.4...v5.65.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-13 11:42:03 +05:30
10048b877b chore(deps-dev): bump webpack in /src/packages/excalidraw (#4389)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.4 to 5.65.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.4...v5.65.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-13 10:41:00 +05:30
5dd5862bb9 chore(deps-dev): bump sass-loader in /src/packages/excalidraw (#4393)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.3.0 to 12.4.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.3.0...v12.4.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-13 10:40:41 +05:30
79989fedda chore: bump roughjs version (#4386)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-12-11 22:46:44 +01:00
cecabc2196 fix: husky not able to execute pre-commit on windows (#4370) 2021-12-09 15:15:54 +01:00
ed8fb40b63 fix: make firebase config parsing not fail on undefined env (#4381) 2021-12-09 12:24:41 +00:00
6e391728fe build: remove file loader and migrate to asset modules webpack for font assets (#4380)
* build: use type:javascript/auto so font file assets aren't duplicated

* update changelog

* remove file loader and use asset modules

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 16:10:06 +02:00
650930c5ce chore(deps-dev): bump webpack in /src/packages/utils (#4328)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.3 to 5.64.4.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.3...v5.64.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 16:09:53 +02:00
79c0d59244 chore(deps-dev): bump webpack in /src/packages/excalidraw (#4327)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.3 to 5.64.4.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.3...v5.64.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 16:09:44 +02:00
cd50b5f7e9 chore(deps): bump @types/react from 17.0.35 to 17.0.37 (#4332)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.35 to 17.0.37.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 16:09:20 +02:00
c0434957ff chore(deps): bump @testing-library/jest-dom from 5.15.0 to 5.15.1 (#4333)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.15.0 to 5.15.1.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.15.0...v5.15.1)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 16:08:30 +02:00
7f545e74ab chore(deps): bump sass from 1.43.4 to 1.43.5 (#4330)
Bumps [sass](https://github.com/sass/dart-sass) from 1.43.4 to 1.43.5.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.43.4...1.43.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-12-02 15:12:01 +02:00
a776955579 chore(deps-dev): bump prettier from 2.4.1 to 2.5.0 (#4331)
Bumps [prettier](https://github.com/prettier/prettier) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/2.4.1...2.5.0)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 16:55:14 +00:00
78bb3b3d84 chore(deps-dev): bump @babel/plugin-transform-runtime (#4292)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.16.0 to 7.16.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.4/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 18:54:33 +02:00
7d9d7ad297 chore(deps-dev): bump @babel/plugin-transform-runtime (#4288)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.16.0 to 7.16.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.4/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 18:54:13 +02:00
de20a5e3ba chore(deps-dev): bump webpack in /src/packages/excalidraw (#4314)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.0 to 5.64.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.0...v5.64.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 18:52:51 +02:00
289f72e45d chore(deps-dev): bump webpack in /src/packages/utils (#4315)
Bumps [webpack](https://github.com/webpack/webpack) from 5.64.0 to 5.64.3.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.64.0...v5.64.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 18:52:36 +02:00
6dd0e6a4c5 fix: remove 100% height from tooltip container to fix layout issues (#3980) 2021-11-24 17:16:18 +01:00
96b31ecbce fix: inline ENV variables when building excalidraw package (#4311) 2021-11-24 16:25:19 +01:00
a132f154cb chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#4286)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.16.0 to 7.16.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.4/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 17:22:57 +02:00
23acd8f6d1 chore(deps-dev): bump mini-css-extract-plugin (#4289)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.4.4 to 2.4.5.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.4.4...v2.4.5)

---
updated-dependencies:
- dependency-name: mini-css-extract-plugin
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 17:22:34 +02:00
a60709f5ea chore(deps-dev): bump lint-staged from 12.0.1 to 12.1.2 (#4312)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 12.0.1 to 12.1.2.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v12.0.1...v12.1.2)

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

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

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

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 15:40:49 +02:00
a2136bfe9d chore(deps): bump @types/react from 17.0.34 to 17.0.35 (#4297)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.34 to 17.0.35.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 15:39:32 +02:00
6fbd64fdaa chore(deps-dev): bump firebase-tools from 9.22.0 to 9.23.0 (#4293)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.22.0 to 9.23.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.22.0...v9.23.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-24 15:39:16 +02:00
cc4b0c2932 feat: supply version param when installing libraries (#4305) 2021-11-23 17:59:26 +01:00
b6ef953dc9 fix: SVG export in dark mode with embedded bitmap image (#4285)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-11-20 15:31:51 +01:00
620b662085 chore: bump typescript@4.5.2 (#4283)
* chore: bump typescript@4.5.2

* bump typescript and fix tsconfig for npm package
2021-11-19 19:51:28 +01:00
1c11df011a fix: new FS API not working on Linux (#4280) 2021-11-19 14:20:42 +01:00
59e9651547 feat: log FS abortError to console (#4279) 2021-11-19 10:54:23 +01:00
1c48d122e0 fix: url -> URL for consistency (#4277) 2021-11-19 11:21:23 +05:30
e4d02fb275 feat: Add validation for website and remove validation for library item name (#4269)
* Github->GitHub

* allow numbers

* remove validation for lib/item name
2021-11-18 10:24:26 +00:00
34a382ace9 fix: prevent adding images to library via contextMenu (#4264) 2021-11-17 20:06:26 +00:00
e60e48e67d fix: account for libraries v2 when prompting (#4263) 2021-11-17 19:54:40 +00:00
84d1d9993c feat: Allow publishing libraries from UI (#4115)
* feat: Allow publishing libraries from UI

* Add status for each library item and show publish only for unpublished libs

* Add publish library dialog

* Pass the data to publish the library

* pass lib blob

* Handle old and new libraries when importing

* Better error handling

* Show publish success when library submitted for review

* don't close library when publish success dialog open

* Support multiple libs deletion and publish

* Set status to published once library submitted for review

* Save  to LS after library published

* unique key for publish and delete

* fix layout shift when hover and also highlight selected library items

* design improvements

* migrate old library to the new one

* fix

* fix tests

* use i18n

* Support submit type in toolbutton

* Use html5 form validation, add asteriks for required fields, add twitter handle, mark github handle optional

* Add twitter handle in form state

* revert html5 validation as fetch is giving some issues :/

* clarify types around LibraryItems

* Add website optional field

* event.preventDefault to make htm5 form validationw work

* improve png generation by drawing a bounding box rect and aligining pngs to support multiple libs png

* remove ts-ignore

* add placeholders for fields

* decrease clickable area for checkbox by 0.5em

* add checkbox background color

* rename `items` to `elements`

* improve checkbox hit area

* show selected library items in publish dialog

* decrease dimensions by 3px to improve jerky experience when opening/closing library menu

* Don't close publish dialog when clicked outside

* Show selected library actions only when any library item selected and use icons instead of button

* rename library to libraryItems in excalidrawLib and added migration

* change icon and swap bg/color

* use blue brand color for hover/selected states

* prompt for confirmation when deleting library items

* separate unpublished items from published

* factor `LibraryMenu` into own file

* i18n and minor fixes for unpublished items

* fix not rendering empty cells when library empty

* don't render published section if empty and unpublished is not

* Add edit name functionality for library items

* fix

* edit lib name with onchange/blur

* bump library version

* prefer response error message

* add library urls to ENV vars

* mark lib item name as required

* Use input only for lib item name

* better error validation for lib items

* fix label styling for lib items

* design and i18n fixes

* Save publish dialog data to local storage and clear once published

* Add a note about MIT License

* Add note for guidelines

* Add tooltip for publish button

* Show spinner in submit button when submission is in progress

* assign id for older lib items when installed and set status as published for all lib when installed

* update export icon and support export library for selected items

* move LibraryMenuItems into its own component as its best to keep one comp per file

* fix spec

* Refactoring the library actions for reusablility

* show only load when items not present

* close on click outside in publish dialog

* ad dialog description and tweak copy

* vertically center input labels

* align input styles

* move author name input to other usernames

* rename param

* inline to simplify

* fix to not inline `undefined` class names

* fix version & include only latest lib schema in library export type

* await response callback

* refactor types

* refactor

* i18n

* align casing & tweaks

* move ls logic to publishLibrary

* support removal of item inside publish dialog

* fix labels for trash icon when items selected

* replace window.confirm for removal libs with confirm dialog

* fix input/textarea styling

* move library item menu scss to its own file

* use blue for load and cyan for publish

* reduce margin for submit and make submit => Submit

* Make library items header sticky

* move publish icon to left so there is no jerkiness when unpublish items selected

* update url

* fix grid gap between lib items

* Mark older items imported from initial data as unpublished

* add text to publish button on non-mobile

* add items counter

* fix test

* show personal and excal libs sections and personal goes first

* show toast on adding to library via contextMenu

* Animate plus icon and not the pending item

* fix snap

* use i18n when no item in publish dialog

* tweak style of new lib item

* show empty cells for both sections and set status as published for installed libs

* fix

* push selected item first in unpublished section

* set status as published for imported from webiste but unpublished for json

* Add items to the begining of library

* add `created` library item attr

* fix test

* use `defaultValue` instead of `value`

* fix dark theme styles

* fix toggle button not closing library

* close library menu on Escape

* tweak publish dialog item remove style

* fix remove icon in publish dialog

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-11-17 23:53:43 +05:30
3ff9744b39 feat: create confirm dialog to use instead of window.confirm (#4256)
* feat: create confirm dialog to use instead of window.confirm

* move confirm to right

* add types

* less margin
2021-11-16 18:55:56 +05:30
b9abcc825a chore(deps-dev): bump webpack in /src/packages/utils (#4245)
Bumps [webpack](https://github.com/webpack/webpack) from 5.62.1 to 5.64.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.62.1...v5.64.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:47:31 +01:00
9679eaf74c chore(deps-dev): bump @types/web from 0.0.46 to 0.0.47 (#4249)
Bumps [@types/web](https://github.com/microsoft/TypeScript-DOM-Lib-Generator) from 0.0.46 to 0.0.47.
- [Release notes](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/releases)
- [Commits](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/compare/@types/web@0.0.46...@types/web@0.0.47)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:46:42 +01:00
284747d742 chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#4243)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.2.4 to 5.2.5.
- [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.2.4...v5.2.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:46:18 +01:00
876f85fd7a chore(deps-dev): bump webpack in /src/packages/excalidraw (#4244)
Bumps [webpack](https://github.com/webpack/webpack) from 5.62.1 to 5.64.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.62.1...v5.64.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:45:55 +01:00
efc2bbed21 chore(deps): bump browser-fs-access from 0.21.0 to 0.21.1 (#4248)
Bumps [browser-fs-access](https://github.com/GoogleChromeLabs/browser-fs-access) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/GoogleChromeLabs/browser-fs-access/releases)
- [Commits](https://github.com/GoogleChromeLabs/browser-fs-access/compare/v0.21.0...v0.21.1)

---
updated-dependencies:
- dependency-name: browser-fs-access
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:45:29 +01:00
61d193b87b chore(deps-dev): bump lint-staged from 11.2.6 to 12.0.1 (#4250)
Bumps [lint-staged](https://github.com/okonet/lint-staged) from 11.2.6 to 12.0.1.
- [Release notes](https://github.com/okonet/lint-staged/releases)
- [Commits](https://github.com/okonet/lint-staged/compare/v11.2.6...v12.0.1)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:45:06 +01:00
3989d6a989 chore(deps): bump roughjs from 4.4.5 to 4.5.0 (#4246)
Bumps [roughjs](https://github.com/pshihn/rough) from 4.4.5 to 4.5.0.
- [Release notes](https://github.com/pshihn/rough/releases)
- [Changelog](https://github.com/rough-stuff/rough/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pshihn/rough/commits)

---
updated-dependencies:
- dependency-name: roughjs
  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>
2021-11-14 10:44:44 +01:00
f6559b65ef chore: Update translations from Crowdin (#4201) 2021-11-12 12:13:34 +01:00
bc6b066c07 Remove outdated OT info (#4232) 2021-11-09 12:16:32 +01:00
6370d517a2 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4209)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.5.0...v6.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 17:45:06 +00:00
b8a37c42e4 chore(deps): bump @tldraw/vec from 0.0.132 to 0.1.3 (#4215)
Bumps [@tldraw/vec](https://github.com/tldraw/vec) from 0.0.132 to 0.1.3.
- [Release notes](https://github.com/tldraw/vec/releases)
- [Commits](https://github.com/tldraw/vec/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>
2021-11-08 18:42:21 +01:00
76763b80a9 chore(deps-dev): bump @types/web from 0.0.45 to 0.0.46 (#4214)
Bumps [@types/web](https://github.com/microsoft/TypeScript-DOM-Lib-Generator) from 0.0.45 to 0.0.46.
- [Release notes](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/releases)
- [Commits](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/compare/@types/web@0.0.45...@types/web@0.0.46)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:41:59 +01:00
d2a2c9d6b5 chore(deps-dev): bump webpack in /src/packages/utils (#4211)
Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.62.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.61.0...v5.62.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>
2021-11-08 17:41:50 +00:00
3a72f347d2 chore(deps-dev): bump webpack in /src/packages/excalidraw (#4208)
Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.62.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.61.0...v5.62.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>
2021-11-08 18:41:13 +01:00
c1d9456235 chore(deps-dev): bump mini-css-extract-plugin (#4207)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.4.3...v2.4.4)

---
updated-dependencies:
- dependency-name: mini-css-extract-plugin
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:40:45 +01:00
c4f8b98208 chore(deps-dev): bump css-loader in /src/packages/utils (#4210)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.5.0...v6.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:40:18 +01:00
b6eb57d3f1 chore(deps): bump @testing-library/jest-dom from 5.14.1 to 5.15.0 (#4212)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.14.1 to 5.15.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.14.1...v5.15.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  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>
2021-11-08 18:39:52 +01:00
473b8ca0ca chore(deps): bump @types/react from 17.0.33 to 17.0.34 (#4216)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.33 to 17.0.34.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:39:24 +01:00
45206c4ef1 chore(deps-dev): bump firebase-tools from 9.21.0 to 9.22.0 (#4219)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.21.0 to 9.22.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.21.0...v9.22.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:38:55 +01:00
56b4a29aaa chore(deps): bump @types/react-dom from 17.0.10 to 17.0.11 (#4218)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-07 15:00:24 +01:00
bb4dda64b5 chore(deps): bump roughjs from 4.4.4 to 4.4.5 (#4221)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-07 14:57:09 +01:00
39e53b4ae7 feat: Allow letters in IDs for storing files in backend (#4224) 2021-11-07 14:37:13 +01:00
6143d5195a refactor: deduplicate encryption helpers (#4146) 2021-11-07 14:33:21 +01:00
f59e608f18 fix: Skia rendering issues (#4200) 2021-11-04 15:55:10 +01:00
6b24592e4a chore: Update translations from Crowdin (#4150) 2021-11-04 14:10:21 +02:00
7b442997dc chore: Update docker action to v2 (#4198) 2021-11-04 14:10:00 +02:00
4bfc5bbcaa chore: Update i18next-browser-languagedetector (#4196) 2021-11-03 21:07:13 +00:00
2b29b9a96d chore: Consistent case for clear canvas, change font of buttons and clean up unused strings (#4195)
* chore: Consistent case for clear canvas and font

* Remove unused

* remove
2021-11-03 21:31:27 +02:00
cc201a6d80 fix: ellipse roughness when 0 (#4194) 2021-11-03 12:50:54 +01:00
5be58b59e0 fix: Proper string for invalid SVG (#4191) 2021-11-03 10:10:58 +01:00
f1eb969565 feat: Remove support for V1 unencrypted backend (#4189) 2021-11-02 14:52:25 +02:00
8d4f455cd3 chore: Update Typescript to 4.4.4 (#4188) 2021-11-02 14:24:16 +02:00
60262cb4cc chore(deps): bump idb-keyval from 5.1.3 to 6.0.3 (#4181)
Bumps [idb-keyval](https://github.com/jakearchibald/idb-keyval) from 5.1.3 to 6.0.3.
- [Release notes](https://github.com/jakearchibald/idb-keyval/releases)
- [Changelog](https://github.com/jakearchibald/idb-keyval/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jakearchibald/idb-keyval/compare/v5.1.3...v6.0.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-02 09:44:05 +02:00
7501c24f22 feat: Use separate backend for local storage (#4187) 2021-11-02 09:33:27 +02:00
00d81aa982 chore(deps-dev): bump @babel/plugin-transform-typescript (#4160)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.15.8 to 7.16.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.1/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>
2021-11-01 13:36:09 +00:00
67fe156d06 chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#4162)
Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.14.5 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-plugin-transform-async-to-generator)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:31:12 +00:00
ef433233d1 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#4161)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.15.0 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-11-01 13:24:26 +00:00
1c7056bdaa chore: bump Prettier to the latest (#4185) 2021-11-01 15:24:05 +02:00
277ffaacb9 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4122)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.6 to 6.5.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.6...v6.5.0)

---
updated-dependencies:
- dependency-name: css-loader
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:23:38 +00:00
2a3e242cfd chore(deps-dev): bump @babel/plugin-transform-runtime (#4164)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-11-01 13:20:22 +00:00
b1c6051d6b chore(deps-dev): bump webpack in /src/packages/utils (#4134)
Bumps [webpack](https://github.com/webpack/webpack) from 5.50.0 to 5.61.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.50.0...v5.61.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:19:04 +00:00
8df9742463 chore(deps-dev): bump webpack-cli in /src/packages/excalidraw (#4132)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.7.2 to 4.9.1.
- [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.7.2...webpack-cli@4.9.1)

---
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>
2021-11-01 13:18:24 +00:00
9fdc382d71 chore(deps-dev): bump @babel/plugin-transform-arrow-functions (#4165)
Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.14.5 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-11-01 13:15:16 +00:00
f70d11c2d1 chore(deps-dev): bump @babel/plugin-transform-typescript (#4167)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.15.8 to 7.16.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.1/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>
2021-11-01 15:11:04 +02:00
05e54d6785 chore(deps-dev): bump mini-css-extract-plugin (#4094)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.6.1 to 2.4.3.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v1.6.1...v2.4.3)

---
updated-dependencies:
- dependency-name: mini-css-extract-plugin
  dependency-type: direct:development
  update-type: version-update:semver-major
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:10:36 +00:00
795a6e4546 chore(deps-dev): bump webpack-cli in /src/packages/utils (#4118)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.7.2 to 4.9.1.
- [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.7.2...webpack-cli@4.9.1)

---
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>
2021-11-01 13:09:32 +00:00
a01a4ad739 chore(deps): bump @testing-library/react from 11.2.6 to 12.1.2 (#4177)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.6 to 12.1.2.
- [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/v11.2.6...v12.1.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:03:24 +00:00
e09b96ac6f chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#4166)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 12:56:22 +00:00
d48fb17718 chore(deps-dev): bump webpack in /src/packages/excalidraw (#4144)
Bumps [webpack](https://github.com/webpack/webpack) from 5.50.0 to 5.61.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.50.0...v5.61.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 18:24:40 +05:30
ede3c4af82 chore(deps-dev): bump postcss-loader in /src/packages/excalidraw (#4169)
Bumps [postcss-loader](https://github.com/webpack-contrib/postcss-loader) from 6.1.1 to 6.2.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.1.1...v6.2.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:53:43 +02:00
8bcfd97fc5 chore(deps-dev): bump css-loader in /src/packages/utils (#4119)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.2.0 to 6.5.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.2.0...v6.5.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:50:49 +02:00
5721c6dfb5 chore(deps): bump nanoid from 3.1.22 to 3.1.30 (#4176)
Bumps [nanoid](https://github.com/ai/nanoid) from 3.1.22 to 3.1.30.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.1.22...3.1.30)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 12:47:06 +00:00
9b1f77c3be chore(deps-dev): bump @babel/plugin-transform-async-to-generator (#4168)
Bumps [@babel/plugin-transform-async-to-generator](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-async-to-generator) from 7.14.5 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-plugin-transform-async-to-generator)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 12:43:37 +00:00
3369035f40 feat: add hint around canvas panning (#4159) 2021-11-01 13:36:06 +01:00
dbc7a8599b chore(deps): bump @tldraw/vec from 0.0.106 to 0.0.132 (#4175)
Bumps [@tldraw/vec](https://github.com/tldraw/tldraw/tree/HEAD/packages/vec) from 0.0.106 to 0.0.132.
- [Release notes](https://github.com/tldraw/tldraw/releases)
- [Commits](https://github.com/tldraw/tldraw/commits/HEAD/packages/vec)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:35:17 +02:00
09f649daf7 chore(deps-dev): bump @types/pako from 1.0.1 to 1.0.2 (#4170)
Bumps [@types/pako](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pako) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pako)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:33:09 +02:00
d357664850 chore(deps): bump perfect-freehand from 1.0.15 to 1.0.16 (#4178)
Bumps [perfect-freehand](https://github.com/steveruizok/perfect-freehand) from 1.0.15 to 1.0.16.
- [Release notes](https://github.com/steveruizok/perfect-freehand/releases)
- [Commits](https://github.com/steveruizok/perfect-freehand/compare/v1.0.15...v1.0.16)

---
updated-dependencies:
- dependency-name: perfect-freehand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:32:37 +02:00
f973fdfa89 chore(deps): bump fake-indexeddb from 3.1.3 to 3.1.7 (#4172)
Bumps [fake-indexeddb](https://github.com/dumbmatter/fakeIndexedDB) from 3.1.3 to 3.1.7.
- [Release notes](https://github.com/dumbmatter/fakeIndexedDB/releases)
- [Changelog](https://github.com/dumbmatter/fakeIndexedDB/blob/master/CHANGELOG.md)
- [Commits](https://github.com/dumbmatter/fakeIndexedDB/compare/v3.1.3...v3.1.7)

---
updated-dependencies:
- dependency-name: fake-indexeddb
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:31:54 +02:00
c15bc50f17 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#4163)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-11-01 14:31:34 +02:00
c2d0107cc5 chore(deps): bump sass from 1.32.10 to 1.43.4 (#4174)
Bumps [sass](https://github.com/sass/dart-sass) from 1.32.10 to 1.43.4.
- [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.32.10...1.43.4)

---
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>
2021-11-01 14:31:13 +02:00
c43fac31a1 chore(deps): bump @types/react from 17.0.3 to 17.0.33 (#4179)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.3 to 17.0.33.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:30:43 +02:00
9dfaf1752b chore(deps): bump open-color from 1.8.0 to 1.9.1 (#4180)
Bumps [open-color](https://github.com/yeun/open-color) from 1.8.0 to 1.9.1.
- [Release notes](https://github.com/yeun/open-color/releases)
- [Changelog](https://github.com/yeun/open-color/blob/master/build_release)
- [Commits](https://github.com/yeun/open-color/compare/v1.8.0...v1.9.1)

---
updated-dependencies:
- dependency-name: open-color
  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>
2021-11-01 14:30:17 +02:00
d9a1eb2f01 chore(deps-dev): bump firebase-tools from 9.9.0 to 9.21.0 (#4184)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.9.0 to 9.21.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.9.0...v9.21.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:29:56 +02:00
f1e17a320f chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#4151)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-11-01 10:05:36 +00:00
75ecd818b3 chore(deps-dev): bump webpack-bundle-analyzer in /src/packages/utils (#4158)
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.4.2 to 4.5.0.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.4.2...v4.5.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 10:05:03 +00:00
a7abc71f6a chore: Update dependabot limits (#4145) 2021-11-01 12:03:16 +02:00
6d0f0c8f21 chore(deps-dev): bump @babel/plugin-transform-runtime (#4152)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-11-01 12:00:41 +02:00
790e6da500 fix: images not initialized correctly (#4157)
* fix: image not initialized correctly due to not renewing `state.pendingImageElement`

* ensure we replace elements on update

* set file as errored on >= 400 status respones
2021-11-01 10:44:57 +01:00
8df1a11535 chore(deps-dev): bump @babel/plugin-transform-arrow-functions (#4148)
Bumps [@babel/plugin-transform-arrow-functions](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-arrow-functions) from 7.14.5 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-10-31 10:00:11 +00:00
b61ee56dc8 chore(deps-dev): bump @babel/core in /src/packages/utils (#4149)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-31 11:55:32 +02:00
c61f95a327 fix: image-related fixes (#4147)
* flush queues on portal close

* fix mouse broadcast race condition

* stop mutating image elements when updating status

to fix race condition when closing/opening collab room

* check `files` when resolving `LayerUI`

* fix displaying AbortError
2021-10-30 23:40:35 +02:00
d6d629f416 chore: Update translations from Crowdin (#4109) 2021-10-30 19:26:54 +03:00
65dec605f2 chore(deps-dev): bump @babel/preset-typescript (#4143)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.15.0 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-10-30 16:20:16 +00:00
cacec0b5c4 chore(deps-dev): bump @babel/preset-react in /src/packages/excalidraw (#4140)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.14.5 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/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>
2021-10-30 12:03:51 +03:00
87a302d7e9 chore(deps-dev): bump sass-loader in /src/packages/excalidraw (#4139)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.1.0 to 12.3.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.1.0...v12.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 11:34:17 +03:00
899b36c206 chore(deps-dev): bump babel-loader in /src/packages/excalidraw (#4138)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.2 to 8.2.3.
- [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.2...v8.2.3)

---
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>
2021-10-30 00:41:13 +00:00
534cbef982 chore(deps-dev): bump autoprefixer in /src/packages/excalidraw (#4137)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.3.1 to 10.4.0.
- [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.3.1...10.4.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 00:01:45 +03:00
b7f118404e chore(deps-dev): bump webpack-bundle-analyzer (#4121)
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.4.2 to 4.5.0.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.4.2...v4.5.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 18:37:49 +03:00
aab5067718 chore(deps-dev): bump @types/resize-observer-browser from 0.1.5 to 0.1.6 (#4135)
Bumps [@types/resize-observer-browser](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/resize-observer-browser) from 0.1.5 to 0.1.6.
- [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>
2021-10-29 15:36:23 +00:00
b679da02ee chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#4128)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.1.4 to 5.2.4.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.1.4...v5.2.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 18:26:30 +03:00
ec652820ea chore(deps): bump @types/jest from 26.0.22 to 27.0.2 (#4131)
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.22 to 27.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 18:26:15 +03:00
5d941ed107 chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#4129)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.15.0 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 18:25:59 +03:00
adc478ca34 chore(deps-dev): bump @babel/preset-env in /src/packages/excalidraw (#4130)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.14.9 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-29 18:25:47 +03:00
f1202adb15 feat: stop using production services for development (#4113) 2021-10-29 17:13:28 +02:00
fd439cf38a chore(deps-dev): bump babel-loader in /src/packages/utils (#4124)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.2 to 8.2.3.
- [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.2...v8.2.3)

---
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>
2021-10-29 11:32:52 +00:00
83c63be846 chore(deps-dev): bump @babel/preset-typescript (#4127)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.14.5 to 7.15.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.0/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>
2021-10-29 11:26:28 +00:00
b59d49dd7f chore(deps-dev): bump sass-loader in /src/packages/utils (#4126)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.1.0 to 12.3.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.1.0...v12.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:25:02 +00:00
0116b70edf chore(deps): bump @testing-library/jest-dom from 5.11.10 to 5.14.1 (#4125)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.11.10 to 5.14.1.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.11.10...v5.14.1)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  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>
2021-10-29 11:24:54 +00:00
3f390d4858 chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#4002)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.4 to 9.2.6.
- [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.4...v9.2.6)

---
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>
2021-10-29 11:24:16 +00:00
fdde73bff4 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#4069)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.8 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-29 11:21:07 +00:00
90a416e265 chore(deps): bump @types/react-dom from 17.0.3 to 17.0.10 (#4120)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.3 to 17.0.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 14:17:59 +03:00
a828b2e5de chore(deps-dev): bump @babel/plugin-transform-typescript (#4044)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.14.6 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-29 11:16:44 +00:00
7c51d3c24c chore(deps-dev): bump ts-loader in /src/packages/utils (#4117)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.4 to 9.2.6.
- [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.4...v9.2.6)

---
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>
2021-10-29 11:13:03 +00:00
4d2d6f181a chore(deps): bump tar from 4.4.15 to 4.4.19 (#3953)
Bumps [tar](https://github.com/npm/node-tar) from 4.4.15 to 4.4.19.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.15...v4.4.19)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:10:33 +00:00
071416f6ef chore(deps-dev): bump @babel/plugin-transform-typescript (#4045)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.14.6 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-29 14:10:19 +03:00
d675b07089 chore(deps-dev): bump @babel/core in /src/packages/utils (#4043)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.8 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-29 14:09:53 +03:00
3975fd592a chore(deps-dev): bump @babel/plugin-transform-runtime (#4042)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.14.5 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-29 14:09:37 +03:00
34a9a4dac6 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#3928)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.14.5 to 7.15.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.0/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>
2021-10-29 14:08:37 +03:00
78e419b790 chore(deps-dev): Upgrade commit hooks to Husky 7 (#4116)
* Upgrade to Husky 7

* Husky
2021-10-29 14:06:13 +03:00
8d8769ba4e feat: add triangle arrowhead (#4024)
Co-authored-by: ad1992 <aakansha1216@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-10-27 19:27:39 +02:00
d89fb3371b fix: rewrite collab element reconciliation to fix z-index issues (#4076) 2021-10-27 15:14:20 +02:00
8410972cff chore: Update translations from Crowdin (#4047)
* New translations en.json (Occitan)

* New translations en.json (Catalan)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Bengali)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

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

* New translations en.json (Punjabi)

* New translations en.json (Persian)

* 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 (Norwegian Bokmal)

* New translations en.json (Polish)

* New translations en.json (Dutch)

* New translations en.json (Catalan)

* New translations en.json (Bulgarian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Chinese Simplified)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Korean)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Chinese Traditional)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* New translations en.json (Occitan)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

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

* New translations en.json (Punjabi)

* New translations en.json (Persian)

* 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 (Norwegian Bokmal)

* New translations en.json (Polish)

* New translations en.json (Dutch)

* New translations en.json (Catalan)

* New translations en.json (Bulgarian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Chinese Simplified)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Korean)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* New translations en.json (Kazakh)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Finnish)

* Auto commit: Calculate translation coverage

* New translations en.json (Sinhala)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese, Brazilian)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

* New translations en.json (Sinhala)

* Auto commit: Calculate translation coverage

* New translations en.json (Sinhala)

* Auto commit: Calculate translation coverage

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

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage
2021-10-27 12:41:00 +05:30
2c8d041987 Migrate the implementation back to browser-fs-access (#4106) 2021-10-27 08:57:10 +02:00
94519c8250 fix: redirect excalidraw.com/about to for-webex.excalidraw.com (#4104) 2021-10-26 17:01:05 +02:00
add8a1b1a7 fix: redirect to webex LP instead of rewrite to fix SW (#4103) 2021-10-26 16:02:30 +02:00
516e7656f3 feat: Add rewrite to webex landing page (#4102)
* feat: Add rewrite to webex landing page

* blacklist webex url

* dont cache webex

* Unregister sw for webex

* fix

* fix

* reload in callback

Co-authored-by: dwelle <luzar.david@gmail.com>
2021-10-26 18:19:41 +05:30
d7cdee37bf feat: switch collab server (#4092) 2021-10-24 11:47:45 +02:00
5c5b8c517f fix: clear image/shape cache of affected elements when adding files (#4089) 2021-10-23 14:17:04 +02:00
7dbd0c5e0a fix: clear LibraryUnit DOM on unmount (#4084) 2021-10-22 22:07:20 +02:00
ba35eb8f8c fix: pasting images on firefox (#4085) 2021-10-22 21:04:04 +02:00
163ad1f4c4 feat: image support (#4011)
Co-authored-by: Emil Atanasov <heitara@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2021-10-21 22:05:48 +02:00
0f0244224d feat: Use dialog component for clear canvas instead of window confirm (#4075)
* feat: Use dialog component for clear canvas instead of window confirm

* reduce font weight

* fix specs

* update button name and use action

* export clearCanvas from actions
2021-10-21 17:35:28 +05:30
6eecadce60 feat: export isLinearElement and getNonDeletedElements (#4072)
* feat: export isLinearElement and getNonDeletedElements

* fix
2021-10-19 14:40:48 +05:30
bc88cf5002 fix: Don't show save file to disk when UIOptions.canvasActions.export.saveFileToDisk is false (#4073) 2021-10-19 14:39:47 +05:30
571be9c0fe chore(deps-dev): bump @babel/plugin-transform-runtime (#4053)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.14.5 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/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>
2021-10-18 15:59:56 +05:30
5d925c7d3f build: Allow package.json changes when autoreleasing next (#4068) 2021-10-18 15:58:58 +05:30
45c520341f chore: bump @dwelle/browser-fs-access to 0.21.2 (#4067) 2021-10-18 11:08:12 +02:00
c6ffc06541 feat: support renderTopRightUI in mobile (#4065) 2021-10-17 21:44:46 +05:30
ff29780760 Refactor: convert initializeApp to func component and use JSX transform in the codebase (#4056) 2021-10-14 22:56:51 +05:30
463857ad9a feat: Export THEME from the package (#4055)
* Use Theme type everywhere
* Rename Appearance type to Theme for consistency
* Reorder headers in readme
The host don't need to pass hardcoded strings any more and instead can use the exported constant
2021-10-14 14:15:57 +05:30
be2da9539e chore(deps): bump path-parse in /src/packages/excalidraw (#3912)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 20:18:29 +05:30
bb7829ef90 chore(deps): bump url-parse from 1.5.1 to 1.5.3 (#3927)
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
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>
2021-10-13 20:18:09 +05:30
1104f6891e chore(deps): bump semver-regex from 3.1.2 to 3.1.3 (#3988)
Bumps [semver-regex](https://github.com/sindresorhus/semver-regex) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/sindresorhus/semver-regex/releases)
- [Commits](https://github.com/sindresorhus/semver-regex/commits)

---
updated-dependencies:
- dependency-name: semver-regex
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 20:17:43 +05:30
a97e172070 chore(deps): bump tmpl from 1.0.4 to 1.0.5 (#3992)
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 20:17:24 +05:30
267 changed files with 25358 additions and 7385 deletions

5
.env
View File

@ -1,5 +0,0 @@
REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.com
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'

8
.env.development Normal file
View File

@ -0,0 +1,8 @@
REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_SOCKET_SERVER_URL=http://localhost:3002
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"}'

View File

@ -1 +1,11 @@
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_SOCKET_SERVER_URL=https://oss-collab-us1.excalidraw.com
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
# production-only vars
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13

View File

@ -5,3 +5,4 @@ package-lock.json
firebase/
dist/
public/workbox
src/packages/excalidraw/types

View File

@ -1,6 +1,7 @@
{
"extends": ["@excalidraw/eslint-config", "react-app"],
"rules": {
"import/no-anonymous-default-export": "off"
"import/no-anonymous-default-export": "off",
"no-restricted-globals": "off"
}
}

View File

@ -10,6 +10,7 @@ updates:
- lipis
assignees:
- lipis
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/excalidraw/
@ -21,6 +22,7 @@ updates:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20
- package-ecosystem: npm
directory: /src/packages/utils/
@ -32,3 +34,4 @@ updates:
- ad1992
assignees:
- ad1992
open-pull-requests-limit: 20

View File

@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: docker/build-push-action@v1
- uses: docker/build-push-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@ -10,7 +10,7 @@ jobs:
- name: Setup Node.js 14.x
uses: actions/setup-node@v2
with:
node-version: 14.x
node-version: 16.x
- name: Install and test
run: |
yarn --frozen-lockfile

2
.husky/pre-commit Executable file
View File

@ -0,0 +1,2 @@
#!/bin/sh
yarn lint-staged

View File

@ -118,6 +118,10 @@ yarn start
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
#### Collaboration
For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local.
#### Commands
| Command | Description |

View File

@ -19,24 +19,28 @@
]
},
"dependencies": {
"@dwelle/browser-fs-access": "0.21.1",
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.11.10",
"@testing-library/react": "11.2.6",
"@tldraw/vec": "0.0.106",
"@types/jest": "26.0.22",
"@types/react": "17.0.3",
"@types/react-dom": "17.0.3",
"@testing-library/jest-dom": "5.16.1",
"@testing-library/react": "12.1.2",
"@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "17.0.38",
"@types/react-dom": "17.0.11",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.23.0",
"clsx": "1.1.1",
"fake-indexeddb": "3.1.7",
"firebase": "8.3.3",
"i18next-browser-languagedetector": "6.1.0",
"i18next-browser-languagedetector": "6.1.2",
"idb-keyval": "6.0.3",
"image-blob-reduce": "3.0.1",
"lodash.throttle": "4.1.1",
"nanoid": "3.1.22",
"open-color": "1.8.0",
"nanoid": "3.1.30",
"open-color": "1.9.1",
"pako": "1.0.11",
"perfect-freehand": "1.0.15",
"perfect-freehand": "1.0.16",
"png-chunk-text": "1.0.0",
"png-chunks-encode": "1.0.0",
"png-chunks-extract": "1.0.0",
@ -45,39 +49,40 @@
"react": "17.0.2",
"react-dom": "17.0.2",
"react-scripts": "4.0.3",
"roughjs": "4.4.1",
"sass": "1.32.10",
"roughjs": "4.5.2",
"sass": "1.47.0",
"socket.io-client": "2.3.1",
"typescript": "4.2.4"
"typescript": "4.5.4"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.1",
"@types/resize-observer-browser": "0.1.5",
"@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4",
"dotenv": "10.0.0",
"eslint-config-prettier": "8.3.0",
"eslint-plugin-prettier": "3.3.1",
"firebase-tools": "9.9.0",
"husky": "4.3.8",
"firebase-tools": "9.23.0",
"husky": "7.0.4",
"jest-canvas-mock": "2.3.1",
"lint-staged": "10.5.4",
"lint-staged": "12.1.7",
"pepjs": "0.5.3",
"prettier": "2.2.1",
"prettier": "2.5.1",
"rewire": "5.0.0"
},
"resolutions": {
"@typescript-eslint/typescript-estree": "5.3.0"
},
"engines": {
"node": ">=14.0.0"
},
"homepage": ".",
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|@dwelle/browser-fs-access)/)"
"node_modules/(?!(roughjs|points-on-curve|path-data-parser|points-on-path|browser-fs-access)/)"
],
"resetMocks": false
},
@ -96,6 +101,7 @@
"fix": "yarn fix:other && yarn fix:code",
"locales-coverage": "node scripts/build-locales-coverage.js",
"locales-coverage:description": "node scripts/locales-coverage-description.js",
"prepare": "husky install",
"prettier": "prettier \"**/*.{css,scss,json,md,html,yml}\" --ignore-path=.eslintignore",
"start": "react-scripts start",
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watchAll=false",

View File

@ -13,18 +13,6 @@
<meta name="theme-color" content="#000" />
<!-- Declarative Link Capturing (https://web.dev/declarative-link-capturing/) -->
<meta
http-equiv="origin-trial"
content="Ak3VyzTheARtX2CnxBZ3ZKxImB0mNyvDakmMxeAChgxrWFMZ3IGN64VP+uj36VxM5OegsbLmrP258b1xvqp7+Q8AAABbeyJvcmlnaW4iOiJodHRwczovL2V4Y2FsaWRyYXcuY29tOjQ0MyIsImZlYXR1cmUiOiJXZWJBcHBMaW5rQ2FwdHVyaW5nIiwiZXhwaXJ5IjoxNjM0MDgzMTk5fQ=="
/>
<!-- File Handling (https://web.dev/file-handling/) -->
<meta
http-equiv="origin-trial"
content="AkMQsAnFmKfRfPKQHNCv2WmZREqgwkqhyt2M7aOwQiCStB+hPYnGnv+mNbkPDAsGXrwsj/waFi76wPzTDUaEeQ0AAABUeyJvcmlnaW4iOiJodHRwczovL2V4Y2FsaWRyYXcuY29tOjQ0MyIsImZlYXR1cmUiOiJGaWxlSGFuZGxpbmciLCJleHBpcnkiOjE2MzQwODMxOTl9"
/>
<!-- General tags -->
<meta
name="description"

View File

@ -26,7 +26,6 @@
}
}
],
"capture_links": "new-client",
"share_target": {
"action": "/web-share-target",
"method": "POST",

View File

@ -15,8 +15,8 @@ const publish = () => {
execSync(`yarn --frozen-lockfile`, { cwd: excalidrawDir });
execSync(`yarn run build:umd`, { cwd: excalidrawDir });
execSync(`yarn --cwd ${excalidrawDir} publish`);
} catch (e) {
console.error(e);
} catch (error) {
console.error(error);
}
};
@ -31,9 +31,11 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
const filesToIgnoreRegex = /src\/excalidraw-app|packages\/utils/;
const excalidrawPackageFiles = changedFiles.filter((file) => {
return file.indexOf("src") >= 0 && !filesToIgnoreRegex.test(file);
return (
(file.indexOf("src") >= 0 || file.indexOf("package.json")) >= 0 &&
!filesToIgnoreRegex.test(file)
);
});
if (!excalidrawPackageFiles.length) {
process.exit(0);
}
@ -46,6 +48,5 @@ exec(`git diff --name-only HEAD^ HEAD`, async (error, stdout, stderr) => {
// update readme
const data = fs.readFileSync(`${excalidrawDir}/README_NEXT.md`, "utf8");
fs.writeFileSync(`${excalidrawDir}/README.md`, data, "utf8");
publish();
});

View File

@ -1,11 +1,16 @@
const { readdirSync, writeFileSync } = require("fs");
const files = readdirSync(`${__dirname}/../src/locales`);
const flatten = (object) =>
Object.keys(object).reduce(
(initial, current) => ({ ...initial, ...object[current] }),
{},
);
const flatten = (object = {}, result = {}, extraKey = "") => {
for (const key in object) {
if (typeof object[key] !== "object") {
result[extraKey + key] = object[key];
} else {
flatten(object[key], result, `${extraKey}${key}.`);
}
}
return result;
};
const locales = files.filter(
(file) => file !== "README.md" && file !== "percentages.json",
@ -19,10 +24,8 @@ for (let index = 0; index < locales.length; index++) {
const allKeys = Object.keys(data);
const translatedKeys = allKeys.filter((item) => data[item] !== "");
const percentage = (100 * translatedKeys.length) / allKeys.length;
percentages[currentLocale.replace(".json", "")] = parseInt(percentage);
const percentage = Math.floor((100 * translatedKeys.length) / allKeys.length);
percentages[currentLocale.replace(".json", "")] = percentage;
}
writeFileSync(

View File

@ -5,7 +5,9 @@ const THRESSHOLD = 85;
const crowdinMap = {
"ar-SA": "en-ar",
"bg-BG": "en-bg",
"bn-BD": "en-bn",
"ca-ES": "en-ca",
"da-DK": "en-da",
"de-DE": "en-de",
"el-GR": "en-el",
"es-ES": "en-es",
@ -31,11 +33,14 @@ const crowdinMap = {
"pt-PT": "en-pt",
"ro-RO": "en-ro",
"ru-RU": "en-ru",
"si-LK": "en-silk",
"sk-SK": "en-sk",
"sv-SE": "en-sv",
"ta-IN": "en-ta",
"tr-TR": "en-tr",
"uk-UA": "en-uk",
"zh-CN": "en-zhcn",
"zh-HK": "en-zhhk",
"zh-TW": "en-zhtw",
"lv-LV": "en-lv",
"cs-CZ": "en-cs",
@ -45,7 +50,10 @@ const crowdinMap = {
const flags = {
"ar-SA": "🇸🇦",
"bg-BG": "🇧🇬",
"bn-BD": "🇧🇩",
"ca-ES": "🏳",
"cs-CZ": "🇨🇿",
"da-DK": "🇩🇰",
"de-DE": "🇩🇪",
"el-GR": "🇬🇷",
"es-ES": "🇪🇸",
@ -59,7 +67,9 @@ const flags = {
"it-IT": "🇮🇹",
"ja-JP": "🇯🇵",
"kab-KAB": "🏳",
"kk-KZ": "🇰🇿",
"ko-KR": "🇰🇷",
"lv-LV": "🇱🇻",
"my-MM": "🇲🇲",
"nb-NO": "🇳🇴",
"nl-NL": "🇳🇱",
@ -71,21 +81,24 @@ const flags = {
"pt-PT": "🇵🇹",
"ro-RO": "🇷🇴",
"ru-RU": "🇷🇺",
"si-LK": "🇱🇰",
"sk-SK": "🇸🇰",
"sv-SE": "🇸🇪",
"ta-IN": "🇮🇳",
"tr-TR": "🇹🇷",
"uk-UA": "🇺🇦",
"zh-CN": "🇨🇳",
"zh-HK": "🇭🇰",
"zh-TW": "🇹🇼",
"lv-LV": "🇱🇻",
"cs-CZ": "🇨🇿",
"kk-KZ": "🇰🇿",
};
const languages = {
"ar-SA": "العربية",
"bg-BG": "Български",
"bn-BD": "Bengali",
"ca-ES": "Català",
"cs-CZ": "Česky",
"da-DK": "Dansk",
"de-DE": "Deutsch",
"el-GR": "Ελληνικά",
"es-ES": "Español",
@ -99,7 +112,9 @@ const languages = {
"it-IT": "Italiano",
"ja-JP": "日本語",
"kab-KAB": "Taqbaylit",
"kk-KZ": "Қазақ тілі",
"ko-KR": "한국어",
"lv-LV": "Latviešu",
"my-MM": "Burmese",
"nb-NO": "Norsk bokmål",
"nl-NL": "Nederlands",
@ -111,15 +126,15 @@ const languages = {
"pt-PT": "Português",
"ro-RO": "Română",
"ru-RU": "Русский",
"si-LK": "සිංහල",
"sk-SK": "Slovenčina",
"sv-SE": "Svenska",
"ta-IN": "Tamil",
"tr-TR": "Türkçe",
"uk-UA": "Українська",
"zh-CN": "简体中文",
"zh-HK": "繁體中文 (香港)",
"zh-TW": "繁體中文",
"lv-LV": "Latviešu",
"cs-CZ": "Česky",
"kk-KZ": "Қазақ тілі",
};
const percentages = fs.readFileSync(

View File

@ -25,8 +25,8 @@ const release = async (nextVersion) => {
);
/* eslint-disable no-console */
console.log("Done!");
} catch (e) {
console.error(e);
} catch (error) {
console.error(error);
process.exit(1);
}
};

View File

@ -28,8 +28,8 @@ const getCommitHashForLastVersion = async () => {
`git log --format=format:"%H" --grep=${commitMessage}`,
);
return stdout;
} catch (e) {
console.error(e);
} catch (error) {
console.error(error);
}
};

View File

@ -2,6 +2,8 @@ import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { deepCopyElement } from "../element/newElement";
import { randomId } from "../random";
import { t } from "../i18n";
export const actionAddToLibrary = register({
name: "addToLibrary",
@ -9,15 +11,49 @@ export const actionAddToLibrary = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
true,
);
if (selectedElements.some((element) => element.type === "image")) {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: "Support for adding images to the library coming soon!",
},
};
}
app.library.loadLibrary().then((items) => {
app.library.saveLibrary([
...items,
selectedElements.map(deepCopyElement),
]);
});
return false;
return app.library
.loadLibrary()
.then((items) => {
return app.library.saveLibrary([
{
id: randomId(),
status: "unpublished",
elements: selectedElements.map(deepCopyElement),
created: Date.now(),
},
...items,
]);
})
.then(() => {
return {
commitToHistory: false,
appState: {
...appState,
toastMessage: t("toast.addedToLibrary"),
},
};
})
.catch((error) => {
return {
commitToHistory: false,
appState: {
...appState,
errorMessage: error.message,
},
};
});
},
contextItemLabel: "labels.addToLibrary",
});

View File

@ -1,4 +1,3 @@
import React from "react";
import { alignElements, Alignment } from "../align";
import {
AlignBottomIcon,
@ -9,13 +8,13 @@ import {
CenterVerticallyIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { getElementMap, getNonDeletedElements } from "../element";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { getShortcutKey } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const enableActionGroup = (
@ -35,9 +34,11 @@ const alignSelectedElements = (
const updatedElements = alignElements(selectedElements, alignment);
const updatedElementsMap = getElementMap(updatedElements);
const updatedElementsMap = arrayToMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
export const actionAlignTop = register({

View File

@ -1,15 +1,11 @@
import React from "react";
import { getDefaultAppState } from "../appState";
import { ColorPicker } from "../components/ColorPicker";
import { trash, zoomIn, zoomOut } from "../components/icons";
import { zoomIn, zoomOut } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { ZOOM_STEP } from "../constants";
import { THEME, ZOOM_STEP } from "../constants";
import { getCommonBounds, getNonDeletedElements } from "../element";
import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll";
@ -18,6 +14,9 @@ import { AppState, NormalizedZoomValue } from "../types";
import { getShortcutKey } from "../utils";
import { register } from "./register";
import { Tooltip } from "../components/Tooltip";
import { newElementWith } from "../element/mutateElement";
import { getDefaultAppState } from "../appState";
import ClearCanvas from "../components/ClearCanvas";
export const actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
@ -48,13 +47,15 @@ export const actionChangeViewBackgroundColor = register({
export const actionClearCanvas = register({
name: "clearCanvas",
perform: (elements, appState: AppState) => {
perform: (elements, appState, _, app) => {
app.imageCache.clear();
return {
elements: elements.map((element) =>
newElementWith(element, { isDeleted: true }),
),
appState: {
...getDefaultAppState(),
files: {},
theme: appState.theme,
elementLocked: appState.elementLocked,
exportBackground: appState.exportBackground,
@ -66,21 +67,8 @@ export const actionClearCanvas = register({
commitToHistory: true,
};
},
PanelComponent: ({ updateData }) => (
<ToolButton
type="button"
icon={trash}
title={t("buttons.clearReset")}
aria-label={t("buttons.clearReset")}
showAriaLabel={useIsMobile()}
onClick={() => {
if (window.confirm(t("alerts.clearReset"))) {
updateData(null);
}
}}
data-testid="clear-canvas-button"
/>
),
PanelComponent: ({ updateData }) => <ClearCanvas onConfirm={updateData} />,
});
export const actionZoomIn = register({
@ -172,7 +160,7 @@ export const actionResetZoom = register({
};
},
PanelComponent: ({ updateData, appState }) => (
<Tooltip label={t("buttons.resetZoom")}>
<Tooltip label={t("buttons.resetZoom")} style={{ height: "100%" }}>
<ToolButton
type="button"
className="reset-zoom-button"
@ -279,7 +267,8 @@ export const actionToggleTheme = register({
return {
appState: {
...appState,
theme: value || (appState.theme === "light" ? "dark" : "light"),
theme:
value || (appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT),
},
commitToHistory: false,
};

View File

@ -9,8 +9,8 @@ import { t } from "../i18n";
export const actionCopy = register({
name: "copy",
perform: (elements, appState) => {
copyToClipboard(getNonDeletedElements(elements), appState);
perform: (elements, appState, _, app) => {
copyToClipboard(getNonDeletedElements(elements), appState, app.files);
return {
commitToHistory: false,
@ -42,6 +42,7 @@ export const actionCopyAsSvg = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
true,
);
try {
await exportCanvas(
@ -50,12 +51,13 @@ export const actionCopyAsSvg = register({
? selectedElements
: getNonDeletedElements(elements),
appState,
app.files,
appState,
);
return {
commitToHistory: false,
};
} catch (error) {
} catch (error: any) {
console.error(error);
return {
appState: {
@ -80,6 +82,7 @@ export const actionCopyAsPng = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
true,
);
try {
await exportCanvas(
@ -88,6 +91,7 @@ export const actionCopyAsPng = register({
? selectedElements
: getNonDeletedElements(elements),
appState,
app.files,
appState,
);
return {
@ -104,7 +108,7 @@ export const actionCopyAsPng = register({
},
commitToHistory: false,
};
} catch (error) {
} catch (error: any) {
console.error(error);
return {
appState: {

View File

@ -1,7 +1,6 @@
import { isSomeElementSelected } from "../scene";
import { KEYS } from "../keys";
import { ToolButton } from "../components/ToolButton";
import React from "react";
import { trash } from "../components/icons";
import { t } from "../i18n";
import { register } from "./register";
@ -12,6 +11,7 @@ import { newElementWith } from "../element/mutateElement";
import { getElementsInGroup } from "../groups";
import { LinearElementEditor } from "../element/linearElementEditor";
import { fixBindingsAfterDeletion } from "../element/binding";
import { isBoundToContainer } from "../element/typeChecks";
const deleteSelectedElements = (
elements: readonly ExcalidrawElement[],
@ -22,6 +22,12 @@ const deleteSelectedElements = (
if (appState.selectedElementIds[el.id]) {
return newElementWith(el, { isDeleted: true });
}
if (
isBoundToContainer(el) &&
appState.selectedElementIds[el.containerId]
) {
return newElementWith(el, { isDeleted: true });
}
return el;
}),
appState: {
@ -56,7 +62,7 @@ export const actionDeleteSelected = register({
if (appState.editingLinearElement) {
const {
elementId,
activePointIndex,
selectedPointsIndices,
startBindingElement,
endBindingElement,
} = appState.editingLinearElement;
@ -66,8 +72,7 @@ export const actionDeleteSelected = register({
}
if (
// case: no point selected → delete whole element
activePointIndex == null ||
activePointIndex === -1 ||
selectedPointsIndices == null ||
// case: deleting last remaining point
element.points.length < 2
) {
@ -87,15 +92,17 @@ export const actionDeleteSelected = register({
// We cannot do this inside `movePoint` because it is also called
// when deleting the uncommitted point (which hasn't caused any binding)
const binding = {
startBindingElement:
activePointIndex === 0 ? null : startBindingElement,
endBindingElement:
activePointIndex === element.points.length - 1
? null
: endBindingElement,
startBindingElement: selectedPointsIndices?.includes(0)
? null
: startBindingElement,
endBindingElement: selectedPointsIndices?.includes(
element.points.length - 1,
)
? null
: endBindingElement,
};
LinearElementEditor.movePoint(element, activePointIndex, "delete");
LinearElementEditor.deletePoints(element, selectedPointsIndices);
return {
elements,
@ -104,17 +111,17 @@ export const actionDeleteSelected = register({
editingLinearElement: {
...appState.editingLinearElement,
...binding,
activePointIndex: activePointIndex > 0 ? activePointIndex - 1 : 0,
selectedPointsIndices:
selectedPointsIndices?.[0] > 0
? [selectedPointsIndices[0] - 1]
: [0],
},
},
commitToHistory: true,
};
}
let {
elements: nextElements,
appState: nextAppState,
} = deleteSelectedElements(elements, appState);
let { elements: nextElements, appState: nextAppState } =
deleteSelectedElements(elements, appState);
fixBindingsAfterDeletion(
nextElements,
elements.filter(({ id }) => appState.selectedElementIds[id]),

View File

@ -1,17 +1,16 @@
import React from "react";
import {
DistributeHorizontallyIcon,
DistributeVerticallyIcon,
} from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { distributeElements, Distribution } from "../disitrubte";
import { getElementMap, getNonDeletedElements } from "../element";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { getShortcutKey } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const enableActionGroup = (
@ -31,9 +30,11 @@ const distributeSelectedElements = (
const updatedElements = distributeElements(selectedElements, distribution);
const updatedElementsMap = getElementMap(updatedElements);
const updatedElementsMap = arrayToMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
export const distributeHorizontally = register({

View File

@ -1,15 +1,13 @@
import React from "react";
import { KEYS } from "../keys";
import { register } from "./register";
import { ExcalidrawElement } from "../element/types";
import { duplicateElement, getNonDeletedElements } from "../element";
import { isSomeElementSelected } from "../scene";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
import { clone } from "../components/icons";
import { t } from "../i18n";
import { getShortcutKey } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { LinearElementEditor } from "../element/linearElementEditor";
import { mutateElement } from "../element/mutateElement";
import {
selectGroupsForSelectedElements,
getSelectedGroupForElement,
@ -19,41 +17,23 @@ import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import { bindTextToShapeAfterDuplication } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
export const actionDuplicateSelection = register({
name: "duplicateSelection",
perform: (elements, appState) => {
// duplicate point if selected while editing multi-point element
// duplicate selected point(s) if editing a line
if (appState.editingLinearElement) {
const { activePointIndex, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (!element || activePointIndex === null) {
const ret = LinearElementEditor.duplicateSelectedPoints(appState);
if (!ret) {
return false;
}
const { points } = element;
const selectedPoint = points[activePointIndex];
const nextPoint = points[activePointIndex + 1];
mutateElement(element, {
points: [
...points.slice(0, activePointIndex + 1),
nextPoint
? [
(selectedPoint[0] + nextPoint[0]) / 2,
(selectedPoint[1] + nextPoint[1]) / 2,
]
: [selectedPoint[0] + 30, selectedPoint[1] + 30],
...points.slice(activePointIndex + 1),
],
});
return {
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
activePointIndex: activePointIndex + 1,
},
},
elements,
appState: ret.appState,
commitToHistory: true,
};
}
@ -107,9 +87,12 @@ const duplicateElements = (
const finalElements: ExcalidrawElement[] = [];
let index = 0;
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, true),
);
while (index < elements.length) {
const element = elements[index];
if (appState.selectedElementIds[element.id]) {
if (selectedElementIds.get(element.id)) {
if (element.groupIds.length) {
const groupId = getSelectedGroupForElement(appState, element);
// if group selected, duplicate it atomically
@ -131,7 +114,11 @@ const duplicateElements = (
}
index++;
}
bindTextToShapeAfterDuplication(
finalElements,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
return {
@ -141,7 +128,9 @@ const duplicateElements = (
...appState,
selectedGroupIds: {},
selectedElementIds: newElements.reduce((acc, element) => {
acc[element.id] = true;
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
}, {} as any),
},

View File

@ -1,11 +1,10 @@
import React from "react";
import { trackEvent } from "../analytics";
import { load, questionCircle, saveAs } from "../components/icons";
import { ProjectName } from "../components/ProjectName";
import { ToolButton } from "../components/ToolButton";
import "../components/ToolIcon.scss";
import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
import { DarkModeToggle } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data";
import { resaveAsImageWithScene } from "../data/resave";
import { t } from "../i18n";
@ -14,12 +13,13 @@ import { KEYS } from "../keys";
import { register } from "./register";
import { CheckboxItem } from "../components/CheckboxItem";
import { getExportSize } from "../scene/export";
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants";
import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES, THEME } from "../constants";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element";
import { ActiveFile } from "../components/ActiveFile";
import { isImageFileHandle } from "../data/blob";
import { nativeFileSystemSupported } from "../data/filesystem";
import { Theme } from "../element/types";
export const actionChangeProjectName = register({
name: "changeProjectName",
@ -128,13 +128,13 @@ export const actionChangeExportEmbedScene = register({
export const actionSaveToActiveFile = register({
name: "saveToActiveFile",
perform: async (elements, appState, value) => {
perform: async (elements, appState, value, app) => {
const fileHandleExists = !!appState.fileHandle;
try {
const { fileHandle } = isImageFileHandle(appState.fileHandle)
? await resaveAsImageWithScene(elements, appState)
: await saveAsJSON(elements, appState);
? await resaveAsImageWithScene(elements, appState, app.files)
: await saveAsJSON(elements, appState, app.files);
return {
commitToHistory: false,
@ -151,9 +151,11 @@ export const actionSaveToActiveFile = register({
: null,
},
};
} catch (error) {
} catch (error: any) {
if (error?.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
return { commitToHistory: false };
}
@ -170,16 +172,22 @@ export const actionSaveToActiveFile = register({
export const actionSaveFileToDisk = register({
name: "saveFileToDisk",
perform: async (elements, appState, value) => {
perform: async (elements, appState, value, app) => {
try {
const { fileHandle } = await saveAsJSON(elements, {
...appState,
fileHandle: null,
});
const { fileHandle } = await saveAsJSON(
elements,
{
...appState,
fileHandle: null,
},
app.files,
);
return { commitToHistory: false, appState: { ...appState, fileHandle } };
} catch (error) {
} catch (error: any) {
if (error?.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
return { commitToHistory: false };
}
@ -202,24 +210,28 @@ export const actionSaveFileToDisk = register({
export const actionLoadScene = register({
name: "loadScene",
perform: async (elements, appState) => {
perform: async (elements, appState, _, app) => {
try {
const {
elements: loadedElements,
appState: loadedAppState,
files,
} = await loadFromJSON(appState, elements);
return {
elements: loadedElements,
appState: loadedAppState,
files,
commitToHistory: true,
};
} catch (error) {
} catch (error: any) {
if (error?.name === "AbortError") {
console.warn(error);
return false;
}
return {
elements,
appState: { ...appState, errorMessage: error.message },
files: app.files,
commitToHistory: false,
};
}
@ -256,9 +268,9 @@ export const actionExportWithDarkMode = register({
}}
>
<DarkModeToggle
value={appState.exportWithDarkMode ? "dark" : "light"}
onChange={(theme: Appearence) => {
updateData(theme === "dark");
value={appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT}
onChange={(theme: Theme) => {
updateData(theme === THEME.DARK);
}}
title={t("labels.toggleExportColorScheme")}
/>

View File

@ -1,7 +1,6 @@
import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element";
import { resetCursor } from "../utils";
import React from "react";
import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons";
import { t } from "../i18n";
@ -20,11 +19,8 @@ export const actionFinalize = register({
name: "finalize",
perform: (elements, appState, _, { canvas, focusContainer }) => {
if (appState.editingLinearElement) {
const {
elementId,
startBindingElement,
endBindingElement,
} = appState.editingLinearElement;
const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId);
if (element) {
@ -50,6 +46,11 @@ export const actionFinalize = register({
}
let newElements = elements;
if (appState.pendingImageElement) {
mutateElement(appState.pendingImageElement, { isDeleted: true }, false);
}
if (window.document.activeElement instanceof HTMLElement) {
focusContainer();
}
@ -153,6 +154,7 @@ export const actionFinalize = register({
[multiPointElement.id]: true,
}
: appState.selectedElementIds,
pendingImageElement: null,
},
commitToHistory: appState.elementType === "freedraw",
};

View File

@ -1,6 +1,6 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getElementMap, getNonDeletedElements } from "../element";
import { getNonDeletedElements } from "../element";
import { mutateElement } from "../element/mutateElement";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { normalizeAngle, resizeSingleElement } from "../element/resizeElements";
@ -9,6 +9,7 @@ import { getTransformHandles } from "../element/transformHandles";
import { isFreeDrawElement, isLinearElement } from "../element/typeChecks";
import { updateBoundElements } from "../element/binding";
import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils";
const enableActionFlipHorizontal = (
elements: readonly ExcalidrawElement[],
@ -83,9 +84,11 @@ const flipSelectedElements = (
flipDirection,
);
const updatedElementsMap = getElementMap(updatedElements);
const updatedElementsMap = arrayToMap(updatedElements);
return elements.map((element) => updatedElementsMap[element.id] || element);
return elements.map(
(element) => updatedElementsMap.get(element.id) || element,
);
};
const flipElements = (
@ -93,13 +96,13 @@ const flipElements = (
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
for (let i = 0; i < elements.length; i++) {
flipElement(elements[i], appState);
elements.forEach((element) => {
flipElement(element, appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(elements[i], Math.PI);
rotateElement(element, Math.PI);
}
}
});
return elements;
};
@ -142,10 +145,9 @@ const flipElement = (
}
if (isLinearElement(element)) {
for (let i = 1; i < element.points.length; i++) {
LinearElementEditor.movePoint(element, i, [
-element.points[i][0],
element.points[i][1],
for (let index = 1; index < element.points.length; index++) {
LinearElementEditor.movePoints(element, [
{ index, point: [-element.points[index][0], element.points[index][1]] },
]);
}
LinearElementEditor.normalizePoints(element);

View File

@ -1,7 +1,6 @@
import React from "react";
import { CODES, KEYS } from "../keys";
import { t } from "../i18n";
import { getShortcutKey } from "../utils";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
@ -18,8 +17,9 @@ import {
import { getNonDeletedElements } from "../element";
import { randomId } from "../random";
import { ToolButton } from "../components/ToolButton";
import { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
if (elements.length >= 2) {
@ -45,6 +45,7 @@ const enableActionGroup = (
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
true,
);
return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
@ -57,6 +58,7 @@ export const actionGroup = register({
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
true,
);
if (selectedElements.length < 2) {
// nothing to group
@ -84,8 +86,9 @@ export const actionGroup = register({
}
}
const newGroupId = randomId();
const selectElementIds = arrayToMap(selectedElements);
const updatedElements = elements.map((element) => {
if (!appState.selectedElementIds[element.id]) {
if (!selectElementIds.get(element.id)) {
return element;
}
return newElementWith(element, {
@ -100,9 +103,8 @@ export const actionGroup = register({
// to the z order of the highest element in the layer stack
const elementsInGroup = getElementsInGroup(updatedElements, newGroupId);
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
const lastGroupElementIndex = updatedElements.lastIndexOf(
lastElementInGroup,
);
const lastGroupElementIndex =
updatedElements.lastIndexOf(lastElementInGroup);
const elementsAfterGroup = updatedElements.slice(lastGroupElementIndex + 1);
const elementsBeforeGroup = updatedElements
.slice(0, lastGroupElementIndex)
@ -150,7 +152,12 @@ export const actionUngroup = register({
if (groupIds.length === 0) {
return { appState, elements, commitToHistory: false };
}
const boundTextElementIds: ExcalidrawTextElement["id"][] = [];
const nextElements = elements.map((element) => {
if (isBoundToContainer(element)) {
boundTextElementIds.push(element.id);
}
const nextGroupIds = removeFromSelectedGroups(
element.groupIds,
appState.selectedGroupIds,
@ -162,11 +169,19 @@ export const actionUngroup = register({
groupIds: nextGroupIds,
});
});
const updateAppState = selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
);
// remove binded text elements from selection
boundTextElementIds.forEach(
(id) => (updateAppState.selectedElementIds[id] = false),
);
return {
appState: selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
),
appState: updateAppState,
elements: nextElements,
commitToHistory: true,
};

View File

@ -1,5 +1,4 @@
import { Action, ActionResult } from "./types";
import React from "react";
import { undo, redo } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";
@ -7,9 +6,9 @@ import History, { HistoryEntry } from "../history";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { isWindows, KEYS } from "../keys";
import { getElementMap } from "../element";
import { newElementWith } from "../element/mutateElement";
import { fixBindingsAfterDeletion } from "../element/binding";
import { arrayToMap } from "../utils";
const writeData = (
prevElements: readonly ExcalidrawElement[],
@ -28,17 +27,17 @@ const writeData = (
return { commitToHistory };
}
const prevElementMap = getElementMap(prevElements);
const prevElementMap = arrayToMap(prevElements);
const nextElements = data.elements;
const nextElementMap = getElementMap(nextElements);
const nextElementMap = arrayToMap(nextElements);
const deletedElements = prevElements.filter(
(prevElement) => !nextElementMap.hasOwnProperty(prevElement.id),
(prevElement) => !nextElementMap.has(prevElement.id),
);
const elements = nextElements
.map((nextElement) =>
newElementWith(
prevElementMap[nextElement.id] || nextElement,
prevElementMap.get(nextElement.id) || nextElement,
nextElement,
),
)

View File

@ -1,4 +1,3 @@
import React from "react";
import { menu, palette } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";

View File

@ -1,4 +1,3 @@
import React from "react";
import { getClientColors, getClientInitials } from "../clients";
import { Avatar } from "../components/Avatar";
import { centerScrollOn } from "../scene/scroll";

View File

@ -1,4 +1,3 @@
import React from "react";
import { AppState } from "../../src/types";
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker";
@ -7,6 +6,7 @@ import {
ArrowheadArrowIcon,
ArrowheadBarIcon,
ArrowheadDotIcon,
ArrowheadTriangleIcon,
ArrowheadNoneIcon,
EdgeRoundIcon,
EdgeSharpIcon,
@ -41,8 +41,16 @@ import {
isTextElement,
redrawTextBoundingBox,
} from "../element";
import { newElementWith } from "../element/mutateElement";
import { isLinearElement, isLinearElementType } from "../element/typeChecks";
import { mutateElement, newElementWith } from "../element/mutateElement";
import {
getBoundTextElement,
getContainerElement,
} from "../element/textElement";
import {
isBoundToContainer,
isLinearElement,
isLinearElementType,
} from "../element/typeChecks";
import {
Arrowhead,
ExcalidrawElement,
@ -52,24 +60,34 @@ import {
TextAlign,
} from "../element/types";
import { getLanguage, t } from "../i18n";
import { KEYS } from "../keys";
import { randomInteger } from "../random";
import {
canChangeSharpness,
canHaveArrowheads,
getCommonAttributeOfSelectedElements,
getSelectedElements,
getTargetElements,
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import { arrayToMap } from "../utils";
import { register } from "./register";
const FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
const changeProperty = (
elements: readonly ExcalidrawElement[],
appState: AppState,
callback: (element: ExcalidrawElement) => ExcalidrawElement,
includeBoundText = false,
) => {
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, includeBoundText),
);
return elements.map((element) => {
if (
appState.selectedElementIds[element.id] ||
selectedElementIds.get(element.id) ||
element.id === appState.editingElement?.id
) {
return callback(element);
@ -99,15 +117,95 @@ const getFormValue = function <T>(
);
};
const offsetElementAfterFontResize = (
prevElement: ExcalidrawTextElement,
nextElement: ExcalidrawTextElement,
) => {
if (isBoundToContainer(nextElement)) {
return nextElement;
}
return mutateElement(
nextElement,
{
x:
prevElement.textAlign === "left"
? prevElement.x
: prevElement.x +
(prevElement.width - nextElement.width) /
(prevElement.textAlign === "center" ? 2 : 1),
// centering vertically is non-standard, but for Excalidraw I think
// it makes sense
y: prevElement.y + (prevElement.height - nextElement.height) / 2,
},
false,
);
};
const changeFontSize = (
elements: readonly ExcalidrawElement[],
appState: AppState,
getNewFontSize: (element: ExcalidrawTextElement) => number,
) => {
const newFontSizes = new Set<number>();
return {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newFontSize = getNewFontSize(oldElement);
newFontSizes.add(newFontSize);
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
fontSize: newFontSize,
});
redrawTextBoundingBox(
newElement,
getContainerElement(oldElement),
appState,
);
newElement = offsetElementAfterFontResize(oldElement, newElement);
return newElement;
}
return oldElement;
},
true,
),
appState: {
...appState,
// update state only if we've set all select text elements to
// the same font size
currentItemFontSize:
newFontSizes.size === 1
? [...newFontSizes][0]
: appState.currentItemFontSize,
},
commitToHistory: true,
};
};
// -----------------------------------------------------------------------------
export const actionChangeStrokeColor = register({
name: "changeStrokeColor",
perform: (elements, appState, value) => {
return {
...(value.currentItemStrokeColor && {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
}),
elements: changeProperty(
elements,
appState,
(el) => {
return hasStrokeColor(el.type)
? newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
})
: el;
},
true,
),
}),
appState: {
@ -422,24 +520,7 @@ export const actionChangeOpacity = register({
export const actionChangeFontSize = register({
name: "changeFontSize",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
fontSize: value,
});
redrawTextBoundingBox(element);
return element;
}
return el;
}),
appState: {
...appState,
currentItemFontSize: value,
},
commitToHistory: true,
};
return changeFontSize(elements, appState, () => value);
},
PanelComponent: ({ elements, appState, updateData }) => (
<fieldset>
@ -471,7 +552,16 @@ export const actionChangeFontSize = register({
value={getFormValue(
elements,
appState,
(element) => isTextElement(element) && element.fontSize,
(element) => {
if (isTextElement(element)) {
return element.fontSize;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.fontSize;
}
return null;
},
appState.currentItemFontSize || DEFAULT_FONT_SIZE,
)}
onChange={(value) => updateData(value)}
@ -480,21 +570,71 @@ export const actionChangeFontSize = register({
),
});
export const actionDecreaseFontSize = register({
name: "decreaseFontSize",
perform: (elements, appState, value) => {
return changeFontSize(elements, appState, (element) =>
Math.round(
// get previous value before relative increase (doesn't work fully
// due to rounding and float precision issues)
(1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)) * element.fontSize,
),
);
},
keyTest: (event) => {
return (
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
// KEYS.COMMA needed for MacOS
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA)
);
},
});
export const actionIncreaseFontSize = register({
name: "increaseFontSize",
perform: (elements, appState, value) => {
return changeFontSize(elements, appState, (element) =>
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
);
},
keyTest: (event) => {
return (
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
// KEYS.PERIOD needed for MacOS
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD)
);
},
});
export const actionChangeFontFamily = register({
name: "changeFontFamily",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
fontFamily: value,
});
redrawTextBoundingBox(element);
return element;
}
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newElement: ExcalidrawTextElement = newElementWith(
oldElement,
{
fontFamily: value,
},
);
redrawTextBoundingBox(
newElement,
getContainerElement(oldElement),
appState,
);
return newElement;
}
return el;
}),
return oldElement;
},
true,
),
appState: {
...appState,
currentItemFontFamily: value,
@ -534,7 +674,16 @@ export const actionChangeFontFamily = register({
value={getFormValue(
elements,
appState,
(element) => isTextElement(element) && element.fontFamily,
(element) => {
if (isTextElement(element)) {
return element.fontFamily;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.fontFamily;
}
return null;
},
appState.currentItemFontFamily || DEFAULT_FONT_FAMILY,
)}
onChange={(value) => updateData(value)}
@ -548,17 +697,29 @@ export const actionChangeTextAlign = register({
name: "changeTextAlign",
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) => {
if (isTextElement(el)) {
const element: ExcalidrawTextElement = newElementWith(el, {
textAlign: value,
});
redrawTextBoundingBox(element);
return element;
}
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newElement: ExcalidrawTextElement = newElementWith(
oldElement,
{
textAlign: value,
},
);
redrawTextBoundingBox(
newElement,
getContainerElement(oldElement),
appState,
);
return newElement;
}
return el;
}),
return oldElement;
},
true,
),
appState: {
...appState,
currentItemTextAlign: value,
@ -591,7 +752,16 @@ export const actionChangeTextAlign = register({
value={getFormValue(
elements,
appState,
(element) => isTextElement(element) && element.textAlign,
(element) => {
if (isTextElement(element)) {
return element.textAlign;
}
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
return boundTextElement.textAlign;
}
return null;
},
appState.currentItemTextAlign,
)}
onChange={(value) => updateData(value)}
@ -736,6 +906,14 @@ export const actionChangeArrowhead = register({
icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
keyBinding: "r",
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: (
<ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
),
keyBinding: "t",
},
]}
value={getFormValue<Arrowhead | null>(
elements,
@ -778,6 +956,14 @@ export const actionChangeArrowhead = register({
keyBinding: "r",
icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: (
<ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
),
keyBinding: "t",
},
]}
value={getFormValue<Arrowhead | null>(
elements,

View File

@ -1,7 +1,7 @@
import { KEYS } from "../keys";
import { register } from "./register";
import { selectGroupsForSelectedElements } from "../groups";
import { getNonDeletedElements } from "../element";
import { getNonDeletedElements, isTextElement } from "../element";
export const actionSelectAll = register({
name: "selectAll",
@ -15,7 +15,10 @@ export const actionSelectAll = register({
...appState,
editingGroupId: null,
selectedElementIds: elements.reduce((map, element) => {
if (!element.isDeleted) {
if (
!element.isDeleted &&
!(isTextElement(element) && element.containerId)
) {
map[element.id] = true;
}
return map;

View File

@ -12,6 +12,7 @@ import {
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
} from "../constants";
import { getContainerElement } from "../element/textElement";
// `copiedStyles` is exported only for tests.
export let copiedStyles: string = "{}";
@ -55,13 +56,18 @@ export const actionPasteStyles = register({
opacity: pastedElement?.opacity,
roughness: pastedElement?.roughness,
});
if (isTextElement(newElement)) {
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);
redrawTextBoundingBox(
element,
getContainerElement(element),
appState,
);
}
return newElement;
}

View File

@ -8,18 +8,8 @@ import {
PanelComponentProps,
} from "./types";
import { ExcalidrawElement } from "../element/types";
import { AppProps, AppState } from "../types";
import { AppClassProperties, AppState } from "../types";
import { MODES } from "../constants";
import Library from "../data/library";
// This is the <App> component, but for now we don't care about anything but its
// `canvas` state.
type App = {
canvas: HTMLCanvasElement | null;
focusContainer: () => void;
props: AppProps;
library: Library;
};
export class ActionManager implements ActionsManagerInterface {
actions = {} as ActionsManagerInterface["actions"];
@ -28,13 +18,13 @@ export class ActionManager implements ActionsManagerInterface {
getAppState: () => Readonly<AppState>;
getElementsIncludingDeleted: () => readonly ExcalidrawElement[];
app: App;
app: AppClassProperties;
constructor(
updater: UpdaterFn,
getAppState: () => AppState,
getElementsIncludingDeleted: () => readonly ExcalidrawElement[],
app: App,
app: AppClassProperties,
) {
this.updater = (actionResult) => {
if (actionResult && "then" in actionResult) {

View File

@ -1,7 +1,11 @@
import React from "react";
import { ExcalidrawElement } from "../element/types";
import { AppState, ExcalidrawProps } from "../types";
import Library from "../data/library";
import {
AppClassProperties,
AppState,
ExcalidrawProps,
BinaryFiles,
} from "../types";
import { ToolButtonSize } from "../components/ToolButton";
/** if false, the action should be prevented */
@ -12,22 +16,18 @@ export type ActionResult =
AppState,
"offsetTop" | "offsetLeft" | "width" | "height"
> | null;
files?: BinaryFiles | null;
commitToHistory: boolean;
syncHistory?: boolean;
replaceFiles?: boolean;
}
| false;
type AppAPI = {
canvas: HTMLCanvasElement | null;
focusContainer(): void;
library: Library;
};
type ActionFn = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
formData: any,
app: AppAPI,
app: AppClassProperties,
) => ActionResult | Promise<ActionResult>;
export type UpdaterFn = (res: ActionResult) => void;
@ -101,7 +101,9 @@ export type ActionName =
| "flipVertical"
| "viewMode"
| "exportWithDarkMode"
| "toggleTheme";
| "toggleTheme"
| "increaseFontSize"
| "decreaseFontSize";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View File

@ -1,13 +1,7 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { getCommonBounds } from "./element";
interface Box {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
import { Box, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
position: "start" | "center" | "end";
@ -37,28 +31,6 @@ export const alignElements = (
});
};
export const getMaximumGroups = (
elements: ExcalidrawElement[],
): ExcalidrawElement[][] => {
const groups: Map<String, ExcalidrawElement[]> = new Map<
String,
ExcalidrawElement[]
>();
elements.forEach((element: ExcalidrawElement) => {
const groupId =
element.groupIds.length === 0
? element.id
: element.groupIds[element.groupIds.length - 1];
const currentGroupMembers = groups.get(groupId) || [];
groups.set(groupId, [...currentGroupMembers, element]);
});
return Array.from(groups.values());
};
const calculateTranslation = (
group: ExcalidrawElement[],
selectionBoundingBox: Box,
@ -88,8 +60,3 @@ const calculateTranslation = (
(groupBoundingBox[min] + groupBoundingBox[max]) / 2,
};
};
const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return { minX, minY, maxX, maxY };
};

View File

@ -4,6 +4,7 @@ import {
DEFAULT_FONT_SIZE,
DEFAULT_TEXT_ALIGN,
EXPORT_SCALES,
THEME,
} from "./constants";
import { t } from "./i18n";
import { AppState, NormalizedZoomValue } from "./types";
@ -18,7 +19,7 @@ export const getDefaultAppState = (): Omit<
"offsetTop" | "offsetLeft" | "width" | "height"
> => {
return {
theme: "light",
theme: THEME.LIGHT,
collaborators: new Map(),
currentChartType: "bar",
currentItemBackgroundColor: "transparent",
@ -78,6 +79,7 @@ export const getDefaultAppState = (): Omit<
zenModeEnabled: false,
zoom: { value: 1 as NormalizedZoomValue, translation: { x: 0, y: 0 } },
viewModeEnabled: false,
pendingImageElement: null,
};
};
@ -91,78 +93,86 @@ const APP_STATE_STORAGE_CONF = (<
browser: boolean;
/** whether to keep when exporting to file/database */
export: boolean;
/** server (shareLink/collab/...) */
server: boolean;
},
T extends Record<keyof AppState, Values>
>(
config: { [K in keyof T]: K extends keyof AppState ? T[K] : never },
) => config)({
theme: { browser: true, export: false },
collaborators: { browser: false, export: false },
currentChartType: { browser: true, export: false },
currentItemBackgroundColor: { browser: true, export: false },
currentItemEndArrowhead: { browser: true, export: false },
currentItemFillStyle: { browser: true, export: false },
currentItemFontFamily: { browser: true, export: false },
currentItemFontSize: { browser: true, export: false },
currentItemLinearStrokeSharpness: { browser: true, export: false },
currentItemOpacity: { browser: true, export: false },
currentItemRoughness: { browser: true, export: false },
currentItemStartArrowhead: { browser: true, export: false },
currentItemStrokeColor: { browser: true, export: false },
currentItemStrokeSharpness: { browser: true, export: false },
currentItemStrokeStyle: { browser: true, export: false },
currentItemStrokeWidth: { browser: true, export: false },
currentItemTextAlign: { browser: true, export: false },
cursorButton: { browser: true, export: false },
draggingElement: { browser: false, export: false },
editingElement: { browser: false, export: false },
editingGroupId: { browser: true, export: false },
editingLinearElement: { browser: false, export: false },
elementLocked: { browser: true, export: false },
elementType: { browser: true, export: false },
errorMessage: { browser: false, export: false },
exportBackground: { browser: true, export: false },
exportEmbedScene: { browser: true, export: false },
exportScale: { browser: true, export: false },
exportWithDarkMode: { browser: true, export: false },
fileHandle: { browser: false, export: false },
gridSize: { browser: true, export: true },
height: { browser: false, export: false },
isBindingEnabled: { browser: false, export: false },
isLibraryOpen: { browser: false, export: false },
isLoading: { browser: false, export: false },
isResizing: { browser: false, export: false },
isRotating: { browser: false, export: false },
lastPointerDownWith: { browser: true, export: false },
multiElement: { browser: false, export: false },
name: { browser: true, export: false },
offsetLeft: { browser: false, export: false },
offsetTop: { browser: false, export: false },
openMenu: { browser: true, export: false },
openPopup: { browser: false, export: false },
pasteDialog: { browser: false, export: false },
previousSelectedElementIds: { browser: true, export: false },
resizingElement: { browser: false, export: false },
scrolledOutside: { browser: true, export: false },
scrollX: { browser: true, export: false },
scrollY: { browser: true, export: false },
selectedElementIds: { browser: true, export: false },
selectedGroupIds: { browser: true, export: false },
selectionElement: { browser: false, export: false },
shouldCacheIgnoreZoom: { browser: true, export: false },
showHelpDialog: { browser: false, export: false },
showStats: { browser: true, export: false },
startBoundElement: { browser: false, export: false },
suggestedBindings: { browser: false, export: false },
toastMessage: { browser: false, export: false },
viewBackgroundColor: { browser: true, export: true },
width: { browser: false, export: false },
zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false },
viewModeEnabled: { browser: false, export: false },
T extends Record<keyof AppState, Values>,
>(config: { [K in keyof T]: K extends keyof AppState ? T[K] : never }) =>
config)({
theme: { browser: true, export: false, server: false },
collaborators: { browser: false, export: false, server: false },
currentChartType: { browser: true, export: false, server: false },
currentItemBackgroundColor: { browser: true, export: false, server: false },
currentItemEndArrowhead: { browser: true, export: false, server: false },
currentItemFillStyle: { browser: true, export: false, server: false },
currentItemFontFamily: { browser: true, export: false, server: false },
currentItemFontSize: { browser: true, export: false, server: false },
currentItemLinearStrokeSharpness: {
browser: true,
export: false,
server: false,
},
currentItemOpacity: { browser: true, export: false, server: false },
currentItemRoughness: { browser: true, export: false, server: false },
currentItemStartArrowhead: { browser: true, export: false, server: false },
currentItemStrokeColor: { browser: true, export: false, server: false },
currentItemStrokeSharpness: { browser: true, export: false, server: false },
currentItemStrokeStyle: { browser: true, export: false, server: false },
currentItemStrokeWidth: { browser: true, export: false, server: false },
currentItemTextAlign: { browser: true, export: false, server: false },
cursorButton: { browser: true, export: false, server: false },
draggingElement: { browser: false, export: false, server: false },
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
elementLocked: { browser: true, export: false, server: false },
elementType: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },
exportBackground: { browser: true, export: false, server: false },
exportEmbedScene: { browser: true, export: false, server: false },
exportScale: { browser: true, export: false, server: false },
exportWithDarkMode: { browser: true, export: false, server: false },
fileHandle: { browser: false, export: false, server: false },
gridSize: { browser: true, export: true, server: true },
height: { browser: false, export: false, server: false },
isBindingEnabled: { browser: false, export: false, server: false },
isLibraryOpen: { browser: false, export: false, server: false },
isLoading: { browser: false, export: false, server: false },
isResizing: { browser: false, export: false, server: false },
isRotating: { browser: false, export: false, server: false },
lastPointerDownWith: { browser: true, export: false, server: false },
multiElement: { browser: false, export: false, server: false },
name: { browser: true, export: false, server: false },
offsetLeft: { browser: false, export: false, server: false },
offsetTop: { browser: false, export: false, server: false },
openMenu: { browser: true, export: false, server: false },
openPopup: { browser: false, export: false, server: false },
pasteDialog: { browser: false, export: false, server: false },
previousSelectedElementIds: { browser: true, export: false, server: false },
resizingElement: { browser: false, export: false, server: false },
scrolledOutside: { browser: true, export: false, server: false },
scrollX: { browser: true, export: false, server: false },
scrollY: { browser: true, export: false, server: false },
selectedElementIds: { browser: true, export: false, server: false },
selectedGroupIds: { browser: true, export: false, server: false },
selectionElement: { browser: false, export: false, server: false },
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
showHelpDialog: { browser: false, export: false, server: false },
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
toastMessage: { browser: false, export: false, server: false },
viewBackgroundColor: { browser: true, export: true, server: true },
width: { browser: false, export: false, server: false },
zenModeEnabled: { browser: true, export: false, server: false },
zoom: { browser: true, export: false, server: false },
viewModeEnabled: { browser: false, export: false, server: false },
pendingImageElement: { browser: false, export: false, server: false },
});
const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
const _clearAppStateForStorage = <
ExportType extends "export" | "browser" | "server",
>(
appState: Partial<AppState>,
exportType: ExportType,
) => {
@ -175,8 +185,10 @@ const _clearAppStateForStorage = <ExportType extends "export" | "browser">(
for (const key of Object.keys(appState) as (keyof typeof appState)[]) {
const propConfig = APP_STATE_STORAGE_CONF[key];
if (propConfig?.[exportType]) {
// @ts-ignore see https://github.com/microsoft/TypeScript/issues/31445
stateForExport[key] = appState[key];
const nextValue = appState[key];
// https://github.com/microsoft/TypeScript/issues/31445
(stateForExport as any)[key] = nextValue;
}
}
return stateForExport;
@ -189,3 +201,7 @@ export const clearAppStateForLocalStorage = (appState: Partial<AppState>) => {
export const cleanAppStateForExport = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "export");
};
export const clearAppStateForDatabase = (appState: Partial<AppState>) => {
return _clearAppStateForStorage(appState, "server");
};

View File

@ -3,19 +3,22 @@ import {
NonDeletedExcalidrawElement,
} from "./element/types";
import { getSelectedElements } from "./scene";
import { AppState } from "./types";
import { AppState, BinaryFiles } from "./types";
import { SVG_EXPORT_TAG } from "./scene/export";
import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts";
import { EXPORT_DATA_TYPES } from "./constants";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants";
import { isInitializedImageElement } from "./element/typeChecks";
type ElementsClipboard = {
type: typeof EXPORT_DATA_TYPES.excalidrawClipboard;
elements: ExcalidrawElement[];
files: BinaryFiles | undefined;
};
export interface ClipboardData {
spreadsheet?: Spreadsheet;
elements?: readonly ExcalidrawElement[];
files?: BinaryFiles;
text?: string;
errorMessage?: string;
}
@ -37,7 +40,7 @@ export const probablySupportsClipboardBlob =
const clipboardContainsElements = (
contents: any,
): contents is { elements: ExcalidrawElement[] } => {
): contents is { elements: ExcalidrawElement[]; files?: BinaryFiles } => {
if (
[
EXPORT_DATA_TYPES.excalidraw,
@ -53,17 +56,26 @@ const clipboardContainsElements = (
export const copyToClipboard = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
) => {
// select binded text elements when copying
const selectedElements = getSelectedElements(elements, appState, true);
const contents: ElementsClipboard = {
type: EXPORT_DATA_TYPES.excalidrawClipboard,
elements: getSelectedElements(elements, appState),
elements: selectedElements,
files: selectedElements.reduce((acc, element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
acc[element.fileId] = files[element.fileId];
}
return acc;
}, {} as BinaryFiles),
};
const json = JSON.stringify(contents);
CLIPBOARD = json;
try {
PREFER_APP_CLIPBOARD = false;
await copyTextToSystemClipboard(json);
} catch (error) {
} catch (error: any) {
PREFER_APP_CLIPBOARD = true;
console.error(error);
}
@ -76,7 +88,7 @@ const getAppClipboard = (): Partial<ElementsClipboard> => {
try {
return JSON.parse(CLIPBOARD);
} catch (error) {
} catch (error: any) {
console.error(error);
return {};
}
@ -138,7 +150,10 @@ export const parseClipboard = async (
try {
const systemClipboardData = JSON.parse(systemClipboard);
if (clipboardContainsElements(systemClipboardData)) {
return { elements: systemClipboardData.elements };
return {
elements: systemClipboardData.elements,
files: systemClipboardData.files,
};
}
return appClipboardData;
} catch {
@ -153,7 +168,7 @@ export const parseClipboard = async (
export const copyBlobToClipboardAsPng = async (blob: Blob) => {
await navigator.clipboard.write([
new window.ClipboardItem({ "image/png": blob }),
new window.ClipboardItem({ [MIME_TYPES.png]: blob }),
]);
};
@ -165,7 +180,7 @@ export const copyTextToSystemClipboard = async (text: string | null) => {
// not focused
await navigator.clipboard.writeText(text || "");
copied = true;
} catch (error) {
} catch (error: any) {
console.error(error);
}
}
@ -205,7 +220,7 @@ const copyTextViaExecCommand = (text: string) => {
textarea.setSelectionRange(0, textarea.value.length);
success = document.execCommand("copy");
} catch (error) {
} catch (error: any) {
console.error(error);
}

View File

@ -1,7 +1,7 @@
import React from "react";
import { ActionManager } from "../actions/manager";
import { getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement, PointerType } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import {
@ -18,6 +18,7 @@ import { AppState, Zoom } from "../types";
import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
export const SelectedShapeActions = ({
appState,
@ -48,9 +49,22 @@ export const SelectedShapeActions = ({
hasBackground(elementType) ||
targetElements.some((element) => hasBackground(element.type));
let commonSelectedType: string | null = targetElements[0]?.type || null;
for (const element of targetElements) {
if (element.type !== commonSelectedType) {
commonSelectedType = null;
break;
}
}
return (
<div className="panelColumn">
{renderAction("changeStrokeColor")}
{((hasStrokeColor(elementType) &&
elementType !== "image" &&
commonSelectedType !== "image") ||
targetElements.some((element) => hasStrokeColor(element.type))) &&
renderAction("changeStrokeColor")}
{showChangeBackgroundIcons && renderAction("changeBackgroundColor")}
{showFillIcons && renderAction("changeFillStyle")}
@ -155,18 +169,20 @@ export const ShapesSwitcher = ({
canvas,
elementType,
setAppState,
onImageAction,
}: {
canvas: HTMLCanvasElement | null;
elementType: ExcalidrawElement["type"];
setAppState: React.Component<any, AppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
}) => (
<>
{SHAPES.map(({ value, icon, key }, index) => {
const label = t(`toolBar.${value}`);
const letter = typeof key === "string" ? key : key[0];
const shortcut = `${capitalizeString(letter)} ${t("helpDialog.or")} ${
index + 1
}`;
const letter = key && (typeof key === "string" ? key : key[0]);
const shortcut = letter
? `${capitalizeString(letter)} ${t("helpDialog.or")} ${index + 1}`
: `${index + 1}`;
return (
<ToolButton
className="Shape"
@ -180,14 +196,16 @@ export const ShapesSwitcher = ({
aria-label={capitalizeString(label)}
aria-keyshortcuts={shortcut}
data-testid={value}
onChange={() => {
onChange={({ pointerType }) => {
setAppState({
elementType: value,
multiElement: null,
selectedElementIds: {},
});
setCursorForShape(canvas, value);
setAppState({});
if (value === "image") {
onImageAction({ pointerType });
}
}}
/>
);

View File

@ -1,4 +1,3 @@
import React from "react";
import Stack from "../components/Stack";
import { ToolButton } from "../components/ToolButton";
import { save, file } from "../components/icons";

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx";
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />

View File

@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx";
export const ButtonSelect = <T extends Object>({

View File

@ -48,6 +48,10 @@
.ToolIcon__label {
color: $oc-white;
}
.Spinner {
--spinner-color: #fff;
}
}
}
}

View File

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

View File

@ -6,16 +6,19 @@ import "./CheckboxItem.scss";
export const CheckboxItem: React.FC<{
checked: boolean;
onChange: (checked: boolean) => void;
}> = ({ children, checked, onChange }) => {
onChange: (checked: boolean, event: React.MouseEvent) => void;
className?: string;
}> = ({ children, checked, onChange, className }) => {
return (
<div
className={clsx("Checkbox", { "is-checked": checked })}
className={clsx("Checkbox", className, { "is-checked": checked })}
onClick={(event) => {
onChange(!checked);
((event.currentTarget as HTMLDivElement).querySelector(
".Checkbox-box",
) as HTMLButtonElement).focus();
onChange(!checked, event);
(
(event.currentTarget as HTMLDivElement).querySelector(
".Checkbox-box",
) as HTMLButtonElement
).focus();
}}
>
<button className="Checkbox-box" role="checkbox" aria-checked={checked}>

View File

@ -0,0 +1,43 @@
import { useState } from "react";
import { t } from "../i18n";
import { useIsMobile } from "./App";
import { trash } from "./icons";
import { ToolButton } from "./ToolButton";
import ConfirmDialog from "./ConfirmDialog";
const ClearCanvas = ({ onConfirm }: { onConfirm: () => void }) => {
const [showDialog, setShowDialog] = useState(false);
const toggleDialog = () => {
setShowDialog(!showDialog);
};
return (
<>
<ToolButton
type="button"
icon={trash}
title={t("buttons.clearReset")}
aria-label={t("buttons.clearReset")}
showAriaLabel={useIsMobile()}
onClick={toggleDialog}
data-testid="clear-canvas-button"
/>
{showDialog && (
<ConfirmDialog
onConfirm={() => {
onConfirm();
toggleDialog();
}}
onCancel={toggleDialog}
title={t("clearCanvasDialog.title")}
>
<p className="clear-canvas__content"> {t("alerts.clearReset")}</p>
</ConfirmDialog>
)}
</>
);
};
export default ClearCanvas;

View File

@ -1,4 +1,3 @@
import React from "react";
import clsx from "clsx";
import { ToolButton } from "./ToolButton";
import { t } from "../i18n";

View File

@ -0,0 +1,37 @@
@import "../css/variables.module";
.excalidraw {
.confirm-dialog {
&-buttons {
display: flex;
padding: 0.2rem 0;
justify-content: flex-end;
}
.ToolIcon__icon {
min-width: 2.5rem;
width: auto;
font-size: 1rem;
}
.ToolIcon_type_button {
margin-left: 0.8rem;
padding: 0 0.5rem;
}
&__content {
font-size: 1rem;
}
&--confirm.ToolIcon_type_button {
background-color: $oc-red-6;
&:hover {
background-color: $oc-red-8;
}
.ToolIcon__icon {
color: $oc-white;
}
}
}
}

View File

@ -0,0 +1,52 @@
import { t } from "../i18n";
import { Dialog, DialogProps } from "./Dialog";
import { ToolButton } from "./ToolButton";
import "./ConfirmDialog.scss";
interface Props extends Omit<DialogProps, "onCloseRequest"> {
onConfirm: () => void;
onCancel: () => void;
confirmText?: string;
cancelText?: string;
}
const ConfirmDialog = (props: Props) => {
const {
onConfirm,
onCancel,
children,
confirmText = t("buttons.confirm"),
cancelText = t("buttons.cancel"),
className = "",
...rest
} = props;
return (
<Dialog
onCloseRequest={onCancel}
small={true}
{...rest}
className={`confirm-dialog ${className}`}
>
{children}
<div className="confirm-dialog-buttons">
<ToolButton
type="button"
title={cancelText}
aria-label={cancelText}
label={cancelText}
onClick={onCancel}
className="confirm-dialog--cancel"
/>
<ToolButton
type="button"
title={confirmText}
aria-label={confirmText}
label={confirmText}
onClick={onConfirm}
className="confirm-dialog--confirm"
/>
</div>
</Dialog>
);
};
export default ConfirmDialog;

View File

@ -1,4 +1,3 @@
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import clsx from "clsx";
import { Popover } from "./Popover";

View File

@ -1,16 +1,15 @@
import "./ToolIcon.scss";
import React from "react";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
export type Appearence = "light" | "dark";
import { THEME } from "../constants";
import { Theme } from "../element/types";
// We chose to use only explicit toggle and not a third option for system value,
// but this could be added in the future.
export const DarkModeToggle = (props: {
value: Appearence;
onChange: (value: Appearence) => void;
value: Theme;
onChange: (value: Theme) => void;
title?: string;
}) => {
const title =
@ -20,10 +19,12 @@ export const DarkModeToggle = (props: {
return (
<ToolButton
type="icon"
icon={props.value === "light" ? ICONS.MOON : ICONS.SUN}
icon={props.value === THEME.LIGHT ? ICONS.MOON : ICONS.SUN}
title={title}
aria-label={title}
onClick={() => props.onChange(props.value === "dark" ? "light" : "dark")}
onClick={() =>
props.onChange(props.value === THEME.DARK ? THEME.LIGHT : THEME.DARK)
}
data-testid="toggle-dark-mode"
/>
);

View File

@ -10,7 +10,7 @@ import { Island } from "./Island";
import { Modal } from "./Modal";
import { AppState } from "../types";
export const Dialog = (props: {
export interface DialogProps {
children: React.ReactNode;
className?: string;
small?: boolean;
@ -18,7 +18,10 @@ export const Dialog = (props: {
title: React.ReactNode;
autofocus?: boolean;
theme?: AppState["theme"];
}) => {
closeOnClickOutside?: boolean;
}
export const Dialog = (props: DialogProps) => {
const [islandNode, setIslandNode] = useCallbackRefState<HTMLDivElement>();
const [lastActiveElement] = useState(document.activeElement);
const { id } = useExcalidrawContainer();
@ -81,6 +84,7 @@ export const Dialog = (props: {
maxWidth={props.small ? 550 : 800}
onCloseRequest={onClose}
theme={props.theme}
closeOnClickOutside={props.closeOnClickOutside}
>
<Island ref={setIslandNode}>
<h2 id={`${id}-dialog-title`} className="Dialog__title">

View File

@ -154,9 +154,11 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
<Shortcut label={t("toolBar.line")} shortcuts={["P", "6"]} />
<Shortcut
label={t("toolBar.freedraw")}
shortcuts={["Shift+P", "7"]}
shortcuts={["Shift + P", "X", "7"]}
/>
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
<Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
<Shortcut
label={t("helpDialog.editSelectedShape")}
shortcuts={[
@ -258,6 +260,18 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.multiSelect")}
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepSelect")}
shortcuts={[
getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`),
]}
/>
<Shortcut
label={t("helpDialog.deepBoxSelect")}
shortcuts={[
getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`),
]}
/>
<Shortcut
label={t("labels.moveCanvas")}
shortcuts={[
@ -380,6 +394,14 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.showBackground")}
shortcuts={[getShortcutKey("G")]}
/>
<Shortcut
label={t("labels.decreaseFontSize")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+<")]}
/>
<Shortcut
label={t("labels.increaseFontSize")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+>")]}
/>
</ShortcutIsland>
</Column>
</Columns>

View File

@ -1,4 +1,3 @@
import React from "react";
import { questionCircle } from "../components/icons";
type HelpIconProps = {

View File

@ -1,21 +1,27 @@
import React from "react";
import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene";
import "./HintViewer.scss";
import { AppState } from "../types";
import { isLinearElement, isTextElement } from "../element/typeChecks";
import {
isImageElement,
isLinearElement,
isTextBindableContainer,
isTextElement,
} from "../element/typeChecks";
import { getShortcutKey } from "../utils";
interface Hint {
interface HintViewerProps {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean;
}
const getHints = ({ appState, elements }: Hint) => {
const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
if (elementType === "arrow" || elementType === "line") {
if (!multiMode) {
return t("hints.linearElement");
@ -31,7 +37,12 @@ const getHints = ({ appState, elements }: Hint) => {
return t("hints.text");
}
if (appState.elementType === "image" && appState.pendingImageElement) {
return t("hints.placeImage");
}
const selectedElements = getSelectedElements(elements, appState);
if (
isResizing &&
lastPointerDownWith === "mouse" &&
@ -41,22 +52,15 @@ const getHints = ({ appState, elements }: Hint) => {
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
return t("hints.lockAngle");
}
return t("hints.resize");
return isImageElement(targetElement)
? t("hints.resizeImage")
: t("hints.resize");
}
if (isRotating && lastPointerDownWith === "mouse") {
return t("hints.rotate");
}
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (appState.editingLinearElement) {
return appState.editingLinearElement.activePointIndex
? t("hints.lineEditor_pointSelected")
: t("hints.lineEditor_nothingSelected");
}
return t("hints.lineEditor_info");
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
@ -65,13 +69,45 @@ const getHints = ({ appState, elements }: Hint) => {
return t("hints.text_editing");
}
if (elementType === "selection") {
if (
appState.draggingElement?.type === "selection" &&
!appState.editingElement &&
!appState.editingLinearElement
) {
return t("hints.deepBoxSelect");
}
if (!selectedElements.length && !isMobile) {
return t("hints.canvasPanning");
}
}
if (selectedElements.length === 1) {
if (isLinearElement(selectedElements[0])) {
if (appState.editingLinearElement) {
return appState.editingLinearElement.selectedPointsIndices
? t("hints.lineEditor_pointSelected")
: t("hints.lineEditor_nothingSelected");
}
return t("hints.lineEditor_info");
}
if (isTextBindableContainer(selectedElements[0])) {
return t("hints.bindTextToElement");
}
}
return null;
};
export const HintViewer = ({ appState, elements }: Hint) => {
export const HintViewer = ({
appState,
elements,
isMobile,
}: HintViewerProps) => {
let hint = getHints({
appState,
elements,
isMobile,
});
if (!hint) {
return null;

View File

@ -22,7 +22,7 @@
align-items: center;
justify-content: center;
&:focus {
&:focus-visible {
outline: transparent;
background-color: var(--button-gray-2);
& svg {
@ -90,7 +90,7 @@
.picker-content {
padding: 0.5rem;
display: grid;
grid-auto-flow: column;
grid-template-columns: repeat(3, auto);
grid-gap: 0.5rem;
border-radius: 4px;
:root[dir="rtl"] & {

View File

@ -9,7 +9,7 @@ import { t } from "../i18n";
import { useIsMobile } from "./App";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { exportToCanvas } from "../scene/export";
import { AppState } from "../types";
import { AppState, BinaryFiles } from "../types";
import { Dialog } from "./Dialog";
import { clipboard, exportImage } from "./icons";
import Stack from "./Stack";
@ -79,6 +79,7 @@ const ExportButton: React.FC<{
const ImageExportModal = ({
elements,
appState,
files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager,
onExportToPng,
@ -87,6 +88,7 @@ const ImageExportModal = ({
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
exportPadding?: number;
actionManager: ActionsManagerInterface;
onExportToPng: ExportCB;
@ -100,7 +102,7 @@ const ImageExportModal = ({
const { exportBackground, viewBackgroundColor } = appState;
const exportedElements = exportSelected
? getSelectedElements(elements, appState)
? getSelectedElements(elements, appState, true)
: elements;
useEffect(() => {
@ -112,29 +114,25 @@ const ImageExportModal = ({
if (!previewNode) {
return;
}
try {
const canvas = exportToCanvas(exportedElements, appState, {
exportBackground,
viewBackgroundColor,
exportPadding,
});
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
canvasToBlob(canvas)
.then(() => {
exportToCanvas(exportedElements, appState, files, {
exportBackground,
viewBackgroundColor,
exportPadding,
})
.then((canvas) => {
// if converting to blob fails, there's some problem that will
// likely prevent preview and export (e.g. canvas too big)
return canvasToBlob(canvas).then(() => {
renderPreview(canvas, previewNode);
})
.catch((error) => {
console.error(error);
renderPreview(new CanvasError(), previewNode);
});
} catch (error) {
console.error(error);
renderPreview(new CanvasError(), previewNode);
}
})
.catch((error) => {
console.error(error);
renderPreview(new CanvasError(), previewNode);
});
}, [
appState,
files,
exportedElements,
exportBackground,
exportPadding,
@ -220,6 +218,7 @@ const ImageExportModal = ({
export const ImageExportDialog = ({
elements,
appState,
files,
exportPadding = DEFAULT_EXPORT_PADDING,
actionManager,
onExportToPng,
@ -228,6 +227,7 @@ export const ImageExportDialog = ({
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
exportPadding?: number;
actionManager: ActionsManagerInterface;
onExportToPng: ExportCB;
@ -258,6 +258,7 @@ export const ImageExportDialog = ({
<ImageExportModal
elements={elements}
appState={appState}
files={files}
exportPadding={exportPadding}
actionManager={actionManager}
onExportToPng={onExportToPng}

View File

@ -1,30 +1,25 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { LoadingMessage } from "./LoadingMessage";
import { defaultLang, Language, languages, setLanguage } from "../i18n";
interface Props {
langCode: Language["code"];
children: React.ReactElement;
}
interface State {
isLoading: boolean;
}
export class InitializeApp extends React.Component<Props, State> {
public state: { isLoading: boolean } = {
isLoading: true,
};
async componentDidMount() {
export const InitializeApp = (props: Props) => {
const [loading, setLoading] = useState(true);
useEffect(() => {
const updateLang = async () => {
await setLanguage(currentLang);
};
const currentLang =
languages.find((lang) => lang.code === this.props.langCode) ||
defaultLang;
await setLanguage(currentLang);
this.setState({
isLoading: false,
});
}
languages.find((lang) => lang.code === props.langCode) || defaultLang;
updateLang();
setLoading(false);
}, [props.langCode]);
public render() {
return this.state.isLoading ? <LoadingMessage /> : this.props.children;
}
}
return loading ? <LoadingMessage /> : props.children;
};

View File

@ -3,7 +3,7 @@
--padding: 0;
background-color: var(--island-bg-color);
box-shadow: var(--shadow-island);
border-radius: 4px;
border-radius: var(--border-radius-lg);
padding: calc(var(--padding) * var(--space-factor));
position: relative;
transition: box-shadow 0.5s ease-in-out;

View File

@ -3,7 +3,7 @@ import { ActionsManagerInterface } from "../actions/types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { useIsMobile } from "./App";
import { AppState, ExportOpts } from "../types";
import { AppState, ExportOpts, BinaryFiles } from "../types";
import { Dialog } from "./Dialog";
import { exportFile, exportToFileIcon, link } from "./icons";
import { ToolButton } from "./ToolButton";
@ -21,11 +21,13 @@ export type ExportCB = (
const JSONExportModal = ({
elements,
appState,
files,
actionManager,
exportOpts,
canvas,
}: {
appState: AppState;
files: BinaryFiles;
elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionsManagerInterface;
onCloseRequest: () => void;
@ -68,12 +70,14 @@ const JSONExportModal = ({
title={t("exportDialog.link_button")}
aria-label={t("exportDialog.link_button")}
showAriaLabel={true}
onClick={() => onExportToBackend(elements, appState, canvas)}
onClick={() =>
onExportToBackend(elements, appState, files, canvas)
}
/>
</Card>
)}
{exportOpts.renderCustomUI &&
exportOpts.renderCustomUI(elements, appState, canvas)}
exportOpts.renderCustomUI(elements, appState, files, canvas)}
</div>
</div>
);
@ -82,12 +86,14 @@ const JSONExportModal = ({
export const JSONExportDialog = ({
elements,
appState,
files,
actionManager,
exportOpts,
canvas,
}: {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
appState: AppState;
files: BinaryFiles;
actionManager: ActionsManagerInterface;
exportOpts: ExportOpts;
canvas: HTMLCanvasElement | null;
@ -116,6 +122,7 @@ export const JSONExportDialog = ({
<JSONExportModal
elements={elements}
appState={appState}
files={files}
actionManager={actionManager}
onCloseRequest={handleClose}
exportOpts={exportOpts}

View File

@ -1,42 +1,6 @@
@import "open-color/open-color";
.excalidraw {
.layer-ui__library {
margin: auto;
display: flex;
align-items: center;
justify-content: center;
.layer-ui__library-header {
display: flex;
align-items: center;
width: 100%;
margin: 2px 0;
button {
// 2px from the left to account for focus border of left-most button
margin: 0 2px;
}
a {
margin-inline-start: auto;
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
padding-inline-end: 18px;
white-space: nowrap;
}
}
}
.layer-ui__library-message {
padding: 10px 20px;
max-width: 200px;
}
.layer-ui__library-items {
max-height: 50vh;
overflow: auto;
}
.layer-ui__wrapper {
z-index: var(--zIndex-layerUI);

View File

@ -1,28 +1,15 @@
import clsx from "clsx";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import React, { useCallback } from "react";
import { ActionManager } from "../actions/manager";
import { CLASSES } from "../constants";
import { exportCanvas } from "../data";
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
import { isTextElement, showSelectedShapeActions } from "../element";
import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import { useIsMobile } from "../components/App";
import { calculateScrollCenter, getSelectedElements } from "../scene";
import { ExportType } from "../scene/types";
import {
AppProps,
AppState,
ExcalidrawProps,
LibraryItem,
LibraryItems,
} from "../types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import { muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher, ZoomActions } from "./Actions";
import { BackgroundPickerAndDarkModeToggle } from "./BackgroundPickerAndDarkModeToggle";
@ -31,10 +18,7 @@ import { ErrorDialog } from "./ErrorDialog";
import { ExportCB, ImageExportDialog } from "./ImageExportDialog";
import { FixedSideContainer } from "./FixedSideContainer";
import { HintViewer } from "./HintViewer";
import { exportFile, load, trash } from "./icons";
import { Island } from "./Island";
import "./LayerUI.scss";
import { LibraryUnit } from "./LibraryUnit";
import { LoadingMessage } from "./LoadingMessage";
import { LockButton } from "./LockButton";
import { MobileMenu } from "./MobileMenu";
@ -42,17 +26,21 @@ import { PasteChartDialog } from "./PasteChartDialog";
import { Section } from "./Section";
import { HelpDialog } from "./HelpDialog";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import { UserList } from "./UserList";
import Library from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob";
import { LibraryMenu } from "./LibraryMenu";
import "./LayerUI.scss";
import "./Toolbar.scss";
interface LayerUIProps {
actionManager: ActionManager;
appState: AppState;
files: BinaryFiles;
canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
@ -65,7 +53,10 @@ interface LayerUIProps {
toggleZenMode: () => void;
langCode: Language["code"];
isCollaborating: boolean;
renderTopRightUI?: (isMobile: boolean, appState: AppState) => JSX.Element;
renderTopRightUI?: (
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
viewModeEnabled: boolean;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
@ -73,295 +64,13 @@ interface LayerUIProps {
focusContainer: () => void;
library: Library;
id: string;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
}
const useOnClickOutside = (
ref: RefObject<HTMLElement>,
cb: (event: MouseEvent) => void,
) => {
useEffect(() => {
const listener = (event: MouseEvent) => {
if (!ref.current) {
return;
}
if (
event.target instanceof Element &&
(ref.current.contains(event.target) ||
!document.body.contains(event.target))
) {
return;
}
cb(event);
};
document.addEventListener("pointerdown", listener, false);
return () => {
document.removeEventListener("pointerdown", listener);
};
}, [ref, cb]);
};
const LibraryMenuItems = ({
libraryItems,
onRemoveFromLibrary,
onAddToLibrary,
onInsertShape,
pendingElements,
theme,
setAppState,
setLibraryItems,
libraryReturnUrl,
focusContainer,
library,
id,
}: {
libraryItems: LibraryItems;
pendingElements: LibraryItem;
onRemoveFromLibrary: (index: number) => void;
onInsertShape: (elements: LibraryItem) => void;
onAddToLibrary: (elements: LibraryItem) => void;
theme: AppState["theme"];
setAppState: React.Component<any, AppState>["setState"];
setLibraryItems: (library: LibraryItems) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}) => {
const isMobile = useIsMobile();
const numCells = libraryItems.length + (pendingElements.length > 0 ? 1 : 0);
const CELLS_PER_ROW = isMobile ? 4 : 6;
const numRows = Math.max(1, Math.ceil(numCells / CELLS_PER_ROW));
const rows = [];
let addedPendingElements = false;
const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
rows.push(
<div className="layer-ui__library-header" key="library-header">
<ToolButton
key="import"
type="button"
title={t("buttons.load")}
aria-label={t("buttons.load")}
icon={load}
onClick={() => {
importLibraryFromJSON(library)
.then(() => {
// Close and then open to get the libraries updated
setAppState({ isLibraryOpen: false });
setAppState({ isLibraryOpen: true });
})
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
/>
{!!libraryItems.length && (
<>
<ToolButton
key="export"
type="button"
title={t("buttons.export")}
aria-label={t("buttons.export")}
icon={exportFile}
onClick={() => {
saveLibraryAsJSON(library)
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
/>
<ToolButton
key="reset"
type="button"
title={t("buttons.resetLibrary")}
aria-label={t("buttons.resetLibrary")}
icon={trash}
onClick={() => {
if (window.confirm(t("alerts.resetLibrary"))) {
library.resetLibrary();
setLibraryItems([]);
focusContainer();
}
}}
/>
</>
)}
<a
href={`https://libraries.excalidraw.com?target=${
window.name || "_blank"
}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}`}
target="_excalidraw_libraries"
>
{t("labels.libraries")}
</a>
</div>,
);
for (let row = 0; row < numRows; row++) {
const y = CELLS_PER_ROW * row;
const children = [];
for (let x = 0; x < CELLS_PER_ROW; x++) {
const shouldAddPendingElements: boolean =
pendingElements.length > 0 &&
!addedPendingElements &&
y + x >= libraryItems.length;
addedPendingElements = addedPendingElements || shouldAddPendingElements;
children.push(
<Stack.Col key={x}>
<LibraryUnit
elements={libraryItems[y + x]}
pendingElements={
shouldAddPendingElements ? pendingElements : undefined
}
onRemoveFromLibrary={onRemoveFromLibrary.bind(null, y + x)}
onClick={
shouldAddPendingElements
? onAddToLibrary.bind(null, pendingElements)
: onInsertShape.bind(null, libraryItems[y + x])
}
/>
</Stack.Col>,
);
}
rows.push(
<Stack.Row align="center" gap={1} key={row}>
{children}
</Stack.Row>,
);
}
return (
<Stack.Col align="start" gap={1} className="layer-ui__library-items">
{rows}
</Stack.Col>
);
};
const LibraryMenu = ({
onClickOutside,
onInsertShape,
pendingElements,
onAddToLibrary,
theme,
setAppState,
libraryReturnUrl,
focusContainer,
library,
id,
}: {
pendingElements: LibraryItem;
onClickOutside: (event: MouseEvent) => void;
onInsertShape: (elements: LibraryItem) => void;
onAddToLibrary: () => void;
theme: AppState["theme"];
setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => {
// If click on the library icon, do nothing.
if ((event.target as Element).closest(".ToolIcon_type_button__library")) {
return;
}
onClickOutside(event);
});
const [libraryItems, setLibraryItems] = useState<LibraryItems>([]);
const [loadingState, setIsLoading] = useState<
"preloading" | "loading" | "ready"
>("preloading");
const loadingTimerRef = useRef<NodeJS.Timeout | null>(null);
useEffect(() => {
Promise.race([
new Promise((resolve) => {
loadingTimerRef.current = setTimeout(() => {
resolve("loading");
}, 100);
}),
library.loadLibrary().then((items) => {
setLibraryItems(items);
setIsLoading("ready");
}),
]).then((data) => {
if (data === "loading") {
setIsLoading("loading");
}
});
return () => {
clearTimeout(loadingTimerRef.current!);
};
}, [library]);
const removeFromLibrary = useCallback(
async (indexToRemove) => {
const items = await library.loadLibrary();
const nextItems = items.filter((_, index) => index !== indexToRemove);
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setLibraryItems(nextItems);
},
[library, setAppState],
);
const addToLibrary = useCallback(
async (elements: LibraryItem) => {
const items = await library.loadLibrary();
const nextItems = [...items, elements];
onAddToLibrary();
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
setLibraryItems(nextItems);
},
[onAddToLibrary, library, setAppState],
);
return loadingState === "preloading" ? null : (
<Island padding={1} ref={ref} className="layer-ui__library">
{loadingState === "loading" ? (
<div className="layer-ui__library-message">
{t("labels.libraryLoadingMessage")}
</div>
) : (
<LibraryMenuItems
libraryItems={libraryItems}
onRemoveFromLibrary={removeFromLibrary}
onAddToLibrary={addToLibrary}
onInsertShape={onInsertShape}
pendingElements={pendingElements}
setAppState={setAppState}
setLibraryItems={setLibraryItems}
libraryReturnUrl={libraryReturnUrl}
focusContainer={focusContainer}
library={library}
theme={theme}
id={id}
/>
)}
</Island>
);
};
const LayerUI = ({
actionManager,
appState,
files,
setAppState,
canvas,
elements,
@ -381,6 +90,7 @@ const LayerUI = ({
focusContainer,
library,
id,
onImageAction,
}: LayerUIProps) => {
const isMobile = useIsMobile();
@ -393,6 +103,7 @@ const LayerUI = ({
<JSONExportDialog
elements={elements}
appState={appState}
files={files}
actionManager={actionManager}
exportOpts={UIOptions.canvasActions.export}
canvas={canvas}
@ -405,33 +116,40 @@ const LayerUI = ({
return null;
}
const createExporter = (type: ExportType): ExportCB => async (
exportedElements,
) => {
const fileHandle = await exportCanvas(type, exportedElements, appState, {
exportBackground: appState.exportBackground,
name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor,
})
.catch(muteFSAbortError)
.catch((error) => {
console.error(error);
setAppState({ errorMessage: error.message });
});
const createExporter =
(type: ExportType): ExportCB =>
async (exportedElements) => {
const fileHandle = await exportCanvas(
type,
exportedElements,
appState,
files,
{
exportBackground: appState.exportBackground,
name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor,
},
)
.catch(muteFSAbortError)
.catch((error) => {
console.error(error);
setAppState({ errorMessage: error.message });
});
if (
appState.exportEmbedScene &&
fileHandle &&
isImageFileHandle(fileHandle)
) {
setAppState({ fileHandle });
}
};
if (
appState.exportEmbedScene &&
fileHandle &&
isImageFileHandle(fileHandle)
) {
setAppState({ fileHandle });
}
};
return (
<ImageExportDialog
elements={elements}
appState={appState}
files={files}
actionManager={actionManager}
onExportToPng={createExporter("png")}
onExportToSvg={createExporter("svg")}
@ -465,6 +183,7 @@ const LayerUI = ({
</Section>
);
};
const renderCanvasActions = () => (
<Section
heading="canvasActions"
@ -532,12 +251,15 @@ const LayerUI = ({
</Section>
);
const closeLibrary = useCallback(
(event) => {
setAppState({ isLibraryOpen: false });
},
[setAppState],
);
const closeLibrary = useCallback(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
// Prevent closing if any dialog is open
if (isDialogOpen) {
return;
}
setAppState({ isLibraryOpen: false });
}, [setAppState]);
const deselectItems = useCallback(() => {
setAppState({
@ -548,8 +270,8 @@ const LayerUI = ({
const libraryMenu = appState.isLibraryOpen ? (
<LibraryMenu
pendingElements={getSelectedElements(elements, appState)}
onClickOutside={closeLibrary}
pendingElements={getSelectedElements(elements, appState, true)}
onClose={closeLibrary}
onInsertShape={onInsertElements}
onAddToLibrary={deselectItems}
setAppState={setAppState}
@ -557,7 +279,9 @@ const LayerUI = ({
focusContainer={focusContainer}
library={library}
theme={appState.theme}
files={files}
id={id}
appState={appState}
/>
) : null;
@ -583,7 +307,12 @@ const LayerUI = ({
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="start">
<Stack.Row gap={1}>
<Stack.Row
gap={1}
className={clsx("App-toolbar-container", {
"zen-mode": zenModeEnabled,
})}
>
<LockButton
zenModeEnabled={zenModeEnabled}
checked={appState.elementLocked}
@ -592,15 +321,26 @@ const LayerUI = ({
/>
<Island
padding={1}
className={clsx({ "zen-mode": zenModeEnabled })}
className={clsx("App-toolbar", {
"zen-mode": zenModeEnabled,
})}
>
<HintViewer appState={appState} elements={elements} />
<HintViewer
appState={appState}
elements={elements}
isMobile={isMobile}
/>
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/>
</Stack.Row>
</Island>
@ -670,7 +410,8 @@ const LayerUI = ({
{!viewModeEnabled && (
<div
className={clsx("undo-redo-buttons zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
})}
>
{actionManager.renderAction("undo", { size: "small" })}
@ -684,7 +425,8 @@ const LayerUI = ({
className={clsx(
"layer-ui__wrapper__footer-center zen-mode-transition",
{
"layer-ui__wrapper__footer-left--transition-bottom": zenModeEnabled,
"layer-ui__wrapper__footer-left--transition-bottom":
zenModeEnabled,
},
)}
>
@ -761,6 +503,8 @@ const LayerUI = ({
renderCustomFooter={renderCustomFooter}
viewModeEnabled={viewModeEnabled}
showThemeBtn={showThemeBtn}
onImageAction={onImageAction}
renderTopRightUI={renderTopRightUI}
/>
</>
) : (
@ -808,6 +552,7 @@ const areEqual = (prev: LayerUIProps, next: LayerUIProps) => {
prev.renderCustomFooter === next.renderCustomFooter &&
prev.langCode === next.langCode &&
prev.elements === next.elements &&
prev.files === next.files &&
keys.every((key) => prevAppState[key] === nextAppState[key])
);
};

View File

@ -16,18 +16,18 @@ const LIBRARY_ICON = (
export const LibraryButton: React.FC<{
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
}> = ({ appState, setAppState }) => {
isMobile?: boolean;
}> = ({ appState, setAppState, isMobile }) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon_type_floating ToolIcon__library zen-mode-visibility",
"ToolIcon ToolIcon_type_floating ToolIcon__library",
`ToolIcon_size_medium`,
{
"zen-mode-visibility--hidden": appState.zenModeEnabled,
"is-mobile": isMobile,
},
)}
title={`${capitalizeString(t("toolBar.library"))}9`}
style={{ marginInlineStart: "var(--space-factor)" }}
title={`${capitalizeString(t("toolBar.library"))}0`}
>
<input
className="ToolIcon_type_checkbox"
@ -38,7 +38,7 @@ export const LibraryButton: React.FC<{
}}
checked={appState.isLibraryOpen}
aria-label={capitalizeString(t("toolBar.library"))}
aria-keyshortcuts="9"
aria-keyshortcuts="0"
/>
<div className="ToolIcon__icon">{LIBRARY_ICON}</div>
</label>

View File

@ -0,0 +1,55 @@
@import "open-color/open-color";
.excalidraw {
.layer-ui__library {
margin: auto;
display: flex;
align-items: center;
justify-content: center;
.layer-ui__library-header {
display: flex;
align-items: center;
width: 100%;
margin: 2px 0;
button {
// 2px from the left to account for focus border of left-most button
margin: 0 2px;
}
a {
margin-inline-start: auto;
// 17px for scrollbar (needed for overlay scrollbars on Big Sur?) + 1px extra
padding-inline-end: 18px;
white-space: nowrap;
}
}
}
.layer-ui__library-message {
padding: 10px 20px;
max-width: 200px;
}
.publish-library-success {
.Dialog__content {
display: flex;
flex-direction: column;
}
&-close.ToolIcon_type_button {
background-color: $oc-blue-6;
align-self: flex-end;
&:hover {
background-color: $oc-blue-8;
}
.ToolIcon__icon {
width: auto;
font-size: 1rem;
color: $oc-white;
padding: 0 0.5rem;
}
}
}
}

View File

@ -0,0 +1,326 @@
import { useRef, useState, useEffect, useCallback, RefObject } from "react";
import Library from "../data/library";
import { t } from "../i18n";
import { randomId } from "../random";
import {
LibraryItems,
LibraryItem,
AppState,
BinaryFiles,
ExcalidrawProps,
} from "../types";
import { Dialog } from "./Dialog";
import { Island } from "./Island";
import PublishLibrary from "./PublishLibrary";
import { ToolButton } from "./ToolButton";
import "./LibraryMenu.scss";
import LibraryMenuItems from "./LibraryMenuItems";
import { EVENT } from "../constants";
import { KEYS } from "../keys";
import { arrayToMap } from "../utils";
const useOnClickOutside = (
ref: RefObject<HTMLElement>,
cb: (event: MouseEvent) => void,
) => {
useEffect(() => {
const listener = (event: MouseEvent) => {
if (!ref.current) {
return;
}
if (
event.target instanceof Element &&
(ref.current.contains(event.target) ||
!document.body.contains(event.target))
) {
return;
}
cb(event);
};
document.addEventListener("pointerdown", listener, false);
return () => {
document.removeEventListener("pointerdown", listener);
};
}, [ref, cb]);
};
const getSelectedItems = (
libraryItems: LibraryItems,
selectedItems: LibraryItem["id"][],
) => libraryItems.filter((item) => selectedItems.includes(item.id));
export const LibraryMenu = ({
onClose,
onInsertShape,
pendingElements,
onAddToLibrary,
theme,
setAppState,
files,
libraryReturnUrl,
focusContainer,
library,
id,
appState,
}: {
pendingElements: LibraryItem["elements"];
onClose: () => void;
onInsertShape: (elements: LibraryItem["elements"]) => void;
onAddToLibrary: () => void;
theme: AppState["theme"];
files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
focusContainer: () => void;
library: Library;
id: string;
appState: AppState;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useOnClickOutside(ref, (event) => {
// If click on the library icon, do nothing.
if ((event.target as Element).closest(".ToolIcon__library")) {
return;
}
onClose();
});
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === KEYS.ESCAPE) {
onClose();
}
};
document.addEventListener(EVENT.KEYDOWN, handleKeyDown);
return () => {
document.removeEventListener(EVENT.KEYDOWN, handleKeyDown);
};
}, [onClose]);
const [libraryItems, setLibraryItems] = useState<LibraryItems>([]);
const [loadingState, setIsLoading] = useState<
"preloading" | "loading" | "ready"
>("preloading");
const [selectedItems, setSelectedItems] = useState<LibraryItem["id"][]>([]);
const [showPublishLibraryDialog, setShowPublishLibraryDialog] =
useState(false);
const [publishLibSuccess, setPublishLibSuccess] = useState<null | {
url: string;
authorName: string;
}>(null);
const loadingTimerRef = useRef<number | null>(null);
useEffect(() => {
Promise.race([
new Promise((resolve) => {
loadingTimerRef.current = window.setTimeout(() => {
resolve("loading");
}, 100);
}),
library.loadLibrary().then((items) => {
setLibraryItems(items);
setIsLoading("ready");
}),
]).then((data) => {
if (data === "loading") {
setIsLoading("loading");
}
});
return () => {
clearTimeout(loadingTimerRef.current!);
};
}, [library]);
const removeFromLibrary = useCallback(async () => {
const items = await library.loadLibrary();
const nextItems = items.filter((item) => !selectedItems.includes(item.id));
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
setSelectedItems([]);
setLibraryItems(nextItems);
}, [library, setAppState, selectedItems, setSelectedItems]);
const resetLibrary = useCallback(() => {
library.resetLibrary();
setLibraryItems([]);
focusContainer();
}, [library, focusContainer]);
const addToLibrary = useCallback(
async (elements: LibraryItem["elements"]) => {
if (elements.some((element) => element.type === "image")) {
return setAppState({
errorMessage: "Support for adding images to the library coming soon!",
});
}
const items = await library.loadLibrary();
const nextItems: LibraryItems = [
{
status: "unpublished",
elements,
id: randomId(),
created: Date.now(),
},
...items,
];
onAddToLibrary();
library.saveLibrary(nextItems).catch((error) => {
setLibraryItems(items);
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
setLibraryItems(nextItems);
},
[onAddToLibrary, library, setAppState],
);
const renderPublishSuccess = useCallback(() => {
return (
<Dialog
onCloseRequest={() => setPublishLibSuccess(null)}
title={t("publishSuccessDialog.title")}
className="publish-library-success"
small={true}
>
<p>
{t("publishSuccessDialog.content", {
authorName: publishLibSuccess!.authorName,
})}{" "}
<a
href={publishLibSuccess?.url}
target="_blank"
rel="noopener noreferrer"
>
{t("publishSuccessDialog.link")}
</a>
</p>
<ToolButton
type="button"
title={t("buttons.close")}
aria-label={t("buttons.close")}
label={t("buttons.close")}
onClick={() => setPublishLibSuccess(null)}
data-testid="publish-library-success-close"
className="publish-library-success-close"
/>
</Dialog>
);
}, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = useCallback(
(data) => {
setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice();
nextLibItems.forEach((libItem) => {
if (selectedItems.includes(libItem.id)) {
libItem.status = "published";
}
});
library.saveLibrary(nextLibItems);
setLibraryItems(nextLibItems);
},
[
setShowPublishLibraryDialog,
setPublishLibSuccess,
libraryItems,
selectedItems,
library,
],
);
const [lastSelectedItem, setLastSelectedItem] = useState<
LibraryItem["id"] | null
>(null);
return loadingState === "preloading" ? null : (
<Island padding={1} ref={ref} className="layer-ui__library">
{showPublishLibraryDialog && (
<PublishLibrary
onClose={() => setShowPublishLibraryDialog(false)}
libraryItems={getSelectedItems(libraryItems, selectedItems)}
appState={appState}
onSuccess={onPublishLibSuccess}
onError={(error) => window.alert(error)}
updateItemsInStorage={() => library.saveLibrary(libraryItems)}
onRemove={(id: string) =>
setSelectedItems(selectedItems.filter((_id) => _id !== id))
}
/>
)}
{publishLibSuccess && renderPublishSuccess()}
{loadingState === "loading" ? (
<div className="layer-ui__library-message">
{t("labels.libraryLoadingMessage")}
</div>
) : (
<LibraryMenuItems
libraryItems={libraryItems}
onRemoveFromLibrary={removeFromLibrary}
onAddToLibrary={addToLibrary}
onInsertShape={onInsertShape}
pendingElements={pendingElements}
setAppState={setAppState}
libraryReturnUrl={libraryReturnUrl}
library={library}
theme={theme}
files={files}
id={id}
selectedItems={selectedItems}
onToggle={(id, event) => {
const shouldSelect = !selectedItems.includes(id);
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = libraryItems.findIndex(
(item) => item.id === lastSelectedItem,
);
const rangeEnd = libraryItems.findIndex(
(item) => item.id === id,
);
if (rangeStart === -1 || rangeEnd === -1) {
setSelectedItems([...selectedItems, id]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = libraryItems.reduce(
(acc: LibraryItem["id"][], item, idx) => {
if (
(idx >= rangeStart && idx <= rangeEnd) ||
selectedItemsMap.has(item.id)
) {
acc.push(item.id);
}
return acc;
},
[],
);
setSelectedItems(nextSelectedIds);
} else {
setSelectedItems([...selectedItems, id]);
}
setLastSelectedItem(id);
} else {
setLastSelectedItem(null);
setSelectedItems(selectedItems.filter((_id) => _id !== id));
}
}}
onPublish={() => setShowPublishLibraryDialog(true)}
resetLibrary={resetLibrary}
/>
)}
</Island>
);
};

View File

@ -0,0 +1,102 @@
@import "open-color/open-color";
.excalidraw {
.library-menu-items-container {
.library-actions {
display: flex;
button .library-actions-counter {
position: absolute;
right: 2px;
bottom: 2px;
border-radius: 50%;
width: 1em;
height: 1em;
padding: 1px;
font-size: 0.7rem;
background: #fff;
}
&--remove {
background-color: $oc-red-7;
&:hover {
background-color: $oc-red-8;
}
&:active {
background-color: $oc-red-9;
}
svg {
color: $oc-white;
}
.library-actions-counter {
color: $oc-red-7;
}
}
&--export {
background-color: $oc-lime-5;
&:hover {
background-color: $oc-lime-7;
}
&:active {
background-color: $oc-lime-8;
}
svg {
color: $oc-white;
}
.library-actions-counter {
color: $oc-lime-5;
}
}
&--publish {
background-color: $oc-cyan-6;
&:hover {
background-color: $oc-cyan-7;
}
&:active {
background-color: $oc-cyan-9;
}
svg {
color: $oc-white;
}
label {
margin-left: -0.2em;
margin-right: 1.1em;
color: $oc-white;
font-size: 0.86em;
}
.library-actions-counter {
color: $oc-cyan-6;
}
}
&--load {
background-color: $oc-blue-6;
&:hover {
background-color: $oc-blue-7;
}
&:active {
background-color: $oc-blue-9;
}
svg {
color: $oc-white;
}
}
}
&__items {
max-height: 50vh;
overflow: auto;
margin-top: 0.5rem;
}
.separator {
font-weight: 500;
font-size: 0.9rem;
margin: 0.6em 0.2em;
color: var(--text-primary-color);
}
}
}

View File

@ -0,0 +1,323 @@
import { chunk } from "lodash";
import { useCallback, useState } from "react";
import { importLibraryFromJSON, saveLibraryAsJSON } from "../data/json";
import Library from "../data/library";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { t } from "../i18n";
import {
AppState,
BinaryFiles,
ExcalidrawProps,
LibraryItem,
LibraryItems,
} from "../types";
import { muteFSAbortError } from "../utils";
import { useIsMobile } from "./App";
import ConfirmDialog from "./ConfirmDialog";
import { exportToFileIcon, load, publishIcon, trash } from "./icons";
import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { Tooltip } from "./Tooltip";
import "./LibraryMenuItems.scss";
import { VERSIONS } from "../constants";
const LibraryMenuItems = ({
libraryItems,
onRemoveFromLibrary,
onAddToLibrary,
onInsertShape,
pendingElements,
theme,
setAppState,
libraryReturnUrl,
library,
files,
id,
selectedItems,
onToggle,
onPublish,
resetLibrary,
}: {
libraryItems: LibraryItems;
pendingElements: LibraryItem["elements"];
onRemoveFromLibrary: () => void;
onInsertShape: (elements: LibraryItem["elements"]) => void;
onAddToLibrary: (elements: LibraryItem["elements"]) => void;
theme: AppState["theme"];
files: BinaryFiles;
setAppState: React.Component<any, AppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library;
id: string;
selectedItems: LibraryItem["id"][];
onToggle: (id: LibraryItem["id"], event: React.MouseEvent) => void;
onPublish: () => void;
resetLibrary: () => void;
}) => {
const renderRemoveLibAlert = useCallback(() => {
const content = selectedItems.length
? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length })
: t("alerts.resetLibrary");
const title = selectedItems.length
? t("confirmDialog.removeItemsFromLib")
: t("confirmDialog.resetLibrary");
return (
<ConfirmDialog
onConfirm={() => {
if (selectedItems.length) {
onRemoveFromLibrary();
} else {
resetLibrary();
}
setShowRemoveLibAlert(false);
}}
onCancel={() => {
setShowRemoveLibAlert(false);
}}
title={title}
>
<p>{content}</p>
</ConfirmDialog>
);
}, [selectedItems, onRemoveFromLibrary, resetLibrary]);
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState(false);
const isMobile = useIsMobile();
const renderLibraryActions = () => {
const itemsSelected = !!selectedItems.length;
const items = itemsSelected
? libraryItems.filter((item) => selectedItems.includes(item.id))
: libraryItems;
const resetLabel = itemsSelected
? t("buttons.remove")
: t("buttons.resetLibrary");
return (
<div className="library-actions">
{(!itemsSelected || !isMobile) && (
<ToolButton
key="import"
type="button"
title={t("buttons.load")}
aria-label={t("buttons.load")}
icon={load}
onClick={() => {
importLibraryFromJSON(library)
.then(() => {
// Close and then open to get the libraries updated
setAppState({ isLibraryOpen: false });
setAppState({ isLibraryOpen: true });
})
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
className="library-actions--load"
/>
)}
{!!items.length && (
<>
<ToolButton
key="export"
type="button"
title={t("buttons.export")}
aria-label={t("buttons.export")}
icon={exportToFileIcon}
onClick={async () => {
const libraryItems = itemsSelected
? items
: await library.loadLibrary();
saveLibraryAsJSON(libraryItems)
.catch(muteFSAbortError)
.catch((error) => {
setAppState({ errorMessage: error.message });
});
}}
className="library-actions--export"
>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
<ToolButton
key="reset"
type="button"
title={resetLabel}
aria-label={resetLabel}
icon={trash}
onClick={() => setShowRemoveLibAlert(true)}
className="library-actions--remove"
>
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
</>
)}
{itemsSelected && !isPublished && (
<Tooltip label={t("hints.publishLibrary")}>
<ToolButton
type="button"
aria-label={t("buttons.publishLibrary")}
label={t("buttons.publishLibrary")}
icon={publishIcon}
className="library-actions--publish"
onClick={onPublish}
>
{!isMobile && <label>{t("buttons.publishLibrary")}</label>}
{selectedItems.length > 0 && (
<span className="library-actions-counter">
{selectedItems.length}
</span>
)}
</ToolButton>
</Tooltip>
)}
</div>
);
};
const CELLS_PER_ROW = isMobile ? 4 : 6;
const referrer =
libraryReturnUrl || window.location.origin + window.location.pathname;
const isPublished = selectedItems.some(
(id) => libraryItems.find((item) => item.id === id)?.status === "published",
);
const createLibraryItemCompo = (params: {
item:
| LibraryItem
| /* pending library item */ {
id: null;
elements: readonly NonDeleted<ExcalidrawElement>[];
}
| null;
onClick?: () => void;
key: string;
}) => {
return (
<Stack.Col key={params.key}>
<LibraryUnit
elements={params.item?.elements}
files={files}
isPending={!params.item?.id && !!params.item?.elements}
onClick={params.onClick || (() => {})}
id={params.item?.id || null}
selected={!!params.item?.id && selectedItems.includes(params.item.id)}
onToggle={(id, event) => {
onToggle(id, event);
}}
/>
</Stack.Col>
);
};
const renderLibrarySection = (
items: (
| LibraryItem
| /* pending library item */ {
id: null;
elements: readonly NonDeleted<ExcalidrawElement>[];
}
)[],
) => {
const _items = items.map((item) => {
if (item.id) {
return createLibraryItemCompo({
item,
onClick: () => onInsertShape(item.elements),
key: item.id,
});
}
return createLibraryItemCompo({
key: "__pending__item__",
item,
onClick: () => onAddToLibrary(pendingElements),
});
});
// ensure we render all empty cells if no items are present
let rows = chunk(_items, CELLS_PER_ROW);
if (!rows.length) {
rows = [[]];
}
return rows.map((rowItems, index, rows) => {
if (index === rows.length - 1) {
// pad row with empty cells
rowItems = rowItems.concat(
new Array(CELLS_PER_ROW - rowItems.length)
.fill(null)
.map((_, index) => {
return createLibraryItemCompo({
key: `empty_${index}`,
item: null,
});
}),
);
}
return (
<Stack.Row align="center" gap={1} key={index}>
{rowItems}
</Stack.Row>
);
});
};
const publishedItems = libraryItems.filter(
(item) => item.status === "published",
);
const unpublishedItems = [
// append pending library item
...(pendingElements.length
? [{ id: null, elements: pendingElements }]
: []),
...libraryItems.filter((item) => item.status !== "published"),
];
return (
<div className="library-menu-items-container">
{showRemoveLibAlert && renderRemoveLibAlert()}
<div className="layer-ui__library-header" key="library-header">
{renderLibraryActions()}
<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>
<Stack.Col
className="library-menu-items-container__items"
align="start"
gap={1}
>
<>
<div className="separator">{t("labels.personalLib")}</div>
{renderLibrarySection(unpublishedItems)}
</>
<>
<div className="separator">{t("labels.excalidrawLib")} </div>
{renderLibrarySection(publishedItems)}
</>
</Stack.Col>
</div>
);
};
export default LibraryMenuItems;

View File

@ -1,3 +1,5 @@
@import "../css/variables.module";
.excalidraw {
.library-unit {
align-items: center;
@ -7,10 +9,26 @@
position: relative;
width: 63px;
height: 63px; // match width
&--hover {
box-shadow: inset 0px 0px 0px 2px $oc-blue-5;
border-color: $oc-blue-5;
}
&--selected {
box-shadow: inset 0px 0px 0px 2px $oc-blue-8;
border-color: $oc-blue-8;
}
}
&.theme--dark .library-unit {
border-color: rgb(48, 48, 48);
}
.library-unit__dragger {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
@ -22,9 +40,9 @@
max-width: 100%;
}
.library-unit__removeFromLibrary,
.library-unit__removeFromLibrary:hover,
.library-unit__removeFromLibrary:active {
.library-unit__checkbox-container,
.library-unit__checkbox-container:hover,
.library-unit__checkbox-container:active {
align-items: center;
background: none;
border: none;
@ -32,10 +50,35 @@
display: flex;
justify-content: center;
margin: 0;
padding: 0;
padding: 0.5rem;
position: absolute;
right: 5px;
top: 5px;
left: 2rem;
bottom: 2rem;
cursor: pointer;
input {
cursor: pointer;
}
}
.library-unit__checkbox {
position: absolute;
left: 2.3rem;
bottom: 2.3rem;
.Checkbox-box {
width: 13px;
height: 13px;
border-radius: 2px;
margin: 0.5em 0.5em 0.2em 0.2em;
background-color: $oc-blue-1;
}
&.Checkbox:hover {
.Checkbox-box {
background-color: $oc-blue-2;
}
}
}
.library-unit__removeFromLibrary > svg {
@ -43,29 +86,37 @@
width: 16px;
}
.library-unit__pulse {
.library-unit__adder {
transform: scale(1);
animation: library-unit__pulse-animation 1s ease-in infinite;
animation: library-unit__adder-animation 1s ease-in infinite;
}
.library-unit__adder {
position: absolute;
left: 50%;
top: 50%;
width: 20px;
height: 20px;
left: 40%;
top: 40%;
width: 2rem;
height: 2rem;
margin-left: -10px;
margin-top: -10px;
pointer-events: none;
}
.library-unit:hover .library-unit__adder {
fill: $oc-blue-7;
}
.library-unit:active .library-unit__adder {
animation: none;
transform: scale(0.8);
fill: $oc-black;
}
.library-unit__active {
cursor: pointer;
}
@keyframes library-unit__pulse-animation {
@keyframes library-unit__adder-animation {
0% {
transform: scale(0.95);
transform: scale(0.85);
}
50% {
@ -73,7 +124,7 @@
}
100% {
transform: scale(0.95);
transform: scale(0.85);
}
}
}

View File

@ -1,87 +1,103 @@
import clsx from "clsx";
import oc from "open-color";
import React, { useEffect, useRef, useState } from "react";
import { close } from "../components/icons";
import { useEffect, useRef, useState } from "react";
import { MIME_TYPES } from "../constants";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { exportToSvg } from "../scene/export";
import { LibraryItem } from "../types";
import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem";
// fa-plus
const PLUS_ICON = (
<svg viewBox="0 0 1792 1792">
<path
fill="currentColor"
d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z"
d="M1600 736v192c0 26.667-9.33 49.333-28 68-18.67 18.67-41.33 28-68 28h-416v416c0 26.67-9.33 49.33-28 68s-41.33 28-68 28H800c-26.667 0-49.333-9.33-68-28s-28-41.33-28-68v-416H288c-26.667 0-49.333-9.33-68-28-18.667-18.667-28-41.333-28-68V736c0-26.667 9.333-49.333 28-68s41.333-28 68-28h416V224c0-26.667 9.333-49.333 28-68s41.333-28 68-28h192c26.67 0 49.33 9.333 68 28s28 41.333 28 68v416h416c26.67 0 49.33 9.333 68 28s28 41.333 28 68Z"
style={{
stroke: "#fff",
strokeWidth: 140,
}}
transform="translate(0 64)"
/>
</svg>
);
export const LibraryUnit = ({
id,
elements,
pendingElements,
onRemoveFromLibrary,
files,
isPending,
onClick,
selected,
onToggle,
}: {
elements?: LibraryItem;
pendingElements?: LibraryItem;
onRemoveFromLibrary: () => void;
id: LibraryItem["id"] | /** for pending item */ null;
elements?: LibraryItem["elements"];
files: BinaryFiles;
isPending?: boolean;
onClick: () => void;
selected: boolean;
onToggle: (id: string, event: React.MouseEvent) => void;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const elementsToRender = elements || pendingElements;
if (!elementsToRender) {
const node = ref.current;
if (!node) {
return;
}
let svg: SVGSVGElement;
const current = ref.current!;
(async () => {
svg = await exportToSvg(elementsToRender, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
for (const child of ref.current!.children) {
if (child.tagName !== "svg") {
continue;
}
current!.removeChild(child);
if (!elements) {
return;
}
current!.appendChild(svg);
const svg = await exportToSvg(
elements,
{
exportBackground: false,
viewBackgroundColor: oc.white,
},
files,
);
node.innerHTML = svg.outerHTML;
})();
return () => {
if (svg) {
current.removeChild(svg);
}
node.innerHTML = "";
};
}, [elements, pendingElements]);
}, [elements, files]);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useIsMobile();
const adder = (isHovered || isMobile) && pendingElements && (
const adder = isPending && (
<div className="library-unit__adder">{PLUS_ICON}</div>
);
return (
<div
className={clsx("library-unit", {
"library-unit__active": elements || pendingElements,
"library-unit__active": elements,
"library-unit--hover": elements && isHovered,
"library-unit--selected": selected,
})}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={clsx("library-unit__dragger", {
"library-unit__pulse": !!pendingElements,
"library-unit__pulse": !!isPending,
})}
ref={ref}
draggable={!!elements}
onClick={!!elements || !!pendingElements ? onClick : undefined}
onClick={
!!elements || !!isPending
? (event) => {
if (id && event.shiftKey) {
onToggle(id, event);
} else {
onClick();
}
}
: undefined
}
onDragStart={(event) => {
setIsHovered(false);
event.dataTransfer.setData(
@ -91,14 +107,12 @@ export const LibraryUnit = ({
}}
/>
{adder}
{elements && (isHovered || isMobile) && (
<button
className="library-unit__removeFromLibrary"
aria-label={t("labels.removeFromLibrary")}
onClick={onRemoveFromLibrary}
>
{close}
</button>
{id && elements && (isHovered || isMobile || selected) && (
<CheckboxItem
checked={selected}
onChange={(checked, event) => onToggle(id, event)}
className="library-unit__checkbox"
/>
)}
</div>
);

View File

@ -1,4 +1,3 @@
import React from "react";
import { t } from "../i18n";
export const LoadingMessage = () => {

View File

@ -10,6 +10,7 @@ type LockIconProps = {
checked: boolean;
onChange?(): void;
zenModeEnabled?: boolean;
isMobile?: boolean;
};
const DEFAULT_SIZE: ToolButtonSize = "medium";
@ -42,10 +43,10 @@ export const LockButton = (props: LockIconProps) => {
return (
<label
className={clsx(
"ToolIcon ToolIcon__lock ToolIcon_type_floating zen-mode-visibility",
"ToolIcon ToolIcon__lock ToolIcon_type_floating",
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"zen-mode-visibility--hidden": props.zenModeEnabled,
"is-mobile": props.isMobile,
},
)}
title={`${props.title} — Q`}

View File

@ -33,6 +33,11 @@ type MobileMenuProps = {
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
viewModeEnabled: boolean;
showThemeBtn: boolean;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
};
export const MobileMenu = ({
@ -50,6 +55,8 @@ export const MobileMenu = ({
renderCustomFooter,
viewModeEnabled,
showThemeBtn,
onImageAction,
renderTopRightUI,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
@ -57,29 +64,40 @@ export const MobileMenu = ({
<Section heading="shapes">
{(heading) => (
<Stack.Col gap={4} align="center">
<Stack.Row gap={1}>
<Island padding={1}>
<Stack.Row gap={1} className="App-toolbar-container">
<Island padding={1} className="App-toolbar">
{heading}
<Stack.Row gap={1}>
<ShapesSwitcher
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/>
</Stack.Row>
</Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<LockButton
checked={appState.elementLocked}
onChange={onLockToggle}
title={t("toolBar.lock")}
isMobile
/>
<LibraryButton
appState={appState}
setAppState={setAppState}
isMobile
/>
<LibraryButton appState={appState} setAppState={setAppState} />
</Stack.Row>
{libraryMenu}
</Stack.Col>
)}
</Section>
<HintViewer appState={appState} elements={elements} />
<HintViewer appState={appState} elements={elements} isMobile={true} />
</FixedSideContainer>
);
};

View File

@ -6,6 +6,7 @@ import clsx from "clsx";
import { KEYS } from "../keys";
import { useExcalidrawContainer, useIsMobile } from "./App";
import { AppState } from "../types";
import { THEME } from "../constants";
export const Modal = (props: {
className?: string;
@ -14,8 +15,9 @@ export const Modal = (props: {
onCloseRequest(): void;
labelledBy: string;
theme?: AppState["theme"];
closeOnClickOutside?: boolean;
}) => {
const { theme = "light" } = props;
const { theme = THEME.LIGHT, closeOnClickOutside = true } = props;
const modalRoot = useBodyRoot(theme);
if (!modalRoot) {
@ -38,7 +40,10 @@ export const Modal = (props: {
onKeyDown={handleKeydown}
aria-labelledby={props.labelledBy}
>
<div className="Modal__background" onClick={props.onCloseRequest}></div>
<div
className="Modal__background"
onClick={closeOnClickOutside ? props.onCloseRequest : undefined}
></div>
<div
className="Modal__content"
style={{ "--max-width": `${props.maxWidth}px` }}

View File

@ -38,10 +38,14 @@ const ChartPreviewBtn = (props: {
const previewNode = previewRef.current!;
(async () => {
svg = await exportToSvg(elements, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
svg = await exportToSvg(
elements,
{
exportBackground: false,
viewBackgroundColor: oc.white,
},
null, // files
);
previewNode.appendChild(svg);
@ -78,7 +82,7 @@ export const PasteChartDialog = ({
appState: AppState;
onClose: () => void;
setAppState: React.Component<any, AppState>["setState"];
onInsertChart: (elements: LibraryItem) => void;
onInsertChart: (elements: LibraryItem["elements"]) => void;
}) => {
const handleClose = React.useCallback(() => {
if (onClose) {

View File

@ -42,6 +42,7 @@ export const ProjectName = (props: Props) => {
</label>
{props.isNameEditable ? (
<input
type="text"
className="TextInput"
onBlur={handleBlur}
onKeyDown={handleKeyDown}

View File

@ -0,0 +1,92 @@
@import "../css/variables.module";
.excalidraw {
.publish-library {
&__fields {
display: flex;
flex-direction: column;
label {
padding: 1em;
display: flex;
justify-content: space-between;
align-items: center;
span {
font-weight: 500;
font-size: 1rem;
color: $oc-gray-6;
}
input,
textarea {
width: 70%;
padding: 0.6em;
font-family: var(--ui-font);
}
.required {
color: $oc-red-8;
margin: 0.2rem;
}
}
}
&__buttons {
display: flex;
padding: 0.2rem 0;
justify-content: flex-end;
.ToolIcon__icon {
min-width: 2.5rem;
width: auto;
font-size: 1rem;
}
.ToolIcon_type_button {
margin-left: 1rem;
padding: 0 0.5rem;
}
&--confirm.ToolIcon_type_button {
background-color: $oc-blue-6;
&:hover {
background-color: $oc-blue-8;
}
}
&--cancel.ToolIcon_type_button {
background-color: $oc-gray-5;
&:hover {
background-color: $oc-gray-6;
}
}
.ToolIcon__icon {
color: $oc-white;
.Spinner {
--spinner-color: #fff;
svg {
padding: 0.5rem;
}
}
}
}
.selected-library-items {
display: flex;
padding: 0 0.8rem;
flex-wrap: wrap;
.single-library-item-wrapper {
width: 9rem;
}
}
&-note {
padding: 1em;
font-style: italic;
font-size: 14px;
display: block;
}
}
}

View File

@ -0,0 +1,455 @@
import { ReactNode, useCallback, useEffect, useState } from "react";
import OpenColor from "open-color";
import { Dialog } from "./Dialog";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
import { AppState, LibraryItems, LibraryItem } from "../types";
import { exportToCanvas } from "../packages/utils";
import {
EXPORT_DATA_TYPES,
EXPORT_SOURCE,
MIME_TYPES,
VERSIONS,
} from "../constants";
import { ExportedLibraryData } from "../data/types";
import "./PublishLibrary.scss";
import SingleLibraryItem from "./SingleLibraryItem";
import { canvasToBlob, resizeImageFile } from "../data/blob";
import { chunk } from "../utils";
interface PublishLibraryDataParams {
authorName: string;
githubHandle: string;
name: string;
description: string;
twitterHandle: string;
website: string;
}
const LOCAL_STORAGE_KEY_PUBLISH_LIBRARY = "publish-library-data";
const savePublishLibDataToStorage = (data: PublishLibraryDataParams) => {
try {
localStorage.setItem(
LOCAL_STORAGE_KEY_PUBLISH_LIBRARY,
JSON.stringify(data),
);
} catch (error: any) {
// Unable to access window.localStorage
console.error(error);
}
};
const importPublishLibDataFromStorage = () => {
try {
const data = localStorage.getItem(LOCAL_STORAGE_KEY_PUBLISH_LIBRARY);
if (data) {
return JSON.parse(data);
}
} catch (error: any) {
// Unable to access localStorage
console.error(error);
}
return null;
};
const generatePreviewImage = async (libraryItems: LibraryItems) => {
const MAX_ITEMS_PER_ROW = 6;
const BOX_SIZE = 128;
const BOX_PADDING = Math.round(BOX_SIZE / 16);
const BORDER_WIDTH = Math.max(Math.round(BOX_SIZE / 64), 2);
const rows = chunk(libraryItems, MAX_ITEMS_PER_ROW);
const canvas = document.createElement("canvas");
canvas.width =
rows[0].length * BOX_SIZE +
(rows[0].length + 1) * (BOX_PADDING * 2) -
BOX_PADDING * 2;
canvas.height =
rows.length * BOX_SIZE +
(rows.length + 1) * (BOX_PADDING * 2) -
BOX_PADDING * 2;
const ctx = canvas.getContext("2d")!;
ctx.fillStyle = OpenColor.white;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw items
// ---------------------------------------------------------------------------
for (const [index, item] of libraryItems.entries()) {
const itemCanvas = await exportToCanvas({
elements: item.elements,
files: null,
maxWidthOrHeight: BOX_SIZE,
});
const { width, height } = itemCanvas;
// draw item
// -------------------------------------------------------------------------
const rowOffset =
Math.floor(index / MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
const colOffset =
(index % MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
ctx.drawImage(
itemCanvas,
colOffset + (BOX_SIZE - width) / 2 + BOX_PADDING,
rowOffset + (BOX_SIZE - height) / 2 + BOX_PADDING,
);
// draw item border
// -------------------------------------------------------------------------
ctx.lineWidth = BORDER_WIDTH;
ctx.strokeStyle = OpenColor.gray[4];
ctx.strokeRect(
colOffset + BOX_PADDING / 2,
rowOffset + BOX_PADDING / 2,
BOX_SIZE + BOX_PADDING,
BOX_SIZE + BOX_PADDING,
);
}
return await resizeImageFile(
new File([await canvasToBlob(canvas)], "preview", { type: MIME_TYPES.png }),
{
outputType: MIME_TYPES.jpg,
maxWidthOrHeight: 5000,
},
);
};
const PublishLibrary = ({
onClose,
libraryItems,
appState,
onSuccess,
onError,
updateItemsInStorage,
onRemove,
}: {
onClose: () => void;
libraryItems: LibraryItems;
appState: AppState;
onSuccess: (data: {
url: string;
authorName: string;
items: LibraryItems;
}) => void;
onError: (error: Error) => void;
updateItemsInStorage: (items: LibraryItems) => void;
onRemove: (id: string) => void;
}) => {
const [libraryData, setLibraryData] = useState<PublishLibraryDataParams>({
authorName: "",
githubHandle: "",
name: "",
description: "",
twitterHandle: "",
website: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const data = importPublishLibDataFromStorage();
if (data) {
setLibraryData(data);
}
}, []);
const [clonedLibItems, setClonedLibItems] = useState<LibraryItems>(
libraryItems.slice(),
);
useEffect(() => {
setClonedLibItems(libraryItems.slice());
}, [libraryItems]);
const onInputChange = (event: any) => {
setLibraryData({
...libraryData,
[event.target.name]: event.target.value,
});
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
const erroredLibItems: LibraryItem[] = [];
let isError = false;
clonedLibItems.forEach((libItem) => {
let error = "";
if (!libItem.name) {
error = t("publishDialog.errors.required");
isError = true;
}
erroredLibItems.push({ ...libItem, error });
});
if (isError) {
setClonedLibItems(erroredLibItems);
setIsSubmitting(false);
return;
}
const previewImage = await generatePreviewImage(clonedLibItems);
const libContent: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary,
source: EXPORT_SOURCE,
libraryItems: clonedLibItems,
};
const content = JSON.stringify(libContent, null, 2);
const lib = new Blob([content], { type: "application/json" });
const formData = new FormData();
formData.append("excalidrawLib", lib);
formData.append("previewImage", previewImage);
formData.append("previewImageType", previewImage.type);
formData.append("title", libraryData.name);
formData.append("authorName", libraryData.authorName);
formData.append("githubHandle", libraryData.githubHandle);
formData.append("name", libraryData.name);
formData.append("description", libraryData.description);
formData.append("twitterHandle", libraryData.twitterHandle);
formData.append("website", libraryData.website);
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
method: "post",
body: formData,
})
.then(
(response) => {
if (response.ok) {
return response.json().then(({ url }) => {
// flush data from local storage
localStorage.removeItem(LOCAL_STORAGE_KEY_PUBLISH_LIBRARY);
onSuccess({
url,
authorName: libraryData.authorName,
items: clonedLibItems,
});
});
}
return response
.json()
.catch(() => {
throw new Error(response.statusText || "something went wrong");
})
.then((error) => {
throw new Error(
error.message || response.statusText || "something went wrong",
);
});
},
(err) => {
console.error(err);
onError(err);
setIsSubmitting(false);
},
)
.catch((err) => {
console.error(err);
onError(err);
setIsSubmitting(false);
});
};
const renderLibraryItems = () => {
const items: ReactNode[] = [];
clonedLibItems.forEach((libItem, index) => {
items.push(
<div className="single-library-item-wrapper" key={index}>
<SingleLibraryItem
libItem={libItem}
appState={appState}
index={index}
onChange={(val, index) => {
const items = clonedLibItems.slice();
items[index].name = val;
setClonedLibItems(items);
}}
onRemove={onRemove}
/>
</div>,
);
});
return <div className="selected-library-items">{items}</div>;
};
const onDialogClose = useCallback(() => {
updateItemsInStorage(clonedLibItems);
savePublishLibDataToStorage(libraryData);
onClose();
}, [clonedLibItems, onClose, updateItemsInStorage, libraryData]);
const shouldRenderForm = !!libraryItems.length;
return (
<Dialog
onCloseRequest={onDialogClose}
title={t("publishDialog.title")}
className="publish-library"
>
{shouldRenderForm ? (
<form onSubmit={onSubmit}>
<div className="publish-library-note">
{t("publishDialog.noteDescription.pre")}
<a
href="https://libraries.excalidraw.com"
target="_blank"
rel="noopener noreferrer"
>
{t("publishDialog.noteDescription.link")}
</a>{" "}
{t("publishDialog.noteDescription.post")}
</div>
<span className="publish-library-note">
{t("publishDialog.noteGuidelines.pre")}
<a
href="https://github.com/excalidraw/excalidraw-libraries#guidelines"
target="_blank"
rel="noopener noreferrer"
>
{t("publishDialog.noteGuidelines.link")}
</a>
{t("publishDialog.noteGuidelines.post")}
</span>
<div className="publish-library-note">
{t("publishDialog.noteItems")}
</div>
{renderLibraryItems()}
<div className="publish-library__fields">
<label>
<div>
<span>{t("publishDialog.libraryName")}</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
name="name"
required
value={libraryData.name}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.libraryName")}
/>
</label>
<label style={{ alignItems: "flex-start" }}>
<div>
<span>{t("publishDialog.libraryDesc")}</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<textarea
name="description"
rows={4}
required
value={libraryData.description}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.libraryDesc")}
/>
</label>
<label>
<div>
<span>{t("publishDialog.authorName")}</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
name="authorName"
required
value={libraryData.authorName}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.authorName")}
/>
</label>
<label>
<span>{t("publishDialog.githubUsername")}</span>
<input
type="text"
name="githubHandle"
value={libraryData.githubHandle}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.githubHandle")}
/>
</label>
<label>
<span>{t("publishDialog.twitterUsername")}</span>
<input
type="text"
name="twitterHandle"
value={libraryData.twitterHandle}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.twitterHandle")}
/>
</label>
<label>
<span>{t("publishDialog.website")}</span>
<input
type="text"
name="website"
pattern="https?://.+"
title={t("publishDialog.errors.website")}
value={libraryData.website}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.website")}
/>
</label>
<span className="publish-library-note">
{t("publishDialog.noteLicense.pre")}
<a
href="https://github.com/excalidraw/excalidraw-libraries/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>
{t("publishDialog.noteLicense.link")}
</a>
{t("publishDialog.noteLicense.post")}
</span>
</div>
<div className="publish-library__buttons">
<ToolButton
type="button"
title={t("buttons.cancel")}
aria-label={t("buttons.cancel")}
label={t("buttons.cancel")}
onClick={onDialogClose}
data-testid="cancel-clear-canvas-button"
className="publish-library__buttons--cancel"
/>
<ToolButton
type="submit"
title={t("buttons.submit")}
aria-label={t("buttons.submit")}
label={t("buttons.submit")}
className="publish-library__buttons--confirm"
isLoading={isSubmitting}
/>
</div>
</form>
) : (
<p style={{ padding: "1em", textAlign: "center", fontWeight: 500 }}>
{t("publishDialog.atleastOneLibItem")}
</p>
)}
</Dialog>
);
};
export default PublishLibrary;

View File

@ -0,0 +1,66 @@
@import "../css/variables.module";
.excalidraw {
.single-library-item {
position: relative;
&__svg {
width: 7.5rem;
height: 7.5rem;
border: 1px solid var(--button-gray-2);
margin: 0.3rem;
svg {
width: 100%;
height: 100%;
}
}
.ToolIcon__icon {
background-color: $oc-white;
width: auto;
height: auto;
margin: 0 0.5rem;
}
.ToolIcon,
.ToolIcon_type_button:hover {
background-color: white;
}
.required,
.error {
color: $oc-red-8;
font-weight: bold;
font-size: 1rem;
margin: 0.2rem;
}
.error {
font-weight: 500;
margin: 0;
padding: 0.3em 0;
}
&--remove {
position: absolute;
top: 0.2rem;
right: 1.3rem;
.ToolIcon__icon {
margin: 0;
}
.ToolIcon__icon {
background-color: $oc-red-6;
&:hover {
background-color: $oc-red-7;
}
&:active {
background-color: $oc-red-8;
}
}
svg {
color: $oc-white;
padding: 0.26rem;
border-radius: 0.3em;
width: 1rem;
height: 1rem;
}
}
}
}

View File

@ -0,0 +1,99 @@
import oc from "open-color";
import { useEffect, useRef } from "react";
import { t } from "../i18n";
import { exportToSvg } from "../packages/utils";
import { AppState, LibraryItem } from "../types";
import { close } from "./icons";
import "./SingleLibraryItem.scss";
import { ToolButton } from "./ToolButton";
const SingleLibraryItem = ({
libItem,
appState,
index,
onChange,
onRemove,
}: {
libItem: LibraryItem;
appState: AppState;
index: number;
onChange: (val: string, index: number) => void;
onRemove: (id: string) => void;
}) => {
const svgRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
const node = svgRef.current;
if (!node) {
return;
}
(async () => {
const svg = await exportToSvg({
elements: libItem.elements,
appState: {
...appState,
viewBackgroundColor: oc.white,
exportBackground: true,
},
files: null,
});
node.innerHTML = svg.outerHTML;
})();
}, [libItem.elements, appState]);
return (
<div className="single-library-item">
<div ref={svgRef} className="single-library-item__svg" />
<ToolButton
aria-label={t("buttons.remove")}
type="button"
icon={close}
className="single-library-item--remove"
onClick={onRemove.bind(null, libItem.id)}
title={t("buttons.remove")}
/>
<div
style={{
display: "flex",
margin: "0.8rem 0.3rem",
width: "100%",
fontSize: "14px",
fontWeight: 500,
flexDirection: "column",
}}
>
<label
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
}}
>
<div style={{ padding: "0.5em 0" }}>
<span style={{ fontWeight: 500, color: oc.gray[6] }}>
{t("publishDialog.itemName")}
</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
ref={inputRef}
style={{ width: "80%", padding: "0.2rem" }}
defaultValue={libItem.name}
placeholder="Item name"
onChange={(event) => {
onChange(event.target.value, index);
}}
/>
</label>
<span className="error">{libItem.error}</span>
</div>
</div>
);
};
export default SingleLibraryItem;

View File

@ -0,0 +1,48 @@
@import "open-color/open-color.scss";
$duration: 1.6s;
.excalidraw {
.Spinner {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
margin-left: auto;
margin-right: auto;
--spinner-color: var(--icon-fill-color);
svg {
animation: rotate $duration linear infinite;
transform-origin: center center;
}
circle {
stroke: var(--spinner-color);
animation: dash $duration linear 0s infinite;
stroke-linecap: round;
}
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 300;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 150, 300;
stroke-dashoffset: -200;
}
100% {
stroke-dasharray: 1, 300;
stroke-dashoffset: -280;
}
}
}

View File

@ -0,0 +1,28 @@
import React from "react";
import "./Spinner.scss";
const Spinner = ({
size = "1em",
circleWidth = 8,
}: {
size?: string | number;
circleWidth?: number;
}) => {
return (
<div className="Spinner">
<svg viewBox="0 0 100 100" style={{ width: size, height: size }}>
<circle
cx="50"
cy="50"
r={50 - circleWidth / 2}
strokeWidth={circleWidth}
fill="none"
strokeMiterlimit="10"
/>
</svg>
</div>
);
};
export default Spinner;

View File

@ -2,24 +2,6 @@
.excalidraw {
.TextInput {
color: var(--text-primary-color);
display: inline-block;
border: 1.5px solid var(--button-gray-1);
line-height: 1;
padding: 0.75rem;
white-space: nowrap;
border-radius: var(--space-factor);
background-color: var(--input-bg-color);
&:not(:focus) {
&:hover {
background-color: var(--input-hover-bg-color);
}
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
}
}

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef } from "react";
import { useCallback, useEffect, useRef } from "react";
import { TOAST_TIMEOUT } from "../constants";
import "./Toast.scss";

View File

@ -1,8 +1,11 @@
import "./ToolIcon.scss";
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import clsx from "clsx";
import { useExcalidrawContainer } from "./App";
import { AbortError } from "../errors";
import Spinner from "./Spinner";
import { PointerType } from "../element/types";
export type ToolButtonSize = "small" | "medium";
@ -22,13 +25,19 @@ type ToolButtonBaseProps = {
visible?: boolean;
selected?: boolean;
className?: string;
isLoading?: boolean;
};
type ToolButtonProps =
| (ToolButtonBaseProps & {
type: "button";
children?: React.ReactNode;
onClick?(): void;
onClick?(event: React.MouseEvent): void;
})
| (ToolButtonBaseProps & {
type: "submit";
children?: React.ReactNode;
onClick?(event: React.MouseEvent): void;
})
| (ToolButtonBaseProps & {
type: "icon";
@ -38,7 +47,7 @@ type ToolButtonProps =
| (ToolButtonBaseProps & {
type: "radio";
checked: boolean;
onChange?(): void;
onChange?(data: { pointerType: PointerType | null }): void;
});
export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
@ -47,7 +56,48 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
React.useImperativeHandle(ref, () => innerRef.current);
const sizeCn = `ToolIcon_size_${props.size}`;
if (props.type === "button" || props.type === "icon") {
const [isLoading, setIsLoading] = useState(false);
const isMountedRef = useRef(true);
const onClick = async (event: React.MouseEvent) => {
const ret = "onClick" in props && props.onClick?.(event);
if (ret && "then" in ret) {
try {
setIsLoading(true);
await ret;
} catch (error: any) {
if (!(error instanceof AbortError)) {
throw error;
} else {
console.warn(error);
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
}
};
useEffect(
() => () => {
isMountedRef.current = false;
},
[],
);
const lastPointerTypeRef = useRef<PointerType | null>(null);
if (
props.type === "button" ||
props.type === "icon" ||
props.type === "submit"
) {
const type = (props.type === "icon" ? "button" : props.type) as
| "button"
| "submit";
return (
<button
className={clsx(
@ -67,9 +117,10 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
hidden={props.hidden}
title={props.title}
aria-label={props["aria-label"]}
type="button"
onClick={props.onClick}
type={type}
onClick={onClick}
ref={innerRef}
disabled={isLoading || props.isLoading}
>
{(props.icon || props.label) && (
<div className="ToolIcon__icon" aria-hidden="true">
@ -79,10 +130,13 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
{props.keyBindingLabel}
</span>
)}
{props.isLoading && <Spinner />}
</div>
)}
{props.showAriaLabel && (
<div className="ToolIcon__label">{props["aria-label"]}</div>
<div className="ToolIcon__label">
{props["aria-label"]} {isLoading && <Spinner />}
</div>
)}
{props.children}
</button>
@ -90,7 +144,18 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
}
return (
<label className={clsx("ToolIcon", props.className)} title={props.title}>
<label
className={clsx("ToolIcon", props.className)}
title={props.title}
onPointerDown={(event) => {
lastPointerTypeRef.current = event.pointerType || null;
}}
onPointerUp={() => {
requestAnimationFrame(() => {
lastPointerTypeRef.current = null;
});
}}
>
<input
className={`ToolIcon_type_radio ${sizeCn}`}
type="radio"
@ -99,7 +164,9 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
aria-keyshortcuts={props["aria-keyshortcuts"]}
data-testid={props["data-testid"]}
id={`${excalId}-${props.id}`}
onChange={props.onChange}
onChange={() => {
props.onChange?.({ pointerType: lastPointerTypeRef.current });
}}
checked={props.checked}
ref={innerRef}
/>

View File

@ -6,20 +6,9 @@
display: inline-flex;
align-items: center;
position: relative;
font-family: Cascadia;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor);
user-select: none;
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
}
.ToolIcon--plain {
@ -30,6 +19,20 @@
}
}
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon {
background-color: var(--button-gray-1);
&:hover {
background-color: var(--button-gray-2);
}
&:active {
background-color: var(--button-gray-3);
}
}
}
.ToolIcon__icon {
width: 2.5rem;
height: 2.5rem;
@ -39,7 +42,11 @@
justify-content: center;
align-items: center;
border-radius: var(--space-factor);
border-radius: var(--border-radius-lg);
& + .ToolIcon__label {
margin-inline-start: 0;
}
svg {
position: relative;
@ -47,17 +54,19 @@
fill: var(--icon-fill-color);
color: var(--icon-fill-color);
}
& + .ToolIcon__label {
margin-inline-start: 0;
}
}
.ToolIcon__label {
display: flex;
align-items: center;
color: var(--icon-fill-color);
font-family: var(--ui-font);
margin: 0 0.8em;
text-overflow: ellipsis;
.Spinner {
margin-left: 0.6em;
}
}
.ToolIcon_size_small .ToolIcon__icon {
@ -74,7 +83,7 @@
margin: 0;
font-size: inherit;
&:focus {
&:focus-visible {
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
@ -116,7 +125,7 @@
}
}
&:focus + .ToolIcon__icon {
&:focus-visible + .ToolIcon__icon {
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
@ -136,10 +145,6 @@
background-color: transparent;
}
&:focus {
box-shadow: none;
}
.ToolIcon__icon {
background-color: var(--button-gray-1);
&:hover {
@ -154,13 +159,6 @@
}
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__keybinding {
position: absolute;
bottom: 2px;

112
src/components/Toolbar.scss Normal file
View File

@ -0,0 +1,112 @@
@import "open-color/open-color.scss";
@mixin toolbarButtonColorStates {
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
& + .ToolIcon__icon:active {
background: var(--color-primary-light);
}
&:checked + .ToolIcon__icon {
background: var(--color-primary);
--icon-fill-color: #{$oc-white};
--keybinding-color: #{$oc-white};
}
&:checked + .ToolIcon__icon:active {
background: var(--color-primary-darker);
}
}
.ToolIcon__keybinding {
bottom: 4px;
right: 4px;
}
}
.excalidraw {
.App-toolbar-container {
.ToolIcon_type_floating {
@include toolbarButtonColorStates;
&:not(.is-mobile) {
.ToolIcon__icon {
padding: 1px;
background-color: var(--island-bg-color);
box-shadow: 1px 3px 4px 0px rgb(0 0 0 / 15%);
border-radius: 50%;
transition: box-shadow 0.5s ease, transform 0.5s ease;
}
}
.ToolIcon_type_radio,
.ToolIcon_type_checkbox {
&:focus-within + .ToolIcon__icon {
// override for custom floating button shadow
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
}
}
.ToolIcon.ToolIcon__lock {
margin-inline-end: var(--space-factor);
&.ToolIcon_type_floating {
margin-left: 0.1rem;
}
}
.ToolIcon__library {
margin-inline-start: var(--space-factor);
}
&.zen-mode {
.ToolIcon_type_floating {
.ToolIcon__icon {
box-shadow: none;
transform: scale(0.9);
}
.ToolIcon_type_checkbox:not(:checked):not(:hover):not(:active) {
& + .ToolIcon__icon {
svg {
fill: $oc-gray-5;
color: $oc-gray-5;
}
}
}
}
}
}
.App-toolbar {
border-radius: var(--border-radius-lg);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 15%);
.ToolIcon {
&:hover {
--icon-fill-color: var(--color-primary-chubb);
--keybinding-color: var(--color-primary-chubb);
}
&:active {
--icon-fill-color: #{$oc-gray-9};
--keybinding-color: #{$oc-gray-9};
}
.ToolIcon__icon {
background: transparent;
border-radius: var(--border-radius-lg);
}
@include toolbarButtonColorStates;
}
&.zen-mode {
.ToolIcon__keybinding,
.HintViewer {
display: none;
}
}
}
&.theme--dark .App-toolbar .ToolIcon:active {
--icon-fill-color: #{$oc-gray-3};
--keybinding-color: #{$oc-gray-3};
}
}

View File

@ -29,7 +29,6 @@
// wraps the element we want to apply the tooltip to
.excalidraw-tooltip-wrapper {
display: flex;
height: 100%;
}
.excalidraw-tooltip-icon {

View File

@ -34,10 +34,8 @@ const updateTooltip = (
width: itemWidth,
} = item.getBoundingClientRect();
const {
width: labelWidth,
height: labelHeight,
} = tooltip.getBoundingClientRect();
const { width: labelWidth, height: labelHeight } =
tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
@ -64,9 +62,15 @@ type TooltipProps = {
children: React.ReactNode;
label: string;
long?: boolean;
style?: React.CSSProperties;
};
export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
export const Tooltip = ({
children,
label,
long = false,
style,
}: TooltipProps) => {
useEffect(() => {
return () =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
@ -86,6 +90,7 @@ export const Tooltip = ({ children, label, long = false }: TooltipProps) => {
onPointerLeave={() =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible")
}
style={style}
>
{children}
</div>

View File

@ -27,7 +27,7 @@ export class TopErrorBoundary extends React.Component<
for (const [key, value] of Object.entries({ ...localStorage })) {
try {
_localStorage[key] = JSON.parse(value);
} catch (error) {
} catch (error: any) {
_localStorage[key] = value;
}
}
@ -60,7 +60,7 @@ export class TopErrorBoundary extends React.Component<
)
).default;
body = encodeURIComponent(templateStrFn(this.state.sentryEventId));
} catch (error) {
} catch (error: any) {
console.error(error);
}
@ -86,7 +86,7 @@ export class TopErrorBoundary extends React.Component<
try {
localStorage.clear();
window.location.reload();
} catch (error) {
} catch (error: any) {
console.error(error);
}
}}

View File

@ -7,6 +7,10 @@
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
&:empty {
display: none;
}
}
.UserList > * {

File diff suppressed because it is too large Load Diff

View File

@ -70,6 +70,11 @@ export const FONT_FAMILY = {
Cascadia: 3,
};
export const THEME = {
LIGHT: "light",
DARK: "dark",
};
export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const DEFAULT_FONT_SIZE = 20;
@ -85,6 +90,12 @@ export const GRID_SIZE = 20; // TODO make it configurable?
export const MIME_TYPES = {
excalidraw: "application/vnd.excalidraw+json",
excalidrawlib: "application/vnd.excalidrawlib+json",
json: "application/json",
svg: "image/svg+xml",
png: "image/png",
jpg: "image/jpeg",
gif: "image/gif",
binary: "application/octet-stream",
} as const;
export const EXPORT_DATA_TYPES = {
@ -95,11 +106,8 @@ export const EXPORT_DATA_TYPES = {
export const EXPORT_SOURCE = window.location.origin;
export const STORAGE_KEYS = {
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
} as const;
// time in milliseconds
export const IMAGE_RENDER_TIMEOUT = 500;
export const TAP_TWICE_TIMEOUT = 300;
export const TOUCH_CTX_MENU_TIMEOUT = 500;
export const TITLE_TIMEOUT = 10000;
@ -149,3 +157,25 @@ export const MAX_DECIMALS_FOR_SVG_EXPORT = 2;
export const EXPORT_SCALES = [1, 2, 3];
export const DEFAULT_EXPORT_PADDING = 10; // px
export const DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT = 1440;
export const ALLOWED_IMAGE_MIME_TYPES = [
MIME_TYPES.png,
MIME_TYPES.jpg,
MIME_TYPES.svg,
MIME_TYPES.gif,
] as const;
export const MAX_ALLOWED_FILE_BYTES = 2 * 1024 * 1024;
export const SVG_NS = "http://www.w3.org/2000/svg";
export const ENCRYPTION_KEY_BITS = 128;
export const VERSIONS = {
excalidraw: 2,
excalidrawLibrary: 2,
} as const;
export const BOUND_TEXT_PADDING = 5;

View File

@ -180,7 +180,7 @@
}
.buttonList label:focus-within,
input:focus {
input:focus-visible {
outline: transparent;
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
@ -190,14 +190,14 @@
user-select: none;
background-color: var(--button-gray-1);
border: 0;
border-radius: 4px;
border-radius: var(--border-radius-md);
margin: 0.125rem 0;
padding: 0.25rem;
white-space: nowrap;
cursor: pointer;
&:focus {
&:focus-visible {
outline: transparent;
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
@ -217,14 +217,16 @@
.active,
.buttonList label.active {
background-color: var(--button-gray-2);
background-color: var(--color-primary);
--icon-fill-color: #{$oc-white};
&:hover {
background-color: var(--button-gray-2);
background-color: var(--color-primary-darker);
}
&:active {
background-color: var(--button-gray-3);
background-color: var(--color-primary-darkest);
}
}
@ -234,7 +236,7 @@
justify-content: center;
align-items: center;
svg {
width: 36px;
width: 35px;
height: 14px;
padding: 2px;
opacity: 0.6;
@ -311,7 +313,7 @@
}
.App-menu_top {
grid-template-columns: 1fr auto 1fr;
grid-template-columns: auto max-content auto;
grid-gap: 4px;
align-items: flex-start;
cursor: default;
@ -517,6 +519,27 @@
}
}
input[type="text"],
textarea:not(.excalidraw-wysiwyg) {
color: var(--text-primary-color);
border: 1.5px solid var(--input-border-color);
padding: 0.75rem;
white-space: nowrap;
border-radius: var(--space-factor);
background-color: var(--input-bg-color);
&:not(:focus) {
&:hover {
background-color: var(--input-hover-bg-color);
}
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
}
@media print {
.App-bottom-bar,
.FixedSideContainer,

View File

@ -12,11 +12,11 @@
--dialog-border-color: #{$oc-gray-6};
--dropdown-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="292.4" height="292.4" viewBox="0 0 292 292"><path d="M287 197L159 69c-4-3-8-5-13-5s-9 2-13 5L5 197c-3 4-5 8-5 13s2 9 5 13c4 4 8 5 13 5h256c5 0 9-1 13-5s5-8 5-13-1-9-5-13z"/></svg>');
--focus-highlight-color: #{$oc-blue-2};
--icon-fill-color: #{$oc-black};
--icon-fill-color: #{$oc-gray-9};
--icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white};
--input-border-color: #{$oc-gray-3};
--input-border-color: #{$oc-gray-4};
--input-hover-bg-color: #{$oc-gray-1};
--input-label-color: #{$oc-gray-7};
--island-bg-color: rgba(255, 255, 255, 0.96);
@ -32,10 +32,20 @@
--sar: env(safe-area-inset-right);
--sat: env(safe-area-inset-top);
--select-highlight-color: #{$oc-blue-5};
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.85)};
--shadow-island: 0 0 0 1px rgba(0, 0, 0, 0.01), 1px 1px 5px rgb(0 0 0 / 12%);
--space-factor: 0.25rem;
--text-primary-color: #{$oc-gray-8};
--color-primary: #6965db;
--color-primary-chubb: #625ee0; // to offset Chubb illusion
--color-primary-darker: #5b57d1;
--color-primary-darkest: #4a47b1;
--color-primary-light: #e2e1fc;
--border-radius-md: 0.375rem;
--border-radius-lg: 0.5rem;
&.theme--dark {
background: $oc-black;
@ -64,13 +74,20 @@
--input-label-color: #{$oc-gray-2};
--island-bg-color: rgba(30, 30, 30, 0.98);
--keybinding-color: #{$oc-gray-6};
--link-color: #{$oc-blue-4};
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
--popup-bg-color: #2c2c2c;
--popup-secondary-bg-color: #222;
--popup-text-color: #{$oc-gray-4};
--popup-text-inverted-color: #2c2c2c;
--select-highlight-color: #{$oc-blue-4};
--shadow-island: 0 1px 5px #{transparentize($oc-black, 0.7)};
--shadow-island: 1px 1px 5px #{transparentize($oc-black, 0.7)};
--text-primary-color: #{$oc-gray-4};
--color-primary: #5650f0;
--color-primary-chubb: #726dff; // to offset Chubb illusion
--color-primary-darker: #4b46d8;
--color-primary-darkest: #3e39be;
--color-primary-light: #3f3d64;
}
}

View File

@ -1,11 +1,17 @@
import { nanoid } from "nanoid";
import { cleanAppStateForExport } from "../appState";
import { EXPORT_DATA_TYPES } from "../constants";
import {
ALLOWED_IMAGE_MIME_TYPES,
EXPORT_DATA_TYPES,
MIME_TYPES,
} from "../constants";
import { clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types";
import { ExcalidrawElement, FileId } from "../element/types";
import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState } from "../types";
import { AppState, DataURL } from "../types";
import { bytesToHexString } from "../utils";
import { FileSystemHandle } from "./filesystem";
import { isValidExcalidrawData } from "./json";
import { restore } from "./restore";
@ -14,16 +20,22 @@ import { ImportedLibraryData } from "./types";
const parseFileContents = async (blob: Blob | File) => {
let contents: string;
if (blob.type === "image/png") {
if (blob.type === MIME_TYPES.png) {
try {
return await (
await import(/* webpackChunkName: "image" */ "./image")
).decodePngMetadata(blob);
} catch (error) {
} catch (error: any) {
if (error.message === "INVALID") {
throw new Error(t("alerts.imageDoesNotContainScene"));
throw new DOMException(
t("alerts.imageDoesNotContainScene"),
"EncodingError",
);
} else {
throw new Error(t("alerts.cannotRestoreFromImage"));
throw new DOMException(
t("alerts.cannotRestoreFromImage"),
"EncodingError",
);
}
}
} else {
@ -40,18 +52,24 @@ const parseFileContents = async (blob: Blob | File) => {
};
});
}
if (blob.type === "image/svg+xml") {
if (blob.type === MIME_TYPES.svg) {
try {
return await (
await import(/* webpackChunkName: "image" */ "./image")
).decodeSvgMetadata({
svg: contents,
});
} catch (error) {
} catch (error: any) {
if (error.message === "INVALID") {
throw new Error(t("alerts.imageDoesNotContainScene"));
throw new DOMException(
t("alerts.imageDoesNotContainScene"),
"EncodingError",
);
} else {
throw new Error(t("alerts.cannotRestoreFromImage"));
throw new DOMException(
t("alerts.cannotRestoreFromImage"),
"EncodingError",
);
}
}
}
@ -70,13 +88,13 @@ export const getMimeType = (blob: Blob | string): string => {
name = blob.name || "";
}
if (/\.(excalidraw|json)$/.test(name)) {
return "application/json";
return MIME_TYPES.json;
} else if (/\.png$/.test(name)) {
return "image/png";
return MIME_TYPES.png;
} else if (/\.jpe?g$/.test(name)) {
return "image/jpeg";
return MIME_TYPES.jpg;
} else if (/\.svg$/.test(name)) {
return "image/svg+xml";
return MIME_TYPES.svg;
}
return "";
};
@ -100,6 +118,15 @@ export const isImageFileHandle = (handle: FileSystemHandle | null) => {
return type === "png" || type === "svg";
};
export const isSupportedImageFile = (
blob: Blob | null | undefined,
): blob is Blob & { type: typeof ALLOWED_IMAGE_MIME_TYPES[number] } => {
const { type } = blob || {};
return (
!!type && (ALLOWED_IMAGE_MIME_TYPES as readonly string[]).includes(type)
);
};
export const loadFromBlob = async (
blob: Blob,
/** @see restore.localAppState */
@ -123,13 +150,14 @@ export const loadFromBlob = async (
? calculateScrollCenter(data.elements || [], localAppState, null)
: {}),
},
files: data.files,
},
localAppState,
localElements,
);
return result;
} catch (error) {
} catch (error: any) {
console.error(error.message);
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
@ -160,8 +188,104 @@ export const canvasToBlob = async (
}
resolve(blob);
});
} catch (error) {
} catch (error: any) {
reject(error);
}
});
};
/** generates SHA-1 digest from supplied file (if not supported, falls back
to a 40-char base64 random id) */
export const generateIdFromFile = async (file: File): Promise<FileId> => {
try {
const hashBuffer = await window.crypto.subtle.digest(
"SHA-1",
await file.arrayBuffer(),
);
return bytesToHexString(new Uint8Array(hashBuffer)) as FileId;
} catch (error: any) {
console.error(error);
// length 40 to align with the HEX length of SHA-1 (which is 160 bit)
return nanoid(40) as FileId;
}
};
export const getDataURL = async (file: Blob | File): Promise<DataURL> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const dataURL = reader.result as DataURL;
resolve(dataURL);
};
reader.onerror = (error) => reject(error);
reader.readAsDataURL(file);
});
};
export const dataURLToFile = (dataURL: DataURL, filename = "") => {
const dataIndexStart = dataURL.indexOf(",");
const byteString = atob(dataURL.slice(dataIndexStart + 1));
const mimeType = dataURL.slice(0, dataIndexStart).split(":")[1].split(";")[0];
const ab = new ArrayBuffer(byteString.length);
const ia = new Uint8Array(ab);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new File([ab], filename, { type: mimeType });
};
export const resizeImageFile = async (
file: File,
opts: {
/** undefined indicates auto */
outputType?: typeof MIME_TYPES["jpg"];
maxWidthOrHeight: number;
},
): Promise<File> => {
// SVG files shouldn't a can't be resized
if (file.type === MIME_TYPES.svg) {
return file;
}
const [pica, imageBlobReduce] = await Promise.all([
import("pica").then((res) => res.default),
// a wrapper for pica for better API
import("image-blob-reduce").then((res) => res.default),
]);
// CRA's minification settings break pica in WebWorkers, so let's disable
// them for now
// https://github.com/nodeca/image-blob-reduce/issues/21#issuecomment-757365513
const reduce = imageBlobReduce({
pica: pica({ features: ["js", "wasm"] }),
});
if (opts.outputType) {
const { outputType } = opts;
reduce._create_blob = function (env) {
return this.pica.toBlob(env.out_canvas, outputType, 0.8).then((blob) => {
env.out_blob = blob;
return env;
});
};
}
if (!isSupportedImageFile(file)) {
throw new Error(t("errors.unsupportedFileType"));
}
return new File(
[await reduce.toBlob(file, { max: opts.maxWidthOrHeight })],
file.name,
{
type: opts.outputType || file.type,
},
);
};
export const SVGStringToFile = (SVGString: string, filename: string = "") => {
return new File([new TextEncoder().encode(SVGString)], filename, {
type: MIME_TYPES.svg,
}) as File & { type: typeof MIME_TYPES.svg };
};

View File

@ -1,16 +1,19 @@
import { deflate, inflate } from "pako";
import { encryptData, decryptData } from "./encryption";
// -----------------------------------------------------------------------------
// byte (binary) strings
// -----------------------------------------------------------------------------
// fast, Buffer-compatible implem
export const toByteString = (data: string | Uint8Array): Promise<string> => {
export const toByteString = (
data: string | Uint8Array | ArrayBuffer,
): Promise<string> => {
return new Promise((resolve, reject) => {
const blob =
typeof data === "string"
? new Blob([new TextEncoder().encode(data)])
: new Blob([data]);
: new Blob([data instanceof Uint8Array ? data : new Uint8Array(data)]);
const reader = new FileReader();
reader.onload = (event) => {
if (!event.target || typeof event.target.result !== "string") {
@ -44,12 +47,14 @@ const byteStringToString = (byteString: string) => {
* due to reencoding
*/
export const stringToBase64 = async (str: string, isByteString = false) => {
return isByteString ? btoa(str) : btoa(await toByteString(str));
return isByteString ? window.btoa(str) : window.btoa(await toByteString(str));
};
// async to align with stringToBase64
export const base64ToString = async (base64: string, isByteString = false) => {
return isByteString ? atob(base64) : byteStringToString(atob(base64));
return isByteString
? window.atob(base64)
: byteStringToString(window.atob(base64));
};
// -----------------------------------------------------------------------------
@ -80,7 +85,7 @@ export const encode = async ({
if (compress !== false) {
try {
deflated = await toByteString(deflate(text));
} catch (error) {
} catch (error: any) {
console.error("encode: cannot deflate", error);
}
}
@ -114,3 +119,273 @@ export const decode = async (data: EncodedData): Promise<string> => {
return decoded;
};
// -----------------------------------------------------------------------------
// binary encoding
// -----------------------------------------------------------------------------
type FileEncodingInfo = {
/* version 2 is the version we're shipping the initial image support with.
version 1 was a PR version that a lot of people were using anyway.
Thus, if there are issues we can check whether they're not using the
unoffic version */
version: 1 | 2;
compression: "pako@1" | null;
encryption: "AES-GCM" | null;
};
// -----------------------------------------------------------------------------
const CONCAT_BUFFERS_VERSION = 1;
/** how many bytes we use to encode how many bytes the next chunk has.
* Corresponds to DataView setter methods (setUint32, setUint16, etc).
*
* NOTE ! values must not be changed, which would be backwards incompatible !
*/
const VERSION_DATAVIEW_BYTES = 4;
const NEXT_CHUNK_SIZE_DATAVIEW_BYTES = 4;
// -----------------------------------------------------------------------------
const DATA_VIEW_BITS_MAP = { 1: 8, 2: 16, 4: 32 } as const;
// getter
function dataView(buffer: Uint8Array, bytes: 1 | 2 | 4, offset: number): number;
// setter
function dataView(
buffer: Uint8Array,
bytes: 1 | 2 | 4,
offset: number,
value: number,
): Uint8Array;
/**
* abstraction over DataView that serves as a typed getter/setter in case
* you're using constants for the byte size and want to ensure there's no
* discrepenancy in the encoding across refactors.
*
* DataView serves for an endian-agnostic handling of numbers in ArrayBuffers.
*/
function dataView(
buffer: Uint8Array,
bytes: 1 | 2 | 4,
offset: number,
value?: number,
): Uint8Array | number {
if (value != null) {
if (value > Math.pow(2, DATA_VIEW_BITS_MAP[bytes]) - 1) {
throw new Error(
`attempting to set value higher than the allocated bytes (value: ${value}, bytes: ${bytes})`,
);
}
const method = `setUint${DATA_VIEW_BITS_MAP[bytes]}` as const;
new DataView(buffer.buffer)[method](offset, value);
return buffer;
}
const method = `getUint${DATA_VIEW_BITS_MAP[bytes]}` as const;
return new DataView(buffer.buffer)[method](offset);
}
// -----------------------------------------------------------------------------
/**
* Resulting concatenated buffer has this format:
*
* [
* VERSION chunk (4 bytes)
* LENGTH chunk 1 (4 bytes)
* DATA chunk 1 (up to 2^32 bits)
* LENGTH chunk 2 (4 bytes)
* DATA chunk 2 (up to 2^32 bits)
* ...
* ]
*
* @param buffers each buffer (chunk) must be at most 2^32 bits large (~4GB)
*/
const concatBuffers = (...buffers: Uint8Array[]) => {
const bufferView = new Uint8Array(
VERSION_DATAVIEW_BYTES +
NEXT_CHUNK_SIZE_DATAVIEW_BYTES * buffers.length +
buffers.reduce((acc, buffer) => acc + buffer.byteLength, 0),
);
let cursor = 0;
// as the first chunk we'll encode the version for backwards compatibility
dataView(bufferView, VERSION_DATAVIEW_BYTES, cursor, CONCAT_BUFFERS_VERSION);
cursor += VERSION_DATAVIEW_BYTES;
for (const buffer of buffers) {
dataView(
bufferView,
NEXT_CHUNK_SIZE_DATAVIEW_BYTES,
cursor,
buffer.byteLength,
);
cursor += NEXT_CHUNK_SIZE_DATAVIEW_BYTES;
bufferView.set(buffer, cursor);
cursor += buffer.byteLength;
}
return bufferView;
};
/** can only be used on buffers created via `concatBuffers()` */
const splitBuffers = (concatenatedBuffer: Uint8Array) => {
const buffers = [];
let cursor = 0;
// first chunk is the version
const version = dataView(
concatenatedBuffer,
NEXT_CHUNK_SIZE_DATAVIEW_BYTES,
cursor,
);
// If version is outside of the supported versions, throw an error.
// This usually means the buffer wasn't encoded using this API, so we'd only
// waste compute.
if (version > CONCAT_BUFFERS_VERSION) {
throw new Error(`invalid version ${version}`);
}
cursor += VERSION_DATAVIEW_BYTES;
while (true) {
const chunkSize = dataView(
concatenatedBuffer,
NEXT_CHUNK_SIZE_DATAVIEW_BYTES,
cursor,
);
cursor += NEXT_CHUNK_SIZE_DATAVIEW_BYTES;
buffers.push(concatenatedBuffer.slice(cursor, cursor + chunkSize));
cursor += chunkSize;
if (cursor >= concatenatedBuffer.byteLength) {
break;
}
}
return buffers;
};
// helpers for (de)compressing data with JSON metadata including encryption
// -----------------------------------------------------------------------------
/** @private */
const _encryptAndCompress = async (
data: Uint8Array | string,
encryptionKey: string,
) => {
const { encryptedBuffer, iv } = await encryptData(
encryptionKey,
deflate(data),
);
return { iv, buffer: new Uint8Array(encryptedBuffer) };
};
/**
* The returned buffer has following format:
* `[]` refers to a buffers wrapper (see `concatBuffers`)
*
* [
* encodingMetadataBuffer,
* iv,
* [
* contentsMetadataBuffer
* contentsBuffer
* ]
* ]
*/
export const compressData = async <T extends Record<string, any> = never>(
dataBuffer: Uint8Array,
options: {
encryptionKey: string;
} & ([T] extends [never]
? {
metadata?: T;
}
: {
metadata: T;
}),
): Promise<Uint8Array> => {
const fileInfo: FileEncodingInfo = {
version: 2,
compression: "pako@1",
encryption: "AES-GCM",
};
const encodingMetadataBuffer = new TextEncoder().encode(
JSON.stringify(fileInfo),
);
const contentsMetadataBuffer = new TextEncoder().encode(
JSON.stringify(options.metadata || null),
);
const { iv, buffer } = await _encryptAndCompress(
concatBuffers(contentsMetadataBuffer, dataBuffer),
options.encryptionKey,
);
return concatBuffers(encodingMetadataBuffer, iv, buffer);
};
/** @private */
const _decryptAndDecompress = async (
iv: Uint8Array,
decryptedBuffer: Uint8Array,
decryptionKey: string,
isCompressed: boolean,
) => {
decryptedBuffer = new Uint8Array(
await decryptData(iv, decryptedBuffer, decryptionKey),
);
if (isCompressed) {
return inflate(decryptedBuffer);
}
return decryptedBuffer;
};
export const decompressData = async <T extends Record<string, any>>(
bufferView: Uint8Array,
options: { decryptionKey: string },
) => {
// first chunk is encoding metadata (ignored for now)
const [encodingMetadataBuffer, iv, buffer] = splitBuffers(bufferView);
const encodingMetadata: FileEncodingInfo = JSON.parse(
new TextDecoder().decode(encodingMetadataBuffer),
);
try {
const [contentsMetadataBuffer, contentsBuffer] = splitBuffers(
await _decryptAndDecompress(
iv,
buffer,
options.decryptionKey,
!!encodingMetadata.compression,
),
);
const metadata = JSON.parse(
new TextDecoder().decode(contentsMetadataBuffer),
) as T;
return {
/** metadata source is always JSON so we can decode it here */
metadata,
/** data can be anything so the caller must decode it */
data: contentsBuffer,
};
} catch (error: any) {
console.error(
`Error during decompressing and decrypting the file.`,
encodingMetadata,
);
throw error;
}
};
// -----------------------------------------------------------------------------

92
src/data/encryption.ts Normal file
View File

@ -0,0 +1,92 @@
import { ENCRYPTION_KEY_BITS } from "../constants";
export const IV_LENGTH_BYTES = 12;
export const createIV = () => {
const arr = new Uint8Array(IV_LENGTH_BYTES);
return window.crypto.getRandomValues(arr);
};
export const generateEncryptionKey = async <
T extends "string" | "cryptoKey" = "string",
>(
returnAs?: T,
): Promise<T extends "cryptoKey" ? CryptoKey : string> => {
const key = await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: ENCRYPTION_KEY_BITS,
},
true, // extractable
["encrypt", "decrypt"],
);
return (
returnAs === "cryptoKey"
? key
: (await window.crypto.subtle.exportKey("jwk", key)).k
) as T extends "cryptoKey" ? CryptoKey : string;
};
export const getCryptoKey = (key: string, usage: KeyUsage) =>
window.crypto.subtle.importKey(
"jwk",
{
alg: "A128GCM",
ext: true,
k: key,
key_ops: ["encrypt", "decrypt"],
kty: "oct",
},
{
name: "AES-GCM",
length: ENCRYPTION_KEY_BITS,
},
false, // extractable
[usage],
);
export const encryptData = async (
key: string | CryptoKey,
data: Uint8Array | ArrayBuffer | Blob | File | string,
): Promise<{ encryptedBuffer: ArrayBuffer; iv: Uint8Array }> => {
const importedKey =
typeof key === "string" ? await getCryptoKey(key, "encrypt") : key;
const iv = createIV();
const buffer: ArrayBuffer | Uint8Array =
typeof data === "string"
? new TextEncoder().encode(data)
: data instanceof Uint8Array
? data
: data instanceof Blob
? await data.arrayBuffer()
: data;
// We use symmetric encryption. AES-GCM is the recommended algorithm and
// includes checks that the ciphertext has not been modified by an attacker.
const encryptedBuffer = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
importedKey,
buffer as ArrayBuffer | Uint8Array,
);
return { encryptedBuffer, iv };
};
export const decryptData = async (
iv: Uint8Array,
encrypted: Uint8Array | ArrayBuffer,
privateKey: string,
): Promise<ArrayBuffer> => {
const key = await getCryptoKey(privateKey, "decrypt");
return window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
},
key,
encrypted,
);
};

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