Compare commits

...

137 Commits

Author SHA1 Message Date
dwelle
75e2d9e359 remove debug 2021-11-22 23:20:40 +01:00
dwelle
6592517122 import lazily 2021-11-22 22:11:10 +01:00
dwelle
bd953a6287 feat: compress non-transparent PNGs as JPGs and allow larger dimensions 2021-11-22 17:39:59 +01:00
zsviczian
b6ef953dc9 fix: SVG export in dark mode with embedded bitmap image (#4285)
Co-authored-by: dwelle <luzar.david@gmail.com>
2021-11-20 15:31:51 +01:00
David Luzar
620b662085 chore: bump typescript@4.5.2 (#4283)
* chore: bump typescript@4.5.2

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

* allow numbers

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

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

* Add publish library dialog

* Pass the data to publish the library

* pass lib blob

* Handle old and new libraries when importing

* Better error handling

* Show publish success when library submitted for review

* don't close library when publish success dialog open

* Support multiple libs deletion and publish

* Set status to published once library submitted for review

* Save  to LS after library published

* unique key for publish and delete

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

* design improvements

* migrate old library to the new one

* fix

* fix tests

* use i18n

* Support submit type in toolbutton

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

* Add twitter handle in form state

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

* clarify types around LibraryItems

* Add website optional field

* event.preventDefault to make htm5 form validationw work

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

* remove ts-ignore

* add placeholders for fields

* decrease clickable area for checkbox by 0.5em

* add checkbox background color

* rename `items` to `elements`

* improve checkbox hit area

* show selected library items in publish dialog

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

* Don't close publish dialog when clicked outside

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

* rename library to libraryItems in excalidrawLib and added migration

* change icon and swap bg/color

* use blue brand color for hover/selected states

* prompt for confirmation when deleting library items

* separate unpublished items from published

* factor `LibraryMenu` into own file

* i18n and minor fixes for unpublished items

* fix not rendering empty cells when library empty

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

* Add edit name functionality for library items

* fix

* edit lib name with onchange/blur

* bump library version

* prefer response error message

* add library urls to ENV vars

* mark lib item name as required

* Use input only for lib item name

* better error validation for lib items

* fix label styling for lib items

* design and i18n fixes

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

* Add a note about MIT License

* Add note for guidelines

* Add tooltip for publish button

* Show spinner in submit button when submission is in progress

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

* update export icon and support export library for selected items

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

* fix spec

* Refactoring the library actions for reusablility

* show only load when items not present

* close on click outside in publish dialog

* ad dialog description and tweak copy

* vertically center input labels

* align input styles

* move author name input to other usernames

* rename param

* inline to simplify

* fix to not inline `undefined` class names

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

* await response callback

* refactor types

* refactor

* i18n

* align casing & tweaks

* move ls logic to publishLibrary

* support removal of item inside publish dialog

* fix labels for trash icon when items selected

* replace window.confirm for removal libs with confirm dialog

* fix input/textarea styling

* move library item menu scss to its own file

* use blue for load and cyan for publish

* reduce margin for submit and make submit => Submit

* Make library items header sticky

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

* update url

* fix grid gap between lib items

* Mark older items imported from initial data as unpublished

* add text to publish button on non-mobile

* add items counter

* fix test

* show personal and excal libs sections and personal goes first

* show toast on adding to library via contextMenu

* Animate plus icon and not the pending item

* fix snap

* use i18n when no item in publish dialog

* tweak style of new lib item

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

* fix

* push selected item first in unpublished section

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

* Add items to the begining of library

* add `created` library item attr

* fix test

* use `defaultValue` instead of `value`

* fix dark theme styles

* fix toggle button not closing library

* close library menu on Escape

* tweak publish dialog item remove style

* fix remove icon in publish dialog

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

* move confirm to right

* add types

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:46:42 +01:00
dependabot[bot]
284747d742 chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#4243)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.2.4 to 5.2.5.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.2.4...v5.2.5)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-14 10:44:44 +01:00
Excalidraw Bot
f6559b65ef chore: Update translations from Crowdin (#4201) 2021-11-12 12:13:34 +01:00
Thomas Steiner
bc6b066c07 Remove outdated OT info (#4232) 2021-11-09 12:16:32 +01:00
dependabot[bot]
6370d517a2 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4209)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.5.0 to 6.5.1.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v6.5.0...v6.5.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 17:45:06 +00:00
dependabot[bot]
b8a37c42e4 chore(deps): bump @tldraw/vec from 0.0.132 to 0.1.3 (#4215)
Bumps [@tldraw/vec](https://github.com/tldraw/vec) from 0.0.132 to 0.1.3.
- [Release notes](https://github.com/tldraw/vec/releases)
- [Commits](https://github.com/tldraw/vec/commits)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:41:59 +01:00
dependabot[bot]
d2a2c9d6b5 chore(deps-dev): bump webpack in /src/packages/utils (#4211)
Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.62.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.61.0...v5.62.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 17:41:50 +00:00
dependabot[bot]
3a72f347d2 chore(deps-dev): bump webpack in /src/packages/excalidraw (#4208)
Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.62.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.61.0...v5.62.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:41:13 +01:00
dependabot[bot]
c1d9456235 chore(deps-dev): bump mini-css-extract-plugin (#4207)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v2.4.3...v2.4.4)

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-08 18:39:52 +01:00
dependabot[bot]
473b8ca0ca chore(deps): bump @types/react from 17.0.33 to 17.0.34 (#4216)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.33 to 17.0.34.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

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

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

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

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

* Remove unused

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:31:12 +00:00
dependabot[bot]
ef433233d1 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#4161)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.15.0 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:24:26 +00:00
Lipis
1c7056bdaa chore: bump Prettier to the latest (#4185) 2021-11-01 15:24:05 +02:00
dependabot[bot]
277ffaacb9 chore(deps-dev): bump css-loader in /src/packages/excalidraw (#4122)
Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 5.2.6 to 6.5.0.
- [Release notes](https://github.com/webpack-contrib/css-loader/releases)
- [Changelog](https://github.com/webpack-contrib/css-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/css-loader/compare/v5.2.6...v6.5.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:23:38 +00:00
dependabot[bot]
2a3e242cfd chore(deps-dev): bump @babel/plugin-transform-runtime (#4164)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-plugin-transform-runtime)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:19:04 +00:00
dependabot[bot]
8df9742463 chore(deps-dev): bump webpack-cli in /src/packages/excalidraw (#4132)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.7.2 to 4.9.1.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.7.2...webpack-cli@4.9.1)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:15:16 +00:00
dependabot[bot]
f70d11c2d1 chore(deps-dev): bump @babel/plugin-transform-typescript (#4167)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.15.8 to 7.16.1.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.1/packages/babel-plugin-transform-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 15:11:04 +02:00
dependabot[bot]
05e54d6785 chore(deps-dev): bump mini-css-extract-plugin (#4094)
Bumps [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) from 1.6.1 to 2.4.3.
- [Release notes](https://github.com/webpack-contrib/mini-css-extract-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/mini-css-extract-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/mini-css-extract-plugin/compare/v1.6.1...v2.4.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:10:36 +00:00
dependabot[bot]
795a6e4546 chore(deps-dev): bump webpack-cli in /src/packages/utils (#4118)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 4.7.2 to 4.9.1.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/webpack-cli@4.7.2...webpack-cli@4.9.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 13:09:32 +00:00
dependabot[bot]
a01a4ad739 chore(deps): bump @testing-library/react from 11.2.6 to 12.1.2 (#4177)
Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 11.2.6 to 12.1.2.
- [Release notes](https://github.com/testing-library/react-testing-library/releases)
- [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/react-testing-library/compare/v11.2.6...v12.1.2)

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 18:24:40 +05:30
dependabot[bot]
ede3c4af82 chore(deps-dev): bump postcss-loader in /src/packages/excalidraw (#4169)
Bumps [postcss-loader](https://github.com/webpack-contrib/postcss-loader) from 6.1.1 to 6.2.0.
- [Release notes](https://github.com/webpack-contrib/postcss-loader/releases)
- [Changelog](https://github.com/webpack-contrib/postcss-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/postcss-loader/compare/v6.1.1...v6.2.0)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:31:34 +02:00
dependabot[bot]
c2d0107cc5 chore(deps): bump sass from 1.32.10 to 1.43.4 (#4174)
Bumps [sass](https://github.com/sass/dart-sass) from 1.32.10 to 1.43.4.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.32.10...1.43.4)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:31:13 +02:00
dependabot[bot]
c43fac31a1 chore(deps): bump @types/react from 17.0.3 to 17.0.33 (#4179)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 17.0.3 to 17.0.33.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:30:17 +02:00
dependabot[bot]
d9a1eb2f01 chore(deps-dev): bump firebase-tools from 9.9.0 to 9.21.0 (#4184)
Bumps [firebase-tools](https://github.com/firebase/firebase-tools) from 9.9.0 to 9.21.0.
- [Release notes](https://github.com/firebase/firebase-tools/releases)
- [Commits](https://github.com/firebase/firebase-tools/compare/v9.9.0...v9.21.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 14:29:56 +02:00
dependabot[bot]
f1e17a320f chore(deps-dev): bump @babel/preset-env in /src/packages/utils (#4151)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-preset-env)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 10:05:36 +00:00
dependabot[bot]
75ecd818b3 chore(deps-dev): bump webpack-bundle-analyzer in /src/packages/utils (#4158)
Bumps [webpack-bundle-analyzer](https://github.com/webpack-contrib/webpack-bundle-analyzer) from 4.4.2 to 4.5.0.
- [Release notes](https://github.com/webpack-contrib/webpack-bundle-analyzer/releases)
- [Changelog](https://github.com/webpack-contrib/webpack-bundle-analyzer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/webpack-bundle-analyzer/compare/v4.4.2...v4.5.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 10:05:03 +00:00
Lipis
a7abc71f6a chore: Update dependabot limits (#4145) 2021-11-01 12:03:16 +02:00
dependabot[bot]
6d0f0c8f21 chore(deps-dev): bump @babel/plugin-transform-runtime (#4152)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.15.8 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-01 12:00:41 +02:00
David Luzar
790e6da500 fix: images not initialized correctly (#4157)
* fix: image not initialized correctly due to not renewing `state.pendingImageElement`

* ensure we replace elements on update

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

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

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

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

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

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

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

* fix mouse broadcast race condition

* stop mutating image elements when updating status

to fix race condition when closing/opening collab room

* check `files` when resolving `LayerUI`

* fix displaying AbortError
2021-10-30 23:40:35 +02:00
Excalidraw Bot
d6d629f416 chore: Update translations from Crowdin (#4109) 2021-10-30 19:26:54 +03:00
dependabot[bot]
65dec605f2 chore(deps-dev): bump @babel/preset-typescript (#4143)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.15.0 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 16:20:16 +00:00
dependabot[bot]
cacec0b5c4 chore(deps-dev): bump @babel/preset-react in /src/packages/excalidraw (#4140)
Bumps [@babel/preset-react](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-react) from 7.14.5 to 7.16.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.16.0/packages/babel-preset-react)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 12:03:51 +03:00
dependabot[bot]
87a302d7e9 chore(deps-dev): bump sass-loader in /src/packages/excalidraw (#4139)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.1.0 to 12.3.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.1.0...v12.3.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 11:34:17 +03:00
dependabot[bot]
899b36c206 chore(deps-dev): bump babel-loader in /src/packages/excalidraw (#4138)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.2 to 8.2.3.
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v8.2.2...v8.2.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-30 00:41:13 +00:00
dependabot[bot]
534cbef982 chore(deps-dev): bump autoprefixer in /src/packages/excalidraw (#4137)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.3.1 to 10.4.0.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.3.1...10.4.0)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 18:37:49 +03:00
dependabot[bot]
aab5067718 chore(deps-dev): bump @types/resize-observer-browser from 0.1.5 to 0.1.6 (#4135)
Bumps [@types/resize-observer-browser](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/resize-observer-browser) from 0.1.5 to 0.1.6.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/resize-observer-browser)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 15:36:23 +00:00
dependabot[bot]
b679da02ee chore(deps-dev): bump terser-webpack-plugin in /src/packages/excalidraw (#4128)
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 5.1.4 to 5.2.4.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v5.1.4...v5.2.4)

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

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

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

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

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

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

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 18:25:47 +03:00
David Luzar
f1202adb15 feat: stop using production services for development (#4113) 2021-10-29 17:13:28 +02:00
dependabot[bot]
fd439cf38a chore(deps-dev): bump babel-loader in /src/packages/utils (#4124)
Bumps [babel-loader](https://github.com/babel/babel-loader) from 8.2.2 to 8.2.3.
- [Release notes](https://github.com/babel/babel-loader/releases)
- [Changelog](https://github.com/babel/babel-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel-loader/compare/v8.2.2...v8.2.3)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:32:52 +00:00
dependabot[bot]
83c63be846 chore(deps-dev): bump @babel/preset-typescript (#4127)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.14.5 to 7.15.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.0/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:26:28 +00:00
dependabot[bot]
b59d49dd7f chore(deps-dev): bump sass-loader in /src/packages/utils (#4126)
Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 12.1.0 to 12.3.0.
- [Release notes](https://github.com/webpack-contrib/sass-loader/releases)
- [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/sass-loader/compare/v12.1.0...v12.3.0)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:24:54 +00:00
dependabot[bot]
3f390d4858 chore(deps-dev): bump ts-loader in /src/packages/excalidraw (#4002)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.4 to 9.2.6.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.4...v9.2.6)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:24:16 +00:00
dependabot[bot]
fdde73bff4 chore(deps-dev): bump @babel/core in /src/packages/excalidraw (#4069)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.8 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:21:07 +00:00
dependabot[bot]
90a416e265 chore(deps): bump @types/react-dom from 17.0.3 to 17.0.10 (#4120)
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 17.0.3 to 17.0.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 14:17:59 +03:00
dependabot[bot]
a828b2e5de chore(deps-dev): bump @babel/plugin-transform-typescript (#4044)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.14.6 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-plugin-transform-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:16:44 +00:00
dependabot[bot]
7c51d3c24c chore(deps-dev): bump ts-loader in /src/packages/utils (#4117)
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 9.2.4 to 9.2.6.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/main/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v9.2.4...v9.2.6)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:13:03 +00:00
dependabot[bot]
4d2d6f181a chore(deps): bump tar from 4.4.15 to 4.4.19 (#3953)
Bumps [tar](https://github.com/npm/node-tar) from 4.4.15 to 4.4.19.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v4.4.15...v4.4.19)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 11:10:33 +00:00
dependabot[bot]
071416f6ef chore(deps-dev): bump @babel/plugin-transform-typescript (#4045)
Bumps [@babel/plugin-transform-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-typescript) from 7.14.6 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-plugin-transform-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 14:10:19 +03:00
dependabot[bot]
d675b07089 chore(deps-dev): bump @babel/core in /src/packages/utils (#4043)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.14.8 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-core)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 14:09:53 +03:00
dependabot[bot]
3975fd592a chore(deps-dev): bump @babel/plugin-transform-runtime (#4042)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.14.5 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 14:09:37 +03:00
dependabot[bot]
34a9a4dac6 chore(deps-dev): bump @babel/preset-typescript in /src/packages/utils (#3928)
Bumps [@babel/preset-typescript](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-typescript) from 7.14.5 to 7.15.0.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.0/packages/babel-preset-typescript)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 14:08:37 +03:00
Lipis
78e419b790 chore(deps-dev): Upgrade commit hooks to Husky 7 (#4116)
* Upgrade to Husky 7

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

* New translations en.json (Catalan)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Bengali)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Indonesian)

* New translations en.json (Punjabi)

* New translations en.json (Persian)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Polish)

* New translations en.json (Dutch)

* New translations en.json (Catalan)

* New translations en.json (Bulgarian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Chinese Simplified)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Korean)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Italian)

* New translations en.json (Dutch)

* New translations en.json (Chinese Traditional)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (Ukrainian)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* New translations en.json (Occitan)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Traditional)

* New translations en.json (Indonesian)

* New translations en.json (Punjabi)

* New translations en.json (Persian)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Polish)

* New translations en.json (Dutch)

* New translations en.json (Catalan)

* New translations en.json (Bulgarian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Chinese Simplified)

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Czech)

* New translations en.json (Korean)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Occitan)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* Auto commit: Calculate translation coverage

* New translations en.json (Indonesian)

* New translations en.json (Kazakh)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (Italian)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese)

* Auto commit: Calculate translation coverage

* New translations en.json (Finnish)

* Auto commit: Calculate translation coverage

* New translations en.json (Sinhala)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese, Brazilian)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Tamil)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovak)

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

* New translations en.json (Swedish)

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

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

* Auto commit: Calculate translation coverage

* New translations en.json (Sinhala)

* Auto commit: Calculate translation coverage

* New translations en.json (Sinhala)

* Auto commit: Calculate translation coverage

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

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Kabyle)

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

* blacklist webex url

* dont cache webex

* Unregister sw for webex

* fix

* fix

* reload in callback

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

* reduce font weight

* fix specs

* update button name and use action

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

* fix
2021-10-19 14:40:48 +05:30
Aakansha Doshi
bc88cf5002 fix: Don't show save file to disk when UIOptions.canvasActions.export.saveFileToDisk is false (#4073) 2021-10-19 14:39:47 +05:30
dependabot[bot]
571be9c0fe chore(deps-dev): bump @babel/plugin-transform-runtime (#4053)
Bumps [@babel/plugin-transform-runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-runtime) from 7.14.5 to 7.15.8.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.15.8/packages/babel-plugin-transform-runtime)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-18 15:59:56 +05:30
Aakansha Doshi
5d925c7d3f build: Allow package.json changes when autoreleasing next (#4068) 2021-10-18 15:58:58 +05:30
David Luzar
45c520341f chore: bump @dwelle/browser-fs-access to 0.21.2 (#4067) 2021-10-18 11:08:12 +02:00
Aakansha Doshi
c6ffc06541 feat: support renderTopRightUI in mobile (#4065) 2021-10-17 21:44:46 +05:30
Aakansha Doshi
ff29780760 Refactor: convert initializeApp to func component and use JSX transform in the codebase (#4056) 2021-10-14 22:56:51 +05:30
Aakansha Doshi
463857ad9a feat: Export THEME from the package (#4055)
* Use Theme type everywhere
* Rename Appearance type to Theme for consistency
* Reorder headers in readme
The host don't need to pass hardcoded strings any more and instead can use the exported constant
2021-10-14 14:15:57 +05:30
dependabot[bot]
be2da9539e chore(deps): bump path-parse in /src/packages/excalidraw (#3912)
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

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

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

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

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-13 20:18:09 +05:30
dependabot[bot]
1104f6891e chore(deps): bump semver-regex from 3.1.2 to 3.1.3 (#3988)
Bumps [semver-regex](https://github.com/sindresorhus/semver-regex) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/sindresorhus/semver-regex/releases)
- [Commits](https://github.com/sindresorhus/semver-regex/commits)

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

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

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

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

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

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

13
.env
View File

@@ -1,5 +1,8 @@
REACT_APP_BACKEND_V1_GET_URL=https://json.excalidraw.com/api/v1/
REACT_APP_BACKEND_V2_GET_URL=https://json.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json.excalidraw.com/api/v2/post/
REACT_APP_SOCKET_SERVER_URL=https://portal.excalidraw.com
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
REACT_APP_BACKEND_V2_GET_URL=https://json-dev.excalidraw.com/api/v2/
REACT_APP_BACKEND_V2_POST_URL=https://json-dev.excalidraw.com/api/v2/post/
REACT_APP_LIBRARY_URL=https://libraries.excalidraw.com
REACT_APP_LIBRARY_BACKEND=https://us-central1-excalidraw-room-persistence.cloudfunctions.net/libraries
REACT_APP_SOCKET_SERVER_URL=http://localhost:3000
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyCMkxA60XIW8KbqMYL7edC4qT5l4qHX2h8","authDomain":"excalidraw-oss-dev.firebaseapp.com","projectId":"excalidraw-oss-dev","storageBucket":"excalidraw-oss-dev.appspot.com","messagingSenderId":"664559512677","appId":"1:664559512677:web:a385181f2928d328a7aa8c"}'

View File

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

View File

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

View File

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

View File

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

View File

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

1
.husky/pre-commit Executable file
View File

@@ -0,0 +1 @@
yarn lint-staged

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import React from "react";
import { alignElements, Alignment } from "../align";
import {
AlignBottomIcon,

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
import { isSomeElementSelected } from "../scene";
import { KEYS } from "../keys";
import { ToolButton } from "../components/ToolButton";
import React from "react";
import { trash } from "../components/icons";
import { t } from "../i18n";
import { register } from "./register";
@@ -111,10 +110,8 @@ export const actionDeleteSelected = register({
};
}
let {
elements: nextElements,
appState: nextAppState,
} = deleteSelectedElements(elements, appState);
let { elements: nextElements, appState: nextAppState } =
deleteSelectedElements(elements, appState);
fixBindingsAfterDeletion(
nextElements,
elements.filter(({ id }) => appState.selectedElementIds[id]),

View File

@@ -1,4 +1,3 @@
import React from "react";
import {
DistributeHorizontallyIcon,
DistributeVerticallyIcon,

View File

@@ -1,4 +1,3 @@
import React from "react";
import { KEYS } from "../keys";
import { register } from "./register";
import { ExcalidrawElement } from "../element/types";

View File

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

View File

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

View File

@@ -93,13 +93,13 @@ const flipElements = (
appState: AppState,
flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => {
for (let i = 0; i < elements.length; i++) {
flipElement(elements[i], appState);
elements.forEach((element) => {
flipElement(element, appState);
// If vertical flip, rotate an extra 180
if (flipDirection === "vertical") {
rotateElement(elements[i], Math.PI);
rotateElement(element, Math.PI);
}
}
});
return elements;
};

View File

@@ -1,4 +1,3 @@
import React from "react";
import { CODES, KEYS } from "../keys";
import { t } from "../i18n";
import { getShortcutKey } from "../utils";
@@ -100,9 +99,8 @@ export const actionGroup = register({
// to the z order of the highest element in the layer stack
const elementsInGroup = getElementsInGroup(updatedElements, newGroupId);
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
const lastGroupElementIndex = updatedElements.lastIndexOf(
lastElementInGroup,
);
const lastGroupElementIndex =
updatedElements.lastIndexOf(lastElementInGroup);
const elementsAfterGroup = updatedElements.slice(lastGroupElementIndex + 1);
const elementsBeforeGroup = updatedElements
.slice(0, lastGroupElementIndex)

View File

@@ -1,5 +1,4 @@
import { Action, ActionResult } from "./types";
import React from "react";
import { undo, redo } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { t } from "../i18n";

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import React from "react";
import { AppState } from "../../src/types";
import { ButtonIconSelect } from "../components/ButtonIconSelect";
import { ColorPicker } from "../components/ColorPicker";
@@ -7,6 +6,7 @@ import {
ArrowheadArrowIcon,
ArrowheadBarIcon,
ArrowheadDotIcon,
ArrowheadTriangleIcon,
ArrowheadNoneIcon,
EdgeRoundIcon,
EdgeSharpIcon,
@@ -60,6 +60,7 @@ import {
getTargetElements,
isSomeElementSelected,
} from "../scene";
import { hasStrokeColor } from "../scene/comparisons";
import { register } from "./register";
const changeProperty = (
@@ -104,11 +105,13 @@ export const actionChangeStrokeColor = register({
perform: (elements, appState, value) => {
return {
...(value.currentItemStrokeColor && {
elements: changeProperty(elements, appState, (el) =>
newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
}),
),
elements: changeProperty(elements, appState, (el) => {
return hasStrokeColor(el.type)
? newElementWith(el, {
strokeColor: value.currentItemStrokeColor,
})
: el;
}),
}),
appState: {
...appState,
@@ -736,6 +739,14 @@ export const actionChangeArrowhead = register({
icon: <ArrowheadDotIcon theme={appState.theme} flip={!isRTL} />,
keyBinding: "r",
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: (
<ArrowheadTriangleIcon theme={appState.theme} flip={!isRTL} />
),
keyBinding: "t",
},
]}
value={getFormValue<Arrowhead | null>(
elements,
@@ -778,6 +789,14 @@ export const actionChangeArrowhead = register({
keyBinding: "r",
icon: <ArrowheadDotIcon theme={appState.theme} flip={isRTL} />,
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: (
<ArrowheadTriangleIcon theme={appState.theme} flip={isRTL} />
),
keyBinding: "t",
},
]}
value={getFormValue<Arrowhead | null>(
elements,

View File

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

View File

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

View File

@@ -1,13 +1,6 @@
import { ExcalidrawElement } from "./element/types";
import { newElementWith } from "./element/mutateElement";
import { getCommonBounds } from "./element";
interface Box {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
import { Box, getCommonBoundingBox } from "./element/bounds";
export interface Alignment {
position: "start" | "center" | "end";
@@ -88,8 +81,3 @@ const calculateTranslation = (
(groupBoundingBox[min] + groupBoundingBox[max]) / 2,
};
};
const getCommonBoundingBox = (elements: ExcalidrawElement[]): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return { minX, minY, maxX, maxY };
};

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -157,6 +157,8 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
shortcuts={["Shift+P", "7"]}
/>
<Shortcut label={t("toolBar.text")} shortcuts={["T", "8"]} />
<Shortcut label={t("toolBar.image")} shortcuts={["9"]} />
<Shortcut label={t("toolBar.library")} shortcuts={["0"]} />
<Shortcut
label={t("helpDialog.editSelectedShape")}
shortcuts={[

View File

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

View File

@@ -1,21 +1,26 @@
import React from "react";
import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene";
import "./HintViewer.scss";
import { AppState } from "../types";
import { isLinearElement, isTextElement } from "../element/typeChecks";
import {
isImageElement,
isLinearElement,
isTextElement,
} from "../element/typeChecks";
import { getShortcutKey } from "../utils";
interface Hint {
interface HintViewerProps {
appState: AppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean;
}
const getHints = ({ appState, elements }: Hint) => {
const getHints = ({ appState, elements, isMobile }: HintViewerProps) => {
const { elementType, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
if (elementType === "arrow" || elementType === "line") {
if (!multiMode) {
return t("hints.linearElement");
@@ -31,7 +36,12 @@ const getHints = ({ appState, elements }: Hint) => {
return t("hints.text");
}
if (appState.elementType === "image" && appState.pendingImageElement) {
return t("hints.placeImage");
}
const selectedElements = getSelectedElements(elements, appState);
if (
isResizing &&
lastPointerDownWith === "mouse" &&
@@ -41,7 +51,9 @@ const getHints = ({ appState, elements }: Hint) => {
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
return t("hints.lockAngle");
}
return t("hints.resize");
return isImageElement(targetElement)
? t("hints.resizeImage")
: t("hints.resize");
}
if (isRotating && lastPointerDownWith === "mouse") {
@@ -65,13 +77,22 @@ const getHints = ({ appState, elements }: Hint) => {
return t("hints.text_editing");
}
if (elementType === "selection" && !selectedElements.length && !isMobile) {
return t("hints.canvasPanning");
}
return null;
};
export const HintViewer = ({ appState, elements }: Hint) => {
export const HintViewer = ({
appState,
elements,
isMobile,
}: HintViewerProps) => {
let hint = getHints({
appState,
elements,
isMobile,
});
if (!hint) {
return null;

View File

@@ -90,7 +90,7 @@
.picker-content {
padding: 0.5rem;
display: grid;
grid-auto-flow: column;
grid-template-columns: repeat(3, auto);
grid-gap: 0.5rem;
border-radius: 4px;
:root[dir="rtl"] & {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ export const LibraryButton: React.FC<{
"zen-mode-visibility--hidden": appState.zenModeEnabled,
},
)}
title={`${capitalizeString(t("toolBar.library"))}9`}
title={`${capitalizeString(t("toolBar.library"))}0`}
style={{ marginInlineStart: "var(--space-factor)" }}
>
<input
@@ -38,7 +38,7 @@ export const LibraryButton: React.FC<{
}}
checked={appState.isLibraryOpen}
aria-label={capitalizeString(t("toolBar.library"))}
aria-keyshortcuts="9"
aria-keyshortcuts="0"
/>
<div className="ToolIcon__icon">{LIBRARY_ICON}</div>
</label>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,12 @@
import clsx from "clsx";
import oc from "open-color";
import React, { useEffect, useRef, useState } from "react";
import { close } from "../components/icons";
import { useEffect, useRef, useState } from "react";
import { MIME_TYPES } from "../constants";
import { t } from "../i18n";
import { useIsMobile } from "../components/App";
import { exportToSvg } from "../scene/export";
import { LibraryItem } from "../types";
import { BinaryFiles, LibraryItem } from "../types";
import "./LibraryUnit.scss";
import { CheckboxItem } from "./CheckboxItem";
// fa-plus
const PLUS_ICON = (
@@ -20,68 +19,72 @@ const PLUS_ICON = (
);
export const LibraryUnit = ({
id,
elements,
pendingElements,
onRemoveFromLibrary,
files,
isPending,
onClick,
selected,
onToggle,
}: {
elements?: LibraryItem;
pendingElements?: LibraryItem;
onRemoveFromLibrary: () => void;
id: LibraryItem["id"] | /** for pending item */ null;
elements?: LibraryItem["elements"];
files: BinaryFiles;
isPending?: boolean;
onClick: () => void;
selected: boolean;
onToggle: (id: string) => void;
}) => {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const elementsToRender = elements || pendingElements;
if (!elementsToRender) {
const node = ref.current;
if (!node) {
return;
}
let svg: SVGSVGElement;
const current = ref.current!;
(async () => {
svg = await exportToSvg(elementsToRender, {
exportBackground: false,
viewBackgroundColor: oc.white,
});
for (const child of ref.current!.children) {
if (child.tagName !== "svg") {
continue;
}
current!.removeChild(child);
if (!elements) {
return;
}
current!.appendChild(svg);
const svg = await exportToSvg(
elements,
{
exportBackground: false,
viewBackgroundColor: oc.white,
},
files,
);
node.innerHTML = svg.outerHTML;
})();
return () => {
if (svg) {
current.removeChild(svg);
}
node.innerHTML = "";
};
}, [elements, pendingElements]);
}, [elements, files]);
const [isHovered, setIsHovered] = useState(false);
const isMobile = useIsMobile();
const adder = (isHovered || isMobile) && pendingElements && (
const adder = isPending && (
<div className="library-unit__adder">{PLUS_ICON}</div>
);
return (
<div
className={clsx("library-unit", {
"library-unit__active": elements || pendingElements,
"library-unit__active": elements,
"library-unit--hover": elements && isHovered,
"library-unit--selected": selected,
})}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
className={clsx("library-unit__dragger", {
"library-unit__pulse": !!pendingElements,
"library-unit__pulse": !!isPending,
})}
ref={ref}
draggable={!!elements}
onClick={!!elements || !!pendingElements ? onClick : undefined}
onClick={!!elements || !!isPending ? onClick : undefined}
onDragStart={(event) => {
setIsHovered(false);
event.dataTransfer.setData(
@@ -91,14 +94,12 @@ export const LibraryUnit = ({
}}
/>
{adder}
{elements && (isHovered || isMobile) && (
<button
className="library-unit__removeFromLibrary"
aria-label={t("labels.removeFromLibrary")}
onClick={onRemoveFromLibrary}
>
{close}
</button>
{id && elements && (isHovered || isMobile || selected) && (
<CheckboxItem
checked={selected}
onChange={() => onToggle(id)}
className="library-unit__checkbox"
/>
)}
</div>
);

View File

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

View File

@@ -33,6 +33,11 @@ type MobileMenuProps = {
renderCustomFooter?: (isMobile: boolean, appState: AppState) => JSX.Element;
viewModeEnabled: boolean;
showThemeBtn: boolean;
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
isMobile: boolean,
appState: AppState,
) => JSX.Element | null;
};
export const MobileMenu = ({
@@ -50,6 +55,8 @@ export const MobileMenu = ({
renderCustomFooter,
viewModeEnabled,
showThemeBtn,
onImageAction,
renderTopRightUI,
}: MobileMenuProps) => {
const renderToolbar = () => {
return (
@@ -65,9 +72,15 @@ export const MobileMenu = ({
canvas={canvas}
elementType={appState.elementType}
setAppState={setAppState}
onImageAction={({ pointerType }) => {
onImageAction({
insertOnCanvasDirectly: pointerType !== "mouse",
});
}}
/>
</Stack.Row>
</Island>
{renderTopRightUI && renderTopRightUI(true, appState)}
<LockButton
checked={appState.elementLocked}
onChange={onLockToggle}
@@ -79,7 +92,7 @@ export const MobileMenu = ({
</Stack.Col>
)}
</Section>
<HintViewer appState={appState} elements={elements} />
<HintViewer appState={appState} elements={elements} isMobile={true} />
</FixedSideContainer>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,429 @@
import { ReactNode, useCallback, useEffect, useState } from "react";
import oc from "open-color";
import { Dialog } from "./Dialog";
import { t } from "../i18n";
import { ToolButton } from "./ToolButton";
import { AppState, LibraryItems, LibraryItem } from "../types";
import { exportToBlob } from "../packages/utils";
import { EXPORT_DATA_TYPES, EXPORT_SOURCE } from "../constants";
import { ExportedLibraryData } from "../data/types";
import "./PublishLibrary.scss";
import { ExcalidrawElement } from "../element/types";
import { newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import { getCommonBoundingBox } from "../element/bounds";
import SingleLibraryItem from "./SingleLibraryItem";
interface PublishLibraryDataParams {
authorName: string;
githubHandle: string;
name: string;
description: string;
twitterHandle: string;
website: string;
}
const LOCAL_STORAGE_KEY_PUBLISH_LIBRARY = "publish-library-data";
const savePublishLibDataToStorage = (data: PublishLibraryDataParams) => {
try {
localStorage.setItem(
LOCAL_STORAGE_KEY_PUBLISH_LIBRARY,
JSON.stringify(data),
);
} catch (error: any) {
// Unable to access window.localStorage
console.error(error);
}
};
const importPublishLibDataFromStorage = () => {
try {
const data = localStorage.getItem(LOCAL_STORAGE_KEY_PUBLISH_LIBRARY);
if (data) {
return JSON.parse(data);
}
} catch (error: any) {
// Unable to access localStorage
console.error(error);
}
return null;
};
const PublishLibrary = ({
onClose,
libraryItems,
appState,
onSuccess,
onError,
updateItemsInStorage,
onRemove,
}: {
onClose: () => void;
libraryItems: LibraryItems;
appState: AppState;
onSuccess: (data: {
url: string;
authorName: string;
items: LibraryItems;
}) => void;
onError: (error: Error) => void;
updateItemsInStorage: (items: LibraryItems) => void;
onRemove: (id: string) => void;
}) => {
const [libraryData, setLibraryData] = useState<PublishLibraryDataParams>({
authorName: "",
githubHandle: "",
name: "",
description: "",
twitterHandle: "",
website: "",
});
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
const data = importPublishLibDataFromStorage();
if (data) {
setLibraryData(data);
}
}, []);
const [clonedLibItems, setClonedLibItems] = useState<LibraryItems>(
libraryItems.slice(),
);
useEffect(() => {
setClonedLibItems(libraryItems.slice());
}, [libraryItems]);
const onInputChange = (event: any) => {
setLibraryData({
...libraryData,
[event.target.name]: event.target.value,
});
};
const onSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setIsSubmitting(true);
const erroredLibItems: LibraryItem[] = [];
let isError = false;
clonedLibItems.forEach((libItem) => {
let error = "";
if (!libItem.name) {
error = t("publishDialog.errors.required");
isError = true;
}
erroredLibItems.push({ ...libItem, error });
});
if (isError) {
setClonedLibItems(erroredLibItems);
setIsSubmitting(false);
return;
}
const elements: ExcalidrawElement[] = [];
const prevBoundingBox = { minX: 0, minY: 0, maxX: 0, maxY: 0 };
clonedLibItems.forEach((libItem) => {
const boundingBox = getCommonBoundingBox(libItem.elements);
const width = boundingBox.maxX - boundingBox.minX + 30;
const height = boundingBox.maxY - boundingBox.minY + 30;
const offset = {
x: prevBoundingBox.maxX - boundingBox.minX,
y: prevBoundingBox.maxY - boundingBox.minY,
};
const itemsWithUpdatedCoords = libItem.elements.map((element) => {
element = mutateElement(element, {
x: element.x + offset.x + 15,
y: element.y + offset.y + 15,
});
return element;
});
const items = [
...itemsWithUpdatedCoords,
newElement({
type: "rectangle",
width,
height,
x: prevBoundingBox.maxX,
y: prevBoundingBox.maxY,
strokeColor: "#ced4da",
backgroundColor: "transparent",
strokeStyle: "solid",
opacity: 100,
roughness: 0,
strokeSharpness: "sharp",
fillStyle: "solid",
strokeWidth: 1,
}),
];
elements.push(...items);
prevBoundingBox.maxX = prevBoundingBox.maxX + width + 30;
});
const png = await exportToBlob({
elements,
mimeType: "image/png",
appState: {
...appState,
viewBackgroundColor: oc.white,
exportBackground: true,
},
files: null,
});
const libContent: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 2,
source: EXPORT_SOURCE,
libraryItems: clonedLibItems,
};
const content = JSON.stringify(libContent, null, 2);
const lib = new Blob([content], { type: "application/json" });
const formData = new FormData();
formData.append("excalidrawLib", lib);
formData.append("excalidrawPng", png!);
formData.append("title", libraryData.name);
formData.append("authorName", libraryData.authorName);
formData.append("githubHandle", libraryData.githubHandle);
formData.append("name", libraryData.name);
formData.append("description", libraryData.description);
formData.append("twitterHandle", libraryData.twitterHandle);
formData.append("website", libraryData.website);
fetch(`${process.env.REACT_APP_LIBRARY_BACKEND}/submit`, {
method: "post",
body: formData,
})
.then(
(response) => {
if (response.ok) {
return response.json().then(({ url }) => {
// flush data from local storage
localStorage.removeItem(LOCAL_STORAGE_KEY_PUBLISH_LIBRARY);
onSuccess({
url,
authorName: libraryData.authorName,
items: clonedLibItems,
});
});
}
return response
.json()
.catch(() => {
throw new Error(response.statusText || "something went wrong");
})
.then((error) => {
throw new Error(
error.message || response.statusText || "something went wrong",
);
});
},
(err) => {
console.error(err);
onError(err);
setIsSubmitting(false);
},
)
.catch((err) => {
console.error(err);
onError(err);
setIsSubmitting(false);
});
};
const renderLibraryItems = () => {
const items: ReactNode[] = [];
clonedLibItems.forEach((libItem, index) => {
items.push(
<div className="single-library-item-wrapper" key={index}>
<SingleLibraryItem
libItem={libItem}
appState={appState}
index={index}
onChange={(val, index) => {
const items = clonedLibItems.slice();
items[index].name = val;
setClonedLibItems(items);
}}
onRemove={onRemove}
/>
</div>,
);
});
return <div className="selected-library-items">{items}</div>;
};
const onDialogClose = useCallback(() => {
updateItemsInStorage(clonedLibItems);
savePublishLibDataToStorage(libraryData);
onClose();
}, [clonedLibItems, onClose, updateItemsInStorage, libraryData]);
const shouldRenderForm = !!libraryItems.length;
return (
<Dialog
onCloseRequest={onDialogClose}
title={t("publishDialog.title")}
className="publish-library"
>
{shouldRenderForm ? (
<form onSubmit={onSubmit}>
<div className="publish-library-note">
{t("publishDialog.noteDescription.pre")}
<a
href="https://libraries.excalidraw.com"
target="_blank"
rel="noopener noreferrer"
>
{t("publishDialog.noteDescription.link")}
</a>{" "}
{t("publishDialog.noteDescription.post")}
</div>
<span className="publish-library-note">
{t("publishDialog.noteGuidelines.pre")}
<a
href="https://github.com/excalidraw/excalidraw-libraries#guidelines"
target="_blank"
rel="noopener noreferrer"
>
{t("publishDialog.noteGuidelines.link")}
</a>
{t("publishDialog.noteGuidelines.post")}
</span>
<div className="publish-library-note">
{t("publishDialog.noteItems")}
</div>
{renderLibraryItems()}
<div className="publish-library__fields">
<label>
<div>
<span>{t("publishDialog.libraryName")}</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
name="name"
required
value={libraryData.name}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.libraryName")}
/>
</label>
<label style={{ alignItems: "flex-start" }}>
<div>
<span>{t("publishDialog.libraryDesc")}</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<textarea
name="description"
rows={4}
required
value={libraryData.description}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.libraryDesc")}
/>
</label>
<label>
<div>
<span>{t("publishDialog.authorName")}</span>
<span aria-hidden="true" className="required">
*
</span>
</div>
<input
type="text"
name="authorName"
required
value={libraryData.authorName}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.authorName")}
/>
</label>
<label>
<span>{t("publishDialog.githubUsername")}</span>
<input
type="text"
name="githubHandle"
value={libraryData.githubHandle}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.githubHandle")}
/>
</label>
<label>
<span>{t("publishDialog.twitterUsername")}</span>
<input
type="text"
name="twitterHandle"
value={libraryData.twitterHandle}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.twitterHandle")}
/>
</label>
<label>
<span>{t("publishDialog.website")}</span>
<input
type="text"
name="website"
pattern="https?://.+"
title={t("publishDialog.errors.website")}
value={libraryData.website}
onChange={onInputChange}
placeholder={t("publishDialog.placeholder.website")}
/>
</label>
<span className="publish-library-note">
{t("publishDialog.noteLicense.pre")}
<a
href="https://github.com/excalidraw/excalidraw-libraries/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
>
{t("publishDialog.noteLicense.link")}
</a>
{t("publishDialog.noteLicense.post")}
</span>
</div>
<div className="publish-library__buttons">
<ToolButton
type="button"
title={t("buttons.cancel")}
aria-label={t("buttons.cancel")}
label={t("buttons.cancel")}
onClick={onDialogClose}
data-testid="cancel-clear-canvas-button"
className="publish-library__buttons--cancel"
/>
<ToolButton
type="submit"
title={t("buttons.submit")}
aria-label={t("buttons.submit")}
label={t("buttons.submit")}
className="publish-library__buttons--confirm"
isLoading={isSubmitting}
/>
</div>
</form>
) : (
<p style={{ padding: "1em", textAlign: "center", fontWeight: 500 }}>
{t("publishDialog.atleastOneLibItem")}
</p>
)}
</Dialog>
);
};
export default PublishLibrary;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@
display: inline-flex;
align-items: center;
position: relative;
font-family: Cascadia;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
border-radius: var(--space-factor);
@@ -54,10 +53,16 @@
}
.ToolIcon__label {
display: flex;
align-items: center;
color: var(--icon-fill-color);
font-family: var(--ui-font);
margin: 0 0.8em;
text-overflow: ellipsis;
.Spinner {
margin-left: 0.6em;
}
}
.ToolIcon_size_small .ToolIcon__icon {

View File

@@ -34,10 +34,8 @@ const updateTooltip = (
width: itemWidth,
} = item.getBoundingClientRect();
const {
width: labelWidth,
height: labelHeight,
} = tooltip.getBoundingClientRect();
const { width: labelWidth, height: labelHeight } =
tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -517,6 +517,27 @@
}
}
input[type="text"],
textarea:not(.excalidraw-wysiwyg) {
color: var(--text-primary-color);
border: 1.5px solid var(--input-border-color);
padding: 0.75rem;
white-space: nowrap;
border-radius: var(--space-factor);
background-color: var(--input-bg-color);
&:not(:focus) {
&:hover {
background-color: var(--input-hover-bg-color);
}
}
&:focus {
outline: none;
box-shadow: 0 0 0 2px var(--focus-highlight-color);
}
}
@media print {
.App-bottom-bar,
.FixedSideContainer,

View File

@@ -16,7 +16,7 @@
--icon-green-fill-color: #{$oc-green-9};
--default-bg-color: #{$oc-white};
--input-bg-color: #{$oc-white};
--input-border-color: #{$oc-gray-3};
--input-border-color: #{$oc-gray-4};
--input-hover-bg-color: #{$oc-gray-1};
--input-label-color: #{$oc-gray-7};
--island-bg-color: rgba(255, 255, 255, 0.96);
@@ -64,6 +64,7 @@
--input-label-color: #{$oc-gray-2};
--island-bg-color: rgba(30, 30, 30, 0.98);
--keybinding-color: #{$oc-gray-6};
--link-color: #{$oc-blue-4};
--overlay-bg-color: #{transparentize($oc-gray-8, 0.88)};
--popup-bg-color: #2c2c2c;
--popup-secondary-bg-color: #222;

View File

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

View File

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

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

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

View File

@@ -4,12 +4,13 @@ import {
fileSave as _fileSave,
FileSystemHandle,
supported as nativeFileSystemSupported,
} from "@dwelle/browser-fs-access";
} from "browser-fs-access";
import { EVENT, MIME_TYPES } from "../constants";
import { AbortError } from "../errors";
import { debounce } from "../utils";
type FILE_EXTENSION =
| "gif"
| "jpg"
| "png"
| "svg"
@@ -17,20 +18,11 @@ type FILE_EXTENSION =
| "excalidraw"
| "excalidrawlib";
const FILE_TYPE_TO_MIME_TYPE: Record<FILE_EXTENSION, string> = {
jpg: "image/jpeg",
png: "image/png",
svg: "image/svg+xml",
json: "application/json",
excalidraw: MIME_TYPES.excalidraw,
excalidrawlib: MIME_TYPES.excalidrawlib,
};
const INPUT_CHANGE_INTERVAL_MS = 500;
export const fileOpen = <M extends boolean | undefined = false>(opts: {
extensions?: FILE_EXTENSION[];
description?: string;
description: string;
multiple?: M;
}): Promise<
M extends false | undefined ? FileWithHandle : FileWithHandle[]
@@ -41,7 +33,7 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
: FileWithHandle[];
const mimeTypes = opts.extensions?.reduce((mimeTypes, type) => {
mimeTypes.push(FILE_TYPE_TO_MIME_TYPE[type]);
mimeTypes.push(MIME_TYPES[type]);
return mimeTypes;
}, [] as string[]);
@@ -102,7 +94,7 @@ export const fileSave = (
name: string;
/** file extension */
extension: FILE_EXTENSION;
description?: string;
description: string;
/** existing FileSystemHandle */
fileHandle?: FileSystemHandle | null;
},

View File

@@ -1,8 +1,11 @@
import decodePng from "png-chunks-extract";
import extractPngChunks from "png-chunks-extract";
import tEXt from "png-chunk-text";
import encodePng from "png-chunks-encode";
import { stringToBase64, encode, decode, base64ToString } from "./encode";
import { EXPORT_DATA_TYPES, MIME_TYPES } from "../constants";
import { PngChunk } from "../types";
export { extractPngChunks };
// -----------------------------------------------------------------------------
// PNG
@@ -28,7 +31,9 @@ const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {
export const getTEXtChunk = async (
blob: Blob,
): Promise<{ keyword: string; text: string } | null> => {
const chunks = decodePng(new Uint8Array(await blobToArrayBuffer(blob)));
const chunks = extractPngChunks(
new Uint8Array(await blobToArrayBuffer(blob)),
);
const metadataChunk = chunks.find((chunk) => chunk.name === "tEXt");
if (metadataChunk) {
return tEXt.decode(metadataChunk.data);
@@ -36,6 +41,28 @@ export const getTEXtChunk = async (
return null;
};
export const findPngChunk = (
chunks: PngChunk[],
name: PngChunk["name"],
/** this makes the search stop before IDAT chunk (before which most
* metadata chunks reside). This is a perf optim. */
breakBeforeIDAT = true,
) => {
let i = 0;
const len = chunks.length;
while (i <= len) {
const chunk = chunks[i];
if (chunk.name === name) {
return chunk;
}
if (breakBeforeIDAT && chunk.name === "IDAT") {
return null;
}
i++;
}
return null;
};
export const encodePngMetadata = async ({
blob,
metadata,
@@ -43,7 +70,9 @@ export const encodePngMetadata = async ({
blob: Blob;
metadata: string;
}) => {
const chunks = decodePng(new Uint8Array(await blobToArrayBuffer(blob)));
const chunks = extractPngChunks(
new Uint8Array(await blobToArrayBuffer(blob)),
);
const metadataChunk = tEXt.encode(
MIME_TYPES.excalidraw,
@@ -57,7 +86,7 @@ export const encodePngMetadata = async ({
// insert metadata before last chunk (iEND)
chunks.splice(-1, 0, metadataChunk);
return new Blob([encodePng(chunks)], { type: "image/png" });
return new Blob([encodePng(chunks)], { type: MIME_TYPES.png });
};
export const decodePngMetadata = async (blob: Blob) => {
@@ -76,7 +105,7 @@ export const decodePngMetadata = async (blob: Blob) => {
throw new Error("FAILED");
}
return await decode(encodedData);
} catch (error) {
} catch (error: any) {
console.error(error);
throw new Error("FAILED");
}
@@ -127,7 +156,7 @@ export const decodeSvgMetadata = async ({ svg }: { svg: string }) => {
throw new Error("FAILED");
}
return await decode(encodedData);
} catch (error) {
} catch (error: any) {
console.error(error);
throw new Error("FAILED");
}

View File

@@ -2,12 +2,12 @@ import {
copyBlobToClipboardAsPng,
copyTextToSystemClipboard,
} from "../clipboard";
import { DEFAULT_EXPORT_PADDING } from "../constants";
import { DEFAULT_EXPORT_PADDING, MIME_TYPES } from "../constants";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { exportToCanvas, exportToSvg } from "../scene/export";
import { ExportType } from "../scene/types";
import { AppState } from "../types";
import { AppState, BinaryFiles } from "../types";
import { canvasToBlob } from "./blob";
import { fileSave, FileSystemHandle } from "./filesystem";
import { serializeAsJSON } from "./json";
@@ -19,6 +19,7 @@ export const exportCanvas = async (
type: ExportType,
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
{
exportBackground,
exportPadding = DEFAULT_EXPORT_PADDING,
@@ -37,18 +38,23 @@ export const exportCanvas = async (
throw new Error(t("alerts.cannotExportEmptyCanvas"));
}
if (type === "svg" || type === "clipboard-svg") {
const tempSvg = await exportToSvg(elements, {
exportBackground,
exportWithDarkMode: appState.exportWithDarkMode,
viewBackgroundColor,
exportPadding,
exportScale: appState.exportScale,
exportEmbedScene: appState.exportEmbedScene && type === "svg",
});
const tempSvg = await exportToSvg(
elements,
{
exportBackground,
exportWithDarkMode: appState.exportWithDarkMode,
viewBackgroundColor,
exportPadding,
exportScale: appState.exportScale,
exportEmbedScene: appState.exportEmbedScene && type === "svg",
},
files,
);
if (type === "svg") {
return await fileSave(
new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }),
new Blob([tempSvg.outerHTML], { type: MIME_TYPES.svg }),
{
description: "Export to SVG",
name,
extension: "svg",
fileHandle,
@@ -60,7 +66,7 @@ export const exportCanvas = async (
}
}
const tempCanvas = exportToCanvas(elements, appState, {
const tempCanvas = await exportToCanvas(elements, appState, files, {
exportBackground,
viewBackgroundColor,
exportPadding,
@@ -76,11 +82,12 @@ export const exportCanvas = async (
await import(/* webpackChunkName: "image" */ "./image")
).encodePngMetadata({
blob,
metadata: serializeAsJSON(elements, appState),
metadata: serializeAsJSON(elements, appState, files, "local"),
});
}
return await fileSave(blob, {
description: "Export to PNG",
name,
extension: "png",
fileHandle,
@@ -88,7 +95,7 @@ export const exportCanvas = async (
} else if (type === "clipboard") {
try {
await copyBlobToClipboardAsPng(blob);
} catch (error) {
} catch (error: any) {
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw error;
}

View File

@@ -1,9 +1,9 @@
import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport } from "../appState";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element";
import { clearElementsForDatabase, clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { AppState, BinaryFiles, LibraryItems } from "../types";
import { isImageFileHandle, loadFromBlob } from "./blob";
import {
@@ -13,16 +13,50 @@ import {
} from "./types";
import Library from "./library";
/**
* Strips out files which are only referenced by deleted elements
*/
const filterOutDeletedFiles = (
elements: readonly ExcalidrawElement[],
files: BinaryFiles,
) => {
const nextFiles: BinaryFiles = {};
for (const element of elements) {
if (
!element.isDeleted &&
"fileId" in element &&
element.fileId &&
files[element.fileId]
) {
nextFiles[element.fileId] = files[element.fileId];
}
}
return nextFiles;
};
export const serializeAsJSON = (
elements: readonly ExcalidrawElement[],
appState: Partial<AppState>,
files: BinaryFiles,
type: "local" | "database",
): string => {
const data: ExportedDataState = {
type: EXPORT_DATA_TYPES.excalidraw,
version: 2,
source: EXPORT_SOURCE,
elements: clearElementsForExport(elements),
appState: cleanAppStateForExport(appState),
elements:
type === "local"
? clearElementsForExport(elements)
: clearElementsForDatabase(elements),
appState:
type === "local"
? cleanAppStateForExport(appState)
: clearAppStateForDatabase(appState),
files:
type === "local"
? filterOutDeletedFiles(elements, files)
: // will be stripped from JSON
undefined,
};
return JSON.stringify(data, null, 2);
@@ -31,8 +65,9 @@ export const serializeAsJSON = (
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
) => {
const serialized = serializeAsJSON(elements, appState);
const serialized = serializeAsJSON(elements, appState, files, "local");
const blob = new Blob([serialized], {
type: MIME_TYPES.excalidraw,
});
@@ -56,15 +91,7 @@ export const loadFromJSON = async (
description: "Excalidraw files",
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidraw", ".png", ".svg"],
mimeTypes: [
MIME_TYPES.excalidraw,
"application/json",
"image/png",
"image/svg+xml",
],
*/
// extensions: ["json", "excalidraw", "png", "svg"],
});
return loadFromBlob(blob, localAppState, localElements);
};
@@ -87,17 +114,16 @@ export const isValidLibrary = (json: any) => {
typeof json === "object" &&
json &&
json.type === EXPORT_DATA_TYPES.excalidrawLibrary &&
json.version === 1
(json.version === 1 || json.version === 2)
);
};
export const saveLibraryAsJSON = async (library: Library) => {
const libraryItems = await library.loadLibrary();
export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => {
const data: ExportedLibraryData = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: 1,
version: 2,
source: EXPORT_SOURCE,
library: libraryItems,
libraryItems,
};
const serialized = JSON.stringify(data, null, 2);
await fileSave(

View File

@@ -1,6 +1,6 @@
import { loadLibraryFromBlob } from "./blob";
import { LibraryItems, LibraryItem } from "../types";
import { restoreElements } from "./restore";
import { restoreElements, restoreLibraryItems } from "./restore";
import { getNonDeletedElements } from "../element";
import type App from "../components/App";
@@ -18,14 +18,16 @@ class Library {
};
restoreLibraryItem = (libraryItem: LibraryItem): LibraryItem | null => {
const elements = getNonDeletedElements(restoreElements(libraryItem, null));
return elements.length ? elements : null;
const elements = getNonDeletedElements(
restoreElements(libraryItem.elements, null),
);
return elements.length ? { ...libraryItem, elements } : null;
};
/** imports library (currently merges, removing duplicates) */
async importLibrary(blob: Blob) {
async importLibrary(blob: Blob, defaultStatus = "unpublished") {
const libraryFile = await loadLibraryFromBlob(blob);
if (!libraryFile || !libraryFile.library) {
if (!libraryFile || !(libraryFile.libraryItems || libraryFile.library)) {
return;
}
@@ -37,17 +39,17 @@ class Library {
targetLibraryItem: LibraryItem,
) => {
return !existingLibraryItems.find((libraryItem) => {
if (libraryItem.length !== targetLibraryItem.length) {
if (libraryItem.elements.length !== targetLibraryItem.elements.length) {
return false;
}
// detect z-index difference by checking the excalidraw elements
// are in order
return libraryItem.every((libItemExcalidrawItem, idx) => {
return libraryItem.elements.every((libItemExcalidrawItem, idx) => {
return (
libItemExcalidrawItem.id === targetLibraryItem[idx].id &&
libItemExcalidrawItem.id === targetLibraryItem.elements[idx].id &&
libItemExcalidrawItem.versionNonce ===
targetLibraryItem[idx].versionNonce
targetLibraryItem.elements[idx].versionNonce
);
});
});
@@ -55,15 +57,20 @@ class Library {
const existingLibraryItems = await this.loadLibrary();
const filtered = libraryFile.library!.reduce((acc, libraryItem) => {
const restoredItem = this.restoreLibraryItem(libraryItem);
const library = libraryFile.libraryItems || libraryFile.library || [];
const restoredLibItems = restoreLibraryItems(
library,
defaultStatus as "published" | "unpublished",
);
const filteredItems = [];
for (const item of restoredLibItems) {
const restoredItem = this.restoreLibraryItem(item as LibraryItem);
if (restoredItem && isUniqueitem(existingLibraryItems, restoredItem)) {
acc.push(restoredItem);
filteredItems.push(restoredItem);
}
return acc;
}, [] as Mutable<LibraryItems>);
}
await this.saveLibrary([...existingLibraryItems, ...filtered]);
await this.saveLibrary([...filteredItems, ...existingLibraryItems]);
}
loadLibrary = (): Promise<LibraryItems> => {
@@ -90,7 +97,7 @@ class Library {
this.libraryCache = JSON.parse(JSON.stringify(items));
resolve(items);
} catch (error) {
} catch (error: any) {
console.error(error);
resolve([]);
}
@@ -105,7 +112,7 @@ class Library {
// immediately
this.libraryCache = JSON.parse(serializedItems);
await this.app.props.onLibraryChange?.(items);
} catch (error) {
} catch (error: any) {
this.libraryCache = prevLibraryItems;
throw error;
}

View File

@@ -1,5 +1,5 @@
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";
import { AppState, BinaryFiles } from "../types";
import { exportCanvas } from ".";
import { getNonDeletedElements } from "../element";
import { getFileHandleType, isImageFileHandleType } from "./blob";
@@ -7,6 +7,7 @@ import { getFileHandleType, isImageFileHandleType } from "./blob";
export const resaveAsImageWithScene = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
files: BinaryFiles,
) => {
const { exportBackground, viewBackgroundColor, name, fileHandle } = appState;
@@ -26,6 +27,7 @@ export const resaveAsImageWithScene = async (
fileHandleType,
getNonDeletedElements(elements),
appState,
files,
{
exportBackground,
viewBackgroundColor,

View File

@@ -3,7 +3,12 @@ import {
ExcalidrawSelectionElement,
FontFamilyValues,
} from "../element/types";
import { AppState, NormalizedZoomValue } from "../types";
import {
AppState,
BinaryFiles,
LibraryItem,
NormalizedZoomValue,
} from "../types";
import { ImportedDataState } from "./types";
import {
getElementMap,
@@ -37,6 +42,7 @@ export const AllowedExcalidrawElementTypes: Record<
diamond: true,
ellipse: true,
line: true,
image: true,
arrow: true,
freedraw: true,
};
@@ -44,6 +50,7 @@ export const AllowedExcalidrawElementTypes: Record<
export type RestoredDataState = {
elements: ExcalidrawElement[];
appState: RestoredAppState;
files: BinaryFiles;
};
const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
@@ -57,16 +64,19 @@ const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
const restoreElementWithProperties = <
T extends ExcalidrawElement,
K extends keyof Omit<
Required<T>,
Exclude<keyof ExcalidrawElement, "type" | "x" | "y">
>
K extends Pick<T, keyof Omit<Required<T>, keyof ExcalidrawElement>>,
>(
element: Required<T>,
extra: Pick<T, K>,
extra: Pick<
T,
// This extra Pick<T, keyof K> ensure no excess properties are passed.
// @ts-ignore TS complains here but type checks the call sites fine.
keyof K
> &
Partial<Pick<ExcalidrawElement, "type" | "x" | "y">>,
): T => {
const base: Pick<T, keyof ExcalidrawElement> = {
type: (extra as Partial<T>).type || element.type,
type: extra.type || element.type,
// all elements must have version > 0 so getSceneVersion() will pick up
// newly added elements
version: element.version || 1,
@@ -79,8 +89,8 @@ const restoreElementWithProperties = <
roughness: element.roughness ?? 1,
opacity: element.opacity == null ? 100 : element.opacity,
angle: element.angle || 0,
x: (extra as Partial<T>).x ?? element.x ?? 0,
y: (extra as Partial<T>).y ?? element.y ?? 0,
x: extra.x ?? element.x ?? 0,
y: extra.y ?? element.y ?? 0,
strokeColor: element.strokeColor,
backgroundColor: element.backgroundColor,
width: element.width || 0,
@@ -93,25 +103,24 @@ const restoreElementWithProperties = <
boundElementIds: element.boundElementIds ?? [],
};
return ({
return {
...base,
...getNormalizedDimensions(base),
...extra,
} as unknown) as T;
} as unknown as T;
};
const restoreElement = (
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
): typeof element => {
): typeof element | null => {
switch (element.type) {
case "text":
let fontSize = element.fontSize;
let fontFamily = element.fontFamily;
if ("font" in element) {
const [fontPx, _fontFamily]: [
string,
string,
] = (element as any).font.split(" ");
const [fontPx, _fontFamily]: [string, string] = (
element as any
).font.split(" ");
fontSize = parseInt(fontPx, 10);
fontFamily = getFontFamilyByName(_fontFamily);
}
@@ -131,6 +140,12 @@ const restoreElement = (
pressures: element.pressures,
});
}
case "image":
return restoreElementWithProperties(element, {
status: element.status || "pending",
fileId: element.fileId,
scale: element.scale || [1, 1],
});
case "line":
// @ts-ignore LEGACY type
// eslint-disable-next-line no-fallthrough
@@ -194,7 +209,7 @@ export const restoreElements = (
// filtering out selection, which is legacy, no longer kept in elements,
// and causing issues if retained
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement = restoreElement(element);
let migratedElement: ExcalidrawElement | null = restoreElement(element);
if (migratedElement) {
const localElement = localElementsMap?.[element.id];
if (localElement && localElement.version > migratedElement.version) {
@@ -260,5 +275,33 @@ export const restore = (
return {
elements: restoreElements(data?.elements, localElements),
appState: restoreAppState(data?.appState, localAppState || null),
files: data?.files || {},
};
};
export const restoreLibraryItems = (
libraryItems: NonOptional<ImportedDataState["libraryItems"]>,
defaultStatus: LibraryItem["status"],
) => {
const restoredItems: LibraryItem[] = [];
for (const item of libraryItems) {
// migrate older libraries
if (Array.isArray(item)) {
restoredItems.push({
status: defaultStatus,
elements: item,
id: randomId(),
created: Date.now(),
});
} else {
const _item = item as MarkOptional<LibraryItem, "id" | "status">;
restoredItems.push({
..._item,
id: _item.id || randomId(),
status: _item.status || defaultStatus,
created: _item.created || Date.now(),
});
}
}
return restoredItems;
};

View File

@@ -1,5 +1,5 @@
import { ExcalidrawElement } from "../element/types";
import { AppState, LibraryItems } from "../types";
import { AppState, BinaryFiles, LibraryItems, LibraryItems_v1 } from "../types";
import type { cleanAppStateForExport } from "../appState";
export interface ExportedDataState {
@@ -8,6 +8,7 @@ export interface ExportedDataState {
source: string;
elements: readonly ExcalidrawElement[];
appState: ReturnType<typeof cleanAppStateForExport>;
files: BinaryFiles | undefined;
}
export interface ImportedDataState {
@@ -17,14 +18,18 @@ export interface ImportedDataState {
elements?: readonly ExcalidrawElement[] | null;
appState?: Readonly<Partial<AppState>> | null;
scrollToContent?: boolean;
libraryItems?: LibraryItems;
libraryItems?: LibraryItems | LibraryItems_v1;
files?: BinaryFiles;
}
export interface ExportedLibraryData {
type: string;
version: number;
version: 2;
source: string;
library: LibraryItems;
libraryItems: LibraryItems;
}
export interface ImportedLibraryData extends Partial<ExportedLibraryData> {}
export interface ImportedLibraryData extends Partial<ExportedLibraryData> {
/** @deprecated v1 */
library?: LibraryItems;
}

View File

@@ -137,14 +137,13 @@ export const bindOrUnbindSelectedElements = (
const maybeBindBindableElement = (
bindableElement: NonDeleted<ExcalidrawBindableElement>,
): void => {
getElligibleElementsForBindableElementAndWhere(
bindableElement,
).forEach(([linearElement, where]) =>
bindOrUnbindLinearElement(
linearElement,
where === "end" ? "keep" : bindableElement,
where === "start" ? "keep" : bindableElement,
),
getElligibleElementsForBindableElementAndWhere(bindableElement).forEach(
([linearElement, where]) =>
bindOrUnbindLinearElement(
linearElement,
where === "end" ? "keep" : bindableElement,
where === "start" ? "keep" : bindableElement,
),
);
};
@@ -293,9 +292,11 @@ export const updateBoundElements = (
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
simultaneouslyUpdated,
);
(Scene.getScene(changedElement)!.getNonDeletedElements(
boundElementIds,
) as NonDeleted<ExcalidrawLinearElement>[]).forEach((linearElement) => {
(
Scene.getScene(changedElement)!.getNonDeletedElements(
boundElementIds,
) as NonDeleted<ExcalidrawLinearElement>[]
).forEach((linearElement) => {
const bindableElement = changedElement as ExcalidrawBindableElement;
// In case the boundElementIds are stale
if (!doesNeedUpdate(linearElement, bindableElement)) {
@@ -580,9 +581,11 @@ export const fixBindingsAfterDuplication = (
});
// Update the linear elements
(sceneElements.filter(({ id }) =>
allBoundElementIds.has(id),
) as ExcalidrawLinearElement[]).forEach((element) => {
(
sceneElements.filter(({ id }) =>
allBoundElementIds.has(id),
) as ExcalidrawLinearElement[]
).forEach((element) => {
const { startBinding, endBinding } = element;
mutateElement(element, {
startBinding: newBindingAfterDuplication(
@@ -642,17 +645,17 @@ export const fixBindingsAfterDeletion = (
});
}
});
(sceneElements.filter(({ id }) =>
boundElementIds.has(id),
) as ExcalidrawLinearElement[]).forEach(
(element: ExcalidrawLinearElement) => {
const { startBinding, endBinding } = element;
mutateElement(element, {
startBinding: newBindingAfterDeletion(startBinding, deletedElementIds),
endBinding: newBindingAfterDeletion(endBinding, deletedElementIds),
});
},
);
(
sceneElements.filter(({ id }) =>
boundElementIds.has(id),
) as ExcalidrawLinearElement[]
).forEach((element: ExcalidrawLinearElement) => {
const { startBinding, endBinding } = element;
mutateElement(element, {
startBinding: newBindingAfterDeletion(startBinding, deletedElementIds),
endBinding: newBindingAfterDeletion(endBinding, deletedElementIds),
});
});
};
const newBindingAfterDeletion = (

View File

@@ -3,6 +3,7 @@ import {
ExcalidrawLinearElement,
Arrowhead,
ExcalidrawFreeDrawElement,
NonDeleted,
} from "./types";
import { distance2d, rotate } from "../math";
import rough from "roughjs/bin/rough";
@@ -78,7 +79,7 @@ const getMinMaxXYFromCurvePathOps = (
// move, bcurveTo, lineTo, and curveTo
if (op === "move") {
// change starting point
currentP = (data as unknown) as Point;
currentP = data as unknown as Point;
// move operation does not draw anything; so, it always
// returns false
} else if (op === "bcurveTo") {
@@ -227,7 +228,7 @@ export const getArrowheadPoints = (
const prevOp = ops[index - 1];
let p0: Point = [0, 0];
if (prevOp.op === "move") {
p0 = (prevOp.data as unknown) as Point;
p0 = prevOp.data as unknown as Point;
} else if (prevOp.op === "bcurveTo") {
p0 = [prevOp.data[4], prevOp.data[5]];
}
@@ -258,6 +259,7 @@ export const getArrowheadPoints = (
arrow: 30,
bar: 15,
dot: 15,
triangle: 15,
}[arrowhead]; // pixels (will differ for each arrowhead)
let length = 0;
@@ -294,6 +296,7 @@ export const getArrowheadPoints = (
const angle = {
arrow: 20,
bar: 90,
triangle: 25,
}[arrowhead]; // degrees
// Return points
@@ -511,3 +514,17 @@ export const getClosestElementBounds = (
return getElementBounds(closestElement);
};
export interface Box {
minX: number;
minY: number;
maxX: number;
maxY: number;
}
export const getCommonBoundingBox = (
elements: ExcalidrawElement[] | readonly NonDeleted<ExcalidrawElement>[],
): Box => {
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
return { minX, minY, maxX, maxY };
};

View File

@@ -23,6 +23,7 @@ import {
ExcalidrawEllipseElement,
NonDeleted,
ExcalidrawFreeDrawElement,
ExcalidrawImageElement,
} from "./types";
import { getElementAbsoluteCoords, getCurvePathOps, Bounds } from "./bounds";
@@ -30,6 +31,7 @@ import { Point } from "../types";
import { Drawable } from "roughjs/bin/core";
import { AppState } from "../types";
import { getShapeForElement } from "../renderer/renderElement";
import { isImageElement } from "./typeChecks";
const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement,
@@ -47,8 +49,7 @@ const isElementDraggableFromInside = (
if (element.type === "line") {
return isDraggableFromInside && isPathALoop(element.points);
}
return isDraggableFromInside;
return isDraggableFromInside || isImageElement(element);
};
export const hitTest = (
@@ -161,6 +162,7 @@ type HitTestArgs = {
const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
switch (args.element.type) {
case "rectangle":
case "image":
case "text":
case "diamond":
case "ellipse":
@@ -195,6 +197,7 @@ export const distanceToBindableElement = (
): number => {
switch (element.type) {
case "rectangle":
case "image":
case "text":
return distanceToRectangle(element, point);
case "diamond":
@@ -224,7 +227,8 @@ const distanceToRectangle = (
element:
| ExcalidrawRectangleElement
| ExcalidrawTextElement
| ExcalidrawFreeDrawElement,
| ExcalidrawFreeDrawElement
| ExcalidrawImageElement,
point: Point,
): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point);
@@ -486,6 +490,7 @@ export const determineFocusDistance = (
const nabs = Math.abs(n);
switch (element.type) {
case "rectangle":
case "image":
case "text":
return c / (hwidth * (nabs + q * mabs));
case "diamond":
@@ -516,6 +521,7 @@ export const determineFocusPoint = (
let point;
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "diamond":
point = findFocusPointForRectangulars(element, focus, adjecentPointRel);
@@ -565,6 +571,7 @@ const getSortedElementLineIntersections = (
let intersections: GA.Point[];
switch (element.type) {
case "rectangle":
case "image":
case "text":
case "diamond":
const corners = getCorners(element);
@@ -598,6 +605,7 @@ const getSortedElementLineIntersections = (
const getCorners = (
element:
| ExcalidrawRectangleElement
| ExcalidrawImageElement
| ExcalidrawDiamondElement
| ExcalidrawTextElement,
scale: number = 1,
@@ -606,6 +614,7 @@ const getCorners = (
const hy = (scale * element.height) / 2;
switch (element.type) {
case "rectangle":
case "image":
case "text":
return [
GA.point(hx, hy),
@@ -747,6 +756,7 @@ export const findFocusPointForEllipse = (
export const findFocusPointForRectangulars = (
element:
| ExcalidrawRectangleElement
| ExcalidrawImageElement
| ExcalidrawDiamondElement
| ExcalidrawTextElement,
// Between -1 and 1 for how far away should the focus point be relative
@@ -856,7 +866,7 @@ const hitTestRoughShape = (
// move, bcurveTo, lineTo, and curveTo
if (op === "move") {
// change starting point
currentP = (data as unknown) as Point;
currentP = data as unknown as Point;
// move operation does not draw anything; so, it always
// returns false
} else if (op === "bcurveTo") {

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