Compare commits

..

81 Commits

Author SHA1 Message Date
Aakansha Doshi f68f4cb9e0 docs: release @excalidraw/excalidraw@0.14.2 🎉 (#6181) 2023-02-01 19:08:56 +05:30
Aakansha Doshi 71fb573750 fix: horizontal padding when aligning bound text containers (#6180)
* fix: horizontal padding when aligning bound text containers

* Add specs

* fix
2023-02-01 18:54:08 +05:30
David Luzar 7562d9b533 fix: make tunnels work in multi-instance scenarios (#6178)
* fix: make tunnels work in multi-instance scenarios

* factor tunnels out

* use tunnel-rat fork until upsteam updated
2023-02-01 10:46:17 +05:30
David Luzar e6de1fe4a4 feat: rewrite public UI component rendering using tunnels (#6117)
* feat: rewrite public UI component rendering using tunnels

* factor out into components

* comments

* fix variable naming

* fix not hiding welcomeScreen

* factor out AppFooter and memoize components

* remove `UIOptions.welcomeScreen` and render only from host app

* factor out tunnels into own file

* update changelog. Keep `UIOptions.welcomeScreen` as deprecated

* update changelog

* lint

---------

Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-01-31 13:53:20 +01:00
Aakansha Doshi 3a141ca77a fix: add 1px width to the container to calculate more accurately (#6174)
* fix: add 1px width to the container to calculate accurately

* fix tests
2023-01-30 18:52:56 +05:30
JUNYI OU 5ae39c9292 fix: quick typo fix (#6167) 2023-01-29 14:22:25 +01:00
Aakansha Doshi e41ea9562b fix: set the width correctly using measureText in editor (#6162) 2023-01-28 12:09:53 +01:00
Ignacio Cuadra b52c8943e4 fix: 🐛 broken emojis when wrap text (#6153)
* fix: 🐛 broken emojis when wrap text

* refactor: Delete unnecessary "else" (reduce indentation)

* fix: remove code block that causes the emojis to disappear

* Apply suggestions from code review

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

* fix: 🚑 possibly undefined value

* Add spec

Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2023-01-26 11:49:21 +05:30
Aakansha Doshi cf38c0f933 fix: declare css variable for font in excalidraw so its available in host (#6160)
declar css variable for font in excalidraw so its available in host
2023-01-25 15:44:20 +05:30
David Luzar 1db078a3dc feat: close MainMenu and Library dropdown on item select (#6152) 2023-01-23 16:54:35 +01:00
David Luzar d4afd66268 feat: add hand/panning tool (#6141)
* feat: add hand/panning tool

* move hand tool right of tool lock separator

* tweak i18n

* rename `panning` -> `hand`

* toggle between last tool and hand on `H` shortcut

* hide properties sidebar when `hand` active

* revert to rendering HandButton manually due to mobile toolbar
2023-01-23 16:12:28 +01:00
David Luzar 849e6a0c86 fix: button background and svg sizes (#6155)
* fix: button background color fallback

* fix svg width/height
2023-01-23 16:10:04 +01:00
Nishant-l f03f5c948d style: change in ExportButton style (#6147) (#6148)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-01-22 12:37:18 +00:00
David Luzar d2b698093c feat: show copy-as-png export button on firefox and show steps how to enable it (#6125)
* feat: hide copy-as-png shortcut from help dialog if not supported

* fix: support firefox if clipboard.write supported

* show shrotcut in firefox and instead show error message how to enable the flag support

* widen to TypeError because minification

* show copy-as-png on firefox even if it will throw
2023-01-22 12:33:15 +01:00
Excalidraw Bot 0f1720be61 chore: Update translations from Crowdin (#6077) 2023-01-22 12:19:21 +01:00
David Luzar d0b33d35db build: temporarily disable pre-commit (#6132) 2023-01-19 13:50:42 +01:00
Aakansha Doshi d6a5ef1936 docs: release @excalidraw/excalidraw@0.14.1 🎉 (#6112) 2023-01-16 16:08:03 +05:30
Aakansha Doshi c7a11f5cd2 docs: release @excalidraw/excalidraw@0.14.0 🎉 (#6109) 2023-01-13 16:08:29 +05:30
Aakansha Doshi 893c487add fix: remove overflow hidden from button (#6110)
remove overflow hidden from button
2023-01-13 15:44:33 +05:30
Aakansha Doshi 99fdffdab7 fix: mobile tools positioning (#6107)
* fix: mobile tools positioning

* add var for padding

* use css var

* new line

* stupid mistake

* lint
2023-01-13 00:57:25 +05:30
Aakansha Doshi faad8a65f1 feat: new Live Collaboration Component API (#6104)
* feat: new Live Collaboration Component API

* namespace export icons into `icons` dictionary and lowercase

* update readme and changelog

* review fixes

* fix

* fix

* update docs

* remove

* allow button rest props

* update docs

* docs

* add `WelcomeScreen.Center.MenuItemLiveCollaborationTrigger`

* fix lint

* update changelog

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-01-12 23:28:57 +05:30
Aakansha Doshi 9d04479f98 fix: renamed folder MainMenu->main-menu and support rest props (#6103)
* renamed folder MainMenu -> main-menu

* rename ariaLabel -> aria-label and dataTestId -> data-testid

* allow rest props

* fix

* lint

* add ts check

* ts for div

* fix

* fix

* fix
2023-01-12 20:40:09 +05:30
David Luzar 599a8f3c6f feat: support WelcomeScreen customization API (#6048) 2023-01-12 15:49:28 +01:00
David Luzar 0982da38fe feat: render unknown supplied children to UI (#6096) 2023-01-12 15:20:16 +01:00
Barnabás Molnár 699897f71b feat: generic button export (#6092)
Co-authored-by: dwelle <luzar.david@gmail.com>
2023-01-12 13:06:00 +01:00
Aakansha Doshi 328ff6c32d fix: use position absolute for mobile misc tools (#6099) 2023-01-11 19:47:40 +05:30
David Luzar 618442299f fix: React.memo resolvers not accounting for all props (#6042) 2023-01-09 10:24:17 +01:00
Antonio Della Fortuna 06b45e0cfc fix: image horizontal flip fix + improved tests (#5799)
Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
fixes https://github.com/excalidraw/excalidraw/issues/5784
2023-01-08 16:19:13 +00:00
David Luzar 809d5ba17f fix: png-exporting does not preserve angles correctly for flipped images (#6085)
* fix: png-exporting does not preserve angles correctly for flipped images

* refactor related code

* simplify further and comment
2023-01-08 16:22:04 +01:00
David Luzar 40d53d9231 fix: stale appState of MainMenu defaultItems rendered from Actions (#6074) 2023-01-06 14:32:55 +01:00
dependabot[bot] 9803a85381 build(deps): bump loader-utils from 2.0.3 to 2.0.4 in /src/packages/excalidraw (#5892)
build(deps): bump loader-utils in /src/packages/excalidraw

Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 12:31:06 +05:30
dependabot[bot] 72784f9d29 build(deps): bump loader-utils from 2.0.3 to 2.0.4 (#5905)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 2.0.3 to 2.0.4.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v2.0.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v2.0.3...v2.0.4)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 12:30:43 +05:30
dependabot[bot] e3249f930c build(deps): bump json5 from 1.0.1 to 1.0.2 (#6076)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 06:59:44 +00:00
dependabot[bot] cbe0d34f1a build(deps): bump decode-uri-component from 0.2.0 to 0.2.2 (#5963)
Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases)
- [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2)

---
updated-dependencies:
- dependency-name: decode-uri-component
  dependency-type: indirect
...

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 12:26:23 +05:30
dependabot[bot] bed8093e47 build(deps): bump json5 from 2.2.1 to 2.2.3 in /dev-docs (#6060)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 12:25:48 +05:30
dependabot[bot] 1255ca2e84 build(deps): bump json5 from 2.2.1 to 2.2.3 in /src/packages/utils (#6061)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 12:25:35 +05:30
dependabot[bot] 14d02dcaea build(deps): bump json5 from 2.2.1 to 2.2.3 in /src/packages/excalidraw (#6062)
Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.3)

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-06 12:25:20 +05:30
Excalidraw Bot 9747223705 chore: Update translations from Crowdin (#6052)
* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Hindi)

* New translations en.json (Marathi)

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Galician)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* New translations en.json (French)

* New translations en.json (Spanish)

* New translations en.json (Arabic)

* New translations en.json (Bulgarian)

* New translations en.json (Catalan)

* New translations en.json (Czech)

* New translations en.json (Danish)

* New translations en.json (German)

* New translations en.json (Greek)

* New translations en.json (Basque)

* New translations en.json (Finnish)

* New translations en.json (Hebrew)

* New translations en.json (Hungarian)

* New translations en.json (Italian)

* New translations en.json (Japanese)

* New translations en.json (Korean)

* New translations en.json (Kurdish)

* New translations en.json (Lithuanian)

* New translations en.json (Dutch)

* New translations en.json (Punjabi)

* New translations en.json (Polish)

* New translations en.json (Portuguese)

* New translations en.json (Russian)

* New translations en.json (Slovak)

* New translations en.json (Slovenian)

* New translations en.json (Swedish)

* New translations en.json (Turkish)

* New translations en.json (Ukrainian)

* New translations en.json (Chinese Simplified)

* New translations en.json (Chinese Traditional)

* New translations en.json (Vietnamese)

* New translations en.json (Galician)

* New translations en.json (Portuguese, Brazilian)

* New translations en.json (Indonesian)

* New translations en.json (Persian)

* New translations en.json (Tamil)

* New translations en.json (Bengali)

* New translations en.json (Marathi)

* New translations en.json (Norwegian Nynorsk)

* New translations en.json (Kazakh)

* New translations en.json (Latvian)

* New translations en.json (Hindi)

* New translations en.json (Burmese)

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

* New translations en.json (Sinhala)

* New translations en.json (Norwegian Bokmal)

* New translations en.json (Occitan)

* New translations en.json (Kabyle)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Simplified)

* Auto commit: Calculate translation coverage

* New translations en.json (Chinese Traditional)

* New translations en.json (Chinese Traditional)

* New translations en.json (Norwegian Bokmal)

* Auto commit: Calculate translation coverage

* New translations en.json (Latvian)

* Auto commit: Calculate translation coverage

* New translations en.json (Romanian)

* Auto commit: Calculate translation coverage

* New translations en.json (Slovenian)

* Auto commit: Calculate translation coverage

* New translations en.json (Spanish)

* New translations en.json (Russian)

* Auto commit: Calculate translation coverage

* New translations en.json (German)

* Auto commit: Calculate translation coverage

* New translations en.json (Vietnamese)

* Auto commit: Calculate translation coverage

* New translations en.json (Hindi)

* Auto commit: Calculate translation coverage

* New translations en.json (Dutch)

* Auto commit: Calculate translation coverage

* New translations en.json (Marathi)

* Auto commit: Calculate translation coverage

* New translations en.json (Latvian)

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (French)

* Auto commit: Calculate translation coverage

* New translations en.json (Portuguese, Brazilian)

* Auto commit: Calculate translation coverage

* New translations en.json (Japanese)

* Auto commit: Calculate translation coverage
2023-01-06 12:23:14 +05:30
EternalWill43 0f11f7da15 fix: HelpDialog (#6072) 2023-01-05 17:43:19 +01:00
Aakansha Doshi 8420aecb34 feat: new Menu Component API (#6034)
* feat: new Menu Component API

* allow valid children types

* introduce menu group to group items

* Add lang footer

* use display name

* displayName

* define types inside

* fix default menu

* add json export to menu

* fix

* simplify expression

* put open menu into own compo to optimize perf

So that we don't rerun `useOutsideClickHook` (and rebind event listeners
all the time)

* naming tweaks

* rename MenuComponents->MenuDefaultItems and export default items from Menu.Items

* import Menu.scss in Menu.tsx

* move menu scss to excal app

* Don't filter children inside menu group

* move E+ out of socials

* support style prop for MenuItem and MenuGroup

* Support header in menu group and add Excalidraw links header for default items in social section

* rename header to title

* fix padding for lang

* render menu in mobile

* review fixes

* tweaks

* Export collaborators and show in mobile menu

* revert .env

* lint :p

* again lint

* show correct actions in view mode for mobile

* Whitelist Collaborators Comp

* mobile styling

* padding

* don't show nerds when menu open in mobile

* lint :(

* hide shortcuts

* refactor userlist to support mobile and keep a wrapper comp for excal app

* use only UserList

* render only on mobile for default items

* remove unused hooks

* Show collab button in menu when onCollabButtonClick present and hide export when UIOptions.canvasActions.export is false

* fix tests

* lint

* inject userlist inside menu on mobile

* revert userlist

* move menu socials to default menu

* fix collab

* use meny in library

* Make Menu generic and create hamburgemenu for public excal menu and use menu in library as well

* use appState.openMenu for mobile

* fix tests

* styling fixes and support style and class name in menu content

* fix test

* rename MenuDefaultItems->DefaultItems

* move footer css to its own comp

* rename HamburgerMenu -> MainMenu

* rename menu -> dropdownMenu and update classes, onClick->onToggle

* close main menu when dialog closes

* by bye filtering

* update docs

* fix lint

* update example, docs for useDevice and footer in mobile, rename menu ->DropDownMenu everywhere

* spec

* remove isMenuOpenAtom and set openMenu as canvas for main menu, render decreases in specs :)

* [temp] remove cyclic depenedency to fix build

* hack- update appstate to sync lang change

* Add more specs

* wip: rewrite MainMenu footer

* fix margin

* fix snaps

* not needed as lang list no more imported

* simplify custom footer rendering

* Add DropdownMenuItemLink and DropdownMenuItemCustom and update API, docs

* fix `MainMenu.ItemCustom`

* naming

* use onSelect and base class for custom items

* fix lint

* fix snap

* use custom item for lang

* update docs

* fix

* properly use `MainMenu.ItemCustom` for `LanguageList`

* add margin top to custom items

* flex

Co-authored-by: dwelle <luzar.david@gmail.com>
2023-01-05 22:04:23 +05:30
David Luzar 08afb857c3 fix: show error message on collab save failure (#6063)
* fix: show error message on collab save failure

* comment
2023-01-02 23:53:51 +01:00
David Luzar 9230c8f4d2 fix: remove ga from docker build (#6059)
* fix: remove ga from docker build

* lint

* fix debug
2023-01-02 21:21:57 +01:00
Excalidraw Bot dba8f812f1 chore: Update translations from Crowdin (#5807)
Co-authored-by: David Luzar <luzar.david@gmail.com>
2023-01-01 17:01:46 +00:00
DanielJGeiger fdd8552637 feat: Scroll using PageUp and PageDown (#6038)
* feat: Scroll using PageUp and PageDown

* support x-axis via `shift` & enable in viewMode

* tweak test

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-12-31 15:54:37 -06:00
Aakansha Doshi c8370b394c fix: use displayName since name gets stripped off when uglifying/minifiyng in production (#6036)
fix: use displayName since name gets stripped off when uglifying/minifiy in production
2022-12-27 15:17:13 +05:30
David Luzar 5fcf6a4845 fix: remove background from wysiwyg when editing arrow label (#6033)
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
2022-12-23 19:40:52 +01:00
Aakansha Doshi af3b93c410 fix: use canvas measureText to calculate width in measureText (#6030)
* fix: use canvas measureText to calculate width in measureText

* calculate multiline width correctly using canvas measure text and rename functions

* set correct width when pasting in bound container

* take existing value + new pasted

* remove debugger :p

* fix snaps
2022-12-23 21:45:49 +05:30
David Luzar 2595e0de82 fix: restoring deleted bindings (#6029)
* fix: restoring deleted bindings

* add tests

* add one more test

* merge restore tests files
2022-12-23 11:48:14 +01:00
Aakansha Doshi 8ec5f7b982 feat: support shrinking text containers to original height when text removed (#6025)
* fix:cache bind text containers height so that it could autoshrink to original height when text deleted

* revert

* rename

* reset cache when resized

* safe check

* restore original containr height when text is unbind

* update cache when redrawing bounding box

* reset cache when unbind

* make type-safe

* add specs

* skip one test

* remoe mock

* fix

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-12-23 11:57:48 +05:30
David Luzar 9086674b27 chore: bump typescript @ 4.9.4 (#6024) 2022-12-22 19:32:21 +01:00
zsviczian 6273d56524 fix: ColorPicker getColor (#5949)
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-12-22 12:53:49 +00:00
David Luzar 7e135c4e22 feat: move contextMenu into the component tree and control via appState (#6021) 2022-12-21 12:47:09 +01:00
Aakansha Doshi b704705ed8 feat: render footer as a component instead of render prop (#5970)
* feat: render footer as a component instead of render prop

* Export FooterCenter as footer

* remove useDevice export

* revert some changes

* remove

* add spec

* update specs

* parse children into a dictionary

* factor app footer components into a single file

* Add docs

* split app footer components

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-12-21 14:29:06 +05:30
Aakansha Doshi d2e371cdf0 fix: don't push whitespace to next line when exceeding max width during wrapping and make sure to use same width of text editor on DOM when measuring dimensions (#5996)
* fix: don't push whitespace to next line when exceeding max width during wrapping

* add a helper function and never push empty line

* use width same as in text area so dimensions are same

* add tests

* make sure dom element has exact same width as text editor
2022-12-21 12:32:43 +05:30
David Luzar 6ab3f0eb74 fix: showing grabbing cursor when holding spacebar (#6015) 2022-12-20 13:22:20 +01:00
David Luzar 539505affd fix: resize sometimes throwing on missing null-checks (#6013) 2022-12-18 23:06:01 +01:00
David Luzar 95d669390f fix: PWA not working after CRA@5 update (#6012)
* fix: PWA not working after CRA@5 update

* fix: fallback to default locale when fetch fails
2022-12-18 22:23:30 +01:00
David Luzar 73a45e1988 fix: not properly restoring element stroke and bg colors (#6002) 2022-12-16 18:19:26 +01:00
David Luzar 88c2812949 fix: Avatar outline on safari & center (#5997) 2022-12-16 18:18:34 +01:00
David Luzar bdb14723b3 fix: chart pasting not working due to removing tab characters (#5987) 2022-12-16 18:18:27 +01:00
David Luzar cc9e764585 feat: allow readonly actions to be used in viewMode (#5982) 2022-12-11 22:57:03 +01:00
Ryan Di 8466eb0eef fix: apply the right type of roundness when pasting styles (#5979)
* fix: only paste roundness when target and source elements are of the same type

* apply roundness when pasting across different types

* simplify

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-12-10 20:12:09 +08:00
Aakansha Doshi 0ebe6292a3 chore: add display name to context providers (#5974)
* chore: add display name to context providers

* fix typo
2022-12-08 16:19:44 +00:00
Ryan Di 5854ac3eed feat: better default radius sizes for rectangles (#5553)
Co-authored-by: Ryan <diweihao@bytedance.com>
Co-authored-by: dwelle <luzar.david@gmail.com>
2022-12-08 16:48:49 +01:00
Aakansha Doshi 65d84a5d5a fix: remove editor onpaste handler (#5971) 2022-12-07 23:05:57 +05:30
fennghuang 808366d112 fix: remove blank space (#5950) 2022-12-06 17:40:53 +05:30
Fer 9311c99d3c fix: Galego and Kurdî missing in languages plus two locale typos (#5954) 2022-12-06 17:34:22 +05:30
DanielJGeiger d131b31084 fix: ExcalidrawArrowElement rather than ExcalidrawArrowEleement (#5955) 2022-12-06 17:30:03 +05:30
Aakansha Doshi 0111ca2050 fix: renderFooter styling (#5962) 2022-12-06 16:42:54 +05:30
Aakansha Doshi a1dcd6d984 build: move release scripts to use release branch (#5958) 2022-12-06 16:33:02 +05:30
David Luzar fffd4957db fix: repair element bindings on restore (#5956)
* fix: repair element bindings on restore

* fix dropping non-text bound elements

* be more conservative
2022-12-06 00:23:47 +01:00
Aakansha Doshi 760fd7b3a6 feat: Support labels for arrow 🔥 (#5723)
* feat: support arrow with text

* render arrow -> clear rect-> render text

* move bound text when linear elements move

* fix centering cursor when linear element rotated

* fix y coord when new line added and container has 3 points

* update text position when 2nd point moved

* support adding label on top of 2nd point when 3 points are present

* change linear element editor shortcut to cmd+enter and fix tests

* scale bound text points when resizing via bounding box

* ohh yeah rotation works :)

* fix coords when updating text properties

* calculate new position after rotation always from original position

* rotate the bound text by same angle as parent

* don't rotate text and make sure dimensions and coords are always calculated from original point

* hardcoding the text width for now

* Move the linear element when bound text hit

* Rotation working yaay

* consider text element angle when editing

* refactor

* update x2 coords if needed when text updated

* simplify

* consider bound text to be part of bounding box when hit

* show bounding box correctly when multiple element selected

* fix typo

* support rotating multiple elements

* support multiple element resizing

* shift bound text to mid point when odd points

* Always render linear element handles inside editor after element rendered so point is visible for bound text

* Delete bound text when point attached to it deleted

* move bound to mid segement mid point when points are even

* shift bound text when points nearby deleted and handle segment deletion

* Resize working :)

* more resize fixes

* don't update cache-its breaking delete points, look for better soln

* update mid point cache for bound elements when updated

* introduce wrapping when resizing

* wrap when resize for 2 pointer linear elements

* support adding text for linear elements with more than 3 points

* export to svg  working :)

* clip from nearest enclosing element with non transparent color if present when exporting and fill with correct color in canvas

* fix snap

* use visible elements

* Make export to svg work with Mask :)

* remove id

* mask canvas linear element area where label is added

* decide the position of bound text during render

* fix coords when editing

* fix multiple resize

* update cache when bound text version changes

* fix masking when rotated

* render text in correct position in preview

* remove unnecessary code

* fix masking when rotating linear element

* fix masking with zoom

* fix mask in preview for export

* fix offsets in export view

* fix coords on svg export

* fix mask when element rotated in svg

* enable double-click to enter text

* fix hint

* Position cursor correctly and text dimensiosn when height of element is negative

* don't allow 2 pointer linear element with bound text width to go beyond min width

* code cleanup

* fix freedraw

* Add padding

* don't show vertical align action for linear element containers

* Add specs for getBoundTextElementPosition

* more specs

* move some utils to linearElementEditor.ts

* remove only :p

* check absoulte coods in test

* Add test to hide vertical align for linear eleemnt with bound text

* improve export preview

* support labels only for arrows

* spec

* fix large texts

* fix tests

* fix zooming

* enter line editor with cmd+double click

* Allow points to move beyond min width/height for 2 pointer arrow with bound text

* fix hint for line editing

* attempt to fix arrow getting deselected

* fix hint and shortcut

* Add padding of 5px when creating bound text and add spec

* Wrap bound text when arrow binding containers moved

* Add spec

* remove

* set boundTextElementVersion to null if not present

* dont use cache when version mismatch

* Add a padding of 5px vertically when creating text

* Add box sizing content box

* Set bound elements when text element created to fix the padding

* fix zooming in editor

* fix zoom in export

* remove globalCompositeOperation and use clearRect instead of fillRect
2022-12-05 21:03:13 +05:30
Aakansha Doshi 1933116261 fix: don't allow whitespaces for bound text (#5939)
* fix: don't allow whitespaces for bound text

* fix

* remove

* remove empty else

* fix

* fix

* fix
2022-12-02 16:47:50 +05:30
David Luzar 8b33ca3a1a fix: bindings do not survive history serialization (#5942) 2022-12-02 10:36:18 +00:00
Aakansha Doshi a86224c797 fix: Dedupe boundElement ids when container duplicated with alt+drag (#5938)
* Dedupe boundElement ids when container duplicated with alt+drag and add spec

* set to null by default
2022-12-01 20:44:33 +05:30
Aakansha Doshi 66bbfda460 fix: scale font correctly when using shift (#5935)
* fix: scale font correctly when using shift

* fix

* Empty-Commit

* Add spec

* fix
2022-11-30 15:55:01 +05:30
Aakansha Doshi 88b2f4707d refactor: remove unnecessary code (#5933) 2022-11-29 16:41:02 +05:30
Aakansha Doshi 25c6056b03 feat: Don't add midpoint until dragged beyond a threshold (#5927)
* Don't add midpoint until dragged beyond a threshold

* remove unnecessary code

* fix tests

* fix

* add spec

* remove isMidpoint

* cleanup

* fix threshold for zoom

* split into shouldAddMidpoint and addMidpoint

* wrap in flushSync for synchronous updates

* remove threshold for line editor and add spec

* [unrelated] fix stack overflow state update

* fix tests

* don't drag arrow when dragging to add mid point

* add specs

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-11-29 00:01:53 +05:30
Antonio Della Fortuna baf9651d34 feat: changed text copy/paste behaviour (#5786)
Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com>
2022-11-26 23:44:26 +01:00
Aakansha Doshi d2181847be fix: Always bind to container selected by user (#5880)
* fix: Always bind to container selected by user

* Don't bind to container when using text tool

* adjust z-index for bound text

* fix

* Add spec

* Add test

* Allow double click on transparent container and add spec

* fix spec

* adjust z-index only when binding

* update index

* fix

* add index check

* Update src/scene/Scene.ts

Co-authored-by: dwelle <luzar.david@gmail.com>
2022-11-25 15:45:34 +05:30
David Luzar 1f117995d9 fix: fonts not rendered on init if loadingdone not fired (#5923)
* fix: fonts not rendered on init if `loadingdone` not fired

* remove unnecessary check
2022-11-23 21:15:32 +01:00
160 changed files with 3452 additions and 7145 deletions
+61 -1
View File
@@ -1,3 +1,63 @@
# Contributing
Head over to the [docs](https://docs.excalidraw.com/docs/introduction/contributing)
## Setup
### Option 1 - Manual
1. Fork and clone the repo
1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
>
> ```sh
> git remote add upstream https://github.com/excalidraw/excalidraw.git
> git fetch upstream
> git branch --set-upstream-to=upstream/master master
> ```
### Option 2 - CodeSandbox
1. Go to https://codesandbox.io/s/github/excalidraw/excalidraw
1. Connect your GitHub account
1. Go to Git tab on left side
1. Tap on `Fork Sandbox`
1. Write your code
1. Commit and PR automatically
## Pull Request Guidelines
Don't worry if you get any of the below wrong, or if you don't know how. We'll gladly help out.
### Title
Make sure the title starts with a semantic prefix:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing tests or correcting existing tests
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
### Changelog
Add a brief description of your pull request to the changelog located here: [`src/packages/excalidraw/CHANGELOG.md`](src/packages/excalidraw/CHANGELOG.md)
Notes:
- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title
- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request
### Testing
Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.
It's also a good idea to consider if your change should include additional tests. This is highly recommended for new features or bug-fixes. For example, it's good practice to create a test for each bug you fix which ensures that we don't regress the code in the future.
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
+1 -1
View File
@@ -3,7 +3,7 @@ FROM node:14-alpine AS build
WORKDIR /opt/node_app
COPY package.json yarn.lock ./
RUN yarn --ignore-optional --network-timeout 600000
RUN yarn --ignore-optional
ARG NODE_ENV=production
+186 -111
View File
@@ -1,126 +1,201 @@
<a href="https://excalidraw.com/" target="_blank" rel="noopener">
<picture>
<source media="(prefers-color-scheme: dark)" alt="Excalidraw" srcset="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover_dark.png" />
<img alt="Excalidraw" src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2FExcalidraw_Github_cover.png" />
</picture>
</a>
<h4 align="center">
<a href="https://excalidraw.com">Excalidraw Editor</a> |
<a href="https://blog.excalidraw.com">Blog</a> |
<a href="https://docs.excalidraw.com">Documentation</a> |
<a href="https://plus.excalidraw.com">Excalidraw+</a>
</h4>
<div align="center">
<h2>
An open source virtual hand-drawn style whiteboard. </br>
Collaborative and end-to-end encrypted. </br>
<br />
</h3>
</div>
<br />
<p align="center">
<a href="https://github.com/excalidraw/excalidraw/blob/master/LICENSE">
<img alt="Excalidraw is released under the MIT license." src="https://img.shields.io/badge/license-MIT-blue.svg" />
<div align="center" style="display:flex;flex-direction:column;">
<a href="https://excalidraw.com">
<img width="540" src="./public/og-image-sm.png" alt="Excalidraw logo: Sketch handrawn like diagrams." />
</a>
<a href="https://docs.excalidraw.com/docs/introduction/contributing">
<img alt="PRs welcome!" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" />
</a>
<a href="https://discord.gg/UexuTaE">
<img alt="Chat on Discord" src="https://img.shields.io/discord/723672430744174682?color=738ad6&label=Chat%20on%20Discord&logo=discord&logoColor=ffffff&widge=false"/>
</a>
<a href="https://twitter.com/excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+@excalidraw&style=social&logo=twitter"/>
</a>
</p>
<div align="center">
<figure>
<a href="https://excalidraw.com" target="_blank" rel="noopener">
<img src="https://excalidraw.nyc3.cdn.digitaloceanspaces.com/github%2Fproduct_showcase.png" alt="Product showcase" />
<h3>Virtual whiteboard for sketching hand-drawn like diagrams.<br>Collaborative and end-to-end encrypted.</h3>
<p>
<a href="https://twitter.com/Excalidraw">
<img alt="Follow Excalidraw on Twitter" src="https://img.shields.io/twitter/follow/excalidraw.svg?label=follow+excalidraw&style=social&logo=twitter">
</a>
<figcaption>
<p align="center">
Create beautiful hand-drawn like diagrams, wireframes, or whatever you like.
</p>
</figcaption>
</figure>
<a target="_blank" href="https://crowdin.com/project/excalidraw">
<img src="https://badges.crowdin.net/excalidraw/localized.svg">
</a>
</p>
<p>Ask questions or hang out on our <a target="_blank" href="https://discord.gg/UexuTaE">discord.gg/UexuTaE</a>.</p>
</div>
## Features
## Try it now
The Excalidraw editor (npm package) supports:
Go to [excalidraw.com](https://excalidraw.com) to start sketching.
- 💯&nbsp;Free & open-source.
- 🎨&nbsp;Infinite, canvas-based whiteboard.
- ✍️&nbsp;Hand-drawn like style.
- 🌓&nbsp;Dark mode.
- 🏗️&nbsp;Customizable.
- 📷&nbsp;Image support.
- 😀&nbsp;Shape libraries support.
- 👅&nbsp;Localization (i18n) support.
- 🖼️&nbsp;Export to PNG, SVG & clipboard.
- 💾&nbsp;Open format - export drawings as an `.excalidraw` json file.
- ⚒️&nbsp;Wide range of tools - rectangle, circle, diamond, arrow, line, free-draw, eraser...
- ➡️&nbsp;Arrow-binding & labeled arrows.
- 🔙&nbsp;Undo / Redo.
- 🔍&nbsp;Zoom and panning support.
Read the latest news and updates on our [blog](https://blog.excalidraw.com). A good start is to see all the updates of [One Year of Excalidraw](https://blog.excalidraw.com/one-year-of-excalidraw/).
## Excalidraw.com
## Supporting Excalidraw
The app hosted at [excalidraw.com](https://excalidraw.com) is a minimal showcase of what you can build with Excalidraw. Its [source code](https://github.com/excalidraw/excalidraw/tree/master/src/excalidraw-app) is part of this repository as well, and the app features:
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw).
- 📡&nbsp;PWA support (works offline).
- 🤼&nbsp;Real-time collaboration.
- 🔒&nbsp;End-to-end encryption.
- 💾&nbsp;Local-first support (autosaves to the browser).
- 🔗&nbsp;Shareable links (export to a readonly link you can share with others).
We'll be adding these features as drop-in plugins for the npm package in the future.
## Quick start
Install the [Excalidraw npm package](https://www.npmjs.com/package/@excalidraw/excalidraw):
```
npm install react react-dom @excalidraw/excalidraw
```
or via yarn
```
yarn add react react-dom @excalidraw/excalidraw
```
Don't forget to check out our [Documentation](https://docs.excalidraw.com)!
## Contributing
- Missing something or found a bug? [Report here](https://github.com/excalidraw/excalidraw/issues).
- Want to contribute? Check out our [contribution guide](https://docs.excalidraw.com/docs/introduction/contributing) or let us know on [Discord](https://discord.gg/UexuTaE).
- Want to help with translations? See the [translation guide](https://docs.excalidraw.com/docs/introduction/contributing#translating).
## Integrations
- [VScode extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor)
- [npm package](https://www.npmjs.com/package/@excalidraw/excalidraw)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) • and many others
## Sponsors & support
If you like the project, you can become a sponsor at [Open Collective](https://opencollective.com/excalidraw) or use [Excalidraw+](https://plus.excalidraw.com/).
## Thank you for supporting Excalidraw
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120"/>](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
[<img src="https://opencollective.com/excalidraw/tiers/sponsors/0/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/0/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/1/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/1/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/2/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/2/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/3/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/3/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/4/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/4/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/5/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/5/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/6/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/6/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/7/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/7/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/8/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/8/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/9/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/9/website) [<img src="https://opencollective.com/excalidraw/tiers/sponsors/10/avatar.svg?avatarHeight=120">](https://opencollective.com/excalidraw/tiers/sponsors/10/website)
<a href="https://opencollective.com/excalidraw#category-CONTRIBUTE" target="_blank"><img src="https://opencollective.com/excalidraw/tiers/backers.svg?avatarHeight=32"/></a>
Last but not least, we're thankful to these companies for offering their services for free:
[![Vercel](./.github/assets/vercel.svg)](https://vercel.com) [![Sentry](./.github/assets/sentry.svg)](https://sentry.io) [![Crowdin](./.github/assets/crowdin.svg)](https://crowdin.com)
## Who's integrating Excalidraw
[Google Cloud](https://googlecloudcheatsheet.withgoogle.com/architecture) • [Meta](https://meta.com/) • [CodeSandbox](https://codesandbox.io/) • [Obsidian Excalidraw](https://github.com/zsviczian/obsidian-excalidraw-plugin) • [Replit](https://replit.com/) • [Slite](https://slite.com/) • [Notion](https://notion.so/) • [HackerRank](https://www.hackerrank.com/) •
## Documentation
### Shortcuts
You can almost do anything with shortcuts. Click on the help icon on the bottom right corner to see them all.
### Curved lines and arrows
Choose line or arrow and click click click instead of drag.
### Charts
You can easily create charts by copy pasting data from Excel or just plain comma separated text.
### Translating
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
Translations will be available on the app if they exceed a certain threshold of completion (currently 85%).
### Create a collaboration session manually
In order to create a session manually, you just need to generate a link of this form:
```
https://excalidraw.com/#room=[0-9a-f]{20},[a-zA-Z0-9_-]{22}
```
#### Example
```
https://excalidraw.com/#room=91bd46ae3aa84dff9d20,pfLqgEoY1c2ioq8LmGwsFA
```
The first set of digits is the room. This is visible from the server thats going to dispatch messages to everyone that knows this number.
The second set of digits is the encryption key. The Excalidraw server doesnt know about it. This is what all the participants use to encrypt/decrypt the messages.
> Note: Please ensure that the encryption key is 22 characters long.
## Shape libraries
Find a growing list of libraries containing assets for your drawings at [libraries.excalidraw.com](https://libraries.excalidraw.com).
## Embedding Excalidraw in your App?
Try out [`@excalidraw/excalidraw`](https://www.npmjs.com/package/@excalidraw/excalidraw). This package allows you to easily embed Excalidraw as a React component into your apps.
## Development
### Code Sandbox
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
- You may need to sign in with GitHub and reload the page
- You can start coding instantly, and even send PRs from there!
### Local Installation
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
#### Requirements
- [Node.js](https://nodejs.org/en/)
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
- [Git](https://git-scm.com/downloads)
#### Clone the repo
```bash
git clone https://github.com/excalidraw/excalidraw.git
```
#### Install the dependencies
```bash
yarn
```
#### Start the server
```bash
yarn start
```
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
#### Collaboration
For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local.
#### Commands
##### Install the dependencies
```
yarn
```
##### Run the project
```
yarn start
```
##### Reformat all files with Prettier
```
yarn fix
```
##### Run tests
```
yarn test
```
##### Update test snapshots
```
yarn test:update
```
##### Test for formatting with Prettier
```
yarn test:code
```
#### Docker Compose
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
```sh
docker-compose up --build -d
```
### Self-hosting
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc.
```sh
docker build -t excalidraw/excalidraw .
docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latest
```
The Docker image is free of analytics and other tracking libraries.
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
We are working towards providing a full-fledged solution for self-hosting your own Excalidraw.
## Contributing
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
## Notable used tools
- [Create React App](https://github.com/facebook/create-react-app)
- [Rough.js](https://roughjs.com)
- [TypeScript](https://www.typescriptlang.org)
- [Vercel](https://vercel.com)
And the main source of inspiration for starting the project is the awesome [Zwibbler](https://zwibbler.com/demo/) app.
@@ -1,11 +0,0 @@
---
slug: /@excalidraw/excalidraw/api
---
# API
Currently the **API** is divided into 3 broad categories 👇
- [Props](/docs/@excalidraw/excalidraw/api/props) - The `props` you can pass to the `Excalidraw` component.
- [Children components](/docs/@excalidraw/excalidraw/api/children-components) - Official components you can use to customize the UI.
- [Utils](/docs/@excalidraw/excalidraw/api/utils) - Utilities and helpers you can use to export, restore and more.
@@ -1,21 +0,0 @@
---
sidebar_label: Children Components
slug: /@excalidraw/excalidraw/api/children-components
---
# `<Excalidraw/>` children
We expose several components you can render as children of the `<Excalidraw/>` component to customize the UI.
:::info
We have only recently started migrating to this type of component API. Some UI components are still using render props, and some UI customization isn't supported yet (such as the toolbar or the element properties panel). Stay tuned for more updates!
:::
Below are the currently supported components:
- [MainMenu](/docs/@excalidraw/excalidraw/api/children-components/main-menu)
- [WelcomeScreen](/docs/@excalidraw/excalidraw/api/children-components/welcome-screen)
- [Footer](/docs/@excalidraw/excalidraw/api/children-components/footer)
- [LiveCollaborationTrigger](/docs/@excalidraw/excalidraw/api/children-components/live-collaboration-trigger)
@@ -1,69 +0,0 @@
# Footer
Earlier we were using `renderFooter` prop to render custom footer which was removed in [#5970](https://github.com/excalidraw/excalidraw/pull/5970). Now you can pass a `Footer` component instead to render the custom UI for footer.
You will need to import the `Footer` component from the package and wrap your component with the Footer component. The `Footer` should a valid React Node.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "500px"}}>
<Excalidraw>
<Footer>
<button
className="custom-footer"
onClick={() => alert("This is dummy footer")}
>
{" "}
custom footer
</button>
</Footer>
</Excalidraw>
</div>
);
}
```
This will only for `Desktop` devices.
For `mobile` you will need to render it inside the [MainMenu](#mainmenu). You can use the [`useDevice`](#useDevice) hook to check the type of device, this will be available only inside the `children` of `Excalidraw` component.
Open the `Menu` in the below playground and you will see the `custom footer` rendered.
```jsx live noInline
const MobileFooter = ({}) => {
const device = useDevice();
if (device.isMobile) {
return (
<Footer>
<button
className="custom-footer"
style= {{ marginLeft: '20px', height: '2rem'}}
onClick={() => alert("This is custom footer in mobile menu")}
>
custom footer
</button>
</Footer>
);
}
return null;
};
const App = () => (
<div style={{ height: "400px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item> Item1 </MainMenu.Item>
<MainMenu.Item> Item 2 </MainMenu.Item>
<MobileFooter />
</MainMenu>
</Excalidraw>
</div>
);
// Need to render when code is span across multiple components
// in Live Code blocks editor
render(<App />);
```
@@ -1,62 +0,0 @@
# LiveCollaborationTrigger
If you implement live collaboration support and want to expose the same UI button as on [excalidraw.com](https://excalidraw.com), you can render the `<LiveCollaborationTrigger/>` component using the [renderTopRightUI](/docs/@excalidraw/excalidraw/api/props#rendertoprightui) prop.
You'll need to supply `onSelect()` to handle opening of your collaboration dialog, but the button will display `appState.collaborators` count provided you have supplied it.
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `onSelect` | `function` | Yes | | Handler called when the user clicks on the button |
| `isCollaborating` | `boolean` | Yes | false | Whether live collaboration session is in effect. Modifies button style. |
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
const [isCollaborating, setIsCollaborating] = useState(false);
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}>
Selecting the checkbox to see the collaborator count
</p>
<label style={{ fontSize: "16px", fontWeight: "bold" }}>
<input
type="checkbox"
checked={isCollaborating}
onChange={() => {
if (!isCollaborating) {
const collaborators = new Map();
collaborators.set("id1", {
username: "Doremon",
avatarUrl: "../../../../img/doremon.png",
});
collaborators.set("id3", {
username: "Pika",
avatarUrl: "../../../../img/pika.jpeg",
});
excalidrawAPI.updateScene({ collaborators });
} else {
excalidrawAPI.updateScene({
collaborators: new Map(),
});
}
setIsCollaborating(!isCollaborating);
}}
/>
Show Collaborators
</label>
<Excalidraw
ref={(api) => setExcalidrawAPI(api)}
renderTopRightUI={() => (
<LiveCollaborationTrigger
isCollaborating={isCollaborating}
onSelect={() => {
window.alert("You clicked on collab button");
setIsCollaborating(true);
}}
/>
)}
></Excalidraw>
</div>
);
}
```
@@ -1,169 +0,0 @@
# MainMenu
By default Excalidraw will render the `MainMenu` with default options. If you want to customise the `MainMenu`, you can pass the `MainMenu` component with the list options.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "300px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Item onSelect={() => window.alert("Item2")}>
{" "}
Item 2{" "}
</MainMenu.Item>
</MainMenu>
</Excalidraw>
</div>
);
}
```
### `<MainMenu>`
This is the `MainMenu` component. If you render it, you will need to populate the menu with your own items as we will not render any ourselves at that point.
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | No | - | Triggered when any item is selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
### MainMenu.Item
To render an item, its recommended to use `MainMenu.Item`.
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | Yes | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
### MainMenu.ItemLink
To render an item as a link, its recommended to use `MainMenu.ItemLink`.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.ItemLink href="https://google.com">
Google
</MainMenu.ItemLink>
<MainMenu.ItemLink href="https://excalidraw.com">
Excalidraw
</MainMenu.ItemLink>
</MainMenu>
</Excalidraw>
</div>
);
}
```
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `onSelect` | `function` | No | - | Triggered when selected (via mouse). Calling `event.preventDefault()` will stop menu from closing. |
| `href` | `string` | Yes | - | The `href` attribute to be added to the `anchor` element. |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
| `icon` | `JSX.Element` | No | - | The icon used in the menu item |
| `shortcut` | `string` | No | - | The shortcut to be shown for the menu item |
### MainMenu.ItemCustom
To render a custom item, you can use `MainMenu.ItemCustom`.
**Usage**
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.ItemCustom>
<button
style={{ height: "2rem" }}
onClick={() => window.alert("custom menu item")}
>
{" "}
custom item
</button>
</MainMenu.ItemCustom>
</MainMenu>
</Excalidraw>
</div>
);
}
```
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `children` | `React.ReactNode` | Yes | - | The content of the menu item |
### MainMenu.DefaultItems
For the items which are shown in the menu in [excalidraw.com](https://excalidraw.com), you can use `MainMenu.DefaultItems`
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.DefaultItems.Socials />
<MainMenu.DefaultItems.Export />
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Item onSelect={() => window.alert("Item2")}>
Item 2
</MainMenu.Item>
</MainMenu>
</Excalidraw>
</div>
);
}
```
Here is a [complete list](https://github.com/excalidraw/excalidraw/blob/master/src/components/mainMenu/DefaultItems.tsx) of the default items.
### MainMenu.Group
To Group item in the main menu, you can use `MainMenu.Group`
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Group title="Excalidraw items">
<MainMenu.DefaultItems.Socials />
<MainMenu.DefaultItems.Export />
</MainMenu.Group>
<MainMenu.Group title="custom items">
<MainMenu.Item onSelect={() => window.alert("Item1")}>
Item1
</MainMenu.Item>
<MainMenu.Item onSelect={() => window.alert("Item2")}>
Item 2
</MainMenu.Item>
</MainMenu.Group>
</MainMenu>
</Excalidraw>
</div>
);
}
```
| Prop | Type | Required | Default | Description |
| --- | --- | :-: | :-: | --- |
| `children ` | `React.ReactNode` | Yes | - | The content of the `Menu Group` |
@@ -1,140 +0,0 @@
# WelcomeScreen
When the canvas is empty, Excalidraw can show a welcome _splash_ screen with a logo, a few quick action items, and hints explaining what some of the UI buttons do. Once the user picks a tool, or has created an element on the canvas, the welcome screen will disappear.
You can enable this behavior by rendering a `WelcomeScreen` component like this:
```jsx live
function App() {
return (
<div style={{ height: "46rem" }}>
<Excalidraw>
<WelcomeScreen />
</Excalidraw>
</div>
);
}
```
You can also customize the welcome screen by passing children to the `WelcomeScreen` component. See below.
## <WelcomeScreen/>
This is the main component. If you render it without any children, we will render the default welcome screen.
You can customize which welcome screen subcomponents are rendered by passing them as children.
The welcome screen consists of two main groups of subcomponents:
1. [WelcomeScreen.Center](#welcomescreencenterlogo).
2. [WeelcomeScreen.Hints](#welcomescreenhints).
<img
src={require("@site/static/img/welcome-screen-overview.png").default}
alt="Excalidraw logo: Sketch handrawn like diagrams."
/>
### Center
`<WelcomeScreen.Center/>` subcomponent is the center piece of the welcome screen, containing the `logo`, `heading`, and `menu`. All three subcomponents are optional, and you can render whatever you wish into the center component.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<WelcomeScreen>
<WelcomeScreen.Center>
<WelcomeScreen.Center.Logo />
<WelcomeScreen.Center.Heading>
Welcome Screen Heading!
</WelcomeScreen.Center.Heading>
<WelcomeScreen.Center.Menu>
<WelcomeScreen.Center.MenuItemLink href="https://github.com/excalidraw/excalidraw">
Excalidraw GitHub
</WelcomeScreen.Center.MenuItemLink>
<WelcomeScreen.Center.MenuItemHelp />
</WelcomeScreen.Center.Menu>
</WelcomeScreen.Center>
</WelcomeScreen>
</Excalidraw>
</div>
);
}
```
#### Logo
Use the `<WelcomeScreen.Center.Logo/>` to render a logo. By default it renders the Excalidraw logo and name. Supply `children` to customize.
#### Heading
Use the `<WelcomeScreen.Center.Heading/>` to render a heading below the logo. Supply `children` to change the default message.
#### Menu
`<WelcomeScreen.Center.Menu/>` is a wrapper component for the menu items. You can build your menu using the `<WelcomeScreen.Center.MenuItem>` and `<WelcomeScreen.Center.MenuItemLink>` components, render your own, or render one of the default menu items.
The default menu items are:
- `<WelcomeScreen.Center.MenuItemHelp/>` - opens the help dialog.
- `<WelcomeScreen.Center.MenuItemLoadScene/>` - open the load file dialog.
- `<WelcomeScreen.Center.MenuItemLiveCollaborationTrigger/>` - intended to open the live collaboration dialog. Works similarly to [`<LiveCollaborationTrigger>`](/docs/@excalidraw/excalidraw/api/children-components/live-collaboration-trigger) and you must supply `onSelect()` handler to integrate with your collaboration implementation.
#### MenuItem
The `<WelcomeScreen.Center.MenuItem/>` component can be used to render a menu item.
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `onSelect` | `function` | Yes | | The handler is triggered when the item is selected. |
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
**WelcomeScreen.Center.MenuItemLink**
To render an external link in a menu item, you can use this component.
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `href` | `string` | Yes | | The `href` attribute to be added to the `anchor` element. |
| `children` | `React.ReactNode` | Yes | | The content of the menu item |
| `icon` | `JSX.Element` | No | | The icon used in the menu item |
| `shortcut` | `string` | No | | The keyboard shortcut (label-only, does not affect behavior) |
### Hints
These `<WelcomeScreen.Hints.*>` subcomponents render the UI hints. Text of each hint can be customized by supplying `children`.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw>
<WelcomeScreen>
<WelcomeScreen.Hints.ToolbarHint>
<p> ToolBar Hints </p>
</WelcomeScreen.Hints.ToolbarHint>
<WelcomeScreen.Hints.MenuHint />
<WelcomeScreen.Hints.HelpHint />
</WelcomeScreen>
</Excalidraw>
</div>
);
}
```
#### MenuHint
`<WelcomeScreen.Hints.MenuHint>` hint subcomponent for the main menu. Supply `children` to customize the hint text.
#### ToolbarHint
`<WelcomeScreen.Hints.ToolbarHint>` hint subcomponent for the toolbar. Supply `children` to customize the hint text.
#### Help
`<WelcomeScreen.Hints.Help>` hint subcomponent for the help dialog. Supply `children` to customize the hint text.
@@ -1,46 +0,0 @@
# Constants
### FONT_FAMILY
**How to use**
```js
import { FONT_FAMILY } from "@excalidraw/excalidraw";
```
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below
| Font Family | Description |
| ----------- | ---------------------- |
| `Virgil` | The `handwritten` font |
| `Helvetica` | The `Normal` Font |
| `Cascadia` | The `Code` Font |
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`.
### THEME
**How to use**
```js
import { THEME } from "@excalidraw/excalidraw";
```
`THEME` contains all the themes supported by `Excalidraw` as explained below
| Theme | Description |
| ------- | ----------------- |
| `LIGHT` | The `light` theme |
| `DARK` | The `Dark` theme |
Defaults to `THEME.LIGHT` unless passed in `initialData.appState.theme`
### MIME_TYPES
[`MIME_TYPES`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L101) contains all the mime types supported by `Excalidraw`.
**How to use **
```js
import { MIME_TYPES } from "@excalidraw/excalidraw";
```
@@ -1,55 +0,0 @@
# initialData
<pre>
&#123; elements?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>, appState?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> &#125;
</pre>
This helps to load Excalidraw with `initialData`. It must be an object or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to an object containing the below optional fields.
| Name | Type | Description |
| --- | --- | --- |
| `elements` | [ExcalidrawElement[]](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | The `elements` with which `Excalidraw` should be mounted. |
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | The `AppState` with which `Excalidraw` should be mounted. |
| `scrollToContent` | `boolean` | This attribute indicates whether to `scroll` to the nearest element to center once `Excalidraw` is mounted. By default, it will not scroll the nearest element to the center. Make sure you pass `initialData.appState.scrollX` and `initialData.appState.scrollY` when `scrollToContent` is false so that scroll positions are retained |
| `libraryItems` | [LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L247) &#124; Promise&lt;[LibraryItems](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200)&gt; | This library items with which `Excalidraw` should be mounted. |
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82) | The `files` added to the scene. |
You might want to use this when you want to load excalidraw with some initial elements and app state.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
initialData={{
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#000000",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
},
],
appState: { zenModeEnabled: true, viewBackgroundColor: "#a5d8ff" },
scrollToContent: true
}}
/>
</div>
);
}
```
@@ -1,230 +0,0 @@
# Props
All `props` are *optional*.
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata) | `object` &#124; `null` &#124; <code>Promise<object &#124; null></code> | `null` | The initial data with which app loads. |
| [`ref`](/docs/@excalidraw/excalidraw/api/props/ref) | `object` | _ | `Ref` to be passed to Excalidraw |
| [`isCollaborating`](#iscollaborating) | `boolean` | _ | This indicates if the app is in `collaboration` mode |
| [`onChange`](#onchange) | `function` | _ | This callback is triggered whenever the component updates due to any change. This callback will receive the excalidraw `elements` and the current `app state`. |
| [`onPointerUpdate`](#onpointerupdate) | `function` | _ | Callback triggered when mouse pointer is updated. |
| [`onPointerDown`](#onpointerdown) | `function` | _ | This prop if passed gets triggered on pointer down evenets |
| [`onScrollChange`](#onscrollchange) | `function` | _ | This prop if passed gets triggered when scrolling the canvas. |
| [`onPaste`](#onpaste) | `function` | _ | Callback to be triggered if passed when the something is pasted in to the scene |
| [`onLibraryChange`](#onlibrarychange) | `function` | _ | The callback if supplied is triggered when the library is updated and receives the library items. |
| [`onLinkOpen`](#onlinkopen) | `function` | _ | The callback if supplied is triggered when any link is opened. |
| [`langCode`](#langcode) | `string` | `en` | Language code string to be used in Excalidraw |
| [`renderTopRightUI`](/docs/@excalidraw/excalidraw/api/props/render-props#rendertoprightui) | `function` | _ | Render function that renders custom UI in top right corner |
| [`renderCustomStats`](/docs/@excalidraw/excalidraw/api/props/render-props#rendercustomstats) | `function` | _ | Render function that can be used to render custom stats on the stats dialog. |
| [`renderSidebar`](/docs/@excalidraw/excalidraw/api/props/render-props#rendersidebar) | `function` | _ | Render function that renders custom sidebar. |
| [`viewModeEnabled`](#viewmodeenabled) | `boolean` | _ | This indicates if the app is in `view` mode. |
| [`zenModeEnabled`](#zenmodeenabled) | `boolean` | _ | This indicates if the `zen` mode is enabled |
| [`gridModeEnabled`](#gridmodeenabled) | `boolean` | _ | This indicates if the `grid` mode is enabled |
| [`libraryReturnUrl`](#libraryreturnurl) | `string` | _ | What URL should [libraries.excalidraw.com](https://libraries.excalidraw.com) be installed to |
| [`theme`](#theme) | `"light"` &#124; `"dark"` | `"light"` | The theme of the Excalidraw component |
| [`name`](#name) | `string` | | Name of the drawing |
| [`UIOptions`](/docs/@excalidraw/excalidraw/api/props/ui-options) | `object` | [DEFAULT UI OPTIONS](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L151) | To customise UI options. Currently we support customising [`canvas actions`](#canvasactions) |
| [`detectScroll`](#detectscroll) | `boolean` | `true` | Indicates whether to update the offsets when nearest ancestor is scrolled. |
| [`handleKeyboardGlobally`](#handlekeyboardglobally) | `boolean` | `false` | Indicates whether to bind the keyboard events to document. |
| [`autoFocus`](#autofocus) | `boolean` | `false` | indicates whether to focus the Excalidraw component on page load |
| [`generateIdForFile`](#generateidforfile) | `function` | _ | Allows you to override `id` generation for files added on canvas |
### Storing custom data on Excalidraw elements
Beyond attributes that Excalidraw elements already support, you can store `custom` data on each `element` in a `customData` object. The type of the attribute is [`Record<string, any>`](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L66) and is optional.
You can use this to add any extra information you need to keep track of.
You can add `customData` to elements when passing them as [`initialData`](/docs/@excalidraw/excalidraw/api/props/initialdata), or using [`updateScene`](/docs/@excalidraw/excalidraw/api/props/ref#updatescene) / [`updateLibrary`](/docs/@excalidraw/excalidraw/api/props/ref#updatelibrary) afterwards.
```js showLineNumbers
{
type: "rectangle",
id: "oDVXy8D6rom3H1-LLH2-f",
customData: {customId: '162'},
}
```
### isCollaborating
This prop indicates if the app is in `collaboration` mode.
### onChange
Every time component updates, this callback if passed will get triggered and has the below signature.
```js
(excalidrawElements, appState, files) => void;
```
1. `excalidrawElements`: Array of [excalidrawElements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) in the scene.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene.
3. `files`: The [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) which are added to the scene.
Here you can try saving the data to your backend or local storage for example.
### onPointerUpdate
This callback is triggered when mouse pointer is updated.
```js
({ x, y }, button, pointersMap}) => void;
```
1.`{x, y}`: Pointer coordinates
2.`button`: The position of the button. This will be one of `["down", "up"]`
3.`pointersMap`: [`pointers`](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L131) map of the scene
```js
(exportedElements, appState, canvas) => void
```
1. `exportedElements`: An array of [non deleted elements](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L87) which needs to be exported.
2. `appState`: [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) of the scene.
3. `canvas`: The `HTMLCanvasElement` of the scene.
### onPointerDown
This prop if passed will be triggered on pointer down events and has the below signature.
<pre>
(activeTool: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L115"> AppState["activeTool"]</a>, pointerDownState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L424">PointerDownState</a>) => void
</pre>
### onScrollChange
This prop if passed will be triggered when canvas is scrolled and has the below signature.
```ts
(scrollX: number, scrollY: number) => void
```
### onPaste
This callback is triggered if passed when something is pasted into the scene. You can use this callback in case you want to do something additional when the paste event occurs.
<pre>
(data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/clipboard.ts#L18">ClipboardData</a>, event: ClipboardEvent &#124; null) => boolean
</pre>
This callback must return a `boolean` value or a [promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise) which resolves to a boolean value.
In case you want to prevent the excalidraw paste action you must return `false`, it will stop the native excalidraw clipboard management flow (nothing will be pasted into the scene).
### onLibraryChange
This callback if supplied will get triggered when the library is updated and has the below signature.
<pre>
(items:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">
LibraryItems
</a>
) => void | Promise&lt;any&gt;
</pre>
It is invoked with empty items when user clears the library. You can use this callback when you want to do something additional when library is updated for example persisting it to local storage.
### onLinkOpen
This prop if passed will be triggered when clicked on `link`. To handle the redirect yourself (such as when using your own router for internal links), you must call `event.preventDefault()`.
<pre>
(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>,
event: CustomEvent&lt;&#123; nativeEvent: MouseEvent }&gt;) => void
</pre>
Example:
```js showLineNumbers
const history = useHistory();
// open internal links using the app's router, but opens external links in
// a new tab/window
const onLinkOpen: ExcalidrawProps["onLinkOpen"] = useCallback(
(element, event) => {
const link = element.link;
const { nativeEvent } = event.detail;
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
const isNewWindow = nativeEvent.shiftKey;
const isInternalLink =
link.startsWith("/") || link.includes(window.location.origin);
if (isInternalLink && !isNewTab && !isNewWindow) {
history.push(link.replace(window.location.origin, ""));
// signal that we're handling the redirect ourselves
event.preventDefault();
}
},
[history],
);
```
### langCode
Determines the `language` of the UI. It should be one of the [available language codes](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L14). Defaults to `en` (English). We also export default language and supported languages which you can import as shown below.
```js
import { defaultLang, languages } from "@excalidraw/excalidraw";
```
| name | type |
| --- | --- |
| `defaultLang` | `string` |
| `languages` | [`Language[]`](https://github.com/excalidraw/excalidraw/blob/master/src/i18n.ts#L15) |
### viewModeEnabled
This prop indicates whether the app is in `view mode`. When supplied, the value takes precedence over *intialData.appState.viewModeEnabled*, the `view mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### zenModeEnabled
This prop indicates whether the app is in `zen mode`. When supplied, the value takes precedence over *intialData.appState.zenModeEnabled*, the `zen mode` will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### gridModeEnabled
This prop indicates whether the shows the grid. When supplied, the value takes precedence over *intialData.appState.gridModeEnabled*, the grid will be fully controlled by the host app, and users won't be able to toggle it from within the app.
### libraryReturnUrl
If supplied, this URL will be used when user tries to install a library from [libraries.excalidraw.com](https://libraries.excalidraw.com).
Defaults to *window.location.origin + window.location.pathname*. To install the libraries in the same tab from which it was opened, you need to set `window.name` (to any alphanumeric string) — if it's not set it will open in a new tab.
### theme
This prop controls Excalidraw's theme. When supplied, the value takes precedence over *intialData.appState.theme*, the theme will be fully controlled by the host app, and users won't be able to toggle it from within the app unless *UIOptions.canvasActions.toggleTheme* is set to `true`, in which case the `theme` prop will control Excalidraw's default theme with ability to allow theme switching (you must take care of updating the `theme` prop when you detect a change to `appState.theme` from the [onChange](#onchange) callback).
You can use [`THEME`](/docs/@excalidraw/excalidraw/api/utils#theme) to specify the theme.
### name
This prop sets the `name` of the drawing which will be used when exporting the drawing. When supplied, the value takes precedence over *intialData.appState.name*, the `name` will be fully controlled by host app and the users won't be able to edit from within Excalidraw.
### detectScroll
Indicates whether Excalidraw should listen for `scroll` event on the nearest scrollable container in the DOM tree and recompute the coordinates (e.g. to correctly handle the cursor) when the component's position changes. You can disable this when you either know this doesn't affect your app or you want to take care of it yourself (calling the [`refresh()`](#ref) method).
### handleKeyboardGlobally
Indicates whether to bind keyboard events to `document`. Disabled by default, meaning the keyboard events are bound to the Excalidraw component. This allows for multiple Excalidraw components to live on the same page, and ensures that Excalidraw keyboard handling doesn't collide with your app's (or the browser) when the component isn't focused.
Enable this if you want Excalidraw to handle keyboard even if the component isn't focused (e.g. a user is interacting with the navbar, sidebar, or similar).
### autoFocus
This prop indicates whether to `focus` the Excalidraw component on page load. Defaults to false.
### generateIdForFile
Allows you to override `id` generation for files added on canvas (images). By default, an SHA-1 digest of the file is used.
```tsx
(file: File) => string | Promise<string>
```
@@ -1,391 +0,0 @@
# ref
<pre>
<a href="https://reactjs.org/docs/refs-and-the-dom.html#creating-refs">createRef</a> &#124; <a href="https://reactjs.org/docs/hooks-reference.html#useref">useRef</a> &#124; <a href="https://reactjs.org/docs/refs-and-the-dom.html#callback-refs">callbackRef</a> &#124; <br/>&#123; current: &#123; readyPromise: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">resolvablePromise</a> } }
</pre>
You can pass a `ref` when you want to access some excalidraw APIs. We expose the below APIs:
| API | Signature | Usage |
| --- | --- | --- |
| ready | `boolean` | This is set to true once Excalidraw is rendered |
| [readyPromise](#readypromise) | `function` | This promise will be resolved with the api once excalidraw has rendered. This will be helpful when you want do some action on the host app once this promise resolves. For this to work you will have to pass ref as shown [here](#readypromise) |
| [updateScene](#updatescene) | `function` | updates the scene with the sceneData |
| [updateLibrary](#updatelibrary) | `function` | updates the scene with the sceneData |
| [addFiles](#addfiles) | `function` | add files data to the appState |
| [resetScene](#resetscene) | `function` | Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false. |
| [getSceneElementsIncludingDeleted](#getsceneelementsincludingdeleted) | `function` | Returns all the elements including the deleted in the scene |
| [getSceneElements](#getsceneelements) | `function` | Returns all the elements excluding the deleted in the scene |
| [getAppState](#getappstate) | `function` | Returns current appState |
| [history](#history) | `object` | This is the history API. `history.clear()` will clear the history |
| [scrollToContent](#scrolltocontent) | `function` | Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene. |
| [refresh](#refresh) | `function` | Updates the offsets for the Excalidraw component so that the coordinates are computed correctly (for example the cursor position). |
| [setToast](#settoast) | `function` | This API can be used to show the toast with custom message. |
| [id](#id) | `string` | Unique ID for the excalidraw component. |
| [getFiles](#getfiles) | `function` | This API can be used to get the files present in the scene. |
| [setActiveTool](#setactivetool) | `function` | This API can be used to set the active tool |
| [setCursor](#setcursor) | `function` | This API can be used to set customise the mouse cursor on the canvas |
| [resetCursor](#resetcursor) | `function` | This API can be used to reset to default mouse cursor on the canvas |
| [toggleMenu](#togglemenu) | `function` | Toggles specific menus on/off |
## readyPromise
<pre>
const excalidrawRef = &#123; current:&#123; readyPromise:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/utils.ts#L460">
&nbsp;resolvablePromise
</a>
&nbsp;&#125; &#125;
</pre>
Since plain object is passed as a `ref`, the `readyPromise` is resolved as soon as the component is mounted. Most of the time you will not need this unless you have a specific use case where you can't pass the `ref` in the react way and want to do some action on the host when this promise resolves.
```jsx showLineNumbers
const resolvablePromise = () => {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
promise.resolve = resolve;
promise.reject = reject;
return promise;
};
const App = () => {
const excalidrawRef = useMemo(
() => ({
current: {
readyPromise: resolvablePromise(),
},
}),
[],
);
useEffect(() => {
excalidrawRef.current.readyPromise.then((api) => {
console.log("loaded", api);
});
}, [excalidrawRef]);
return (
<div style={{ height: "500px" }}>
<Excalidraw ref={excalidrawRef} />
</div>
);
};
```
## updateScene
<pre>
(scene:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L339">
sceneData
</a>
) => void
</pre>
You can use this function to update the scene with the sceneData. It accepts the below attributes.
| Name | Type | Description |
| --- | --- | --- |
| `elements` | [`ImportedDataState["elements"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L38) | The `elements` to be updated in the scene |
| `appState` | [`ImportedDataState["appState"]`](https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L39) | The `appState` to be updated in the scene. |
| `collaborators` | <code>Map<string, <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L37">Collaborator></a></code> | The list of collaborators to be updated in the scene. |
| `commitToHistory` | `boolean` | Implies if the `history (undo/redo)` should be recorded. Defaults to `false`. |
```jsx live
function App() {
const updateScene = () => {
const sceneData = {
elements: [
{
type: "rectangle",
version: 141,
versionNonce: 361174001,
isDeleted: false,
id: "oDVXy8D6rom3H1-LLH2-f",
fillStyle: "hachure",
strokeWidth: 1,
strokeStyle: "solid",
roughness: 1,
opacity: 100,
angle: 0,
x: 100.50390625,
y: 93.67578125,
strokeColor: "#c92a2a",
backgroundColor: "transparent",
width: 186.47265625,
height: 141.9765625,
seed: 1968410350,
groupIds: [],
boundElements: null,
locked: false,
link: null,
updated: 1,
roundness: {
type: 3,
value: 32,
},
},
],
appState: {
viewBackgroundColor: "#edf2ff",
},
};
excalidrawAPI.updateScene(sceneData);
};
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
<button className="custom-button" onClick={updateScene}>Update Scene</button>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
</div>
);
}
```
### updateLibrary
<pre>
(opts: &#123; <br /> libraryItems:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249">
LibraryItemsSource
</a>
;<br /> merge?: boolean; <br /> prompt?: boolean;
<br /> openLibraryMenu?: boolean;
<br /> defaultStatus?: "unpublished" | "published"; <br /> &#125;) => Promise&lt;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L246">
LibraryItems
</a>
&gt;
</pre>
You can use this function to update the library. It accepts the below attributes.
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `libraryItems` | [LibraryItemsSource](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L249) | \_ | The `libraryItems` to be replaced/merged with current library |
| `merge` | boolean | `false` | Whether to merge with existing library items. |
| `prompt` | boolean | `false` | Whether to prompt user for confirmation. |
| `openLibraryMenu` | boolean | `false` | Keep the library menu open after library is updated. |
| `defaultStatus` | <code>"unpublished" &#124; "published"</code> | `"unpublished"` | Default library item's `status` if not present. |
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
useEffect(() => {
if (!excalidrawAPI) {
return;
}
// to open the library sidebar
excalidrawAPI.updateScene({ appState: { openSidebar: "library" } });
}, [excalidrawAPI]);
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
<button className="custom-button"
onClick={() => {
const libraryItems = [
{
status: "published",
id: "1",
created: 1,
elements: initialData.libraryItems[1],
},
{
status: "unpublished",
id: "2",
created: 2,
elements: initialData.libraryItems[1],
},
];
excalidrawAPI.updateLibrary({
libraryItems,
openLibraryMenu: true
});
}}
>
Update Library
</button>
<Excalidraw
ref={(api) => setExcalidrawAPI(api)}
// initial data retrieved from https://github.com/excalidraw/excalidraw/blob/master/dev-docs/src/initialData.js
initialData={{
libraryItems: initialData.libraryItems,
appState: { openSidebar: "library" },
}}
/>
</div>
);
}
```
### addFiles
<pre>
(files:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
BinaryFileData
</a>
) => void
</pre>
Adds supplied files data to the `appState.files` cache on top of existing files present in the cache.
## resetScene
```tsx
(opts?: { resetLoadingState: boolean }) => void
```
Resets the scene. If `resetLoadingState` is passed as true then it will also force set the loading state to false.
## getSceneElementsIncludingDeleted
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>
</pre>
Returns all the elements including the deleted in the scene.
## getSceneElements
<pre>
() => NonDeleted&#60;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>
[]&#62;
</pre>
Returns all the elements excluding the deleted in the scene
## getAppState
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
</pre>
Returns current appState.
## history
```tsx
{
clear: () => void
}
```
This is the history API. history.clear() will clear the history.
## scrollToContent
<pre>
(target?:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>{" "}
&#124;{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>
[]) => void
</pre>
Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene.
## refresh
```tsx
() => void
```
Updates the `offsets` for the `Excalidraw` component so that the coordinates are computed correctly (for example the cursor position).
You don't have to call this when the position is changed on page scroll or when the excalidraw container resizes (we handle that ourselves).
For any other cases if the position of excalidraw is updated (example due to scroll on parent container and not page scroll) you should call this API.
## setToast
This API can be used to show the toast with custom message.
```tsx
({ message: string, closable?:boolean,duration?:number
} | null) => void
```
| Attribute | type | Description |
| --- | --- | --- |
| message | string | The message to be shown on the toast. |
| closable | boolean | Indicates whether to show the closable button on toast to dismiss the toast. |
| duration | number | Determines the duration after which the toast should auto dismiss. To prevent autodimiss you can pass `Infinity`. |
To dismiss an existing toast you can simple pass `null`
```js
setToast(null);
```
## id
The unique id of the excalidraw component. This can be used to identify the excalidraw component, for example importing the library items to the excalidraw component from where it was initiated when you have multiple excalidraw components rendered on the same page as shown in [multiple excalidraw demo](https://codesandbox.io/s/multiple-excalidraw-k1xx5).
## getFiles
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L82">
files
</a>
</pre>
This API can be used to get the files present in the scene. It may contain files that aren't referenced by any element, so if you're persisting the files to a storage, you should compare them against stored elements.
## setActiveTool
This API has the below signature. It sets the `tool` passed in param as the active tool.
<pre>
(tool: <br/> &#123; type: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/shapes.tsx#L15">SHAPES</a>[number]["value"]&#124; "eraser" &#125; &#124;<br/> &#123; type: "custom"; customType: string &#125;) => void
</pre>
## setCursor
This API can be used to customise the mouse cursor on the canvas and has the below signature.
It sets the mouse cursor to the cursor passed in param.
```tsx
(cursor: string) => void
```
## toggleMenu
```tsx
(type: "library" | "customSidebar", force?: boolean) => boolean;
```
This API can be used to toggle a specific menu (currently only the sidebars), and returns whether the menu was toggled on or off. If the `force` flag passed, it will force the menu to be toggled either on/off based on the `boolean` passed.
This API is especially useful when you render a custom sidebar using [`renderSidebar`](#rendersidebar) prop, and you want to toggle it from your app based on a user action.
## resetCursor
```tsx
() => void
```
This API can be used to reset to default mouse cursor.
@@ -1,126 +0,0 @@
# Render Props
## renderTopRightUI
<pre>
(isMobile: boolean, appState:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">
AppState
</a>
) => JSX | null
</pre>
A function returning `JSX` to render `custom` UI in the top right corner of the app.
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
renderTopRightUI={() => {
return (
<button
style={{
background: "#70b1ec",
border: "none",
color: "#fff",
width: "max-content",
fontWeight: "bold",
}}
onClick={() => window.alert("This is dummy top right UI")}
>
{" "}
Click me{" "}
</button>
);
}}
/>
</div>
);
}
```
## renderCustomStats
A function that can be used to render custom stats (returns JSX) in the `nerd stats` dialog.
![Nerd Stats](../../../../assets/nerd-stats.png)
For example you can use this prop to render the size of the elements in the storage as do in [excalidraw.com](https://excalidraw.com).
```jsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw
renderCustomStats={() => (
<p style={{ color: "#70b1ec", fontWeight: "bold" }}>
{" "}
Dummy stats will be shown here{" "}
</p>
)}
/>
</div>
);
}
```
## renderSidebar
```tsx
() => JSX | null;
```
You can render `custom sidebar` using this prop. This sidebar is the same that the library menu sidebar is using, and can be used for any purposes your app needs.
You need to import the `Sidebar` component from `excalidraw` package and pass your content as its `children`. The function `renderSidebar` should return the `Sidebar` instance.
### Sidebar
The `<Sidebar>` component takes these props (all are optional except `children`):
| Prop | Type | Description |
| --- | --- | --- |
| `children` | `React.ReactNode` | Content you want to render inside the `sidebar`. |
| `onClose` | `function` | Invoked when the component is closed (by user, or the editor). No need to act on this event, as the editor manages the sidebar open state on its own. |
| `onDock` | `function` | Invoked when the user toggles the `dock` button. The callback recieves a `boolean` parameter `isDocked` which indicates whether the sidebar is `docked` |
| `docked` | `boolean` | Indicates whether the sidebar is`docked`. By default, the sidebar is `undocked`. If passed, the docking becomes controlled, and you are responsible for updating the `docked` state by listening on `onDock` callback. To decide the breakpoint for docking you can use [UIOptions.dockedSidebarBreakpoint](/docs/@excalidraw/excalidraw/api/props/ui-options#dockedsidebarbreakpoint) for more info on docking. |
| `dockable` | `boolean` | Indicates whether to show the `dock` button so that user can `dock` the sidebar. If `false`, you can still dock programmatically by passing `docked` as `true`. |
The sidebar will always include a header with `close / dock` buttons (when applicable).
You can also add custom content to the header, by rendering `<Sidebar.Header>` as a child of the `<Sidebar>` component. Note that the custom header will still include the default buttons.
### Sidebar.Header
| name | type | description |
| --- | --- | --- |
| children | `React.ReactNode` | Content you want to render inside the sidebar header as a sibling of `close` / `dock` buttons. |
To control the visibility of the sidebar you can use [`toggleMenu("customSidebar")`](/docs/@excalidraw/excalidraw/api/props/ref#togglemenu) api available via `ref`.
```tsx live
function App() {
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<div style={{ height: "500px" }}>
<button className="custom-button" onClick={() => excalidrawAPI.toggleMenu("customSidebar")}>
{" "}
Toggle Custom Sidebar{" "}
</button>
<Excalidraw
UIOptions={{ dockedSidebarBreakpoint: 100 }}
ref={(api) => setExcalidrawAPI(api)}
renderSidebar={() => {
return (
<Sidebar dockable={true}>
<Sidebar.Header>Custom Sidebar Header </Sidebar.Header>
<p style={{ padding: "1rem" }}> custom Sidebar Content </p>
</Sidebar>
);
}}
/>
</div>
);
}
```
@@ -1,72 +0,0 @@
# UIOptions
This prop can be used to customise UI of Excalidraw. Currently we support customising [`canvasActions`](#canvasactions), [`dockedSidebarBreakpoint`](#dockedsidebarbreakpoint) and [`welcomeScreen`](#welcmescreen).
<pre>
&#123;
<br /> canvasActions?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L372">
CanvasActions
</a>, <br /> dockedSidebarBreakpoint?: number, <br /> welcomeScreen?: boolean <br />
}
</pre>
## canvasActions
This `prop` controls the visibility of the canvas actions inside the `menu`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `changeViewBackgroundColor` | `boolean` | `true` | Indicates whether to show `Background color picker`. |
| `clearCanvas` | `boolean` | `true` | Indicates whether to show `Clear canvas` button. |
| `export` | `false` &#124; [`exportOpts`](#exportopts) | `object` | This prop allows to customize the UI inside the export dialog. By default it shows the `save file to disk`. For more details visit [`exportOpts`](#exportopts). |
| `loadScene` | `boolean` | `true` | Indicates whether to show `Load` button. |
| `saveToActiveFile` | `boolean` | `true` | Indicates whether to show `Save` button to save to current file. |
| `toggleTheme` | `boolean` &#124; `null` | `null` | Indicates whether to show `Theme toggle`. When defined as `boolean`, takes precedence over [`props.theme`](/docs/@excalidraw/excalidraw/api/props#theme) to show `Theme toggle`. |
| `saveAsImage` | `boolean` | `true` | Indicates whether to show `Save as image` button. |
```tsx live
function App() {
const UIOptions = {
canvasActions: {
changeViewBackgroundColor: false,
clearCanvas: false,
loadScene: false,
},
};
return (
<div style={{ height: "500px" }}>
<Excalidraw UIOptions={UIOptions} />
</div>
);
}
```
### exportOpts
The below attributes can be set in `UIOptions.canvasActions.export` to customize the export dialog.
If `UIOptions.canvasActions.export` is `false` the export button will not be rendered.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `saveFileToDisk` | `boolean` | `true` | Indicates whether `save file to disk` button should be shown |
| `onExportToBackend` | `object` | \_ | This callback is triggered when the shareable-link button is clicked in the export dialog. The link button will only be shown if this callback is passed. |
| `renderCustomUI` | `object` | \_ | This callback should be supplied if you want to render custom UI in the export dialog. |
## dockedSidebarBreakpoint
This prop indicates at what point should we break to a docked, permanent sidebar. If not passed it defaults to [`MQ_RIGHT_SIDEBAR_MAX_WIDTH_PORTRAIT`](https://github.com/excalidraw/excalidraw/blob/master/src/constants.ts#L161).
If the _width_ of the _excalidraw_ container exceeds _dockedSidebarBreakpoint_, the sidebar will be `dockable` and the button to `dock` the sidebar will be shown
If user choses to `dock` the sidebar, it will push the right part of the UI towards the left, making space for the sidebar as shown below.
![image](https://user-images.githubusercontent.com/11256141/174664866-c698c3fa-197b-43ff-956c-d79852c7b326.png)
```tsx live
function App() {
return (
<div style={{ height: "500px" }}>
<Excalidraw UIOptions={{dockedSidebarBreakpoint: 200}}/>
</div>
);
}
```
@@ -1,196 +0,0 @@
---
title: Export Utilities
id: "export"
---
:::info
We're working on much improved export utilities. Stay tuned!
:::
### exportToCanvas
**_Signature_**
<pre>
exportToCanvas(&#123;<br/>&nbsp;
elements,<br/>&nbsp;
appState<br/>&nbsp;
getDimensions,<br/>&nbsp;
files,<br/>&nbsp;
exportPadding?: number;<br/>
&#125;: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a>
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `elements` | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to be exported to canvas. |
| `appState` | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L23) | [Default App State](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L17) | The app state of the scene. |
| [`getDimensions`](#getdimensions) | `function` | _ | A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported. |
| `maxWidthOrHeight` | `number` | _ | The maximum `width` or `height` of the exported image. If provided, `getDimensions` is ignored. |
| `files` | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59) | _ | The files added to the scene. |
| `exportPadding` | `number` | `10` | The `padding` to be added on canvas. |
#### getDimensions
```tsx
(width: number, height: number) => {
width: number,
height: number,
scale?: number
}
```
A function which returns the `width`, `height`, and optionally `scale` (defaults to `1`), with which canvas is to be exported.
**How to use**
```js
import { exportToCanvas } from "@excalidraw/excalidraw";
```
This function returns the canvas with the exported elements, appState and dimensions.
```jsx live
function App() {
const [canvasUrl, setCanvasUrl] = useState("");
const [excalidrawAPI, setExcalidrawAPI] = useState(null);
return (
<>
<button
className="custom-button"
onClick={async () => {
if (!excalidrawAPI) {
return
}
const elements = excalidrawAPI.getSceneElements();
if (!elements || !elements.length) {
return
}
const canvas = await exportToCanvas({
elements,
appState: {
...initialData.appState,
exportWithDarkMode: false,
},
files: excalidrawAPI.getFiles(),
getDimensions: () => { return {width: 350, height: 350}}
});
const ctx = canvas.getContext("2d");
ctx.font = "30px Virgil";
ctx.strokeText("My custom text", 50, 60);
setCanvasUrl(canvas.toDataURL());
}}
>
Export to Canvas
</button>
<div className="export export-canvas">
<img src={canvasUrl} alt="" />
</div>
<div style={{ height: "400px" }}>
<Excalidraw ref={(api) => setExcalidrawAPI(api)}
/>
</div>
</>
)
}
```
### exportToBlob
**_Signature_**
<pre>
exportToBlob(<br/>&nbsp;
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L14">ExportOpts</a> & &#123;<br/>&nbsp;
mimeType?: string,<br/>&nbsp;
quality?: number,<br/>&nbsp;
exportPadding?: number;<br/>
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `opts` | `object` | _ | This param is passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas) |
| `mimeType` | `string` | `image/png` | Indicates the image format. |
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg`/`image/webp` MIME types. |
| `exportPadding` | `number` | `10` | The padding to be added on canvas. |
**How to use**
```js
import { exportToBlob } from "@excalidraw/excalidraw";
```
Returns a promise which resolves with a [blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob). It internally uses [canvas.ToBlob](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob).
### exportToSvg
**_Signature_**
<pre>
exportToSvg(&#123;<br/>&nbsp;
elements:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>,<br/>&nbsp;
appState:
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95"> AppState
</a>,<br/>&nbsp;
exportPadding: number,<br/>&nbsp;
metadata: string,<br/>&nbsp;
files:&nbsp;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L59">
BinaryFiles
</a>,<br/>
&#125;);
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| elements | [Excalidraw Element []](https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114) | | The elements to exported as `svg `|
| appState | [AppState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) | [defaultAppState](https://github.com/excalidraw/excalidraw/blob/master/src/appState.ts#L11) | The `appState` of the scene |
| exportPadding | number | 10 | The `padding` to be added on canvas |
| files | [BinaryFiles](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L64) | undefined | The `files` added to the scene. |
This function returns a promise which resolves to `svg` of the exported drawing.
### exportToClipboard
**_Signature_**
<pre>
exportToClipboard(<br/>&nbsp;
opts: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/packages/utils.ts#L21">ExportOpts</a> & &#123;<br/>&nbsp;
mimeType?: string,<br/>&nbsp;
quality?: number;<br/>&nbsp;
type: 'png' | 'svg' |'json'<br/>
})
</pre>
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `opts` | | | This param is same as the params passed to `exportToCanvas`. You can refer to [`exportToCanvas`](#exporttocanvas). |
| `mimeType` | `string` | `image/png` | Indicates the image format, this will be used when exporting as `png`. |
| `quality` | `number` | `0.92` | A value between `0` and `1` indicating the [image quality](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob#parameters). Applies only to `image/jpeg` / `image/webp` MIME types. This will be used when exporting as `png`. |
| `type` | 'png' &#124; 'svg' &#124; 'json' | _ | This determines the format to which the scene data should be `exported`. |
**How to use**
```js
import { exportToClipboard } from "@excalidraw/excalidraw";
```
Copies the scene data in the specified format (determined by `type`) to clipboard.
### Additional attributes of appState for export\* APIs
| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `exportBackground` | `boolean` | `true` | Indicates whether `background` should be exported |
| `viewBackgroundColor` | `string` | `#fff` | The default background color |
| `exportWithDarkMode` | `boolean` | `false` | Indicates whether to export with `dark` mode |
| `exportEmbedScene` | `boolean` | `false` | Indicates whether scene data should be embedded in `svg/png`. This will increase the image size. |
@@ -1,90 +0,0 @@
---
title: Restore Utilities
id: "restore"
---
### restoreAppState
**_Signature_**
<pre>
restoreAppState(appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["appState"]</a>,<br/>&nbsp; localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>
</pre>
**_How to use_**
```js
import { restoreAppState } from "@excalidraw/excalidraw";
```
This function will make sure all the `keys` have appropriate `values` in [appState](https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95) and if any key is missing, it will be set to its `default` value.
When `localAppState` is supplied, it's used in place of values that are missing (`undefined`) in `appState` instead of the defaults.
Use this as a way to not override user's defaults if you persist them.
You can pass `null` / `undefined` if not applicable.
### restoreElements
**_Signature_**
<pre>
restoreElements(
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ImportedDataState["elements"]</a>,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
refreshDimensions?: boolean<br/>
)
</pre>
**_How to use_**
```js
import { restoreElements } from "@excalidraw/excalidraw";
```
This function will make sure all properties of element is correctly set and if any attribute is missing, it will be set to its default value.
When `localElements` are supplied, they are used to ensure that existing restored elements reuse `version` (and increment it), and regenerate `versionNonce`.
Use this when you import elements which may already be present in the scene to ensure that you do not disregard the newly imported elements if you're using element version to detect the updates.
Parameter `refreshDimensions` indicates whether we should also `recalculate` text element dimensions. Defaults to `false`. Since this is a potentially costly operation, you may want to disable it if you restore elements in tight loops, such as during collaboration.
### restore
**_Signature_**
<pre>
restore(
data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState</a>,<br/>&nbsp;
localAppState: Partial&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>> | null | undefined,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null | undefined<br/>): <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L4">DataState</a>
)
</pre>
See [`restoreAppState()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreAppState) about `localAppState`, and [`restoreElements()`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#restoreElements) about `localElements`.
**_How to use_**
```js
import { restore } from "@excalidraw/excalidraw";
```
This function makes sure elements and state is set to appropriate values and set to default value if not present. It is a combination of [restoreElements](#restoreelements) and [restoreAppState](#restoreappstate).
### restoreLibraryItems
**_Signature_**
<pre>
restoreLibraryItems(libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L34">ImportedDataState["libraryItems"]</a>,<br/>&nbsp;
defaultStatus: "published" | "unpublished")
</pre>
**_How to use_**
```js
import { restoreLibraryItems } from "@excalidraw/excalidraw";
restoreLibraryItems(libraryItems, "unpublished");
```
This function normalizes library items elements, adding missing values when needed.
@@ -1,341 +0,0 @@
---
slug: /@excalidraw/excalidraw/api/utils
---
# Utils
These are pure Javascript functions exported from the @excalidraw/excalidraw [`@excalidraw/excalidraw`](https://npmjs.com/@excalidraw/excalidraw). If you want to export your drawings in different formats eg `png`, `svg` and more you can check out [Export Utilities](/docs/@excalidraw/excalidraw/API/utils/export). If you want to restore your drawings you can check out [Restore Utilities](/docs/@excalidraw/excalidraw/API/utils/restore).
### serializeAsJSON
Takes the scene elements and state and returns a JSON string. `Deleted` elements as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L42) source for details).
If you want to overwrite the `source` field in the `JSON` string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
**_Signature_**
<pre>
serializeAsJSON(&#123;<br/>&nbsp;
elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>,<br/>&nbsp;
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a>,<br/>
}): string
</pre>
**How to use**
```js
import { serializeAsJSON } from "@excalidraw/excalidraw";
```
### serializeLibraryAsJSON
Takes the `library` items and returns a `JSON` string.
If you want to overwrite the source field in the JSON string, you can set `window.EXCALIDRAW_EXPORT_SOURCE` to the desired value.
**_Signature_**
<pre>
serializeLibraryAsJSON(
libraryItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems[]</a>)
</pre>
**How to use**
```js
import { serializeLibraryAsJSON } from "@excalidraw/excalidraw";
```
#### isInvisiblySmallElement
Returns `true` if element is invisibly small (e.g. width & height are zero).
**_Signature_**
<pre>
isInvisiblySmallElement(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement</a>): boolean
</pre>
**How to use**
```js
import { isInvisiblySmallElement } from "@excalidraw/excalidraw";
```
### loadFromBlob
This function loads the scene data from the blob (or file). If you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`. Throws if blob doesn't contain valid scene data.
**How to use**
```js
import { loadFromBlob } from "@excalidraw/excalidraw";
const scene = await loadFromBlob(file, null, null);
excalidrawAPI.updateScene(scene);
```
**Signature**
<pre>
loadFromBlob(<br/>&nbsp;
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,<br/>&nbsp;
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
fileHandle?: FileSystemHandle | null <br/>
) => Promise&lt;<a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L61">RestoredDataState</a>>
</pre>
### loadLibraryFromBlob
This function loads the library from the blob. Additonally takes `defaultStatus` param which sets the default status for library item if not present, defaults to `unpublished`.
**How to use **
```js
import { loadLibraryFromBlob } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
loadLibraryFromBlob(blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>, defaultStatus: "published" | "unpublished")
</pre>
### loadSceneOrLibraryFromBlob
This function loads either scene or library data from the supplied blob. If the blob contains scene data, and you pass `localAppState`, `localAppState` value will be preferred over the `appState` derived from `blob`.
:::caution
Throws if blob doesn't contain valid `scene` data or `library` data.
:::
**How to use**
```js showLineNumbers
import { loadSceneOrLibraryFromBlob, MIME_TYPES } from "@excalidraw/excalidraw";
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
if (contents.type === MIME_TYPES.excalidraw) {
excalidrawAPI.updateScene(contents.data);
} else if (contents.type === MIME_TYPES.excalidrawlib) {
excalidrawAPI.updateLibrary(contents.data);
}
```
**_Signature_**
<pre>
loadSceneOrLibraryFromBlob(<br/>&nbsp;
blob: <a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>,
localAppState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a> | null,<br/>&nbsp;
localElements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a> | null,<br/>&nbsp;
fileHandle?: FileSystemHandle | null<br/>
) => Promise&lt;&#123; type: string, data: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/restore.ts#L53">RestoredDataState</a> | <a href="https://github.com/excalidraw/excalidraw/blob/master/src/data/types.ts#L33">ImportedLibraryState</a>}>
</pre>
### getFreeDrawSvgPath
This function returns the `free draw` svg path for the element.
**How to use**
```js
import { getFreeDrawSvgPath } from "@excalidraw/excalidraw";
```
**Signature**
<pre>
getFreeDrawSvgPath(element: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L182">ExcalidrawFreeDrawElement</a>)
</pre>
### isLinearElement
This function returns true if the element is `linear` type (`arrow` |`line`) else returns `false`.
**How to use**
```js
import { isLinearElement } from "@excalidraw/excalidraw";
```
**Signature**
```tsx
isLinearElement(elementType?: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L80">ExcalidrawElement</a>): boolean
```
### getNonDeletedElements
This function returns an array of `deleted` elements.
**How to use**
```js
import { getNonDeletedElements } from "@excalidraw/excalidraw";
```
**Signature**
<pre>
getNonDeletedElements(elements:<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114"> readonly ExcalidrawElement[]</a>): as readonly <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L125">NonDeletedExcalidrawElement[]</a>
</pre>
### mergeLibraryItems
This function merges two `LibraryItems` arrays, where unique items from `otherItems` are sorted first in the returned array.
```js
import { mergeLibraryItems } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
mergeLibraryItems(localItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>,<br/>&nbsp;
otherItems: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L200">LibraryItems</a>) => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L250">LibraryItems</a>
</pre>
### parseLibraryTokensFromUrl
Parses library parameters from URL if present (expects the `#addLibrary` hash key), and returns an object with the `libraryUrl` and `idToken`. Returns `null` if `#addLibrary` hash key not found.
**How to use**
```js
import { parseLibraryTokensFromUrl } from "@excalidraw/excalidraw";
```
**Signature**
```tsx
parseLibraryTokensFromUrl(): {
libraryUrl: string;
idToken: string | null;
} | null
```
### useHandleLibrary
A hook that automatically imports library from url if `#addLibrary` hash key exists on initial load, or when it changes during the editing session (e.g. when a user installs a new library), and handles initial library load if `getInitialLibraryItems` getter is supplied.
**How to use**
```js
import { useHandleLibrary } from "@excalidraw/excalidraw";
export const App = () => {
// ...
useHandleLibrary({ excalidrawAPI });
};
```
**Signature**
<pre>
useHandleLibrary(opts: &#123;<br/>&nbsp;
excalidrawAPI: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L494">ExcalidrawAPI</a>,<br/>&nbsp;
getInitialLibraryItems?: () => <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L253">LibraryItemsSource</a><br/>
});
</pre>
In the future, we will be adding support for handling `library` persistence to `browser storage` (or elsewhere).
### getSceneVersion
This function returns the current `scene` version.
**_Signature_**
<pre>
getSceneVersion(elements: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">ExcalidrawElement[]</a>)
</pre>
**How to use**
```js
import { getSceneVersion } from "@excalidraw/excalidraw";
```
### sceneCoordsToViewportCoords
This function returns equivalent `viewport` coords for the provided `scene` coords in params.
```js
import { sceneCoordsToViewportCoords } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
sceneCoordsToViewportCoords(&#123; sceneX: number, sceneY: number },<br/>&nbsp;
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): &#123; x: number, y: number }
</pre>
### viewportCoordsToSceneCoords
This function returns equivalent `scene` coords for the provided `viewport` coords in params.
```js
import { viewportCoordsToSceneCoords } from "@excalidraw/excalidraw";
```
**_Signature_**
<pre>
viewportCoordsToSceneCoords(&#123; clientX: number, clientY: number },<br/>&nbsp;
appState: <a href="https://github.com/excalidraw/excalidraw/blob/master/src/types.ts#L95">AppState</a><br/>): &#123;x: number, y: number}
</pre>
### useDevice
This hook can be used to check the type of device which is being used. It can only be used inside the `children` of `Excalidraw` component.
Open the `main menu` in the below example to view the footer.
```jsx live noInline
const MobileFooter = ({}) => {
const device = useDevice();
if (device.isMobile) {
return (
<Footer>
<button
className="custom-footer"
style={{ marginLeft: "20px", height: "2rem" }}
onClick={() => alert("This is custom footer in mobile menu")}
>
custom footer
</button>
</Footer>
);
}
return null;
};
const App = () => (
<div style={{ height: "400px" }}>
<Excalidraw>
<MainMenu>
<MainMenu.Item> Item1 </MainMenu.Item>
<MainMenu.Item> Item 2 </MainMenu.Item>
<MobileFooter />
</MainMenu>
</Excalidraw>
</div>
);
// Need to render when code is span across multiple components
// in Live Code blocks editor
render(<App />);
```
The `device` has the following `attributes`
| Name | Type | Description |
| --- | --- | --- |
| `isSmScreen` | `boolean` | Set to `true` when the device small screen is small (Width < `640px` ) |
| `isMobile` | `boolean` | Set to `true` when the device is `mobile` |
| `isTouchScreen` | `boolean` | Set to `true` for `touch` devices |
| `canDeviceFitSidebar` | `boolean` | Implies whether there is enough space to fit the `sidebar` |
@@ -1,49 +0,0 @@
# Customizing Styles
Excalidraw is using CSS variables to style certain components. To override them, you should set your own on the `.excalidraw` and `.excalidraw.theme--dark` (for dark mode variables) selectors.
Make sure the selector has higher specificity, e.g. by prefixing it with your app's selector:
```css
.your-app .excalidraw {
--color-primary: red;
}
.your-app .excalidraw.theme--dark {
--color-primary: pink;
}
```
Most notably, you can customize the primary colors, by overriding these variables:
- `--color-primary`
- `--color-primary-darker`
- `--color-primary-darkest`
- `--color-primary-light`
- `--color-primary-contrast-offset` — a slightly darker (in light mode), or lighter (in dark mode) `--color-primary` color to fix contrast issues (see [Chubb illusion](https://en.wikipedia.org/wiki/Chubb_illusion)). It will fall back to `--color-primary` if not present.
For a complete list of variables, check [theme.scss](https://github.com/excalidraw/excalidraw/blob/master/src/css/theme.scss), though most of them will not make sense to override.
```css showLineNumbers
.custom-styles .excalidraw {
--color-primary: #fcc6d9;
--color-primary-darker: #f783ac;
--color-primary-darkest: #e64980;
--color-primary-light: #f2a9c4;
}
.custom-styles .excalidraw.theme--dark {
--color-primary: #d494aa;
--color-primary-darker: #d64c7e;
--color-primary-darkest: #e86e99;
--color-primary-light: #dcbec9;
}
```
```tsx live
function App() {
return (
<div style={{ height: "500px" }} className="custom-styles">
<Excalidraw />
</div>
);
}
```
@@ -1,59 +0,0 @@
---
pagination_prev: "@excalidraw/excalidraw/installation"
---
# Development
This page relates to developing the `@excalidraw/excalidraw` package itself.
## Example app
To start the example app using the `@excalidraw/excalidraw` package, follow the below steps:
1. Install the dependencies
```bash
cd src/packages/excalidraw && yarn
```
2. Start the example app
```bash
yarn start
```
[http://localhost:3001](http://localhost:3001) will open in your default browser.
The example is same as the [codesandbox example](https://ehlz3.csb.app/)
## Releasing
### Create a test release
You can create a test release by posting the below comment in your pull request:
```bash
@excalibot trigger release
```
Once the version is released `@excalibot` will post a comment with the release version.
### Creating a production release
To release the next stable version follow the below steps:
```bash
yarn prerelease version
```
You need to pass the `version` for which you want to create the release. This will make the changes needed before making the release like updating `package.json`, `changelog` and more.
The next step is to run the `release` script:
```bash
yarn release
```
This will publish the package.
Right now there are two steps to create a production release but once this works fine these scripts will be combined and more automation will be done.
@@ -1,9 +0,0 @@
# FAQ
### Does this package support collaboration ?
No, Excalidraw package doesn't come with collaboration built in, since the implementation is specific to each host app. We expose APIs which you can use to communicate with Excalidraw which you can use to implement it. You can check our own implementation [here](https://github.com/excalidraw/excalidraw/blob/master/src/excalidraw-app/index.tsx). Here is a [detailed answer](https://github.com/excalidraw/excalidraw/discussions/3879#discussioncomment-1110524) on how you can achieve the same.
## Need help?
Check out the existing [Q&A](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw). If you have any queries or need help, ask us [here](https://github.com/excalidraw/excalidraw/discussions?discussions_q=label%3Apackage%3Aexcalidraw).
@@ -1,43 +0,0 @@
# Installation
**Excalidraw** is published to npm as a component you can directly embed in your projects.
Using `npm`:
```bash
npm install react react-dom @excalidraw/excalidraw
```
or `yarn`:
```bash
yarn add react react-dom @excalidraw/excalidraw
```
:::tip
**If you don't want to wait for the next stable release and try out the unreleased changes you can use `@excalidraw/excalidraw@next`.**
:::
### Static assets
Excalidraw depends on assets such as localization files (if you opt to use them), fonts, and others.
By default these assets are loaded from a public CDN [`https://unpkg.com/@excalidraw/excalidraw/dist/`](https://unpkg.com/@excalidraw/excalidraw/dist), so you don't need to do anything on your end.
However, if you want to host these files yourself, you can find them in your `node_modules/@excalidraw/excalidraw/dist` directory, in folders `excalidraw-assets` (for production) and `excalidraw-assets-dev` (for development).
Copy these folders to your static assets directory, and add a `window.EXCALIDRAW_ASSET_PATH` variable in your `index.html` or `index.js` entry file pointing to your public assets path (relative). For example, if you serve your assets from the root of your hostname, you would do:
```js
window.EXCALIDRAW_ASSET_PATH = "/";
```
### Dimensions of Excalidraw
Excalidraw takes _100%_ of `width` and `height` of the containing block so make sure the container in which you render Excalidraw has non zero dimensions.
### Demo
[Try here](https://codesandbox.io/s/excalidraw-ehlz3).
@@ -1,131 +0,0 @@
# Integration
## Module bundler
If you are using a module bundler (for instance, Webpack), you can import it as an ES6 module as shown below
```js
import { Excalidraw } from "@excalidraw/excalidraw";
```
:::info
Throughout the documentation we use live, editable Excalidraw examples like the one shown below.
While we aim for the examples to closely reflect what you'd get if you rendered it yourself, we actually initialize it with some props behind the scenes.
For example, we're passing a `theme` prop to it based on the current color theme of the docs you're just reading.
:::
```jsx live
function App() {
return (
<>
<h1 style={{ textAlign: "center" }}>Excalidraw Example</h1>
<div style={{ height: "500px" }}>
<Excalidraw />
</div>
</>
);
}
```
### Rendering Excalidraw only on client
Since _Excalidraw_ doesn't support server side rendering, you should render the component once the host is `mounted`.
The following worfklow shows one way how to render Excalidraw on Next.js. We'll add more detailed and alternative Next.js examples, soon.
```jsx showLineNumbers
import { useState, useEffect } from "react";
export default function App() {
const [Excalidraw, setExcalidraw] = useState(null);
useEffect(() => {
import("@excalidraw/excalidraw").then((comp) => setExcalidraw(comp.Excalidraw));
}, []);
return <>{Excalidraw && <Excalidraw />}</>;
}
```
The `types` are available at `@excalidraw/excalidraw/types`, you can view [example for typescript](https://codesandbox.io/s/excalidraw-types-9h2dm)
## Browser
To use it in a browser directly:
For development use :point_down:
```js
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.development.js"
></script>
```
For production use :point_down:
```js
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.production.min.js"
></script>
```
You will need to make sure `react`, `react-dom` is available as shown in the below example. For prod please use the production versions of `react`, `react-dom`.
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
<Tabs>
<TabItem value="html" label="html">
```html
<!DOCTYPE html>
<html>
<head>
<title>Excalidraw in browser</title>
<meta charset="UTF-8" />
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
<script
type="text/javascript"
src="https://unpkg.com/@excalidraw/excalidraw/dist/excalidraw.development.js"
></script>
</head>
<body>
<div class="container">
<h1>Excalidraw Embed Example</h1>
<div id="app"></div>
</div>
<script type="text/javascript" src="src/index.js"></script>
</body>
</html>
```
</TabItem>
<TabItem value="js" label="Javascript">
```js showLineNumbers
const App = () => {
return React.createElement(
React.Fragment,
null,
React.createElement(
"div",
{
style: { height: "500px" },
},
React.createElement(ExcalidrawLib.Excalidraw),
),
);
};
const excalidrawWrapper = document.getElementById("app");
const root = ReactDOM.createRoot(excalidrawWrapper);
root.render(React.createElement(App));
```
</TabItem>
</Tabs>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

+6
View File
@@ -0,0 +1,6 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw Readme](https://github.com/excalidraw/excalidraw/blob/master/README.md).
+8
View File
@@ -0,0 +1,8 @@
---
sidebar_position: 1
title: Introduction
---
Want to integrate Excalidraw into your app? Head over to the [package docs](/docs/package/overview).
If you're looking into the Excalidraw codebase itself, start [here](/docs/codebase/overview).
@@ -1,72 +0,0 @@
# Contributing
Pull requests are welcome. For major changes, please [open an issue](https://github.com/excalidraw/excalidraw/issues/new) first to discuss what you would like to change.
## Setup
### Option 1 - Manual
1. Fork and clone the repo
1. Run `yarn` to install dependencies
1. Create a branch for your PR with `git checkout -b your-branch-name`
> To keep `master` branch pointing to remote repository and make pull requests from branches on your fork. To do this, run:
>
> ```bash
> git remote add upstream https://github.com/excalidraw/excalidraw.git
> git fetch upstream
> git branch --set-upstream-to=upstream/master master
> ```
### Option 2 - CodeSandbox
1. Go to https://codesandbox.io/p/github/excalidraw/excalidraw
1. Connect your GitHub account
1. Go to Git tab on left side
1. Tap on `Fork Sandbox`
1. Write your code
1. Commit and PR automatically
## Pull Request Guidelines
Don't worry if you get any of the below wrong, or if you don't know how. We'll gladly help out.
### Title
Make sure the title starts with a semantic prefix:
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation only changes
- **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- **refactor**: A code change that neither fixes a bug nor adds a feature
- **perf**: A code change that improves performance
- **test**: Adding missing tests or correcting existing tests
- **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
- **ci**: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
- **chore**: Other changes that don't modify src or test files
- **revert**: Reverts a previous commit
### Changelog
Add a brief description of your pull request to the changelog located here: [changelog](https://github.com/excalidraw/excalidraw/blob/master/CHANGELOG.md)
Notes:
- Make sure to prepend to the section corresponding with the semantic prefix you selected in the title
- Link to your pull request - this will require updating the CHANGELOG _after_ creating the pull request
### Testing
Once you submit your pull request it will automatically be tested. Be sure to check the results of the test and fix any issues that arise.
It's also a good idea to consider if your change should include additional tests. This is highly recommended for new features or bug-fixes. For example, it's good practice to create a test for each bug you fix which ensures that we don't regress the code in the future.
Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well.
## Translating
To translate Excalidraw into other languages, please visit [our Crowdin page](https://crowdin.com/project/excalidraw). To add a new language, [open an issue](https://github.com/excalidraw/excalidraw/issues/new) so we can get things set up on our end first.
Translations will be available on the app if they exceed a certain threshold of completion (currently **85%**).
-102
View File
@@ -1,102 +0,0 @@
# Development
## Code Sandbox
- Go to https://codesandbox.io/p/github/excalidraw/excalidraw
- You may need to sign in with GitHub and reload the page
- You can start coding instantly, and even send PRs from there!
## Local Installation
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
### Requirements
- [Node.js](https://nodejs.org/en/)
- [Yarn](https://yarnpkg.com/getting-started/install) (v1 or v2.4.2+)
- [Git](https://git-scm.com/downloads)
### Clone the repo
```bash
git clone https://github.com/excalidraw/excalidraw.git
```
### Install the dependencies
```bash
yarn
```
### Start the server
```bash
yarn start
```
Now you can open [http://localhost:3000](http://localhost:3000) and start coding in your favorite code editor.
## Collaboration
For collaboration, you will need to set up [collab server](https://github.com/excalidraw/excalidraw-room) in local.
## Commands
### Install the dependencies
```bash
yarn
```
### Run the project
```bash
yarn start
```
### Reformat all files with Prettier
```bash
yarn fix
```
### Run tests
```bash
yarn test
```
### Update test snapshots
```bash
yarn test:update
```
### Test for formatting with Prettier
```bash
yarn test:code
```
### Docker Compose
You can use docker-compose to work on Excalidraw locally if you don't want to setup a Node.js env.
```bash
docker-compose up --build -d
```
## Self-hosting
We publish a Docker image with the Excalidraw client at [excalidraw/excalidraw](https://hub.docker.com/r/excalidraw/excalidraw). You can use it to self-host your own client under your own domain, on Kubernetes, AWS ECS, etc.
```bash
docker build -t excalidraw/excalidraw .
docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latest
```
The Docker image is free of analytics and other tracking libraries.
**At the moment, self-hosting your own instance doesn't support sharing or collaboration features.**
We are working towards providing a full-fledged solution for self-hosting your own Excalidraw.
@@ -1,16 +0,0 @@
---
title: Introduction
slug: ../
---
## Try now
Go to [excalidraw.com](https://excalidraw.com) to start sketching.
## How are these docs structured
These docs are focused on developers, and structured in the following way:
- [Introduction](/docs/) — development setup and introduction.
- [@excalidraw/excalidraw](/docs/@excalidraw/excalidraw/installation) — docs for the npm package to help you integrate Excalidraw into your own app.
- Editor — IN PROGRESS. Docs describing the internals of the Excalidraw editor to help in contributing to the codebase.
+6
View File
@@ -0,0 +1,6 @@
---
sidebar_position: 1
title: Overview
---
In development. For now, refer to [excalidraw package readme](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md).
+14 -38
View File
@@ -1,12 +1,15 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require("prism-react-renderer/themes/github");
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "Excalidraw developer docs",
tagline:
"For Excalidraw contributors or those integrating the Excalidraw editor",
url: "https://docs.excalidraw.com",
url: "https://docs.excalidraw.com.com",
baseUrl: "/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
@@ -30,16 +33,10 @@ const config = {
docs: {
sidebarPath: require.resolve("./sidebars.js"),
// Please change this to your repo.
editUrl:
"https://github.com/excalidraw/excalidraw/tree/master/dev-docs/",
showLastUpdateAuthor: true,
showLastUpdateTime: true,
editUrl: "https://github.com/excalidraw/docs/tree/master/",
},
theme: {
customCss: [
require.resolve("./src/css/custom.scss"),
require.resolve("../src/packages/excalidraw/example/App.scss"),
],
customCss: require.resolve("./src/css/custom.css"),
},
}),
],
@@ -48,20 +45,18 @@ const config = {
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
colorMode: {
respectPrefersColorScheme: true,
},
navbar: {
title: "Excalidraw",
title: "Excalidraw Docs",
logo: {
alt: "Excalidraw Logo",
src: "img/logo.svg",
},
items: [
{
to: "/docs",
type: "doc",
docId: "get-started",
position: "left",
label: "Docs",
label: "Get started",
},
{
to: "https://blog.excalidraw.com",
@@ -83,7 +78,7 @@ const config = {
items: [
{
label: "Get Started",
to: "/docs",
to: "/docs/get-started",
},
],
},
@@ -98,10 +93,6 @@ const config = {
label: "Twitter",
href: "https://twitter.com/excalidraw",
},
{
label: "Linkedin",
href: "https://www.linkedin.com/company/excalidraw",
},
],
},
{
@@ -118,28 +109,13 @@ const config = {
],
},
],
copyright: `Copyright © 2023 Excalidraw community. Built with Docusaurus ❤️`,
copyright: `Made with ❤️ Built with Docusaurus`,
},
prism: {
theme: require("prism-react-renderer/themes/dracula"),
},
image: "img/og-image.png",
docs: {
sidebar: {
hideable: true,
},
},
tableOfContents: {
maxHeadingLevel: 4,
},
algolia: {
appId: "8FEAOD28DI",
apiKey: "4b07cca33ff2d2919bc95ff98f148e9e",
indexName: "excalidraw",
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
themes: ["@docusaurus/theme-live-codeblock"],
plugins: ["docusaurus-plugin-sass"],
};
module.exports = config;
+3 -7
View File
@@ -15,17 +15,13 @@
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@docusaurus/theme-live-codeblock": "2.2.0",
"@excalidraw/excalidraw": "0.14.2",
"@docusaurus/core": "2.0.0-rc.1",
"@docusaurus/preset-classic": "2.0.0-rc.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"docusaurus-plugin-sass": "0.2.3",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass": "1.57.1"
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-rc.1",
+9 -76
View File
@@ -13,86 +13,19 @@
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
docs: [
{
type: "category",
label: "Introduction",
link: {
type: "doc",
id: "introduction/get-started",
},
items: ["introduction/development", "introduction/contributing"],
},
// By default, Docusaurus generates a sidebar from the docs folder structure
tutorialSidebar: [{ type: "autogenerated", dirName: "." }],
// But you can create a sidebar manually
/*
tutorialSidebar: [
{
type: "category",
label: "@excalidraw/excalidraw",
collapsed: false,
items: [
"@excalidraw/excalidraw/installation",
"@excalidraw/excalidraw/integration",
"@excalidraw/excalidraw/customizing-styles",
{
type: "category",
label: "API",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/api-intro",
},
items: [
{
type: "category",
label: "Props",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/props/props",
},
items: [
"@excalidraw/excalidraw/api/props/initialdata",
"@excalidraw/excalidraw/api/props/ref",
"@excalidraw/excalidraw/api/props/render-props",
"@excalidraw/excalidraw/api/props/ui-options",
],
},
{
type: "category",
label: "Children Components",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/children-components/children-components-intro",
},
items: [
"@excalidraw/excalidraw/api/children-components/main-menu",
"@excalidraw/excalidraw/api/children-components/welcome-screen",
"@excalidraw/excalidraw/api/children-components/footer",
"@excalidraw/excalidraw/api/children-components/live-collaboration-trigger",
],
},
{
type: "category",
label: "Utils",
link: {
type: "doc",
id: "@excalidraw/excalidraw/api/utils/utils-intro",
},
items: [
"@excalidraw/excalidraw/api/utils/export",
"@excalidraw/excalidraw/api/utils/restore",
],
},
{
type: "category",
label: "Constants",
link: { type: "doc", id: "@excalidraw/excalidraw/api/constants" },
items: [],
},
],
},
"@excalidraw/excalidraw/faq",
"@excalidraw/excalidraw/development",
],
type: 'category',
label: 'Tutorial',
items: ['hello'],
},
],
*/
};
module.exports = sidebars;
-15
View File
@@ -1,15 +0,0 @@
import React from "react";
export default function Highlight({ children }) {
return (
<span
style={{
backgroundColor: "#7874e8",
borderRadius: "2px",
color: "#fff",
padding: "0.2rem",
}}
>
{children}
</span>
);
}
@@ -14,13 +14,11 @@
--ifm-color-primary-lighter: #5b57d1;
--ifm-color-primary-lightest: #5b57d1;
--ifm-code-font-size: 95%;
scrollbar-gutter: stable;
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
--ifm-color-primary: #8784e3;
--ifm-color-primary: #5650f0;
--ifm-color-primary-dark: #4b46d8;
--ifm-color-primary-darker: #4b46d8;
--ifm-color-primary-darkest: #3e39be;
@@ -43,59 +41,3 @@
[data-theme="dark"] .navbar__logo {
filter: invert(93%) hue-rotate(180deg);
}
pre a {
color: #5dccff;
&:hover {
color: #8fd3f3;
}
}
.custom-button {
height: 40px;
max-width: 200px;
margin: 10px 0;
padding: 5px;
background: #70b1ec;
color: white;
font-weight: bold;
border: none;
}
.custom-styles .excalidraw {
--color-primary: #fcc6d9;
--color-primary-darker: #f783ac;
--color-primary-darkest: #e64980;
--color-primary-light: #f2a9c4;
}
.custom-styles .excalidraw.theme--dark {
--color-primary: #d494aa;
--color-primary-darker: #d64c7e;
--color-primary-darkest: #e86e99;
--color-primary-light: #dcbec9;
}
/* The global css conflicts with Excal css hence overriding */
.excalidraw .context-menu-item__shortcut {
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
}
.excalidraw .Stats table td,
.excalidraw .Stats table th,
.excalidraw .Stats table tr {
border: none;
background: none;
padding: 0;
}
.excalidraw .Stats .close {
padding: 0;
}
.excalidraw .Stats table {
display: table;
}
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
import React from "react";
import clsx from "clsx";
import Layout from "@theme/Layout";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import styles from "./index.module.css";
import HomepageFeatures from "@site/src/components/Homepage";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/get-started"
>
Get started
</Link>
</div>
</div>
</header>
);
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}
+9 -2
View File
@@ -14,7 +14,10 @@ function HomepageHeader() {
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link className="button button--secondary button--lg" to="/docs">
<Link
className="button button--secondary button--lg"
to="/docs/get-started"
>
Get started
</Link>
</div>
@@ -24,8 +27,12 @@ function HomepageHeader() {
}
export default function Home() {
const { siteConfig } = useDocusaurusContext();
return (
<Layout description="For Excalidraw contributors or those integrating the Excalidraw editor">
<Layout
title={`Hello from ${siteConfig.title}`}
description="Description will go into a meta tag in <head />"
>
<HomepageHeader />
<main>
<HomepageFeatures />
-11
View File
@@ -1,11 +0,0 @@
// Import the original mapper
import MDXComponents from "@theme-original/MDXComponents";
import Highlight from "@site/src/components/Highlight";
export default {
// Re-use the default mapping
...MDXComponents,
// Map the "highlight" tag to our <Highlight /> component!
// `Highlight` will receive all props that were passed to `highlight` in MDX
highlight: Highlight,
};
@@ -1,29 +0,0 @@
import React from "react";
import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment";
import initialData from "@site/src/initialData";
import { useColorMode } from "@docusaurus/theme-common";
let ExcalidrawComp = {};
if (ExecutionEnvironment.canUseDOM) {
ExcalidrawComp = require("@excalidraw/excalidraw");
}
const Excalidraw = React.forwardRef((props, ref) => {
const { colorMode } = useColorMode();
return <ExcalidrawComp.Excalidraw theme={colorMode} {...props} ref={ref} />;
});
// Add react-live imports you need here
const ExcalidrawScope = {
React,
...React,
Excalidraw,
Footer: ExcalidrawComp.Footer,
useDevice: ExcalidrawComp.useDevice,
MainMenu: ExcalidrawComp.MainMenu,
WelcomeScreen: ExcalidrawComp.WelcomeScreen,
LiveCollaborationTrigger: ExcalidrawComp.LiveCollaborationTrigger,
Sidebar: ExcalidrawComp.Sidebar,
exportToCanvas: ExcalidrawComp.exportToCanvas,
initialData,
};
export default ExcalidrawScope;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

+164 -392
View File
@@ -1191,10 +1191,10 @@
"@docsearch/css" "3.1.1"
algoliasearch "^4.0.0"
"@docusaurus/core@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.2.0.tgz#64c9ee31502c23b93c869f8188f73afaf5fd4867"
integrity sha512-Vd6XOluKQqzG12fEs9prJgDtyn6DPok9vmUWDR2E6/nV5Fl9SVkhEQOBxwObjk3kQh7OY7vguFaLh0jqdApWsA==
"@docusaurus/core@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-2.0.0-rc.1.tgz#828d93d241171565d8947a9ab404091e04759141"
integrity sha512-b9FX0Z+EddfQ6wAiNh+Wx4fysKfcvEcWJrZ5USROn3C+EVU5P4luaa8mwWK//O+hTwD9ur7/A44IZ/tWCTAoLQ==
dependencies:
"@babel/core" "^7.18.6"
"@babel/generator" "^7.18.7"
@@ -1206,13 +1206,13 @@
"@babel/runtime" "^7.18.6"
"@babel/runtime-corejs3" "^7.18.6"
"@babel/traverse" "^7.18.8"
"@docusaurus/cssnano-preset" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/cssnano-preset" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/react-loadable" "5.5.2"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@slorber/static-site-generator-webpack-plugin" "^4.0.7"
"@svgr/webpack" "^6.2.1"
autoprefixer "^10.4.7"
@@ -1268,33 +1268,33 @@
webpack-merge "^5.8.0"
webpackbar "^5.0.2"
"@docusaurus/cssnano-preset@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.2.0.tgz#fc05044659051ae74ab4482afcf4a9936e81d523"
integrity sha512-mAAwCo4n66TMWBH1kXnHVZsakW9VAXJzTO4yZukuL3ro4F+JtkMwKfh42EG75K/J/YIFQG5I/Bzy0UH/hFxaTg==
"@docusaurus/cssnano-preset@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-2.0.0-rc.1.tgz#76bbd7f6912779a0667f8f2fd8fc1a05618a6148"
integrity sha512-9/KmQvF+eTlMqUqG6UcXbRgxbGed/8bQInXuKEs+95/jI6jO/3xSzuRwuHHHP0naUvSVWjnNI9jngPrQerXE5w==
dependencies:
cssnano-preset-advanced "^5.3.8"
postcss "^8.4.14"
postcss-sort-media-queries "^4.2.1"
tslib "^2.4.0"
"@docusaurus/logger@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.2.0.tgz#ea2f7feda7b8675485933b87f06d9c976d17423f"
integrity sha512-DF3j1cA5y2nNsu/vk8AG7xwpZu6f5MKkPPMaaIbgXLnWGfm6+wkOeW7kNrxnM95YOhKUkJUophX69nGUnLsm0A==
"@docusaurus/logger@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-2.0.0-rc.1.tgz#db95e9b15bc243695830a5b791c0eff705ef1b54"
integrity sha512-daa3g+SXuO9K60PVMiSUmDEK9Vro+Ed7i7uF8CH6QQJLcNZy/zJc0Xz62eH7ip1x77fmeb6Rg4Us1TqTFc9AbQ==
dependencies:
chalk "^4.1.2"
tslib "^2.4.0"
"@docusaurus/mdx-loader@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.2.0.tgz#fd558f429e5d9403d284bd4214e54d9768b041a0"
integrity sha512-X2bzo3T0jW0VhUU+XdQofcEeozXOTmKQMvc8tUnWRdTnCvj4XEcBVdC3g+/jftceluiwSTNRAX4VBOJdNt18jA==
"@docusaurus/mdx-loader@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-2.0.0-rc.1.tgz#e78d7d416aacc289f2427c5ccdb9145820acb0cb"
integrity sha512-8Fg0c/ceu39knmr7w0dutm7gq3YxKYCqWVS2cB/cPATzChCCNH/AGLfBT6sz/Z4tjVXE+NyREq2pfOFvkhjVXg==
dependencies:
"@babel/parser" "^7.18.8"
"@babel/traverse" "^7.18.8"
"@docusaurus/logger" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@mdx-js/mdx" "^1.6.22"
escape-html "^1.0.3"
file-loader "^6.2.0"
@@ -1323,32 +1323,18 @@
react-helmet-async "*"
react-loadable "npm:@docusaurus/react-loadable@5.5.2"
"@docusaurus/module-type-aliases@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-2.2.0.tgz#1e23e54a1bbb6fde1961e4fa395b1b69f4803ba5"
integrity sha512-wDGW4IHKoOr9YuJgy7uYuKWrDrSpsUSDHLZnWQYM9fN7D5EpSmYHjFruUpKWVyxLpD/Wh0rW8hYZwdjJIQUQCQ==
"@docusaurus/plugin-content-blog@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.0.0-rc.1.tgz#8ae5d5ec2da08c583a057bf2754a5b9278b3eb08"
integrity sha512-BVVrAGZujpjS/0rarY2o24rlylRRh2NZuM65kg0JNkkViF79SeEHsepog7IuHyoqGWPm1N/I7LpEp7k+gowZzQ==
dependencies:
"@docusaurus/react-loadable" "5.5.2"
"@docusaurus/types" "2.2.0"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
"@types/react-router-dom" "*"
react-helmet-async "*"
react-loadable "npm:@docusaurus/react-loadable@5.5.2"
"@docusaurus/plugin-content-blog@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-2.2.0.tgz#dc55982e76771f4e678ac10e26d10e1da2011dc1"
integrity sha512-0mWBinEh0a5J2+8ZJXJXbrCk1tSTNf7Nm4tYAl5h2/xx+PvH/Bnu0V+7mMljYm/1QlDYALNIIaT/JcoZQFUN3w==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
cheerio "^1.0.0-rc.12"
feed "^4.2.2"
fs-extra "^10.1.0"
@@ -1359,18 +1345,18 @@
utility-types "^3.10.0"
webpack "^5.73.0"
"@docusaurus/plugin-content-docs@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.2.0.tgz#0fcb85226fcdb80dc1e2d4a36ef442a650dcc84d"
integrity sha512-BOazBR0XjzsHE+2K1wpNxz5QZmrJgmm3+0Re0EVPYFGW8qndCWGNtXW/0lGKhecVPML8yyFeAmnUCIs7xM2wPw==
"@docusaurus/plugin-content-docs@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-2.0.0-rc.1.tgz#2dda88166bf21b0eeb3821ef748059b20c8c49f7"
integrity sha512-Yk5Hu6uaw3tRplzJnbDygwRhmZ3PCzEXD4SJpBA6cPC73ylfqOEh6qhiU+BWhMTtDXNhY+athk5Kycfk3DW1aQ==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/module-type-aliases" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/module-type-aliases" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@types/react-router-config" "^5.0.6"
combine-promises "^1.1.0"
fs-extra "^10.1.0"
@@ -1381,84 +1367,84 @@
utility-types "^3.10.0"
webpack "^5.73.0"
"@docusaurus/plugin-content-pages@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.2.0.tgz#e3f40408787bbe229545dd50595f87e1393bc3ae"
integrity sha512-+OTK3FQHk5WMvdelz8v19PbEbx+CNT6VSpx7nVOvMNs5yJCKvmqBJBQ2ZSxROxhVDYn+CZOlmyrC56NSXzHf6g==
"@docusaurus/plugin-content-pages@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-2.0.0-rc.1.tgz#2be82f53d6c77e6aa66787726c30dc60b210e6f8"
integrity sha512-FdO79WC5hfWDQu3/CTFLRQzTNc0e5n+HNzavm2MNkSzGV08BFJ6RAkbPbtra5CWef+6iXZav6D/tzv2jDPvLzA==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
fs-extra "^10.1.0"
tslib "^2.4.0"
webpack "^5.73.0"
"@docusaurus/plugin-debug@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.2.0.tgz#b38741d2c492f405fee01ee0ef2e0029cedb689a"
integrity sha512-p9vOep8+7OVl6r/NREEYxf4HMAjV8JMYJ7Bos5fCFO0Wyi9AZEo0sCTliRd7R8+dlJXZEgcngSdxAUo/Q+CJow==
"@docusaurus/plugin-debug@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-2.0.0-rc.1.tgz#73c06ad08d66810941e456d50b07be008f5235cb"
integrity sha512-aOsyYrPMbnsyqHwsVZ+0frrMRtnYqm4eaJpG4sC/6LYAJ07IDRQ9j3GOku2dKr5GsFK1Vx7VlE6ZLwe0MaGstg==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
fs-extra "^10.1.0"
react-json-view "^1.21.3"
tslib "^2.4.0"
"@docusaurus/plugin-google-analytics@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.2.0.tgz#63c7137eff5a1208d2059fea04b5207c037d7954"
integrity sha512-+eZVVxVeEnV5nVQJdey9ZsfyEVMls6VyWTIj8SmX0k5EbqGvnIfET+J2pYEuKQnDIHxy+syRMoRM6AHXdHYGIg==
"@docusaurus/plugin-google-analytics@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-2.0.0-rc.1.tgz#0136cc7534573ca56e023178ec2bda5c1e89ce71"
integrity sha512-f+G8z5OJWfg5QqWDLIdcN2SDoK5J5Gg8HMrqCI6Pfl+rxPb5I1niA+/UkAM+kMCpnekvhSt5AWz2fgkRenkPLA==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
tslib "^2.4.0"
"@docusaurus/plugin-google-gtag@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.2.0.tgz#7b086d169ac5fe9a88aca10ab0fd2bf00c6c6b12"
integrity sha512-6SOgczP/dYdkqUMGTRqgxAS1eTp6MnJDAQMy8VCF1QKbWZmlkx4agHDexihqmYyCujTYHqDAhm1hV26EET54NQ==
"@docusaurus/plugin-google-gtag@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-2.0.0-rc.1.tgz#61698fdc41a4ace912fb8f6c834efd288edad3c0"
integrity sha512-yE1Et9hhhX9qMRnMJzpNq0854qIYiSEc2dZaXNk537HN7Q0rKkr/YONUHz2iqNYwPX2hGOY4LdpTxlMP88uVhA==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
tslib "^2.4.0"
"@docusaurus/plugin-sitemap@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.2.0.tgz#876da60937886032d63143253d420db6a4b34773"
integrity sha512-0jAmyRDN/aI265CbWZNZuQpFqiZuo+5otk2MylU9iVrz/4J7gSc+ZJ9cy4EHrEsW7PV8s1w18hIEsmcA1YgkKg==
"@docusaurus/plugin-sitemap@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-2.0.0-rc.1.tgz#0b638e774b253d90e9f2d11663e961250f557bc4"
integrity sha512-5JmbNpssUF03odFM4ArvIsrO9bv7HnAJ0VtefXhh0WBpaFs8NgI3rTkCTFimvtRQjDR9U2bh23fXz2vjQQz6oA==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
fs-extra "^10.1.0"
sitemap "^7.1.1"
tslib "^2.4.0"
"@docusaurus/preset-classic@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.2.0.tgz#bece5a043eeb74430f7c6c7510000b9c43669eb7"
integrity sha512-yKIWPGNx7BT8v2wjFIWvYrS+nvN04W+UameSFf8lEiJk6pss0kL6SG2MRvyULiI3BDxH+tj6qe02ncpSPGwumg==
"@docusaurus/preset-classic@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-2.0.0-rc.1.tgz#5e5b1cf80b3dd4e2c3f824c78a111f105858d853"
integrity sha512-5jjTVZkhArjyoNHwCI9x4PSG0zPmBJILjZLVrxPcHpm/K0ltkYcp6J3GxYpf5EbMuOh5+yCWM63cSshGcNOo3Q==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/plugin-content-blog" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/plugin-content-pages" "2.2.0"
"@docusaurus/plugin-debug" "2.2.0"
"@docusaurus/plugin-google-analytics" "2.2.0"
"@docusaurus/plugin-google-gtag" "2.2.0"
"@docusaurus/plugin-sitemap" "2.2.0"
"@docusaurus/theme-classic" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-search-algolia" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/plugin-content-blog" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/plugin-content-pages" "2.0.0-rc.1"
"@docusaurus/plugin-debug" "2.0.0-rc.1"
"@docusaurus/plugin-google-analytics" "2.0.0-rc.1"
"@docusaurus/plugin-google-gtag" "2.0.0-rc.1"
"@docusaurus/plugin-sitemap" "2.0.0-rc.1"
"@docusaurus/theme-classic" "2.0.0-rc.1"
"@docusaurus/theme-common" "2.0.0-rc.1"
"@docusaurus/theme-search-algolia" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/react-loadable@5.5.2", "react-loadable@npm:@docusaurus/react-loadable@5.5.2":
version "5.5.2"
@@ -1468,23 +1454,23 @@
"@types/react" "*"
prop-types "^15.6.2"
"@docusaurus/theme-classic@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.2.0.tgz#a048bb1bc077dee74b28bec25f4b84b481863742"
integrity sha512-kjbg/qJPwZ6H1CU/i9d4l/LcFgnuzeiGgMQlt6yPqKo0SOJIBMPuz7Rnu3r/WWbZFPi//o8acclacOzmXdUUEg==
"@docusaurus/theme-classic@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-2.0.0-rc.1.tgz#4ab30745e6b03b0f277837debae786a0a83aee6a"
integrity sha512-qNiz7ieeq3AC+V8TbW6S63pWLJph1CbzWDDPTqxDLHgA8VQaNaSmJM8S92pH+yKALRb9u14ogjjYYc75Nj2JmQ==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/module-type-aliases" "2.2.0"
"@docusaurus/plugin-content-blog" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/plugin-content-pages" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-translations" "2.2.0"
"@docusaurus/types" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-common" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/module-type-aliases" "2.0.0-rc.1"
"@docusaurus/plugin-content-blog" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/plugin-content-pages" "2.0.0-rc.1"
"@docusaurus/theme-common" "2.0.0-rc.1"
"@docusaurus/theme-translations" "2.0.0-rc.1"
"@docusaurus/types" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-common" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
"@mdx-js/react" "^1.6.22"
clsx "^1.2.1"
copy-text-to-clipboard "^3.0.1"
@@ -1499,17 +1485,17 @@
tslib "^2.4.0"
utility-types "^3.10.0"
"@docusaurus/theme-common@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.2.0.tgz#2303498d80448aafdd588b597ce9d6f4cfa930e4"
integrity sha512-R8BnDjYoN90DCL75gP7qYQfSjyitXuP9TdzgsKDmSFPNyrdE3twtPNa2dIN+h+p/pr+PagfxwWbd6dn722A1Dw==
"@docusaurus/theme-common@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-2.0.0-rc.1.tgz#ea5d9732a16b03b488555e50107161bfa2abad98"
integrity sha512-1r9ZLKD9SeoCYVzWzcdR79Dia4ANlrlRjNl6uzETOEybjK6FF7yEa9Yra8EJcOCbi3coyYz5xFh/r1YHFTFHug==
dependencies:
"@docusaurus/mdx-loader" "2.2.0"
"@docusaurus/module-type-aliases" "2.2.0"
"@docusaurus/plugin-content-blog" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/plugin-content-pages" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/mdx-loader" "2.0.0-rc.1"
"@docusaurus/module-type-aliases" "2.0.0-rc.1"
"@docusaurus/plugin-content-blog" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/plugin-content-pages" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router-config" "*"
@@ -1519,34 +1505,19 @@
tslib "^2.4.0"
utility-types "^3.10.0"
"@docusaurus/theme-live-codeblock@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-live-codeblock/-/theme-live-codeblock-2.2.0.tgz#a507e496a1a74d261beee30ad072e4341310809a"
integrity sha512-4XRFxfZGcyqmbLmNbnbZ2ZOsoY7FYCJUZKsYW5yzhZYjmjGg7lkdJH5trt9otUoKBsZopBpPWvcDZwCu1SENYg==
dependencies:
"@docusaurus/core" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-translations" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@philpl/buble" "^0.19.7"
clsx "^1.2.1"
fs-extra "^10.1.0"
react-live "2.2.3"
tslib "^2.4.0"
"@docusaurus/theme-search-algolia@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.2.0.tgz#77fd9f7a600917e6024fe3ac7fb6cfdf2ce84737"
integrity sha512-2h38B0tqlxgR2FZ9LpAkGrpDWVdXZ7vltfmTdX+4RsDs3A7khiNsmZB+x/x6sA4+G2V2CvrsPMlsYBy5X+cY1w==
"@docusaurus/theme-search-algolia@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.0.0-rc.1.tgz#e78c0aeaea6a3717ae3a6ecd75a8652bd7c8e974"
integrity sha512-H5yq6V/B4qo6GZrDKMbeSpk3T9e9K2MliDzLonRu0w3QHW9orVGe0c/lZvRbGlDZjnsOo7XGddhXXIDWGwnpaA==
dependencies:
"@docsearch/react" "^3.1.1"
"@docusaurus/core" "2.2.0"
"@docusaurus/logger" "2.2.0"
"@docusaurus/plugin-content-docs" "2.2.0"
"@docusaurus/theme-common" "2.2.0"
"@docusaurus/theme-translations" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/utils-validation" "2.2.0"
"@docusaurus/core" "2.0.0-rc.1"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/plugin-content-docs" "2.0.0-rc.1"
"@docusaurus/theme-common" "2.0.0-rc.1"
"@docusaurus/theme-translations" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
"@docusaurus/utils-validation" "2.0.0-rc.1"
algoliasearch "^4.13.1"
algoliasearch-helper "^3.10.0"
clsx "^1.2.1"
@@ -1556,10 +1527,10 @@
tslib "^2.4.0"
utility-types "^3.10.0"
"@docusaurus/theme-translations@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.2.0.tgz#5fbd4693679806f80c26eeae1381e1f2c23d83e7"
integrity sha512-3T140AG11OjJrtKlY4pMZ5BzbGRDjNs2co5hJ6uYJG1bVWlhcaFGqkaZ5lCgKflaNHD7UHBHU9Ec5f69jTdd6w==
"@docusaurus/theme-translations@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-2.0.0-rc.1.tgz#bd647f78c741ee7f6c6d2cbbd3e3f282ef2f89ad"
integrity sha512-JLhNdlnbQhxVQzOnLyiCaTzKFa1lpVrM3nCrkGQKscoG2rY6ARGYMgMN2DkoH6hm7TflQ8+PE1S5MzzASeLs4Q==
dependencies:
fs-extra "^10.1.0"
tslib "^2.4.0"
@@ -1578,44 +1549,30 @@
webpack "^5.73.0"
webpack-merge "^5.8.0"
"@docusaurus/types@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-2.2.0.tgz#02c577a4041ab7d058a3c214ccb13647e21a9857"
integrity sha512-b6xxyoexfbRNRI8gjblzVOnLr4peCJhGbYGPpJ3LFqpi5nsFfoK4mmDLvWdeah0B7gmJeXabN7nQkFoqeSdmOw==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
commander "^5.1.0"
joi "^17.6.0"
react-helmet-async "^1.3.0"
utility-types "^3.10.0"
webpack "^5.73.0"
webpack-merge "^5.8.0"
"@docusaurus/utils-common@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.2.0.tgz#a401c1b93a8697dd566baf6ac64f0fdff1641a78"
integrity sha512-qebnerHp+cyovdUseDQyYFvMW1n1nv61zGe5JJfoNQUnjKuApch3IVsz+/lZ9a38pId8kqehC1Ao2bW/s0ntDA==
"@docusaurus/utils-common@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-2.0.0-rc.1.tgz#3e233a28794325d5d9d3af3f7b1c22b59aa8b847"
integrity sha512-+iZICpeFPZJ9oGJXuG92WTWee6WRnVx5BdzlcfuKf/f5KQX8PvwXR2tDME78FGGhShB8zr+vjuNEXuLvXT7j2A==
dependencies:
tslib "^2.4.0"
"@docusaurus/utils-validation@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.2.0.tgz#04d4d103137ad0145883971d3aa497f4a1315f25"
integrity sha512-I1hcsG3yoCkasOL5qQAYAfnmVoLei7apugT6m4crQjmDGxq+UkiRrq55UqmDDyZlac/6ax/JC0p+usZ6W4nVyg==
"@docusaurus/utils-validation@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-2.0.0-rc.1.tgz#dded12f036cda8a54a19e01694b35859fe0cf1d5"
integrity sha512-lj36gm9Ksu4tt/EUeLDWoMbXe3sfBxeIPIUUdqYcBYkF/rpQkh+uL/dncjNGiw6uvBOqXhOfsFVP045HtgShVw==
dependencies:
"@docusaurus/logger" "2.2.0"
"@docusaurus/utils" "2.2.0"
"@docusaurus/logger" "2.0.0-rc.1"
"@docusaurus/utils" "2.0.0-rc.1"
joi "^17.6.0"
js-yaml "^4.1.0"
tslib "^2.4.0"
"@docusaurus/utils@2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.2.0.tgz#3d6f9b7a69168d5c92d371bf21c556a4f50d1da6"
integrity sha512-oNk3cjvx7Tt1Lgh/aeZAmFpGV2pDr5nHKrBVx6hTkzGhrnMuQqLt6UPlQjdYQ3QHXwyF/ZtZMO1D5Pfi0lu7SA==
"@docusaurus/utils@2.0.0-rc.1":
version "2.0.0-rc.1"
resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-2.0.0-rc.1.tgz#53584b800df9e13864d5ef1a76aa7655a90ec86e"
integrity sha512-ym9I1OwIYbKs1LGaUajaA/vDG8VweJj/6YoZjHp+eDQHhTRIrHXiYoGDqorafRhftKwnA1EnyomuXpNd9bq8Gg==
dependencies:
"@docusaurus/logger" "2.2.0"
"@docusaurus/logger" "2.0.0-rc.1"
"@svgr/webpack" "^6.2.1"
file-loader "^6.2.0"
fs-extra "^10.1.0"
@@ -1631,11 +1588,6 @@
url-loader "^4.1.1"
webpack "^5.73.0"
"@excalidraw/excalidraw@0.14.2":
version "0.14.2"
resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.14.2.tgz#150cb4b7a1bf0d11cd64295936c930e7e0db8375"
integrity sha512-8LdjpTBWEK5waDWB7Bt/G9YBI4j0OxkstUhvaDGz7dwQGfzF6FW5CXBoYHNEoX0qmb+Fg/NPOlZ7FrKsrSVCqg==
"@hapi/hoek@^9.0.0":
version "9.3.0"
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
@@ -1757,21 +1709,6 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@philpl/buble@^0.19.7":
version "0.19.7"
resolved "https://registry.yarnpkg.com/@philpl/buble/-/buble-0.19.7.tgz#27231e6391393793b64bc1c982fc7b593198b893"
integrity sha512-wKTA2DxAGEW+QffRQvOhRQ0VBiYU2h2p8Yc1oBNlqSKws48/8faxqKNIuub0q4iuyTuLwtB8EkwiKwhlfV1PBA==
dependencies:
acorn "^6.1.1"
acorn-class-fields "^0.2.1"
acorn-dynamic-import "^4.0.0"
acorn-jsx "^5.0.1"
chalk "^2.4.2"
magic-string "^0.25.2"
minimist "^1.2.0"
os-homedir "^1.0.1"
regexpu-core "^4.5.4"
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1"
@@ -2305,36 +2242,16 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
mime-types "~2.1.34"
negotiator "0.6.3"
acorn-class-fields@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.2.1.tgz#748058bceeb0ef25164bbc671993984083f5a085"
integrity sha512-US/kqTe0H8M4LN9izoL+eykVAitE68YMuYZ3sHn3i1fjniqR7oQ3SPvuMK/VT1kjOQHrx5Q88b90TtOKgAv2hQ==
acorn-dynamic-import@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948"
integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==
acorn-import-assertions@^1.7.6:
version "1.8.0"
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
acorn-jsx@^5.0.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn-walk@^8.0.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^6.1.1:
version "6.4.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
@@ -2701,18 +2618,6 @@ browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4
node-releases "^2.0.6"
update-browserslist-db "^1.0.4"
buble@0.19.6:
version "0.19.6"
resolved "https://registry.yarnpkg.com/buble/-/buble-0.19.6.tgz#915909b6bd5b11ee03b1c885ec914a8b974d34d3"
integrity sha512-9kViM6nJA1Q548Jrd06x0geh+BG2ru2+RMDkIHHgJY/8AcyCs34lTHwra9BX7YdPrZXd5aarkpr/SY8bmPgPdg==
dependencies:
chalk "^2.4.1"
magic-string "^0.25.1"
minimist "^1.2.0"
os-homedir "^1.0.1"
regexpu-core "^4.2.0"
vlq "^1.0.0"
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -2792,7 +2697,7 @@ ccount@^1.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043"
integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg==
chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
chalk@^2.0.0:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -2849,7 +2754,7 @@ cheerio@^1.0.0-rc.12:
parse5 "^7.0.0"
parse5-htmlparser2-tree-adapter "^7.0.0"
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.2, chokidar@^3.5.3:
chokidar@^3.4.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
@@ -3000,16 +2905,6 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==
component-props@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/component-props/-/component-props-1.1.1.tgz#f9b7df9b9927b6e6d97c9bd272aa867670f34944"
integrity sha512-69pIRJs9fCCHRqCz3390YF2LV1Lu6iEMZ5zuVqqUn+G20V9BNXlMs0cWawWeW9g4Ynmg29JmkG6R7/lUJoGd1Q==
component-xor@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/component-xor/-/component-xor-0.0.4.tgz#c55d83ccc1b94cd5089a4e93fa7891c7263e59aa"
integrity sha512-ZIt6sla8gfo+AFVRZoZOertcnD5LJaY2T9CKE2j13NJxQt/mUafD69Bl7/Y4AnpI2LGjiXH7cOfJDx/n2G9edA==
compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -3121,11 +3016,6 @@ core-js-pure@^3.20.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.24.0.tgz#10eeb90dbf0d670a6b22b081aecc7deb2faec7e1"
integrity sha512-uzMmW8cRh7uYw4JQtzqvGWRyC2T5+4zipQLQdi2FmiRqP83k3d6F3stv2iAlNhOs6cXN401FCD5TL0vvleuHgA==
core-js@^2.4.1:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.23.3:
version "3.24.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.24.0.tgz#4928d4e99c593a234eb1a1f9abd3122b04d3ac57"
@@ -3455,13 +3345,6 @@ dns-packet@^5.2.2:
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
docusaurus-plugin-sass@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/docusaurus-plugin-sass/-/docusaurus-plugin-sass-0.2.3.tgz#5b61f7e560d236cfc1531ed497ac32fc166fc5e2"
integrity sha512-FbaE06K8NF8SPUYTwiG+83/jkXrwHJ/Afjqz3SUIGon6QvFwSSoKOcoxGQmUBnjTOk+deUONDx8jNWsegFJcBQ==
dependencies:
sass-loader "^10.1.1"
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@@ -3469,14 +3352,6 @@ dom-converter@^0.2.0:
dependencies:
utila "~0.4"
dom-iterator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/dom-iterator/-/dom-iterator-1.0.0.tgz#9c09899846ec41c2d257adc4d6015e4759ef05ad"
integrity sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig==
dependencies:
component-props "1.1.1"
component-xor "0.0.4"
dom-serializer@^1.0.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30"
@@ -4465,11 +4340,6 @@ immer@^9.0.7:
resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.15.tgz#0b9169e5b1d22137aba7d43f8a81a495dd1b62dc"
integrity sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==
immutable@^4.0.0:
version "4.2.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.2.2.tgz#2da9ff4384a4330c36d4d1bc88e90f9e0b0ccd16"
integrity sha512-fTMKDwtbvO5tldky9QZ2fMX7slR0mYpY5nbnFWYp0fOzDhHqhgIw9KoYgxLWsoNTS9ZHGauHj18DTyEw6BK3Og==
import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
@@ -4852,7 +4722,7 @@ kleur@^3.0.3:
resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e"
integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
klona@^2.0.4, klona@^2.0.5:
klona@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc"
integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==
@@ -4981,13 +4851,6 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
magic-string@^0.25.1, magic-string@^0.25.2:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
dependencies:
sourcemap-codec "^1.4.8"
make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f"
@@ -5345,11 +5208,6 @@ opener@^1.5.2:
resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598"
integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==
os-homedir@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==
p-cancelable@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
@@ -5879,7 +5737,7 @@ pretty-time@^1.1.0:
resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e"
integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==
prism-react-renderer@^1.0.1, prism-react-renderer@^1.3.5:
prism-react-renderer@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/prism-react-renderer/-/prism-react-renderer-1.3.5.tgz#786bb69aa6f73c32ba1ee813fbe17a0115435085"
integrity sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==
@@ -5909,7 +5767,7 @@ prompts@^2.4.2:
kleur "^3.0.3"
sisteransi "^1.0.5"
prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -6109,19 +5967,6 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-live@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/react-live/-/react-live-2.2.3.tgz#260f99194213799f0005e473e7a4154c699d6a7c"
integrity sha512-tpKruvfytNETuzO3o1mrQUj180GVrq35IE8F5gH1NJVPt4szYCx83/dOSCOyjgRhhc3gQvl0pQ3k/CjOjwJkKQ==
dependencies:
buble "0.19.6"
core-js "^2.4.1"
dom-iterator "^1.0.0"
prism-react-renderer "^1.0.1"
prop-types "^15.5.8"
react-simple-code-editor "^0.10.0"
unescape "^1.0.1"
react-loadable-ssr-addon-v5-slorber@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz#2cdc91e8a744ffdf9e3556caabeb6e4278689883"
@@ -6165,11 +6010,6 @@ react-router@5.3.3, react-router@^5.3.3:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-simple-code-editor@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-simple-code-editor/-/react-simple-code-editor-0.10.0.tgz#73e7ac550a928069715482aeb33ccba36efe2373"
integrity sha512-bL5W5mAxSW6+cLwqqVWY47Silqgy2DKDTR4hDBrLrUqC5BXc29YVx17l2IZk5v36VcDEq1Bszu2oHm1qBwKqBA==
react-textarea-autosize@^8.3.2:
version "8.3.4"
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-8.3.4.tgz#270a343de7ad350534141b02c9cb78903e553524"
@@ -6242,13 +6082,6 @@ regenerate-unicode-properties@^10.0.1:
dependencies:
regenerate "^1.4.2"
regenerate-unicode-properties@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326"
integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA==
dependencies:
regenerate "^1.4.2"
regenerate@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
@@ -6266,18 +6099,6 @@ regenerator-transform@^0.15.0:
dependencies:
"@babel/runtime" "^7.8.4"
regexpu-core@^4.2.0, regexpu-core@^4.5.4:
version "4.8.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.8.0.tgz#e5605ba361b67b1718478501327502f4479a98f0"
integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg==
dependencies:
regenerate "^1.4.2"
regenerate-unicode-properties "^9.0.0"
regjsgen "^0.5.2"
regjsparser "^0.7.0"
unicode-match-property-ecmascript "^2.0.0"
unicode-match-property-value-ecmascript "^2.0.0"
regexpu-core@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.1.0.tgz#2f8504c3fd0ebe11215783a41541e21c79942c6d"
@@ -6304,23 +6125,11 @@ registry-url@^5.0.0:
dependencies:
rc "^1.2.8"
regjsgen@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733"
integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==
regjsgen@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.6.0.tgz#83414c5354afd7d6627b16af5f10f41c4e71808d"
integrity sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==
regjsparser@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.7.0.tgz#a6b667b54c885e18b52554cb4960ef71187e9968"
integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ==
dependencies:
jsesc "~0.5.0"
regjsparser@^0.8.2:
version "0.8.4"
resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.8.4.tgz#8a14285ffcc5de78c5b95d62bbf413b6bc132d5f"
@@ -6508,26 +6317,6 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sass-loader@^10.1.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.4.1.tgz#bea4e173ddf512c9d7f53e9ec686186146807cbf"
integrity sha512-aX/iJZTTpNUNx/OSYzo2KsjIUQHqvWsAhhUijFjAPdZTEhstjZI9zTNvkTTwsx+uNUJqUwOw5gacxQMx4hJxGQ==
dependencies:
klona "^2.0.4"
loader-utils "^2.0.0"
neo-async "^2.6.2"
schema-utils "^3.0.0"
semver "^7.3.2"
sass@1.57.1:
version "1.57.1"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.57.1.tgz#dfafd46eb3ab94817145e8825208ecf7281119b5"
integrity sha512-O2+LwLS79op7GI0xZ8fqzF7X2m/m8WFfI02dHOdsK5R2ECeS5F62zrwg/relM1rjSLy7Vd/DiMNIvPrQGsA0jw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0"
sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
@@ -6805,7 +6594,7 @@ sort-css-media-queries@2.0.4:
resolved "https://registry.yarnpkg.com/sort-css-media-queries/-/sort-css-media-queries-2.0.4.tgz#b2badfa519cb4a938acbc6d3aaa913d4949dc908"
integrity sha512-PAIsEK/XupCQwitjv7XxoMvYhT7EAfyzI3hsy/MyDgTvc+Ft55ctdkctJLOy6cQejaIC+zjpUL4djFVm2ivOOw==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
@@ -6828,11 +6617,6 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
space-separated-tokens@^1.0.0:
version "1.1.5"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
@@ -7163,13 +6947,6 @@ ua-parser-js@^0.7.30:
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.31.tgz#649a656b191dffab4f21d5e053e27ca17cbff5c6"
integrity sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==
unescape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/unescape/-/unescape-1.0.1.tgz#956e430f61cad8a4d57d82c518f5e6cc5d0dda96"
integrity sha512-O0+af1Gs50lyH1nUu3ZyYS1cRh01Q/kUKatTOkSs7jukXE6/NebucDVxyiDsA9AQ4JC1V1jUH9EO8JX2nMDgGQ==
dependencies:
extend-shallow "^2.0.1"
unherit@^1.0.4:
version "1.1.3"
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.3.tgz#6c9b503f2b41b262330c80e91c8614abdaa69c22"
@@ -7426,11 +7203,6 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
vlq@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/vlq/-/vlq-1.0.1.tgz#c003f6e7c0b4c1edd623fd6ee50bbc0d6a1de468"
integrity sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==
wait-on@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.1.tgz#16bbc4d1e4ebdd41c5b4e63a2e16dbd1f4e5601e"
+2 -1
View File
@@ -38,7 +38,7 @@ export const actionUnbindText = register({
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const { width, height } = measureText(
const { width, height, baseline } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
);
@@ -51,6 +51,7 @@ export const actionUnbindText = register({
containerId: null,
width,
height,
baseline,
text: boundTextElement.originalText,
});
mutateElement(element, {
+1 -3
View File
@@ -154,9 +154,7 @@ export const actionDeleteSelected = register({
};
},
contextItemLabel: "labels.delete",
keyTest: (event, appState, elements) =>
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) &&
!event[KEYS.CTRL_OR_CMD],
keyTest: (event) => event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE,
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
type="button"
+20 -99
View File
@@ -16,12 +16,8 @@ import { AppState } from "../types";
import { fixBindingsAfterDuplication } from "../element/binding";
import { ActionResult } from "./types";
import { GRID_SIZE } from "../constants";
import {
bindTextToShapeAfterDuplication,
getBoundTextElement,
} from "../element/textElement";
import { bindTextToShapeAfterDuplication } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
import { normalizeElementOrder } from "../element/sortElements";
import { DuplicateIcon } from "../components/icons";
export const actionDuplicateSelection = register({
@@ -68,11 +64,6 @@ const duplicateElements = (
elements: readonly ExcalidrawElement[],
appState: AppState,
): Partial<ActionResult> => {
// ---------------------------------------------------------------------------
// step (1)
const sortedElements = normalizeElementOrder(elements);
const groupIdMap = new Map();
const newElements: ExcalidrawElement[] = [];
const oldElements: ExcalidrawElement[] = [];
@@ -94,112 +85,42 @@ const duplicateElements = (
return newElement;
};
const finalElements: ExcalidrawElement[] = [];
let index = 0;
const selectedElementIds = arrayToMap(
getSelectedElements(sortedElements, appState, true),
getSelectedElements(elements, appState, true),
);
// Ids of elements that have already been processed so we don't push them
// into the array twice if we end up backtracking when retrieving
// discontiguous group of elements (can happen due to a bug, or in edge
// cases such as a group containing deleted elements which were not selected).
//
// This is not enough to prevent duplicates, so we do a second loop afterwards
// to remove them.
//
// For convenience we mark even the newly created ones even though we don't
// loop over them.
const processedIds = new Map<ExcalidrawElement["id"], true>();
const markAsProcessed = (elements: ExcalidrawElement[]) => {
for (const element of elements) {
processedIds.set(element.id, true);
}
return elements;
};
const elementsWithClones: ExcalidrawElement[] = [];
let index = -1;
while (++index < sortedElements.length) {
const element = sortedElements[index];
if (processedIds.get(element.id)) {
continue;
}
const boundTextElement = getBoundTextElement(element);
while (index < elements.length) {
const element = elements[index];
if (selectedElementIds.get(element.id)) {
// if a group or a container/bound-text, duplicate atomically
if (element.groupIds.length || boundTextElement) {
if (element.groupIds.length) {
const groupId = getSelectedGroupForElement(appState, element);
// if group selected, duplicate it atomically
if (groupId) {
const groupElements = getElementsInGroup(sortedElements, groupId);
elementsWithClones.push(
...markAsProcessed([
...groupElements,
...groupElements.map((element) =>
duplicateAndOffsetElement(element),
),
]),
);
continue;
}
if (boundTextElement) {
elementsWithClones.push(
...markAsProcessed([
element,
boundTextElement,
const groupElements = getElementsInGroup(elements, groupId);
finalElements.push(
...groupElements,
...groupElements.map((element) =>
duplicateAndOffsetElement(element),
duplicateAndOffsetElement(boundTextElement),
]),
),
);
index = index + groupElements.length;
continue;
}
}
elementsWithClones.push(
...markAsProcessed([element, duplicateAndOffsetElement(element)]),
);
finalElements.push(element, duplicateAndOffsetElement(element));
} else {
elementsWithClones.push(...markAsProcessed([element]));
finalElements.push(element);
}
index++;
}
// step (2)
// second pass to remove duplicates. We loop from the end as it's likelier
// that the last elements are in the correct order (contiguous or otherwise).
// Thus we need to reverse as the last step (3).
const finalElementsReversed: ExcalidrawElement[] = [];
const finalElementIds = new Map<ExcalidrawElement["id"], true>();
index = elementsWithClones.length;
while (--index >= 0) {
const element = elementsWithClones[index];
if (!finalElementIds.get(element.id)) {
finalElementIds.set(element.id, true);
finalElementsReversed.push(element);
}
}
// step (3)
const finalElements = finalElementsReversed.reverse();
// ---------------------------------------------------------------------------
bindTextToShapeAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(
elementsWithClones,
finalElements,
oldElements,
oldIdToDuplicatedId,
);
fixBindingsAfterDuplication(finalElements, oldElements, oldIdToDuplicatedId);
return {
elements: finalElements,
-2
View File
@@ -8,7 +8,6 @@ export type ShortcutName =
ActionName,
| "toggleTheme"
| "loadScene"
| "clearCanvas"
| "cut"
| "copy"
| "paste"
@@ -42,7 +41,6 @@ const shortcutMap: Record<ShortcutName, string[]> = {
toggleTheme: [getShortcutKey("Shift+Alt+D")],
saveScene: [getShortcutKey("CtrlOrCmd+S")],
loadScene: [getShortcutKey("CtrlOrCmd+O")],
clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")],
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
cut: [getShortcutKey("CtrlOrCmd+X")],
copy: [getShortcutKey("CtrlOrCmd+C")],
+1 -1
View File
@@ -21,7 +21,7 @@ export const getClientColors = (clientId: string, appState: AppState) => {
};
export const getClientInitials = (userName?: string | null) => {
if (!userName?.trim()) {
if (!userName) {
return "?";
}
return userName.trim()[0].toUpperCase();
+13 -123
View File
@@ -108,7 +108,6 @@ import {
textWysiwyg,
transformElements,
updateTextElement,
redrawTextBoundingBox,
} from "../element";
import {
bindOrUnbindLinearElement,
@@ -175,10 +174,7 @@ import {
} from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer/renderScene";
import {
generateElementWithCanvas,
invalidateShapeForElement,
} from "../renderer/renderElement";
import { invalidateShapeForElement } from "../renderer/renderElement";
import {
calculateScrollCenter,
getElementsAtPosition,
@@ -230,7 +226,6 @@ import {
setEraserCursor,
updateActiveTool,
getShortcutKey,
isTransparent,
} from "../utils";
import {
ContextMenu,
@@ -268,7 +263,6 @@ import {
getBoundTextElement,
getContainerCenter,
getContainerDims,
getContainerElement,
getTextBindableContainerAtPosition,
isValidTextContainer,
} from "../element/textElement";
@@ -285,8 +279,6 @@ import { shouldShowBoundingBox } from "../element/transformHandles";
import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard";
import { actionToggleHandTool } from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
const deviceContextInitialValue = {
isSmScreen: false,
@@ -842,7 +834,7 @@ class App extends React.Component<AppProps, AppState> {
},
};
}
const scene = restore(initialData, null, null, { repairBindings: true });
const scene = restore(initialData, null, null);
scene.appState = {
...scene.appState,
theme: this.props.theme || scene.appState.theme,
@@ -1630,7 +1622,6 @@ class App extends React.Component<AppProps, AppState> {
oldIdToDuplicatedId.set(element.id, newElement.id);
return newElement;
});
bindTextToShapeAfterDuplication(newElements, elements, oldIdToDuplicatedId);
const nextElements = [
...this.scene.getElementsIncludingDeleted(),
@@ -1643,14 +1634,6 @@ class App extends React.Component<AppProps, AppState> {
}
this.scene.replaceAllElements(nextElements);
newElements.forEach((newElement) => {
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
const container = getContainerElement(newElement);
redrawTextBoundingBox(newElement, container);
}
});
this.history.resumeRecording();
this.setState(
@@ -1969,6 +1952,7 @@ class App extends React.Component<AppProps, AppState> {
);
// Input handling
private onKeyDown = withBatchedUpdates(
(event: React.KeyboardEvent | KeyboardEvent) => {
// normalize `event.key` when CapsLock is pressed #2372
@@ -2210,13 +2194,6 @@ class App extends React.Component<AppProps, AppState> {
event.stopPropagation();
}
}
if (
event[KEYS.CTRL_OR_CMD] &&
(event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)
) {
jotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
}
},
);
@@ -2677,6 +2654,14 @@ class App extends React.Component<AppProps, AppState> {
element,
]);
}
// case: creating new text not centered to parent element → offset Y
// so that the text is centered to cursor position
if (!parentCenterPosition) {
mutateElement(element, {
y: element.y - element.baseline / 2,
});
}
}
this.setState({
@@ -2769,15 +2754,7 @@ class App extends React.Component<AppProps, AppState> {
sceneY,
);
if (container) {
if (
isArrowElement(container) ||
hasBoundTextElement(container) ||
!isTransparent(container.backgroundColor) ||
isHittingElementNotConsideringBoundingBox(container, this.state, [
sceneX,
sceneY,
])
) {
if (isArrowElement(container) || hasBoundTextElement(container)) {
const midPoint = getContainerCenter(container, this.state);
sceneX = midPoint.x;
@@ -6370,94 +6347,7 @@ class App extends React.Component<AppProps, AppState> {
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
if (!this.unmounted) {
const cursorButton: {
[id: string]: string | undefined;
} = {};
const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] =
{};
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
{};
const pointerUsernames: { [id: string]: string } = {};
const pointerUserStates: { [id: string]: string } = {};
this.state.collaborators.forEach((user, socketId) => {
if (user.selectedElementIds) {
for (const id of Object.keys(user.selectedElementIds)) {
if (!(id in remoteSelectedElementIds)) {
remoteSelectedElementIds[id] = [];
}
remoteSelectedElementIds[id].push(socketId);
}
}
if (!user.pointer) {
return;
}
if (user.username) {
pointerUsernames[socketId] = user.username;
}
if (user.userState) {
pointerUserStates[socketId] = user.userState;
}
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
{
sceneX: user.pointer.x,
sceneY: user.pointer.y,
},
this.state,
);
cursorButton[socketId] = user.button;
});
const renderingElements = this.scene
.getNonDeletedElements()
.filter((element) => {
if (isImageElement(element)) {
if (
// not placed on canvas yet (but in elements array)
this.state.pendingImageElementId === element.id
) {
return false;
}
}
// don't render text element that's being currently edited (it's
// rendered on remote only)
return (
!this.state.editingElement ||
this.state.editingElement.type !== "text" ||
element.id !== this.state.editingElement.id
);
});
const selectionColor = getComputedStyle(
document.querySelector(".excalidraw")!,
).getPropertyValue("--color-selection");
const renderConfig: RenderConfig = {
selectionColor,
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
viewBackgroundColor: this.state.viewBackgroundColor,
zoom: this.state.zoom,
remotePointerViewportCoords: pointerViewportCoords,
remotePointerButton: cursorButton,
remoteSelectedElementIds,
remotePointerUsernames: pointerUsernames,
remotePointerUserStates: pointerUserStates,
shouldCacheIgnoreZoom: false,
theme: this.state.theme,
imageCache: this.imageCache,
isExporting: false,
renderScrollbars: !this.device.isMobile,
};
let i = 0;
for (; i < renderingElements.length; i += 200) {
const chunk = renderingElements.slice(i, i + 200);
setTimeout(
() =>
chunk.forEach((el) => generateElementWithCanvas(el, renderConfig)),
i * 50,
);
}
setTimeout(() => this.setState({ shouldCacheIgnoreZoom: false }), i * 50);
this.setState({ shouldCacheIgnoreZoom: false });
}
}, 300);
+20 -24
View File
@@ -273,6 +273,22 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
className="HelpDialog__island--editor"
caption={t("helpDialog.editor")}
>
<Shortcut
label={t("labels.selectAll")}
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
/>
<Shortcut
label={t("labels.multiSelect")}
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepBoxSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
/>
<Shortcut
label={t("labels.moveCanvas")}
shortcuts={[
@@ -281,14 +297,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
]}
isOr={true}
/>
<Shortcut
label={t("buttons.clearReset")}
shortcuts={[getShortcutKey("CtrlOrCmd+Delete")]}
/>
<Shortcut
label={t("labels.delete")}
shortcuts={[getShortcutKey("Delete")]}
/>
<Shortcut
label={t("labels.cut")}
shortcuts={[getShortcutKey("CtrlOrCmd+X")]}
@@ -305,22 +313,6 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.pasteAsPlaintext")}
shortcuts={[getShortcutKey("CtrlOrCmd+Shift+V")]}
/>
<Shortcut
label={t("labels.selectAll")}
shortcuts={[getShortcutKey("CtrlOrCmd+A")]}
/>
<Shortcut
label={t("labels.multiSelect")}
shortcuts={[getShortcutKey(`Shift+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]}
/>
<Shortcut
label={t("helpDialog.deepBoxSelect")}
shortcuts={[getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]}
/>
{/* firefox supports clipboard API under a flag, so we'll
show users what they can do in the error message */}
{(probablySupportsClipboardBlob || isFirefox) && (
@@ -337,6 +329,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
label={t("labels.pasteStyles")}
shortcuts={[getShortcutKey("CtrlOrCmd+Alt+V")]}
/>
<Shortcut
label={t("labels.delete")}
shortcuts={[getShortcutKey("Delete")]}
/>
<Shortcut
label={t("labels.sendToBack")}
shortcuts={[
+1 -1
View File
@@ -124,6 +124,7 @@ const LayerUI = ({
children,
}: LayerUIProps) => {
const device = useDevice();
const tunnels = useInitializeTunnels();
const renderJSONExportDialog = () => {
@@ -408,7 +409,6 @@ const LayerUI = ({
renderCustomStats={renderCustomStats}
renderSidebars={renderSidebars}
device={device}
renderWelcomeScreen={renderWelcomeScreen}
/>
)}
+1 -3
View File
@@ -41,7 +41,6 @@ type MobileMenuProps = {
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean;
};
export const MobileMenu = ({
@@ -58,13 +57,12 @@ export const MobileMenu = ({
renderCustomStats,
renderSidebars,
device,
renderWelcomeScreen,
}: MobileMenuProps) => {
const { welcomeScreenCenterTunnel, mainMenuTunnel } = useTunnels();
const renderToolbar = () => {
return (
<FixedSideContainer side="top" className="App-top-bar">
{renderWelcomeScreen && <welcomeScreenCenterTunnel.Out />}
<welcomeScreenCenterTunnel.Out />
<Section heading="shapes">
{(heading: React.ReactNode) => (
<Stack.Col gap={4} align="center">
+22 -11
View File
@@ -1,5 +1,5 @@
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { useI18n } from "../../i18n";
import { t } from "../../i18n";
import {
useExcalidrawAppState,
useExcalidrawSetAppState,
@@ -33,7 +33,9 @@ import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
export const LoadScene = () => {
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionLoadScene)) {
@@ -55,7 +57,9 @@ export const LoadScene = () => {
LoadScene.displayName = "LoadScene";
export const SaveToActiveFile = () => {
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
@@ -76,7 +80,9 @@ SaveToActiveFile.displayName = "SaveToActiveFile";
export const SaveAsImage = () => {
const setAppState = useExcalidrawSetAppState();
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
return (
<DropdownMenuItem
icon={ExportImageIcon}
@@ -92,7 +98,9 @@ export const SaveAsImage = () => {
SaveAsImage.displayName = "SaveAsImage";
export const Help = () => {
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
@@ -111,8 +119,9 @@ export const Help = () => {
Help.displayName = "Help";
export const ClearCanvas = () => {
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
const actionManager = useExcalidrawActionManager();
@@ -134,7 +143,6 @@ export const ClearCanvas = () => {
ClearCanvas.displayName = "ClearCanvas";
export const ToggleTheme = () => {
const { t } = useI18n();
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
@@ -167,7 +175,6 @@ export const ToggleTheme = () => {
ToggleTheme.displayName = "ToggleTheme";
export const ChangeCanvasBackground = () => {
const { t } = useI18n();
const appState = useExcalidrawAppState();
const actionManager = useExcalidrawActionManager();
@@ -188,7 +195,9 @@ export const ChangeCanvasBackground = () => {
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
export const Export = () => {
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
const setAppState = useExcalidrawSetAppState();
return (
<DropdownMenuItem
@@ -239,7 +248,9 @@ export const LiveCollaborationTrigger = ({
onSelect: () => void;
isCollaborating: boolean;
}) => {
const { t } = useI18n();
// FIXME Hack until we tie "t" to lang state
// eslint-disable-next-line
const appState = useExcalidrawAppState();
return (
<DropdownMenuItem
data-testid="collab-button"
@@ -1,6 +1,6 @@
import { actionLoadScene, actionShortcuts } from "../../actions";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { t, useI18n } from "../../i18n";
import { t } from "../../i18n";
import {
useDevice,
useExcalidrawActionManager,
@@ -172,7 +172,10 @@ const MenuItemLiveCollaborationTrigger = ({
}: {
onSelect: () => any;
}) => {
const { t } = useI18n();
// FIXME when we tie t() to lang state
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const appState = useExcalidrawAppState();
return (
<WelcomeScreenMenuItem shortcut={null} onSelect={onSelect} icon={usersIcon}>
{t("labels.liveCollaboration")}
-3
View File
@@ -9,9 +9,6 @@ export const isFirefox =
"netscape" in window &&
navigator.userAgent.indexOf("rv:") > 1 &&
navigator.userAgent.indexOf("Gecko") > 1;
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
export const isSafari =
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
export const APP_NAME = "Excalidraw";
+2 -3
View File
@@ -530,7 +530,6 @@
// (doesn't work in Firefox)
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
::-webkit-scrollbar-thumb {
@@ -568,8 +567,8 @@
}
.App-toolbar--mobile {
overflow-x: auto;
max-width: 90vw;
overflow-x: hidden;
max-width: 100vw;
.ToolIcon__keybinding {
display: none;
-5
View File
@@ -95,9 +95,6 @@
--color-gray-90: #1e1e1e;
--color-gray-100: #121212;
--color-warning: #fceeca;
--color-text-warning: var(--text-primary-color);
--color-danger: #db6965;
--color-promo: #e70078;
@@ -166,8 +163,6 @@
--color-primary-darkest: #beb9ff;
--color-primary-light: #4f4d6f;
--color-text-warning: var(--color-gray-80);
--color-danger: #ffa8a5;
--color-promo: #d297ff;
}
-1
View File
@@ -156,7 +156,6 @@ export const loadSceneOrLibraryFromBlob = async (
},
localAppState,
localElements,
{ repairBindings: true },
),
};
} else if (isValidLibrary(data)) {
+4 -8
View File
@@ -171,6 +171,7 @@ const restoreElement = (
fontSize,
fontFamily,
text: element.text ?? "",
baseline: element.baseline,
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
containerId: element.containerId ?? null,
@@ -338,7 +339,7 @@ export const restoreElements = (
elements: ImportedDataState["elements"],
/** NOTE doesn't serve for reconciliation */
localElements: readonly ExcalidrawElement[] | null | undefined,
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
refreshDimensions = false,
): ExcalidrawElement[] => {
const localElementsMap = localElements ? arrayToMap(localElements) : null;
const restoredElements = (elements || []).reduce((elements, element) => {
@@ -347,7 +348,7 @@ export const restoreElements = (
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
let migratedElement: ExcalidrawElement | null = restoreElement(
element,
opts?.refreshDimensions,
refreshDimensions,
);
if (migratedElement) {
const localElement = localElementsMap?.get(element.id);
@@ -360,10 +361,6 @@ export const restoreElements = (
return elements;
}, [] as ExcalidrawElement[]);
if (!opts?.repairBindings) {
return restoredElements;
}
// repair binding. Mutates elements.
const restoredElementsMap = arrayToMap(restoredElements);
for (const element of restoredElements) {
@@ -500,10 +497,9 @@ export const restore = (
*/
localAppState: Partial<AppState> | null | undefined,
localElements: readonly ExcalidrawElement[] | null | undefined,
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
): RestoredDataState => {
return {
elements: restoreElements(data?.elements, localElements, elementsConfig),
elements: restoreElements(data?.elements, localElements),
appState: restoreAppState(data?.appState, localAppState || null),
files: data?.files || {},
};
+51 -8
View File
@@ -22,15 +22,15 @@ import { getElementAbsoluteCoords } from ".";
import { adjustXYWithRotation } from "../math";
import { getResizedElementAbsoluteCoords } from "./bounds";
import {
getBoundTextElement,
getBoundTextElementOffset,
getContainerDims,
getContainerElement,
measureText,
normalizeText,
wrapText,
getMaxContainerWidth,
} from "./textElement";
import { VERTICAL_ALIGN } from "../constants";
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { isArrowElement } from "./typeChecks";
type ElementConstructorOpts = MarkOptional<
@@ -153,6 +153,7 @@ export const newTextElement = (
y: opts.y - offsets.y,
width: metrics.width,
height: metrics.height,
baseline: metrics.baseline,
containerId: opts.containerId || null,
originalText: text,
},
@@ -169,13 +170,18 @@ const getAdjustedDimensions = (
y: number;
width: number;
height: number;
baseline: number;
} => {
let maxWidth = null;
const container = getContainerElement(element);
const { width: nextWidth, height: nextHeight } = measureText(
nextText,
getFontString(element),
);
if (container) {
maxWidth = getMaxContainerWidth(container);
}
const {
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
} = measureText(nextText, getFontString(element), maxWidth);
const { textAlign, verticalAlign } = element;
let x: number;
let y: number;
@@ -184,7 +190,11 @@ const getAdjustedDimensions = (
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId
) {
const prevMetrics = measureText(element.text, getFontString(element));
const prevMetrics = measureText(
element.text,
getFontString(element),
maxWidth,
);
const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height,
@@ -248,6 +258,7 @@ const getAdjustedDimensions = (
height: nextHeight,
x: Number.isFinite(x) ? x : element.x,
y: Number.isFinite(y) ? y : element.y,
baseline: nextBaseline,
};
};
@@ -267,6 +278,38 @@ export const refreshTextDimensions = (
return { text, ...dimensions };
};
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
if (isArrowElement(container)) {
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
if (containerWidth <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.width;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return containerWidth;
}
return width - BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
const height = getContainerDims(container).height;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return height;
}
return height - BOUND_TEXT_PADDING * 2;
};
export const updateTextElement = (
textElement: ExcalidrawTextElement,
{
+40 -30
View File
@@ -43,10 +43,12 @@ import {
getApproxMinLineWidth,
getBoundTextElement,
getBoundTextElementId,
getBoundTextElementOffset,
getContainerElement,
handleBindTextResize,
getMaxContainerWidth,
measureText,
} from "./textElement";
import { getMaxContainerWidth } from "./newElement";
export const normalizeAngle = (angle: number): number => {
if (angle >= 2 * Math.PI) {
@@ -190,10 +192,11 @@ const rescalePointsInElement = (
const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = (
const measureFontSizeFromWH = (
element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number,
): number | null => {
nextHeight: number,
): { size: number; baseline: number } | null => {
// We only use width to scale font on resize
let width = element.width;
@@ -208,8 +211,15 @@ const measureFontSizeFromWidth = (
if (nextFontSize < MIN_FONT_SIZE) {
return null;
}
return nextFontSize;
const metrics = measureText(
element.text,
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
element.containerId ? width : null,
);
return {
size: nextFontSize,
baseline: metrics.baseline + (nextHeight - metrics.height),
};
};
const getSidesForTransformHandle = (
@@ -280,8 +290,8 @@ const resizeSingleTextElement = (
if (scale > 0) {
const nextWidth = element.width * scale;
const nextHeight = element.height * scale;
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
if (nextFontSize === null) {
const nextFont = measureFontSizeFromWH(element, nextWidth, nextHeight);
if (nextFont === null) {
return;
}
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
@@ -305,9 +315,10 @@ const resizeSingleTextElement = (
deltaY2,
);
mutateElement(element, {
fontSize: nextFontSize,
fontSize: nextFont.size,
width: nextWidth,
height: nextHeight,
baseline: nextFont.baseline,
x: nextElementX,
y: nextElementY,
});
@@ -360,7 +371,7 @@ export const resizeSingleElement = (
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number } = {};
let boundTextFont: { fontSize?: number; baseline?: number } = {};
const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) {
@@ -412,24 +423,23 @@ export const resizeSingleElement = (
if (stateOfBoundTextElementAtResize) {
boundTextFont = {
fontSize: stateOfBoundTextElementAtResize.fontSize,
baseline: stateOfBoundTextElementAtResize.baseline,
};
}
if (shouldMaintainAspectRatio) {
const updatedElement = {
...element,
width: eleNewWidth,
height: eleNewHeight,
};
const nextFontSize = measureFontSizeFromWidth(
const boundTextElementPadding =
getBoundTextElementOffset(boundTextElement);
const nextFont = measureFontSizeFromWH(
boundTextElement,
getMaxContainerWidth(updatedElement),
eleNewWidth - boundTextElementPadding * 2,
eleNewHeight - boundTextElementPadding * 2,
);
if (nextFontSize === null) {
if (nextFont === null) {
return;
}
boundTextFont = {
fontSize: nextFontSize,
fontSize: nextFont.size,
baseline: nextFont.baseline,
};
} else {
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
@@ -673,6 +683,7 @@ const resizeMultipleElements = (
y: number;
points?: Point[];
fontSize?: number;
baseline?: number;
} = {
width,
height,
@@ -681,32 +692,31 @@ const resizeMultipleElements = (
...rescaledPoints,
};
let boundTextUpdates: { fontSize: number } | null = null;
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
const boundTextElement = getBoundTextElement(element.latest);
if (boundTextElement || isTextElement(element.orig)) {
const updatedElement = {
...element.latest,
width,
height,
};
const fontSize = measureFontSizeFromWidth(
const optionalPadding = getBoundTextElementOffset(boundTextElement) * 2;
const textMeasurements = measureFontSizeFromWH(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
getMaxContainerWidth(updatedElement),
width - optionalPadding,
height - optionalPadding,
);
if (!fontSize) {
if (!textMeasurements) {
return;
}
if (isTextElement(element.orig)) {
update.fontSize = fontSize;
update.fontSize = textMeasurements.size;
update.baseline = textMeasurements.baseline;
}
if (boundTextElement) {
boundTextUpdates = {
fontSize,
fontSize: textMeasurements.size,
baseline: textMeasurements.baseline,
};
}
}
-402
View File
@@ -1,402 +0,0 @@
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
import { normalizeElementOrder } from "./sortElements";
import { ExcalidrawElement } from "./types";
const assertOrder = (
elements: readonly ExcalidrawElement[],
expectedOrder: string[],
) => {
const actualOrder = elements.map((element) => element.id);
expect(actualOrder).toEqual(expectedOrder);
};
describe("normalizeElementsOrder", () => {
it("sort bound-text elements", () => {
const container = API.createElement({
id: "container",
type: "rectangle",
});
const boundText = API.createElement({
id: "boundText",
type: "text",
containerId: container.id,
});
const otherElement = API.createElement({
id: "otherElement",
type: "rectangle",
boundElements: [],
});
const otherElement2 = API.createElement({
id: "otherElement2",
type: "rectangle",
boundElements: [],
});
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
assertOrder(normalizeElementOrder([container, boundText]), [
"container",
"boundText",
]);
assertOrder(normalizeElementOrder([boundText, container]), [
"container",
"boundText",
]);
assertOrder(
normalizeElementOrder([
boundText,
container,
otherElement,
otherElement2,
]),
["container", "boundText", "otherElement", "otherElement2"],
);
assertOrder(normalizeElementOrder([container, otherElement, boundText]), [
"container",
"boundText",
"otherElement",
]);
assertOrder(
normalizeElementOrder([
container,
otherElement,
otherElement2,
boundText,
]),
["container", "boundText", "otherElement", "otherElement2"],
);
assertOrder(
normalizeElementOrder([
boundText,
otherElement,
container,
otherElement2,
]),
["otherElement", "container", "boundText", "otherElement2"],
);
// noop
assertOrder(
normalizeElementOrder([
otherElement,
container,
boundText,
otherElement2,
]),
["otherElement", "container", "boundText", "otherElement2"],
);
// text has existing containerId, but container doesn't list is
// as a boundElement
assertOrder(
normalizeElementOrder([
API.createElement({
id: "boundText",
type: "text",
containerId: "container",
}),
API.createElement({
id: "container",
type: "rectangle",
}),
]),
["boundText", "container"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "boundText",
type: "text",
containerId: "container",
}),
]),
["boundText"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "container",
type: "rectangle",
boundElements: [],
}),
]),
["container"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "container",
type: "rectangle",
boundElements: [{ id: "x", type: "text" }],
}),
]),
["container"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "arrow",
type: "arrow",
}),
API.createElement({
id: "container",
type: "rectangle",
boundElements: [{ id: "arrow", type: "arrow" }],
}),
]),
["arrow", "container"],
);
});
it("normalize group order", () => {
assertOrder(
normalizeElementOrder([
API.createElement({
id: "A_rect1",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "rect2",
type: "rectangle",
}),
API.createElement({
id: "rect3",
type: "rectangle",
}),
API.createElement({
id: "A_rect4",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "A_rect5",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "rect6",
type: "rectangle",
}),
API.createElement({
id: "A_rect7",
type: "rectangle",
groupIds: ["A"],
}),
]),
["A_rect1", "A_rect4", "A_rect5", "A_rect7", "rect2", "rect3", "rect6"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "A_rect1",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "rect2",
type: "rectangle",
}),
API.createElement({
id: "B_rect3",
type: "rectangle",
groupIds: ["B"],
}),
API.createElement({
id: "A_rect4",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "B_rect5",
type: "rectangle",
groupIds: ["B"],
}),
API.createElement({
id: "rect6",
type: "rectangle",
}),
API.createElement({
id: "A_rect7",
type: "rectangle",
groupIds: ["A"],
}),
]),
["A_rect1", "A_rect4", "A_rect7", "rect2", "B_rect3", "B_rect5", "rect6"],
);
// nested groups
assertOrder(
normalizeElementOrder([
API.createElement({
id: "A_rect1",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "BA_rect2",
type: "rectangle",
groupIds: ["B", "A"],
}),
]),
["A_rect1", "BA_rect2"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "BA_rect1",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "A_rect2",
type: "rectangle",
groupIds: ["A"],
}),
]),
["BA_rect1", "A_rect2"],
);
assertOrder(
normalizeElementOrder([
API.createElement({
id: "BA_rect1",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "A_rect2",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "CBA_rect3",
type: "rectangle",
groupIds: ["C", "B", "A"],
}),
API.createElement({
id: "rect4",
type: "rectangle",
}),
API.createElement({
id: "A_rect5",
type: "rectangle",
groupIds: ["A"],
}),
API.createElement({
id: "BA_rect5",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "BA_rect6",
type: "rectangle",
groupIds: ["B", "A"],
}),
API.createElement({
id: "CBA_rect7",
type: "rectangle",
groupIds: ["C", "B", "A"],
}),
API.createElement({
id: "X_rect8",
type: "rectangle",
groupIds: ["X"],
}),
API.createElement({
id: "rect9",
type: "rectangle",
}),
API.createElement({
id: "YX_rect10",
type: "rectangle",
groupIds: ["Y", "X"],
}),
API.createElement({
id: "X_rect11",
type: "rectangle",
groupIds: ["X"],
}),
]),
[
"BA_rect1",
"BA_rect5",
"BA_rect6",
"A_rect2",
"A_rect5",
"CBA_rect3",
"CBA_rect7",
"rect4",
"X_rect8",
"X_rect11",
"YX_rect10",
"rect9",
],
);
});
// TODO
it.skip("normalize boundElements array", () => {
const container = API.createElement({
id: "container",
type: "rectangle",
boundElements: [],
});
const boundText = API.createElement({
id: "boundText",
type: "text",
containerId: container.id,
});
mutateElement(container, {
boundElements: [
{ type: "text", id: boundText.id },
{ type: "text", id: "xxx" },
],
});
expect(normalizeElementOrder([container, boundText])).toEqual([
expect.objectContaining({
id: container.id,
}),
expect.objectContaining({ id: boundText.id }),
]);
});
// should take around <100ms for 10K iterations (@dwelle's PC 22-05-25)
it.skip("normalizeElementsOrder() perf", () => {
const makeElements = (iterations: number) => {
const elements: ExcalidrawElement[] = [];
while (iterations--) {
const container = API.createElement({
type: "rectangle",
boundElements: [],
groupIds: ["B", "A"],
});
const boundText = API.createElement({
type: "text",
containerId: container.id,
groupIds: ["A"],
});
const otherElement = API.createElement({
type: "rectangle",
boundElements: [],
groupIds: ["C", "A"],
});
mutateElement(container, {
boundElements: [{ type: "text", id: boundText.id }],
});
elements.push(boundText, otherElement, container);
}
return elements;
};
const elements = makeElements(10000);
const t0 = Date.now();
normalizeElementOrder(elements);
console.info(`${Date.now() - t0}ms`);
});
});
-123
View File
@@ -1,123 +0,0 @@
import { arrayToMapWithIndex } from "../utils";
import { ExcalidrawElement } from "./types";
const normalizeGroupElementOrder = (elements: readonly ExcalidrawElement[]) => {
const origElements: ExcalidrawElement[] = elements.slice();
const sortedElements = new Set<ExcalidrawElement>();
const orderInnerGroups = (
elements: readonly ExcalidrawElement[],
): ExcalidrawElement[] => {
const firstGroupSig = elements[0]?.groupIds?.join("");
const aGroup: ExcalidrawElement[] = [elements[0]];
const bGroup: ExcalidrawElement[] = [];
for (const element of elements.slice(1)) {
if (element.groupIds?.join("") === firstGroupSig) {
aGroup.push(element);
} else {
bGroup.push(element);
}
}
return bGroup.length ? [...aGroup, ...orderInnerGroups(bGroup)] : aGroup;
};
const groupHandledElements = new Map<string, true>();
origElements.forEach((element, idx) => {
if (groupHandledElements.has(element.id)) {
return;
}
if (element.groupIds?.length) {
const topGroup = element.groupIds[element.groupIds.length - 1];
const groupElements = origElements.slice(idx).filter((element) => {
const ret = element?.groupIds?.some((id) => id === topGroup);
if (ret) {
groupHandledElements.set(element!.id, true);
}
return ret;
});
for (const elem of orderInnerGroups(groupElements)) {
sortedElements.add(elem);
}
} else {
sortedElements.add(element);
}
});
// if there's a bug which resulted in losing some of the elements, return
// original instead as that's better than losing data
if (sortedElements.size !== elements.length) {
console.error("normalizeGroupElementOrder: lost some elements... bailing!");
return elements;
}
return [...sortedElements];
};
/**
* In theory, when we have text elements bound to a container, they
* should be right after the container element in the elements array.
* However, this is not guaranteed due to old and potential future bugs.
*
* This function sorts containers and their bound texts together. It prefers
* original z-index of container (i.e. it moves bound text elements after
* containers).
*/
const normalizeBoundElementsOrder = (
elements: readonly ExcalidrawElement[],
) => {
const elementsMap = arrayToMapWithIndex(elements);
const origElements: (ExcalidrawElement | null)[] = elements.slice();
const sortedElements = new Set<ExcalidrawElement>();
origElements.forEach((element, idx) => {
if (!element) {
return;
}
if (element.boundElements?.length) {
sortedElements.add(element);
origElements[idx] = null;
element.boundElements.forEach((boundElement) => {
const child = elementsMap.get(boundElement.id);
if (child && boundElement.type === "text") {
sortedElements.add(child[0]);
origElements[child[1]] = null;
}
});
} else if (element.type === "text" && element.containerId) {
const parent = elementsMap.get(element.containerId);
if (!parent?.[0].boundElements?.find((x) => x.id === element.id)) {
sortedElements.add(element);
origElements[idx] = null;
// if element has a container and container lists it, skip this element
// as it'll be taken care of by the container
}
} else {
sortedElements.add(element);
origElements[idx] = null;
}
});
// if there's a bug which resulted in losing some of the elements, return
// original instead as that's better than losing data
if (sortedElements.size !== elements.length) {
console.error(
"normalizeBoundElementsOrder: lost some elements... bailing!",
);
return elements;
}
return [...sortedElements];
};
export const normalizeElementOrder = (
elements: readonly ExcalidrawElement[],
) => {
// console.time();
const ret = normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
// console.timeEnd();
return ret;
};
+26 -121
View File
@@ -1,12 +1,5 @@
import { BOUND_TEXT_PADDING } from "../constants";
import { API } from "../tests/helpers/api";
import {
computeContainerHeightForBoundText,
getContainerCoords,
getMaxContainerWidth,
getMaxContainerHeight,
wrapText,
} from "./textElement";
import { measureText, wrapText } from "./textElement";
import { FontString } from "./types";
describe("Test wrapText", () => {
@@ -72,13 +65,6 @@ up`,
width: 250,
res: "Hello whats up",
},
{
desc: "should push the word if its equal to max width",
width: 60,
res: `Hello
whats
up`,
},
].forEach((data) => {
it(`should ${data.desc}`, () => {
const res = wrapText(text, font, data.width - BOUND_TEXT_PADDING * 2);
@@ -86,7 +72,6 @@ up`,
});
});
});
describe("When text contain new lines", () => {
const text = `Hello
whats up`;
@@ -177,115 +162,35 @@ break it now`,
});
describe("Test measureText", () => {
describe("Test getContainerCoords", () => {
const params = { width: 200, height: 100, x: 10, y: 20 };
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
const text = "Hello World";
it("should compute coords correctly when ellipse", () => {
const element = API.createElement({
type: "ellipse",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 44.2893218813452455,
y: 39.64466094067262,
});
});
it("should add correct attributes when maxWidth is passed", () => {
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
const res = measureText(text, font, maxWidth);
it("should compute coords correctly when rectangle", () => {
const element = API.createElement({
type: "rectangle",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 15,
y: 25,
});
});
it("should compute coords correctly when diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(getContainerCoords(element)).toEqual({
x: 65,
y: 50,
});
});
expect(res.container).toMatchInlineSnapshot(`
<div
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; max-width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
>
<span
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
/>
</div>
`);
});
describe("Test computeContainerHeightForBoundText", () => {
const params = {
width: 178,
height: 194,
};
it("should add correct attributes when maxWidth is not passed", () => {
const res = measureText(text, font);
it("should compute container height correctly for rectangle", () => {
const element = API.createElement({
type: "rectangle",
...params,
});
expect(computeContainerHeightForBoundText(element, 150)).toEqual(160);
});
it("should compute container height correctly for ellipse", () => {
const element = API.createElement({
type: "ellipse",
...params,
});
expect(computeContainerHeightForBoundText(element, 150)).toEqual(226);
});
it("should compute container height correctly for diamond", () => {
const element = API.createElement({
type: "diamond",
...params,
});
expect(computeContainerHeightForBoundText(element, 150)).toEqual(320);
});
});
describe("Test getMaxContainerWidth", () => {
const params = {
width: 178,
height: 194,
};
it("should return max width when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerWidth(container)).toBe(168);
});
it("should return max width when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerWidth(container)).toBe(116);
});
it("should return max width when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerWidth(container)).toBe(79);
});
});
describe("Test getMaxContainerHeight", () => {
const params = {
width: 178,
height: 194,
};
it("should return max height when container is rectangle", () => {
const container = API.createElement({ type: "rectangle", ...params });
expect(getMaxContainerHeight(container)).toBe(184);
});
it("should return max height when container is ellipse", () => {
const container = API.createElement({ type: "ellipse", ...params });
expect(getMaxContainerHeight(container)).toBe(127);
});
it("should return max height when container is diamond", () => {
const container = API.createElement({ type: "diamond", ...params });
expect(getMaxContainerHeight(container)).toBe(87);
});
expect(res.container).toMatchInlineSnapshot(`
<div
style="position: absolute; white-space: pre; font: Emoji 20px 20px; min-height: 1em;"
>
<span
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"
/>
</div>
`);
});
});
+135 -214
View File
@@ -12,6 +12,7 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { isTextElement } from ".";
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
import {
isBoundToContainer,
isImageElement,
@@ -43,66 +44,68 @@ export const redrawTextBoundingBox = (
container: ExcalidrawElement | null,
) => {
let maxWidth = undefined;
const boundTextUpdates = {
x: textElement.x,
y: textElement.y,
text: textElement.text,
width: textElement.width,
height: textElement.height,
};
boundTextUpdates.text = textElement.text;
let text = textElement.text;
if (container) {
maxWidth = getMaxContainerWidth(container);
boundTextUpdates.text = wrapText(
text = wrapText(
textElement.originalText,
getFontString(textElement),
maxWidth,
);
}
const metrics = measureText(
boundTextUpdates.text,
getFontString(textElement),
);
boundTextUpdates.width = metrics.width;
boundTextUpdates.height = metrics.height;
const metrics = measureText(text, getFontString(textElement), maxWidth);
let coordY = textElement.y;
let coordX = textElement.x;
// Resize container and vertically center align the text
if (container) {
if (isArrowElement(container)) {
if (!isArrowElement(container)) {
const containerDims = getContainerDims(container);
let nextHeight = containerDims.height;
if (textElement.verticalAlign === VERTICAL_ALIGN.TOP) {
coordY = container.y;
} else if (textElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY =
container.y +
containerDims.height -
metrics.height -
BOUND_TEXT_PADDING;
} else {
coordY = container.y + containerDims.height / 2 - metrics.height / 2;
if (metrics.height > getMaxContainerHeight(container)) {
nextHeight = metrics.height + BOUND_TEXT_PADDING * 2;
coordY = container.y + nextHeight / 2 - metrics.height / 2;
}
}
if (textElement.textAlign === TEXT_ALIGN.LEFT) {
coordX = container.x + BOUND_TEXT_PADDING;
} else if (textElement.textAlign === TEXT_ALIGN.RIGHT) {
coordX =
container.x +
containerDims.width -
metrics.width -
BOUND_TEXT_PADDING;
} else {
coordX = container.x + containerDims.width / 2 - metrics.width / 2;
}
updateOriginalContainerCache(container.id, nextHeight);
mutateElement(container, { height: nextHeight });
} else {
const centerX = textElement.x + textElement.width / 2;
const centerY = textElement.y + textElement.height / 2;
const diffWidth = metrics.width - textElement.width;
const diffHeight = metrics.height - textElement.height;
boundTextUpdates.x = centerY - (textElement.height + diffHeight) / 2;
boundTextUpdates.y = centerX - (textElement.width + diffWidth) / 2;
} else {
const containerDims = getContainerDims(container);
let maxContainerHeight = getMaxContainerHeight(container);
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerHeightForBoundText(
container,
metrics.height,
);
mutateElement(container, { height: nextHeight });
maxContainerHeight = getMaxContainerHeight(container);
updateOriginalContainerCache(container.id, nextHeight);
}
const updatedTextElement = {
...textElement,
...boundTextUpdates,
} as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement);
boundTextUpdates.x = x;
boundTextUpdates.y = y;
coordY = centerY - (textElement.height + diffHeight) / 2;
coordX = centerX - (textElement.width + diffWidth) / 2;
}
}
mutateElement(textElement, boundTextUpdates);
mutateElement(textElement, {
width: metrics.width,
height: metrics.height,
baseline: metrics.baseline,
y: coordY,
x: coordX,
text,
});
};
export const bindTextToShapeAfterDuplication = (
@@ -124,16 +127,10 @@ export const bindTextToShapeAfterDuplication = (
const newContainer = sceneElementMap.get(newElementId);
if (newContainer) {
mutateElement(newContainer, {
boundElements: (element.boundElements || [])
.filter(
(boundElement) =>
boundElement.id !== newTextElementId &&
boundElement.id !== boundTextElementId,
)
.concat({
type: "text",
id: newTextElementId,
}),
boundElements: (newContainer.boundElements || []).concat({
type: "text",
id: newTextElementId,
}),
});
}
const newTextElement = sceneElementMap.get(newTextElementId);
@@ -174,6 +171,7 @@ export const handleBindTextResize = (
const maxWidth = getMaxContainerWidth(container);
const maxHeight = getMaxContainerHeight(container);
let containerHeight = containerDims.height;
let nextBaseLine = textElement.baseline;
if (transformHandleType !== "n" && transformHandleType !== "s") {
if (text) {
text = wrapText(
@@ -182,17 +180,18 @@ export const handleBindTextResize = (
maxWidth,
);
}
const dimensions = measureText(text, getFontString(textElement));
const dimensions = measureText(
text,
getFontString(textElement),
maxWidth,
);
nextHeight = dimensions.height;
nextWidth = dimensions.width;
nextBaseLine = dimensions.baseline;
}
// increase height in case text element height exceeds
if (nextHeight > maxHeight) {
containerHeight = computeContainerHeightForBoundText(
container,
nextHeight,
);
containerHeight = nextHeight + getBoundTextElementOffset(textElement) * 2;
const diff = containerHeight - containerDims.height;
// fix the y coord when resizing from ne/nw/n
const updatedY =
@@ -212,64 +211,94 @@ export const handleBindTextResize = (
text,
width: nextWidth,
height: nextHeight,
});
baseline: nextBaseLine,
});
if (!isArrowElement(container)) {
mutateElement(
textElement,
computeBoundTextPosition(
container,
textElement as ExcalidrawTextElementWithContainer,
),
updateBoundTextPosition(
container,
textElement as ExcalidrawTextElementWithContainer,
);
}
}
};
const computeBoundTextPosition = (
const updateBoundTextPosition = (
container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer,
) => {
const containerCoords = getContainerCoords(container);
const maxContainerHeight = getMaxContainerHeight(container);
const maxContainerWidth = getMaxContainerWidth(container);
let x;
const containerDims = getContainerDims(container);
const boundTextElementPadding = getBoundTextElementOffset(boundTextElement);
let y;
if (boundTextElement.verticalAlign === VERTICAL_ALIGN.TOP) {
y = containerCoords.y;
y = container.y + boundTextElementPadding;
} else if (boundTextElement.verticalAlign === VERTICAL_ALIGN.BOTTOM) {
y = containerCoords.y + (maxContainerHeight - boundTextElement.height);
} else {
y =
containerCoords.y +
(maxContainerHeight / 2 - boundTextElement.height / 2);
}
if (boundTextElement.textAlign === TEXT_ALIGN.LEFT) {
x = containerCoords.x;
} else if (boundTextElement.textAlign === TEXT_ALIGN.RIGHT) {
x = containerCoords.x + (maxContainerWidth - boundTextElement.width);
container.y +
containerDims.height -
boundTextElement.height -
boundTextElementPadding;
} else {
x =
containerCoords.x + (maxContainerWidth / 2 - boundTextElement.width / 2);
y = container.y + containerDims.height / 2 - boundTextElement.height / 2;
}
return { x, y };
const x =
boundTextElement.textAlign === TEXT_ALIGN.LEFT
? container.x + boundTextElementPadding
: boundTextElement.textAlign === TEXT_ALIGN.RIGHT
? container.x +
containerDims.width -
boundTextElement.width -
boundTextElementPadding
: container.x + containerDims.width / 2 - boundTextElement.width / 2;
mutateElement(boundTextElement, { x, y });
};
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
export const measureText = (text: string, font: FontString) => {
export const measureText = (
text: string,
font: FontString,
maxWidth?: number | null,
) => {
text = text
.split("\n")
// replace empty lines with single space because leading/trailing empty
// lines would be stripped from computation
.map((x) => x || " ")
.join("\n");
const container = document.createElement("div");
container.style.position = "absolute";
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
const height = getTextHeight(text, font);
const width = getTextWidth(text, font);
if (maxWidth) {
const lineHeight = getApproxLineHeight(font);
// since we are adding a span of width 1px later
container.style.maxWidth = `${maxWidth + 1}px`;
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.lineHeight = `${String(lineHeight)}px`;
container.style.whiteSpace = "pre-wrap";
}
document.body.appendChild(container);
container.innerText = text;
return { width, height };
const span = document.createElement("span");
span.style.display = "inline-block";
span.style.overflow = "hidden";
span.style.width = "1px";
span.style.height = "1px";
container.appendChild(span);
// Baseline is important for positioning text on canvas
const baseline = span.offsetTop + span.offsetHeight;
// since we are adding a span of width 1px
const width = container.offsetWidth + 1;
const height = container.offsetHeight;
document.body.removeChild(container);
if (isTestEnv()) {
return { width, height, baseline, container };
}
return { width, height, baseline };
};
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
@@ -279,47 +308,40 @@ export const getApproxLineHeight = (font: FontString) => {
if (cacheApproxLineHeight[font]) {
return cacheApproxLineHeight[font];
}
const fontSize = parseInt(font);
// Calculate line height relative to font size
cacheApproxLineHeight[font] = fontSize * 1.2;
cacheApproxLineHeight[font] = measureText(DUMMY_TEXT, font, null).height;
return cacheApproxLineHeight[font];
};
let canvas: HTMLCanvasElement | undefined;
const getLineWidth = (text: string, font: FontString) => {
if (!canvas) {
canvas = document.createElement("canvas");
}
const canvas2dContext = canvas.getContext("2d")!;
canvas2dContext.font = font;
const width = canvas2dContext.measureText(text).width;
const metrics = canvas2dContext.measureText(text);
// since in test env the canvas measureText algo
// doesn't measure text and instead just returns number of
// characters hence we assume that each letteris 10px
if (isTestEnv()) {
return width * 10;
return metrics.width * 10;
}
return width;
// Since measureText behaves differently in different browsers
// OS so considering a adjustment factor of 0.2
const adjustmentFactor = 0.2;
return metrics.width + adjustmentFactor;
};
export const getTextWidth = (text: string, font: FontString) => {
const lines = text.replace(/\r\n?/g, "\n").split("\n");
const lines = text.split("\n");
let width = 0;
lines.forEach((line) => {
width = Math.max(width, getLineWidth(line, font));
});
return width;
};
export const getTextHeight = (text: string, font: FontString) => {
const lines = text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = getApproxLineHeight(font);
return lineHeight * lines.length;
};
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
const lines: Array<string> = [];
const originalLines = text.split("\n");
@@ -341,23 +363,16 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
let currentLineWidthTillNow = 0;
let index = 0;
while (index < words.length) {
const currentWordWidth = getLineWidth(words[index], font);
// This will only happen when single word takes entire width
if (currentWordWidth === maxWidth) {
push(words[index]);
index++;
}
// Start breaking longer words exceeding max width
else if (currentWordWidth > maxWidth) {
if (currentWordWidth >= maxWidth) {
// push current line since the current word exceeds the max width
// so will be appended in next line
push(currentLine);
currentLine = "";
currentLineWidthTillNow = 0;
while (words[index].length > 0) {
const currentChar = String.fromCodePoint(
words[index].codePointAt(0)!,
@@ -458,9 +473,9 @@ export const charWidth = (() => {
getCache,
};
})();
export const getApproxMinLineWidth = (font: FontString) => {
const maxCharWidth = getMaxCharWidth(font);
if (maxCharWidth === 0) {
return (
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
@@ -600,26 +615,6 @@ export const getContainerCenter = (
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
};
export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
let offsetX = BOUND_TEXT_PADDING;
let offsetY = BOUND_TEXT_PADDING;
if (container.type === "ellipse") {
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6172
offsetX += (container.width / 2) * (1 - Math.sqrt(2) / 2);
offsetY += (container.height / 2) * (1 - Math.sqrt(2) / 2);
}
// The derivation of coordinates is explained in https://github.com/excalidraw/excalidraw/pull/6265
if (container.type === "diamond") {
offsetX += container.width / 4;
offsetY += container.height / 4;
}
return {
x: container.x + offsetX,
y: container.y + offsetY,
};
};
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
const container = getContainerElement(textElement);
if (!container || isArrowElement(container)) {
@@ -632,13 +627,12 @@ export const getBoundTextElementOffset = (
boundTextElement: ExcalidrawTextElement | null,
) => {
const container = getContainerElement(boundTextElement);
if (!container || !boundTextElement) {
if (!container) {
return 0;
}
if (isArrowElement(container)) {
return BOUND_TEXT_PADDING * 8;
}
return BOUND_TEXT_PADDING;
};
@@ -723,76 +717,3 @@ export const isValidTextContainer = (element: ExcalidrawElement) => {
isArrowElement(element)
);
};
export const computeContainerHeightForBoundText = (
container: NonDeletedExcalidrawElement,
boundTextElementHeight: number,
) => {
if (container.type === "ellipse") {
return Math.round(
((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2,
);
}
if (isArrowElement(container)) {
return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2;
}
if (container.type === "diamond") {
return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2);
}
return boundTextElementHeight + BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
const width = getContainerDims(container).width;
if (isArrowElement(container)) {
const containerWidth = width - BOUND_TEXT_PADDING * 8 * 2;
if (containerWidth <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.width;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return containerWidth;
}
if (container.type === "ellipse") {
// The width of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse -https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((width / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
}
if (container.type === "diamond") {
// The width of the largest rectangle inscribed inside a rhombus is
// Math.round(width / 2) - https://github.com/excalidraw/excalidraw/pull/6265
return Math.round(width / 2) - BOUND_TEXT_PADDING * 2;
}
return width - BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerHeight = (container: ExcalidrawElement) => {
const height = getContainerDims(container).height;
if (isArrowElement(container)) {
const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2;
if (containerHeight <= 0) {
const boundText = getBoundTextElement(container);
if (boundText) {
return boundText.height;
}
return BOUND_TEXT_PADDING * 8 * 2;
}
return height;
}
if (container.type === "ellipse") {
// The height of the largest rectangle inscribed inside an ellipse is
// Math.round((ellipse.height / 2) * Math.sqrt(2)) which is derived from
// equation of an ellipse - https://github.com/excalidraw/excalidraw/pull/6172
return Math.round((height / 2) * Math.sqrt(2)) - BOUND_TEXT_PADDING * 2;
}
if (container.type === "diamond") {
// The height of the largest rectangle inscribed inside a rhombus is
// Math.round(height / 2) - https://github.com/excalidraw/excalidraw/pull/6265
return Math.round(height / 2) - BOUND_TEXT_PADDING * 2;
}
return height - BOUND_TEXT_PADDING * 2;
};
+148 -130
View File
@@ -6,11 +6,12 @@ import { CODES, KEYS } from "../keys";
import { fireEvent } from "../tests/test-utils";
import { queryByText } from "@testing-library/react";
import { FONT_FAMILY } from "../constants";
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import {
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
} from "./types";
import * as textElementUtils from "./textElement";
import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
import { resize } from "../tests/utils";
@@ -439,6 +440,17 @@ describe("textWysiwyg", () => {
let rectangle: any;
const { h } = window;
const DUMMY_HEIGHT = 240;
const DUMMY_WIDTH = 160;
const APPROX_LINE_HEIGHT = 25;
const INITIAL_WIDTH = 10;
beforeAll(() => {
jest
.spyOn(textElementUtils, "getApproxLineHeight")
.mockReturnValue(APPROX_LINE_HEIGHT);
});
beforeEach(async () => {
await render(<ExcalidrawApp />);
h.elements = [];
@@ -451,21 +463,14 @@ describe("textWysiwyg", () => {
});
});
it("should bind text to container when double clicked inside filled container", async () => {
const rectangle = API.createElement({
type: "rectangle",
x: 10,
y: 20,
width: 90,
height: 75,
backgroundColor: "red",
});
h.elements = [rectangle];
it("should bind text to container when double clicked on center of filled container", async () => {
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
@@ -499,37 +504,24 @@ describe("textWysiwyg", () => {
});
h.elements = [rectangle];
mouse.doubleClickAt(rectangle.x + 10, rectangle.y + 10);
expect(h.elements.length).toBe(2);
let text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(null);
mouse.down();
let editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
editor.blur();
mouse.doubleClickAt(
rectangle.x + rectangle.width / 2,
rectangle.y + rectangle.height / 2,
);
expect(h.elements.length).toBe(3);
expect(h.elements.length).toBe(2);
text = h.elements[1] as ExcalidrawTextElementWithContainer;
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
mouse.down();
editor = document.querySelector(
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
@@ -559,43 +551,6 @@ describe("textWysiwyg", () => {
]);
});
it("should bind text to container when double clicked on container stroke", async () => {
const rectangle = API.createElement({
type: "rectangle",
x: 10,
y: 20,
width: 90,
height: 75,
strokeWidth: 4,
});
h.elements = [rectangle];
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(rectangle.id);
mouse.doubleClickAt(rectangle.x + 2, rectangle.y + 2);
expect(h.elements.length).toBe(2);
const text = h.elements[1] as ExcalidrawTextElementWithContainer;
expect(text.type).toBe("text");
expect(text.containerId).toBe(rectangle.id);
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
mouse.down();
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, { target: { value: "Hello World!" } });
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.boundElements).toStrictEqual([
{ id: text.id, type: "text" },
]);
});
it("shouldn't bind to non-text-bindable containers", async () => {
const freedraw = API.createElement({
type: "freedraw",
@@ -720,6 +675,39 @@ describe("textWysiwyg", () => {
});
it("should wrap text and vertcially center align once text submitted", async () => {
jest
.spyOn(textElementUtils, "measureText")
.mockImplementation((text, font, maxWidth) => {
let width = INITIAL_WIDTH;
let height = APPROX_LINE_HEIGHT;
let baseline = 10;
if (!text) {
return {
width,
height,
baseline,
};
}
baseline = 30;
width = DUMMY_WIDTH;
if (text === "Hello \nWorld!") {
height = APPROX_LINE_HEIGHT * 2;
}
if (maxWidth) {
width = maxWidth;
// To capture cases where maxWidth passed is initial width
// due to which the text is not wrapped correctly
if (maxWidth === INITIAL_WIDTH) {
height = DUMMY_HEIGHT;
}
}
return {
width,
height,
baseline,
};
});
expect(h.elements.length).toBe(1);
Keyboard.keyDown(KEYS.ENTER);
@@ -728,6 +716,11 @@ describe("textWysiwyg", () => {
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
// mock scroll height
jest
.spyOn(editor, "scrollHeight", "get")
.mockImplementation(() => APPROX_LINE_HEIGHT * 2);
fireEvent.change(editor, {
target: {
value: "Hello World!",
@@ -742,11 +735,11 @@ describe("textWysiwyg", () => {
expect(text.text).toBe("Hello \nWorld!");
expect(text.originalText).toBe("Hello World!");
expect(text.y).toBe(
rectangle.y + h.elements[0].height / 2 - text.height / 2,
rectangle.y + rectangle.height / 2 - (APPROX_LINE_HEIGHT * 2) / 2,
);
expect(text.x).toBe(25);
expect(text.height).toBe(48);
expect(text.width).toBe(60);
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
expect(text.height).toBe(APPROX_LINE_HEIGHT * 2);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
// Edit and text by removing second line and it should
// still vertically align correctly
@@ -763,6 +756,11 @@ describe("textWysiwyg", () => {
},
});
// mock scroll height
jest
.spyOn(editor, "scrollHeight", "get")
.mockImplementation(() => APPROX_LINE_HEIGHT);
editor.style.height = "25px";
editor.dispatchEvent(new Event("input"));
await new Promise((r) => setTimeout(r, 0));
@@ -772,12 +770,12 @@ describe("textWysiwyg", () => {
expect(text.text).toBe("Hello");
expect(text.originalText).toBe("Hello");
expect(text.height).toBe(24);
expect(text.width).toBe(50);
expect(text.y).toBe(
rectangle.y + h.elements[0].height / 2 - text.height / 2,
rectangle.y + rectangle.height / 2 - APPROX_LINE_HEIGHT / 2,
);
expect(text.x).toBe(30);
expect(text.x).toBe(rectangle.x + BOUND_TEXT_PADDING);
expect(text.height).toBe(APPROX_LINE_HEIGHT);
expect(text.width).toBe(rectangle.width - BOUND_TEXT_PADDING * 2);
});
it("should unbind bound text when unbind action from context menu is triggered", async () => {
@@ -864,8 +862,8 @@ describe("textWysiwyg", () => {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
85,
5,
109.5,
17,
]
`);
@@ -879,8 +877,6 @@ describe("textWysiwyg", () => {
editor.select();
fireEvent.click(screen.getByTitle("Left"));
await new Promise((r) => setTimeout(r, 0));
fireEvent.click(screen.getByTitle("Align bottom"));
await new Promise((r) => setTimeout(r, 0));
@@ -891,7 +887,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
66,
90,
]
`);
@@ -914,7 +910,7 @@ describe("textWysiwyg", () => {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
375,
424,
-539,
]
`);
@@ -1029,9 +1025,9 @@ describe("textWysiwyg", () => {
mouse.moveTo(rectangle.x + 100, rectangle.y + 50);
mouse.up(rectangle.x + 100, rectangle.y + 50);
expect(rectangle.x).toBe(80);
expect(rectangle.y).toBe(-35);
expect(text.x).toBe(85);
expect(text.y).toBe(-30);
expect(rectangle.y).toBe(85);
expect(text.x).toBe(89.5);
expect(text.y).toBe(90);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z);
@@ -1061,6 +1057,29 @@ describe("textWysiwyg", () => {
});
it("should restore original container height and clear cache once text is unbind", async () => {
jest
.spyOn(textElementUtils, "measureText")
.mockImplementation((text, font, maxWidth) => {
let width = INITIAL_WIDTH;
let height = APPROX_LINE_HEIGHT;
let baseline = 10;
if (!text) {
return {
width,
height,
baseline,
};
}
baseline = 30;
width = DUMMY_WIDTH;
height = APPROX_LINE_HEIGHT * 5;
return {
width,
height,
baseline,
};
});
const originalRectHeight = rectangle.height;
expect(rectangle.height).toBe(originalRectHeight);
@@ -1074,7 +1093,7 @@ describe("textWysiwyg", () => {
target: { value: "Online whiteboard collaboration made easy" },
});
editor.blur();
expect(rectangle.height).toBe(178);
expect(rectangle.height).toBe(135);
mouse.select(rectangle);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
@@ -1100,7 +1119,7 @@ describe("textWysiwyg", () => {
editor.blur();
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect(rectangle.height).toBe(156);
expect(rectangle.height).toBe(215);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(null);
mouse.select(rectangle);
@@ -1112,12 +1131,13 @@ describe("textWysiwyg", () => {
await new Promise((r) => setTimeout(r, 0));
editor.blur();
expect(rectangle.height).toBe(156);
expect(rectangle.height).toBe(215);
// cache updated again
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(156);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(215);
});
it("should reset the container height cache when font properties updated", async () => {
//@todo fix this test later once measureText is mocked correctly
it.skip("should reset the container height cache when font properties updated", async () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
@@ -1143,9 +1163,7 @@ describe("textWysiwyg", () => {
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
).toEqual(36);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(
96.39999999999999,
);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
});
describe("should align correctly", () => {
@@ -1173,7 +1191,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
25,
20,
]
`);
});
@@ -1183,8 +1201,8 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Align top"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
30,
25,
94.5,
20,
]
`);
});
@@ -1194,22 +1212,22 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Align top"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
45,
25,
]
`);
Array [
174,
20,
]
`);
});
it("when center left", async () => {
fireEvent.click(screen.getByTitle("Center vertically"));
fireEvent.click(screen.getByTitle("Left"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
45.5,
]
`);
Array [
15,
25,
]
`);
});
it("when center center", async () => {
@@ -1217,11 +1235,11 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center vertically"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
30,
45.5,
]
`);
Array [
-25,
25,
]
`);
});
it("when center right", async () => {
@@ -1229,11 +1247,11 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Center vertically"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
45,
45.5,
]
`);
Array [
174,
25,
]
`);
});
it("when bottom left", async () => {
@@ -1241,33 +1259,33 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle("Align bottom"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
66,
]
`);
Array [
15,
25,
]
`);
});
it("when bottom center", async () => {
fireEvent.click(screen.getByTitle("Center"));
fireEvent.click(screen.getByTitle("Align bottom"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
30,
66,
]
`);
Array [
94.5,
25,
]
`);
});
it("when bottom right", async () => {
fireEvent.click(screen.getByTitle("Right"));
fireEvent.click(screen.getByTitle("Align bottom"));
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
45,
66,
]
`);
Array [
174,
25,
]
`);
});
});
});
+56 -24
View File
@@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants";
import { CLASSES, VERTICAL_ALIGN } from "../constants";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -24,17 +24,13 @@ import { mutateElement } from "./mutateElement";
import {
getApproxLineHeight,
getBoundTextElementId,
getContainerCoords,
getBoundTextElementOffset,
getContainerDims,
getContainerElement,
getTextElementAngle,
getTextWidth,
measureText,
normalizeText,
redrawTextBoundingBox,
wrapText,
getMaxContainerHeight,
getMaxContainerWidth,
} from "./textElement";
import {
actionDecreaseFontSize,
@@ -42,6 +38,7 @@ import {
} from "../actions/actionProperties";
import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
import App from "../components/App";
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
import { LinearElementEditor } from "./linearElementEditor";
import { parseClipboard } from "../clipboard";
@@ -160,7 +157,7 @@ export const textWysiwyg = ({
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
let textElementWidth = updatedTextElement.width;
const width = updatedTextElement.width;
// Set to element height by default since that's
// what is going to be used for unbounded text
let textElementHeight = updatedTextElement.height;
@@ -233,17 +230,19 @@ export const textWysiwyg = ({
// Start pushing text upward until a diff of 30px (padding)
// is reached
else {
const containerCoords = getContainerCoords(container);
// vertically center align the text
if (verticalAlign === VERTICAL_ALIGN.MIDDLE) {
if (!isArrowElement(container)) {
coordY =
containerCoords.y + maxHeight / 2 - textElementHeight / 2;
container.y + containerDims.height / 2 - textElementHeight / 2;
}
}
if (verticalAlign === VERTICAL_ALIGN.BOTTOM) {
coordY = containerCoords.y + (maxHeight - textElementHeight);
coordY =
container.y +
containerDims.height -
textElementHeight -
getBoundTextElementOffset(updatedTextElement);
}
}
}
@@ -273,10 +272,7 @@ export const textWysiwyg = ({
if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
}
// As firefox, Safari needs little higher dimensions on DOM
if (isFirefox || isSafari) {
textElementWidth += 0.5;
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.height - viewportY) / appState.zoom.value;
@@ -284,12 +280,12 @@ export const textWysiwyg = ({
font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`,
width: `${textElementWidth}px`,
width: `${Math.min(width, maxWidth)}px`,
height: `${textElementHeight}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: getTransform(
textElementWidth,
width,
textElementHeight,
getTextElementAngle(updatedTextElement),
appState,
@@ -382,16 +378,55 @@ export const textWysiwyg = ({
id,
) as ExcalidrawTextElement;
const font = getFontString(updatedTextElement);
if (isBoundToContainer(element)) {
// using scrollHeight here since we need to calculate
// number of lines so cannot use editable.style.height
// as that gets updated below
// Rounding here so that the lines calculated is more accurate in all browsers.
// The scrollHeight and approxLineHeight differs in diff browsers
// eg it gives 1.05 in firefox for handewritten small font due to which
// height gets updated as lines > 1 and leads to jumping text for first line in bound container
// hence rounding here to avoid that
const lines = Math.round(
editable.scrollHeight / getApproxLineHeight(font),
);
// auto increase height only when lines > 1 so its
// measured correctly and vertically aligns for
// first line as well as setting height to "auto"
// doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) {
const container = getContainerElement(element);
let height = "auto";
editable.style.height = "0px";
let heightSet = false;
if (lines === 2) {
const actualLineCount = wrapText(
editable.value,
font,
getMaxContainerWidth(container!),
).split("\n").length;
// This is browser behaviour when setting height to "auto"
// It sets the height needed for 2 lines even if actual
// line count is 1 as mentioned above as well
// hence reducing the height by half if actual line count is 1
// so single line aligns vertically when deleting
if (actualLineCount === 1) {
height = `${editable.scrollHeight / 2}px`;
editable.style.height = height;
heightSet = true;
}
}
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getMaxContainerWidth(container!),
);
const { width, height } = measureText(wrappedText, font);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
editable.style.height = `${height}px`;
if (!heightSet) {
editable.style.height = `${editable.scrollHeight}px`;
}
}
onChange(normalizeText(editable.value));
};
@@ -428,9 +463,7 @@ export const textWysiwyg = ({
event.code === CODES.BRACKET_RIGHT))
) {
event.preventDefault();
if (event.isComposing) {
return;
} else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
outdent();
} else {
indent();
@@ -579,7 +612,6 @@ export const textWysiwyg = ({
),
});
}
redrawTextBoundingBox(updateElement, container);
}
onSubmit({
+1 -1
View File
@@ -137,7 +137,7 @@ export const isExcalidrawElement = (element: any): boolean => {
export const hasBoundTextElement = (
element: ExcalidrawElement | null,
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
): element is ExcalidrawBindableElement => {
return (
isBindableElement(element) &&
!!element.boundElements?.some(({ type }) => type === "text")
+1
View File
@@ -130,6 +130,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
fontSize: number;
fontFamily: FontFamilyValues;
text: string;
baseline: number;
textAlign: TextAlign;
verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;
-3
View File
@@ -1,3 +0,0 @@
import { unstable_createStore } from "jotai";
export const appJotaiStore = unstable_createStore();
+6 -16
View File
@@ -70,12 +70,11 @@ import { decryptData } from "../../data/encryption";
import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai";
import { appJotaiStore } from "../app-jotai";
import { jotaiStore } from "../../jotai";
export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false);
export const isCollaboratingAtom = atom(false);
export const isOfflineAtom = atom(false);
interface CollabState {
errorMessage: string;
@@ -153,8 +152,6 @@ class Collab extends PureComponent<Props, CollabState> {
componentDidMount() {
window.addEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.addEventListener("online", this.onOfflineStatusToggle);
window.addEventListener("offline", this.onOfflineStatusToggle);
window.addEventListener(EVENT.UNLOAD, this.onUnload);
const collabAPI: CollabAPI = {
@@ -167,8 +164,7 @@ class Collab extends PureComponent<Props, CollabState> {
setUsername: this.setUsername,
};
appJotaiStore.set(collabAPIAtom, collabAPI);
this.onOfflineStatusToggle();
jotaiStore.set(collabAPIAtom, collabAPI);
if (
process.env.NODE_ENV === ENV.TEST ||
@@ -184,13 +180,7 @@ class Collab extends PureComponent<Props, CollabState> {
}
}
onOfflineStatusToggle = () => {
appJotaiStore.set(isOfflineAtom, !window.navigator.onLine);
};
componentWillUnmount() {
window.removeEventListener("online", this.onOfflineStatusToggle);
window.removeEventListener("offline", this.onOfflineStatusToggle);
window.removeEventListener(EVENT.BEFORE_UNLOAD, this.beforeUnload);
window.removeEventListener(EVENT.UNLOAD, this.onUnload);
window.removeEventListener(EVENT.POINTER_MOVE, this.onPointerMove);
@@ -208,10 +198,10 @@ class Collab extends PureComponent<Props, CollabState> {
}
}
isCollaborating = () => appJotaiStore.get(isCollaboratingAtom)!;
isCollaborating = () => jotaiStore.get(isCollaboratingAtom)!;
private setIsCollaborating = (isCollaborating: boolean) => {
appJotaiStore.set(isCollaboratingAtom, isCollaborating);
jotaiStore.set(isCollaboratingAtom, isCollaborating);
};
private onUnload = () => {
@@ -610,7 +600,7 @@ class Collab extends PureComponent<Props, CollabState> {
const localElements = this.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
remoteElements = restoreElements(remoteElements, null);
remoteElements = restoreElements(remoteElements, null, false);
const reconciledElements = _reconcileElements(
localElements,
@@ -804,7 +794,7 @@ class Collab extends PureComponent<Props, CollabState> {
);
handleClose = () => {
appJotaiStore.set(collabDialogShownAtom, false);
jotaiStore.set(collabDialogShownAtom, false);
};
setUsername = (username: string) => {
+2 -3
View File
@@ -10,13 +10,13 @@ import {
shareWindows,
} from "../../components/icons";
import { ToolButton } from "../../components/ToolButton";
import { t } from "../../i18n";
import "./RoomDialog.scss";
import Stack from "../../components/Stack";
import { AppState } from "../../types";
import { trackEvent } from "../../analytics";
import { getFrame } from "../../utils";
import DialogActionButton from "../../components/DialogActionButton";
import { useI18n } from "../../i18n";
const getShareIcon = () => {
const navigator = window.navigator as any;
@@ -51,7 +51,6 @@ const RoomDialog = ({
setErrorMessage: (message: string) => void;
theme: AppState["theme"];
}) => {
const { t } = useI18n();
const roomLinkInput = useRef<HTMLInputElement>(null);
const copyRoomLink = async () => {
@@ -145,7 +144,7 @@ const RoomDialog = ({
<input
type="text"
id="username"
value={username.trim() || ""}
value={username || ""}
className="RoomDialog-username TextInput"
onChange={(event) => onUsernameChange(event.target.value)}
onKeyPress={(event) => event.key === "Enter" && handleClose()}
+34 -18
View File
@@ -1,7 +1,6 @@
import { PRECEDING_ELEMENT_KEY } from "../../constants";
import { ExcalidrawElement } from "../../element/types";
import { AppState } from "../../types";
import { arrayToMapWithIndex } from "../../utils";
export type ReconciledElements = readonly ExcalidrawElement[] & {
_brand: "reconciledElements";
@@ -34,13 +33,30 @@ const shouldDiscardRemoteElement = (
return false;
};
const getElementsMapWithIndex = <T extends ExcalidrawElement>(
elements: readonly T[],
) =>
elements.reduce(
(
acc: {
[key: string]: [element: T, index: number] | undefined;
},
element: T,
idx,
) => {
acc[element.id] = [element, idx];
return acc;
},
{},
);
export const reconcileElements = (
localElements: readonly ExcalidrawElement[],
remoteElements: readonly BroadcastedExcalidrawElement[],
localAppState: AppState,
): ReconciledElements => {
const localElementsData =
arrayToMapWithIndex<ExcalidrawElement>(localElements);
getElementsMapWithIndex<ExcalidrawElement>(localElements);
const reconciledElements: ExcalidrawElement[] = localElements.slice();
@@ -53,7 +69,7 @@ export const reconcileElements = (
for (const remoteElement of remoteElements) {
remoteElementIdx++;
const local = localElementsData.get(remoteElement.id);
const local = localElementsData[remoteElement.id];
if (shouldDiscardRemoteElement(localAppState, local?.[0], remoteElement)) {
if (remoteElement[PRECEDING_ELEMENT_KEY]) {
@@ -89,21 +105,21 @@ export const reconcileElements = (
offset++;
if (cursor === 0) {
reconciledElements.unshift(remoteElement);
localElementsData.set(remoteElement.id, [
localElementsData[remoteElement.id] = [
remoteElement,
cursor - offset,
]);
];
} else {
reconciledElements.splice(cursor + 1, 0, remoteElement);
localElementsData.set(remoteElement.id, [
localElementsData[remoteElement.id] = [
remoteElement,
cursor + 1 - offset,
]);
];
cursor++;
}
} else {
let idx = localElementsData.has(parent)
? localElementsData.get(parent)![1]
let idx = localElementsData[parent]
? localElementsData[parent]![1]
: null;
if (idx != null) {
idx += offset;
@@ -111,38 +127,38 @@ export const reconcileElements = (
if (idx != null && idx >= cursor) {
reconciledElements.splice(idx + 1, 0, remoteElement);
offset++;
localElementsData.set(remoteElement.id, [
localElementsData[remoteElement.id] = [
remoteElement,
idx + 1 - offset,
]);
];
cursor = idx + 1;
} else if (idx != null) {
reconciledElements.splice(cursor + 1, 0, remoteElement);
offset++;
localElementsData.set(remoteElement.id, [
localElementsData[remoteElement.id] = [
remoteElement,
cursor + 1 - offset,
]);
];
cursor++;
} else {
reconciledElements.push(remoteElement);
localElementsData.set(remoteElement.id, [
localElementsData[remoteElement.id] = [
remoteElement,
reconciledElements.length - 1 - offset,
]);
];
}
}
// no parent z-index information, local element exists → replace in place
} else if (local) {
reconciledElements[local[1]] = remoteElement;
localElementsData.set(remoteElement.id, [remoteElement, local[1]]);
localElementsData[remoteElement.id] = [remoteElement, local[1]];
// otherwise push to the end
} else {
reconciledElements.push(remoteElement);
localElementsData.set(remoteElement.id, [
localElementsData[remoteElement.id] = [
remoteElement,
reconciledElements.length - 1 - offset,
]);
];
}
}
@@ -1,13 +1,12 @@
import React from "react";
import { PlusPromoIcon } from "../../components/icons";
import { useI18n } from "../../i18n";
import { t } from "../../i18n";
import { WelcomeScreen } from "../../packages/excalidraw/index";
import { isExcalidrawPlusSignedUser } from "../app_constants";
export const AppWelcomeScreen: React.FC<{
setCollabDialogShown: (toggle: boolean) => any;
}> = React.memo((props) => {
const { t } = useI18n();
let headingContent;
if (isExcalidrawPlusSignedUser) {
+14 -18
View File
@@ -1,21 +1,17 @@
import { shield } from "../../components/icons";
import { Tooltip } from "../../components/Tooltip";
import { useI18n } from "../../i18n";
import { t } from "../../i18n";
export const EncryptedIcon = () => {
const { t } = useI18n();
return (
<a
className="encrypted-icon tooltip"
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("encrypted.link")}
>
<Tooltip label={t("encrypted.tooltip")} long={true}>
{shield}
</Tooltip>
</a>
);
};
export const EncryptedIcon = () => (
<a
className="encrypted-icon tooltip"
href="https://blog.excalidraw.com/end-to-end-encryption/"
target="_blank"
rel="noopener noreferrer"
aria-label={t("encrypted.link")}
>
<Tooltip label={t("encrypted.tooltip")} long={true}>
{shield}
</Tooltip>
</a>
);
@@ -6,7 +6,7 @@ import { loadFirebaseStorage, saveFilesToFirebase } from "../data/firebase";
import { FileId, NonDeletedExcalidrawElement } from "../../element/types";
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
import { nanoid } from "nanoid";
import { useI18n } from "../../i18n";
import { t } from "../../i18n";
import { excalidrawPlusIcon } from "./icons";
import { encryptData, generateEncryptionKey } from "../../data/encryption";
import { isInitializedImageElement } from "../../element/typeChecks";
@@ -79,7 +79,6 @@ export const ExportToExcalidrawPlus: React.FC<{
files: BinaryFiles;
onError: (error: Error) => void;
}> = ({ elements, appState, files, onError }) => {
const { t } = useI18n();
return (
<Card color="primary">
<div className="Card-icon">{excalidrawPlusIcon}</div>
@@ -1,23 +1,22 @@
import { useSetAtom } from "jotai";
import { useAtom } from "jotai";
import React from "react";
import { appLangCodeAtom } from "..";
import { defaultLang, useI18n } from "../../i18n";
import { langCodeAtom } from "..";
import * as i18n from "../../i18n";
import { languages } from "../../i18n";
export const LanguageList = ({ style }: { style?: React.CSSProperties }) => {
const { t, langCode } = useI18n();
const setLangCode = useSetAtom(appLangCodeAtom);
const [langCode, setLangCode] = useAtom(langCodeAtom);
return (
<select
className="dropdown-select dropdown-select__language"
onChange={({ target }) => setLangCode(target.value)}
value={langCode}
aria-label={t("buttons.selectLanguage")}
aria-label={i18n.t("buttons.selectLanguage")}
style={style}
>
<option key={defaultLang.code} value={defaultLang.code}>
{defaultLang.label}
<option key={i18n.defaultLang.code} value={i18n.defaultLang.code}>
{i18n.defaultLang.label}
</option>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
+1 -4
View File
@@ -263,12 +263,9 @@ export const loadScene = async (
await importFromBackend(id, privateKey),
localDataState?.appState,
localDataState?.elements,
{ repairBindings: true },
);
} else {
data = restore(localDataState || null, null, null, {
repairBindings: true,
});
data = restore(localDataState || null, null, null);
}
return {
-17
View File
@@ -45,23 +45,6 @@
}
}
}
.collab-offline-warning {
pointer-events: none;
position: absolute;
top: 6.5rem;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1rem;
font-size: 0.875rem;
text-align: center;
line-height: 1.5;
border-radius: var(--border-radius-md);
background-color: var(--color-warning);
color: var(--color-text-warning);
z-index: 6;
white-space: pre;
}
}
.excalidraw-app.is-collaborating {
+12 -20
View File
@@ -52,7 +52,6 @@ import Collab, {
collabAPIAtom,
collabDialogShownAtom,
isCollaboratingAtom,
isOfflineAtom,
} from "./collab/Collab";
import {
exportToBackend,
@@ -67,7 +66,10 @@ import {
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restore, restoreAppState, RestoredDataState } from "../data/restore";
import "./index.scss";
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
import { updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "../element/mutateElement";
import { isInitializedImageElement } from "../element/typeChecks";
@@ -75,16 +77,13 @@ import { loadFilesFromFirebase } from "./data/firebase";
import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
import clsx from "clsx";
import { atom, Provider, useAtom } from "jotai";
import { jotaiStore, useAtomWithInitialValue } from "../jotai";
import { reconcileElements } from "./collab/reconciliation";
import { parseLibraryTokensFromUrl, useHandleLibrary } from "../data/library";
import { AppMainMenu } from "./components/AppMainMenu";
import { AppWelcomeScreen } from "./components/AppWelcomeScreen";
import { AppFooter } from "./components/AppFooter";
import { atom, Provider, useAtom, useAtomValue } from "jotai";
import { useAtomWithInitialValue } from "../jotai";
import { appJotaiStore } from "./app-jotai";
import "./index.scss";
polyfill();
@@ -227,15 +226,15 @@ const initializeScene = async (opts: {
return { scene: null, isExternalScene: false };
};
const detectedLangCode = languageDetector.detect() || defaultLang.code;
export const appLangCodeAtom = atom(
Array.isArray(detectedLangCode) ? detectedLangCode[0] : detectedLangCode,
const currentLangCode = languageDetector.detect() || defaultLang.code;
export const langCodeAtom = atom(
Array.isArray(currentLangCode) ? currentLangCode[0] : currentLangCode,
);
const ExcalidrawWrapper = () => {
const [errorMessage, setErrorMessage] = useState("");
const [langCode, setLangCode] = useAtom(appLangCodeAtom);
const [langCode, setLangCode] = useAtom(langCodeAtom);
// initial state
// ---------------------------------------------------------------------------
@@ -363,7 +362,7 @@ const ExcalidrawWrapper = () => {
if (data.scene) {
excalidrawAPI.updateScene({
...data.scene,
...restore(data.scene, null, null, { repairBindings: true }),
...restore(data.scene, null, null),
commitToHistory: true,
});
}
@@ -600,8 +599,6 @@ const ExcalidrawWrapper = () => {
localStorage.setItem(STORAGE_KEYS.LOCAL_STORAGE_LIBRARY, serializedItems);
};
const isOffline = useAtomValue(isOfflineAtom);
return (
<div
style={{ height: "100%" }}
@@ -664,11 +661,6 @@ const ExcalidrawWrapper = () => {
/>
<AppWelcomeScreen setCollabDialogShown={setCollabDialogShown} />
<AppFooter />
{isCollaborating && isOffline && (
<div className="collab-offline-warning">
{t("alerts.collabOfflineWarning")}
</div>
)}
</Excalidraw>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && (
@@ -684,7 +676,7 @@ const ExcalidrawWrapper = () => {
const ExcalidrawApp = () => {
return (
<TopErrorBoundary>
<Provider unstable_createStore={() => appJotaiStore}>
<Provider unstable_createStore={() => jotaiStore}>
<ExcalidrawWrapper />
</Provider>
</TopErrorBoundary>
-16
View File
@@ -1,8 +1,6 @@
import fallbackLangData from "./locales/en.json";
import percentages from "./locales/percentages.json";
import { ENV } from "./constants";
import { jotaiScope, jotaiStore } from "./jotai";
import { atom, useAtomValue } from "jotai";
const COMPLETION_THRESHOLD = 85;
@@ -101,8 +99,6 @@ export const setLanguage = async (lang: Language) => {
currentLangData = fallbackLangData;
}
}
jotaiStore.set(editorLangCodeAtom, lang.code);
};
export const getLanguage = () => currentLang;
@@ -147,15 +143,3 @@ export const t = (
}
return translation;
};
/** @private atom used solely to rerender components using `useI18n` hook */
const editorLangCodeAtom = atom(defaultLang.code);
// Should be used in components that fall under these cases:
// - component is rendered as an <Excalidraw> child
// - component is rendered internally by <Excalidraw>, but the component
// is memoized w/o being updated on `langCode`, `AppState`, or `UIAppState`
export const useI18n = () => {
const langCode = useAtomValue(editorLangCodeAtom, jotaiScope);
return { t, langCode };
};
+2 -2
View File
@@ -1,4 +1,4 @@
import { PrimitiveAtom, unstable_createStore, useAtom } from "jotai";
import { unstable_createStore, useAtom, WritableAtom } from "jotai";
import { useLayoutEffect } from "react";
export const jotaiScope = Symbol();
@@ -6,7 +6,7 @@ export const jotaiStore = unstable_createStore();
export const useAtomWithInitialValue = <
T extends unknown,
A extends PrimitiveAtom<T>,
A extends WritableAtom<T, T>,
>(
atom: A,
initialValue: T | (() => T),
+26 -29
View File
@@ -109,35 +109,35 @@
"decreaseFontSize": "تصغير حجم الخط",
"increaseFontSize": "تكبير حجم الخط",
"unbindText": "فك ربط النص",
"bindText": "ربط النص بالحاوية",
"bindText": "",
"link": {
"edit": "تعديل الرابط",
"create": "إنشاء رابط",
"label": "رابط"
},
"lineEditor": {
"edit": "تحرير السطر",
"exit": "الخروج من المُحرر"
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "قفل",
"unlock": "فتح",
"lockAll": "قفل الكل",
"unlockAll": "فتح الكل"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "نُشر",
"sidebarLock": "إبقاء الشريط الجانبي مفتوح"
"statusPublished": "",
"sidebarLock": ""
},
"library": {
"noItems": "لا توجد عناصر أضيفت بعد...",
"hint_emptyLibrary": "حدد عنصر على القماش لإضافته هنا، أو تثبيت مكتبة من المستودع العام أدناه.",
"hint_emptyPrivateLibrary": "حدد عنصر على القماش لإضافته هنا."
"noItems": "",
"hint_emptyLibrary": "",
"hint_emptyPrivateLibrary": ""
},
"buttons": {
"clearReset": "إعادة تعيين اللوحة",
"exportJSON": "صدر الملف",
"exportImage": "تصدير الصورة...",
"export": "حفظ إلى...",
"exportImage": "",
"export": "",
"exportToPng": "تصدير بصيغة PNG",
"exportToSvg": "تصدير بصيغة SVG",
"copyToClipboard": "نسخ إلى الحافظة",
@@ -179,7 +179,7 @@
"couldNotLoadInvalidFile": "تعذر التحميل، الملف غير صالح",
"importBackendFailed": "فشل الاستيراد من الخادوم.",
"cannotExportEmptyCanvas": "لا يمكن تصدير لوحة فارغة.",
"couldNotCopyToClipboard": "تعذر النسخ إلى الحافظة.",
"couldNotCopyToClipboard": "",
"decryptFailed": "تعذر فك تشفير البيانات.",
"uploadedSecurly": "تم تأمين التحميل بتشفير النهاية إلى النهاية، مما يعني أن خادوم Excalidraw والأطراف الثالثة لا يمكنها قراءة المحتوى.",
"loadSceneOverridePrompt": "تحميل الرسم الخارجي سيحل محل المحتوى الموجود لديك. هل ترغب في المتابعة؟",
@@ -192,8 +192,7 @@
"invalidSceneUrl": "تعذر استيراد المشهد من عنوان URL المتوفر. إما أنها مشوهة، أو لا تحتوي على بيانات Excalidraw JSON صالحة.",
"resetLibrary": "هذا سوف يمسح مكتبتك. هل أنت متأكد؟",
"removeItemsFromsLibrary": "حذف {{count}} عنصر (عناصر) من المكتبة؟",
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل.",
"collabOfflineWarning": ""
"invalidEncryptionKey": "مفتاح التشفير يجب أن يكون من 22 حرفاً. التعاون المباشر معطل."
},
"errors": {
"unsupportedFileType": "نوع الملف غير مدعوم.",
@@ -201,10 +200,10 @@
"fileTooBig": "الملف كبير جداً. الحد الأقصى المسموح به للحجم هو {{maxSize}}.",
"svgImageInsertError": "تعذر إدراج صورة SVG. يبدو أن ترميز SVG غير صحيح.",
"invalidSVGString": "SVG غير صالح.",
"cannotResolveCollabServer": "تعذر الاتصال بخادم التعاون. الرجاء إعادة تحميل الصفحة والمحاولة مرة أخرى.",
"importLibraryError": "تعذر تحميل المكتبة",
"collabSaveFailed": "تعذر الحفظ في قاعدة البيانات. إذا استمرت المشاكل، يفضل أن تحفظ ملفك محليا كي لا تفقد عملك.",
"collabSaveFailed_sizeExceeded": "تعذر الحفظ في قاعدة البيانات، يبدو أن القماش كبير للغاية، يفضّل حفظ الملف محليا كي لا تفقد عملك."
"cannotResolveCollabServer": "",
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
},
"toolBar": {
"selection": "تحديد",
@@ -218,10 +217,9 @@
"text": "نص",
"library": "مكتبة",
"lock": "الحفاظ على أداة التحديد نشطة بعد الرسم",
"penMode": "وضع القلم - امنع اللمس",
"link": "إضافة/تحديث الرابط للشكل المحدد",
"eraser": "ممحاة",
"hand": ""
"penMode": "",
"link": "",
"eraser": "ممحاة"
},
"headings": {
"canvasActions": "إجراءات اللوحة",
@@ -229,7 +227,7 @@
"shapes": "الأشكال"
},
"hints": {
"canvasPanning": "لتحريك القماش، اضغط على عجلة الفأرة أو مفتاح المسافة أثناء السحب، أو استخدم أداة اليد",
"canvasPanning": "لتحريك لوحة الرسم ، استمر في الضغط على عجلة الماوس أو مفتاح المسافة أثناء السحب",
"linearElement": "انقر لبدء نقاط متعددة، اسحب لخط واحد",
"freeDraw": "انقر واسحب، افرج عند الانتهاء",
"text": "نصيحة: يمكنك أيضًا إضافة نص بالنقر المزدوج في أي مكان بأداة الاختيار",
@@ -240,15 +238,14 @@
"resize": "يمكنك تقييد النسب بالضغط على SHIFT أثناء تغيير الحجم،\nاضغط على ALT لتغيير الحجم من المركز",
"resizeImage": "يمكنك تغيير الحجم بحرية بالضغط بأستمرار على SHIFT،\nاضغط بأستمرار على ALT أيضا لتغيير الحجم من المركز",
"rotate": "يمكنك تقييد الزوايا من خلال الضغط على SHIFT أثناء الدوران",
"lineEditor_info": "اضغط على مفتاح (Ctrl أو Cmd) و انقر بشكل مزدوج، أو اضغط على مفتاحي (Ctrl أو Cmd) و (Enter) لتعديل النقاط",
"lineEditor_info": "",
"lineEditor_pointSelected": "",
"lineEditor_nothingSelected": "",
"placeImage": "",
"publishLibrary": "نشر مكتبتك",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "تعذر عرض المعاينة",
+3 -6
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "Този файлов формат не се поддържа.",
@@ -220,8 +219,7 @@
"lock": "Поддържайте избрания инструмент активен след рисуване",
"penMode": "",
"link": "",
"eraser": "",
"hand": ""
"eraser": ""
},
"headings": {
"canvasActions": "Действия по платното",
@@ -247,8 +245,7 @@
"publishLibrary": "",
"bindTextToElement": "Натиснете Enter, за да добавите",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "Невъзможност за показване на preview",
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "সরবরাহ করা লিঙ্ক থেকে দৃশ্য লোড করা যায়নি৷ এটি হয় বিকৃত, অথবা বৈধ এক্সক্যালিড্র জেসন তথ্য নেই৷",
"resetLibrary": "এটি আপনার সংগ্রহ পরিষ্কার করবে। আপনি কি নিশ্চিত?",
"removeItemsFromsLibrary": "সংগ্রহ থেকে {{count}} বস্তু বিয়োগ করা হবে। আপনি কি নিশ্চিত?",
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।",
"collabOfflineWarning": ""
"invalidEncryptionKey": "অবৈধ এনক্রীপশন কী।"
},
"errors": {
"unsupportedFileType": "অসমর্থিত ফাইল।",
@@ -220,8 +219,7 @@
"lock": "আঁকার পরে নির্বাচিত টুল সক্রিয় রাখুন",
"penMode": "",
"link": "একটি নির্বাচিত আকৃতির জন্য লিঙ্ক যোগ বা আপডেট করুন",
"eraser": "ঝাড়ন",
"hand": ""
"eraser": "ঝাড়ন"
},
"headings": {
"canvasActions": "ক্যানভাস কার্যকলাপ",
@@ -229,7 +227,7 @@
"shapes": "আকার(গুলি)"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "ক্যানভাস সরানোর জন্য মাউস হুইল বা স্পেসবার ধরে টানুন",
"linearElement": "একাধিক বিন্দু শুরু করতে ক্লিক করুন, একক লাইনের জন্য টেনে আনুন",
"freeDraw": "ক্লিক করুন এবং টেনে আনুন, আপনার কাজ শেষ হলে ছেড়ে দিন",
"text": "বিশেষ্য: আপনি নির্বাচন টুলের সাথে যে কোনো জায়গায় ডাবল-ক্লিক করে পাঠ্য যোগ করতে পারেন",
@@ -247,8 +245,7 @@
"publishLibrary": "আপনার নিজস্ব সংগ্রহ প্রকাশ করুন",
"bindTextToElement": "লেখা যোগ করতে এন্টার টিপুন",
"deepBoxSelect": "",
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন",
"firefox_clipboard_write": ""
"eraserRevert": "মুছে ফেলার জন্য চিহ্নিত উপাদানগুলিকে ফিরিয়ে আনতে অল্ট ধরে রাখুন"
},
"canvasError": {
"cannotShowPreview": "প্রিভিউ দেখাতে অপারগ",
+26 -29
View File
@@ -1,7 +1,7 @@
{
"labels": {
"paste": "Enganxa",
"pasteAsPlaintext": "Enganxar com a text pla",
"pasteAsPlaintext": "",
"pasteCharts": "Enganxa els diagrames",
"selectAll": "Selecciona-ho tot",
"multiSelect": "Afegeix un element a la selecció",
@@ -72,7 +72,7 @@
"layers": "Capes",
"actions": "Accions",
"language": "Llengua",
"liveCollaboration": "Col·laboració en directe...",
"liveCollaboration": "",
"duplicateSelection": "Duplica",
"untitled": "Sense títol",
"name": "Nom",
@@ -116,8 +116,8 @@
"label": "Enllaç"
},
"lineEditor": {
"edit": "Editar línia",
"exit": "Sortir de l'editor de línia"
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "Bloca",
@@ -136,8 +136,8 @@
"buttons": {
"clearReset": "Neteja el llenç",
"exportJSON": "Exporta a un fitxer",
"exportImage": "Exporta la imatge...",
"export": "Guardar a...",
"exportImage": "",
"export": "",
"exportToPng": "Exporta a PNG",
"exportToSvg": "Exporta a SNG",
"copyToClipboard": "Copia al porta-retalls",
@@ -145,7 +145,7 @@
"scale": "Escala",
"save": "Desa al fitxer actual",
"saveAs": "Anomena i desa",
"load": "Obrir",
"load": "",
"getShareableLink": "Obté l'enllaç per a compartir",
"close": "Tanca",
"selectLanguage": "Trieu la llengua",
@@ -192,8 +192,7 @@
"invalidSceneUrl": "No s'ha pogut importar l'escena des de l'adreça URL proporcionada. Està malformada o no conté dades Excalidraw JSON vàlides.",
"resetLibrary": "Això buidarà la biblioteca. N'esteu segur?",
"removeItemsFromsLibrary": "Suprimir {{count}} element(s) de la biblioteca?",
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada.",
"collabOfflineWarning": "Sense connexió a internet disponible.\nEls vostres canvis no seran guardats!"
"invalidEncryptionKey": "La clau d'encriptació ha de tenir 22 caràcters. La col·laboració en directe està desactivada."
},
"errors": {
"unsupportedFileType": "Tipus de fitxer no suportat.",
@@ -203,8 +202,8 @@
"invalidSVGString": "SVG no vàlid.",
"cannotResolveCollabServer": "No ha estat possible connectar amb el servidor collab. Si us plau recarregueu la pàgina i torneu a provar.",
"importLibraryError": "No s'ha pogut carregar la biblioteca",
"collabSaveFailed": "No s'ha pogut desar a la base de dades de fons. Si els problemes persisteixen, hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball.",
"collabSaveFailed_sizeExceeded": "No s'ha pogut desar a la base de dades de fons, sembla que el llenç és massa gran. Hauríeu de desar el fitxer localment per assegurar-vos que no perdeu el vostre treball."
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
},
"toolBar": {
"selection": "Selecció",
@@ -218,10 +217,9 @@
"text": "Text",
"library": "Biblioteca",
"lock": "Mantenir activa l'eina seleccionada desprès de dibuixar",
"penMode": "Mode de llapis - evita tocar",
"penMode": "",
"link": "Afegeix / actualitza l'enllaç per a la forma seleccionada",
"eraser": "Esborrador",
"hand": "Mà (eina de desplaçament)"
"eraser": "Esborrador"
},
"headings": {
"canvasActions": "Accions del llenç",
@@ -229,7 +227,7 @@
"shapes": "Formes"
},
"hints": {
"canvasPanning": "Per moure el llenç, manteniu premuda la roda del ratolí o la barra espaiadora mentre arrossegueu o utilitzeu l'eina manual",
"canvasPanning": "Per a moure el llenç, mantingueu premuda la roda del ratolí o la tecla espai mentre l'arrossegueu",
"linearElement": "Feu clic per a dibuixar múltiples punts; arrossegueu per a una sola línia",
"freeDraw": "Feu clic i arrossegueu, deixeu anar per a finalitzar",
"text": "Consell: també podeu afegir text fent doble clic en qualsevol lloc amb l'eina de selecció",
@@ -240,15 +238,14 @@
"resize": "Per restringir les proporcions mentres es canvia la mida, mantenir premut el majúscul (SHIFT); per canviar la mida des del centre, mantenir premut ALT",
"resizeImage": "Podeu redimensionar lliurement prement MAJÚSCULA;\nper a redimensionar des del centre, premeu ALT",
"rotate": "Per restringir els angles mentre gira, mantenir premut el majúscul (SHIFT)",
"lineEditor_info": "Mantingueu premut Ctrl o Cmd i feu doble clic o premeu Ctrl o Cmd + Retorn per editar els punts",
"lineEditor_info": "",
"lineEditor_pointSelected": "Premeu Suprimir per a eliminar el(s) punt(s), CtrlOrCmd+D per a duplicar-lo, o arrossegueu-lo per a moure'l",
"lineEditor_nothingSelected": "Seleccioneu un punt per a editar-lo (premeu SHIFT si voleu\nselecció múltiple), o manteniu Alt i feu clic per a afegir més punts",
"placeImage": "Feu clic per a col·locar la imatge o clic i arrossegar per a establir-ne la mida manualment",
"publishLibrary": "Publiqueu la vostra pròpia llibreria",
"bindTextToElement": "Premeu enter per a afegir-hi text",
"deepBoxSelect": "Manteniu CtrlOrCmd per a selecció profunda, i per a evitar l'arrossegament",
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar",
"firefox_clipboard_write": "És probable que aquesta funció es pugui activar posant la marca \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Per canviar les marques del navegador al Firefox, visiteu la pàgina \"about:config\"."
"eraserRevert": "Mantingueu premuda Alt per a revertir els elements seleccionats per a esborrar"
},
"canvasError": {
"cannotShowPreview": "No es pot mostrar la previsualització",
@@ -296,7 +293,7 @@
"blog": "Llegiu el nostre blog",
"click": "clic",
"deepSelect": "Selecció profunda",
"deepBoxSelect": "Seleccioneu profundament dins del quadre i eviteu arrossegar",
"deepBoxSelect": "",
"curvedArrow": "Fletxa corba",
"curvedLine": "Línia corba",
"documentation": "Documentació",
@@ -317,8 +314,8 @@
"zoomToFit": "Zoom per veure tots els elements",
"zoomToSelection": "Zoom per veure la selecció",
"toggleElementLock": "Blocar/desblocar la selecció",
"movePageUpDown": "Mou la pàgina cap amunt/a baix",
"movePageLeftRight": "Mou la pàgina cap a l'esquerra/dreta"
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": "Neteja el llenç"
@@ -400,7 +397,7 @@
"fileSavedToFilename": "S'ha desat a {filename}",
"canvas": "el llenç",
"selection": "la selecció",
"pasteAsSingleElement": "Fer servir {{shortcut}} per enganxar com un sol element,\no enganxeu-lo en un editor de text existent"
"pasteAsSingleElement": ""
},
"colors": {
"ffffff": "Blanc",
@@ -451,15 +448,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "Totes les vostres dades es guarden localment al vostre navegador.",
"center_heading_plus": "Vols anar a Excalidraw+ en comptes?",
"menuHint": "Exportar, preferències, llenguatges..."
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "Exportar, preferències i més...",
"center_heading": "Diagrames. Fer. Simple.",
"toolbarHint": "Selecciona una eina i comença a dibuixar!",
"helpHint": "Dreceres i ajuda"
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}
+3 -6
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
@@ -220,8 +219,7 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": "Guma",
"hand": ""
"eraser": "Guma"
},
"headings": {
"canvasActions": "",
@@ -247,8 +245,7 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "",
+3 -6
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "",
"resetLibrary": "",
"removeItemsFromsLibrary": "",
"invalidEncryptionKey": "",
"collabOfflineWarning": ""
"invalidEncryptionKey": ""
},
"errors": {
"unsupportedFileType": "",
@@ -220,8 +219,7 @@
"lock": "",
"penMode": "",
"link": "",
"eraser": "",
"hand": ""
"eraser": ""
},
"headings": {
"canvasActions": "",
@@ -247,8 +245,7 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "",
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Die Szene konnte nicht von der angegebenen URL importiert werden. Sie ist entweder fehlerhaft oder enthält keine gültigen Excalidraw JSON-Daten.",
"resetLibrary": "Dieses löscht deine Bibliothek. Bist du sicher?",
"removeItemsFromsLibrary": "{{count}} Element(e) aus der Bibliothek löschen?",
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert.",
"collabOfflineWarning": "Keine Internetverbindung verfügbar.\nDeine Änderungen werden nicht gespeichert!"
"invalidEncryptionKey": "Verschlüsselungsschlüssel muss 22 Zeichen lang sein. Die Live-Zusammenarbeit ist deaktiviert."
},
"errors": {
"unsupportedFileType": "Nicht unterstützter Dateityp.",
@@ -220,8 +219,7 @@
"lock": "Ausgewähltes Werkzeug nach Zeichnen aktiv lassen",
"penMode": "Stift-Modus - Berührung verhindern",
"link": "Link für ausgewählte Form hinzufügen / aktualisieren",
"eraser": "Radierer",
"hand": "Hand (Schwenkwerkzeug)"
"eraser": "Radierer"
},
"headings": {
"canvasActions": "Aktionen für Zeichenfläche",
@@ -229,7 +227,7 @@
"shapes": "Formen"
},
"hints": {
"canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens, oder verwende das Hand-Werkzeug",
"canvasPanning": "Um die Zeichenfläche zu verschieben, halte das Mausrad oder die Leertaste während des Ziehens",
"linearElement": "Klicken für Linie mit mehreren Punkten, Ziehen für einzelne Linie",
"freeDraw": "Klicke und ziehe. Lass los, wenn du fertig bist",
"text": "Tipp: Du kannst auch Text hinzufügen, indem du mit dem Auswahlwerkzeug auf eine beliebige Stelle doppelklickst",
@@ -247,8 +245,7 @@
"publishLibrary": "Veröffentliche deine eigene Bibliothek",
"bindTextToElement": "Zum Hinzufügen Eingabetaste drücken",
"deepBoxSelect": "Halte CtrlOrCmd gedrückt, um innerhalb der Gruppe auszuwählen, und um Ziehen zu vermeiden",
"eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen",
"firefox_clipboard_write": "Diese Funktion kann wahrscheinlich aktiviert werden, indem die Einstellung \"dom.events.asyncClipboard.clipboardItem\" auf \"true\" gesetzt wird. Um die Browsereinstellungen in Firefox zu ändern, besuche die Seite \"about:config\"."
"eraserRevert": "Halte Alt gedrückt, um die zum Löschen markierten Elemente zurückzusetzen"
},
"canvasError": {
"cannotShowPreview": "Vorschau kann nicht angezeigt werden",
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Δεν ήταν δυνατή η εισαγωγή σκηνής από το URL που δώσατε. Είτε έχει λάθος μορφή, είτε δεν περιέχει έγκυρα δεδομένα JSON Excalidraw.",
"resetLibrary": "Αυτό θα καθαρίσει τη βιβλιοθήκη σας. Είστε σίγουροι;",
"removeItemsFromsLibrary": "Διαγραφή {{count}} αντικειμένου(ων) από τη βιβλιοθήκη;",
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη.",
"collabOfflineWarning": "Δεν υπάρχει διαθέσιμη σύνδεση στο internet.\nΟι αλλαγές σας δεν θα αποθηκευτούν!"
"invalidEncryptionKey": "Το κλειδί κρυπτογράφησης πρέπει να είναι 22 χαρακτήρες. Η ζωντανή συνεργασία είναι απενεργοποιημένη."
},
"errors": {
"unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου.",
@@ -220,8 +219,7 @@
"lock": "Κράτησε επιλεγμένο το εργαλείο μετά το σχέδιο",
"penMode": "Λειτουργία μολυβιού - αποτροπή αφής",
"link": "Προσθήκη/ Ενημέρωση συνδέσμου για ένα επιλεγμένο σχήμα",
"eraser": "Γόμα",
"hand": ""
"eraser": "Γόμα"
},
"headings": {
"canvasActions": "Ενέργειες καμβά",
@@ -229,7 +227,7 @@
"shapes": "Σχήματα"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "Για να μετακινήσετε καμβά, κρατήστε πατημένο τον τροχό του ποντικιού ή το πλήκτρο διαστήματος ενώ σύρετε",
"linearElement": "Κάνε κλικ για να ξεκινήσεις πολλαπλά σημεία, σύρε για μια γραμμή",
"freeDraw": "Κάντε κλικ και σύρτε, απελευθερώσατε όταν έχετε τελειώσει",
"text": "Tip: μπορείτε επίσης να προσθέστε κείμενο με διπλό-κλικ οπουδήποτε με το εργαλείο επιλογών",
@@ -247,8 +245,7 @@
"publishLibrary": "Δημοσιεύστε τη δική σας βιβλιοθήκη",
"bindTextToElement": "Πατήστε Enter για προσθήκη κειμένου",
"deepBoxSelect": "Κρατήστε πατημένο το CtrlOrCmd για να επιλέξετε βαθιά, και να αποτρέψετε τη μεταφορά",
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή",
"firefox_clipboard_write": "Αυτή η επιλογή μπορεί πιθανώς να ενεργοποιηθεί αλλάζοντας την ρύθμιση \"dom.events.asyncClipboard.clipboardItem\" σε \"true\". Για να αλλάξετε τις ρυθμίσεις του προγράμματος περιήγησης στο Firefox, επισκεφθείτε τη σελίδα \"about:config\"."
"eraserRevert": "Κρατήστε πατημένο το Alt για να επαναφέρετε τα στοιχεία που σημειώθηκαν για διαγραφή"
},
"canvasError": {
"cannotShowPreview": "Αδυναμία εμφάνισης προεπισκόπησης",
+1 -2
View File
@@ -193,8 +193,7 @@
"invalidSceneUrl": "Couldn't import scene from the supplied URL. It's either malformed, or doesn't contain valid Excalidraw JSON data.",
"resetLibrary": "This will clear your library. Are you sure?",
"removeItemsFromsLibrary": "Delete {{count}} item(s) from library?",
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled.",
"collabOfflineWarning": "No internet connection available.\nYour changes will not be saved!"
"invalidEncryptionKey": "Encryption key must be of 22 characters. Live collaboration is disabled."
},
"errors": {
"unsupportedFileType": "Unsupported file type.",
+11 -14
View File
@@ -103,7 +103,7 @@
"share": "Compartir",
"showStroke": "Mostrar selector de color de trazo",
"showBackground": "Mostrar el selector de color de fondo",
"toggleTheme": "Cambiar tema",
"toggleTheme": "Alternar tema",
"personalLib": "Biblioteca personal",
"excalidrawLib": "Biblioteca Excalidraw",
"decreaseFontSize": "Disminuir tamaño de letra",
@@ -192,8 +192,7 @@
"invalidSceneUrl": "No se ha podido importar la escena desde la URL proporcionada. Está mal formada, o no contiene datos de Excalidraw JSON válidos.",
"resetLibrary": "Esto borrará tu biblioteca. ¿Estás seguro?",
"removeItemsFromsLibrary": "¿Eliminar {{count}} elemento(s) de la biblioteca?",
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada.",
"collabOfflineWarning": "No hay conexión a internet disponible.\n¡No se guardarán los cambios!"
"invalidEncryptionKey": "La clave de cifrado debe tener 22 caracteres. La colaboración en vivo está deshabilitada."
},
"errors": {
"unsupportedFileType": "Tipo de archivo no admitido.",
@@ -220,8 +219,7 @@
"lock": "Mantener la herramienta seleccionada activa después de dibujar",
"penMode": "Modo Lápiz - previene toque",
"link": "Añadir/Actualizar enlace para una forma seleccionada",
"eraser": "Borrar",
"hand": "Mano (herramienta de panoramización)"
"eraser": "Borrar"
},
"headings": {
"canvasActions": "Acciones del lienzo",
@@ -229,12 +227,12 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra espaciadora mientras arrastra o utilice la herramienta de mano",
"canvasPanning": "Para mover el lienzo, mantenga la rueda del ratón o la barra de espacio mientras arrastra",
"linearElement": "Haz clic para dibujar múltiples puntos, arrastrar para solo una línea",
"freeDraw": "Haz clic y arrastra, suelta al terminar",
"text": "Consejo: también puedes añadir texto haciendo doble clic en cualquier lugar con la herramienta de selección",
"text_selected": "Doble clic o pulse ENTER para editar el texto",
"text_editing": "Pulse Escape o Ctrl/Cmd + ENTER para terminar de editar",
"text_editing": "Pulse Escape o CtrlOrCmd+ENTER para terminar de editar",
"linearElementMulti": "Haz clic en el último punto o presiona Escape o Enter para finalizar",
"lockAngle": "Puedes restringir el ángulo manteniendo presionado el botón SHIFT",
"resize": "Para mantener las proporciones mantén SHIFT presionado mientras modificas el tamaño, \nmantén presionado ALT para modificar el tamaño desde el centro",
@@ -247,8 +245,7 @@
"publishLibrary": "Publica tu propia biblioteca",
"bindTextToElement": "Presione Entrar para agregar",
"deepBoxSelect": "Mantén CtrlOrCmd para seleccionar en profundidad, y para evitar arrastrar",
"eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación",
"firefox_clipboard_write": "Esta característica puede ser habilitada estableciendo la bandera \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar las banderas del navegador en Firefox, visite la página \"about:config\"."
"eraserRevert": "Mantenga pulsado Alt para revertir los elementos marcados para su eliminación"
},
"canvasError": {
"cannotShowPreview": "No se puede mostrar la vista previa",
@@ -315,7 +312,7 @@
"title": "Ayuda",
"view": "Vista",
"zoomToFit": "Ajustar la vista para mostrar todos los elementos",
"zoomToSelection": "Ampliar selección",
"zoomToSelection": "Zoom a la selección",
"toggleElementLock": "Bloquear/desbloquear selección",
"movePageUpDown": "Mover página hacia arriba/abajo",
"movePageLeftRight": "Mover página hacia la izquierda/derecha"
@@ -327,9 +324,9 @@
"title": "Publicar biblioteca",
"itemName": "Nombre del artículo",
"authorName": "Nombre del autor",
"githubUsername": "Nombre de usuario de GitHub",
"githubUsername": "Nombre de usuario de Github",
"twitterUsername": "Nombre de usuario de Twitter",
"libraryName": "Nombre de la biblioteca",
"libraryName": "Nombre de la librería",
"libraryDesc": "Descripción de la biblioteca",
"website": "Sitio Web",
"placeholder": {
@@ -337,7 +334,7 @@
"libraryName": "Nombre de tu biblioteca",
"libraryDesc": "Descripción de su biblioteca para ayudar a la gente a entender su uso",
"githubHandle": "Nombre de usuario de GitHub (opcional), así podrá editar la biblioteca una vez enviada para su revisión",
"twitterHandle": "Nombre de usuario de Twitter (opcional), así sabemos a quién acreditar cuando se promociona en Twitter",
"twitterHandle": "Nombre de usuario de Twitter (opcional), así que sabemos a quién acreditar cuando se promociona en Twitter",
"website": "Enlace a su sitio web personal o en cualquier otro lugar (opcional)"
},
"errors": {
@@ -459,7 +456,7 @@
"menuHint": "Exportar, preferencias y más...",
"center_heading": "Diagramas. Hecho. Simplemente.",
"toolbarHint": "¡Elige una herramienta y empieza a dibujar!",
"helpHint": "Atajos y ayuda"
"helpHint": "Atajos & ayuda"
}
}
}
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Ezin izan da eszena inportatu emandako URLtik. Gaizki eratuta dago edo ez du baliozko Excalidraw JSON daturik.",
"resetLibrary": "Honek zure liburutegia garbituko du. Ziur zaude?",
"removeItemsFromsLibrary": "Liburutegitik {{count}} elementu ezabatu?",
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago.",
"collabOfflineWarning": "Ez dago Interneteko konexiorik.\nZure aldaketak ez dira gordeko!"
"invalidEncryptionKey": "Enkriptazio-gakoak 22 karaktere izan behar ditu. Zuzeneko lankidetza desgaituta dago."
},
"errors": {
"unsupportedFileType": "Onartu gabeko fitxategi mota.",
@@ -220,8 +219,7 @@
"lock": "Mantendu aktibo hautatutako tresna marraztu ondoren",
"penMode": "Luma modua - ukipena saihestu",
"link": "Gehitu / Eguneratu esteka hautatutako forma baterako",
"eraser": "Borragoma",
"hand": "Eskua (panoratze tresna)"
"eraser": "Borragoma"
},
"headings": {
"canvasActions": "Canvas ekintzak",
@@ -229,7 +227,7 @@
"shapes": "Formak"
},
"hints": {
"canvasPanning": "Oihala mugitzeko, eutsi saguaren gurpila edo zuriune-barra arrastatzean, edo erabili esku tresna",
"canvasPanning": "Oihala mugitzeko, sakatu saguaren gurpila edo zuriune-barra arrastatzean",
"linearElement": "Egin klik hainbat puntu hasteko, arrastatu lerro bakarrerako",
"freeDraw": "Egin klik eta arrastatu, askatu amaitutakoan",
"text": "Aholkua: testua gehitu dezakezu edozein lekutan klik bikoitza eginez hautapen tresnarekin",
@@ -247,8 +245,7 @@
"publishLibrary": "Argitaratu zure liburutegia",
"bindTextToElement": "Sakatu Sartu testua gehitzeko",
"deepBoxSelect": "Eutsi Ctrl edo Cmd sakatuta aukeraketa sakona egiteko eta arrastatzea saihesteko",
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko",
"firefox_clipboard_write": "Ezaugarri hau \"dom.events.asyncClipboard.clipboardItem\" marka \"true\" gisa ezarrita gaitu daiteke. Firefox-en arakatzailearen banderak aldatzeko, bisitatu \"about:config\" orrialdera."
"eraserRevert": "Eduki Alt sakatuta ezabatzeko markatutako elementuak leheneratzeko"
},
"canvasError": {
"cannotShowPreview": "Ezin da oihala aurreikusi",
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "بوم نقاشی از آدرس ارائه شده وارد نشد. این یا نادرست است، یا حاوی داده Excalidraw JSON معتبر نیست.",
"resetLibrary": "ین کار کل صفحه را پاک میکند. آیا مطمئنید?",
"removeItemsFromsLibrary": "حذف {{count}} آیتم(ها) از کتابخانه?",
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است.",
"collabOfflineWarning": ""
"invalidEncryptionKey": "کلید رمزگذاری باید 22 کاراکتر باشد. همکاری زنده غیرفعال است."
},
"errors": {
"unsupportedFileType": "نوع فایل پشتیبانی نشده.",
@@ -220,8 +219,7 @@
"lock": "ابزار انتخاب شده را بعد از کشیدن نگه دار",
"penMode": "حالت قلم - جلوگیری از تماس",
"link": "افزودن/به‌روزرسانی پیوند برای شکل انتخابی",
"eraser": "پاک کن",
"hand": ""
"eraser": "پاک کن"
},
"headings": {
"canvasActions": "عملیات روی بوم",
@@ -229,7 +227,7 @@
"shapes": "شکل‌ها"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "برای حرکت دادن بوم، چرخ ماوس یا فاصله را در حین کشیدن نگه دارید",
"linearElement": "برای چند نقطه کلیک و برای یک خط بکشید",
"freeDraw": "کلیک کنید و بکشید و وقتی کار تمام شد رها کنید",
"text": "نکته: با برنامه انتخاب شده شما میتوانید با دوبار کلیک کردن هرکجا میخواید متن اظاف کنید",
@@ -247,8 +245,7 @@
"publishLibrary": "کتابخانه خود را منتشر کنید",
"bindTextToElement": "برای افزودن اینتر را بزنید",
"deepBoxSelect": "CtrlOrCmd را برای انتخاب عمیق و جلوگیری از کشیدن نگه دارید",
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند",
"firefox_clipboard_write": ""
"eraserRevert": "Alt را نگه دارید تا عناصر علامت گذاری شده برای حذف برگردند"
},
"canvasError": {
"cannotShowPreview": "پیش نمایش نشان داده نمی شود",
+31 -34
View File
@@ -1,7 +1,7 @@
{
"labels": {
"paste": "Liitä",
"pasteAsPlaintext": "Liitä pelkkänä tekstinä",
"pasteAsPlaintext": "",
"pasteCharts": "Liitä kaaviot",
"selectAll": "Valitse kaikki",
"multiSelect": "Lisää kohde valintaan",
@@ -72,7 +72,7 @@
"layers": "Tasot",
"actions": "Toiminnot",
"language": "Kieli",
"liveCollaboration": "Live Yhteistyö...",
"liveCollaboration": "",
"duplicateSelection": "Monista",
"untitled": "Nimetön",
"name": "Nimi",
@@ -116,14 +116,14 @@
"label": "Linkki"
},
"lineEditor": {
"edit": "Muokkaa riviä",
"exit": "Poistu rivieditorista"
"edit": "",
"exit": ""
},
"elementLock": {
"lock": "Lukitse",
"unlock": "Poista lukitus",
"lockAll": "Lukitse kaikki",
"unlockAll": "Poista lukitus kaikista"
"lock": "",
"unlock": "",
"lockAll": "",
"unlockAll": ""
},
"statusPublished": "Julkaistu",
"sidebarLock": "Pidä sivupalkki avoinna"
@@ -136,8 +136,8 @@
"buttons": {
"clearReset": "Tyhjennä piirtoalue",
"exportJSON": "Vie tiedostoon",
"exportImage": "Vie kuva...",
"export": "Tallenna nimellä...",
"exportImage": "",
"export": "",
"exportToPng": "Vie PNG-tiedostona",
"exportToSvg": "Vie SVG-tiedostona",
"copyToClipboard": "Kopioi leikepöydälle",
@@ -145,7 +145,7 @@
"scale": "Koko",
"save": "Tallenna nykyiseen tiedostoon",
"saveAs": "Tallenna nimellä",
"load": "Avaa",
"load": "",
"getShareableLink": "Hae jaettava linkki",
"close": "Sulje",
"selectLanguage": "Valitse kieli",
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Teosta ei voitu tuoda annetusta URL-osoitteesta. Tallenne on vioittunut, tai osoitteessa ei ole Excalidraw JSON-dataa.",
"resetLibrary": "Tämä tyhjentää kirjastosi. Jatketaanko?",
"removeItemsFromsLibrary": "Poista {{count}} kohdetta kirjastosta?",
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä.",
"collabOfflineWarning": "Internet-yhteyttä ei ole saatavilla.\nMuutoksiasi ei tallenneta!"
"invalidEncryptionKey": "Salausavaimen on oltava 22 merkkiä pitkä. Live-yhteistyö ei ole käytössä."
},
"errors": {
"unsupportedFileType": "Tiedostotyyppiä ei tueta.",
@@ -202,9 +201,9 @@
"svgImageInsertError": "SVG- kuvaa ei voitu lisätä. Tiedoston SVG-sisältö näyttää virheelliseltä.",
"invalidSVGString": "Virheellinen SVG.",
"cannotResolveCollabServer": "Yhteyden muodostaminen collab-palvelimeen epäonnistui. Virkistä sivu ja yritä uudelleen.",
"importLibraryError": "Kokoelman lataaminen epäonnistui",
"collabSaveFailed": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi.",
"collabSaveFailed_sizeExceeded": "Ei voitu tallentaan palvelimen tietokantaan. Jos ongelmia esiintyy, sinun kannatta tallentaa tallentaa tiedosto paikallisesti varmistaaksesi, että et menetä työtäsi."
"importLibraryError": "",
"collabSaveFailed": "",
"collabSaveFailed_sizeExceeded": ""
},
"toolBar": {
"selection": "Valinta",
@@ -218,10 +217,9 @@
"text": "Teksti",
"library": "Kirjasto",
"lock": "Pidä valittu työkalu aktiivisena piirron jälkeen",
"penMode": "Kynätila - estä kosketus",
"penMode": "",
"link": "Lisää/päivitä linkki valitulle muodolle",
"eraser": "Poistotyökalu",
"hand": "Käsi (panning-työkalu)"
"eraser": "Poistotyökalu"
},
"headings": {
"canvasActions": "Piirtoalueen toiminnot",
@@ -229,7 +227,7 @@
"shapes": "Muodot"
},
"hints": {
"canvasPanning": "Piirtoalueen liikuttamiseksi pidä hiiren pyörää tai välilyöntiä pohjassa tai käytä käsityökalua",
"canvasPanning": "Liikuttaaksesi piirtoaluetta, raahaa hiiren vieritysrulla tai välilyöntinäppäin alaspainettuna",
"linearElement": "Klikkaa piirtääksesi useampi piste, raahaa piirtääksesi yksittäinen viiva",
"freeDraw": "Paina ja raahaa, päästä irti kun olet valmis",
"text": "Vinkki: voit myös lisätä tekstiä kaksoisnapsauttamalla mihin tahansa valintatyökalulla",
@@ -240,15 +238,14 @@
"resize": "Voit rajoittaa mittasuhteet pitämällä SHIFT-näppäintä alaspainettuna kun muutat kokoa, pidä ALT-näppäintä alaspainettuna muuttaaksesi kokoa keskipisteen suhteen",
"resizeImage": "Voit muuttaa kokoa vapaasti pitämällä SHIFTiä pohjassa, pidä ALT pohjassa muuttaaksesi kokoa keskipisteen ympäri",
"rotate": "Voit rajoittaa kulman pitämällä SHIFT pohjassa pyörittäessäsi",
"lineEditor_info": "Pidä CtrlOrCmd pohjassa ja kaksoisnapsauta tai paina CtrlOrCmd + Enter muokataksesi pisteitä",
"lineEditor_info": "",
"lineEditor_pointSelected": "Poista piste(et) painamalla delete, monista painamalla CtrlOrCmd+D, tai liikuta raahaamalla",
"lineEditor_nothingSelected": "Valitse muokattava piste (monivalinta pitämällä SHIFT pohjassa), tai paina Alt ja klikkaa lisätäksesi uusia pisteitä",
"placeImage": "Klikkaa asettaaksesi kuvan, tai klikkaa ja raahaa asettaaksesi sen koon manuaalisesti",
"publishLibrary": "Julkaise oma kirjasto",
"bindTextToElement": "Lisää tekstiä painamalla enter",
"deepBoxSelect": "Käytä syvävalintaa ja estä raahaus painamalla CtrlOrCmd",
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen",
"firefox_clipboard_write": "Tämä ominaisuus voidaan todennäköisesti ottaa käyttöön asettamalla \"dom.events.asyncClipboard.clipboardItem\" kohta \"true\":ksi. Vaihtaaksesi selaimen kohdan Firefoxissa, käy \"about:config\" sivulla."
"eraserRevert": "Pidä Alt alaspainettuna, kumotaksesi merkittyjen elementtien poistamisen"
},
"canvasError": {
"cannotShowPreview": "Esikatselua ei voitu näyttää",
@@ -316,9 +313,9 @@
"view": "Näkymä",
"zoomToFit": "Näytä kaikki elementit",
"zoomToSelection": "Näytä valinta",
"toggleElementLock": "Lukitse / poista lukitus valinta",
"movePageUpDown": "Siirrä sivua ylös/alas",
"movePageLeftRight": "Siirrä sivua vasemmalle/oikealle"
"toggleElementLock": "",
"movePageUpDown": "",
"movePageLeftRight": ""
},
"clearCanvasDialog": {
"title": "Pyyhi piirtoalue"
@@ -400,7 +397,7 @@
"fileSavedToFilename": "Tallennettiin kohteeseen {filename}",
"canvas": "piirtoalue",
"selection": "valinta",
"pasteAsSingleElement": "Käytä {{shortcut}} liittääksesi yhtenä elementtinä,\ntai liittääksesi olemassa olevaan tekstieditoriin"
"pasteAsSingleElement": ""
},
"colors": {
"ffffff": "Valkoinen",
@@ -451,15 +448,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "Kaikki tietosi on tallennettu paikallisesti selaimellesi.",
"center_heading_plus": "Haluatko sen sijaan mennä Excalidraw+:aan?",
"menuHint": "Vie, asetukset, kielet, ..."
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "Vie, asetukset ja lisää...",
"center_heading": "Kaaviot. Tehty. Yksinkertaiseksi.",
"toolbarHint": "Valitse työkalu ja aloita piirtäminen!",
"helpHint": "Pikanäppäimet & ohje"
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}
+5 -8
View File
@@ -45,7 +45,7 @@
"exportEmbedScene": "Intégrer la scène",
"exportEmbedScene_details": "Les données de scène seront enregistrées dans le fichier PNG/SVG exporté, afin que la scène puisse être restaurée à partir de celui-ci.\nCela augmentera la taille du fichier exporté.",
"addWatermark": "Ajouter \"Réalisé avec Excalidraw\"",
"handDrawn": "À main levée",
"handDrawn": "Manuscrit",
"normal": "Normale",
"code": "Code",
"small": "Petite",
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Impossible d'importer la scène depuis l'URL fournie. Elle est soit incorrecte, soit ne contient pas de données JSON Excalidraw valides.",
"resetLibrary": "Cela va effacer votre bibliothèque. Êtes-vous sûr·e ?",
"removeItemsFromsLibrary": "Supprimer {{count}} élément(s) de la bibliothèque ?",
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée.",
"collabOfflineWarning": "Aucune connexion internet disponible.\nVos modifications ne seront pas enregistrées !"
"invalidEncryptionKey": "La clé de chiffrement doit comporter 22 caractères. La collaboration en direct est désactivée."
},
"errors": {
"unsupportedFileType": "Type de fichier non supporté.",
@@ -220,8 +219,7 @@
"lock": "Garder l'outil sélectionné actif après le dessin",
"penMode": "Mode stylo - évite le toucher",
"link": "Ajouter/mettre à jour le lien pour une forme sélectionnée",
"eraser": "Gomme",
"hand": "Mains (outil de déplacement de la vue)"
"eraser": "Gomme"
},
"headings": {
"canvasActions": "Actions du canevas",
@@ -229,7 +227,7 @@
"shapes": "Formes"
},
"hints": {
"canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser, ou utiliser l'outil main.",
"canvasPanning": "Pour déplacer la zone de dessin, maintenez la molette de la souris enfoncée ou la barre d'espace tout en faisant glisser",
"linearElement": "Cliquez pour démarrer plusieurs points, faites glisser pour une seule ligne",
"freeDraw": "Cliquez et faites glissez, relâchez quand vous avez terminé",
"text": "Astuce : vous pouvez aussi ajouter du texte en double-cliquant n'importe où avec l'outil de sélection",
@@ -247,8 +245,7 @@
"publishLibrary": "Publier votre propre bibliothèque",
"bindTextToElement": "Appuyer sur Entrée pour ajouter du texte",
"deepBoxSelect": "Maintenir Ctrl ou Cmd pour sélectionner dans les groupes et empêcher le déplacement",
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression",
"firefox_clipboard_write": "Cette fonctionnalité devrait pouvoir être activée en définissant l'option \"dom.events.asyncClipboard.clipboard.clipboardItem\" à \"true\". Pour modifier les paramètres du navigateur dans Firefox, visitez la page \"about:config\"."
"eraserRevert": "Maintenez Alt enfoncé pour annuler les éléments marqués pour suppression"
},
"canvasError": {
"cannotShowPreview": "Impossible dafficher laperçu",
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Non se puido importar a escena dende a URL proporcionada. Ou ben está malformada ou non contén un JSON con información válida para Excalidraw.",
"resetLibrary": "Isto limpará a súa biblioteca. Está seguro?",
"removeItemsFromsLibrary": "Eliminar {{count}} elemento(s) da biblioteca?",
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada.",
"collabOfflineWarning": ""
"invalidEncryptionKey": "A clave de cifrado debe ter 22 caracteres. A colaboración en directo está desactivada."
},
"errors": {
"unsupportedFileType": "Tipo de ficheiro non soportado.",
@@ -220,8 +219,7 @@
"lock": "Manter a ferramenta seleccionada activa despois de debuxar",
"penMode": "Modo lapis - evitar o contacto",
"link": "Engadir/ Actualizar ligazón para a forma seleccionada",
"eraser": "Goma de borrar",
"hand": "Man (ferramenta de desprazamento)"
"eraser": "Goma de borrar"
},
"headings": {
"canvasActions": "Accións do lenzo",
@@ -229,7 +227,7 @@
"shapes": "Formas"
},
"hints": {
"canvasPanning": "Para mover o lenzo, manteña pulsada a roda do rato ou a barra de espazo mentres arrastra, ou utilice a ferramenta da man",
"canvasPanning": "Para mover o lenzo, manteña a roda do rato ou a barra de espazo mentres arrastra",
"linearElement": "Faga clic para iniciar varios puntos, arrastre para unha sola liña",
"freeDraw": "Fai clic e arrastra, solta cando acabes",
"text": "Consello: tamén podes engadir texto facendo dobre-clic en calquera lugar coa ferramenta de selección",
@@ -247,8 +245,7 @@
"publishLibrary": "Publica a túa propia biblioteca",
"bindTextToElement": "Prema a tecla enter para engadir texto",
"deepBoxSelect": "Manteña pulsado CtrlOrCmd para seleccionar en profundidade e evitar o arrastre",
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación",
"firefox_clipboard_write": "Esta función pódese activar establecendo a opción \"dom.events.asyncClipboard.clipboardItem\" a \"true\". Para cambiar as opcións do navegador en Firefox, visita a páxina \"about:config\"."
"eraserRevert": "Manteña pulsado Alt para reverter os elementos marcados para a súa eliminación"
},
"canvasError": {
"cannotShowPreview": "Non se pode mostrar a vista previa",
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "ייבוא המידע מן סצינה מכתובת האינטרנט נכשלה. המידע בנוי באופן משובש או שהוא אינו קובץ JSON תקין של Excalidraw.",
"resetLibrary": "פעולה זו תנקה את כל הלוח. אתה בטוח?",
"removeItemsFromsLibrary": "מחיקת {{count}} פריטים(ים) מתוך הספריה?",
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל.",
"collabOfflineWarning": ""
"invalidEncryptionKey": "מפתח ההצפנה חייב להיות בן 22 תוים. השיתוף החי מבוטל."
},
"errors": {
"unsupportedFileType": "סוג הקובץ אינו נתמך.",
@@ -220,8 +219,7 @@
"lock": "השאר את הכלי הנבחר פעיל גם לאחר סיום הציור",
"penMode": "",
"link": "הוספה/עדכון של קישור עבור הצורה הנבחרת",
"eraser": "מחק",
"hand": ""
"eraser": "מחק"
},
"headings": {
"canvasActions": "פעולות הלוח",
@@ -229,7 +227,7 @@
"shapes": "צורות"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "כדי להזיז את הקנבס לחצו על גלגל העכבר או על מקש הרווח תוך כדי גרירה",
"linearElement": "הקלק בשביל לבחור נקודות מרובות, גרור בשביל קו בודד",
"freeDraw": "לחץ וגרור, שחרר כשסיימת",
"text": "טיפ: אפשר להוסיף טקסט על ידי לחיצה כפולה בכל מקום עם כלי הבחירה",
@@ -247,8 +245,7 @@
"publishLibrary": "פירסום ספריה אישית",
"bindTextToElement": "יש להקיש Enter כדי להוסיף טקסט",
"deepBoxSelect": "",
"eraserRevert": "",
"firefox_clipboard_write": ""
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "לא הצלחנו להציג את התצוגה המקדימה",
+11 -14
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "दिये गये युआरेल से दृश्य आयात नहीं किया जा सका. यह या तो अनुचित है, या इसमें उचित Excalidraw JSON डेटा नहीं है।",
"resetLibrary": "यह पूरा संग्रह रिक्त करेगा. क्या आपको यक़ीन हैं?",
"removeItemsFromsLibrary": "{{count}} वस्तु(यें) संग्रह से हटायें?",
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं",
"collabOfflineWarning": "कोई इंटरनेट कनेक्शन उपलब्ध नहीं है।\nआपके बदलाव सहेजे नहीं जाएंगे!"
"invalidEncryptionKey": "कूटलेखन कुंजी 22 अक्षरों की होनी चाहिये, इसलिये जीवंत सहयोग अक्षम हैं"
},
"errors": {
"unsupportedFileType": "असमर्थित फाइल प्रकार",
@@ -220,8 +219,7 @@
"lock": "ड्राइंग के बाद चयनित टूल को सक्रिय रखें",
"penMode": "पेन का मोड - स्पर्श टाले",
"link": "",
"eraser": "रबड़",
"hand": "हाथ ( खिसकाने का औज़ार)"
"eraser": "रबड़"
},
"headings": {
"canvasActions": "कैनवास क्रिया",
@@ -229,7 +227,7 @@
"shapes": "आकृतियाँ"
},
"hints": {
"canvasPanning": "कैनवास को सरकाने के लिए, ड्रैग करते समय माउस व्हील को पकड़े रखे या स्पेसबार को दबाए रखे, अथवा हाथ वाले औज़ार का उपयोग करें",
"canvasPanning": "",
"linearElement": "कई बिंदुओं को शुरू करने के लिए क्लिक करें, सिंगल लाइन के लिए खींचें",
"freeDraw": "क्लिक करें और खींचें। समाप्त करने के लिए, छोड़ो",
"text": "आप चयन टूल से कहीं भी डबल-क्लिक करके टेक्स्ट जोड़ सकते हैं",
@@ -247,8 +245,7 @@
"publishLibrary": "",
"bindTextToElement": "",
"deepBoxSelect": "",
"eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए",
"firefox_clipboard_write": "\"dom.events.asyncClipboard.clipboardItem\" फ़्लैग को \"true\" पर सेट करके इस सुविधा को संभवतः सक्षम किया जा सकता है। Firefox में ब्राउज़र फ़्लैग बदलने के लिए, \"about:config\" पृष्ठ पर जाएँ।"
"eraserRevert": "मिटाने के लिए चुने हुए चीजों को ना चुनने के लिए Alt साथ में दबाए"
},
"canvasError": {
"cannotShowPreview": "पूर्वावलोकन नहीं दिखा सकते हैं",
@@ -451,15 +448,15 @@
},
"welcomeScreen": {
"app": {
"center_heading": "आपका सर्व डेटा ब्राउज़र के भीतर स्थानिक जगह पे सुरक्षित किया गया.",
"center_heading_plus": "बजाय आपको Excalidraw+ पर जाना है?",
"menuHint": "निर्यात, पसंद, भाषायें, ..."
"center_heading": "",
"center_heading_plus": "",
"menuHint": ""
},
"defaults": {
"menuHint": "निर्यात, पसंद, और भी...",
"center_heading": "चित्रांकन। बनाया गया। सरल।",
"toolbarHint": "एक औजार चुने और चित्रकारी प्रारंभ करे!",
"helpHint": "शॉर्ट्कट और सहाय्य"
"menuHint": "",
"center_heading": "",
"toolbarHint": "",
"helpHint": ""
}
}
}
+4 -7
View File
@@ -192,8 +192,7 @@
"invalidSceneUrl": "Nem sikerült importálni a jelenetet a megadott URL-ről. Rossz formátumú, vagy nem tartalmaz érvényes Excalidraw JSON-adatokat.",
"resetLibrary": "Ezzel törlöd a könyvtárát. biztos vagy ebben?",
"removeItemsFromsLibrary": "{{count}} elemet törölsz a könyvtárból?",
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva.",
"collabOfflineWarning": ""
"invalidEncryptionKey": "A titkosítási kulcsnak 22 karakterből kell állnia. Az élő együttműködés le van tiltva."
},
"errors": {
"unsupportedFileType": "Nem támogatott fájltípus.",
@@ -220,8 +219,7 @@
"lock": "Rajzolás után az aktív eszközt tartsa kijelölve",
"penMode": "",
"link": "Hivatkozás hozzáadása/frissítése a kiválasztott alakzathoz",
"eraser": "",
"hand": ""
"eraser": ""
},
"headings": {
"canvasActions": "Vászon műveletek",
@@ -229,7 +227,7 @@
"shapes": "Alakzatok"
},
"hints": {
"canvasPanning": "",
"canvasPanning": "A vászon mozgatásához tartsd lenyomva az egér görgőjét vagy a szóköz billentyűt húzás közben",
"linearElement": "Kattintással görbe, az eger húzásával pedig egyenes nyilat rajzolhatsz",
"freeDraw": "Kattints és húzd, majd engedd el, amikor végeztél",
"text": "Tipp: A kijelölés eszközzel a dupla kattintás új szöveget hoz létre",
@@ -247,8 +245,7 @@
"publishLibrary": "Tedd közzé saját könyvtáradat",
"bindTextToElement": "Nyomd meg az Entert szöveg hozzáadáshoz",
"deepBoxSelect": "Tartsd lenyomva a Ctrl/Cmd billentyűt a mély kijelöléshez és a húzás megakadályozásához",
"eraserRevert": "",
"firefox_clipboard_write": ""
"eraserRevert": ""
},
"canvasError": {
"cannotShowPreview": "Előnézet nem jeleníthető meg",

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