Compare commits

..
Author SHA1 Message Date
zsviczian 203de10249 throttle take 3 2023-02-26 21:57:54 +01:00
zsviczian 6ecd843df2 throttle take 2 2023-02-26 21:47:35 +01:00
zsviczian 84e01cde87 added delay 2023-02-26 21:31:08 +01:00
zsviczian 48deb68326 throttle attempt 2023-02-26 21:25:46 +01:00
+16 1d5e865da1 update to latest master (#6286)
* fix: fonts not rendered on init if `loadingdone` not fired (#5923)

* fix: fonts not rendered on init if `loadingdone` not fired

* remove unnecessary check

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

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

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

* refactor: remove unnecessary code (#5933)

* fix: scale font correctly when using shift (#5935)

* fix: scale font correctly when using shift

* fix

* Empty-Commit

* Add spec

* fix

* 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

* fix: bindings do not survive history serialization (#5942)

* 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

* 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

* fix: repair element bindings on restore (#5956)

* fix: repair element bindings on restore

* fix dropping non-text bound elements

* be more conservative

* build: move release scripts to use release branch (#5958)

* fix: renderFooter styling (#5962)

* fix: `ExcalidrawArrowElement` rather than `ExcalidrawArrowEleement` (#5955)

* fix: Galego and Kurdî missing in languages plus two locale typos (#5954)

* fix: remove blank space (#5950)

* fix: remove editor onpaste handler (#5971)

* feat: better default radius sizes for rectangles (#5553)

Co-authored-by: Ryan <diweihao@bytedance.com>
Co-authored-by: dwelle <luzar.david@gmail.com>

* chore: add display name to context providers (#5974)

* chore: add display name to context providers

* fix typo

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

* feat: allow readonly actions to be used in viewMode (#5982)

* fix: chart pasting not working due to removing tab characters (#5987)

* fix: Avatar outline on safari & center (#5997)

* fix: not properly restoring element stroke and bg colors (#6002)

* 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

* fix: resize sometimes throwing on missing null-checks (#6013)

* fix: showing `grabbing` cursor when holding `spacebar` (#6015)

* 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

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

* feat: move contextMenu into the component tree and control via appState (#6021)

* fix: ColorPicker getColor (#5949)

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

* chore: bump typescript @ 4.9.4 (#6024)

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

* fix: restoring deleted bindings (#6029)

* fix: restoring deleted bindings

* add tests

* add one more test

* merge restore tests files

* 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

* fix: remove background from wysiwyg when editing arrow label (#6033)

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

* 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

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

* chore: Update translations from Crowdin (#5807)

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

* fix: remove ga from docker build (#6059)

* fix: remove ga from docker build

* lint

* fix debug

* fix: show error message on collab save failure (#6063)

* fix: show error message on collab save failure

* comment

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

* fix: HelpDialog (#6072)

* 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

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

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

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

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

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

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

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

* fix: stale appState of MainMenu defaultItems rendered from Actions (#6074)

* 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

* 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

* fix: React.memo resolvers not accounting for all props (#6042)

* fix: use position absolute for mobile misc tools (#6099)

* feat: generic button export (#6092)

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

* feat: render unknown supplied children to UI (#6096)

* feat: support WelcomeScreen customization API (#6048)

* 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

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

* fix: mobile tools positioning (#6107)

* fix: mobile tools positioning

* add var for padding

* use css var

* new line

* stupid mistake

* lint

* fix: remove overflow hidden from button (#6110)

remove overflow hidden from button

* docs: release @excalidraw/excalidraw@0.14.0  🎉 (#6109)

* docs: release @excalidraw/excalidraw@0.14.1 🎉 (#6112)

* build: temporarily disable pre-commit (#6132)

* chore: Update translations from Crowdin (#6077)

* 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

* style: change in ExportButton style (#6147) (#6148)

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

* fix: button background and svg sizes (#6155)

* fix: button background color fallback

* fix svg width/height

* 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

* feat: close MainMenu and Library dropdown on item select (#6152)

* 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

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

* fix: set the width correctly using measureText in editor (#6162)

* fix: quick typo fix (#6167)

* fix: add 1px width to the container to calculate more accurately (#6174)

* fix: add 1px width to the container to calculate accurately

* fix tests

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

* 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

* fix: horizontal padding when aligning bound text containers (#6180)

* fix: horizontal padding when aligning bound text containers

* Add specs

* fix

* docs: release @excalidraw/excalidraw@0.14.2  🎉 (#6181)

* docs: migrating dev docs to docusaurus :) (#6073)

* docs: migrating existing docs to docosaraus :)

* log broken links

* lint :p

* fix

* divide the doc into diff categories

* fix

* order sidebars and more

* fix lint

* point to installation

* making docs better :)

* fix

* renaming git

* renaming git

* fix links

* fix

* update readme

* fix

* resolve duplicate url and make /docs as base url

* fix

* move main docs as well

* making docs better

* support mdx

* update og

* fix title

* upgrade docusarus to stable version

* use draculla theme

* fix

* make entire sidebar collapsable

* live editor for footer wohoo

* render excalidraw only on client to fix the prod build

* migrate MainMenu to live editor too :)

* lint :p

* cleanup integration and use live editor and tabs

* fix

* Add welcome screen doc

* Live Collaboration comp docs

* Add collaborator example

* Add example

* add more

* remove isCollaborating

* Rewrite ref and move to sidebar

* change color of links inside pre

* add initial data

* fix lint

* Add styling

* fix lint

* Add example for customizing styles

* fix lint

* fix

* fix lint

* Add link to livecollabtrigger

* fix

* rewrite UIOptions to sidebar

* move initialdata to sidebar

* move render props to sidebar and rewrite renderTopRightUI and renderCustomStats

* rewrite renderSidebar

* update og

* update url for testing

* fix url

* update readme

* fix style

* tweaks

* Add highlight comp to highlight text

* Add bash syntax highlight

* fix

* tweaks

* fix

* rewrite export utilities

* fix restore

* rewrite utils

* move constants to sidebar

* update readme

* add copyright

* fix links style

* Add linkedin

* tweaks

* rename package to @excalidraw/excalidraw

* enable algolia with dummy creds

* tweaks to integration doc

* tweak WelcomeScreen docs to reflect upcoming API changes

* tweak components intro

* tweak nomenclature

* fix admonition

* rename `components` sidebar item and change order of components list

* uncollapse package section in sidebar

* show level 4 haeadings in TOC

* remove algolia

* remove unused assets

* capitalize C

* tweak

* rename components to App

* rename components -> children-components in the routes

* move notable used tools to intro

* update MainMenu docs with `onSelect` preventDefault behavior

* change sidebar label for children components

* use code

* tweak README & docs intro

* tweak package development doc

* make scrollbar gutter stable

* tweak api intro

* add admonition for export utils

* use next

* wip

* wip

* make excalidraw examples use current color theme & prefer system

* fix welcomescreen docs

* use latest temp release

* fix component order

* revert wip changes

* use next

* tweak

* increase height to fix welcome screen hint

* tweak editor height

* update excal version

* wrap Excal with forwardRef to fix refs

* migrate contributing.md

* fix broken links

---------

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

* fix: edit link in docs (#6182)

* docs: show last updated time and author (#6183)

docs:show last updated time and author

* fix: hide welcome screen on mobile once user interacts (#6185)

* fix: hide welcome screen on mobile once started drawing

* Add specs

* fix: sort bound text elements to fix text duplication z-index error (#5130)

* fix: sort bound text elements to fix text duplication z-index error

* improve & sort groups & add tests

* fix backtracking and discontiguous groups

---------

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

* feat: disable canvas smoothing (antialiasing) for right-angled elements (#6186)Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* feat: disable canvas smoothing for text and other types

* disable smoothing for all right-angled elements

* Update src/renderer/renderElement.ts

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* Update src/renderer/renderElement.ts

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* fix lint

* always enable smoothing while zooming

---------

Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>

* chore: Update translations from Crowdin (#6150)

* feat: shortcut for clearCanvas confirmDialog (#6114)

Co-authored-by: dwelle <luzar.david@gmail.com>
resolve https://github.com/excalidraw/excalidraw/issues/5818

* feat: show error message when not connected to internet while collabo… (#6165)

Co-authored-by: dwelle <luzar.david@gmail.com>
Resolves https://github.com/excalidraw/excalidraw/issues/5994

* fix: docker build architecture:linux/amd64 error occur on linux/arm64 instance (#6197)

fix docker build
when in linux/arm64 use docker buildx plugin to build linux/amd64 image, a build error will occur causing the build to break

* refactor: Make the example React app reusable without duplication (#6188)

* fix: don't allow blank space in collab name (#6211)

* don't allow blank space in collab name

* add spec

* prevent blank

* docs: enable Algolia for search (#6230)

* feat: Make repair and refreshDimensions configurable in restoreElements (#6238)

* fix: don't repair during reconcilation

* Add opts to restoreElement and enable refreshDimensions and repair via config

* remove

* update changelog

* fix tests

* rename to repairBindings

* docs: Fixed broken codesandbox link in the dev-docs (#6229)

fixed broken link

* docs: new readme (#6240)

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

* docs: fix next.js example (#6241)

* docs: fix typo (#6252)

* feat: Bind text to container if double clicked on filled shape or stroke (#6250)

* feat: bind text to container when clicked on filled shape or element stroke

* Bind if double clicked on stroke as well

* remove

* specs

* remove

* shuffle

* fix

* back to normal

* docs: Fix outdated link in README.md (#6263)

* fix: improve text wrapping in ellipse and alignment (#6172)

* fix: improve text wrapping in ellipse

* compute height when font properties updated

* fix alignment

* fix alignment when resizing

* fix

* ad padding

* always compute height when redrawing bounding box and refactor

* lint

* fix specs

* fix

* redraw text bounding box when pasted or refreshed

* fix

* Add specs

* fix

* restore on font load

* add comments

* fix: improve text wrapping inside rhombus and more fixes (#6265)

* fix: improve text wrapping inside rhombus

* Add comments

* specs

* fix: shift resize and multiple element regression for ellipse and rhombus

* use container width for scaling font size

* fix

* fix multiple resize

* lint

* redraw on submit

* redraw only newly pasted elements

* no padding when center

* fix tests

* fix

* dont add padding in rhombus when aligning

* refactor

* fix

* move getMaxContainerHeight and getMaxContainerWidth to textElement.ts

* Add specs

* fix: indenting via `tab` clashing with IME compositor (#6258)

* chore: Update translations from Crowdin (#6191)

* fix: rerender i18n in host components on lang change (#6224)

* fix: fit mobile toolbar and make scrollable (#6270)

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

* feat: improve text measurements in bound containers (#6187)

* feat: move to canvas measureText

* calcualte height with better heuristic

* improve heuristic more

* remove vertical offset as its not needed

* lint

* calculate width of individual char and ceil to calculate width and remove adjustment factor

* push the word if equal to max width

* update height when text overflows for vertical alignment top/bottom

* remove the hack of updating height when line mismatch as its not needed

* remove scroll height and calculate the height instead

* remove unused code

* fix

* remove

* use math.ceil for whole width instead of individual chars

* fix tests

* fix

* fix

* redraw text bounding box instead when font loaded to fix alignment as well

* fix

* fix

* fix

* Add a 0.05px extra only for firefox

* Add spec

* stop taking ceil and increase firefox editor width by 0.05px

* Ad 0.05px in safari too

* lint

* lint

* remove baseline from measureFontSizeFromWH

* don't redraw on font load

* lint

* refactor name and signature

* fix: compute container height from bound text correctly (#6273)

* fix: compute container height from bound text correctly

* fix specs

* Add tests

* fix: svg text baseline (#6285

* fix: svg text baseline

* fix for multiline

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: David Luzar <luzar.david@gmail.com>
Co-authored-by: Aakansha Doshi <aakansha1216@gmail.com>
Co-authored-by: Antonio Della Fortuna <50418432+adarkforce@users.noreply.github.com>
Co-authored-by: Antonio Della Fortuna <a.dellafortuna00@gmail.com>
Co-authored-by: DanielJGeiger <1852529+DanielJGeiger@users.noreply.github.com>
Co-authored-by: Fer <63980689+1fbr@users.noreply.github.com>
Co-authored-by: fennghuang <89014758+fennghuang@users.noreply.github.com>
Co-authored-by: Ryan Di <ryan.weihao.di@gmail.com>
Co-authored-by: Ryan <diweihao@bytedance.com>
Co-authored-by: Excalidraw Bot <77840495+excalibot@users.noreply.github.com>
Co-authored-by: EternalWill43 <70084418+EternalWill43@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Barnabás Molnár <38168628+barnabasmolnar@users.noreply.github.com>
Co-authored-by: Nishant-l <61119157+Nishant-l@users.noreply.github.com>
Co-authored-by: Ignacio Cuadra <67276174+ignacio-cuadra@users.noreply.github.com>
Co-authored-by: JUNYI OU <49964599+Irvingouj@users.noreply.github.com>
Co-authored-by: Jang Min <jangmin.dev@gmail.com>
Co-authored-by: Matthieu Rossignon <51274353+Mattross45@users.noreply.github.com>
Co-authored-by: Dejavu Moe <jialong.vip@gmail.com>
Co-authored-by: Luka Hietala <95122845+LukaHietala@users.noreply.github.com>
Co-authored-by: Milos Vetesnik <maielo.mv@gmail.com>
Co-authored-by: Jan Klass <kissaki@posteo.de>
Co-authored-by: Hikaru Yoshino <57059705+osushicrusher@users.noreply.github.com>
Co-authored-by: Tengku Farhan <109069184+tfarhan00@users.noreply.github.com>
2023-02-26 20:48:47 +01:00
zsviczianandGitHub 82e56919ca Update renderElement.ts 2022-11-20 21:06:27 +01:00
zsviczianandGitHub 2eb259ae48 Update renderElement.ts 2022-11-20 21:00:23 +01:00
zsviczianandGitHub 94a9e82228 trying to capture the element.id of the element where it breaks.... 2022-11-20 20:56:26 +01:00
zsviczianandGitHub e82918f1c8 Update renderElement.ts 2022-11-20 20:46:50 +01:00
zsviczianandGitHub 12c1d6cd09 remove contex.save and restore 2022-11-20 20:37:51 +01:00
zsviczianandGitHub 3fb1a870e6 generate2DShape but don't draw 2022-11-20 20:31:15 +01:00
zsviczianandGitHub 2be34b770a don't fill 2022-11-20 20:26:14 +01:00
zsviczianandGitHub e1f9b89f0a don't fill freedraw 2022-11-20 20:22:18 +01:00
zsviczianandGitHub c6f175762c skip drawElementOnCanvas for freedraw 2022-11-20 19:44:26 +01:00
zsviczianandGitHub 8028eb1d60 do not generate freedraw items 2022-11-20 19:36:32 +01:00
zsviczianandGitHub 97273f4e4e do not draw freedraw on canvas 2022-11-20 19:30:29 +01:00
76 changed files with 603 additions and 2126 deletions
-10
View File
@@ -22,13 +22,3 @@ REACT_APP_DEV_ENABLE_SW=
REACT_APP_DEV_DISABLE_LIVE_RELOAD=
FAST_REFRESH=false
# MATOMO
REACT_APP_MATOMO_URL=
REACT_APP_CDN_MATOMO_TRACKER_URL=
REACT_APP_MATOMO_SITE_ID=
#Debug flags
# To enable bounding box for text containers
REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX=
-7
View File
@@ -12,13 +12,6 @@ REACT_APP_WS_SERVER_URL=
REACT_APP_FIREBASE_CONFIG='{"apiKey":"AIzaSyAd15pYlMci_xIp9ko6wkEsDzAAA0Dn0RU","authDomain":"excalidraw-room-persistence.firebaseapp.com","databaseURL":"https://excalidraw-room-persistence.firebaseio.com","projectId":"excalidraw-room-persistence","storageBucket":"excalidraw-room-persistence.appspot.com","messagingSenderId":"654800341332","appId":"1:654800341332:web:4a692de832b55bd57ce0c1"}'
# production-only vars
# GOOGLE ANALYTICS
REACT_APP_GOOGLE_ANALYTICS_ID=UA-387204-13
# MATOMO
REACT_APP_MATOMO_URL=https://excalidraw.matomo.cloud/
REACT_APP_CDN_MATOMO_TRACKER_URL=//cdn.matomo.cloud/excalidraw.matomo.cloud/matomo.js
REACT_APP_MATOMO_SITE_ID=1
REACT_APP_PLUS_APP=https://app.excalidraw.com
-1
View File
@@ -25,4 +25,3 @@ src/packages/excalidraw/types
src/packages/excalidraw/example/public/bundle.js
src/packages/excalidraw/example/public/excalidraw-assets-dev
src/packages/excalidraw/example/public/excalidraw.development.js
coverage
@@ -1,19 +1,6 @@
# 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> } }
<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:
@@ -152,9 +139,7 @@ function App() {
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the scene</p>
<button className="custom-button" onClick={updateScene}>
Update Scene
</button>
<button className="custom-button" onClick={updateScene}>Update Scene</button>
<Excalidraw ref={(api) => setExcalidrawAPI(api)} />
</div>
);
@@ -202,8 +187,7 @@ function App() {
return (
<div style={{ height: "500px" }}>
<p style={{ fontSize: "16px" }}> Click to update the library items</p>
<button
className="custom-button"
<button className="custom-button"
onClick={() => {
const libraryItems = [
{
@@ -221,8 +205,10 @@ function App() {
];
excalidrawAPI.updateLibrary({
libraryItems,
openLibraryMenu: true,
openLibraryMenu: true
});
}}
>
Update Library
@@ -264,7 +250,7 @@ Resets the scene. If `resetLoadingState` is passed as true then it will also for
<pre>
() =>{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement[]
</a>
</pre>
@@ -275,7 +261,7 @@ Returns all the elements including the deleted in the scene.
<pre>
() => NonDeleted&#60;
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>
[]&#62;
@@ -307,31 +293,18 @@ This is the history API. history.clear() will clear the history.
## scrollToContent
<pre>
(<br />
{" "}
target?:{" "}
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L115">
(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#L115">
<a href="https://github.com/excalidraw/excalidraw/blob/master/src/element/types.ts#L114">
ExcalidrawElement
</a>
[],
<br />
{" "}opts?: &#123; fitToContent?: boolean; animate?: boolean; duration?: number
&#125;
<br />) => void
[]) => void
</pre>
Scroll the nearest element out of the elements supplied to the center of the viewport. Defaults to the elements on the scene.
| Attribute | type | default | Description |
| --- | --- | --- | --- |
| target | <code>ExcalidrawElement &#124; ExcalidrawElement[]</code> | All scene elements | The element(s) to scroll to. |
| opts.fitToContent | boolean | false | Whether to fit the elements to viewport by automatically changing zoom as needed. |
| opts.animate | boolean | false | Whether to animate between starting and ending position. Note that for larger scenes the animation may not be smooth due to performance issues. |
| opts.duration | number | 500 | Duration of the animation if `opts.animate` is `true`. |
Scroll the nearest element out of the elements supplied to the center. Defaults to the elements on the scene.
## refresh
@@ -350,7 +323,7 @@ For any other cases if the position of excalidraw is updated (example due to scr
This API can be used to show the toast with custom message.
```tsx
({ message: string, closable?:boolean,duration?:number
({ message: string, closable?:boolean,duration?:number
} | null) => void
```
@@ -385,18 +358,15 @@ This API can be used to get the files present in the scene. It may contain files
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
(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.
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
@@ -4,34 +4,6 @@
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.
### Turning off Aggressive Anti-Fingerprinting in Brave browser
When *Aggressive Anti-Fingerprinting* is turned on, the `measureText` API breaks which in turn breaks the Text Elements in your drawings. Here is more [info](https://github.com/excalidraw/excalidraw/pull/6336) on the same.
We strongly recommend turning it off. You can follow the steps below on how to do so.
1. Open [excalidraw.com](https://excalidraw.com) in Brave and click on the **Shield** button
![Shield button](../../assets/brave-shield.png)
<div style={{width:'30rem'}}>
2. Once opened, look for **Aggressively Block Fingerprinting**
![Aggresive block fingerprinting](../../assets/aggressive-block-fingerprint.png)
3. Switch to **Block Fingerprinting**
![Block filtering](../../assets/block-fingerprint.png)
4. Thats all. All text elements should be fixed now 🎉
</div>
If disabling this setting doesn't fix the display of text elements, please consider opening an [issue](https://github.com/excalidraw/excalidraw/issues/new) on our GitHub, or message us on [Discord](https://discord.gg/UexuTaE).
## 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).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

+9 -9
View File
@@ -1785,9 +1785,9 @@
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
@@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1:
entities "^4.3.0"
http-cache-semantics@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
version "4.1.0"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http-deceiver@^1.2.7:
version "1.2.7"
@@ -7542,9 +7542,9 @@ webpack-sources@^3.2.2, webpack-sources@^3.2.3:
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.73.0:
version "5.76.1"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.1.tgz#7773de017e988bccb0f13c7d75ec245f377d295c"
integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==
version "5.74.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980"
integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
+7 -7
View File
@@ -25,6 +25,11 @@
"@testing-library/jest-dom": "5.16.2",
"@testing-library/react": "12.1.5",
"@tldraw/vec": "1.7.1",
"@types/jest": "27.4.0",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/socket.io-client": "1.4.36",
"browser-fs-access": "0.29.1",
"clsx": "1.1.1",
"cross-env": "7.0.3",
@@ -52,6 +57,7 @@
"sass": "1.51.0",
"socket.io-client": "2.3.1",
"tunnel-rat": "0.1.0",
"typescript": "4.9.4",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
@@ -69,14 +75,9 @@
"@excalidraw/eslint-config": "1.0.0",
"@excalidraw/prettier-config": "1.0.2",
"@types/chai": "4.3.0",
"@types/jest": "27.4.0",
"@types/lodash.throttle": "4.1.7",
"@types/pako": "1.0.3",
"@types/pica": "5.1.3",
"@types/react": "18.0.15",
"@types/react-dom": "18.0.6",
"@types/resize-observer-browser": "0.1.7",
"@types/socket.io-client": "1.4.36",
"chai": "4.3.6",
"dotenv": "16.0.1",
"eslint-config-prettier": "8.5.0",
@@ -87,8 +88,7 @@
"lint-staged": "12.3.7",
"pepjs": "0.5.3",
"prettier": "2.6.2",
"rewire": "6.0.0",
"typescript": "4.9.4"
"rewire": "6.0.0"
},
"engines": {
"node": ">=14.0.0"
+2 -31
View File
@@ -146,10 +146,8 @@
// setting this so that libraries installation reuses this window tab.
window.name = "_excalidraw";
</script>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true') { %>
<!-- LEGACY GOOGLE ANALYTICS -->
<% if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<% if (process.env.REACT_APP_DISABLE_TRACKING !== 'true' &&
process.env.REACT_APP_GOOGLE_ANALYTICS_ID) { %>
<script
async
src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_GOOGLE_ANALYTICS_ID%"
@@ -162,33 +160,6 @@
gtag("js", new Date());
gtag("config", "%REACT_APP_GOOGLE_ANALYTICS_ID%");
</script>
<% } %>
<!-- end LEGACY GOOGLE ANALYTICS -->
<!-- Matomo -->
<% if (process.env.REACT_APP_MATOMO_URL &&
process.env.REACT_APP_MATOMO_SITE_ID &&
process.env.REACT_APP_CDN_MATOMO_TRACKER_URL) { %>
<script>
var _paq = (window._paq = window._paq || []);
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
var u = "%REACT_APP_MATOMO_URL%";
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", "%REACT_APP_MATOMO_SITE_ID%"]);
var d = document,
g = d.createElement("script"),
s = d.getElementsByTagName("script")[0];
g.async = true;
g.src = "%REACT_APP_CDN_MATOMO_TRACKER_URL%";
s.parentNode.insertBefore(g, s);
})();
</script>
<% } %>
<!-- end Matomo analytics -->
<% } %>
<!-- FIXME: remove this when we update CRA (fix SW caching) -->
-6
View File
@@ -2,9 +2,6 @@ const fs = require("fs");
const THRESSHOLD = 85;
// we're using BCP 47 language tags as keys
// e.g. https://gist.github.com/typpo/b2b828a35e683b9bf8db91b5404f1bd1
const crowdinMap = {
"ar-SA": "en-ar",
"bg-BG": "en-bg",
@@ -55,7 +52,6 @@ const crowdinMap = {
"kk-KZ": "en-kk",
"vi-VN": "en-vi",
"mr-IN": "en-mr",
"th-TH": "en-th",
};
const flags = {
@@ -108,7 +104,6 @@ const flags = {
"eu-ES": "🇪🇦",
"vi-VN": "🇻🇳",
"mr-IN": "🇮🇳",
"th-TH": "🇹🇭",
};
const languages = {
@@ -161,7 +156,6 @@ const languages = {
"zh-TW": "繁體中文",
"vi-VN": "Tiếng Việt",
"mr-IN": "मराठी",
"th-TH": "ภาษาไทย",
};
const percentages = fs.readFileSync(
+13 -159
View File
@@ -1,8 +1,7 @@
import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants";
import { getNonDeletedElements, isTextElement, newElement } from "../element";
import { VERTICAL_ALIGN } from "../constants";
import { getNonDeletedElements, isTextElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import {
computeContainerDimensionForBoundText,
getBoundTextElement,
measureText,
redrawTextBoundingBox,
@@ -14,11 +13,8 @@ import {
import {
hasBoundTextElement,
isTextBindableContainer,
isUsingAdaptiveRadius,
} from "../element/typeChecks";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
@@ -42,10 +38,9 @@ export const actionUnbindText = register({
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
const { width, height, baseline } = measureText(
const { width, height } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
boundTextElement.lineHeight,
);
const originalContainerHeight = getOriginalContainerHeightFromCache(
element.id,
@@ -56,7 +51,6 @@ export const actionUnbindText = register({
containerId: null,
width,
height,
baseline,
text: boundTextElement.originalText,
});
mutateElement(element, {
@@ -135,159 +129,19 @@ export const actionBindText = register({
}),
});
redrawTextBoundingBox(textElement, container);
const updatedElements = elements.slice();
const textElementIndex = updatedElements.findIndex(
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 1);
const containerIndex = updatedElements.findIndex(
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex + 1, 0, textElement);
return {
elements: pushTextAboveContainer(elements, container, textElement),
elements: updatedElements,
appState: { ...appState, selectedElementIds: { [container.id]: true } },
commitToHistory: true,
};
},
});
const pushTextAboveContainer = (
elements: readonly ExcalidrawElement[],
container: ExcalidrawElement,
textElement: ExcalidrawTextElement,
) => {
const updatedElements = elements.slice();
const textElementIndex = updatedElements.findIndex(
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 1);
const containerIndex = updatedElements.findIndex(
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex + 1, 0, textElement);
return updatedElements;
};
const pushContainerBelowText = (
elements: readonly ExcalidrawElement[],
container: ExcalidrawElement,
textElement: ExcalidrawTextElement,
) => {
const updatedElements = elements.slice();
const containerIndex = updatedElements.findIndex(
(ele) => ele.id === container.id,
);
updatedElements.splice(containerIndex, 1);
const textElementIndex = updatedElements.findIndex(
(ele) => ele.id === textElement.id,
);
updatedElements.splice(textElementIndex, 0, container);
return updatedElements;
};
export const actionCreateContainerFromText = register({
name: "createContainerFromText",
contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" },
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1 && isTextElement(selectedElements[0]);
},
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = elements.slice();
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
const textElement = selectedElements[0];
const container = newElement({
type: "rectangle",
backgroundColor: appState.currentItemBackgroundColor,
boundElements: [
...(textElement.boundElements || []),
{ id: textElement.id, type: "text" },
],
angle: textElement.angle,
fillStyle: appState.currentItemFillStyle,
strokeColor: appState.currentItemStrokeColor,
roughness: appState.currentItemRoughness,
strokeWidth: appState.currentItemStrokeWidth,
strokeStyle: appState.currentItemStrokeStyle,
roundness:
appState.currentItemRoundness === "round"
? {
type: isUsingAdaptiveRadius("rectangle")
? ROUNDNESS.ADAPTIVE_RADIUS
: ROUNDNESS.PROPORTIONAL_RADIUS,
}
: null,
opacity: 100,
locked: false,
x: textElement.x - BOUND_TEXT_PADDING,
y: textElement.y - BOUND_TEXT_PADDING,
width: computeContainerDimensionForBoundText(
textElement.width,
"rectangle",
),
height: computeContainerDimensionForBoundText(
textElement.height,
"rectangle",
),
groupIds: textElement.groupIds,
});
// update bindings
if (textElement.boundElements?.length) {
const linearElementIds = textElement.boundElements
.filter((ele) => ele.type === "arrow")
.map((el) => el.id);
const linearElements = updatedElements.filter((ele) =>
linearElementIds.includes(ele.id),
) as ExcalidrawLinearElement[];
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id,
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding });
}
});
}
mutateElement(textElement, {
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
});
redrawTextBoundingBox(textElement, container);
return {
elements: pushContainerBelowText(
[...elements, container],
container,
textElement,
),
appState: {
...appState,
selectedElementIds: {
[container.id]: true,
[textElement.id]: false,
},
},
commitToHistory: true,
};
}
return {
elements: updatedElements,
appState,
commitToHistory: true,
};
},
});
+1 -1
View File
@@ -226,7 +226,7 @@ const zoomValueToFitBoundsOnViewport = (
return clampedZoomValueToFitElements as NormalizedZoomValue;
};
export const zoomToFitElements = (
const zoomToFitElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
zoomToSelection: boolean,
-5
View File
@@ -54,7 +54,6 @@ import { mutateElement, newElementWith } from "../element/mutateElement";
import {
getBoundTextElement,
getContainerElement,
getDefaultLineHeight,
} from "../element/textElement";
import {
isBoundToContainer,
@@ -638,7 +637,6 @@ export const actionChangeFontFamily = register({
oldElement,
{
fontFamily: value,
lineHeight: getDefaultLineHeight(value),
},
);
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
@@ -747,19 +745,16 @@ export const actionChangeTextAlign = register({
value: "left",
text: t("labels.left"),
icon: TextAlignLeftIcon,
testId: "align-left",
},
{
value: "center",
text: t("labels.center"),
icon: TextAlignCenterIcon,
testId: "align-horizontal-center",
},
{
value: "right",
text: t("labels.right"),
icon: TextAlignRightIcon,
testId: "align-right",
},
]}
value={getFormValue(
+4 -13
View File
@@ -12,10 +12,7 @@ import {
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
} from "../constants";
import {
getBoundTextElement,
getDefaultLineHeight,
} from "../element/textElement";
import { getBoundTextElement } from "../element/textElement";
import {
hasBoundTextElement,
canApplyRoundnessTypeToElement,
@@ -95,18 +92,12 @@ export const actionPasteStyles = register({
});
if (isTextElement(newElement)) {
const fontSize =
elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE;
const fontFamily =
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY;
newElement = newElementWith(newElement, {
fontSize,
fontFamily,
fontSize: elementStylesToCopyFrom?.fontSize || DEFAULT_FONT_SIZE,
fontFamily:
elementStylesToCopyFrom?.fontFamily || DEFAULT_FONT_FAMILY,
textAlign:
elementStylesToCopyFrom?.textAlign || DEFAULT_TEXT_ALIGN,
lineHeight:
elementStylesToCopyFrom.lineHeight ||
getDefaultLineHeight(fontFamily),
});
let container = null;
if (newElement.containerId) {
-1
View File
@@ -1,6 +1,5 @@
import { isDarwin } from "../constants";
import { t } from "../i18n";
import { SubtypeOf } from "../utility-types";
import { getShortcutKey } from "../utils";
import { ActionName } from "./types";
+1 -3
View File
@@ -6,7 +6,6 @@ import {
ExcalidrawProps,
BinaryFiles,
} from "../types";
import { MarkOptional } from "../utility-types";
export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api";
@@ -114,8 +113,7 @@ export type ActionName =
| "toggleLock"
| "toggleLinearEditor"
| "toggleEraserTool"
| "toggleHandTool"
| "createContainerFromText";
| "toggleHandTool";
export type PanelComponentProps = {
elements: readonly ExcalidrawElement[];
+22 -30
View File
@@ -1,30 +1,22 @@
export const trackEvent = (
category: string,
action: string,
label?: string,
value?: number,
) => {
try {
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
return;
}
if (process.env.REACT_APP_GOOGLE_ANALYTICS_ID && window.gtag) {
window.gtag("event", action, {
event_category: category,
event_label: label,
value,
});
}
// MATOMO event tracking _paq must be same as the one in index.html
if (window._paq) {
window._paq.push(["trackEvent", category, action, label, value]);
}
} catch (error) {
console.error("error during analytics", error);
}
};
export const trackEvent =
typeof process !== "undefined" &&
process.env?.REACT_APP_GOOGLE_ANALYTICS_ID &&
typeof window !== "undefined" &&
window.gtag
? (category: string, action: string, label?: string, value?: number) => {
try {
window.gtag("event", action, {
event_category: category,
event_label: label,
value,
});
} catch (error) {
console.error("error logging to ga", error);
}
}
: typeof process !== "undefined" && process.env?.JEST_WORKER_ID
? (category: string, action: string, label?: string, value?: number) => {}
: (category: string, action: string, label?: string, value?: number) => {
// Uncomment the next line to track locally
// console.log("Track Event", { category, action, label, value });
};
+2 -6
View File
@@ -30,10 +30,7 @@ import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import "./Actions.scss";
import { Tooltip } from "./Tooltip";
import {
shouldAllowVerticalAlign,
suppportsHorizontalAlign,
} from "../element/textElement";
import { shouldAllowVerticalAlign } from "../element/textElement";
export const SelectedShapeActions = ({
appState,
@@ -125,8 +122,7 @@ export const SelectedShapeActions = ({
{renderAction("changeFontFamily")}
{suppportsHorizontalAlign(targetElements) &&
renderAction("changeTextAlign")}
{renderAction("changeTextAlign")}
</>
)}
-2
View File
@@ -1,7 +1,6 @@
import { atom, useAtom } from "jotai";
import { actionClearCanvas } from "../actions";
import { t } from "../i18n";
import { jotaiScope } from "../jotai";
import { useExcalidrawActionManager } from "./App";
import ConfirmDialog from "./ConfirmDialog";
@@ -10,7 +9,6 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null);
export const ActiveConfirmDialog = () => {
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
activeConfirmDialogAtom,
jotaiScope,
);
const actionManager = useExcalidrawActionManager();
-45
View File
@@ -1,45 +0,0 @@
import ReactDOM from "react-dom";
import * as Renderer from "../renderer/renderScene";
import { reseed } from "../random";
import { render, queryByTestId } from "../tests/test-utils";
import ExcalidrawApp from "../excalidraw-app";
const renderScene = jest.spyOn(Renderer, "renderScene");
describe("Test <App/>", () => {
beforeEach(async () => {
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
localStorage.clear();
renderScene.mockClear();
reseed(7);
});
it("should show error modal when using brave and measureText API is not working", async () => {
(global.navigator as any).brave = {
isBrave: {
name: "isBrave",
},
};
const originalContext = global.HTMLCanvasElement.prototype.getContext("2d");
//@ts-ignore
global.HTMLCanvasElement.prototype.getContext = (contextId) => {
return {
...originalContext,
measureText: () => ({
width: 0,
}),
};
};
await render(<ExcalidrawApp />);
expect(
queryByTestId(
document.querySelector(".excalidraw-modal-container")!,
"brave-measure-text-error",
),
).toMatchSnapshot();
});
});
+129 -138
View File
@@ -62,7 +62,6 @@ import {
GRID_SIZE,
IMAGE_RENDER_TIMEOUT,
isAndroid,
isBrave,
LINE_CONFIRM_THRESHOLD,
MAX_ALLOWED_FILE_BYTES,
MIME_TYPES,
@@ -176,7 +175,10 @@ import {
} from "../keys";
import { distance2d, getGridPoint, isPathALoop } from "../math";
import { renderScene } from "../renderer/renderScene";
import { invalidateShapeForElement } from "../renderer/renderElement";
import {
generateElementWithCanvas,
invalidateShapeForElement,
} from "../renderer/renderElement";
import {
calculateScrollCenter,
getElementsAtPosition,
@@ -229,7 +231,6 @@ import {
updateActiveTool,
getShortcutKey,
isTransparent,
easeToValuesRAF,
} from "../utils";
import {
ContextMenu,
@@ -261,16 +262,14 @@ import throttle from "lodash.throttle";
import { fileOpen, FileSystemHandle } from "../data/filesystem";
import {
bindTextToShapeAfterDuplication,
getApproxLineHeight,
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElement,
getContainerCenter,
getContainerDims,
getContainerElement,
getDefaultLineHeight,
getLineHeightInPx,
getTextBindableContainerAtPosition,
isMeasureTextSupported,
isValidTextContainer,
} from "../element/textElement";
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
@@ -285,14 +284,9 @@ import {
import { shouldShowBoundingBox } from "../element/transformHandles";
import { Fonts } from "../scene/Fonts";
import { actionPaste } from "../actions/actionClipboard";
import {
actionToggleHandTool,
zoomToFitElements,
} from "../actions/actionCanvas";
import { actionToggleHandTool } from "../actions/actionCanvas";
import { jotaiStore } from "../jotai";
import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionCreateContainerFromText } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
const deviceContextInitialValue = {
isSmScreen: false,
@@ -437,6 +431,7 @@ class App extends React.Component<AppProps, AppState> {
};
this.id = nanoid();
this.library = new Library(this);
if (excalidrawRef) {
const readyPromise =
@@ -718,8 +713,6 @@ class App extends React.Component<AppProps, AppState> {
const theme =
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
let name = actionResult?.appState?.name ?? this.state.name;
const errorMessage =
actionResult?.appState?.errorMessage ?? this.state.errorMessage;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
@@ -735,6 +728,7 @@ class App extends React.Component<AppProps, AppState> {
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
this.setState(
(state) => {
// using Object.assign instead of spread to fool TS 4.2.2+ into
@@ -752,7 +746,6 @@ class App extends React.Component<AppProps, AppState> {
gridSize,
theme,
name,
errorMessage,
});
},
() => {
@@ -881,6 +874,7 @@ class App extends React.Component<AppProps, AppState> {
),
};
}
// FontFaceSet loadingdone event we listen on may not always fire
// (looking at you Safari), so on init we manually load fonts for current
// text elements on canvas, and rerender them once done. This also
@@ -1008,13 +1002,6 @@ class App extends React.Component<AppProps, AppState> {
} else {
this.updateDOMRect(this.initializeScene);
}
// note that this check seems to always pass in localhost
if (isBrave() && !isMeasureTextSupported()) {
this.setState({
errorMessage: <BraveMeasureTextError />,
});
}
}
public componentWillUnmount() {
@@ -1736,14 +1723,12 @@ class App extends React.Component<AppProps, AppState> {
(acc: ExcalidrawTextElement[], line, idx) => {
const text = line.trim();
const lineHeight = getDefaultLineHeight(textElementProps.fontFamily);
if (text.length) {
const element = newTextElement({
...textElementProps,
x,
y: currentY,
text,
lineHeight,
});
acc.push(element);
currentY += element.height + LINE_GAP;
@@ -1752,9 +1737,14 @@ class App extends React.Component<AppProps, AppState> {
// add paragraph only if previous line was not empty, IOW don't add
// more than one empty line
if (prevLine) {
currentY +=
getLineHeightInPx(textElementProps.fontSize, lineHeight) +
LINE_GAP;
const defaultLineHeight = getApproxLineHeight(
getFontString({
fontSize: textElementProps.fontSize,
fontFamily: textElementProps.fontFamily,
}),
);
currentY += defaultLineHeight + LINE_GAP;
}
}
@@ -1847,89 +1837,18 @@ class App extends React.Component<AppProps, AppState> {
this.actionManager.executeAction(actionToggleHandTool);
};
/**
* Zooms on canvas viewport center
*/
zoomCanvas = (
/** decimal fraction between 0.1 (10% zoom) and 30 (3000% zoom) */
value: number,
) => {
this.setState({
...getStateForZoom(
{
viewportX: this.state.width / 2 + this.state.offsetLeft,
viewportY: this.state.height / 2 + this.state.offsetTop,
nextZoom: getNormalizedZoom(value),
},
this.state,
),
});
};
private cancelInProgresAnimation: (() => void) | null = null;
scrollToContent = (
target:
| ExcalidrawElement
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
opts?: { fitToContent?: boolean; animate?: boolean; duration?: number },
) => {
this.cancelInProgresAnimation?.();
// convert provided target into ExcalidrawElement[] if necessary
const targets = Array.isArray(target) ? target : [target];
let zoom = this.state.zoom;
let scrollX = this.state.scrollX;
let scrollY = this.state.scrollY;
if (opts?.fitToContent) {
// compute an appropriate viewport location (scroll X, Y) and zoom level
// that fit the target elements on the scene
const { appState } = zoomToFitElements(targets, this.state, false);
zoom = appState.zoom;
scrollX = appState.scrollX;
scrollY = appState.scrollY;
} else {
// compute only the viewport location, without any zoom adjustment
const scroll = calculateScrollCenter(targets, this.state, this.canvas);
scrollX = scroll.scrollX;
scrollY = scroll.scrollY;
}
// when animating, we use RequestAnimationFrame to prevent the animation
// from slowing down other processes
if (opts?.animate) {
const origScrollX = this.state.scrollX;
const origScrollY = this.state.scrollY;
// zoom animation could become problematic on scenes with large number
// of elements, setting it to its final value to improve user experience.
//
// using zoomCanvas() to zoom on current viewport center
this.zoomCanvas(zoom.value);
const cancel = easeToValuesRAF(
[origScrollX, origScrollY],
[scrollX, scrollY],
(scrollX, scrollY) => this.setState({ scrollX, scrollY }),
{ duration: opts?.duration ?? 500 },
);
this.cancelInProgresAnimation = () => {
cancel();
this.cancelInProgresAnimation = null;
};
} else {
this.setState({ scrollX, scrollY, zoom });
}
};
/** use when changing scrollX/scrollY/zoom based on user interaction */
private translateCanvas: React.Component<any, AppState>["setState"] = (
state,
) => {
this.cancelInProgresAnimation?.();
this.setState(state);
this.setState({
...calculateScrollCenter(
Array.isArray(target) ? target : [target],
this.state,
this.canvas,
),
});
};
setToast = (
@@ -2130,13 +2049,9 @@ class App extends React.Component<AppProps, AppState> {
offset = -offset;
}
if (event.shiftKey) {
this.translateCanvas((state) => ({
scrollX: state.scrollX + offset,
}));
this.setState((state) => ({ scrollX: state.scrollX + offset }));
} else {
this.translateCanvas((state) => ({
scrollY: state.scrollY + offset,
}));
this.setState((state) => ({ scrollY: state.scrollY + offset }));
}
}
@@ -2684,13 +2599,6 @@ class App extends React.Component<AppProps, AppState> {
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
}
const fontFamily =
existingTextElement?.fontFamily || this.state.currentItemFontFamily;
const lineHeight =
existingTextElement?.lineHeight || getDefaultLineHeight(fontFamily);
const fontSize = this.state.currentItemFontSize;
if (
!existingTextElement &&
shouldBindToContainer &&
@@ -2698,14 +2606,11 @@ class App extends React.Component<AppProps, AppState> {
!isArrowElement(container)
) {
const fontString = {
fontSize,
fontFamily,
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
};
const minWidth = getApproxMinLineWidth(
getFontString(fontString),
lineHeight,
);
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const minWidth = getApproxMinLineWidth(getFontString(fontString));
const minHeight = getApproxMinLineHeight(getFontString(fontString));
const containerDims = getContainerDims(container);
const newHeight = Math.max(containerDims.height, minHeight);
const newWidth = Math.max(containerDims.width, minWidth);
@@ -2739,8 +2644,8 @@ class App extends React.Component<AppProps, AppState> {
opacity: this.state.currentItemOpacity,
roundness: null,
text: "",
fontSize,
fontFamily,
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
textAlign: parentCenterPosition
? "center"
: this.state.currentItemTextAlign,
@@ -2750,7 +2655,6 @@ class App extends React.Component<AppProps, AppState> {
containerId: shouldBindToContainer ? container?.id : undefined,
groupIds: container?.groupIds ?? [],
locked: false,
lineHeight,
});
if (!existingTextElement && shouldBindToContainer && container) {
@@ -2866,6 +2770,7 @@ class App extends React.Component<AppProps, AppState> {
);
if (container) {
if (
isArrowElement(container) ||
hasBoundTextElement(container) ||
!isTransparent(container.backgroundColor) ||
isHittingElementNotConsideringBoundingBox(container, this.state, [
@@ -3017,12 +2922,12 @@ class App extends React.Component<AppProps, AppState> {
state,
);
this.translateCanvas({
return {
zoom: zoomState.zoom,
scrollX: zoomState.scrollX + deltaX / nextZoom,
scrollY: zoomState.scrollY + deltaY / nextZoom,
shouldCacheIgnoreZoom: true,
});
};
});
this.resetShouldCacheIgnoreZoomDebounced();
} else {
@@ -3798,7 +3703,7 @@ class App extends React.Component<AppProps, AppState> {
window.addEventListener(EVENT.POINTER_UP, enableNextPaste);
}
this.translateCanvas({
this.setState({
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
scrollY: this.state.scrollY - deltaY / this.state.zoom.value,
});
@@ -4944,7 +4849,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.scrollbars.isOverHorizontal) {
const x = event.clientX;
const dx = x - pointerDownState.lastCoords.x;
this.translateCanvas({
this.setState({
scrollX: this.state.scrollX - dx / this.state.zoom.value,
});
pointerDownState.lastCoords.x = x;
@@ -4954,7 +4859,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.scrollbars.isOverVertical) {
const y = event.clientY;
const dy = y - pointerDownState.lastCoords.y;
this.translateCanvas({
this.setState({
scrollY: this.state.scrollY - dy / this.state.zoom.value,
});
pointerDownState.lastCoords.y = y;
@@ -6336,7 +6241,6 @@ class App extends React.Component<AppProps, AppState> {
actionGroup,
actionUnbindText,
actionBindText,
actionCreateContainerFromText,
actionUngroup,
CONTEXT_MENU_SEPARATOR,
actionAddToLibrary,
@@ -6383,7 +6287,7 @@ class App extends React.Component<AppProps, AppState> {
// reduced amplification for small deltas (small movements on a trackpad)
Math.min(1, absDelta / 20);
this.translateCanvas((state) => ({
this.setState((state) => ({
...getStateForZoom(
{
viewportX: cursorX,
@@ -6400,14 +6304,14 @@ class App extends React.Component<AppProps, AppState> {
// scroll horizontally when shift pressed
if (event.shiftKey) {
this.translateCanvas(({ zoom, scrollX }) => ({
this.setState(({ zoom, scrollX }) => ({
// on Mac, shift+wheel tends to result in deltaX
scrollX: scrollX - (deltaY || deltaX) / zoom.value,
}));
return;
}
this.translateCanvas(({ zoom, scrollX, scrollY }) => ({
this.setState(({ zoom, scrollX, scrollY }) => ({
scrollX: scrollX - deltaX / zoom.value,
scrollY: scrollY - deltaY / zoom.value,
}));
@@ -6466,7 +6370,94 @@ class App extends React.Component<AppProps, AppState> {
private resetShouldCacheIgnoreZoomDebounced = debounce(() => {
if (!this.unmounted) {
this.setState({ shouldCacheIgnoreZoom: false });
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);
}
}, 300);
-42
View File
@@ -1,42 +0,0 @@
import { t } from "../i18n";
const BraveMeasureTextError = () => {
return (
<div data-testid="brave-measure-text-error">
<p>
{t("errors.brave_measure_text_error.start")} &nbsp;
<span style={{ fontWeight: 600 }}>
{t("errors.brave_measure_text_error.aggressive_block_fingerprint")}
</span>{" "}
{t("errors.brave_measure_text_error.setting_enabled")}.
<br />
<br />
{t("errors.brave_measure_text_error.break")}{" "}
<span style={{ fontWeight: 600 }}>
{t("errors.brave_measure_text_error.text_elements")}
</span>{" "}
{t("errors.brave_measure_text_error.in_your_drawings")}.
</p>
<p>
{t("errors.brave_measure_text_error.strongly_recommend")}{" "}
<a href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser">
{" "}
{t("errors.brave_measure_text_error.steps")}
</a>{" "}
{t("errors.brave_measure_text_error.how")}.
</p>
<p>
{t("errors.brave_measure_text_error.disable_setting")}{" "}
<a href="https://github.com/excalidraw/excalidraw/issues/new">
{t("errors.brave_measure_text_error.issue")}
</a>{" "}
{t("errors.brave_measure_text_error.write")}{" "}
<a href="https://discord.gg/UexuTaE">
{t("errors.brave_measure_text_error.discord")}
</a>
.
</p>
</div>
);
};
export default BraveMeasureTextError;
+1 -2
View File
@@ -6,7 +6,6 @@ import DialogActionButton from "./DialogActionButton";
import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
import { useExcalidrawSetAppState } from "./App";
import { jotaiScope } from "../jotai";
interface Props extends Omit<DialogProps, "onCloseRequest"> {
onConfirm: () => void;
@@ -25,7 +24,7 @@ const ConfirmDialog = (props: Props) => {
...rest
} = props;
const setAppState = useExcalidrawSetAppState();
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
return (
<Dialog
+1 -2
View File
@@ -16,7 +16,6 @@ import { AppState } from "../types";
import { queryFocusableElements } from "../utils";
import { useSetAtom } from "jotai";
import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent";
import { jotaiScope } from "../jotai";
export interface DialogProps {
children: React.ReactNode;
@@ -73,7 +72,7 @@ export const Dialog = (props: DialogProps) => {
}, [islandNode, props.autofocus]);
const setAppState = useExcalidrawSetAppState();
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope);
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
const onClose = () => {
setAppState({ openMenu: null });
+4 -4
View File
@@ -5,13 +5,13 @@ import { Dialog } from "./Dialog";
import { useExcalidrawContainer } from "./App";
export const ErrorDialog = ({
children,
message,
onClose,
}: {
children?: React.ReactNode;
message: string;
onClose?: () => void;
}) => {
const [modalIsShown, setModalIsShown] = useState(!!children);
const [modalIsShown, setModalIsShown] = useState(!!message);
const { container: excalidrawContainer } = useExcalidrawContainer();
const handleClose = React.useCallback(() => {
@@ -32,7 +32,7 @@ export const ErrorDialog = ({
onCloseRequest={handleClose}
title={t("errorDialog.title")}
>
<div style={{ whiteSpace: "pre-wrap" }}>{children}</div>
<div style={{ whiteSpace: "pre-wrap" }}>{message}</div>
</Dialog>
)}
</>
-4
View File
@@ -9,10 +9,6 @@
text-align: center;
padding: var(--preview-padding);
margin-bottom: calc(var(--space-factor) * 3);
display: flex;
justify-content: center;
align-items: center;
}
.ExportDialog__preview canvas {
+3 -3
View File
@@ -1,7 +1,7 @@
import { t } from "../i18n";
import { HelpIcon } from "./icons";
type HelpButtonProps = {
title?: string;
name?: string;
id?: string;
onClick?(): void;
@@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => (
className="help-icon"
onClick={props.onClick}
type="button"
title={`${t("helpDialog.title")} — ?`}
aria-label={t("helpDialog.title")}
title={`${props.title} — ?`}
aria-label={props.title}
>
{HelpIcon}
</button>
+4 -3
View File
@@ -364,9 +364,10 @@ const LayerUI = ({
{appState.isLoading && <LoadingMessage delay={250} />}
{appState.errorMessage && (
<ErrorDialog onClose={() => setAppState({ errorMessage: null })}>
{appState.errorMessage}
</ErrorDialog>
<ErrorDialog
message={appState.errorMessage}
onClose={() => setAppState({ errorMessage: null })}
/>
)}
{appState.openDialog === "help" && (
<HelpDialog
@@ -48,7 +48,6 @@ export const LibraryMenuHeader: React.FC<{
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
isLibraryMenuOpenAtom,
jotaiScope,
);
const renderRemoveLibAlert = useCallback(() => {
const content = selectedItems.length
@@ -1,63 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Test <App/> should show error modal when using brave and measureText API is not working 1`] = `
<div
data-testid="brave-measure-text-error"
>
<p>
Looks like you are using Brave browser with the
 
<span
style="font-weight: 600;"
>
Aggressively Block Fingerprinting
</span>
setting enabled
.
<br />
<br />
This could result in breaking the
<span
style="font-weight: 600;"
>
Text Elements
</span>
in your drawings
.
</p>
<p>
We strongly recommend disabling this setting. You can follow
<a
href="http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser"
>
these steps
</a>
on how to do so
.
</p>
<p>
If disabling this setting doesn't fix the display of text elements, please open an
<a
href="https://github.com/excalidraw/excalidraw/issues/new"
>
issue
</a>
on our GitHub, or write us on
<a
href="https://discord.gg/UexuTaE"
>
Discord
</a>
.
</p>
</div>
`;
+1 -5
View File
@@ -31,7 +31,6 @@ import "./DefaultItems.scss";
import clsx from "clsx";
import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { jotaiScope } from "../../jotai";
export const LoadScene = () => {
const { t } = useI18n();
@@ -114,10 +113,7 @@ Help.displayName = "Help";
export const ClearCanvas = () => {
const { t } = useI18n();
const setActiveConfirmDialog = useSetAtom(
activeConfirmDialogAtom,
jotaiScope,
);
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionClearCanvas)) {
-3
View File
@@ -12,9 +12,6 @@ export const isFirefox =
export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
export const isSafari =
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
// keeping function so it can be mocked in test
export const isBrave = () =>
(navigator as any).brave?.isBrave?.name === "isBrave";
export const APP_NAME = "Excalidraw";
+1 -2
View File
@@ -7,7 +7,6 @@ import { CanvasError } from "../errors";
import { t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { AppState, DataURL, LibraryItem } from "../types";
import { ValueOf } from "../utility-types";
import { bytesToHexString } from "../utils";
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
import { isValidExcalidrawData, isValidLibrary } from "./json";
@@ -157,7 +156,7 @@ export const loadSceneOrLibraryFromBlob = async (
},
localAppState,
localElements,
{ repairBindings: true, refreshDimensions: true },
{ repairBindings: true },
),
};
} else if (isValidLibrary(data)) {
+1 -3
View File
@@ -89,9 +89,7 @@ export const exportCanvas = async (
return await fileSave(blob, {
description: "Export to PNG",
name,
// FIXME reintroduce `excalidraw.png` when most people upgrade away
// from 111.0.5563.64 (arm64), see #6349
extension: /* appState.exportEmbedScene ? "excalidraw.png" : */ "png",
extension: appState.exportEmbedScene ? "excalidraw.png" : "png",
fileHandle,
});
} else if (type === "clipboard") {
+5 -36
View File
@@ -31,15 +31,9 @@ import {
import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement";
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
import { arrayToMap } from "../utils";
import oc from "open-color";
import { MarkOptional, Mutable } from "../utility-types";
import {
detectLineHeight,
getDefaultLineHeight,
getDOMMetrics,
} from "../element/textElement";
type RestoredAppState = Omit<
AppState,
@@ -170,40 +164,17 @@ const restoreElement = (
const [fontPx, _fontFamily]: [string, string] = (
element as any
).font.split(" ");
fontSize = parseFloat(fontPx);
fontSize = parseInt(fontPx, 10);
fontFamily = getFontFamilyByName(_fontFamily);
}
const text = element.text ?? "";
// line-height might not be specified either when creating elements
// programmatically, or when importing old diagrams.
// For the latter we want to detect the original line height which
// will likely differ from our per-font fixed line height we now use,
// to maintain backward compatibility.
const lineHeight =
element.lineHeight ||
(element.height
? // detect line-height from current element height and font-size
detectLineHeight(element)
: // no element height likely means programmatic use, so default
// to a fixed line height
getDefaultLineHeight(element.fontFamily));
const { baseline } = getDOMMetrics(
element.text,
getFontString(element),
lineHeight,
);
element = restoreElementWithProperties(element, {
fontSize,
fontFamily,
text,
text: element.text ?? "",
textAlign: element.textAlign || DEFAULT_TEXT_ALIGN,
verticalAlign: element.verticalAlign || DEFAULT_VERTICAL_ALIGN,
containerId: element.containerId ?? null,
originalText: element.originalText || text,
lineHeight,
baseline,
originalText: element.originalText || element.text,
});
if (refreshDimensions) {
@@ -507,9 +478,7 @@ export const restoreAppState = (
? {
value: appState.zoom as NormalizedZoomValue,
}
: appState.zoom?.value
? appState.zoom
: defaultAppState.zoom,
: appState.zoom || defaultAppState.zoom,
// when sidebar docked and user left it open in last session,
// keep it open. If not docked, keep it closed irrespective of last state.
openSidebar:
-1
View File
@@ -23,7 +23,6 @@ import {
import { rescalePoints } from "../points";
import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";
import { Mutable } from "../utility-types";
// x and y position of top left corner, x and y position of bottom right corner
export type Bounds = readonly [number, number, number, number];
+1 -7
View File
@@ -38,7 +38,6 @@ import { isTextElement } from ".";
import { isTransparent } from "../utils";
import { shouldShowBoundingBox } from "./transformHandles";
import { getBoundTextElement } from "./textElement";
import { Mutable } from "../utility-types";
const isElementDraggableFromInside = (
element: NonDeletedExcalidrawElement,
@@ -786,12 +785,7 @@ export const findFocusPointForEllipse = (
orientation * py * Math.sqrt(Math.max(0, squares - a ** 2 * b ** 2))) /
squares;
let n = (-m * px - 1) / py;
if (n === 0) {
// if zero {-0, 0}, fall back to a same-sign value in the similar range
n = (Object.is(n, -0) ? -1 : 1) * 0.01;
}
const n = (-m * px - 1) / py;
const x = -(a ** 2 * m) / (n ** 2 * b ** 2 + m ** 2 * a ** 2);
return GA.point(x, (-m * x - 1) / n);
-1
View File
@@ -41,7 +41,6 @@ import { shouldRotateWithDiscreteAngle } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { getShapeForElement } from "../renderer/renderElement";
import { DRAGGING_THRESHOLD } from "../constants";
import { Mutable } from "../utility-types";
const editorMidPointsCache: {
version: number | null;
-1
View File
@@ -5,7 +5,6 @@ import { getSizeFromPoints } from "../points";
import { randomInteger } from "../random";
import { Point } from "../types";
import { getUpdatedTimestamp } from "../utils";
import { Mutable } from "../utility-types";
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
Partial<TElement>,
+6 -20
View File
@@ -29,11 +29,9 @@ import {
normalizeText,
wrapText,
getMaxContainerWidth,
getDefaultLineHeight,
} from "./textElement";
import { VERTICAL_ALIGN } from "../constants";
import { isArrowElement } from "./typeChecks";
import { MarkOptional, Merge, Mutable } from "../utility-types";
type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@@ -138,14 +136,11 @@ export const newTextElement = (
textAlign: TextAlign;
verticalAlign: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"];
lineHeight?: ExcalidrawTextElement["lineHeight"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => {
const lineHeight = opts.lineHeight || getDefaultLineHeight(opts.fontFamily);
const text = normalizeText(opts.text);
const metrics = measureText(text, getFontString(opts), lineHeight);
const metrics = measureText(text, getFontString(opts));
const offsets = getTextElementPositionOffsets(opts, metrics);
const textElement = newElementWith(
{
..._newElementBase<ExcalidrawTextElement>("text", opts),
@@ -158,10 +153,8 @@ export const newTextElement = (
y: opts.y - offsets.y,
width: metrics.width,
height: metrics.height,
baseline: metrics.baseline,
containerId: opts.containerId || null,
originalText: text,
lineHeight,
},
{},
);
@@ -176,15 +169,13 @@ const getAdjustedDimensions = (
y: number;
width: number;
height: number;
baseline: number;
} => {
const container = getContainerElement(element);
const {
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
} = measureText(nextText, getFontString(element), element.lineHeight);
const { width: nextWidth, height: nextHeight } = measureText(
nextText,
getFontString(element),
);
const { textAlign, verticalAlign } = element;
let x: number;
let y: number;
@@ -193,11 +184,7 @@ const getAdjustedDimensions = (
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId
) {
const prevMetrics = measureText(
element.text,
getFontString(element),
element.lineHeight,
);
const prevMetrics = measureText(element.text, getFontString(element));
const offsets = getTextElementPositionOffsets(element, {
width: nextWidth - prevMetrics.width,
height: nextHeight - prevMetrics.height,
@@ -259,7 +246,6 @@ const getAdjustedDimensions = (
return {
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
x: Number.isFinite(x) ? x : element.x,
y: Number.isFinite(y) ? y : element.y,
};
+21 -52
View File
@@ -39,15 +39,13 @@ import {
import { Point, PointerDownState } from "../types";
import Scene from "../scene/Scene";
import {
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElement,
getBoundTextElementId,
getContainerElement,
handleBindTextResize,
getMaxContainerWidth,
getApproxMinLineHeight,
measureText,
getMaxContainerHeight,
} from "./textElement";
export const normalizeAngle = (angle: number): number => {
@@ -195,8 +193,7 @@ const MIN_FONT_SIZE = 1;
const measureFontSizeFromWidth = (
element: NonDeleted<ExcalidrawTextElement>,
nextWidth: number,
nextHeight: number,
): { size: number; baseline: number } | null => {
): number | null => {
// We only use width to scale font on resize
let width = element.width;
@@ -211,15 +208,8 @@ const measureFontSizeFromWidth = (
if (nextFontSize < MIN_FONT_SIZE) {
return null;
}
const metrics = measureText(
element.text,
getFontString({ fontSize: nextFontSize, fontFamily: element.fontFamily }),
element.lineHeight,
);
return {
size: nextFontSize,
baseline: metrics.baseline + (nextHeight - metrics.height),
};
return nextFontSize;
};
const getSidesForTransformHandle = (
@@ -290,8 +280,8 @@ const resizeSingleTextElement = (
if (scale > 0) {
const nextWidth = element.width * scale;
const nextHeight = element.height * scale;
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
if (metrics === null) {
const nextFontSize = measureFontSizeFromWidth(element, nextWidth);
if (nextFontSize === null) {
return;
}
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
@@ -315,10 +305,9 @@ const resizeSingleTextElement = (
deltaY2,
);
mutateElement(element, {
fontSize: metrics.size,
fontSize: nextFontSize,
width: nextWidth,
height: nextHeight,
baseline: metrics.baseline,
x: nextElementX,
y: nextElementY,
});
@@ -371,7 +360,7 @@ export const resizeSingleElement = (
let scaleX = atStartBoundsWidth / boundsCurrentWidth;
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
let boundTextFont: { fontSize?: number; baseline?: number } = {};
let boundTextFont: { fontSize?: number } = {};
const boundTextElement = getBoundTextElement(element);
if (transformHandleDirection.includes("e")) {
@@ -423,7 +412,6 @@ export const resizeSingleElement = (
if (stateOfBoundTextElementAtResize) {
boundTextFont = {
fontSize: stateOfBoundTextElementAtResize.fontSize,
baseline: stateOfBoundTextElementAtResize.baseline,
};
}
if (shouldMaintainAspectRatio) {
@@ -433,27 +421,19 @@ export const resizeSingleElement = (
height: eleNewHeight,
};
const nextFont = measureFontSizeFromWidth(
const nextFontSize = measureFontSizeFromWidth(
boundTextElement,
getMaxContainerWidth(updatedElement),
getMaxContainerHeight(updatedElement),
);
if (nextFont === null) {
if (nextFontSize === null) {
return;
}
boundTextFont = {
fontSize: nextFont.size,
baseline: nextFont.baseline,
fontSize: nextFontSize,
};
} else {
const minWidth = getApproxMinLineWidth(
getFontString(boundTextElement),
boundTextElement.lineHeight,
);
const minHeight = getApproxMinLineHeight(
boundTextElement.fontSize,
boundTextElement.lineHeight,
);
const minWidth = getApproxMinLineWidth(getFontString(boundTextElement));
const minHeight = getApproxMinLineHeight(getFontString(boundTextElement));
eleNewWidth = Math.ceil(Math.max(eleNewWidth, minWidth));
eleNewHeight = Math.ceil(Math.max(eleNewHeight, minHeight));
}
@@ -586,11 +566,8 @@ export const resizeSingleElement = (
});
mutateElement(element, resizedElement);
if (boundTextElement && boundTextFont != null) {
mutateElement(boundTextElement, {
fontSize: boundTextFont.fontSize,
baseline: boundTextFont.baseline,
});
if (boundTextElement && boundTextFont) {
mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize });
}
handleBindTextResize(element, transformHandleDirection);
}
@@ -696,7 +673,6 @@ const resizeMultipleElements = (
y: number;
points?: Point[];
fontSize?: number;
baseline?: number;
} = {
width,
height,
@@ -705,7 +681,7 @@ const resizeMultipleElements = (
...rescaledPoints,
};
let boundTextUpdates: { fontSize: number; baseline: number } | null = null;
let boundTextUpdates: { fontSize: number } | null = null;
const boundTextElement = getBoundTextElement(element.latest);
@@ -715,29 +691,22 @@ const resizeMultipleElements = (
width,
height,
};
const metrics = measureFontSizeFromWidth(
const fontSize = measureFontSizeFromWidth(
boundTextElement ?? (element.orig as ExcalidrawTextElement),
boundTextElement
? getMaxContainerWidth(updatedElement)
: updatedElement.width,
boundTextElement
? getMaxContainerHeight(updatedElement)
: updatedElement.height,
getMaxContainerWidth(updatedElement),
);
if (!metrics) {
if (!fontSize) {
return;
}
if (isTextElement(element.orig)) {
update.fontSize = metrics.size;
update.baseline = metrics.baseline;
update.fontSize = fontSize;
}
if (boundTextElement) {
boundTextUpdates = {
fontSize: metrics.size,
baseline: metrics.baseline,
fontSize,
};
}
}
+26 -79
View File
@@ -1,14 +1,11 @@
import { BOUND_TEXT_PADDING, FONT_FAMILY } from "../constants";
import { BOUND_TEXT_PADDING } from "../constants";
import { API } from "../tests/helpers/api";
import {
computeContainerDimensionForBoundText,
computeContainerHeightForBoundText,
getContainerCoords,
getMaxContainerWidth,
getMaxContainerHeight,
wrapText,
detectLineHeight,
getLineHeightInPx,
getDefaultLineHeight,
} from "./textElement";
import { FontString } from "./types";
@@ -19,7 +16,7 @@ describe("Test wrapText", () => {
const text = "Hello whats up ";
const maxWidth = 200 - BOUND_TEXT_PADDING * 2;
const res = wrapText(text, font, maxWidth);
expect(res).toBe(text);
expect(res).toBe("Hello whats up ");
});
it("should work with emojis", () => {
@@ -29,7 +26,7 @@ describe("Test wrapText", () => {
expect(res).toBe("😀");
});
it("should show the text correctly when max width reached", () => {
it("should show the text correctly when min width reached", () => {
const text = "Hello😀";
const maxWidth = 10;
const res = wrapText(text, font, maxWidth);
@@ -38,12 +35,13 @@ describe("Test wrapText", () => {
describe("When text doesn't contain new lines", () => {
const text = "Hello whats up";
[
{
desc: "break all words when width of each word is less than container width",
width: 80,
res: `Hello \nwhats \nup`,
width: 90,
res: `Hello
whats
up`,
},
{
desc: "break all characters when width of each character is less than container width",
@@ -64,8 +62,9 @@ p`,
{
desc: "break words as per the width",
width: 140,
res: `Hello whats \nup`,
width: 150,
res: `Hello whats
up`,
},
{
desc: "fit the container",
@@ -94,8 +93,10 @@ whats up`;
[
{
desc: "break all words when width of each word is less than container width",
width: 80,
res: `Hello\nwhats \nup`,
width: 90,
res: `Hello
whats
up`,
},
{
desc: "break all characters when width of each character is less than container width",
@@ -134,14 +135,17 @@ whats up`,
});
});
});
describe("When text is long", () => {
const text = `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg break it now`;
[
{
desc: "fit characters of long string as per container width",
width: 170,
res: `hellolongtextth\nisiswhatsupwith\nyouIamtypingggg\ngandtypinggg \nbreak it now`,
res: `hellolongtextth
isiswhatsupwith
youIamtypingggg
gandtypinggg
break it now`,
},
{
@@ -160,7 +164,8 @@ now`,
desc: "fit the long text when container width is greater than text length and move the rest to next line",
width: 600,
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg \nbreak it now`,
res: `hellolongtextthisiswhatsupwithyouIamtypingggggandtypinggg
break it now`,
},
].forEach((data) => {
it(`should ${data.desc}`, () => {
@@ -169,20 +174,6 @@ now`,
});
});
});
it("should wrap the text correctly when word length is exactly equal to max width", () => {
const text = "Hello Excalidraw";
// Length of "Excalidraw" is 100 and exacty equal to max width
const res = wrapText(text, font, 100);
expect(res).toEqual(`Hello \nExcalidraw`);
});
it("should return the text as is if max width is invalid", () => {
const text = "Hello Excalidraw";
expect(wrapText(text, font, NaN)).toEqual(text);
expect(wrapText(text, font, -1)).toEqual(text);
expect(wrapText(text, font, Infinity)).toEqual(text);
});
});
describe("Test measureText", () => {
@@ -223,7 +214,7 @@ describe("Test measureText", () => {
});
});
describe("Test computeContainerDimensionForBoundText", () => {
describe("Test computeContainerHeightForBoundText", () => {
const params = {
width: 178,
height: 194,
@@ -234,9 +225,7 @@ describe("Test measureText", () => {
type: "rectangle",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
160,
);
expect(computeContainerHeightForBoundText(element, 150)).toEqual(160);
});
it("should compute container height correctly for ellipse", () => {
@@ -244,9 +233,7 @@ describe("Test measureText", () => {
type: "ellipse",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
226,
);
expect(computeContainerHeightForBoundText(element, 150)).toEqual(226);
});
it("should compute container height correctly for diamond", () => {
@@ -254,9 +241,7 @@ describe("Test measureText", () => {
type: "diamond",
...params,
});
expect(computeContainerDimensionForBoundText(150, element.type)).toEqual(
320,
);
expect(computeContainerHeightForBoundText(element, 150)).toEqual(320);
});
});
@@ -304,41 +289,3 @@ describe("Test measureText", () => {
});
});
});
const textElement = API.createElement({
type: "text",
text: "Excalidraw is a\nvirtual \nopensource \nwhiteboard for \nsketching \nhand-drawn like\ndiagrams",
fontSize: 20,
fontFamily: 1,
height: 175,
});
describe("Test detectLineHeight", () => {
it("should return correct line height", () => {
expect(detectLineHeight(textElement)).toBe(1.25);
});
});
describe("Test getLineHeightInPx", () => {
it("should return correct line height", () => {
expect(
getLineHeightInPx(textElement.fontSize, textElement.lineHeight),
).toBe(25);
});
});
describe("Test getDefaultLineHeight", () => {
it("should return line height using default font family when not passed", () => {
//@ts-ignore
expect(getDefaultLineHeight()).toBe(1.25);
});
it("should return line height using default font family for unknown font", () => {
const UNKNOWN_FONT = 5;
expect(getDefaultLineHeight(UNKNOWN_FONT)).toBe(1.25);
});
it("should return correct line height", () => {
expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
});
});
+93 -249
View File
@@ -4,24 +4,19 @@ import {
ExcalidrawTextContainer,
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
FontFamilyValues,
FontString,
NonDeletedExcalidrawElement,
} from "./types";
import { mutateElement } from "./mutateElement";
import {
BOUND_TEXT_PADDING,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
FONT_FAMILY,
isSafari,
TEXT_ALIGN,
VERTICAL_ALIGN,
} from "../constants";
import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
import { isTextElement } from ".";
import { isBoundToContainer, isArrowElement } from "./typeChecks";
import {
isBoundToContainer,
isImageElement,
isArrowElement,
} from "./typeChecks";
import { LinearElementEditor } from "./linearElementEditor";
import { AppState } from "../types";
import { isTextBindableContainer } from "./typeChecks";
@@ -32,7 +27,6 @@ import {
resetOriginalContainerCache,
updateOriginalContainerCache,
} from "./textWysiwyg";
import { ExtractSetType } from "../utility-types";
export const normalizeText = (text: string) => {
return (
@@ -44,22 +38,18 @@ export const normalizeText = (text: string) => {
);
};
export const splitIntoLines = (text: string) => {
return normalizeText(text).split("\n");
};
export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null,
) => {
let maxWidth = undefined;
const boundTextUpdates = {
x: textElement.x,
y: textElement.y,
text: textElement.text,
width: textElement.width,
height: textElement.height,
baseline: textElement.baseline,
};
boundTextUpdates.text = textElement.text;
@@ -75,12 +65,10 @@ export const redrawTextBoundingBox = (
const metrics = measureText(
boundTextUpdates.text,
getFontString(textElement),
textElement.lineHeight,
);
boundTextUpdates.width = metrics.width;
boundTextUpdates.height = metrics.height;
boundTextUpdates.baseline = metrics.baseline;
if (container) {
if (isArrowElement(container)) {
@@ -96,9 +84,9 @@ export const redrawTextBoundingBox = (
let nextHeight = containerDims.height;
if (metrics.height > maxContainerHeight) {
nextHeight = computeContainerDimensionForBoundText(
nextHeight = computeContainerHeightForBoundText(
container,
metrics.height,
container.type,
);
mutateElement(container, { height: nextHeight });
maxContainerHeight = getMaxContainerHeight(container);
@@ -186,7 +174,6 @@ 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(
@@ -195,20 +182,15 @@ export const handleBindTextResize = (
maxWidth,
);
}
const metrics = measureText(
text,
getFontString(textElement),
textElement.lineHeight,
);
nextHeight = metrics.height;
nextWidth = metrics.width;
nextBaseLine = metrics.baseline;
const dimensions = measureText(text, getFontString(textElement));
nextHeight = dimensions.height;
nextWidth = dimensions.width;
}
// increase height in case text element height exceeds
if (nextHeight > maxHeight) {
containerHeight = computeContainerDimensionForBoundText(
containerHeight = computeContainerHeightForBoundText(
container,
nextHeight,
container.type,
);
const diff = containerHeight - containerDims.height;
@@ -230,7 +212,6 @@ export const handleBindTextResize = (
text,
width: nextWidth,
height: nextHeight,
baseline: nextBaseLine,
});
if (!isArrowElement(container)) {
@@ -277,106 +258,32 @@ const computeBoundTextPosition = (
// https://github.com/grassator/canvas-text-editor/blob/master/lib/FontMetrics.js
export const measureText = (
text: string,
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
export const measureText = (text: string, font: FontString) => {
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 fontSize = parseFloat(font);
const height = getTextHeight(text, fontSize, lineHeight);
const height = getTextHeight(text, font);
const width = getTextWidth(text, font);
const { baseline } = getDOMMetrics(text, font, lineHeight);
return { width, height, baseline };
return { width, height };
};
export const getDOMMetrics = (
text: string,
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
wrapInContainer?: boolean,
) => {
const container = document.createElement("div");
container.style.position = "absolute";
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
if (wrapInContainer) {
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.whiteSpace = "pre-wrap";
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
const cacheApproxLineHeight: { [key: FontString]: number } = {};
export const getApproxLineHeight = (font: FontString) => {
if (cacheApproxLineHeight[font]) {
return cacheApproxLineHeight[font];
}
const fontSize = parseInt(font);
container.style.lineHeight = String(lineHeight);
const canvasHeight = getTextHeight(text, parseFloat(font), lineHeight);
container.innerText = text;
// Baseline is important for positioning text on canvas
document.body.appendChild(container);
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);
let baseline = span.offsetTop + span.offsetHeight;
const height = container.offsetHeight;
if (isSafari) {
// In Safari sometimes DOM height could be less than canvas height due to
// which text could go out of the bounding box hence shifting the baseline
// to make sure text is rendered correctly
if (canvasHeight > height) {
baseline += canvasHeight - height;
}
// In Safari sometimes DOM height could be more than canvas height due to
// which text could go out of the bounding box hence shifting the baseline
// to make sure text is rendered correctly
if (height > canvasHeight) {
baseline -= height - canvasHeight;
}
}
document.body.removeChild(container);
return { baseline, height };
};
/**
* To get unitless line-height (if unknown) we can calculate it by dividing
* height-per-line by fontSize.
*/
export const detectLineHeight = (textElement: ExcalidrawTextElement) => {
const lineCount = splitIntoLines(textElement.text).length;
return (textElement.height /
lineCount /
textElement.fontSize) as ExcalidrawTextElement["lineHeight"];
};
/**
* We calculate the line height from the font size and the unitless line height,
* aligning with the W3C spec.
*/
export const getLineHeightInPx = (
fontSize: ExcalidrawTextElement["fontSize"],
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
const res = fontSize * lineHeight;
return res;
};
// FIXME rename to getApproxMinContainerHeight
export const getApproxMinLineHeight = (
fontSize: ExcalidrawTextElement["fontSize"],
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
return getLineHeightInPx(fontSize, lineHeight) + BOUND_TEXT_PADDING * 2;
// Calculate line height relative to font size
cacheApproxLineHeight[font] = fontSize * 1.2;
return cacheApproxLineHeight[font];
};
let canvas: HTMLCanvasElement | undefined;
@@ -399,7 +306,7 @@ const getLineWidth = (text: string, font: FontString) => {
};
export const getTextWidth = (text: string, font: FontString) => {
const lines = splitIntoLines(text);
const lines = text.replace(/\r\n?/g, "\n").split("\n");
let width = 0;
lines.forEach((line) => {
width = Math.max(width, getLineWidth(line, font));
@@ -407,58 +314,36 @@ export const getTextWidth = (text: string, font: FontString) => {
return width;
};
export const getTextHeight = (
text: string,
fontSize: number,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
const lineCount = splitIntoLines(text).length;
return getLineHeightInPx(fontSize, lineHeight) * lineCount;
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) => {
// if maxWidth is not finite or NaN which can happen in case of bugs in
// computation, we need to make sure we don't continue as we'll end up
// in an infinite loop
if (!Number.isFinite(maxWidth) || maxWidth < 0) {
return text;
}
const lines: Array<string> = [];
const originalLines = text.split("\n");
const spaceWidth = getLineWidth(" ", font);
let currentLine = "";
let currentLineWidthTillNow = 0;
const push = (str: string) => {
if (str.trim()) {
lines.push(str);
}
};
const resetParams = () => {
currentLine = "";
currentLineWidthTillNow = 0;
};
originalLines.forEach((originalLine) => {
const currentLineWidth = getTextWidth(originalLine, font);
//Push the line if its <= maxWidth
if (currentLineWidth <= maxWidth) {
lines.push(originalLine);
const words = originalLine.split(" ");
// This means its newline so push it
if (words.length === 1 && words[0] === "") {
lines.push(words[0]);
return; // continue
}
const words = originalLine.split(" ");
resetParams();
let currentLine = "";
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]);
@@ -470,8 +355,8 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// push current line since the current word exceeds the max width
// so will be appended in next line
push(currentLine);
resetParams();
currentLine = "";
currentLineWidthTillNow = 0;
while (words[index].length > 0) {
const currentChar = String.fromCodePoint(
@@ -482,6 +367,10 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
words[index] = words[index].slice(currentChar.length);
if (currentLineWidthTillNow >= maxWidth) {
// only remove last trailing space which we have added when joining words
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
push(currentLine);
currentLine = currentChar;
currentLineWidthTillNow = width;
@@ -489,11 +378,11 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine += currentChar;
}
}
// push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
push(currentLine);
resetParams();
currentLine = "";
currentLineWidthTillNow = 0;
} else {
// space needs to be appended before next word
// as currentLine contains chars which couldn't be appended
@@ -501,6 +390,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLine += " ";
currentLineWidthTillNow += spaceWidth;
}
index++;
} else {
// Start appending words in a line till max width reached
@@ -508,9 +398,10 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
const word = words[index];
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
if (currentLineWidthTillNow > maxWidth) {
if (currentLineWidthTillNow >= maxWidth) {
push(currentLine);
resetParams();
currentLineWidthTillNow = 0;
currentLine = "";
break;
}
@@ -521,15 +412,22 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
if (currentLineWidthTillNow + spaceWidth >= maxWidth) {
const word = currentLine.slice(0, -1);
push(word);
resetParams();
currentLine = "";
currentLineWidthTillNow = 0;
break;
}
}
if (currentLineWidthTillNow === maxWidth) {
currentLine = "";
currentLineWidthTillNow = 0;
}
}
}
if (currentLine.slice(-1) === " ") {
if (currentLine) {
// only remove last trailing space which we have added when joining words
currentLine = currentLine.slice(0, -1);
if (currentLine.slice(-1) === " ") {
currentLine = currentLine.slice(0, -1);
}
push(currentLine);
}
});
@@ -561,23 +459,21 @@ export const charWidth = (() => {
};
})();
const DUMMY_TEXT = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".toLocaleUpperCase();
// FIXME rename to getApproxMinContainerWidth
export const getApproxMinLineWidth = (
font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"],
) => {
export const getApproxMinLineWidth = (font: FontString) => {
const maxCharWidth = getMaxCharWidth(font);
if (maxCharWidth === 0) {
return (
measureText(DUMMY_TEXT.split("").join("\n"), font, lineHeight).width +
measureText(DUMMY_TEXT.split("").join("\n"), font).width +
BOUND_TEXT_PADDING * 2
);
}
return maxCharWidth + BOUND_TEXT_PADDING * 2;
};
export const getApproxMinLineHeight = (font: FontString) => {
return getApproxLineHeight(font) + BOUND_TEXT_PADDING * 2;
};
export const getMinCharWidth = (font: FontString) => {
const cache = charWidth.getCache(font);
if (!cache) {
@@ -770,24 +666,14 @@ export const shouldAllowVerticalAlign = (
}
return true;
}
return false;
});
};
export const suppportsHorizontalAlign = (
selectedElements: NonDeletedExcalidrawElement[],
) => {
return selectedElements.some((element) => {
const hasBoundContainer = isBoundToContainer(element);
if (hasBoundContainer) {
const container = getContainerElement(element);
if (isTextElement(element) && isArrowElement(container)) {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
if (isArrowElement(element)) {
return false;
}
return true;
}
return isTextElement(element);
return false;
});
};
@@ -828,34 +714,32 @@ export const getTextBindableContainerAtPosition = (
return isTextBindableContainer(hitElement, false) ? hitElement : null;
};
const VALID_CONTAINER_TYPES = new Set([
"rectangle",
"ellipse",
"diamond",
"image",
"arrow",
]);
export const isValidTextContainer = (element: ExcalidrawElement) => {
return (
element.type === "rectangle" ||
element.type === "ellipse" ||
element.type === "diamond" ||
isImageElement(element) ||
isArrowElement(element)
);
};
export const isValidTextContainer = (element: ExcalidrawElement) =>
VALID_CONTAINER_TYPES.has(element.type);
export const computeContainerDimensionForBoundText = (
dimension: number,
containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>,
export const computeContainerHeightForBoundText = (
container: NonDeletedExcalidrawElement,
boundTextElementHeight: number,
) => {
dimension = Math.ceil(dimension);
const padding = BOUND_TEXT_PADDING * 2;
if (containerType === "ellipse") {
return Math.round(((dimension + padding) / Math.sqrt(2)) * 2);
if (container.type === "ellipse") {
return Math.round(
((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2,
);
}
if (containerType === "arrow") {
return dimension + padding * 8;
if (isArrowElement(container)) {
return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2;
}
if (containerType === "diamond") {
return 2 * (dimension + padding);
if (container.type === "diamond") {
return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2);
}
return dimension + padding;
return boundTextElementHeight + BOUND_TEXT_PADDING * 2;
};
export const getMaxContainerWidth = (container: ExcalidrawElement) => {
@@ -912,43 +796,3 @@ export const getMaxContainerHeight = (container: ExcalidrawElement) => {
}
return height - BOUND_TEXT_PADDING * 2;
};
export const isMeasureTextSupported = () => {
const width = getTextWidth(
DUMMY_TEXT,
getFontString({
fontSize: DEFAULT_FONT_SIZE,
fontFamily: DEFAULT_FONT_FAMILY,
}),
);
return width > 0;
};
/**
* Unitless line height
*
* In previous versions we used `normal` line height, which browsers interpret
* differently, and based on font-family and font-size.
*
* To make line heights consistent across browsers we hardcode the values for
* each of our fonts based on most common average line-heights.
* See https://github.com/excalidraw/excalidraw/pull/6360#issuecomment-1477635971
* where the values come from.
*/
const DEFAULT_LINE_HEIGHT = {
// ~1.25 is the average for Virgil in WebKit and Blink.
// Gecko (FF) uses ~1.28.
[FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
// ~1.15 is the average for Virgil in WebKit and Blink.
// Gecko if all over the place.
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
// ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
[FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
};
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
if (fontFamily in DEFAULT_LINE_HEIGHT) {
return DEFAULT_LINE_HEIGHT[fontFamily];
}
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
};
+20 -215
View File
@@ -3,14 +3,10 @@ import ExcalidrawApp from "../excalidraw-app";
import { GlobalTestState, render, screen } from "../tests/test-utils";
import { Keyboard, Pointer, UI } from "../tests/helpers/ui";
import { CODES, KEYS } from "../keys";
import {
fireEvent,
mockBoundingClientRect,
restoreOriginalGetBoundingClientRect,
} from "../tests/test-utils";
import { fireEvent } from "../tests/test-utils";
import { queryByText } from "@testing-library/react";
import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants";
import { FONT_FAMILY } from "../constants";
import {
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
@@ -19,7 +15,6 @@ import { API } from "../tests/helpers/api";
import { mutateElement } from "./mutateElement";
import { resize } from "../tests/utils";
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
// Unmount ReactDOM from root
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -226,19 +221,11 @@ describe("textWysiwyg", () => {
describe("Test container-unbound text", () => {
const { h } = window;
const dimensions = { height: 400, width: 800 };
let textarea: HTMLTextAreaElement;
let textElement: ExcalidrawTextElement;
beforeAll(() => {
mockBoundingClientRect(dimensions);
});
beforeEach(async () => {
await render(<ExcalidrawApp />);
//@ts-ignore
h.app.refreshDeviceState(h.app.excalidrawContainerRef.current!);
textElement = UI.createElement("text");
@@ -248,10 +235,6 @@ describe("textWysiwyg", () => {
)!;
});
afterAll(() => {
restoreOriginalGetBoundingClientRect();
});
it("should add a tab at the start of the first line", () => {
const event = new KeyboardEvent("keydown", { key: KEYS.TAB });
textarea.value = "Line#1\nLine#2";
@@ -450,27 +433,6 @@ describe("textWysiwyg", () => {
);
expect(h.state.zoom.value).toBe(1);
});
it("text should never go beyond max width", async () => {
UI.clickTool("text");
mouse.clickAt(750, 300);
textarea = document.querySelector(
".excalidraw-textEditorContainer > textarea",
)!;
fireEvent.change(textarea, {
target: {
value:
"Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!",
},
});
textarea.dispatchEvent(new Event("input"));
await new Promise((cb) => setTimeout(cb, 0));
textarea.blur();
expect(textarea.style.width).toBe("792px");
expect(h.elements[0].width).toBe(1000);
});
});
describe("Test container-bound text", () => {
@@ -526,44 +488,6 @@ describe("textWysiwyg", () => {
]);
});
it("should compute the container height correctly and not throw error when height is updated while editing the text", async () => {
const diamond = API.createElement({
type: "diamond",
x: 10,
y: 20,
width: 90,
height: 75,
});
h.elements = [diamond];
expect(h.elements.length).toBe(1);
expect(h.elements[0].id).toBe(diamond.id);
API.setSelectedElements([diamond]);
Keyboard.keyPress(KEYS.ENTER);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
const value = new Array(1000).fill("1").join("\n");
// Pasting large text to simulate height increase
expect(() =>
fireEvent.input(editor, { target: { value } }),
).not.toThrow();
expect(diamond.height).toBe(50020);
// Clearing text to simulate height decrease
expect(() =>
fireEvent.input(editor, { target: { value: "" } }),
).not.toThrow();
expect(diamond.height).toBe(70);
});
it("should bind text to container when double clicked on center of transparent container", async () => {
const rectangle = API.createElement({
type: "rectangle",
@@ -707,11 +631,11 @@ describe("textWysiwyg", () => {
["freedraw", "line"].forEach((type: any) => {
it(`shouldn't create text element when pressing 'Enter' key on ${type} `, async () => {
h.elements = [];
const element = UI.createElement(type, {
const elemnet = UI.createElement(type, {
width: 100,
height: 50,
});
API.setSelectedElements([element]);
API.setSelectedElements([elemnet]);
Keyboard.keyPress(KEYS.ENTER);
expect(h.elements.length).toBe(1);
});
@@ -821,7 +745,7 @@ describe("textWysiwyg", () => {
rectangle.y + h.elements[0].height / 2 - text.height / 2,
);
expect(text.x).toBe(25);
expect(text.height).toBe(50);
expect(text.height).toBe(48);
expect(text.width).toBe(60);
// Edit and text by removing second line and it should
@@ -848,7 +772,7 @@ describe("textWysiwyg", () => {
expect(text.text).toBe("Hello");
expect(text.originalText).toBe("Hello");
expect(text.height).toBe(25);
expect(text.height).toBe(24);
expect(text.width).toBe(50);
expect(text.y).toBe(
rectangle.y + h.elements[0].height / 2 - text.height / 2,
@@ -941,7 +865,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
85,
4.5,
5,
]
`);
@@ -967,7 +891,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
65,
66,
]
`);
@@ -1105,9 +1029,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(-40);
expect(rectangle.y).toBe(-35);
expect(text.x).toBe(85);
expect(text.y).toBe(-35);
expect(text.y).toBe(-30);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyPress(KEYS.Z);
@@ -1150,7 +1074,7 @@ describe("textWysiwyg", () => {
target: { value: "Online whiteboard collaboration made easy" },
});
editor.blur();
expect(rectangle.height).toBe(185);
expect(rectangle.height).toBe(178);
mouse.select(rectangle);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
@@ -1219,42 +1143,9 @@ describe("textWysiwyg", () => {
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontSize,
).toEqual(36);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(97);
});
it("should update line height when font family updated", async () => {
Keyboard.keyPress(KEYS.ENTER);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
await new Promise((r) => setTimeout(r, 0));
fireEvent.change(editor, { target: { value: "Hello World!" } });
editor.blur();
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.25);
mouse.select(rectangle);
Keyboard.keyPress(KEYS.ENTER);
fireEvent.click(screen.getByTitle(/code/i));
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.2);
fireEvent.click(screen.getByTitle(/normal/i));
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Helvetica);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.15);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(
96.39999999999999,
);
});
describe("should align correctly", () => {
@@ -1316,7 +1207,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
45,
45.5,
]
`);
});
@@ -1328,7 +1219,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
30,
45,
45.5,
]
`);
});
@@ -1340,7 +1231,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
45,
45,
45.5,
]
`);
});
@@ -1352,7 +1243,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
15,
65,
66,
]
`);
});
@@ -1363,7 +1254,7 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
30,
65,
66,
]
`);
});
@@ -1374,96 +1265,10 @@ describe("textWysiwyg", () => {
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
45,
65,
66,
]
`);
});
});
it("should wrap text in a container when wrap text in container triggered from context menu", async () => {
UI.clickTool("text");
mouse.clickAt(20, 30);
const editor = document.querySelector(
".excalidraw-textEditorContainer > textarea",
) as HTMLTextAreaElement;
fireEvent.change(editor, {
target: {
value: "Excalidraw is an opensource virtual collaborative whiteboard",
},
});
editor.dispatchEvent(new Event("input"));
await new Promise((cb) => setTimeout(cb, 0));
editor.select();
fireEvent.click(screen.getByTitle("Left"));
await new Promise((r) => setTimeout(r, 0));
editor.blur();
const textElement = h.elements[1] as ExcalidrawTextElement;
expect(textElement.width).toBe(600);
expect(textElement.height).toBe(25);
expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT);
expect((textElement as ExcalidrawTextElement).text).toBe(
"Excalidraw is an opensource virtual collaborative whiteboard",
);
API.setSelectedElements([textElement]);
fireEvent.contextMenu(GlobalTestState.canvas, {
button: 2,
clientX: 20,
clientY: 30,
});
const contextMenu = document.querySelector(".context-menu");
fireEvent.click(
queryByText(contextMenu as HTMLElement, "Wrap text in a container")!,
);
expect(h.elements.length).toBe(3);
expect(h.elements[1]).toEqual(
expect.objectContaining({
angle: 0,
backgroundColor: "transparent",
boundElements: [
{
id: h.elements[2].id,
type: "text",
},
],
fillStyle: "hachure",
groupIds: [],
height: 35,
isDeleted: false,
link: null,
locked: false,
opacity: 100,
roughness: 1,
roundness: {
type: 3,
},
strokeColor: "#000000",
strokeStyle: "solid",
strokeWidth: 1,
type: "rectangle",
updated: 1,
version: 1,
width: 610,
x: 15,
y: 25,
}),
);
expect(h.elements[2] as ExcalidrawTextElement).toEqual(
expect.objectContaining({
text: "Excalidraw is an opensource virtual collaborative whiteboard",
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.LEFT,
boundElements: null,
}),
);
});
});
});
+31 -46
View File
@@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, isSafari, VERTICAL_ALIGN } from "../constants";
import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -22,6 +22,7 @@ import {
import { AppState } from "../types";
import { mutateElement } from "./mutateElement";
import {
getApproxLineHeight,
getBoundTextElementId,
getContainerCoords,
getContainerDims,
@@ -34,10 +35,6 @@ import {
wrapText,
getMaxContainerHeight,
getMaxContainerWidth,
computeContainerDimensionForBoundText,
getDOMMetrics,
splitIntoLines,
detectLineHeight,
} from "./textElement";
import {
actionDecreaseFontSize,
@@ -153,7 +150,9 @@ export const textWysiwyg = ({
return;
}
const { textAlign, verticalAlign } = updatedTextElement;
const approxLineHeight = getApproxLineHeight(
getFontString(updatedTextElement),
);
if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y;
@@ -212,12 +211,11 @@ export const textWysiwyg = ({
// autogrow container height if text exceeds
if (!isArrowElement(container) && textElementHeight > maxHeight) {
const targetContainerHeight = computeContainerDimensionForBoundText(
textElementHeight,
container.type,
const diff = Math.min(
textElementHeight - maxHeight,
approxLineHeight,
);
mutateElement(container, { height: targetContainerHeight });
mutateElement(container, { height: containerDims.height + diff });
return;
} else if (
// autoshrink container height until original container height
@@ -226,11 +224,11 @@ export const textWysiwyg = ({
containerDims.height > originalContainerData.height &&
textElementHeight < maxHeight
) {
const targetContainerHeight = computeContainerDimensionForBoundText(
textElementHeight,
container.type,
const diff = Math.min(
maxHeight - textElementHeight,
approxLineHeight,
);
mutateElement(container, { height: targetContainerHeight });
mutateElement(container, { height: containerDims.height - diff });
}
// Start pushing text upward until a diff of 30px (padding)
// is reached
@@ -268,44 +266,36 @@ export const textWysiwyg = ({
editable.selectionEnd = editable.value.length - diff;
}
const lines = updatedTextElement.originalText.split("\n");
const lineHeight = updatedTextElement.containerId
? approxLineHeight
: updatedTextElement.height / lines.length;
if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
textElementWidth = Math.min(textElementWidth, maxWidth);
} else {
}
// As firefox, Safari needs little higher dimensions on DOM
if (isFirefox || isSafari) {
textElementWidth += 0.5;
}
const { height: domHeight } = getDOMMetrics(
updatedTextElement.text,
getFontString(updatedTextElement),
updatedTextElement.lineHeight,
);
let lineHeight = element.lineHeight;
const fontSize = Math.floor(updatedTextElement.fontSize);
if (isSafari) {
//@ts-ignore
lineHeight =
updatedTextElement.height /
splitIntoLines(updatedTextElement.text).length /
fontSize;
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.height - viewportY) / appState.zoom.value;
Object.assign(editable.style, {
font: getFontString({
fontSize,
fontFamily: updatedTextElement.fontFamily,
}),
font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight,
lineHeight: `${lineHeight}px`,
width: `${textElementWidth}px`,
height: `${textElementHeight}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: `scale(${updatedTextElement.fontSize / fontSize})`,
transform: getTransform(
textElementWidth,
textElementHeight,
getTextElementAngle(updatedTextElement),
appState,
maxWidth,
editorMaxHeight,
),
textAlign,
verticalAlign,
color: updatedTextElement.strokeColor,
@@ -313,7 +303,6 @@ export const textWysiwyg = ({
filter: "var(--theme-filter)",
maxHeight: `${editorMaxHeight}px`,
});
editable.scrollTop = 0;
// For some reason updating font attribute doesn't set font family
// hence updating font family explicitly for test environment
if (isTestEnv()) {
@@ -400,11 +389,7 @@ export const textWysiwyg = ({
font,
getMaxContainerWidth(container!),
);
const { width, height } = measureText(
wrappedText,
font,
updatedTextElement.lineHeight,
);
const { width, height } = measureText(wrappedText, font);
editable.style.width = `${width}px`;
editable.style.height = `${height}px`;
}
-66
View File
@@ -1,66 +0,0 @@
import { API } from "../tests/helpers/api";
import { hasBoundTextElement } from "./typeChecks";
describe("Test TypeChecks", () => {
describe("Test hasBoundTextElement", () => {
it("should return true for text bindable containers with bound text", () => {
expect(
hasBoundTextElement(
API.createElement({
type: "rectangle",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeTruthy();
expect(
hasBoundTextElement(
API.createElement({
type: "ellipse",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeTruthy();
expect(
hasBoundTextElement(
API.createElement({
type: "arrow",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeTruthy();
expect(
hasBoundTextElement(
API.createElement({
type: "image",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeTruthy();
});
it("should return false for text bindable containers without bound text", () => {
expect(
hasBoundTextElement(
API.createElement({
type: "freedraw",
boundElements: [{ type: "arrow", id: "arrow-id" }],
}),
),
).toBeFalsy();
});
it("should return false for non text bindable containers", () => {
expect(
hasBoundTextElement(
API.createElement({
type: "freedraw",
boundElements: [{ type: "text", id: "text-id" }],
}),
),
).toBeFalsy();
});
});
});
+1 -2
View File
@@ -1,6 +1,5 @@
import { ROUNDNESS } from "../constants";
import { AppState } from "../types";
import { MarkNonNullable } from "../utility-types";
import {
ExcalidrawElement,
ExcalidrawTextElement,
@@ -140,7 +139,7 @@ export const hasBoundTextElement = (
element: ExcalidrawElement | null,
): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => {
return (
isTextBindableContainer(element) &&
isBindableElement(element) &&
!!element.boundElements?.some(({ type }) => type === "text")
);
};
-7
View File
@@ -6,7 +6,6 @@ import {
THEME,
VERTICAL_ALIGN,
} from "../constants";
import { MarkNonNullable, ValueOf } from "../utility-types";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid";
@@ -131,16 +130,10 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
fontSize: number;
fontFamily: FontFamilyValues;
text: string;
baseline: number;
textAlign: TextAlign;
verticalAlign: VerticalAlign;
containerId: ExcalidrawGenericElement["id"] | null;
originalText: string;
/**
* Unitless line height (aligned to W3C). To get line height in px, multiply
* with font size (using `getLineHeightInPx` helper).
*/
lineHeight: number & { _brand: "unitlessLineHeight" };
}>;
export type ExcalidrawBindableElement =
+4 -3
View File
@@ -838,9 +838,10 @@ class Collab extends PureComponent<Props, CollabState> {
/>
)}
{errorMessage && (
<ErrorDialog onClose={() => this.setState({ errorMessage: "" })}>
{errorMessage}
</ErrorDialog>
<ErrorDialog
message={errorMessage}
onClose={() => this.setState({ errorMessage: "" })}
/>
)}
</>
);
-1
View File
@@ -14,7 +14,6 @@ import { encryptData, decryptData } from "../../data/encryption";
import { MIME_TYPES } from "../../constants";
import { reconcileElements } from "../collab/reconciliation";
import { getSyncableElements, SyncableExcalidrawElement } from ".";
import { ResolutionType } from "../../utility-types";
// private
// -----------------------------------------------------------------------------
+1 -1
View File
@@ -263,7 +263,7 @@ export const loadScene = async (
await importFromBackend(id, privateKey),
localDataState?.appState,
localDataState?.elements,
{ repairBindings: true, refreshDimensions: true },
{ repairBindings: true },
);
} else {
data = restore(localDataState || null, null, null, {
+4 -4
View File
@@ -85,7 +85,6 @@ import { useAtomWithInitialValue } from "../jotai";
import { appJotaiStore } from "./app-jotai";
import "./index.scss";
import { ResolutionType } from "../utility-types";
polyfill();
@@ -673,9 +672,10 @@ const ExcalidrawWrapper = () => {
</Excalidraw>
{excalidrawAPI && <Collab excalidrawAPI={excalidrawAPI} />}
{errorMessage && (
<ErrorDialog onClose={() => setErrorMessage("")}>
{errorMessage}
</ErrorDialog>
<ErrorDialog
message={errorMessage}
onClose={() => setErrorMessage("")}
/>
)}
</div>
);
+47 -2
View File
@@ -18,8 +18,6 @@ interface Window {
EXCALIDRAW_EXPORT_SOURCE: string;
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
gtag: Function;
_paq: any[];
_mtm: any[];
}
interface CanvasRenderingContext2D {
@@ -52,6 +50,36 @@ interface Clipboard extends EventTarget {
write(data: any[]): Promise<void>;
}
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
type ValueOf<T> = T[keyof T];
type Merge<M, N> = Omit<M, keyof N> & N;
/** utility type to assert that the second type is a subtype of the first type.
* Returns the subtype. */
type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
type ResolutionType<T extends (...args: any) => any> = T extends (
...args: any
) => Promise<infer R>
? R
: any;
// https://github.com/krzkaczor/ts-essentials
type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
Required<Pick<T, RK>>;
type MarkNonNullable<T, K extends keyof T> = {
[P in K]-?: P extends K ? NonNullable<T[P]> : T[P];
} & { [P in keyof T]: T[P] };
type NonOptional<T> = Exclude<T, undefined>;
// PNG encoding/decoding
// -----------------------------------------------------------------------------
type TEXtChunk = { name: "tEXt"; data: Uint8Array };
@@ -73,6 +101,23 @@ declare module "png-chunks-extract" {
}
// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
// type getter for interface's callable type
// src: https://stackoverflow.com/a/58658851/927631
// -----------------------------------------------------------------------------
type SignatureType<T> = T extends (...args: infer R) => any ? R : never;
type CallableType<T extends (...args: any[]) => any> = (
...args: SignatureType<T>
) => ReturnType<T>;
// --------------------------------------------------------------------------—
// Type for React.forwardRef --- supply only the first generic argument T
type ForwardRef<T, P = any> = Parameters<
CallableType<React.ForwardRefRenderFunction<T, P>>
>[1];
// --------------------------------------------------------------------------—
interface Blob {
handle?: import("browser-fs-acces").FileSystemHandle;
name?: string;
-1
View File
@@ -2,7 +2,6 @@ import { AppState } from "./types";
import { ExcalidrawElement } from "./element/types";
import { isLinearElement } from "./element/typeChecks";
import { deepCopyElement } from "./element/newElement";
import { Mutable } from "./utility-types";
export interface HistoryEntry {
appState: ReturnType<typeof clearAppStatePropertiesForHistory>;
+2 -17
View File
@@ -110,7 +110,6 @@
"increaseFontSize": "Increase font size",
"unbindText": "Unbind text",
"bindText": "Bind text to the container",
"createContainerFromText": "Wrap text in a container",
"link": {
"edit": "Edit link",
"create": "Create link",
@@ -120,6 +119,7 @@
"edit": "Edit line",
"exit": "Exit line editor"
},
"elementLock": {
"lock": "Lock",
"unlock": "Unlock",
@@ -205,22 +205,7 @@
"cannotResolveCollabServer": "Couldn't connect to the collab server. Please reload the page and try again.",
"importLibraryError": "Couldn't load library",
"collabSaveFailed": "Couldn't save to the backend database. If problems persist, you should save your file locally to ensure you don't lose your work.",
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work.",
"brave_measure_text_error": {
"start": "Looks like you are using Brave browser with the",
"aggressive_block_fingerprint": "Aggressively Block Fingerprinting",
"setting_enabled": "setting enabled",
"break": "This could result in breaking the",
"text_elements": "Text Elements",
"in_your_drawings": "in your drawings",
"strongly_recommend": "We strongly recommend disabling this setting. You can follow",
"steps": "these steps",
"how": "on how to do so",
"disable_setting": " If disabling this setting doesn't fix the display of text elements, please open an",
"issue": "issue",
"write": "on our GitHub, or write us on",
"discord": "Discord"
}
"collabSaveFailed_sizeExceeded": "Couldn't save to the backend database, the canvas seems to be too big. You should save the file locally to ensure you don't lose your work."
},
"toolBar": {
"selection": "Selection",
-1
View File
@@ -12,7 +12,6 @@ import {
} from "./element/types";
import { getShapeForElement } from "./renderer/renderElement";
import { getCurvePathOps } from "./element/bounds";
import { Mutable } from "./utility-types";
export const rotate = (
x1: number,
-2
View File
@@ -15,8 +15,6 @@ Please add the latest change on the top under the correct section.
### Features
- [`ExcalidrawAPI.scrolToContent`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props/ref#scrolltocontent) has new opts object allowing you to fit viewport to content, and animate the scrolling. [#6319](https://github.com/excalidraw/excalidraw/pull/6319)
- Expose `useI18n()` hook return an object containing `t()` i18n helper and current `langCode`. You can use this in components you render as `<Excalidraw>` children to render any of our i18n locale strings. [#6224](https://github.com/excalidraw/excalidraw/pull/6224)
- [`restoreElements`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/utils/restore#restoreelements) API now takes an optional parameter `opts` which currently supports the below attributes
+1 -1
View File
@@ -64,7 +64,7 @@
"terser-webpack-plugin": "5.3.3",
"ts-loader": "9.3.1",
"typescript": "4.7.4",
"webpack": "5.76.0",
"webpack": "5.73.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.9.3",
+22 -22
View File
@@ -1393,10 +1393,10 @@ acorn-walk@^8.0.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0:
version "8.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
ajv-formats@^2.1.1:
version "2.1.1"
@@ -2032,9 +2032,9 @@ dns-equal@^1.0.0:
integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0=
dns-packet@^5.2.2:
version "5.4.0"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b"
integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==
version "5.3.1"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.3.1.tgz#eb94413789daec0f0ebe2fcc230bdc9d7c91b43d"
integrity sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw==
dependencies:
"@leichtgewicht/ip-codec" "^2.0.1"
@@ -2068,10 +2068,10 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
version "5.10.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6"
integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -3751,10 +3751,10 @@ vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
watchpack@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
@@ -3858,21 +3858,21 @@ webpack-sources@^3.2.3:
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@5.76.0:
version "5.76.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
webpack@5.73.0:
version "5.73.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
acorn "^8.7.1"
acorn "^8.4.1"
acorn-import-assertions "^1.7.6"
browserslist "^4.14.5"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.10.0"
enhanced-resolve "^5.9.3"
es-module-lexer "^0.9.0"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -3885,7 +3885,7 @@ webpack@5.76.0:
schema-utils "^3.1.0"
tapable "^2.1.1"
terser-webpack-plugin "^5.1.3"
watchpack "^2.4.0"
watchpack "^2.3.1"
webpack-sources "^3.2.3"
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
+1 -1
View File
@@ -48,7 +48,7 @@
"file-loader": "6.2.0",
"sass-loader": "13.0.2",
"ts-loader": "9.3.1",
"webpack": "5.76.0",
"webpack": "5.73.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0"
},
+46 -20
View File
@@ -1187,10 +1187,10 @@ acorn-walk@^8.0.0:
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.0.2.tgz#d4632bfc63fd93d0f15fd05ea0e984ffd3f5a8c3"
integrity sha512-+bpA9MJsHdZ4bgfDcpk0ozQyhhVct7rzOmO0s1IIr0AGGgKBljss8n2zp11rRP2wid5VGeh04CgeKzgat5/25A==
acorn@^8.0.4, acorn@^8.5.0, acorn@^8.7.1:
version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
acorn@^8.0.4, acorn@^8.4.1, acorn@^8.5.0:
version "8.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
ajv-keywords@^3.5.2:
version "3.5.2"
@@ -1383,7 +1383,18 @@ braces@^3.0.1:
dependencies:
fill-range "^7.0.1"
browserslist@^4.14.5, browserslist@^4.20.2, browserslist@^4.21.2:
browserslist@^4.14.5:
version "4.19.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.3.tgz#29b7caad327ecf2859485f696f9604214bedd383"
integrity sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==
dependencies:
caniuse-lite "^1.0.30001312"
electron-to-chromium "^1.4.71"
escalade "^3.1.1"
node-releases "^2.0.2"
picocolors "^1.0.0"
browserslist@^4.20.2, browserslist@^4.21.2:
version "4.21.2"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.2.tgz#59a400757465535954946a400b841ed37e2b4ecf"
integrity sha512-MonuOgAtUB46uP5CezYbRaYKBNt2LxP0yX+Pmj4LkcDFGkn9Cbpi83d9sCjwQDErXsIJSzY5oKGDbgOlF/LPAA==
@@ -1406,6 +1417,11 @@ call-bind@^1.0.0:
function-bind "^1.1.1"
get-intrinsic "^1.0.2"
caniuse-lite@^1.0.30001312:
version "1.0.30001312"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
caniuse-lite@^1.0.30001366:
version "1.0.30001367"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001367.tgz#2b97fe472e8fa29c78c5970615d7cd2ee414108a"
@@ -1585,15 +1601,20 @@ electron-to-chromium@^1.4.188:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.195.tgz#139b2d95a42a3f17df217589723a1deac71d1473"
integrity sha512-vefjEh0sk871xNmR5whJf9TEngX+KTKS3hOHpjoMpauKkwlGwtMz1H8IaIjAT/GNnX0TbGwAdmVoXCAzXf+PPg==
electron-to-chromium@^1.4.71:
version "1.4.75"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.75.tgz#d1ad9bb46f2f1bf432118c2be21d27ffeae82fdd"
integrity sha512-LxgUNeu3BVU7sXaKjUDD9xivocQLxFtq6wgERrutdY/yIOps3ODOZExK1jg8DTEg4U8TUCb5MLGeWFOYuxjF3Q==
emojis-list@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78"
integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0:
version "5.12.0"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634"
integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==
enhanced-resolve@^5.0.0, enhanced-resolve@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.9.3.tgz#44a342c012cbc473254af5cc6ae20ebd0aae5d88"
integrity sha512-Bq9VSor+kjvW3f9/MiiR4eE3XYgOl7/rS8lnSxbRbF3kS0B2r+Y9w5krBWxZgDxASVZbdYrn5wT4j/Wb0J9qow==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.2.0"
@@ -1990,6 +2011,11 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
node-releases@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
node-releases@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
@@ -2468,10 +2494,10 @@ util-deprecate@^1.0.2:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
watchpack@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
watchpack@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25"
integrity sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
@@ -2522,21 +2548,21 @@ webpack-sources@^3.2.3:
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@5.76.0:
version "5.76.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c"
integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==
webpack@5.73.0:
version "5.73.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.73.0.tgz#bbd17738f8a53ee5760ea2f59dce7f3431d35d38"
integrity sha512-svjudQRPPa0YiOYa2lM/Gacw0r6PvxptHj4FuEKQ2kX05ZLkjbVc5MnPs6its5j7IZljnIqSVo/OsY2X0IpHGA==
dependencies:
"@types/eslint-scope" "^3.7.3"
"@types/estree" "^0.0.51"
"@webassemblyjs/ast" "1.11.1"
"@webassemblyjs/wasm-edit" "1.11.1"
"@webassemblyjs/wasm-parser" "1.11.1"
acorn "^8.7.1"
acorn "^8.4.1"
acorn-import-assertions "^1.7.6"
browserslist "^4.14.5"
chrome-trace-event "^1.0.2"
enhanced-resolve "^5.10.0"
enhanced-resolve "^5.9.3"
es-module-lexer "^0.9.0"
eslint-scope "5.1.1"
events "^3.2.0"
@@ -2549,7 +2575,7 @@ webpack@5.76.0:
schema-utils "^3.1.0"
tapable "^2.1.1"
terser-webpack-plugin "^5.1.3"
watchpack "^2.4.0"
watchpack "^2.3.1"
webpack-sources "^3.2.3"
which@^2.0.1:
+12 -37
View File
@@ -14,7 +14,6 @@ import {
isFreeDrawElement,
isInitializedImageElement,
isArrowElement,
hasBoundTextElement,
} from "../element/typeChecks";
import {
getDiamondPoints,
@@ -40,12 +39,9 @@ import {
} from "../constants";
import { getStroke, StrokeOptions } from "perfect-freehand";
import {
getApproxLineHeight,
getBoundTextElement,
getContainerCoords,
getContainerElement,
getLineHeightInPx,
getMaxContainerHeight,
getMaxContainerWidth,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
@@ -200,6 +196,7 @@ const drawImagePlaceholder = (
size,
);
};
const drawElementOnCanvas = (
element: NonDeletedExcalidrawElement,
rc: RoughCanvas,
@@ -272,32 +269,28 @@ const drawElementOnCanvas = (
}
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
context.save();
context.font = getFontString({
...element,
fontSize: Math.floor(element.fontSize),
});
context.font = getFontString(element);
context.fillStyle = element.strokeColor;
context.textAlign = element.textAlign as CanvasTextAlign;
// Canvas does not support multiline text by default
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.containerId
? getApproxLineHeight(getFontString(element))
: element.height / lines.length;
const horizontalOffset =
element.textAlign === "center"
? element.width / 2
: element.textAlign === "right"
? element.width
: 0;
const lineHeightPx = getLineHeightInPx(
element.fontSize,
element.lineHeight,
);
const verticalOffset = element.height - element.baseline;
context.textBaseline = "bottom";
for (let index = 0; index < lines.length; index++) {
context.fillText(
lines[index],
horizontalOffset,
(index + 1) * lineHeightPx - verticalOffset,
(index + 1) * lineHeight,
);
}
context.restore();
@@ -662,7 +655,7 @@ const generateElementShape = (
}
};
const generateElementWithCanvas = (
export const generateElementWithCanvas = (
element: NonDeletedExcalidrawElement,
renderConfig: RenderConfig,
) => {
@@ -818,21 +811,6 @@ const drawElementFromCanvas = (
elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom,
elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom,
);
if (
process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX &&
hasBoundTextElement(element)
) {
const coords = getContainerCoords(element);
context.strokeStyle = "#c92a2a";
context.lineWidth = 3;
context.strokeRect(
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
getMaxContainerWidth(element) * window.devicePixelRatio,
getMaxContainerHeight(element) * window.devicePixelRatio,
);
}
}
context.restore();
@@ -1316,10 +1294,7 @@ export const renderElementToSvg = (
}) rotate(${degree} ${cx} ${cy})`,
);
const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeightPx = getLineHeightInPx(
element.fontSize,
element.lineHeight,
);
const lineHeight = element.height / lines.length;
const horizontalOffset =
element.textAlign === "center"
? element.width / 2
@@ -1337,7 +1312,7 @@ export const renderElementToSvg = (
const text = svgRoot.ownerDocument!.createElementNS(SVG_NS, "text");
text.textContent = lines[i];
text.setAttribute("x", `${horizontalOffset}`);
text.setAttribute("y", `${i * lineHeightPx}`);
text.setAttribute("y", `${i * lineHeight}`);
text.setAttribute("font-family", getFontFamilyString(element));
text.setAttribute("font-size", `${element.fontSize}px`);
text.setAttribute("fill", element.strokeColor);
@@ -119,15 +119,6 @@ Object {
"category": "element",
},
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
"category": "element",
},
},
Object {
"PanelComponent": [Function],
"contextItemLabel": "labels.ungroup",
@@ -4516,15 +4507,6 @@ Object {
"category": "element",
},
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
"category": "element",
},
},
Object {
"PanelComponent": [Function],
"contextItemLabel": "labels.ungroup",
@@ -5066,15 +5048,6 @@ Object {
"category": "element",
},
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
"category": "element",
},
},
Object {
"PanelComponent": [Function],
"contextItemLabel": "labels.ungroup",
@@ -5915,15 +5888,6 @@ Object {
"category": "element",
},
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
"category": "element",
},
},
Object {
"PanelComponent": [Function],
"contextItemLabel": "labels.ungroup",
@@ -6261,15 +6225,6 @@ Object {
"category": "element",
},
},
Object {
"contextItemLabel": "labels.createContainerFromText",
"name": "createContainerFromText",
"perform": [Function],
"predicate": [Function],
"trackEvent": Object {
"category": "element",
},
},
Object {
"PanelComponent": [Function],
"contextItemLabel": "labels.ungroup",
@@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te
class="excalidraw-wysiwyg"
data-type="wysiwyg"
dir="auto"
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
style="position: absolute; display: inline-block; min-height: 1em; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10px; height: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;"
tabindex="0"
wrap="off"
/>
-100
View File
@@ -4,7 +4,6 @@ import { UI, Pointer, Keyboard } from "./helpers/ui";
import { getTransformHandles } from "../element/transformHandles";
import { API } from "./helpers/api";
import { KEYS } from "../keys";
import { actionCreateContainerFromText } from "../actions/actionBoundText";
const { h } = window;
@@ -210,103 +209,4 @@ describe("element binding", () => {
).toBe(null);
expect(arrow.endBinding?.elementId).toBe(text.id);
});
it("should update binding when text containerized", async () => {
const rectangle1 = API.createElement({
type: "rectangle",
id: "rectangle1",
width: 100,
height: 100,
boundElements: [
{ id: "arrow1", type: "arrow" },
{ id: "arrow2", type: "arrow" },
],
});
const arrow1 = API.createElement({
type: "arrow",
id: "arrow1",
points: [
[0, 0],
[0, -87.45777932247563],
],
startBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
endBinding: {
elementId: "text1",
focus: 0.2,
gap: 7,
},
});
const arrow2 = API.createElement({
type: "arrow",
id: "arrow2",
points: [
[0, 0],
[0, -87.45777932247563],
],
startBinding: {
elementId: "text1",
focus: 0.2,
gap: 7,
},
endBinding: {
elementId: "rectangle1",
focus: 0.2,
gap: 7,
},
});
const text1 = API.createElement({
type: "text",
id: "text1",
text: "ola",
boundElements: [
{ id: "arrow1", type: "arrow" },
{ id: "arrow2", type: "arrow" },
],
});
h.elements = [rectangle1, arrow1, arrow2, text1];
API.setSelectedElements([text1]);
expect(h.state.selectedElementIds[text1.id]).toBe(true);
h.app.actionManager.executeAction(actionCreateContainerFromText);
// new text container will be placed before the text element
const container = h.elements.at(-2)!;
expect(container.type).toBe("rectangle");
expect(container.id).not.toBe(rectangle1.id);
expect(container).toEqual(
expect.objectContaining({
boundElements: expect.arrayContaining([
{
type: "text",
id: text1.id,
},
{
type: "arrow",
id: arrow1.id,
},
{
type: "arrow",
id: arrow2.id,
},
]),
}),
);
expect(arrow1.startBinding?.elementId).toBe(rectangle1.id);
expect(arrow1.endBinding?.elementId).toBe(container.id);
expect(arrow2.startBinding?.elementId).toBe(container.id);
expect(arrow2.endBinding?.elementId).toBe(rectangle1.id);
});
});
+18 -16
View File
@@ -3,10 +3,8 @@ import { render, waitFor, GlobalTestState } from "./test-utils";
import { Pointer, Keyboard } from "./helpers/ui";
import ExcalidrawApp from "../excalidraw-app";
import { KEYS } from "../keys";
import {
getDefaultLineHeight,
getLineHeightInPx,
} from "../element/textElement";
import { getApproxLineHeight } from "../element/textElement";
import { getFontString } from "../utils";
import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types";
@@ -120,10 +118,12 @@ describe("paste text as single lines", () => {
it("should space items correctly", async () => {
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
const lineHeightPx =
getLineHeightInPx(
h.app.state.currentItemFontSize,
getDefaultLineHeight(h.state.currentItemFontFamily),
const lineHeight =
getApproxLineHeight(
getFontString({
fontSize: h.app.state.currentItemFontSize,
fontFamily: h.app.state.currentItemFontFamily,
}),
) +
10 / h.app.state.zoom.value;
mouse.moveTo(100, 100);
@@ -135,17 +135,19 @@ describe("paste text as single lines", () => {
for (let i = 1; i < h.elements.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, elY] = getElementBounds(h.elements[i]);
expect(elY).toEqual(firstElY + lineHeightPx * i);
expect(elY).toEqual(firstElY + lineHeight * i);
}
});
});
it("should leave a space for blank new lines", async () => {
const text = "hkhkjhki\n\njgkjhffjh";
const lineHeightPx =
getLineHeightInPx(
h.app.state.currentItemFontSize,
getDefaultLineHeight(h.state.currentItemFontFamily),
const lineHeight =
getApproxLineHeight(
getFontString({
fontSize: h.app.state.currentItemFontSize,
fontFamily: h.app.state.currentItemFontFamily,
}),
) +
10 / h.app.state.zoom.value;
mouse.moveTo(100, 100);
@@ -156,7 +158,7 @@ describe("paste text as single lines", () => {
const [fx, firstElY] = getElementBounds(h.elements[0]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [lx, lastElY] = getElementBounds(h.elements[1]);
expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
expect(lastElY).toEqual(firstElY + lineHeight * 2);
});
});
});
@@ -222,7 +224,7 @@ describe("Paste bound text container", () => {
await sleep(1);
expect(h.elements.length).toEqual(2);
const container = h.elements[0];
expect(container.height).toBe(368);
expect(container.height).toBe(354);
expect(container.width).toBe(166);
});
});
@@ -245,7 +247,7 @@ describe("Paste bound text container", () => {
await sleep(1);
expect(h.elements.length).toEqual(2);
const container = h.elements[0];
expect(container.height).toBe(770);
expect(container.height).toBe(740);
expect(container.width).toBe(166);
});
});
@@ -282,7 +282,6 @@ exports[`restoreElements should restore text element correctly passing value for
Object {
"angle": 0,
"backgroundColor": "transparent",
"baseline": 0,
"boundElements": Array [],
"containerId": null,
"fillStyle": "hachure",
@@ -292,7 +291,6 @@ Object {
"height": 100,
"id": "id-text01",
"isDeleted": false,
"lineHeight": 1.25,
"link": null,
"locked": false,
"opacity": 100,
@@ -314,7 +312,7 @@ Object {
"verticalAlign": "middle",
"width": 100,
"x": -20,
"y": -8.75,
"y": -8.4,
}
`;
@@ -322,7 +320,6 @@ exports[`restoreElements should restore text element correctly with unknown font
Object {
"angle": 0,
"backgroundColor": "transparent",
"baseline": 0,
"boundElements": Array [],
"containerId": null,
"fillStyle": "hachure",
@@ -332,7 +329,6 @@ Object {
"height": 100,
"id": "id-text01",
"isDeleted": false,
"lineHeight": 1.25,
"link": null,
"locked": false,
"opacity": 100,
-189
View File
@@ -1,189 +0,0 @@
import { render } from "./test-utils";
import { API } from "./helpers/api";
import ExcalidrawApp from "../excalidraw-app";
const { h } = window;
describe("fitToContent", () => {
it("should zoom to fit the selected element", async () => {
await render(<ExcalidrawApp />);
h.state.width = 10;
h.state.height = 10;
const rectElement = API.createElement({
width: 50,
height: 100,
x: 50,
y: 100,
});
expect(h.state.zoom.value).toBe(1);
h.app.scrollToContent(rectElement, { fitToContent: true });
// element is 10x taller than the viewport size,
// zoom should be at least 1/10
expect(h.state.zoom.value).toBeLessThanOrEqual(0.1);
});
it("should zoom to fit multiple elements", async () => {
await render(<ExcalidrawApp />);
const topLeft = API.createElement({
width: 20,
height: 20,
x: 0,
y: 0,
});
const bottomRight = API.createElement({
width: 20,
height: 20,
x: 80,
y: 80,
});
h.state.width = 10;
h.state.height = 10;
expect(h.state.zoom.value).toBe(1);
h.app.scrollToContent([topLeft, bottomRight], {
fitToContent: true,
});
// elements take 100x100, which is 10x bigger than the viewport size,
// zoom should be at least 1/10
expect(h.state.zoom.value).toBeLessThanOrEqual(0.1);
});
it("should scroll the viewport to the selected element", async () => {
await render(<ExcalidrawApp />);
h.state.width = 10;
h.state.height = 10;
const rectElement = API.createElement({
width: 100,
height: 100,
x: 100,
y: 100,
});
expect(h.state.zoom.value).toBe(1);
expect(h.state.scrollX).toBe(0);
expect(h.state.scrollY).toBe(0);
h.app.scrollToContent(rectElement);
// zoom level should stay the same
expect(h.state.zoom.value).toBe(1);
// state should reflect some scrolling
expect(h.state.scrollX).not.toBe(0);
expect(h.state.scrollY).not.toBe(0);
});
});
const waitForNextAnimationFrame = () => {
return new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve);
});
});
};
describe("fitToContent animated", () => {
beforeEach(() => {
jest.spyOn(window, "requestAnimationFrame");
});
afterEach(() => {
jest.restoreAllMocks();
});
it("should ease scroll the viewport to the selected element", async () => {
await render(<ExcalidrawApp />);
h.state.width = 10;
h.state.height = 10;
const rectElement = API.createElement({
width: 100,
height: 100,
x: -100,
y: -100,
});
h.app.scrollToContent(rectElement, { animate: true });
expect(window.requestAnimationFrame).toHaveBeenCalled();
// Since this is an animation, we expect values to change through time.
// We'll verify that the scroll values change at 50ms and 100ms
expect(h.state.scrollX).toBe(0);
expect(h.state.scrollY).toBe(0);
await waitForNextAnimationFrame();
const prevScrollX = h.state.scrollX;
const prevScrollY = h.state.scrollY;
expect(h.state.scrollX).not.toBe(0);
expect(h.state.scrollY).not.toBe(0);
await waitForNextAnimationFrame();
expect(h.state.scrollX).not.toBe(prevScrollX);
expect(h.state.scrollY).not.toBe(prevScrollY);
});
it("should animate the scroll but not the zoom", async () => {
await render(<ExcalidrawApp />);
h.state.width = 50;
h.state.height = 50;
const rectElement = API.createElement({
width: 100,
height: 100,
x: 100,
y: 100,
});
expect(h.state.scrollX).toBe(0);
expect(h.state.scrollY).toBe(0);
h.app.scrollToContent(rectElement, { animate: true, fitToContent: true });
expect(window.requestAnimationFrame).toHaveBeenCalled();
// Since this is an animation, we expect values to change through time.
// We'll verify that the zoom/scroll values change in each animation frame
// zoom is not animated, it should be set to its final value, which in our
// case zooms out to 50% so that th element is fully visible (it's 2x large
// as the canvas)
expect(h.state.zoom.value).toBeLessThanOrEqual(0.5);
// FIXME I think this should be [-100, -100] so we may have a bug in our zoom
// hadnling, alas
expect(h.state.scrollX).toBe(25);
expect(h.state.scrollY).toBe(25);
await waitForNextAnimationFrame();
const prevScrollX = h.state.scrollX;
const prevScrollY = h.state.scrollY;
expect(h.state.scrollX).not.toBe(0);
expect(h.state.scrollY).not.toBe(0);
await waitForNextAnimationFrame();
expect(h.state.scrollX).not.toBe(prevScrollX);
expect(h.state.scrollY).not.toBe(prevScrollY);
});
});
+2 -12
View File
@@ -19,7 +19,6 @@ import { newFreeDrawElement, newImageElement } from "../../element/newElement";
import { Point } from "../../types";
import { getSelectedElements } from "../../scene/selection";
import { isLinearElementType } from "../../element/typeChecks";
import { Mutable } from "../../utility-types";
const readFile = util.promisify(fs.readFile);
@@ -111,9 +110,6 @@ export class API {
fileId?: T extends "image" ? string : never;
scale?: T extends "image" ? ExcalidrawImageElement["scale"] : never;
status?: T extends "image" ? ExcalidrawImageElement["status"] : never;
startBinding?: T extends "arrow"
? ExcalidrawLinearElement["startBinding"]
: never;
endBinding?: T extends "arrow"
? ExcalidrawLinearElement["endBinding"]
: never;
@@ -181,13 +177,11 @@ export class API {
});
break;
case "text":
const fontSize = rest.fontSize ?? appState.currentItemFontSize;
const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily;
element = newTextElement({
...base,
text: rest.text || "test",
fontSize,
fontFamily,
fontSize: rest.fontSize ?? appState.currentItemFontSize,
fontFamily: rest.fontFamily ?? appState.currentItemFontFamily,
textAlign: rest.textAlign ?? appState.currentItemTextAlign,
verticalAlign: rest.verticalAlign ?? DEFAULT_VERTICAL_ALIGN,
containerId: rest.containerId ?? undefined,
@@ -226,10 +220,6 @@ export class API {
});
break;
}
if (element.type === "arrow") {
element.startBinding = rest.startBinding ?? null;
element.endBinding = rest.endBinding ?? null;
}
if (id) {
element.id = id;
}
+7 -19
View File
@@ -1031,7 +1031,7 @@ describe("Test Linear Elements", () => {
expect({ width: container.width, height: container.height })
.toMatchInlineSnapshot(`
Object {
"height": 130,
"height": 128,
"width": 367,
}
`);
@@ -1040,7 +1040,7 @@ describe("Test Linear Elements", () => {
.toMatchInlineSnapshot(`
Object {
"x": 272,
"y": 45,
"y": 46,
}
`);
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
@@ -1052,11 +1052,11 @@ describe("Test Linear Elements", () => {
.toMatchInlineSnapshot(`
Array [
20,
35,
36,
502,
95,
94,
205.9061448421403,
52.5,
53,
]
`);
});
@@ -1090,7 +1090,7 @@ describe("Test Linear Elements", () => {
expect({ width: container.width, height: container.height })
.toMatchInlineSnapshot(`
Object {
"height": 130,
"height": 128,
"width": 340,
}
`);
@@ -1099,7 +1099,7 @@ describe("Test Linear Elements", () => {
.toMatchInlineSnapshot(`
Object {
"x": 75,
"y": -5,
"y": -4,
}
`);
expect(textElement.text).toMatchInlineSnapshot(`
@@ -1179,17 +1179,5 @@ describe("Test Linear Elements", () => {
easy"
`);
});
it("should not render horizontal align tool when element selected", () => {
createTwoPointerLinearElement("arrow");
const arrow = h.elements[0] as ExcalidrawLinearElement;
createBoundTextElement(DEFAULT_TEXT, arrow);
API.setSelectedElements([arrow]);
expect(queryByTestId(container, "align-left")).toBeNull();
expect(queryByTestId(container, "align-horizontal-center")).toBeNull();
expect(queryByTestId(container, "align-right")).toBeNull();
});
});
});
-30
View File
@@ -1,30 +0,0 @@
import { KEYS } from "../keys";
import { Excalidraw } from "../packages/excalidraw/entry";
import { API } from "./helpers/api";
import { Keyboard } from "./helpers/ui";
import { fireEvent, render, waitFor } from "./test-utils";
describe("shortcuts", () => {
it("Clear canvas shortcut should display confirm dialog", async () => {
await render(
<Excalidraw
initialData={{ elements: [API.createElement({ type: "rectangle" })] }}
handleKeyboardGlobally
/>,
);
expect(window.h.elements.length).toBe(1);
Keyboard.withModifierKeys({ ctrl: true }, () => {
Keyboard.keyDown(KEYS.DELETE);
});
const confirmDialog = document.querySelector(".confirm-dialog")!;
expect(confirmDialog).not.toBe(null);
fireEvent.click(confirmDialog.querySelector('[aria-label="Confirm"]')!);
await waitFor(() => {
expect(window.h.elements[0].isDeleted).toBe(true);
});
});
});
+1 -3
View File
@@ -31,8 +31,6 @@ import Library from "./data/library";
import type { FileSystemHandle } from "./data/filesystem";
import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef } from "./utility-types";
import React from "react";
export type Point = Readonly<RoughPoint>;
@@ -102,7 +100,7 @@ export type AppState = {
} | null;
showWelcomeScreen: boolean;
isLoading: boolean;
errorMessage: React.ReactNode;
errorMessage: string | null;
draggingElement: NonDeletedExcalidrawElement | null;
resizingElement: NonDeletedExcalidrawElement | null;
multiElement: NonDeleted<ExcalidrawLinearElement> | null;
-49
View File
@@ -1,49 +0,0 @@
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
export type ValueOf<T> = T[keyof T];
export type Merge<M, N> = Omit<M, keyof N> & N;
/** utility type to assert that the second type is a subtype of the first type.
* Returns the subtype. */
export type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype;
export type ResolutionType<T extends (...args: any) => any> = T extends (
...args: any
) => Promise<infer R>
? R
: any;
// https://github.com/krzkaczor/ts-essentials
export type MarkOptional<T, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>>;
export type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> &
Required<Pick<T, RK>>;
export type MarkNonNullable<T, K extends keyof T> = {
[P in K]-?: P extends K ? NonNullable<T[P]> : T[P];
} & { [P in keyof T]: T[P] };
export type NonOptional<T> = Exclude<T, undefined>;
// -----------------------------------------------------------------------------
// type getter for interface's callable type
// src: https://stackoverflow.com/a/58658851/927631
// -----------------------------------------------------------------------------
export type SignatureType<T> = T extends (...args: infer R) => any ? R : never;
export type CallableType<T extends (...args: any[]) => any> = (
...args: SignatureType<T>
) => ReturnType<T>;
// --------------------------------------------------------------------------—
// Type for React.forwardRef --- supply only the first generic argument T
export type ForwardRef<T, P = any> = Parameters<
CallableType<React.ForwardRefRenderFunction<T, P>>
>[1];
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
? U
: never;
+5 -75
View File
@@ -16,7 +16,6 @@ import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom";
import { SHAPES } from "./shapes";
import { isEraserActive, isHandToolActive } from "./appState";
import { ResolutionType } from "./utility-types";
let mockDateTime: string | null = null;
@@ -181,79 +180,6 @@ export const throttleRAF = <T extends any[]>(
return ret;
};
/**
* Exponential ease-out method
*
* @param {number} k - The value to be tweened.
* @returns {number} The tweened value.
*/
function easeOut(k: number): number {
return 1 - Math.pow(1 - k, 4);
}
/**
* Compute new values based on the same ease function and trigger the
* callback through a requestAnimationFrame call
*
* use `opts` to define a duration and/or an easeFn
*
* for example:
* ```ts
* easeToValuesRAF([10, 20, 10], [0, 0, 0], (a, b, c) => setState(a,b, c))
* ```
*
* @param fromValues The initial values, must be numeric
* @param toValues The destination values, must also be numeric
* @param callback The callback receiving the values
* @param opts default to 250ms duration and the easeOut function
*/
export const easeToValuesRAF = (
fromValues: number[],
toValues: number[],
callback: (...values: number[]) => void,
opts?: { duration?: number; easeFn?: (value: number) => number },
) => {
let canceled = false;
let frameId = 0;
let startTime: number;
const duration = opts?.duration || 250; // default animation to 0.25 seconds
const easeFn = opts?.easeFn || easeOut; // default the easeFn to easeOut
function step(timestamp: number) {
if (canceled) {
return;
}
if (startTime === undefined) {
startTime = timestamp;
}
const elapsed = timestamp - startTime;
if (elapsed < duration) {
// console.log(elapsed, duration, elapsed / duration);
const factor = easeFn(elapsed / duration);
const newValues = fromValues.map(
(fromValue, index) =>
(toValues[index] - fromValue) * factor + fromValue,
);
callback(...newValues);
frameId = window.requestAnimationFrame(step);
} else {
// ensure final values are reached at the end of the transition
callback(...toValues);
}
}
frameId = window.requestAnimationFrame(step);
return () => {
canceled = true;
window.cancelAnimationFrame(frameId);
};
};
// https://github.com/lodash/lodash/blob/es/chunk.js
export const chunk = <T extends any>(
array: readonly T[],
@@ -689,7 +615,11 @@ export const arrayToMapWithIndex = <T extends { id: string }>(
return acc;
}, new Map<string, [element: T, index: number]>());
export const isTestEnv = () => process.env.NODE_ENV === "test";
export const isTestEnv = () =>
typeof process !== "undefined" && process.env?.NODE_ENV === "test";
export const isProdEnv = () =>
typeof process !== "undefined" && process.env?.NODE_ENV === "production";
export const wrapEvent = <T extends Event>(name: EVENT, nativeEvent: T) => {
return new CustomEvent(name, {