Compare commits

..

72 Commits

Author SHA1 Message Date
ad1992
6d5813e9d2 fix: restore elements and app state in updateScene 2022-01-13 12:57:08 +05:30
David Luzar
1ef287027b fix: support collaboration in bounded text (#4580) 2022-01-12 23:21:45 +01:00
David Luzar
a51ed9ced6 feat: support decreasing/increasing fontSize via keyboard (#4553)
Co-authored-by: david <dw@dw.local>
2022-01-12 15:21:36 +01:00
dependabot[bot]
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
Aakansha Doshi
92a5936c7f fix: port for collab server and update docs (#4569) 2022-01-11 18:40:09 +05:30
Aakansha Doshi
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
Arun
0a89c4b0c8 fix: prevent canvas drag while editing text (#4552) 2022-01-08 23:50:25 +01:00
Aakansha Doshi
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
David Luzar
d5a6014076 fix: prefer spreadsheet data over image (#4533) 2022-01-07 23:18:04 +01:00
Jérémy Mouzin
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
Milos Vetesnik
ac71ee7278 feat: link to new LP for excalidraw plus (#4549) 2022-01-07 16:32:35 +01:00
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
Aakansha Doshi
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
Aakansha Doshi
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
Aakansha Doshi
cec92c1d17 feat: update stroke color of bounded text along with container (#4541) 2022-01-05 16:27:48 +05:30
dependabot[bot]
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
dependabot[bot]
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
Aakansha Doshi
a2e8806f57 fix: rotate bounded text when container is rotated before typing (#4535) 2022-01-04 18:03:24 +05:30
Aakansha Doshi
b71e702991 fix: undo should work when selecting bounded textr (#4537) 2022-01-04 18:02:16 +05:30
Aakansha Doshi
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
David Luzar
28546fbb55 fix: bound text doesn't inherit container (#4521) 2021-12-31 14:55:02 +01:00
dependabot[bot]
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
David Luzar
b621d065de feat: hints and shortcuts help around deep selection (#4502) 2021-12-31 11:00:20 +01:00
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
dependabot[bot]
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
zsviczian
477cce2ed6 fix: Text wrapping with grid (#4505) (#4506) 2021-12-30 19:10:31 +01:00
Aakansha Doshi
dd8e465304 feat: Support updating text properties by clicking on container (#4499) 2021-12-29 16:49:52 +05:30
Aakansha Doshi
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
Aakansha Doshi
38236bc5e0 tests: Add tests for wrapText util (#4495) 2021-12-28 16:52:57 +05:30
Aakansha Doshi
63ce5b82d7 fix: pending review fixes for sticky notes (#4493) 2021-12-28 16:24:44 +05:30
David Luzar
bae0e985b2 fix: prevent browser from scrolling when panning (#4489) 2021-12-27 14:18:11 +01:00
Aakansha Doshi
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
Aakansha Doshi
f463c047c0 fix: pasted elements except binded text once paste action is complete (#4472) 2021-12-23 22:07:16 +05:30
Aakansha Doshi
1fd347cade fix: don't select binded text when ungrouping (#4470) 2021-12-23 21:36:29 +05:30
Aakansha Doshi
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
Aakansha Doshi
bf2bca221e fix: align and distribute binded text in container and cleanup (#4468) 2021-12-23 17:02:13 +05:30
Aakansha Doshi
d0733b1960 fix: move binded text when moving container using keyboard (#4466) 2021-12-23 01:47:14 +05:30
Aakansha Doshi
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
zsviczian
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
Aakansha Doshi
25e54e5999 fix: vertically align single line when deleting text in bounded container (#4460) 2021-12-22 15:32:21 +05:30
Aakansha Doshi
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
David Luzar
c1c37a6ee7 fix: align library item previews to center (#4447) 2021-12-21 19:59:36 +01:00
Aakansha Doshi
25b529f519 fix: vertically center align text when text deleted (#4457) 2021-12-22 00:07:55 +05:30
Aakansha Doshi
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
Aakansha Doshi
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
Aakansha Doshi
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
62 changed files with 5883 additions and 2017 deletions

View File

@@ -4,5 +4,5 @@ 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:3000
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

@@ -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

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

@@ -21,12 +21,12 @@
"dependencies": {
"@sentry/browser": "6.2.5",
"@sentry/integrations": "6.2.5",
"@testing-library/jest-dom": "5.15.1",
"@testing-library/jest-dom": "5.16.1",
"@testing-library/react": "12.1.2",
"@tldraw/vec": "1.1.5",
"@types/jest": "27.0.3",
"@tldraw/vec": "1.4.3",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "17.0.37",
"@types/react": "17.0.38",
"@types/react-dom": "17.0.11",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.23.0",
@@ -50,16 +50,16 @@
"react-dom": "17.0.2",
"react-scripts": "4.0.3",
"roughjs": "4.5.2",
"sass": "1.43.5",
"sass": "1.47.0",
"socket.io-client": "2.3.1",
"typescript": "4.5.2"
"typescript": "4.5.4"
},
"devDependencies": {
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.2.22",
"@types/chai": "4.3.0",
"@types/lodash.throttle": "4.1.6",
"@types/pako": "1.0.2",
"@types/pako": "1.0.3",
"@types/resize-observer-browser": "0.1.6",
"chai": "4.3.4",
"dotenv": "10.0.0",
@@ -68,9 +68,9 @@
"firebase-tools": "9.23.0",
"husky": "7.0.4",
"jest-canvas-mock": "2.3.1",
"lint-staged": "12.1.2",
"lint-staged": "12.1.7",
"pepjs": "0.5.3",
"prettier": "2.5.0",
"prettier": "2.5.1",
"rewire": "5.0.0"
},
"resolutions": {

View File

@@ -14,60 +14,10 @@ import {
bindOrUnbindLinearElement,
} from "../element/binding";
import { isBindingElement } from "../element/typeChecks";
import { ExcalidrawImageElement } from "../element/types";
import { imageFromImageData } from "../element/image";
export const actionFinalize = register({
name: "finalize",
perform: (
elements,
appState,
_,
{ canvas, focusContainer, imageCache, addFiles },
) => {
if (appState.editingImageElement) {
const { elementId, imageData } = appState.editingImageElement;
const editingImageElement = elements.find((el) => el.id === elementId) as
| ExcalidrawImageElement
| undefined;
if (editingImageElement?.fileId) {
const cachedImageData = imageCache.get(editingImageElement.fileId);
if (cachedImageData) {
const { image, dataURL } = imageFromImageData(imageData);
imageCache.set(editingImageElement.fileId, {
...cachedImageData,
image,
});
addFiles([
{
id: editingImageElement.fileId,
dataURL,
mimeType: cachedImageData.mimeType,
created: Date.now(),
},
]);
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
}
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
perform: (elements, appState, _, { canvas, focusContainer }) => {
if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement;
@@ -212,7 +162,6 @@ export const actionFinalize = register({
keyTest: (event, appState) =>
(event.key === KEYS.ESCAPE &&
(appState.editingLinearElement !== null ||
appState.editingImageElement !== null ||
(!appState.draggingElement && appState.multiElement === null))) ||
((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) &&
appState.multiElement !== null),

View File

@@ -17,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) {
@@ -151,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,
@@ -163,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,75 +0,0 @@
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { ToolButton } from "../components/ToolButton";
import { backgroundIcon } from "../components/icons";
import { register } from "./register";
import { getNonDeletedElements } from "../element";
import { isInitializedImageElement } from "../element/typeChecks";
import Scene from "../scene/Scene";
export const actionEditImageAlpha = register({
name: "editImageAlpha",
perform: async (elements, appState, _, app) => {
if (appState.editingImageElement) {
return {
appState: {
...appState,
editingImageElement: null,
},
commitToHistory: false,
};
}
const selectedElements = getSelectedElements(elements, appState);
const selectedElement = selectedElements[0];
if (
selectedElements.length === 1 &&
isInitializedImageElement(selectedElement)
) {
const imgData = app.imageCache.get(selectedElement.fileId);
if (!imgData) {
return false;
}
const image = await imgData.image;
const { width, height } = image;
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d")!;
context.drawImage(image, 0, 0, width, height);
const imageData = context.getImageData(0, 0, width, height);
Scene.mapElementToScene(selectedElement.id, app.scene);
return {
appState: {
...appState,
editingImageElement: {
editorType: "alpha",
elementId: selectedElement.id,
origImageData: imageData,
imageData,
pointerDownState: { screenX: 0, screenY: 0, sampledPixel: null },
},
},
commitToHistory: false,
};
}
return false;
},
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
icon={backgroundIcon}
label="Edit Image Alpha"
className={appState.editingImageElement ? "active" : ""}
title={"Edit image alpha"}
aria-label={"Edit image alpha"}
onClick={() => updateData(null)}
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
/>
),
});

View File

@@ -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,25 +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);
@@ -100,18 +117,96 @@ 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) => {
return hasStrokeColor(el.type)
? newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
})
: el;
}),
elements: changeProperty(
elements,
appState,
(el) => {
return hasStrokeColor(el.type)
? newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
})
: el;
},
true,
),
}),
appState: {
...appState,
@@ -425,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>
@@ -474,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)}
@@ -483,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,
@@ -537,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)}
@@ -551,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,
@@ -594,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)}

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

@@ -80,4 +80,3 @@ export { actionToggleGridMode } from "./actionToggleGridMode";
export { actionToggleZenMode } from "./actionToggleZenMode";
export { actionToggleStats } from "./actionToggleStats";
export { actionEditImageAlpha } from "./actionImageEditing";

View File

@@ -102,7 +102,8 @@ export type ActionName =
| "viewMode"
| "exportWithDarkMode"
| "toggleTheme"
| "editImageAlpha";
| "increaseFontSize"
| "decreaseFontSize";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];

View File

@@ -1,6 +1,7 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { Box, getCommonBoundingBox } from "./element/bounds";
import { getMaximumGroups } from "./groups";
export interface Alignment {
position: "start" | "center" | "end";
@@ -30,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,

View File

@@ -41,7 +41,6 @@ export const getDefaultAppState = (): Omit<
editingElement: null,
editingGroupId: null,
editingLinearElement: null,
editingImageElement: null,
elementLocked: false,
elementType: "selection",
errorMessage: null,
@@ -126,7 +125,6 @@ const APP_STATE_STORAGE_CONF = (<
editingElement: { browser: false, export: false, server: false },
editingGroupId: { browser: true, export: false, server: false },
editingLinearElement: { browser: false, export: false, server: false },
editingImageElement: { browser: false, export: false, server: false },
elementLocked: { browser: true, export: false, server: false },
elementType: { browser: true, export: false, server: false },
errorMessage: { browser: false, export: false, server: false },

View File

@@ -19,7 +19,6 @@ import { capitalizeString, isTransparent, setCursorForShape } from "../utils";
import Stack from "./Stack";
import { ToolButton } from "./ToolButton";
import { hasStrokeColor } from "../scene/comparisons";
import { isImageElement } from "../element/typeChecks";
export const SelectedShapeActions = ({
appState,
@@ -106,13 +105,6 @@ export const SelectedShapeActions = ({
<>{renderAction("changeArrowhead")}</>
)}
<fieldset>
<div className="buttonList">
{targetElements.some((element) => isImageElement(element)) &&
renderAction("editImageAlpha")}
</div>
</fieldset>
{renderAction("changeOpacity")}
<fieldset>

View File

@@ -72,7 +72,12 @@ import {
import { loadFromBlob } from "../data";
import { isValidLibrary } from "../data/json";
import Library from "../data/library";
import { restore, restoreElements, restoreLibraryItems } from "../data/restore";
import {
restore,
restoreAppState,
restoreElements,
restoreLibraryItems,
} from "../data/restore";
import {
dragNewElement,
dragSelectedElements,
@@ -123,6 +128,7 @@ import {
hasBoundTextElement,
isBindingElement,
isBindingElementType,
isBoundToContainer,
isImageElement,
isInitializedImageElement,
isLinearElement,
@@ -237,7 +243,6 @@ import {
getBoundTextElementId,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
import { ImageEditor } from "../element/imageEditor";
const IsMobileContext = React.createContext(false);
export const useIsMobile = () => useContext(IsMobileContext);
@@ -282,7 +287,7 @@ class App extends React.Component<AppProps, AppState> {
UIOptions: DEFAULT_UI_OPTIONS,
};
public scene: Scene;
private scene: Scene;
private resizeObserver: ResizeObserver | undefined;
private nearestScrollableContainer: HTMLElement | Document | undefined;
public library: AppClassProperties["library"];
@@ -918,8 +923,16 @@ class App extends React.Component<AppProps, AppState> {
window.removeEventListener(EVENT.RESIZE, this.onResize, false);
window.removeEventListener(EVENT.UNLOAD, this.onUnload, false);
window.removeEventListener(EVENT.BLUR, this.onBlur, false);
window.removeEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
window.removeEventListener(EVENT.DROP, this.disableEvent, false);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.DRAG_OVER,
this.disableEvent,
false,
);
this.excalidrawContainerRef.current?.removeEventListener(
EVENT.DROP,
this.disableEvent,
false,
);
document.removeEventListener(
EVENT.GESTURE_START,
@@ -988,8 +1001,16 @@ class App extends React.Component<AppProps, AppState> {
window.addEventListener(EVENT.RESIZE, this.onResize, false);
window.addEventListener(EVENT.UNLOAD, this.onUnload, false);
window.addEventListener(EVENT.BLUR, this.onBlur, false);
window.addEventListener(EVENT.DRAG_OVER, this.disableEvent, false);
window.addEventListener(EVENT.DROP, this.disableEvent, false);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.DRAG_OVER,
this.disableEvent,
false,
);
this.excalidrawContainerRef.current?.addEventListener(
EVENT.DROP,
this.disableEvent,
false,
);
}
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
@@ -1032,14 +1053,8 @@ class App extends React.Component<AppProps, AppState> {
);
if (
(this.state.editingLinearElement &&
!this.state.selectedElementIds[
this.state.editingLinearElement.elementId
]) ||
(this.state.editingImageElement &&
!this.state.selectedElementIds[
this.state.editingImageElement.elementId
])
this.state.editingLinearElement &&
!this.state.selectedElementIds[this.state.editingLinearElement.elementId]
) {
// defer so that the commitToHistory flag isn't reset via current update
setTimeout(() => {
@@ -1142,7 +1157,6 @@ class App extends React.Component<AppProps, AppState> {
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.isMobile,
editingImageElement: this.state.editingImageElement,
},
);
if (scrollBars) {
@@ -1297,7 +1311,8 @@ class App extends React.Component<AppProps, AppState> {
}
}
if (isSupportedImageFile(file)) {
// prefer spreadsheet data over image file (MS Office/Libre Office)
if (isSupportedImageFile(file) && !data.spreadsheet) {
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX: cursorX, clientY: cursorY },
this.state,
@@ -1411,7 +1426,7 @@ class App extends React.Component<AppProps, AppState> {
...this.state,
isLibraryOpen: false,
selectedElementIds: newElements.reduce((map, element) => {
if (isTextElement(element) && !element.containerId) {
if (!isBoundToContainer(element)) {
map[element.id] = true;
}
return map;
@@ -1573,22 +1588,31 @@ class App extends React.Component<AppProps, AppState> {
appState?: Pick<AppState, K> | null;
collaborators?: SceneData["collaborators"];
commitToHistory?: SceneData["commitToHistory"];
libraryItems?: SceneData["libraryItems"];
}) => {
if (sceneData.commitToHistory) {
this.history.resumeRecording();
}
if (sceneData.appState) {
this.setState(sceneData.appState);
this.setState(restoreAppState(sceneData.appState, null));
}
if (sceneData.elements) {
this.scene.replaceAllElements(sceneData.elements);
this.scene.replaceAllElements(
restoreElements(sceneData.elements, null),
);
}
if (sceneData.collaborators) {
this.setState({ collaborators: sceneData.collaborators });
}
if (sceneData.libraryItems) {
this.library.saveLibrary(
restoreLibraryItems(sceneData.libraryItems, "unpublished"),
);
}
},
);
@@ -1632,7 +1656,10 @@ class App extends React.Component<AppProps, AppState> {
}
if (
(isWritableElement(event.target) && event.key !== KEYS.ESCAPE) ||
(isWritableElement(event.target) &&
event.key !== KEYS.ESCAPE &&
// handle cmd/ctrl-modifier shortcuts even inside inputs
!event[KEYS.CTRL_OR_CMD]) ||
// case: using arrows to move between buttons
(isArrowKey(event.key) && isInputLike(event.target))
) {
@@ -1671,9 +1698,11 @@ class App extends React.Component<AppProps, AppState> {
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
: ELEMENT_TRANSLATE_AMOUNT);
const selectedElements = this.scene
.getElements()
.filter((element) => this.state.selectedElementIds[element.id]);
const selectedElements = getSelectedElements(
this.scene.getElements(),
this.state,
true,
);
let offsetX = 0;
let offsetY = 0;
@@ -1754,6 +1783,7 @@ class App extends React.Component<AppProps, AppState> {
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
isHoldingSpace = true;
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
event.preventDefault();
}
if (event.key === KEYS.G || event.key === KEYS.S) {
@@ -1891,8 +1921,8 @@ class App extends React.Component<AppProps, AppState> {
const updateElement = (
text: string,
originalText: string,
isDeleted = false,
updateDimensions = false,
isDeleted: boolean,
isSubmit: boolean,
) => {
this.scene.replaceAllElements([
...this.scene.getElementsIncludingDeleted().map((_element) => {
@@ -1904,7 +1934,7 @@ class App extends React.Component<AppProps, AppState> {
isDeleted,
originalText,
},
updateDimensions,
isSubmit,
);
}
return _element;
@@ -1930,7 +1960,7 @@ class App extends React.Component<AppProps, AppState> {
];
},
onChange: withBatchedUpdates((text) => {
updateElement(text, text, false, !element.containerId);
updateElement(text, text, false, false);
if (isNonDeletedElement(element)) {
updateBoundElements(element);
}
@@ -1976,7 +2006,7 @@ class App extends React.Component<AppProps, AppState> {
// do an initial update to re-initialize element position since we were
// modifying element's x/y for sake of editor (case: syncing to remote)
updateElement(element.text, element.originalText);
updateElement(element.text, element.originalText, false, false);
}
private deselectElements() {
@@ -2074,7 +2104,7 @@ class App extends React.Component<AppProps, AppState> {
/** whether to attempt to insert at element center if applicable */
insertAtParentCenter?: boolean;
}) => {
const parentCenterPosition =
let parentCenterPosition =
insertAtParentCenter &&
this.getTextWysiwygSnappedToCenterPosition(
sceneX,
@@ -2089,10 +2119,9 @@ class App extends React.Component<AppProps, AppState> {
const container =
shouldBind || parentCenterPosition
? getElementContainingPosition(
this.scene.getElements(),
this.scene.getElements().filter((ele) => !isTextElement(ele)),
sceneX,
sceneY,
"text",
)
: null;
@@ -2119,6 +2148,15 @@ class App extends React.Component<AppProps, AppState> {
mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2;
if (parentCenterPosition) {
parentCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
sceneX,
sceneY,
this.state,
this.canvas,
window.devicePixelRatio,
);
}
}
const element = existingTextElement
@@ -2148,6 +2186,7 @@ class App extends React.Component<AppProps, AppState> {
? "middle"
: DEFAULT_VERTICAL_ALIGN,
containerId: container?.id ?? undefined,
groupIds: container?.groupIds ?? [],
});
this.setState({ editingElement: element });
@@ -2338,10 +2377,6 @@ class App extends React.Component<AppProps, AppState> {
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer;
if (this.state.editingImageElement) {
return;
}
if (
this.state.editingLinearElement &&
!this.state.editingLinearElement.isDragging
@@ -2710,11 +2745,13 @@ class App extends React.Component<AppProps, AppState> {
(event.button === POINTER_BUTTON.WHEEL ||
(event.button === POINTER_BUTTON.MAIN && isHoldingSpace) ||
this.state.viewModeEnabled)
)
) ||
isTextElement(this.state.editingElement)
) {
return false;
}
isPanning = true;
event.preventDefault();
let nextPastePrevented = false;
const isLinux = /Linux/.test(window.navigator.platform);
@@ -2932,14 +2969,6 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState,
): boolean => {
if (this.state.elementType === "selection") {
if (this.state.editingImageElement) {
ImageEditor.handlePointerDown(
this.state.editingImageElement,
pointerDownState.origin,
);
return false;
}
const elements = this.scene.getElements();
const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
@@ -3500,22 +3529,6 @@ class App extends React.Component<AppProps, AppState> {
}
}
if (this.state.editingImageElement) {
const newImageData = ImageEditor.handlePointerMove(
this.state.editingImageElement,
pointerCoords,
);
if (newImageData) {
this.setState({
editingImageElement: {
...this.state.editingImageElement,
imageData: newImageData,
},
});
}
return;
}
if (this.state.editingLinearElement) {
const didDrag = LinearElementEditor.handlePointDragging(
this.state,
@@ -3585,6 +3598,7 @@ class App extends React.Component<AppProps, AppState> {
lockDirection,
dragDistanceX,
dragDistanceY,
this.state,
);
this.maybeSuggestBindingForAll(selectedElements);
@@ -3838,10 +3852,6 @@ class App extends React.Component<AppProps, AppState> {
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
if (this.state.editingImageElement) {
ImageEditor.handlePointerUp(this.state.editingImageElement);
}
// Handle end of dragging a point of a linear element, might close a loop
// and sets binding element
if (this.state.editingLinearElement) {

View File

@@ -154,7 +154,7 @@ 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"]} />
@@ -260,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={[
@@ -382,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

@@ -61,6 +61,27 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
return t("hints.rotate");
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
if (appState.editingElement && isTextElement(appState.editingElement)) {
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) {
@@ -75,18 +96,6 @@ const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
}
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
if (appState.editingElement && isTextElement(appState.editingElement)) {
return t("hints.text_editing");
}
if (elementType === "selection" && !selectedElements.length && !isMobile) {
return t("hints.canvasPanning");
}
return null;
};

View File

@@ -27,6 +27,8 @@
.library-unit__dragger {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}

View File

@@ -89,14 +89,6 @@ export const trash = createIcon(
{ width: 448, height: 512 },
);
export const backgroundIcon = createIcon(
<path
fill="currentColor"
d="M512 320s-64 92.65-64 128c0 35.35 28.66 64 64 64s64-28.65 64-64-64-128-64-128zm-9.37-102.94L294.94 9.37C288.69 3.12 280.5 0 272.31 0s-16.38 3.12-22.62 9.37l-81.58 81.58L81.93 4.76c-6.25-6.25-16.38-6.25-22.62 0L36.69 27.38c-6.24 6.25-6.24 16.38 0 22.62l86.19 86.18-94.76 94.76c-37.49 37.48-37.49 98.26 0 135.75l117.19 117.19c18.74 18.74 43.31 28.12 67.87 28.12 24.57 0 49.13-9.37 67.87-28.12l221.57-221.57c12.5-12.5 12.5-32.75.01-45.25zm-116.22 70.97H65.93c1.36-3.84 3.57-7.98 7.43-11.83l13.15-13.15 81.61-81.61 58.6 58.6c12.49 12.49 32.75 12.49 45.24 0s12.49-32.75 0-45.24l-58.6-58.6 58.95-58.95 162.44 162.44-48.34 48.34z"
></path>,
{ width: 576, height: 512 },
);
export const palette = createIcon(
"M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368.1-26.9 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z",

View File

@@ -106,10 +106,6 @@ 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;
@@ -182,4 +178,4 @@ export const VERSIONS = {
excalidrawLibrary: 2,
} as const;
export const PADDING = 30;
export const BOUND_TEXT_PADDING = 5;

View File

@@ -136,7 +136,7 @@ const restoreElement = (
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
containerId: element.containerId ?? null,
originalText: element.originalText ?? "",
originalText: element.originalText || element.text,
});
case "freedraw": {
return restoreElementWithProperties(element, {

View File

@@ -1,17 +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;
midX: number;
midY: number;
width: number;
height: number;
}
import { getMaximumGroups } from "./groups";
import { getCommonBoundingBox } from "./element/bounds";
export interface Distribution {
space: "between";
@@ -98,39 +88,3 @@ export const distributeElements = (
);
});
};
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 getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
midX: (minX + maxX) / 2,
midY: (minY + maxY) / 2,
};
};

View File

@@ -520,11 +520,24 @@ export interface Box {
minY: number;
maxX: number;
maxY: number;
midX: number;
midY: number;
width: number;
height: number;
}
export const getCommonBoundingBox = (
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return { minX, minY, maxX, maxY };
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
midX: (minX + maxX) / 2,
midY: (minY + maxY) / 2,
};
};

View File

@@ -46,8 +46,7 @@ const isElementDraggableFromInside = (
return true;
}
const isDraggableFromInside =
!isTransparent(element.backgroundColor) ||
(isTransparent(element.backgroundColor) && hasBoundTextElement(element));
!isTransparent(element.backgroundColor) || hasBoundTextElement(element);
if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points);
}

View File

@@ -5,8 +5,9 @@ import { mutateElement } from "./mutateElement";
import { getPerfectElementSize } from "./sizeHelpers";
import Scene from "../scene/Scene";
import { NonDeletedExcalidrawElement } from "./types";
import { PointerDownState } from "../types";
import { AppState, PointerDownState } from "../types";
import { getBoundTextElementId } from "./textElement";
import { isSelectedViaGroup } from "../groups";
export const dragSelectedElements = (
pointerDownState: PointerDownState,
@@ -16,6 +17,7 @@ export const dragSelectedElements = (
lockDirection: boolean = false,
distanceX: number = 0,
distanceY: number = 0,
appState: AppState,
) => {
const [x1, y1] = getCommonBounds(selectedElements);
const offset = { x: pointerX - x1, y: pointerY - y1 };
@@ -28,7 +30,15 @@ export const dragSelectedElements = (
element,
offset,
);
if (!element.groupIds.length) {
// update coords of bound text only if we're dragging the container directly
// (we don't drag the group that it's part of)
if (
// container isn't part of any group
// (perf optim so we don't check `isSelectedViaGroup()` in every case)
!element.groupIds.length ||
// container is part of a group, but we're dragging the container directly
(appState.editingGroupId && !isSelectedViaGroup(appState, element))
) {
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement =

View File

@@ -109,16 +109,3 @@ export const normalizeSVG = async (SVGString: string) => {
return svg.outerHTML;
}
};
export const imageFromImageData = (imagedata: ImageData) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
canvas.width = imagedata.width;
canvas.height = imagedata.height;
ctx.putImageData(imagedata, 0, 0);
const image = new Image();
const dataURL = canvas.toDataURL() as DataURL;
image.src = dataURL;
return { image, dataURL };
};

View File

@@ -1,112 +0,0 @@
import { distance2d } from "../math";
import Scene from "../scene/Scene";
import {
ExcalidrawImageElement,
InitializedExcalidrawImageElement,
} from "./types";
export type EditingImageElement = {
editorType: "alpha";
elementId: ExcalidrawImageElement["id"];
origImageData: Readonly<ImageData>;
imageData: ImageData;
pointerDownState: {
screenX: number;
screenY: number;
sampledPixel: readonly [number, number, number, number] | null;
};
};
const getElement = (id: EditingImageElement["elementId"]) => {
const element = Scene.getScene(id)?.getNonDeletedElement(id);
if (element) {
return element as InitializedExcalidrawImageElement;
}
return null;
};
export class ImageEditor {
static handlePointerDown(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const imageElement = getElement(editingElement.elementId);
if (imageElement) {
if (
scenePointer.x >= imageElement.x &&
scenePointer.x <= imageElement.x + imageElement.width &&
scenePointer.y >= imageElement.y &&
scenePointer.y <= imageElement.y + imageElement.height
) {
editingElement.pointerDownState.screenX = scenePointer.x;
editingElement.pointerDownState.screenY = scenePointer.y;
const { width, height, data } = editingElement.origImageData;
const imageOffsetX = Math.round(
(scenePointer.x - imageElement.x) * (width / imageElement.width),
);
const imageOffsetY = Math.round(
(scenePointer.y - imageElement.y) * (height / imageElement.height),
);
const sampledPixel = [
data[(imageOffsetY * width + imageOffsetX) * 4 + 0],
data[(imageOffsetY * width + imageOffsetX) * 4 + 1],
data[(imageOffsetY * width + imageOffsetX) * 4 + 2],
data[(imageOffsetY * width + imageOffsetX) * 4 + 3],
] as const;
editingElement.pointerDownState.sampledPixel = sampledPixel;
}
}
}
static handlePointerMove(
editingElement: EditingImageElement,
scenePointer: { x: number; y: number },
) {
const { sampledPixel } = editingElement.pointerDownState;
if (sampledPixel) {
const { screenX, screenY } = editingElement.pointerDownState;
const distance = distance2d(
scenePointer.x,
scenePointer.y,
screenX,
screenY,
);
const { width, height, data } = editingElement.origImageData;
const newImageData = new ImageData(width, height);
for (let x = 0; x < width; ++x) {
for (let y = 0; y < height; ++y) {
if (
Math.abs(sampledPixel[0] - data[(y * width + x) * 4 + 0]) +
Math.abs(sampledPixel[1] - data[(y * width + x) * 4 + 1]) +
Math.abs(sampledPixel[2] - data[(y * width + x) * 4 + 2]) <
distance
) {
newImageData.data[(y * width + x) * 4 + 0] = 0;
newImageData.data[(y * width + x) * 4 + 1] = 255;
newImageData.data[(y * width + x) * 4 + 2] = 0;
newImageData.data[(y * width + x) * 4 + 3] = 0;
} else {
for (let p = 0; p < 4; ++p) {
newImageData.data[(y * width + x) * 4 + p] =
data[(y * width + x) * 4 + p];
}
}
}
}
return newImageData;
}
}
static handlePointerUp(editingElement: EditingImageElement) {
editingElement.pointerDownState.sampledPixel = null;
editingElement.origImageData = editingElement.imageData;
}
}

View File

@@ -13,7 +13,7 @@ import {
FontFamilyValues,
ExcalidrawRectangleElement,
} from "../element/types";
import { getFontString, getUpdatedTimestamp } from "../utils";
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
@@ -21,10 +21,9 @@ import { AppState } from "../types";
import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import { measureText } from "./textElement";
import { getContainerElement, measureText } from "./textElement";
import { isBoundToContainer } from "./typeChecks";
import Scene from "../scene/Scene";
import { PADDING } from "../constants";
import { BOUND_TEXT_PADDING } from "../constants";
type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -158,7 +157,11 @@ const getAdjustedDimensions = (
height: number;
baseline: number;
} => {
const maxWidth = element.containerId ? element.width : null;
let maxWidth = null;
const container = getContainerElement(element);
if (container) {
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
}
const {
width: nextWidth,
height: nextHeight,
@@ -216,14 +219,14 @@ const getAdjustedDimensions = (
// make sure container dimensions are set properly when
// text editor overflows beyond viewport dimensions
if (isBoundToContainer(element)) {
const container = Scene.getScene(element)!.getElement(element.containerId)!;
const container = getContainerElement(element)!;
let height = container.height;
let width = container.width;
if (nextHeight > height - PADDING * 2) {
height = nextHeight + PADDING * 2;
if (nextHeight > height - BOUND_TEXT_PADDING * 2) {
height = nextHeight + BOUND_TEXT_PADDING * 2;
}
if (nextWidth > width - PADDING * 2) {
width = nextWidth + PADDING * 2;
if (nextWidth > width - BOUND_TEXT_PADDING * 2) {
width = nextWidth + BOUND_TEXT_PADDING * 2;
}
if (height !== container.height || width !== container.width) {
mutateElement(container, { height, width });
@@ -246,11 +249,15 @@ export const updateTextElement = (
originalText,
}: { text: string; isDeleted?: boolean; originalText: string },
updateDimensions: boolean,
isSubmit: boolean,
): ExcalidrawTextElement => {
const dimensions = updateDimensions
? getAdjustedDimensions(element, text)
: undefined;
const boundToContainer = isBoundToContainer(element);
// Don't update dimensions and text value for bounded text unless submitted
const dimensions =
boundToContainer && !isSubmit
? undefined
: getAdjustedDimensions(element, text);
return newElementWith(element, {
text,
originalText,
@@ -369,7 +376,7 @@ export const duplicateElement = <TElement extends Mutable<ExcalidrawElement>>(
overrides?: Partial<TElement>,
): TElement => {
let copy: TElement = deepCopyElement(element);
if (process.env.NODE_ENV === "test") {
if (isTestEnv()) {
copy.id = `${copy.id}_copy`;
// `window.h` may not be defined in some unit tests
if (

View File

@@ -0,0 +1,140 @@
import { wrapText } from "./textElement";
import { FontString } from "./types";
describe("Test wrapText", () => {
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
describe("When text doesn't contain new lines", () => {
const text = "Hello whats up";
[
{
desc: "break all words when width of each word is less than container width",
width: 90,
res: `Hello
whats
up`,
},
{
desc: "break all characters when width of each character is less than container width",
width: 25,
res: `H
e
l
l
o
w
h
a
t
s
u
p`,
},
{
desc: "break words as per the width",
width: 150,
res: `Hello whats
up`,
},
{
desc: "fit the container",
width: 250,
res: "Hello whats up",
},
].forEach((data) => {
it(`should ${data.desc}`, () => {
const res = wrapText(text, font, data.width);
expect(res).toEqual(data.res);
});
});
});
describe("When text contain new lines", () => {
const text = `Hello
whats up`;
[
{
desc: "break all words when width of each word is less than container width",
width: 90,
res: `Hello
whats
up`,
},
{
desc: "break all characters when width of each character is less than container width",
width: 25,
res: `H
e
l
l
o
w
h
a
t
s
u
p`,
},
{
desc: "break words as per the width",
width: 150,
res: `Hello
whats up`,
},
{
desc: "fit the container",
width: 250,
res: `Hello
whats up`,
},
].forEach((data) => {
it(`should respect new lines and ${data.desc}`, () => {
const res = wrapText(text, font, data.width);
expect(res).toEqual(data.res);
});
});
});
describe("When text is long", () => {
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
[
{
desc: "fit characters of long string as per container width",
width: 170,
res: `hellolongtextth
isiswhatsupwith
youIamtypingggg
gandtypinggg
break it now`,
},
{
desc: "fit characters of long string as per container width and break words as per the width",
width: 130,
res: `hellolongte
xtthisiswha
tsupwithyou
Iamtypinggg
ggandtyping
gg break it
now`,
},
{
desc: "fit the long text when container width is greater than text length and move the rest to next line",
width: 600,
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
break it now`,
},
].forEach((data) => {
it(`should ${data.desc}`, () => {
const res = wrapText(text, font, data.width);
expect(res).toEqual(data.res);
});
});
});
});

View File

@@ -1,20 +1,34 @@
import { getFontString, arrayToMap } from "../utils";
import { getFontString, arrayToMap, isTestEnv } from "../utils";
import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
FontString,
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement";
import { PADDING } from "../constants";
import { BOUND_TEXT_PADDING } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { AppState } from "../types";
export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
let maxWidth;
if (element.containerId) {
maxWidth = element.width;
export const redrawTextBoundingBox = (
element: ExcalidrawTextElement,
container: ExcalidrawElement | null,
appState: AppState,
) => {
const maxWidth = container
? container.width - BOUND_TEXT_PADDING * 2
: undefined;
let text = element.text;
if (container) {
text = wrapText(
element.originalText,
getFontString(element),
container.width,
);
}
const metrics = measureText(
element.originalText,
@@ -22,10 +36,24 @@ export const redrawTextBoundingBox = (element: ExcalidrawTextElement) => {
maxWidth,
);
let coordY = element.y;
// Resize container and vertically center align the text
if (container) {
coordY = container.y + container.height / 2 - metrics.height / 2;
let nextHeight = container.height;
if (metrics.height > container.height - BOUND_TEXT_PADDING * 2) {
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
coordY = container.y + nextHeight / 2 - metrics.height / 2;
}
mutateElement(container, { height: nextHeight });
}
mutateElement(element, {
width: metrics.width,
height: metrics.height,
baseline: metrics.baseline,
y: coordY,
text,
});
};
@@ -82,20 +110,12 @@ export const handleBindTextResize = (
let containerHeight = element.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
let minCharWidthTillNow = 0;
if (text) {
minCharWidthTillNow = getMinCharWidth(getFontString(textElement));
// check if the diff has exceeded min char width needed
const diff = Math.abs(
element.width - textElement.width + PADDING * 2,
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
if (diff >= minCharWidthTillNow) {
text = wrapText(
textElement.originalText,
getFontString(textElement),
element.width,
);
}
}
const dimensions = measureText(
@@ -107,8 +127,8 @@ export const handleBindTextResize = (
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > element.height - PADDING * 2) {
containerHeight = nextHeight + PADDING * 2;
if (nextHeight > element.height - BOUND_TEXT_PADDING * 2) {
containerHeight = nextHeight + BOUND_TEXT_PADDING * 2;
const diff = containerHeight - element.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
@@ -127,9 +147,9 @@ export const handleBindTextResize = (
mutateElement(textElement, {
text,
// preserve padding and set width correctly
width: element.width - PADDING * 2,
width: element.width - BOUND_TEXT_PADDING * 2,
height: nextHeight,
x: element.x + PADDING,
x: element.x + BOUND_TEXT_PADDING,
y: updatedY,
baseline: nextBaseLine,
});
@@ -198,6 +218,12 @@ const getTextWidth = (text: string, font: FontString) => {
canvas2dContext.font = font;
const metrics = canvas2dContext.measureText(text);
// since in test env the canvas measureText algo
// doesn't measure text and instead just returns number of
// characters hence we assume that each letteris 10px
if (isTestEnv()) {
return metrics.width * 10;
}
return metrics.width;
};
@@ -207,7 +233,7 @@ export const wrapText = (
font: FontString,
containerWidth: number,
) => {
const maxWidth = containerWidth - PADDING * 2;
const maxWidth = containerWidth - BOUND_TEXT_PADDING * 2;
const lines: Array<string> = [];
const originalLines = text.split("\n");
@@ -226,7 +252,7 @@ export const wrapText = (
const currentWordWidth = getTextWidth(words[index], font);
// Start breaking longer words exceeding max width
if (currentWordWidth > maxWidth) {
if (currentWordWidth >= maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
if (currentLine) {
@@ -257,7 +283,7 @@ export const wrapText = (
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth > maxWidth) {
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
lines.push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
@@ -285,8 +311,15 @@ export const wrapText = (
}
index++;
currentLine += `${word} `;
}
// Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
lines.push(currentLine.slice(0, -1));
currentLine = "";
currentLineWidthTillNow = 0;
break;
}
}
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
@@ -320,34 +353,23 @@ export const charWidth = (() => {
return cachedCharWidth[font][ascii];
};
const updateCache = (char: string, font: FontString) => {
const ascii = char.charCodeAt(0);
if (!cachedCharWidth[font][ascii]) {
cachedCharWidth[font][ascii] = calculate(char, font);
}
};
const clearCacheforFont = (font: FontString) => {
cachedCharWidth[font] = [];
};
const getCache = (font: FontString) => {
return cachedCharWidth[font];
};
return {
calculate,
updateCache,
clearCacheforFont,
getCache,
};
})();
export const getApproxMinLineWidth = (font: FontString) => {
return measureText(DUMMY_TEXT.split("").join("\n"), font).width + PADDING * 2;
return (
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
BOUND_TEXT_PADDING * 2
);
};
export const getApproxMinLineHeight = (font: FontString) => {
return getApproxLineHeight(font) + PADDING * 2;
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
};
export const getMinCharWidth = (font: FontString) => {
@@ -387,3 +409,32 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
return container?.boundElements?.filter((ele) => ele.type === "text")[0]?.id;
};
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
if (!element) {
return null;
}
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
return (
(Scene.getScene(element)?.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer) || null
);
}
return null;
};
export const getContainerElement = (
element:
| (ExcalidrawElement & { containerId: ExcalidrawElement["id"] | null })
| null,
) => {
if (!element) {
return null;
}
if (element.containerId) {
return Scene.getScene(element)?.getElement(element.containerId) || null;
}
return null;
};

View File

@@ -1,169 +1,413 @@
import ReactDOM from "react-dom";
import ExcalidrawApp from "../excalidraw-app";
import { render } from "../tests/test-utils";
import { Pointer, UI } from "../tests/helpers/ui";
import { render, screen } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { KEYS } from "../keys";
import { fireEvent } from "../tests/test-utils";
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import { ExcalidrawTextElementWithContainer } from "./types";
import * as textElementUtils from "./textElement";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
const tab = " ";
const mouse = new Pointer("mouse");
describe("textWysiwyg", () => {
let textarea: HTMLTextAreaElement;
beforeEach(async () => {
await render(<ExcalidrawApp />);
describe("Test unbounded text", () => {
let textarea: HTMLTextAreaElement;
beforeEach(async () => {
await render(<ExcalidrawApp />);
const element = UI.createElement("text");
const element = UI.createElement("text");
new Pointer("mouse").clickOn(element);
textarea = document.querySelector(
".excalidraw-textEditorContainer > textarea",
)!;
});
it("should add a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
// cursor: "|Line#1\nLine#2"
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
// cursor: " |Line#1\nLine#2"
expect(textarea.selectionStart).toEqual(4);
expect(textarea.selectionEnd).toEqual(4);
});
it("should add a tab at the start of the second line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
// cursor: "Line#1\nLin|e#2"
textarea.selectionStart = 10;
textarea.selectionEnd = 10;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
// cursor: "Line#1\n Lin|e#2"
expect(textarea.selectionStart).toEqual(14);
expect(textarea.selectionEnd).toEqual(14);
});
it("should add a tab at the start of the first and second line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2\nLine#3";
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
textarea.selectionStart = 2;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
expect(textarea.selectionStart).toEqual(6);
expect(textarea.selectionEnd).toEqual(17);
});
it("should remove a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
mouse.clickOn(element);
textarea = document.querySelector(
".excalidraw-textEditorContainer > textarea",
)!;
});
textarea.value = `${tab}Line#1\nLine#2`;
// cursor: "| Line#1\nLine#2"
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
textarea.dispatchEvent(event);
it("should add a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
// cursor: "|Line#1\nLine#2"
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
// cursor: "|Line#1\nLine#2"
expect(textarea.selectionStart).toEqual(0);
expect(textarea.selectionEnd).toEqual(0);
});
it("should remove a tab at the start of the second line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
expect(textarea.value).toEqual(`${tab}Line#1\nLine#2`);
// cursor: " |Line#1\nLine#2"
expect(textarea.selectionStart).toEqual(4);
expect(textarea.selectionEnd).toEqual(4);
});
// cursor: "Line#1\n Lin|e#2"
textarea.value = `Line#1\n${tab}Line#2`;
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
textarea.dispatchEvent(event);
it("should add a tab at the start of the second line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
// cursor: "Line#1\nLin|e#2"
textarea.selectionStart = 10;
textarea.selectionEnd = 10;
expect(textarea.value).toEqual(`Line#1\nLine#2`);
// cursor: "Line#1\nLin|e#2"
expect(textarea.selectionStart).toEqual(11);
expect(textarea.selectionEnd).toEqual(11);
});
textarea.dispatchEvent(event);
it("should remove a tab at the start of the first and second line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
expect(textarea.value).toEqual(`Line#1\n${tab}Line#2`);
// cursor: "Line#1\n Lin|e#2"
expect(textarea.selectionStart).toEqual(14);
expect(textarea.selectionEnd).toEqual(14);
});
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
textarea.selectionStart = 6;
textarea.selectionEnd = 17;
textarea.dispatchEvent(event);
it("should add a tab at the start of the first and second line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2\nLine#3";
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
textarea.selectionStart = 2;
textarea.selectionEnd = 9;
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
expect(textarea.selectionStart).toEqual(2);
expect(textarea.selectionEnd).toEqual(9);
});
textarea.dispatchEvent(event);
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
expect(textarea.value).toEqual(`${tab}Line#1\n${tab}Line#2\nLine#3`);
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
expect(textarea.selectionStart).toEqual(6);
expect(textarea.selectionEnd).toEqual(17);
});
// cursor: "Line#1\n | Line#2"
textarea.value = `Line#1\n${tab}Line#2`;
textarea.selectionStart = 9;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
// cursor: "Line#1\n|Line#2"
expect(textarea.selectionStart).toEqual(7);
// expect(textarea.selectionEnd).toEqual(7);
});
it("should remove a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
textarea.value = `${tab}Line#1\nLine#2`;
// cursor: "| Line#1\nLine#2"
textarea.selectionStart = 0;
textarea.selectionEnd = 0;
it("should remove partial tabs", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
// cursor: "|Line#1\nLine#2"
expect(textarea.selectionStart).toEqual(0);
expect(textarea.selectionEnd).toEqual(0);
});
// cursor: "Line#1\n Line#|2"
textarea.value = `Line#1\n Line#2`;
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
});
it("should remove a tab at the start of the second line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n Lin|e#2"
textarea.value = `Line#1\n${tab}Line#2`;
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
it("should remove nothing", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
// cursor: "Line#1\nLin|e#2"
expect(textarea.selectionStart).toEqual(11);
expect(textarea.selectionEnd).toEqual(11);
});
// cursor: "Line#1\n Li|ne#2"
textarea.value = `Line#1\nLine#2`;
textarea.selectionStart = 9;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
it("should remove a tab at the start of the first and second line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: " Li|ne#1\n Li|ne#2\nLine#3"
textarea.value = `${tab}Line#1\n${tab}Line#2\nLine#3`;
textarea.selectionStart = 6;
textarea.selectionEnd = 17;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2\nLine#3`);
// cursor: "Li|ne#1\nLi|ne#2\nLine#3"
expect(textarea.selectionStart).toEqual(2);
expect(textarea.selectionEnd).toEqual(9);
});
it("should remove a tab at the start of the second line and cursor stay on this line", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n | Line#2"
textarea.value = `Line#1\n${tab}Line#2`;
textarea.selectionStart = 9;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
// cursor: "Line#1\n|Line#2"
expect(textarea.selectionStart).toEqual(7);
// expect(textarea.selectionEnd).toEqual(7);
});
it("should remove partial tabs", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n Line#|2"
textarea.value = `Line#1\n Line#2`;
textarea.selectionStart = 15;
textarea.selectionEnd = 15;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
});
it("should remove nothing", () => {
const event = new KeyboardEvent("keydown", {
key: KEYS.TAB,
shiftKey: true,
});
// cursor: "Line#1\n Li|ne#2"
textarea.value = `Line#1\nLine#2`;
textarea.selectionStart = 9;
textarea.selectionEnd = 9;
textarea.dispatchEvent(event);
expect(textarea.value).toEqual(`Line#1\nLine#2`);
});
});
describe("Test bounded text", () => {
let rectangle: any;
const {
h,
}: {
h: {
elements: any;
};
} = window;
const DUMMY_HEIGHT = 240;
const DUMMY_WIDTH = 160;
const APPROX_LINE_HEIGHT = 25;
const INITIAL_WIDTH = 10;
beforeAll(() => {
jest
.spyOn(textElementUtils, "getApproxLineHeight")
.mockReturnValue(APPROX_LINE_HEIGHT);
});
beforeEach(async () => {
await render(<ExcalidrawApp />);
rectangle = UI.createElement("rectangle", {
x: 10,
y: 20,
width: 90,
height: 75,
});
});
it("should bind text to container when double clicked on center", async () => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("should bind text to container when clicked on container and enter pressed", async () => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
Keyboard.withModifierKeys({}, () => {
Keyboard.keyPress(KEYS.ENTER);
});
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("should update font family correctly on undo/redo by selecting bounded text when font family was updated", async () => {
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
mouse.down();
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
editor.blur();
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
UI.clickTool("text");
mouse.clickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
mouse.down();
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
editor.select();
fireEvent.click(screen.getByTitle(/code/i));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia);
//undo
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z);
});
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Virgil);
//redo
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
Keyboard.keyPress(KEYS.Z);
});
expect(h.elements[1].fontFamily).toEqual(FONT_FAMILY.Cascadia);
});
it("should wrap text and vertcially center align once text submitted", async () => {
jest
.spyOn(textElementUtils, "measureText")
.mockImplementation((text, font, maxWidth) => {
let width = INITIAL_WIDTH;
let height = APPROX_LINE_HEIGHT;
let baseline = 10;
if (!text) {
return {
width,
height,
baseline,
};
}
baseline = 30;
width = DUMMY_WIDTH;
if (text === "Hello \nWorld!") {
height = APPROX_LINE_HEIGHT * 2;
}
if (maxWidth) {
width = maxWidth;
// To capture cases where maxWidth passed is initial width
// due to which the text is not wrapped correctly
if (maxWidth === INITIAL_WIDTH) {
height = DUMMY_HEIGHT;
}
}
return {
width,
height,
baseline,
};
});
Keyboard.withModifierKeys({}, () => {
Keyboard.keyPress(KEYS.ENTER);
});
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
// mock scroll height
jest
.spyOn(editor, "scrollHeight", "get")
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
fireEvent.change(editor, {
target: {
value: "Hello World!",
},
});
editor.dispatchEvent(new Event("input"));
await new Promise((cb) => setTimeout(cb, 0));
editor.blur();
text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.text).toBe("Hello \nWorld!");
expect(text.originalText).toBe("Hello World!");
expect(text.y).toBe(
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
);
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
// Edit and text by removing second line and it should
// still vertically align correctly
mouse.select(rectangle);
Keyboard.withModifierKeys({}, () => {
Keyboard.keyPress(KEYS.ENTER);
});
editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, {
target: {
value: "Hello",
},
});
// mock scroll height
jest
.spyOn(editor, "scrollHeight", "get")
.mockImplementation(() => APPROX_LINE_HEIGHT);
editor.style.height = "25px";
editor.dispatchEvent(new Event("input"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.text).toBe("Hello");
expect(text.originalText).toBe("Hello");
expect(text.y).toBe(
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
);
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
expect(text.height).toBe(APPROX_LINE_HEIGHT);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
});
});
});

View File

@@ -2,22 +2,19 @@ import { CODES, KEYS } from "../keys";
import {
isWritableElement,
getFontString,
viewportCoordsToSceneCoords,
getFontFamilyString,
isTestEnv,
} from "../utils";
import Scene from "../scene/Scene";
import { isBoundToContainer, isTextElement } from "./typeChecks";
import { CLASSES, PADDING } from "../constants";
import {
ExcalidrawBindableElement,
ExcalidrawElement,
ExcalidrawTextElement,
} from "./types";
import { CLASSES, BOUND_TEXT_PADDING } from "../constants";
import { ExcalidrawElement, ExcalidrawTextElement } from "./types";
import { AppState } from "../types";
import { mutateElement } from "./mutateElement";
import {
getApproxLineHeight,
getBoundTextElementId,
getContainerElement,
wrapText,
} from "./textElement";
@@ -37,16 +34,20 @@ const getTransform = (
angle: number,
appState: AppState,
maxWidth: number,
maxHeight: number,
) => {
const { zoom, offsetTop, offsetLeft } = appState;
const degree = (180 * angle) / Math.PI;
// offsets must be multiplied by 2 to account for the division by 2 of
// the whole expression afterwards
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
const translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
let translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
if (width > maxWidth && zoom.value !== 1) {
translateX = (maxWidth / 2) * (zoom.value - 1);
}
if (height > maxHeight && zoom.value !== 1) {
translateY = ((maxHeight - offsetTop * 2) * (zoom.value - 1)) / 2;
}
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
};
@@ -69,7 +70,7 @@ export const textWysiwyg = ({
originalText: string;
}) => void;
getViewportCoords: (x: number, y: number) => [number, number];
element: ExcalidrawElement;
element: ExcalidrawTextElement;
canvas: HTMLCanvasElement | null;
excalidrawContainer: HTMLDivElement | null;
}) => {
@@ -90,93 +91,73 @@ export const textWysiwyg = ({
return false;
};
let originalContainerHeight: number;
let approxLineHeight = isTextElement(element)
? getApproxLineHeight(getFontString(element))
: 0;
let approxLineHeight = getApproxLineHeight(getFontString(element));
const initialText = element.originalText;
const updateWysiwygStyle = () => {
const updatedElement = Scene.getScene(element)?.getElement(id);
if (updatedElement && isTextElement(updatedElement)) {
let coordX = updatedElement.x;
let coordY = updatedElement.y;
let container = updatedElement?.containerId
? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId)
: null;
const container = getContainerElement(updatedElement);
let maxWidth = updatedElement.width;
let maxHeight = updatedElement.height;
let width = updatedElement.width;
// Set to element height by default since thats
// what is going to be used for unbounded text
let height = updatedElement.height;
if (container && updatedElement.containerId) {
const propertiesUpdated = textPropertiesUpdated(
updatedElement,
editable,
);
// using editor.style.height to get the accurate height of text editor
const editorHeight = Number(editable.style.height.slice(0, -2));
if (editorHeight > 0) {
height = editorHeight;
}
if (propertiesUpdated) {
const currentContainer = Scene.getScene(updatedElement)?.getElement(
updatedElement.containerId,
) as ExcalidrawBindableElement;
approxLineHeight = isTextElement(updatedElement)
? getApproxLineHeight(getFontString(updatedElement))
: 0;
if (updatedElement.height > currentContainer.height - PADDING * 2) {
const nextHeight = updatedElement.height + PADDING * 2;
originalContainerHeight = nextHeight;
mutateElement(container, { height: nextHeight });
container = { ...container, height: nextHeight };
}
editable.style.height = `${updatedElement.height}px`;
approxLineHeight = getApproxLineHeight(getFontString(updatedElement));
originalContainerHeight = container.height;
// update height of the editor after properties updated
height = updatedElement.height;
}
if (!originalContainerHeight) {
originalContainerHeight = container.height;
}
maxWidth = container.width - PADDING * 2;
maxHeight = container.height - PADDING * 2;
maxWidth = container.width - BOUND_TEXT_PADDING * 2;
maxHeight = container.height - BOUND_TEXT_PADDING * 2;
width = maxWidth;
height = Math.min(height, maxHeight);
// The coordinates of text box set a distance of
// 30px to preserve padding
coordX = container.x + PADDING;
coordX = container.x + BOUND_TEXT_PADDING;
// autogrow container height if text exceeds
if (editable.clientHeight > maxHeight) {
const diff = Math.min(
editable.clientHeight - maxHeight,
approxLineHeight,
);
if (height > maxHeight) {
const diff = Math.min(height - maxHeight, approxLineHeight);
mutateElement(container, { height: container.height + diff });
return;
} else if (
// autoshrink container height until original container height
// is reached when text is removed
container.height > originalContainerHeight &&
editable.clientHeight < maxHeight
height < maxHeight
) {
const diff = Math.min(
maxHeight - editable.clientHeight,
approxLineHeight,
);
const diff = Math.min(maxHeight - height, approxLineHeight);
mutateElement(container, { height: container.height - diff });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const lines = editable.clientHeight / approxLineHeight;
// For some reason the scrollHeight gets set to twice the lineHeight
// when you start typing for first time and thus line count is 2
// hence this check
if (lines > 2 || propertiesUpdated) {
// vertically center align the text
coordY =
container.y + container.height / 2 - editable.clientHeight / 2;
}
// vertically center align the text
coordY = container.y + container.height / 2 - height / 2;
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const { textAlign, angle } = updatedElement;
editable.value = updatedElement.originalText || updatedElement.text;
const { textAlign } = updatedElement;
editable.value = updatedElement.originalText;
const lines = updatedElement.originalText.split("\n");
const lineHeight = updatedElement.containerId
? approxLineHeight
@@ -192,19 +173,36 @@ export const textWysiwyg = ({
).marginRight.slice(0, -2),
);
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.offsetTop + appState.height - viewportY) /
(appState.height -
viewportY -
// There is a ~14px difference which keeps on increasing
// with every zoom step when offset present hence I am subtracting it here
// However this is not the best fix and breaks in
// few scenarios
(appState.offsetTop
? ((appState.zoom.value * 100 - 100) / 10) * 14
: 0)) /
appState.zoom.value;
const angle = container ? container.angle : updatedElement.angle;
Object.assign(editable.style, {
font: getFontString(updatedElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`,
width: `${width}px`,
height: `${Math.max(editable.clientHeight, updatedElement.height)}px`,
height: `${height}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: getTransform(width, height, angle, appState, maxWidth),
transform: getTransform(
width,
height,
angle,
appState,
maxWidth,
editorMaxHeight,
),
textAlign,
color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100,
@@ -212,6 +210,11 @@ export const textWysiwyg = ({
maxWidth: `${maxWidth}px`,
maxHeight: `${editorMaxHeight}px`,
});
// For some reason updating font attribute doesn't set font family
// hence updating font family explicitly for test environment
if (isTestEnv()) {
editable.style.fontFamily = getFontFamilyString(updatedElement);
}
}
};
@@ -254,8 +257,35 @@ export const textWysiwyg = ({
if (onChange) {
editable.oninput = () => {
if (isBoundToContainer(element)) {
editable.style.height = "auto";
// using scrollHeight here since we need to calculate
// number of lines so cannot use editable.style.height
// as that gets updated below
const lines = editable.scrollHeight / approxLineHeight;
// auto increase height only when lines > 1 so its
// measured correctly and vertically alignes for
// first line as well as setting height to "auto"
// doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) {
let height = "auto";
if (lines === 2) {
const container = getContainerElement(element);
const actualLineCount = wrapText(
editable.value,
getFontString(element),
container!.width,
).split("\n").length;
// This is browser behaviour when setting height to "auto"
// It sets the height needed for 2 lines even if actual
// line count is 1 as mentioned above as well
// hence reducing the height by half if actual line count is 1
// so single line aligns vertically when deleting
if (actualLineCount === 1) {
height = `${editable.scrollHeight / 2}px`;
}
}
editable.style.height = height;
editable.style.height = `${editable.scrollHeight}px`;
}
onChange(normalizeText(editable.value));
@@ -263,13 +293,16 @@ export const textWysiwyg = ({
}
editable.onkeydown = (event) => {
event.stopPropagation();
if (!event[KEYS.CTRL_OR_CMD]) {
event.stopPropagation();
}
if (event.key === KEYS.ESCAPE) {
event.preventDefault();
submittedViaKeyboard = true;
handleSubmit();
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
event.preventDefault();
event.stopPropagation();
if (event.isComposing || event.keyCode === 229) {
return;
}
@@ -282,6 +315,7 @@ export const textWysiwyg = ({
event.code === CODES.BRACKET_RIGHT))
) {
event.preventDefault();
event.stopPropagation();
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
outdent();
} else {
@@ -406,9 +440,7 @@ export const textWysiwyg = ({
}
let wrappedText = "";
if (isTextElement(updateElement) && updateElement?.containerId) {
const container = Scene.getScene(updateElement)!.getElement(
updateElement.containerId,
) as ExcalidrawBindableElement;
const container = getContainerElement(updateElement);
if (container) {
wrappedText = wrapText(
@@ -416,21 +448,23 @@ export const textWysiwyg = ({
getFontString(updateElement),
container.width,
);
const { x, y } = viewportCoordsToSceneCoords(
{
clientX: Number(editable.style.left.slice(0, -2)),
clientY: Number(editable.style.top.slice(0, -2)),
},
appState,
);
if (isTextElement(updateElement) && updateElement.containerId) {
if (updateElement.containerId) {
const editorHeight = Number(editable.style.height.slice(0, -2));
if (editable.value) {
mutateElement(updateElement, {
y: y + appState.offsetTop,
height: Number(editable.style.height.slice(0, -2)),
width: Number(editable.style.width.slice(0, -2)),
x: x + appState.offsetLeft,
});
// Don't mutate if text is not updated
if (initialText !== editable.value) {
mutateElement(updateElement, {
// vertically center align
y: container.y + container.height / 2 - editorHeight / 2,
height: editorHeight,
width: Number(editable.style.width.slice(0, -2)),
// preserve padding
x: container.x + BOUND_TEXT_PADDING,
angle: container.angle,
});
}
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) {
mutateElement(container, {

View File

@@ -5,13 +5,13 @@ import {
getDefaultAppState,
} from "../../appState";
import { clearElementsForLocalStorage } from "../../element";
import { STORAGE_KEYS as APP_STORAGE_KEYS } from "../../constants";
export const STORAGE_KEYS = {
LOCAL_STORAGE_ELEMENTS: "excalidraw",
LOCAL_STORAGE_APP_STATE: "excalidraw-state",
LOCAL_STORAGE_COLLAB: "excalidraw-collab",
LOCAL_STORAGE_KEY_COLLAB_FORCE_FLAG: "collabLinkForceLoadFlag",
LOCAL_STORAGE_LIBRARY: "excalidraw-library",
};
export const saveUsernameToLocalStorage = (username: string) => {
@@ -113,9 +113,7 @@ export const getTotalStorageSize = () => {
try {
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
const library = localStorage.getItem(
APP_STORAGE_KEYS.LOCAL_STORAGE_LIBRARY,
);
const library = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY);
const appStateSize = appState?.length || 0;
const collabSize = collab?.length || 0;

View File

@@ -7,7 +7,6 @@ import { TopErrorBoundary } from "../components/TopErrorBoundary";
import {
APP_NAME,
EVENT,
STORAGE_KEYS,
TITLE_TIMEOUT,
URL_HASH_KEYS,
VERSION_TIMEOUT,
@@ -53,6 +52,7 @@ import { exportToBackend, getCollaborationLinkData, loadScene } from "./data";
import {
importFromLocalStorage,
saveToLocalStorage,
STORAGE_KEYS,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restoreAppState, RestoredDataState } from "../data/restore";
@@ -262,7 +262,7 @@ const PlusLinkJSX = (
Introducing Excalidraw+
<br />
<a
href="https://plus.excalidraw.com/?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
href="https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=banner&utm_campaign=launch"
target="_blank"
rel="noreferrer"
>

View File

@@ -1,6 +1,13 @@
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types";
import {
GroupId,
ExcalidrawElement,
NonDeleted,
ExcalidrawTextElementWithContainer,
} from "./element/types";
import { AppState } from "./types";
import { getSelectedElements } from "./scene";
import { getBoundTextElementId } from "./element/textElement";
import Scene from "./scene/Scene";
export const selectGroup = (
groupId: GroupId,
@@ -158,3 +165,33 @@ export const removeFromSelectedGroups = (
groupIds: ExcalidrawElement["groupIds"],
selectedGroupIds: { [groupId: string]: boolean },
) => groupIds.filter((groupId) => !selectedGroupIds[groupId]);
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) || [];
// Include bounded text if present when grouping
const boundTextElementId = getBoundTextElementId(element);
if (boundTextElementId) {
const textElement = Scene.getScene(element)!.getElement(
boundTextElementId,
) as ExcalidrawTextElementWithContainer;
currentGroupMembers.push(textElement);
}
groups.set(groupId, [...currentGroupMembers, element]);
});
return Array.from(groups.values());
};

View File

@@ -40,6 +40,10 @@ export const KEYS = {
QUESTION_MARK: "?",
SPACE: " ",
TAB: "Tab",
CHEVRON_LEFT: "<",
CHEVRON_RIGHT: ">",
PERIOD: ".",
COMMA: ",",
A: "a",
D: "d",

View File

@@ -102,7 +102,9 @@
"showBackground": "Show background color picker",
"toggleTheme": "Toggle theme",
"personalLib": "Personal Library",
"excalidrawLib": "Excalidraw Library"
"excalidrawLib": "Excalidraw Library",
"decreaseFontSize": "Decrease font size",
"increaseFontSize": "Increase font size"
},
"buttons": {
"clearReset": "Reset the canvas",
@@ -208,7 +210,8 @@
"lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points",
"placeImage": "Click to place the image, or click and drag to set its size manually",
"publishLibrary": "Publish your own library",
"bindTextToElement": "Press enter to add text"
"bindTextToElement": "Press enter to add text",
"deepBoxSelect": "Hold CtrlOrCmd to deep select, and to prevent dragging"
},
"canvasError": {
"cannotShowPreview": "Cannot show preview",
@@ -255,6 +258,8 @@
"helpDialog": {
"blog": "Read our blog",
"click": "click",
"deepSelect": "Deep select",
"deepBoxSelect": "Deep select within box, and prevent dragging",
"curvedArrow": "Curved arrow",
"curvedLine": "Curved line",
"documentation": "Documentation",

View File

@@ -19,6 +19,8 @@ Please add the latest change on the top under the correct section.
### Features
- Support updating library using [`updateScene`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#updateScene) API [#4546](https://github.com/excalidraw/excalidraw/pull/4546).
- Introduced primary colors to the app. The colors can be overriden. Check [readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#customizing-styles) on how to do so.
- #### BREAKING CHANGE
@@ -64,8 +66,15 @@ Please add the latest change on the top under the correct section.
The `Appearance` type is now removed and renamed to `Theme` so `Theme` type needs to be used.
### Fixes
- Panning the canvas using `mousewheel-drag` and `space-drag` now prevents the browser from scrolling the container/page [#4489](https://github.com/excalidraw/excalidraw/pull/4489).
- Scope drag and drop events to Excalidraw container to prevent overriding host application drag and drop events.
### Build
- Added an example to test and develop the package [locally](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#Development) using `yarn start`
- Remove `file-loader` so font assets are not duplicated by webpack and use webpack asset modules for font generation [#4380](https://github.com/excalidraw/excalidraw/pull/4380)
- We're now compiling to `es2017` target. Notably, `async/await` is not compiled down to generators. [#4341](https://github.com/excalidraw/excalidraw/pull/4341)

View File

@@ -509,6 +509,7 @@ You can use this function to update the scene with the sceneData. It accepts the
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L18) | The `appState` to be updated in the scene. |
| `collaborators` | <pre>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L29">Collaborator></a></pre> | The list of collaborators to be updated in the scene. |
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L258) | The `libraryItems` to be update in the scene. |
### `addFiles`
@@ -1008,3 +1009,21 @@ Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
### Development
#### Install the dependencies
```bash
yarn
```
#### Start the server
```bash
yarn start
```
[http://localhost:3001](http://localhost:3001) will open in your default browser.
The example is same as the [codesandbox example](https://ehlz3.csb.app/)

View File

@@ -0,0 +1,328 @@
import { useEffect, useState, useRef } from "react";
import InitialData from "./initialData";
import Sidebar from "./sidebar/Sidebar";
import "./App.scss";
import initialData from "./initialData";
// This is so that we use the bundled excalidraw.developement.js file instead
// of the actual source code
const { exportToCanvas, exportToSvg, exportToBlob } = window.Excalidraw;
const Excalidraw = window.Excalidraw.default;
const renderTopRightUI = () => {
return (
<button onClick={() => alert("This is dummy top right UI")}>
{" "}
Click me{" "}
</button>
);
};
const renderFooter = () => {
return (
<button onClick={() => alert("This is dummy footer")}>
{" "}
custom footer{" "}
</button>
);
};
export default function App() {
const excalidrawRef = useRef(null);
const [viewModeEnabled, setViewModeEnabled] = useState(false);
const [zenModeEnabled, setZenModeEnabled] = useState(false);
const [gridModeEnabled, setGridModeEnabled] = useState(false);
const [blobUrl, setBlobUrl] = useState(null);
const [canvasUrl, setCanvasUrl] = useState(null);
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
const [shouldAddWatermark, setShouldAddWatermark] = useState(false);
const [theme, setTheme] = useState("light");
useEffect(() => {
const onHashChange = () => {
const hash = new URLSearchParams(window.location.hash.slice(1));
const libraryUrl = hash.get("addLibrary");
if (libraryUrl) {
excalidrawRef.current.importLibrary(libraryUrl, hash.get("token"));
}
};
window.addEventListener("hashchange", onHashChange, false);
return () => {
window.removeEventListener("hashchange", onHashChange);
};
}, []);
const updateScene = () => {
const sceneData = {
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
{
id: "Ioiq-J6pE6vMftvReHcq7",
type: "rectangle",
x: 530.01171875,
y: 120.31640625,
width: 158.34375,
height: 175.52734375,
angle: 0,
strokeColor: "#000000",
backgroundColor: "#e64980",
fillStyle: "solid",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
groupIds: [],
strokeSharpness: "sharp",
seed: 1752793487,
version: 243,
versionNonce: 1367252367,
isDeleted: false,
boundElementIds: ["QEIIo0aX3LIkipph5r9Vc"],
updated: 1642057965561,
},
{
id: "QEIIo0aX3LIkipph5r9Vc",
type: "arrow",
x: 300.73046875,
y: 200.47265625,
width: 231.7578125,
height: 9.823413575122089,
angle: 0,
strokeColor: "#000000",
backgroundColor: "#e64980",
fillStyle: "solid",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
groupIds: [],
strokeSharpness: "round",
seed: 1335966913,
version: 180,
versionNonce: 24053473,
isDeleted: false,
boundElementIds: null,
updated: 1642057965562,
points: [
[0, 0],
[231.7578125, -9.823413575122089],
],
lastCommittedPoint: null,
startBinding: null,
endBinding: {
elementId: "Ioiq-J6pE6vMftvReHcq7",
focus: 0.012598149333175906,
gap: 8.5234375,
},
startArrowhead: null,
endArrowhead: "arrow",
},
],
appState: {
viewBackgroundColor: "#edf2ff",
},
};
excalidrawRef.current.updateScene(sceneData);
};
return (
<div className="App">
<h1> Excalidraw Example</h1>
<Sidebar>
<div className="button-wrapper">
<button className="update-scene" onClick={updateScene}>
Update Scene
</button>
<button
className="reset-scene"
onClick={() => {
excalidrawRef.current.resetScene();
}}
>
Reset Scene
</button>
<button
onClick={() => {
excalidrawRef.current.updateScene({
libraryItems: [
{
status: "published",
elements: initialData.libraryItems[0],
},
{
status: "unpublished",
elements: initialData.libraryItems[1],
},
],
});
}}
>
Update Library
</button>
<label>
<input
type="checkbox"
checked={viewModeEnabled}
onChange={() => setViewModeEnabled(!viewModeEnabled)}
/>
View mode
</label>
<label>
<input
type="checkbox"
checked={zenModeEnabled}
onChange={() => setZenModeEnabled(!zenModeEnabled)}
/>
Zen mode
</label>
<label>
<input
type="checkbox"
checked={gridModeEnabled}
onChange={() => setGridModeEnabled(!gridModeEnabled)}
/>
Grid mode
</label>
<label>
<input
type="checkbox"
checked={theme === "dark"}
onChange={() => {
let newTheme = "light";
if (theme === "light") {
newTheme = "dark";
}
setTheme(newTheme);
}}
/>
Switch to Dark Theme
</label>
</div>
<div className="excalidraw-wrapper">
<Excalidraw
ref={excalidrawRef}
initialData={InitialData}
onChange={(elements, state) =>
console.info("Elements :", elements, "State : ", state)
}
onPointerUpdate={(payload) => console.info(payload)}
onCollabButtonClick={() =>
window.alert("You clicked on collab button")
}
viewModeEnabled={viewModeEnabled}
zenModeEnabled={zenModeEnabled}
gridModeEnabled={gridModeEnabled}
theme={theme}
name="Custom name of drawing"
UIOptions={{ canvasActions: { loadScene: false } }}
renderTopRightUI={renderTopRightUI}
renderFooter={renderFooter}
/>
</div>
<div className="export-wrapper button-wrapper">
<label className="export-wrapper__checkbox">
<input
type="checkbox"
checked={exportWithDarkMode}
onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
/>
Export with dark mode
</label>
<label className="export-wrapper__checkbox">
<input
type="checkbox"
checked={shouldAddWatermark}
onChange={() => setShouldAddWatermark(!shouldAddWatermark)}
/>
Add Watermark
</label>
<button
onClick={async () => {
const svg = await exportToSvg({
elements: excalidrawRef.current.getSceneElements(),
appState: {
...initialData.appState,
exportWithDarkMode,
shouldAddWatermark,
width: 300,
height: 100,
},
embedScene: true,
});
document.querySelector(".export-svg").innerHTML = svg.outerHTML;
}}
>
Export to SVG
</button>
<div className="export export-svg"></div>
<button
onClick={async () => {
const blob = await exportToBlob({
elements: excalidrawRef.current.getSceneElements(),
mimeType: "image/png",
appState: {
...initialData.appState,
exportWithDarkMode,
shouldAddWatermark,
},
});
setBlobUrl(window.URL.createObjectURL(blob));
}}
>
Export to Blob
</button>
<div className="export export-blob">
<img src={blobUrl} alt="" />
</div>
<button
onClick={() => {
const canvas = exportToCanvas({
elements: excalidrawRef.current.getSceneElements(),
appState: {
...initialData.appState,
exportWithDarkMode,
shouldAddWatermark,
},
});
const ctx = canvas.getContext("2d");
ctx.font = "30px Virgil";
ctx.strokeText("My custom text", 50, 60);
setCanvasUrl(canvas.toDataURL());
}}
>
Export to Canvas
</button>
<div className="export export-canvas">
<img src={canvasUrl} alt="" />
</div>
</div>
</Sidebar>
</div>
);
}

View File

@@ -0,0 +1,42 @@
.App {
font-family: sans-serif;
text-align: center;
}
.button-wrapper button {
z-index: 1;
height: 40px;
max-width: 200px;
margin: 10px;
padding: 5px;
}
.excalidraw .App-menu_top .buttonList {
display: flex;
}
.excalidraw-wrapper {
height: 800px;
margin: 50px;
}
:root[dir="ltr"]
.excalidraw
.layer-ui__wrapper
.zen-mode-transition.App-menu_bottom--transition-left {
transform: none;
}
.excalidraw .panelColumn {
text-align: left;
}
.export-wrapper {
display: flex;
flex-direction: column;
margin: 50px;
&__checkbox {
display: flex;
}
}

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000" />
<title>React App</title>
<script>
window.name = "codesandbox";
</script>
</head>
<body>
<noscript> You need to enable JavaScript to run this app. </noscript>
<div id="root"></div>
<script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script>
<!-- This is so that we use the bundled excalidraw.developement.js file instead
of the actual source code -->
<script src="./excalidraw.development.js"></script>
<script src="./bundle.js"></script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement,
);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
import { useState } from "react";
import "./Sidebar.scss";
export default function Sidebar(props) {
const [open, setOpen] = useState(false);
return (
<>
<div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}>
<button className="closebtn" onClick={() => setOpen(false)}>
x
</button>
<div className="sidebar-links">
<button>Dummy Home</button>
<button>Dummy About</button>{" "}
</div>
</div>
<div className={`${open ? "sidebar-open" : ""}`}>
<button
className="openbtn"
onClick={() => {
setOpen(!open);
}}
>
Open Sidebar
</button>
{props.children}
</div>
</>
);
}

View File

@@ -0,0 +1,66 @@
.sidebar {
height: 100%;
width: 0;
position: absolute;
z-index: 1;
top: 0;
left: 0;
background-color: #111;
overflow-x: hidden;
transition: 0.5s;
padding-top: 60px;
&.open {
width: 300px;
}
&-links {
display: flex;
flex-direction: column;
padding: 20px;
button {
padding: 10px;
margin: 10px;
background: #faa2c1;
color: #fff;
border: none;
cursor: pointer;
}
}
}
.sidebar a {
padding: 8px 8px 8px 32px;
text-decoration: none;
font-size: 25px;
color: #818181;
display: block;
transition: 0.3s;
}
.sidebar a:hover {
color: #f1f1f1;
}
.sidebar .closebtn {
position: absolute;
top: 0;
right: 0;
font-size: 36px;
margin-left: 50px;
}
.openbtn {
font-size: 20px;
cursor: pointer;
background-color: #111;
color: white;
padding: 10px 15px;
border: none;
display: flex;
margin-left: 50px;
}
.sidebar-open {
margin-left: 300px;
}

View File

@@ -44,28 +44,30 @@
"react-dom": "^17.0.2"
},
"devDependencies": {
"@babel/core": "7.16.0",
"@babel/plugin-transform-arrow-functions": "7.16.0",
"@babel/core": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.7",
"@babel/plugin-transform-async-to-generator": "7.16.0",
"@babel/plugin-transform-runtime": "7.16.4",
"@babel/plugin-transform-typescript": "7.16.1",
"@babel/preset-env": "7.16.4",
"@babel/preset-react": "7.16.0",
"@babel/preset-typescript": "7.16.0",
"autoprefixer": "10.4.0",
"@babel/preset-env": "7.16.7",
"@babel/preset-react": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"autoprefixer": "10.4.2",
"babel-loader": "8.2.3",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",
"css-loader": "6.5.1",
"mini-css-extract-plugin": "2.4.5",
"mini-css-extract-plugin": "2.4.6",
"postcss-loader": "6.2.1",
"sass-loader": "12.4.0",
"terser-webpack-plugin": "5.2.5",
"terser-webpack-plugin": "5.3.0",
"ts-loader": "9.2.6",
"typescript": "4.5.3",
"typescript": "4.5.4",
"webpack": "5.65.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.9.1"
"webpack-cli": "4.9.1",
"webpack-dev-server": "4.7.2",
"webpack-merge": "5.8.0"
},
"bugs": "https://github.com/excalidraw/excalidraw/issues",
"homepage": "https://github.com/excalidraw/excalidraw/tree/master/src/packages/excalidraw",
@@ -73,7 +75,8 @@
"gen:types": "tsc --project ../../../tsconfig-types.json",
"build:umd": "rm -rf dist && cross-env NODE_ENV=production webpack --config webpack.prod.config.js && cross-env NODE_ENV=development webpack --config webpack.dev.config.js && yarn gen:types",
"build:umd:withAnalyzer": "cross-env NODE_ENV=production ANALYZER=true webpack --config webpack.prod.config.js",
"pack": "yarn build:umd && yarn pack"
"pack": "yarn build:umd && yarn pack",
"start": "webpack serve --config webpack.dev-server.config.js "
},
"dependencies": {
"dotenv": "10.0.0"

View File

@@ -0,0 +1,28 @@
const path = require("path");
const { merge } = require("webpack-merge");
const devConfig = require("./webpack.dev.config");
const devServerConfig = {
entry: {
bundle: "./example/index.js",
},
// Server Configuration options
devServer: {
port: 3001,
host: "localhost",
hot: true,
compress: true,
static: {
directory: path.join(__dirname, "example"),
},
client: {
progress: true,
logging: "info",
overlay: true, //Shows a full-screen overlay in the browser when there are compiler errors or warnings.
},
open: ["./"],
},
};
module.exports = merge(devServerConfig, devConfig);

File diff suppressed because it is too large Load Diff

View File

@@ -34,13 +34,13 @@
]
},
"devDependencies": {
"@babel/core": "7.16.0",
"@babel/core": "7.16.7",
"@babel/plugin-transform-arrow-functions": "7.16.0",
"@babel/plugin-transform-async-to-generator": "7.16.0",
"@babel/plugin-transform-async-to-generator": "7.16.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/plugin-transform-typescript": "7.16.1",
"@babel/preset-env": "7.16.4",
"@babel/preset-typescript": "7.16.0",
"@babel/plugin-transform-typescript": "7.16.7",
"@babel/preset-env": "7.16.7",
"@babel/preset-typescript": "7.16.7",
"babel-loader": "8.2.3",
"babel-plugin-transform-class-properties": "6.24.1",
"cross-env": "7.0.3",

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
import { Random } from "roughjs/bin/math";
import { nanoid } from "nanoid";
import { isTestEnv } from "./utils";
let random = new Random(Date.now());
let testIdBase = 0;
@@ -11,5 +12,4 @@ export const reseed = (seed: number) => {
testIdBase = 0;
};
export const randomId = () =>
process.env.NODE_ENV === "test" ? `id${testIdBase++}` : nanoid();
export const randomId = () => (isTestEnv() ? `id${testIdBase++}` : nanoid());

View File

@@ -12,7 +12,6 @@ import {
isLinearElement,
isFreeDrawElement,
isInitializedImageElement,
isImageElement,
} from "../element/typeChecks";
import {
getDiamondPoints,
@@ -222,31 +221,19 @@ const drawElementOnCanvas = (
break;
}
case "image": {
if (renderConfig.editingImageElement) {
const { imageData } = renderConfig.editingImageElement;
const imgCanvas = document.createElement("canvas");
imgCanvas.width = imageData.width;
imgCanvas.height = imageData.height;
const imgContext = imgCanvas.getContext("2d")!;
imgContext.putImageData(imageData, 0, 0);
context.drawImage(imgCanvas, 0, 0, element.width, element.height);
const img = isInitializedImageElement(element)
? renderConfig.imageCache.get(element.fileId)?.image
: undefined;
if (img != null && !(img instanceof Promise)) {
context.drawImage(
img,
0 /* hardcoded for the selection box*/,
0,
element.width,
element.height,
);
} else {
const img = isInitializedImageElement(element)
? renderConfig.imageCache.get(element.fileId)?.image
: undefined;
if (img != null && !(img instanceof Promise)) {
context.drawImage(
img,
0 /* hardcoded for the selection box*/,
0,
element.width,
element.height,
);
} else {
drawImagePlaceholder(element, context, renderConfig.zoom.value);
}
drawImagePlaceholder(element, context, renderConfig.zoom.value);
}
break;
}
@@ -423,23 +410,23 @@ const generateElementShape = (
topY + (rightY - topY) * 0.25
} L ${rightX - (rightX - topX) * 0.25} ${
rightY - (rightY - topY) * 0.25
}
}
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
rightX - (rightX - bottomX) * 0.25
} ${rightY + (bottomY - rightY) * 0.25}
} ${rightY + (bottomY - rightY) * 0.25}
L ${bottomX + (rightX - bottomX) * 0.25} ${
bottomY - (bottomY - rightY) * 0.25
}
}
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
bottomX - (bottomX - leftX) * 0.25
} ${bottomY - (bottomY - leftY) * 0.25}
} ${bottomY - (bottomY - leftY) * 0.25}
L ${leftX + (bottomX - leftX) * 0.25} ${
leftY + (bottomY - leftY) * 0.25
}
}
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${
leftX + (topX - leftX) * 0.25
} ${leftY - (leftY - topY) * 0.25}
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
} ${leftY - (leftY - topY) * 0.25}
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
C ${topX} ${topY}, ${topX} ${topY}, ${
topX + (rightX - topX) * 0.25
} ${topY + (rightY - topY) * 0.25}`,
@@ -621,10 +608,7 @@ const generateElementWithCanvas = (
if (
!prevElementWithCanvas ||
shouldRegenerateBecauseZoom ||
prevElementWithCanvas.theme !== renderConfig.theme ||
(renderConfig.editingImageElement &&
isImageElement(element) &&
element.id === renderConfig.editingImageElement.elementId)
prevElementWithCanvas.theme !== renderConfig.theme
) {
const elementWithCanvas = generateElementCanvas(
element,

View File

@@ -4,13 +4,15 @@ import {
NonDeleted,
} from "../element/types";
import { getNonDeletedElements, isNonDeletedElement } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
type ElementKey = ExcalidrawElement | ExcalidrawElement["id"];
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void;
const isIdKey = (elementKey: ElementKey): elementKey is string => {
const isIdKey = (elementKey: ElementKey): elementKey is ElementIdKey => {
if (typeof elementKey === "string") {
return true;
}

View File

@@ -75,7 +75,6 @@ export const getElementContainingPosition = (
elements: readonly ExcalidrawElement[],
x: number,
y: number,
excludedType?: ExcalidrawElement["type"],
) => {
let hitElement = null;
// We need to to hit testing from front (end of the array) to back (beginning of the array)
@@ -84,13 +83,7 @@ export const getElementContainingPosition = (
continue;
}
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]);
if (
x1 < x &&
x < x2 &&
y1 < y &&
y < y2 &&
elements[index].type !== excludedType
) {
if (x1 < x && x < x2 && y1 < y && y < y2) {
hitElement = elements[index];
break;
}

View File

@@ -67,7 +67,6 @@ export const exportToCanvas = async (
renderSelection: false,
renderGrid: false,
isExporting: true,
editingImageElement: null,
});
return canvas;

View File

@@ -77,4 +77,4 @@ export const getTargetElements = (
) =>
appState.editingElement
? [appState.editingElement]
: getSelectedElements(elements, appState);
: getSelectedElements(elements, appState, true);

View File

@@ -11,7 +11,6 @@ export type RenderConfig = {
zoom: AppState["zoom"];
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
theme: AppState["theme"];
editingImageElement: AppState["editingImageElement"];
// collab-related state
// ---------------------------------------------------------------------------
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };

View File

@@ -80,7 +80,7 @@ export const SHAPES = [
</svg>
),
value: "freedraw",
key: KEYS.X,
key: [KEYS.X, KEYS.P.toUpperCase()],
},
{
icon: (

View File

@@ -29,8 +29,6 @@ import { MaybeTransformHandleType } from "./element/transformHandles";
import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { EditingImageElement } from "./element/imageEditor";
import Scene from "./scene/Scene";
export type Point = Readonly<RoughPoint>;
@@ -79,7 +77,6 @@ export type AppState = {
// (e.g. text element when typing into the input)
editingElement: NonDeletedExcalidrawElement | null;
editingLinearElement: LinearElementEditor | null;
editingImageElement: EditingImageElement | null;
elementType: typeof SHAPES[number]["value"];
elementLocked: boolean;
exportBackground: boolean;
@@ -258,6 +255,7 @@ export type SceneData = {
appState?: ImportedDataState["appState"];
collaborators?: Map<string, Collaborator>;
commitToHistory?: boolean;
libraryItems?: LibraryItems | LibraryItems_v1;
};
export enum UserIdleState {
@@ -319,8 +317,6 @@ export type AppClassProperties = {
}
>;
files: BinaryFiles;
scene: Scene;
addFiles: App["addFiles"];
};
export type PointerDownState = Readonly<{

View File

@@ -443,8 +443,7 @@ export const bytesToHexString = (bytes: Uint8Array) => {
.join("");
};
export const getUpdatedTimestamp = () =>
process.env.NODE_ENV === "test" ? 1 : Date.now();
export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
/**
* Transforms array of objects containing `id` attribute,
@@ -458,3 +457,6 @@ export const arrayToMap = <T extends { id: string } | string>(
return acc;
}, new Map());
};
export const isTestEnv = () =>
typeof process !== "undefined" && process.env?.NODE_ENV === "test";

149
yarn.lock
View File

@@ -2136,14 +2136,14 @@
lz-string "^1.4.4"
pretty-format "^27.0.2"
"@testing-library/jest-dom@5.15.1":
version "5.15.1"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.15.1.tgz#4c49ba4d244f235aec53f0a83498daeb4ee06c33"
integrity sha512-kmj8opVDRE1E4GXyLlESsQthCXK7An28dFWxhiMwD7ZUI7ZxA6sjdJRxLerD9Jd8cHX4BDc1jzXaaZKqzlUkvg==
"@testing-library/jest-dom@5.16.1":
version "5.16.1"
resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz#3db7df5ae97596264a7da9696fe14695ba02e51f"
integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw==
dependencies:
"@babel/runtime" "^7.9.2"
"@types/testing-library__jest-dom" "^5.9.1"
aria-query "^4.2.2"
aria-query "^5.0.0"
chalk "^3.0.0"
css "^3.0.0"
css.escape "^1.5.1"
@@ -2159,10 +2159,10 @@
"@babel/runtime" "^7.12.5"
"@testing-library/dom" "^8.0.0"
"@tldraw/vec@1.1.5":
version "1.1.5"
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.1.5.tgz#a0ec75742c20da43e3328f824ef7fca0f982b1fc"
integrity sha512-reEos3gJ9OiNvAYFtJzHbYWNjPTvWPmpjAY70HWHMjfhyNk6lHwwzDjwSgej4/KxVBbxB3ppNfLD9mEMq9yCOQ==
"@tldraw/vec@1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@tldraw/vec/-/vec-1.4.3.tgz#c1460da019063b8eb9c62d715c61dff44dd23b02"
integrity sha512-p7Nu9OWqorQ+nWPlvkN6Zn1oDbRgr4sheLzRif/+xB7BzMQFJh1T8syX6Jj/S88GPvn5PwZgYUKMnMFvvL5t/w==
"@tootallnate/once@1":
version "1.1.2"
@@ -2219,10 +2219,10 @@
dependencies:
"@babel/types" "^7.3.0"
"@types/chai@4.2.22":
version "4.2.22"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7"
integrity sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==
"@types/chai@4.3.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
"@types/duplexify@^3.6.0":
version "3.6.0"
@@ -2283,10 +2283,10 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@*", "@types/jest@27.0.3":
version "27.0.3"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a"
integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==
"@types/jest@*", "@types/jest@27.4.0":
version "27.4.0"
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.0.tgz#037ab8b872067cae842a320841693080f9cb84ed"
integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ==
dependencies:
jest-diff "^27.0.0"
pretty-format "^27.0.0"
@@ -2338,10 +2338,10 @@
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/pako@1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.2.tgz#17c9b136877f33d9ecc8e73cd26944f1f6dd39a1"
integrity sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw==
"@types/pako@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@types/pako/-/pako-1.0.3.tgz#2e61c2b02020b5f44e2e5e946dfac74f4ec33c58"
integrity sha512-EDxOsHAD5dqjbjEUM1xwa7rpKPFb8ECBE5irONTQU7/OsO3thI5YrNEWSPNMvYmvFM0l/OLQJ6Mgw7PEdXSjhg==
"@types/parse-json@^4.0.0":
version "4.0.0"
@@ -2375,10 +2375,10 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@17.0.37":
version "17.0.37"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.37.tgz#6884d0aa402605935c397ae689deed115caad959"
integrity sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==
"@types/react@*", "@types/react@17.0.38":
version "17.0.38"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.38.tgz#f24249fefd89357d5fa71f739a686b8d7c7202bd"
integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
@@ -4379,11 +4379,6 @@ clone@^1.0.2:
resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz"
integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
clone@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
clsx@1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz"
@@ -5163,10 +5158,10 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2:
version "4.3.2"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
@@ -5723,7 +5718,7 @@ enhanced-resolve@^4.3.0:
memory-fs "^0.5.0"
tapable "^1.0.0"
enquirer@^2.3.5, enquirer@^2.3.6:
enquirer@^2.3.5:
version "2.3.6"
resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@@ -7755,6 +7750,11 @@ immer@8.0.1:
resolved "https://registry.npmjs.org/immer/-/immer-8.0.1.tgz"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
immutable@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz"
@@ -9276,24 +9276,23 @@ lines-and-columns@^1.1.6:
resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
lint-staged@12.1.2:
version "12.1.2"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.2.tgz#90c571927e1371fc133e720671dd7989eab53f74"
integrity sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==
lint-staged@12.1.7:
version "12.1.7"
resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.1.7.tgz#fe9137992ac18a456422bb8484dd30be0140629f"
integrity sha512-bltv/ejiLWtowExpjU+s5z8j1Byjg9AlmaAjMmqNbIicY69u6sYIwXGg0dCn0TlkrrY2CphtHIXAkbZ+1VoWQQ==
dependencies:
cli-truncate "^3.1.0"
colorette "^2.0.16"
commander "^8.3.0"
debug "^4.3.2"
enquirer "^2.3.6"
debug "^4.3.3"
execa "^5.1.1"
lilconfig "2.0.4"
listr2 "^3.13.3"
listr2 "^3.13.5"
micromatch "^4.0.4"
normalize-path "^3.0.0"
object-inspect "^1.11.0"
object-inspect "^1.11.1"
string-argv "^0.3.1"
supports-color "^9.0.2"
supports-color "^9.2.1"
yaml "^1.10.2"
listenercount@~1.0.1:
@@ -9301,16 +9300,16 @@ listenercount@~1.0.1:
resolved "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz"
integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=
listr2@^3.13.3:
version "3.13.3"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.3.tgz#d8f6095c9371b382c9b1c2bc33c5941d8e177f11"
integrity sha512-VqAgN+XVfyaEjSaFewGPcDs5/3hBbWVaX1VgWv2f52MF7US45JuARlArULctiB44IIcEk3JF7GtoFCLqEdeuPA==
listr2@^3.13.5:
version "3.13.5"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.13.5.tgz#105a813f2eb2329c4aae27373a281d610ee4985f"
integrity sha512-3n8heFQDSk+NcwBn3CgxEibZGaRzx+pC64n3YjpMD1qguV4nWus3Al+Oo3KooqFKTQEJ1v7MmnbnyyNspgx3NA==
dependencies:
cli-truncate "^2.1.0"
clone "^2.1.2"
colorette "^2.0.16"
log-update "^4.0.0"
p-map "^4.0.0"
rfdc "^1.3.0"
rxjs "^7.4.0"
through "^2.3.8"
wrap-ansi "^7.0.0"
@@ -10404,10 +10403,10 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
object-inspect@^1.11.0, object-inspect@^1.9.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1"
integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==
object-inspect@^1.11.1, object-inspect@^1.9.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0"
integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==
object-is@^1.0.1:
version "1.1.5"
@@ -11788,10 +11787,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
prettier@2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.0.tgz#a6370e2d4594e093270419d9cc47f7670488f893"
integrity sha512-FM/zAKgWTxj40rH03VxzIPdXmj39SwSjwG0heUcNFwI+EMZJnY93yAiKXM3dObIKAM5TA88werc8T/EwhB45eg==
prettier@2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
pretty-bytes@^5.3.0:
version "5.6.0"
@@ -12719,6 +12718,11 @@ rework@1.0.1:
convert-source-map "^0.3.3"
css "^2.0.0"
rfdc@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==
rgb-regex@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz"
@@ -12911,12 +12915,14 @@ sass-loader@^10.0.5:
schema-utils "^3.0.0"
semver "^7.3.2"
sass@1.43.5:
version "1.43.5"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.5.tgz#25a9d91dd098793ef7229d7b04dd3daae2fc4a65"
integrity sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg==
sass@1.47.0:
version "1.47.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.47.0.tgz#c22dd0eed2e4a991430dae0b03c8e694bc41c2b4"
integrity sha512-GtXwvwgD7/6MLUZPnlA5/8cdRgC9SzT5kAnnJMRmEZQFRE3J56Foswig4NyyyQGsnmNvg6EUM/FP0Pe9Y2zywQ==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@~1.2.4:
version "1.2.4"
@@ -13348,6 +13354,11 @@ source-list-map@^2.0.0:
resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz"
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
"source-map-js@>=0.6.2 <2.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf"
integrity sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==
source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
version "0.5.3"
resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz"
@@ -13864,10 +13875,10 @@ supports-color@^7.0.0, supports-color@^7.1.0:
dependencies:
has-flag "^4.0.0"
supports-color@^9.0.2:
version "9.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.1.0.tgz#558963681dafeff41ed68220488cbf438d29f351"
integrity sha512-lOCGOTmBSN54zKAoPWhHkjoqVQ0MqgzPE5iirtoSixhr0ZieR/6l7WZ32V53cvy9+1qghFnIk7k52p991lKd6g==
supports-color@^9.2.1:
version "9.2.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-9.2.1.tgz#599dc9d45acf74c6176e0d880bab1d7d718fe891"
integrity sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==
supports-hyperlinks@^1.0.1:
version "1.0.1"
@@ -14381,10 +14392,10 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998"
integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==
typescript@4.5.4:
version "4.5.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
typeson-registry@^1.0.0-alpha.20:
version "1.0.0-alpha.39"