kopia lustrzana https://github.com/Tldraw/Tldraw
Squashed commit of the following:
commitpull/3329/head1a642387de
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Fri Apr 5 10:56:14 2024 +0100 lint commitc5059f15bb
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Fri Apr 5 10:49:54 2024 +0100 textfields: bring shape into view when editing (#3362) This actually is a problem in production anyway, for any shape, not particular to new auto-editing or stickies. Case in point: - shape is selected - move the shape offscreen - hit Enter - you'll be editing the shape but it won't be visible to you. This change consolidates some of the duplicate logic in `Idle.ts` and in `noteHelpers.ts`. Two questions - `Idle.ts` didn't have the `select()` call but `noteHelpers.ts` did - is this really important? - `noteHelpers` didn't have the `mark()` call but `Idle.ts` did - seems like it was missing in noteHelpers, but lemme know if that was intended to be left out. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Release Notes - Textfields: bring shape to view that's being edited. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> commit7671e6291e
Merge:1df05133a
58286db90
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Fri Apr 5 10:29:41 2024 +0100 merge commit1df05133af
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Fri Apr 5 09:15:07 2024 +0100 stickies: update geometry to differentiate between shape and text label (#3361) Separates out the geometry - we want clicking in the negative space to not have the text editing mechanics. This definitely feels better without the sticky note textfield assuming it takes up the whole note. Before: <img width="614" alt="Screenshot 2024-04-04 at 15 35 58" src="https://github.com/tldraw/tldraw/assets/469604/f4673fc5-5b9a-4ab9-a169-a91ba7f49d17"> After: <img width="636" alt="Screenshot 2024-04-04 at 15 35 42" src="https://github.com/tldraw/tldraw/assets/469604/645c439a-662b-4244-af7f-6c718e5c5fc2"> ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> commit58286db90c
Author: Steve Ruiz <steveruizok@gmail.com> Date: Thu Apr 4 22:50:01 2024 +0100 Add long press event (#3275) This PR adds a "long press" event that fires when pointing for more than 500ms. This event is used in the same way that dragging is used (e.g. to transition to from pointing_selection to translating) but only on desktop. On mobile, long presses are used to open the context menu. ![Kapture 2024-03-26 at 18 57 15](https://github.com/tldraw/tldraw/assets/23072548/34a7ee2b-bde6-443b-93e0-082453a1cb61) ## Background This idea came out of @TodePond's #3208 PR. We use a "dead zone" to avoid accidentally moving / rotating things when clicking on them, which is especially common on mobile if a dead zone feature isn't implemented. However, this makes it difficult to make "fine adjustments" because you need to drag out of the dead zone (to start translating) and then drag back to where you want to go. ![Kapture 2024-03-26 at 19 00 38](https://github.com/tldraw/tldraw/assets/23072548/9a15852d-03d0-4b88-b594-27dbd3b68780) With this change, you can long press on desktop to get to that translating state. It's a micro UX optimization but especially nice if apps want to display different UI for "dragging" shapes before the user leaves the dead zone. ![Kapture 2024-03-26 at 19 02 59](https://github.com/tldraw/tldraw/assets/23072548/f0ff337e-2cbd-4b73-9ef5-9b7deaf0ae91) ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [x] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Long press shapes, selections, resize handles, rotate handles, crop handles. 2. You should enter the corresponding states, just as you would have with a drag. - [ ] Unit Tests TODO ### Release Notes - Add support for long pressing on desktop. commit4a84b3482c
Author: Steve Ruiz <steveruizok@gmail.com> Date: Thu Apr 4 22:49:28 2024 +0100 Simplify RTL commite3ceb270e7
Merge:8683c642d
43edeb09b
Author: Steve Ruiz <steveruizok@gmail.com> Date: Thu Apr 4 22:17:27 2024 +0100 Merge branch 'main' into stickies-rc commit43edeb09b5
Author: Steve Ruiz <steveruizok@gmail.com> Date: Thu Apr 4 19:16:17 2024 +0100 Add white migration (#3334) This PR adds a down migration for #3321. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `dunno` — I don't know commit8683c642d3
Author: Lu Wilson <l2wilson94@gmail.com> Date: Thu Apr 4 16:05:24 2024 +0100 Stickies: Kickout occluded shapes after nudge (frames and stickies) (#3360) This PR kicks out occluded shapes after nudging. This affects frames as well as stickies. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. commit574724be57
Author: Lu Wilson <l2wilson94@gmail.com> Date: Thu Apr 4 16:05:03 2024 +0100 Stickies: Bring stickies to front when you move them (#3353) When you start translating stickies, bring them to front. https://github.com/tldraw/tldraw/assets/15892272/bdd1bc0c-8e94-435a-98ef-d09f9f93f4cb ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. commit2ae2b6516c
Merge:a590e1ddb
0161ec796
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Thu Apr 4 15:09:00 2024 +0100 merge commita590e1ddb5
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Thu Apr 4 11:39:55 2024 +0100 textfields: fix very long words not causing growY to be calculated correctly commit00d5400e15
Author: Lu Wilson <l2wilson94@gmail.com> Date: Thu Apr 4 10:41:45 2024 +0100 Stickies: Use page space for sticky shadow rotation (#3352) This PR makes sticky shadows use page space rotation, not shape space rotation. This brings it in line with what our other shapes do (eg: bookmarks) https://github.com/tldraw/tldraw/assets/15892272/6b0ae307-7811-4a11-a29d-2c61cafd3d1d ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [ ] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. commit3c91ed78da
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Thu Apr 4 10:21:05 2024 +0100 textareas: place cursor at intended position (#3328) This is take 2 on this original PR: https://github.com/tldraw/tldraw/pull/3132 It goes further to try to address these issues that came up: - [x] fixes up problem with being able to click "through" an occluded shape because of z-layer issues. This is done by only "raising" the textareas when a shape is selected, priming them for easy selection. - [x] add a `data-isediting` property which lets us stay in editing mode and keep the z-indices of the textareas raised when in editing mode. - [x] Initiating a drag from within a textarea was losing pointer events when going over the UI. Would love feedback on this piece - had to disable pointer events to the UI while dragging which I think is the right move anyway. But wiring that up properly could use work since it relies on cursor changes, heh. Btw, @MitjaBezensek I had to undo your PR https://github.com/tldraw/tldraw/pull/3283 because that would conflict with this sticky work where we want text editing to be more fluid. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> commit0161ec796e
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Apr 4 09:34:07 2024 +0200 Bump the npm_and_yarn group across 1 directory with 1 update (#3348) Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 5.2.7 to 5.2.8 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's changelog</a>.</em></p> <blockquote> <h2><!-- raw HTML omitted -->5.2.8 (2024-04-03)<!-- raw HTML omitted --></h2> <ul> <li>fix: csp nonce injection when no closing tag (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16281">#16281</a>) (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16282">#16282</a>) (<a href="https://github.com/vitejs/vite/commit/3c85c6b">3c85c6b</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16281">#16281</a> <a href="https://redirect.github.com/vitejs/vite/issues/16282">#16282</a></li> <li>fix: do not access document in <code>/@vite/client</code> when not defined (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16318">#16318</a>) (<a href="https://github.com/vitejs/vite/commit/646319c">646319c</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16318">#16318</a></li> <li>fix: fix sourcemap when using object as <code>define</code> value (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15805">#15805</a>) (<a href="https://github.com/vitejs/vite/commit/445c4f2">445c4f2</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/15805">#15805</a></li> <li>fix(css): unknown file error happened with lightningcss (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16306">#16306</a>) (<a href="https://github.com/vitejs/vite/commit/01af308">01af308</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16306">#16306</a></li> <li>fix(hmr): multiple updates happened when invalidate is called while multiple tabs open (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16307">#16307</a>) (<a href="https://github.com/vitejs/vite/commit/21cc10b">21cc10b</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16307">#16307</a></li> <li>fix(scanner): duplicate modules for same id if glob is used in html-like types (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16305">#16305</a>) (<a href="https://github.com/vitejs/vite/commit/eca68fa">eca68fa</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16305">#16305</a></li> <li>chore(deps): update all non-major dependencies (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16325">#16325</a>) (<a href="https://github.com/vitejs/vite/commit/a78e265">a78e265</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16325">#16325</a></li> <li>refactor: use types from sass instead of <code>@types/sass</code> (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16340">#16340</a>) (<a href="https://github.com/vitejs/vite/commit/4581e83">4581e83</a>), closes <a href="https://redirect.github.com/vitejs/vite/issues/16340">#16340</a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="8b8d4024fb
"><code>8b8d402</code></a> release: v5.2.8</li> <li><a href="646319cc84
"><code>646319c</code></a> fix: do not access document in <code>/@vite/client</code> when not defined (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16318">#16318</a>)</li> <li><a href="445c4f2158
"><code>445c4f2</code></a> fix: fix sourcemap when using object as <code>define</code> value (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15805">#15805</a>)</li> <li><a href="a78e265822
"><code>a78e265</code></a> chore(deps): update all non-major dependencies (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16325">#16325</a>)</li> <li><a href="4581e8371d
"><code>4581e83</code></a> refactor: use types from sass instead of <code>@types/sass</code> (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16340">#16340</a>)</li> <li><a href="3c85c6b52e
"><code>3c85c6b</code></a> fix: csp nonce injection when no closing tag (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16281">#16281</a>) (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16282">#16282</a>)</li> <li><a href="21cc10bfda
"><code>21cc10b</code></a> fix(hmr): multiple updates happened when invalidate is called while multiple ...</li> <li><a href="01af308dfd
"><code>01af308</code></a> fix(css): unknown file error happened with lightningcss (<a href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/16306">#16306</a>)</li> <li><a href="eca68fa942
"><code>eca68fa</code></a> fix(scanner): duplicate modules for same id if glob is used in html-like type...</li> <li>See full diff in <a href="https://github.com/vitejs/vite/commits/v5.2.8/packages/vite">compare view</a></li> </ul> </details> <br /> [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=5.2.7&new-version=5.2.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/tldraw/tldraw/network/alerts). </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> commit3f6b385880
Author: Lu Wilson <l2wilson94@gmail.com> Date: Wed Apr 3 17:15:19 2024 +0100 Stickies: parenting (of frames and stickies) (#3297) This PR adds parenting behaviour to stickies, and also changes how parenting works on the whole. - Rescaled timings for drag and drop manager. It should feel tighter, closer to figma. https://github.com/tldraw/tldraw/assets/15892272/c36c5bba-9964-403e-a5cd-6dcb35e1fe27 - We now 'kick out' any occluded children from their parents. We manually do this after many interactions and actions. - We now only parent shapes if their geometry overlaps *as well* as your cursor dragging over. - We now hint parents when translating a child inside it. - We hide the hint if your selected shape will be kicked out of a frame/note when you let go. - We now *only* hint if a child will successfully drop inside a parent. - Removed `shouldHint` option from `onDragShapesOver`. The editor handles that now. I will add more gifs demonstrating all these cases. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [x] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [x] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 2. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> commitdf7e3c4d31
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 17:05:58 2024 +0100 Stickies: fix sticky shadows (#3345) This PR tweaks sticky shadows. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix commit1ba9cbfa2a
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 16:42:52 2024 +0100 only buffer pointer events (#3337) This PR changes our input buffering to only buffer pointer events. We were already moving in this direction with the complete / cancel flushes, now it's just more explicit. If we want to add other events into here, then we can. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix commit3f4a170968
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 16:41:56 2024 +0100 Fix blur bug in editable text (#3343) This PR fixes a bug that was introduced by #3223. There was a code path that normally used to never run (a blur event running when the shape was no longer editing) but which was being run now that shapes aren't immediately removed on pointer down. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix ### Test Plan 1. Create a sticky note 2. Begin editing the note 3. click on the canvas 4. You should be in pointing_canvas commit5d7e38a7ed
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 16:32:47 2024 +0100 Update noteHelpers.ts commite60f8d56a0
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 16:17:40 2024 +0100 Fix weird text complete (#3342) This PR fixes a weird issue where editing text notes would call blur / complete, preventing certain interactions. Before: 1. Select a sticky note 2. Start editing the note 3. pointer down on the canvas 4. Instead of being in pointing_canvas, you'll be in idle After: 4. You'll be in pointing_canvas ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `bugfix` — Bug fix commit30e605ef7e
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 15:56:56 2024 +0100 Font size adjustment migration (#3338) This PR adds migrations for font size adjustment. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [x] `improvement` — Improving existing features commit139ea35171
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 15:53:15 2024 +0100 fix colors commit4ef1fb3c70
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 15:16:26 2024 +0100 fix up long growY on some notes, we dont have a border anymore commitcf51f5c2f4
Merge:d4a162d1a
4f2cf3dee
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 15:14:50 2024 +0100 Merge branch 'main' into stickies-rc commit4f2cf3dee0
Author: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com> Date: Wed Apr 3 12:25:07 2024 +0100 Tool with child states (#3074) Adds an example of a tool with child states. I'm going over the annotations at the moment, just wanted to validate the idea in the meantime. Closes tld-2114 - [x] `documentation` — Changes to the documentation only[^2] ### Release Notes - Add an example of a tool with child states --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> commitd4a162d1ad
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 12:00:22 2024 +0100 some nit cleanup commit7ae03ca32b
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 11:52:42 2024 +0100 textfields: pull out change where textareas are present all the time commitded85e4ebf
Merge:16b84ef38
03e4c8575
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 11:12:13 2024 +0100 Merge branch 'main' into stickies-rc commit03e4c8575c
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 11:01:04 2024 +0100 textfields: fix regression with Text shape and resizing (#3333) The refactor of the textfields in this PR https://github.com/tldraw/tldraw/pull/3050 caused a regression in resizing Text shapes. (as demonstrated in this PR's video: https://github.com/tldraw/tldraw/pull/3327) We reverted that PR and now this PR updates the CSS to fix the gap that was introduced when it was refactored. ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know commit843347bde1
Author: Mime Čuvalo <mimecuvalo@gmail.com> Date: Wed Apr 3 10:59:11 2024 +0100 Revert "Fix text resizing bug (#3327)" (#3332) This reverts commit0e912fe0f2
. (The fix is more to do with a CSS regression instead of a JS fix.) ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know commit16b84ef38e
Merge:4c854f4cb
5557f6be5
Author: Steve Ruiz <steveruizok@gmail.com> Date: Wed Apr 3 10:53:20 2024 +0100 Merge branch 'main' into stickies-rc commit5557f6be5b
Author: David Sheldrick <d.j.sheldrick@gmail.com> Date: Wed Apr 3 10:31:28 2024 +0100 Revert "squish sync data events before sending them out" (#3331) Reverts tldraw/tldraw#3118 commit4c854f4cb8
Author: Lu[ke] Wilson <l2wilson94@gmail.com> Date: Wed Apr 3 09:45:09 2024 +0100 fix api commit0e912fe0f2
Author: Taha <98838967+Taha-Hassan-Git@users.noreply.github.com> Date: Tue Apr 2 17:22:58 2024 +0100 Fix text resizing bug (#3327) Fixes a bug with text resizing on text shapes, now the transform origin is set depending on the alignment. ![2024-04-02 at 16 50 49 - Aqua Snail](https://github.com/tldraw/tldraw/assets/98838967/86b59691-e950-4367-8632-03ae6dfef7f6) ![2024-04-02 at 16 49 37 - Teal Tuna](https://github.com/tldraw/tldraw/assets/98838967/6b6c97a8-fc53-45a0-8282-6bd63e77507b) ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Make a text shape 2. resize it 3. It should stay within the bounds ### Release Notes - Fixes an issue with text shapes overflowing their bounds when resized. commit584380ba8b
Author: Mitja Bezenšek <mitja.bezensek@gmail.com> Date: Tue Apr 2 16:29:14 2024 +0200 Input buffering (#3223) This PR buffs input events. ## The story so far In the olde days, we throttled events from the canvas events hook so that a pointer event would only be sent every 1/60th of a second. This was fine but made drawing on the iPad / 120FPS displays a little sad. Then we removed this throttle. It seemed fine! Drawing at 120FPS was great. We improved some rendering speeds and tightened some loops so that the engine could keep up with 2x the number of points in a line. Then we started noticing that iPads and other screens could start choking on events as it received new inputs and tried to process and render inputs while still recovering from a previous dropped frame. Even worse, on iPad the work of rendering at 120FPS was causing the browser to throttle the app after some sustained drawing. Yikes! ### Batching I did an experimental PR (#3180) to bring back batching but do it in the editor instead. What we would do is: rather than immediately processing an event when we get it, we would instead put the event into a buffer. On the next 60FPS tick, we would flush the buffer and process all of the events. We'd have them all in the same transaction so that the app would only render once. ### Render batching? We then tried batching the renders, so that the app would only ever render once per (next) frame. This added a bunch of complexity around events that needed to happen synchronously, such as writing text in a text field. Some inputs could "lag" in a way familiar to anyone who's tried to update an input's state asynchronously. So we backed out of this. ### Coalescing? Another idea from @ds300 was to "coalesce" the events. This would be useful because, while some interactions like drawing would require the in-between frames in order to avoid data loss, most interactions (like resizing) didn't actually need the in-between frames, they could just use the last input of a given type. Coalescing turned out to be trickier than we thought, though. Often a state node required information from elsewhere in the app when processing an event (such as camera position or page point, which is derived from the camera position), and so the coalesced events would need to also include this information or else the handlers wouldn't work the way they should when processing the "final" event during a tick. So we backed out of the coalescing strategy for now. Here's the [PR that removes](937469d69d
) it. ### Let's just buffer the fuckers So this PR now should only include input buffering. I think there are ways to achieve the same coalescing-like results through the state nodes, which could gather information during the `onPointerMove` handler and then actually make changes during the `onTick` handler, so that the changes are only done as many time as necessary. This should help with e.g. resizing lots of shapes at once. But first let's land the buffering! --- Mitja's original text: This PR builds on top of Steve's [experiment PR](https://github.com/tldraw/tldraw/pull/3180) here. It also adds event coalescing for [`pointerMove` events](https://github.com/tldraw/tldraw/blob/mitja/input-buffering/packages/editor/src/lib/editor/Editor.ts#L8364-L8368). The API is [somewhat similar ](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/getCoalescedEvents) to `getCoalescedEvent`. In `StateNodes` we register an `onPointerMove` handler. When the event happens it gets called with the event `info`. There's now an additional field on `TLMovePointerEvent` called `coalescedInfo` which includes all the events. It's then on the user to process all of these. I decided on this API since it allows us to only expose one event handler, but it still gives the users access to all events if they need them. We would otherwise either need to: - Expose two events (coalesced and non-coalesced one and complicate the api) so that state nodes like Resizing would not be triggered for each pointer move. - Offer some methods on the editor that would allow use to get the coalesced information. Then the nodes that need that info could request it. I [tried this](9ad973da3a (diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR67)
), but it didn't feel good. This also complicated the editor inputs. The events need to store information about the event (like the mouse position when the event happened for `onPointerMove`). But we cannot immediately update inputs when the event happens. To make this work for `pointerMove` events I've added `pagePoint`. It's [calculated](https://github.com/tldraw/tldraw/pull/3223/files#diff-980beb0aa0ee9aa6d1cd386cef3dc05a500c030638ffb58d45fd11b79126103fR71) when the event triggers and then consumers can get it straight from the event (like [Drawing](https://github.com/tldraw/tldraw/pull/3223/files#diff-32f1de9a5a9ec72aa49a8d18a237fbfff301610f4689a4af6b37f47af435aafcR104)). ### Change Type <!-- ❗ Please select a 'Scope' label ❗️ --> - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff <!-- ❗ Please select a 'Type' label ❗️ --> - [ ] `bugfix` — Bug fix - [ ] `feature` — New feature - [x] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know ### Test Plan 1. Add a step-by-step description of how to test your PR here. 4. - [ ] Unit Tests - [ ] End to end tests ### Release Notes - Add a brief release note for your PR here. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com> commitb87f007df4
Merge:5ee01f1c0
d96b69394
Author: Steve Ruiz <steveruizok@gmail.com> Date: Tue Apr 2 14:45:46 2024 +0100 Merge branch 'stickies-rc' of https://github.com/tldraw/tldraw into stickies-rc commit5ee01f1c07
Author: Steve Ruiz <steveruizok@gmail.com> Date: Tue Apr 2 14:45:38 2024 +0100 highest one first
rodzic
82c3be1619
commit
316ec7d5c0
|
@ -21,6 +21,7 @@ test.describe('Canvas events', () => {
|
|||
await page.mouse.move(200, 200) // to kill any double clicks
|
||||
await page.mouse.move(100, 100)
|
||||
await page.mouse.down()
|
||||
await page.waitForTimeout(20)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
|
@ -46,6 +47,7 @@ test.describe('Canvas events', () => {
|
|||
await page.mouse.down()
|
||||
await page.mouse.move(101, 101)
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(20)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
|
@ -118,6 +120,7 @@ test.describe('Shape events', () => {
|
|||
test('pointer down', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
await page.mouse.down()
|
||||
await page.waitForTimeout(20)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
|
@ -128,6 +131,7 @@ test.describe('Shape events', () => {
|
|||
test('pointer move', async () => {
|
||||
await page.mouse.move(51, 51)
|
||||
await page.mouse.move(52, 52)
|
||||
await page.waitForTimeout(20)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
|
@ -139,6 +143,7 @@ test.describe('Shape events', () => {
|
|||
await page.mouse.move(51, 51)
|
||||
await page.mouse.down()
|
||||
await page.mouse.up()
|
||||
await page.waitForTimeout(20)
|
||||
expect(await page.evaluate(() => __tldraw_editor_events.at(-1))).toMatchObject({
|
||||
target: 'canvas',
|
||||
type: 'pointer',
|
||||
|
|
|
@ -112,6 +112,7 @@ test.describe('Shape Tools', () => {
|
|||
|
||||
// Click on the page
|
||||
await page.mouse.click(200, 200)
|
||||
await page.waitForTimeout(20)
|
||||
|
||||
// We should have a corresponding shape in the page
|
||||
expect(await getAllShapeTypes(page)).toEqual([shape])
|
||||
|
@ -119,6 +120,7 @@ test.describe('Shape Tools', () => {
|
|||
// Reset for next time
|
||||
await page.mouse.click(50, 50) // to ensure we're not focused
|
||||
await page.keyboard.press('v') // go to the select tool
|
||||
await page.waitForTimeout(20)
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
}
|
||||
|
@ -156,6 +158,7 @@ test.describe('Shape Tools', () => {
|
|||
// Reset for next time
|
||||
await page.mouse.click(50, 50) // to ensure we're not focused
|
||||
await page.keyboard.press('v')
|
||||
await page.waitForTimeout(20)
|
||||
await page.keyboard.press('Control+a')
|
||||
await page.keyboard.press('Backspace')
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { ShapePropsType } from '@tldraw/tlschema/src/shapes/TLBaseShape'
|
||||
import {
|
||||
DefaultColorStyle,
|
||||
DefaultFontStyle,
|
||||
|
@ -9,6 +8,7 @@ import {
|
|||
Geometry2d,
|
||||
LABEL_FONT_SIZES,
|
||||
Polygon2d,
|
||||
ShapePropsType,
|
||||
ShapeUtil,
|
||||
T,
|
||||
TEXT_PROPS,
|
||||
|
@ -20,11 +20,11 @@ import {
|
|||
TextLabel,
|
||||
Vec,
|
||||
ZERO_INDEX_KEY,
|
||||
getDefaultColorTheme,
|
||||
resizeBox,
|
||||
structuredClone,
|
||||
vecModelValidator,
|
||||
} from 'tldraw'
|
||||
import { useDefaultColorTheme } from 'tldraw/src/lib/shapes/shared/ShapeFill'
|
||||
import { getSpeechBubbleVertices, getTailIntersectionPoint } from './helpers'
|
||||
|
||||
// Copied from tldraw/tldraw
|
||||
|
@ -176,11 +176,11 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
type,
|
||||
props: { color, font, size, align, text },
|
||||
} = shape
|
||||
const theme = getDefaultColorTheme({
|
||||
isDarkMode: this.editor.user.getIsDarkMode(),
|
||||
})
|
||||
const vertices = getSpeechBubbleVertices(shape)
|
||||
const pathData = 'M' + vertices[0] + 'L' + vertices.slice(1) + 'Z'
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const theme = useDefaultColorTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -192,7 +192,6 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
fill={'none'}
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<TextLabel
|
||||
id={id}
|
||||
type={type}
|
||||
|
@ -202,7 +201,8 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
|
|||
align={align}
|
||||
verticalAlign="start"
|
||||
text={text}
|
||||
labelColor={color}
|
||||
labelColor={theme[color].solid}
|
||||
isSelected={isSelected}
|
||||
wrap
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: Tool with child states
|
||||
component: ./ToolWithChildStatesExample.tsx
|
||||
category: shapes/tools
|
||||
priority: 2
|
||||
---
|
||||
|
||||
You can implement more complex behaviour in a custom tool by using child states
|
||||
|
||||
---
|
||||
|
||||
Tools are nodes in tldraw's state machine. They are responsible for handling user input. You can create custom tools by extending the StateNode class and overriding its methods. In this example we expand on the sticker tool from the custom tool example to show how to create a tool that can handle more complex interactions by using child states.
|
|
@ -0,0 +1,258 @@
|
|||
import {
|
||||
StateNode,
|
||||
TLEventHandlers,
|
||||
TLShapePartial,
|
||||
TLTextShape,
|
||||
Tldraw,
|
||||
createShapeId,
|
||||
} from 'tldraw'
|
||||
import 'tldraw/tldraw.css'
|
||||
|
||||
// There's a guide at the bottom of this file!
|
||||
|
||||
const OFFSET = -12
|
||||
|
||||
// [1]
|
||||
class StickerTool extends StateNode {
|
||||
static override id = 'sticker'
|
||||
static override initial = 'idle'
|
||||
static override children = () => [Idle, Pointing, Dragging]
|
||||
}
|
||||
|
||||
// [2]
|
||||
class Idle extends StateNode {
|
||||
static override id = 'idle'
|
||||
//[a]
|
||||
override onEnter = () => {
|
||||
this.editor.setCursor({ type: 'cross' })
|
||||
}
|
||||
//[b]
|
||||
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
const { editor } = this
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
|
||||
if (hitShape) {
|
||||
this.onPointerDown({
|
||||
...info,
|
||||
shape: hitShape,
|
||||
target: 'shape',
|
||||
})
|
||||
return
|
||||
}
|
||||
this.parent.transition('pointing', { shape: null })
|
||||
break
|
||||
}
|
||||
case 'shape': {
|
||||
if (editor.inputs.shiftKey) {
|
||||
editor.updateShape({
|
||||
id: info.shape.id,
|
||||
type: 'text',
|
||||
props: { text: '👻 boo!' },
|
||||
})
|
||||
} else {
|
||||
this.parent.transition('pointing', { shape: info.shape })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
//[c]
|
||||
override onDoubleClick: TLEventHandlers['onDoubleClick'] = (info) => {
|
||||
const { editor } = this
|
||||
if (info.phase !== 'up') return
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
const hitShape = editor.getShapeAtPoint(editor.inputs.currentPagePoint)
|
||||
|
||||
if (hitShape) {
|
||||
this.onDoubleClick({
|
||||
...info,
|
||||
shape: hitShape,
|
||||
target: 'shape',
|
||||
})
|
||||
return
|
||||
}
|
||||
const { currentPagePoint } = editor.inputs
|
||||
editor.createShape({
|
||||
type: 'text',
|
||||
x: currentPagePoint.x + OFFSET,
|
||||
y: currentPagePoint.y + OFFSET,
|
||||
props: { text: '❤️' },
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'shape': {
|
||||
editor.deleteShapes([info.shape.id])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [3]
|
||||
class Pointing extends StateNode {
|
||||
static override id = 'pointing'
|
||||
private shape: TLTextShape | null = null
|
||||
|
||||
override onEnter = (info: { shape: TLTextShape | null }) => {
|
||||
this.shape = info.shape
|
||||
}
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
this.parent.transition('idle')
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.parent.transition('dragging', { shape: this.shape })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [4]
|
||||
class Dragging extends StateNode {
|
||||
static override id = 'dragging'
|
||||
// [a]
|
||||
private shape: TLShapePartial | null = null
|
||||
private emojiArray = ['❤️', '🔥', '👍', '👎', '😭', '🤣']
|
||||
|
||||
// [b]
|
||||
override onEnter = (info: { shape: TLShapePartial }) => {
|
||||
const { currentPagePoint } = this.editor.inputs
|
||||
const newShape = {
|
||||
id: createShapeId(),
|
||||
type: 'text',
|
||||
x: currentPagePoint.x + OFFSET,
|
||||
y: currentPagePoint.y + OFFSET,
|
||||
props: { text: '❤️' },
|
||||
}
|
||||
if (info.shape) {
|
||||
this.shape = info.shape
|
||||
} else {
|
||||
this.editor.createShape(newShape)
|
||||
this.shape = { ...newShape }
|
||||
}
|
||||
}
|
||||
//[c]
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
this.parent.transition('idle')
|
||||
}
|
||||
//[d]
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
|
||||
const { shape } = this
|
||||
const { originPagePoint, currentPagePoint } = this.editor.inputs
|
||||
const distance = originPagePoint.dist(currentPagePoint)
|
||||
if (shape) {
|
||||
this.editor.updateShape({
|
||||
id: shape.id,
|
||||
type: 'text',
|
||||
props: {
|
||||
text: this.emojiArray[Math.floor(distance / 20) % this.emojiArray.length],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [5]
|
||||
const customTools = [StickerTool]
|
||||
export default function ToolWithChildStatesExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
// Pass in the array of custom tool classes
|
||||
tools={customTools}
|
||||
// Set the initial state to the sticker tool
|
||||
initialState="sticker"
|
||||
// hide the ui
|
||||
hideUi
|
||||
// Put some helpful text on the canvas
|
||||
onMount={(editor) => {
|
||||
editor.createShape({
|
||||
type: 'text',
|
||||
x: 50,
|
||||
y: 50,
|
||||
props: {
|
||||
text: '-Double click the canvas to add a sticker\n-Double click a sticker to delete it\n-Click and drag on a sticker to change it\n-Click and drag on the canvas to create a sticker\n-Shift click a sticker for a surprise!',
|
||||
size: 's',
|
||||
align: 'start',
|
||||
},
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Introduction:
|
||||
|
||||
Tools are nodes in tldraw's state machine. They are responsible for handling user input.
|
||||
You can create custom tools by extending the `StateNode` class and overriding its
|
||||
methods. In this example we expand on the sticker tool from the custom tool example to
|
||||
show how to create a tool that can handle more complex interactions by using child states.
|
||||
|
||||
[1]
|
||||
This is our custom tool. It has three child states: `Idle`, `Pointing`, and `Dragging`.
|
||||
We need to define the `id` and `initial` properties, the id is a unique string that
|
||||
identifies the tool to the editor, and the initial property is the initial state of the
|
||||
tool. We also need to define a `children` method that returns an array of the tool's
|
||||
child states.
|
||||
|
||||
[2]
|
||||
This is our Idle state. It is the initial state of the tool. It's job is to figure out
|
||||
what the user is trying to do and transition to the appropriate state. When transitioning
|
||||
between states we can use the second argument to pass data to the new state. It has three
|
||||
methods:
|
||||
|
||||
[a] `onEnter`
|
||||
When entering any state, the `onEnter` method is called. In this case, we set the cursor to
|
||||
a crosshair.
|
||||
|
||||
[b] `onPointerDown`
|
||||
This method is called when the user presses the mouse button. The target parameter is always
|
||||
the canvas, so we can use an editor method to check if we're over a shape, and call the
|
||||
method again with the shape as the target. If we are over a shape, we transition to the
|
||||
`pointing` state with the shape in the info object. If we're over a shape and holding the
|
||||
shift key, we update the shape's text. If we're over the canvas, we transition to the
|
||||
`pointing` state with a null shape in the info object.
|
||||
|
||||
[c] `onDoubleClick`
|
||||
This method is called when the user double clicks the mouse button. We're using some similar
|
||||
logic here to check if we're over a shape, and if we are, we delete it. If we're over the canvas,
|
||||
we create a new shape.
|
||||
|
||||
[3]
|
||||
This is our `Pointing` state. It's a transitionary state, we use it to store the shape we're pointing
|
||||
at, and transition to the dragging state if the user starts dragging. It has three methods:
|
||||
|
||||
[a] `onEnter`
|
||||
When entering this state, we store the shape we're pointing at by getting it from the info object.
|
||||
|
||||
[b] `onPointerUp`
|
||||
This method is called when the user releases the mouse button. We transition to the `idle` state.
|
||||
|
||||
[c] `onPointerMove`
|
||||
This method is called when the user moves the mouse. If the user starts dragging, we transition to
|
||||
the `dragging` state and pass the shape we're pointing at.
|
||||
|
||||
[4]
|
||||
This is our `Dragging` state. It's responsible for creating and updating the shape that the user is
|
||||
dragging.
|
||||
|
||||
[a] `onEnter`
|
||||
When entering this state, we create a new shape if we're not dragging an existing one. If we are,
|
||||
we store the shape we're dragging.
|
||||
|
||||
[b] `onPointerUp`
|
||||
This method is called when the user releases the mouse button. We transition to the `idle` state.
|
||||
|
||||
[c] `onPointerMove`
|
||||
This method is called when the user moves the mouse. We use the distance between the origin and
|
||||
current mouse position to cycle through an array of emojis and update the shape's text.
|
||||
|
||||
[5]
|
||||
We pass our custom tool to the `Tldraw` component as an array. We also set the initial state to our
|
||||
custom tool. For the purposes of this demo, we're also hiding the UI and adding some helpful text to
|
||||
the canvas.
|
||||
*/
|
|
@ -1,8 +1,7 @@
|
|||
[
|
||||
{
|
||||
"locale": "ar",
|
||||
"label": "عربي",
|
||||
"isRTL": true
|
||||
"label": "عربي"
|
||||
},
|
||||
{
|
||||
"locale": "ca",
|
||||
|
@ -30,8 +29,7 @@
|
|||
},
|
||||
{
|
||||
"locale": "fa",
|
||||
"label": "فارسی",
|
||||
"isRTL": true
|
||||
"label": "فارسی"
|
||||
},
|
||||
{
|
||||
"locale": "fi",
|
||||
|
@ -47,8 +45,7 @@
|
|||
},
|
||||
{
|
||||
"locale": "he",
|
||||
"label": "עברית",
|
||||
"isRTL": true
|
||||
"label": "עברית"
|
||||
},
|
||||
{
|
||||
"locale": "hr",
|
||||
|
@ -68,8 +65,7 @@
|
|||
},
|
||||
{
|
||||
"locale": "ku",
|
||||
"label": "کوردی",
|
||||
"isRTL": true
|
||||
"label": "کوردی"
|
||||
},
|
||||
{
|
||||
"locale": "hi-in",
|
||||
|
|
|
@ -702,6 +702,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
getInstanceState(): TLInstance;
|
||||
getIsMenuOpen(): boolean;
|
||||
getOnlySelectedShape(): null | TLShape;
|
||||
getOnlySelectedShapeId(): null | TLShapeId;
|
||||
getOpenMenus(): string[];
|
||||
getOutermostSelectableShape(shape: TLShape | TLShapeId, filter?: (shape: TLShape) => boolean): TLShape;
|
||||
getPage(page: TLPage | TLPageId): TLPage | undefined;
|
||||
|
@ -814,7 +815,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
pointerVelocity: Vec;
|
||||
};
|
||||
interrupt(): this;
|
||||
isAncestorSelected(shape: TLShape | TLShapeId): boolean;
|
||||
isIn(path: string): boolean;
|
||||
isInAny(...paths: string[]): boolean;
|
||||
isPointInShape(shape: TLShape | TLShapeId, point: VecLike, opts?: {
|
||||
|
@ -1446,6 +1446,9 @@ export class Polygon2d extends Polyline2d {
|
|||
});
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function polygonIntersectsPolyline(polygon: VecLike[], polyline: VecLike[]): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export function polygonsIntersect(a: VecLike[], b: VecLike[]): boolean;
|
||||
|
||||
|
@ -1655,9 +1658,7 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
onDoubleClickEdge?: TLOnDoubleClickHandler<Shape>;
|
||||
onDoubleClickHandle?: TLOnDoubleClickHandleHandler<Shape>;
|
||||
onDragShapesOut?: TLOnDragHandler<Shape>;
|
||||
onDragShapesOver?: TLOnDragHandler<Shape, {
|
||||
shouldHint: boolean;
|
||||
}>;
|
||||
onDragShapesOver?: TLOnDragHandler<Shape>;
|
||||
onDropShapesOver?: TLOnDragHandler<Shape>;
|
||||
onEditEnd?: TLOnEditEndHandler<Shape>;
|
||||
onHandleDrag?: TLOnHandleDragHandler<Shape>;
|
||||
|
@ -1829,6 +1830,8 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
// (undocumented)
|
||||
onKeyUp?: TLEventHandlers['onKeyUp'];
|
||||
// (undocumented)
|
||||
onLongPress?: TLEventHandlers['onLongPress'];
|
||||
// (undocumented)
|
||||
onMiddleClick?: TLEventHandlers['onMiddleClick'];
|
||||
// (undocumented)
|
||||
onPointerDown?: TLEventHandlers['onPointerDown'];
|
||||
|
@ -2149,6 +2152,8 @@ export interface TLEventHandlers {
|
|||
// (undocumented)
|
||||
onKeyUp: TLKeyboardEvent;
|
||||
// (undocumented)
|
||||
onLongPress: TLPointerEvent;
|
||||
// (undocumented)
|
||||
onMiddleClick: TLPointerEvent;
|
||||
// (undocumented)
|
||||
onPointerDown: TLPointerEvent;
|
||||
|
@ -2423,7 +2428,7 @@ export type TLPointerEventInfo = TLBaseEventInfo & {
|
|||
} & TLPointerEventTarget;
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLPointerEventName = 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click';
|
||||
export type TLPointerEventName = 'long_press' | 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click';
|
||||
|
||||
// @public (undocumented)
|
||||
export type TLPointerEventTarget = {
|
||||
|
|
|
@ -11339,6 +11339,42 @@
|
|||
"isAbstract": false,
|
||||
"name": "getOnlySelectedShape"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getOnlySelectedShapeId:member(1)",
|
||||
"docComment": "/**\n * The id of the app's only selected shape.\n *\n * @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.\n *\n * @public @readonly\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getOnlySelectedShapeId(): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "null | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getOnlySelectedShapeId"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#getOpenMenus:member(1)",
|
||||
|
@ -14578,64 +14614,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "interrupt"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#isAncestorSelected:member(1)",
|
||||
"docComment": "/**\n * Determine whether or not any of a shape's ancestors are selected.\n *\n * @param id - The id of the shape to check.\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "isAncestorSelected(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": " | "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShapeId",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShapeId:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 5,
|
||||
"endIndex": 6
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "isAncestorSelected"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "@tldraw/editor!Editor#isIn:member(1)",
|
||||
|
@ -28079,6 +28057,77 @@
|
|||
},
|
||||
"implementsTokenRanges": []
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!polygonIntersectsPolyline:function(1)",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export declare function polygonIntersectsPolyline(polygon: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", polyline: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "VecLike",
|
||||
"canonicalReference": "@tldraw/editor!VecLike:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/editor/src/lib/primitives/intersect.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 8
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "polygon",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isOptional": false
|
||||
},
|
||||
{
|
||||
"parameterName": "polyline",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 4,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"name": "polygonIntersectsPolyline"
|
||||
},
|
||||
{
|
||||
"kind": "Function",
|
||||
"canonicalReference": "@tldraw/editor!polygonsIntersect:function(1)",
|
||||
|
@ -31678,7 +31727,7 @@
|
|||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!ShapeUtil#onDragShapesOver:member",
|
||||
"docComment": "/**\n * A callback called when some other shapes are dragged over this one.\n *\n * @param shape - The shape.\n *\n * @param shapes - The shapes that are being dragged over this one.\n *\n * @returns An object specifying whether the shape should hint that it can receive the dragged shapes.\n *\n * @example\n * ```ts\n * onDragShapesOver = (shape, shapes) => {\n * \treturn { shouldHint: true }\n * }\n * ```\n *\n * @public\n */\n",
|
||||
"docComment": "/**\n * A callback called when some other shapes are dragged over this one.\n *\n * @param shape - The shape.\n *\n * @param shapes - The shapes that are being dragged over this one.\n *\n * @example\n * ```ts\n * onDragShapesOver = (shape, shapes) => {\n * \tthis.editor.reparentShapes(shapes, shape.id)\n * }\n * ```\n *\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -31691,7 +31740,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Shape, {\n shouldHint: boolean;\n }>"
|
||||
"text": "<Shape>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -34963,6 +35012,41 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!StateNode#onLongPress:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onLongPress?: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLEventHandlers",
|
||||
"canonicalReference": "@tldraw/editor!TLEventHandlers:interface"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "['onLongPress']"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": true,
|
||||
"releaseTag": "Public",
|
||||
"name": "onLongPress",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 3
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "@tldraw/editor!StateNode#onMiddleClick:member",
|
||||
|
@ -38378,6 +38462,34 @@
|
|||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/editor!TLEventHandlers#onLongPress:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onLongPress: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLPointerEvent",
|
||||
"canonicalReference": "@tldraw/editor!TLPointerEvent:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onLongPress",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "PropertySignature",
|
||||
"canonicalReference": "@tldraw/editor!TLEventHandlers#onMiddleClick:member",
|
||||
|
@ -41027,7 +41139,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click'"
|
||||
"text": "'long_press' | 'middle_click' | 'pointer_down' | 'pointer_move' | 'pointer_up' | 'right_click'"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -28,6 +28,12 @@
|
|||
--layer-shapes: 300;
|
||||
--layer-overlays: 400;
|
||||
--layer-following-indicator: 1000;
|
||||
|
||||
/* z index for text editors */
|
||||
--layer-text-container: 1;
|
||||
--layer-text-content: 3;
|
||||
--layer-text-editor: 4;
|
||||
|
||||
/* Misc */
|
||||
--tl-zoom: 1;
|
||||
|
||||
|
@ -703,7 +709,8 @@ input,
|
|||
padding: 0px;
|
||||
margin: 0px;
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
font-variant: normal;
|
||||
font-style: normal;
|
||||
pointer-events: all;
|
||||
|
@ -769,7 +776,6 @@ input,
|
|||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
pointer-events: all;
|
||||
text-rendering: auto;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
|
@ -812,6 +818,17 @@ input,
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.tl-text-content__wrapper {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.tl-text-content {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -824,9 +841,8 @@ input,
|
|||
cursor: var(--tl-cursor-text);
|
||||
}
|
||||
|
||||
.tl-text-shape__wrapper[data-isediting='false'] .tl-text-input,
|
||||
.tl-arrow-label[data-isediting='false'] .tl-text-input,
|
||||
.tl-text-label[data-isediting='false'] .tl-text-input {
|
||||
.tl-text-wrapper[data-isediting='false'] .tl-text-input,
|
||||
.tl-arrow-label[data-isediting='false'] .tl-text-input {
|
||||
opacity: 0;
|
||||
cursor: var(--tl-cursor-default);
|
||||
}
|
||||
|
@ -1028,15 +1044,9 @@ input,
|
|||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tl-text-label__inner {
|
||||
position: relative;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
min-height: auto;
|
||||
.tl-text-wrapper .tl-text-content {
|
||||
pointer-events: all;
|
||||
z-index: var(--layer-text-content);
|
||||
}
|
||||
|
||||
.tl-text-label__inner > .tl-text-content {
|
||||
|
@ -1048,7 +1058,6 @@ input,
|
|||
width: fit-content;
|
||||
border-radius: var(--radius-1);
|
||||
max-width: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.tl-text-label__inner > .tl-text-input {
|
||||
|
@ -1057,7 +1066,29 @@ input,
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.tl-text-wrapper[data-isselected='true'] .tl-text-input {
|
||||
z-index: var(--layer-text-editor);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
/* These three rules help preserve clicking into specific points in text areas *while*
|
||||
* already in edit mode when jumping from shape to shape. */
|
||||
.tl-canvas[data-iseditinganything='true'] .tl-text-wrapper:hover .tl-text-input {
|
||||
z-index: var(--layer-text-editor);
|
||||
pointer-events: all;
|
||||
}
|
||||
/* This part of the rule helps preserve the occlusion rules for the shapes so we
|
||||
* don't click on shapes that are behind other shapes.
|
||||
* One extra nuance is arrows which have weird geometry and just gets in the way.
|
||||
*/
|
||||
.tl-canvas[data-iseditinganything='true'] .tl-shape:not([data-shape-type='arrow']) {
|
||||
pointer-events: all;
|
||||
}
|
||||
/* But, re-disable the pointer-events rule for the svg container. */
|
||||
.tl-canvas[data-iseditinganything='true'] .tl-shape .tl-svg-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tl-text-label[data-textwrap='true'] > .tl-text-label__inner {
|
||||
|
@ -1111,7 +1142,7 @@ input,
|
|||
position: relative;
|
||||
height: max-content;
|
||||
width: max-content;
|
||||
pointer-events: all;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
@ -1120,24 +1151,12 @@ input,
|
|||
.tl-arrow-label .tl-arrow {
|
||||
position: relative;
|
||||
height: max-content;
|
||||
z-index: 2;
|
||||
padding: 4px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.tl-arrow-label textarea {
|
||||
z-index: 3;
|
||||
margin: 0px;
|
||||
padding: 4px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
resize: none;
|
||||
border: 0px;
|
||||
user-select: all;
|
||||
-webkit-user-select: text;
|
||||
caret-color: var(--color-text);
|
||||
border-image: none;
|
||||
/* Don't allow textarea to be zero width */
|
||||
min-width: 4px;
|
||||
}
|
||||
|
@ -1150,14 +1169,13 @@ input,
|
|||
height: 100%;
|
||||
pointer-events: all;
|
||||
opacity: 1;
|
||||
z-index: 1;
|
||||
z-index: var(--layer-text-container);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.tl-note__container > .tl-text-label {
|
||||
text-shadow: none;
|
||||
color: currentColor;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
/* --------------------- Loading -------------------- */
|
||||
|
|
|
@ -301,6 +301,7 @@ export {
|
|||
intersectPolygonBounds,
|
||||
intersectPolygonPolygon,
|
||||
linesIntersect,
|
||||
polygonIntersectsPolyline,
|
||||
polygonsIntersect,
|
||||
} from './lib/primitives/intersect'
|
||||
export {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
|
||||
/** @public */
|
||||
|
@ -6,7 +7,7 @@ export type HTMLContainerProps = React.HTMLAttributes<HTMLDivElement>
|
|||
/** @public */
|
||||
export function HTMLContainer({ children, className = '', ...rest }: HTMLContainerProps) {
|
||||
return (
|
||||
<div {...rest} className={`tl-html-container ${className}`}>
|
||||
<div {...rest} className={classNames('tl-html-container', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import classNames from 'classnames'
|
||||
import * as React from 'react'
|
||||
|
||||
/** @public */
|
||||
|
@ -6,7 +7,7 @@ export type SVGContainerProps = React.HTMLAttributes<SVGElement>
|
|||
/** @public */
|
||||
export function SVGContainer({ children, className = '', ...rest }: SVGContainerProps) {
|
||||
return (
|
||||
<svg {...rest} className={`tl-svg-container ${className}`}>
|
||||
<svg {...rest} className={classNames('tl-svg-container', className)}>
|
||||
{children}
|
||||
</svg>
|
||||
)
|
||||
|
|
|
@ -88,6 +88,11 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
const debugGeometry = useValue('debug_geometry', () => debugFlags.debugGeometry.get(), [
|
||||
debugFlags,
|
||||
])
|
||||
const isEditingAnything = useValue(
|
||||
'isEditingAnything',
|
||||
() => editor.getEditingShapeId() !== null,
|
||||
[editor]
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -95,6 +100,7 @@ export function DefaultCanvas({ className }: TLCanvasComponentProps) {
|
|||
draggable={false}
|
||||
className={classNames('tl-canvas', className)}
|
||||
data-testid="canvas"
|
||||
data-iseditinganything={isEditingAnything}
|
||||
{...events}
|
||||
>
|
||||
<svg className="tl-svg-context">
|
||||
|
|
|
@ -107,3 +107,6 @@ export const HANDLE_RADIUS = 12
|
|||
|
||||
/** @public */
|
||||
export const SIDES = ['top', 'right', 'bottom', 'left'] as const
|
||||
|
||||
/** @internal */
|
||||
export const LONG_PRESS_DURATION = 500
|
||||
|
|
|
@ -79,6 +79,7 @@ import {
|
|||
FOLLOW_CHASE_ZOOM_UNSNAP,
|
||||
HIT_TEST_MARGIN,
|
||||
INTERNAL_POINTER_IDS,
|
||||
LONG_PRESS_DURATION,
|
||||
MAX_PAGES,
|
||||
MAX_SHAPES_PER_PAGE,
|
||||
MAX_ZOOM,
|
||||
|
@ -637,7 +638,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
this.updateRenderingBounds()
|
||||
|
||||
this.on('tick', this.tick)
|
||||
this.on('tick', this._flushEventsForTick)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._tickManager.start()
|
||||
|
@ -795,6 +796,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
undo(): this {
|
||||
this._flushEventsForTick(0)
|
||||
this.history.undo()
|
||||
return this
|
||||
}
|
||||
|
@ -819,6 +821,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
redo(): this {
|
||||
this._flushEventsForTick(0)
|
||||
this.history.redo()
|
||||
return this
|
||||
}
|
||||
|
@ -1515,21 +1518,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Determine whether or not any of a shape's ancestors are selected.
|
||||
*
|
||||
* @param id - The id of the shape to check.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
isAncestorSelected(shape: TLShape | TLShapeId): boolean {
|
||||
const id = typeof shape === 'string' ? shape : shape?.id ?? null
|
||||
const _shape = this.getShape(id)
|
||||
if (!_shape) return false
|
||||
const selectedShapeIds = this.getSelectedShapeIds()
|
||||
return !!this.findShapeAncestor(_shape, (parent) => selectedShapeIds.includes(parent.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* Select one or more shapes.
|
||||
*
|
||||
|
@ -1611,11 +1599,22 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the app's only selected shape.
|
||||
*
|
||||
* @returns Null if there is no shape or more than one selected shape, otherwise the selected shape's id.
|
||||
*
|
||||
* @public
|
||||
* @readonly
|
||||
*/
|
||||
@computed getOnlySelectedShapeId(): TLShapeId | null {
|
||||
return this.getOnlySelectedShape()?.id ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* The app's only selected shape.
|
||||
*
|
||||
* @returns Null if there is no shape or more than one selected shape, otherwise the selected
|
||||
* shape.
|
||||
* @returns Null if there is no shape or more than one selected shape, otherwise the selected shape.
|
||||
*
|
||||
* @public
|
||||
* @readonly
|
||||
|
@ -2085,7 +2084,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
}
|
||||
|
||||
/** @internal */
|
||||
private _setCamera(point: VecLike): this {
|
||||
private _setCamera(point: VecLike, immediate = false): this {
|
||||
const currentCamera = this.getCamera()
|
||||
|
||||
if (currentCamera.x === point.x && currentCamera.y === point.y && currentCamera.z === point.z) {
|
||||
|
@ -2107,7 +2106,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
currentScreenPoint.y / camera.z - camera.y !== currentPagePoint.y
|
||||
) {
|
||||
// If it's changed, dispatch a pointer event
|
||||
this.dispatch({
|
||||
const event: TLPointerEventInfo = {
|
||||
type: 'pointer',
|
||||
target: 'canvas',
|
||||
name: 'pointer_move',
|
||||
|
@ -2119,7 +2118,12 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
shiftKey: this.inputs.shiftKey,
|
||||
button: 0,
|
||||
isPen: this.getInstanceState().isPenMode ?? false,
|
||||
})
|
||||
}
|
||||
if (immediate) {
|
||||
this._flushEventForTick(event)
|
||||
} else {
|
||||
this.dispatch(event)
|
||||
}
|
||||
}
|
||||
|
||||
this._tickCameraState()
|
||||
|
@ -2495,6 +2499,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
if (!this.getInstanceState().canMoveCamera) return this
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
this.setCamera({ x: cx + offset.x / cz, y: cy + offset.y / cz, z: cz }, animation)
|
||||
this._flushEventsForTick(0)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -4045,7 +4050,13 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
/** @internal */
|
||||
@computed private _getShapeMaskCache(): ComputedCache<Vec[], TLShape> {
|
||||
return this.store.createComputedCache('pageMaskCache', (shape) => {
|
||||
if (isPageId(shape.parentId)) return undefined
|
||||
// todo: Consider adding a flag for this hardcoded behaviour
|
||||
if (
|
||||
isPageId(shape.parentId) ||
|
||||
shape.type === 'note' ||
|
||||
this.findShapeAncestor(shape, (v) => v.type === 'note')
|
||||
)
|
||||
return undefined
|
||||
|
||||
const frameAncestors = this.getShapeAncestors(shape.id).filter((shape) =>
|
||||
this.isShapeOfType<TLFrameShape>(shape, 'frame')
|
||||
|
@ -5019,6 +5030,8 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const shape = currentPageShapesSorted[i]
|
||||
|
||||
if (
|
||||
// don't allow dropping on selected shapes
|
||||
this.getSelectedShapeIds().includes(shape.id) ||
|
||||
// only allow shapes that can receive children
|
||||
!this.getShapeUtil(shape).canDropShapes(shape, droppingShapes) ||
|
||||
// don't allow dropping a shape on itself or one of it's children
|
||||
|
@ -8174,7 +8187,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
|
||||
const sx = info.point.x - screenBounds.x
|
||||
const sy = info.point.y - screenBounds.y
|
||||
const sz = info.point.z
|
||||
const sz = info.point.z ?? 0.5
|
||||
|
||||
previousScreenPoint.setTo(currentScreenPoint)
|
||||
previousPagePoint.setTo(currentPagePoint)
|
||||
|
@ -8184,7 +8197,7 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
// it will be 0,0 when its actual screen position is equal
|
||||
// to screenBounds.point. This is confusing!
|
||||
currentScreenPoint.set(sx, sy)
|
||||
currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz ?? 0.5)
|
||||
currentPagePoint.set(sx / cz - cx, sy / cz - cy, sz)
|
||||
|
||||
this.inputs.isPen = info.type === 'pointer' && info.isPen
|
||||
|
||||
|
@ -8211,12 +8224,6 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
])
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
private tick = (elapsed = 0) => {
|
||||
this.dispatch({ type: 'misc', name: 'tick', elapsed })
|
||||
this.scribbles.tick(elapsed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a cancel event.
|
||||
*
|
||||
|
@ -8348,6 +8355,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
/** @internal */
|
||||
private _selectedShapeIdsAtPointerDown: TLShapeId[] = []
|
||||
|
||||
/** @internal */
|
||||
private _longPressTimeout = -1 as any
|
||||
|
||||
/** @internal */
|
||||
capturedPointerId: number | null = null
|
||||
|
||||
|
@ -8364,6 +8374,32 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
* @public
|
||||
*/
|
||||
dispatch = (info: TLEventInfo): this => {
|
||||
this._pendingEventsForNextTick.push(info)
|
||||
if (!(info.type === 'pointer' || info.type === 'wheel' || info.type === 'pinch')) {
|
||||
this._flushEventsForTick(0)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
private _pendingEventsForNextTick: TLEventInfo[] = []
|
||||
|
||||
private _flushEventsForTick = (elapsed: number) => {
|
||||
this.batch(() => {
|
||||
if (this._pendingEventsForNextTick.length > 0) {
|
||||
const events = [...this._pendingEventsForNextTick]
|
||||
this._pendingEventsForNextTick.length = 0
|
||||
for (const info of events) {
|
||||
this._flushEventForTick(info)
|
||||
}
|
||||
}
|
||||
if (elapsed > 0) {
|
||||
this.root.handleEvent({ type: 'misc', name: 'tick', elapsed })
|
||||
}
|
||||
this.scribbles.tick(elapsed)
|
||||
})
|
||||
}
|
||||
|
||||
private _flushEventForTick = (info: TLEventInfo) => {
|
||||
// prevent us from spamming similar event errors if we're crashed.
|
||||
// todo: replace with new readonly mode?
|
||||
if (this.getCrashingError()) return this
|
||||
|
@ -8371,161 +8407,266 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
const { inputs } = this
|
||||
const { type } = info
|
||||
|
||||
this.batch(() => {
|
||||
if (info.type === 'misc') {
|
||||
// stop panning if the interaction is cancelled or completed
|
||||
if (info.name === 'cancel' || info.name === 'complete') {
|
||||
this.inputs.isDragging = false
|
||||
if (info.type === 'misc') {
|
||||
// stop panning if the interaction is cancelled or completed
|
||||
if (info.name === 'cancel' || info.name === 'complete') {
|
||||
this.inputs.isDragging = false
|
||||
|
||||
if (this.inputs.isPanning) {
|
||||
this.inputs.isPanning = false
|
||||
this.updateInstanceState({
|
||||
cursor: {
|
||||
type: this._prevCursor,
|
||||
rotation: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
if (this.inputs.isPanning) {
|
||||
this.inputs.isPanning = false
|
||||
this.updateInstanceState({
|
||||
cursor: {
|
||||
type: this._prevCursor,
|
||||
rotation: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
this.root.handleEvent(info)
|
||||
return
|
||||
}
|
||||
|
||||
if (info.shiftKey) {
|
||||
clearInterval(this._shiftKeyTimeout)
|
||||
this._shiftKeyTimeout = -1
|
||||
inputs.shiftKey = true
|
||||
} else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
|
||||
this._shiftKeyTimeout = setTimeout(this._setShiftKeyTimeout, 150)
|
||||
}
|
||||
this.root.handleEvent(info)
|
||||
return
|
||||
}
|
||||
|
||||
if (info.altKey) {
|
||||
clearInterval(this._altKeyTimeout)
|
||||
this._altKeyTimeout = -1
|
||||
inputs.altKey = true
|
||||
} else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
|
||||
this._altKeyTimeout = setTimeout(this._setAltKeyTimeout, 150)
|
||||
}
|
||||
if (info.shiftKey) {
|
||||
clearInterval(this._shiftKeyTimeout)
|
||||
this._shiftKeyTimeout = -1
|
||||
inputs.shiftKey = true
|
||||
} else if (!info.shiftKey && inputs.shiftKey && this._shiftKeyTimeout === -1) {
|
||||
this._shiftKeyTimeout = setTimeout(this._setShiftKeyTimeout, 150)
|
||||
}
|
||||
|
||||
if (info.ctrlKey) {
|
||||
clearInterval(this._ctrlKeyTimeout)
|
||||
this._ctrlKeyTimeout = -1
|
||||
inputs.ctrlKey = true /** @internal */ /** @internal */ /** @internal */
|
||||
} else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
|
||||
this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150)
|
||||
}
|
||||
if (info.altKey) {
|
||||
clearInterval(this._altKeyTimeout)
|
||||
this._altKeyTimeout = -1
|
||||
inputs.altKey = true
|
||||
} else if (!info.altKey && inputs.altKey && this._altKeyTimeout === -1) {
|
||||
this._altKeyTimeout = setTimeout(this._setAltKeyTimeout, 150)
|
||||
}
|
||||
|
||||
const { originPagePoint, originScreenPoint, currentPagePoint, currentScreenPoint } = inputs
|
||||
if (info.ctrlKey) {
|
||||
clearInterval(this._ctrlKeyTimeout)
|
||||
this._ctrlKeyTimeout = -1
|
||||
inputs.ctrlKey = true /** @internal */ /** @internal */ /** @internal */
|
||||
} else if (!info.ctrlKey && inputs.ctrlKey && this._ctrlKeyTimeout === -1) {
|
||||
this._ctrlKeyTimeout = setTimeout(this._setCtrlKeyTimeout, 150)
|
||||
}
|
||||
|
||||
if (!inputs.isPointing) {
|
||||
inputs.isDragging = false
|
||||
}
|
||||
const { originPagePoint, originScreenPoint, currentPagePoint, currentScreenPoint } = inputs
|
||||
|
||||
switch (type) {
|
||||
case 'pinch': {
|
||||
if (!this.getInstanceState().canMoveCamera) return
|
||||
this._updateInputsFromEvent(info)
|
||||
if (!inputs.isPointing) {
|
||||
inputs.isDragging = false
|
||||
}
|
||||
|
||||
switch (info.name) {
|
||||
case 'pinch_start': {
|
||||
if (inputs.isPinching) return
|
||||
switch (type) {
|
||||
case 'pinch': {
|
||||
if (!this.getInstanceState().canMoveCamera) return
|
||||
clearTimeout(this._longPressTimeout)
|
||||
this._updateInputsFromEvent(info)
|
||||
|
||||
if (!inputs.isEditing) {
|
||||
this._pinchStart = this.getCamera().z
|
||||
if (!this._selectedShapeIdsAtPointerDown.length) {
|
||||
this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
|
||||
}
|
||||
switch (info.name) {
|
||||
case 'pinch_start': {
|
||||
if (inputs.isPinching) return
|
||||
|
||||
this._didPinch = true
|
||||
|
||||
inputs.isPinching = true
|
||||
|
||||
this.interrupt()
|
||||
if (!inputs.isEditing) {
|
||||
this._pinchStart = this.getCamera().z
|
||||
if (!this._selectedShapeIdsAtPointerDown.length) {
|
||||
this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
|
||||
}
|
||||
|
||||
return // Stop here!
|
||||
this._didPinch = true
|
||||
|
||||
inputs.isPinching = true
|
||||
|
||||
this.interrupt()
|
||||
}
|
||||
case 'pinch': {
|
||||
if (!inputs.isPinching) return
|
||||
|
||||
const {
|
||||
point: { z = 1 },
|
||||
delta: { x: dx, y: dy },
|
||||
} = info
|
||||
return // Stop here!
|
||||
}
|
||||
case 'pinch': {
|
||||
if (!inputs.isPinching) return
|
||||
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
const { x, y } = Vec.SubXY(info.point, screenBounds.x, screenBounds.y)
|
||||
const {
|
||||
point: { z = 1 },
|
||||
delta: { x: dx, y: dy },
|
||||
} = info
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
|
||||
const { x, y } = Vec.SubXY(info.point, screenBounds.x, screenBounds.y)
|
||||
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z))
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
|
||||
this.setCamera({
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, z))
|
||||
|
||||
this.stopCameraAnimation()
|
||||
if (this.getInstanceState().followingUserId) {
|
||||
this.stopFollowingUser()
|
||||
}
|
||||
this._setCamera(
|
||||
{
|
||||
x: cx + dx / cz - x / cz + x / zoom,
|
||||
y: cy + dy / cz - y / cz + y / zoom,
|
||||
z: zoom,
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
return // Stop here!
|
||||
}
|
||||
case 'pinch_end': {
|
||||
if (!inputs.isPinching) return this
|
||||
|
||||
inputs.isPinching = false
|
||||
const { _selectedShapeIdsAtPointerDown } = this
|
||||
this.setSelectedShapes(this._selectedShapeIdsAtPointerDown, { squashing: true })
|
||||
this._selectedShapeIdsAtPointerDown = []
|
||||
|
||||
if (this._didPinch) {
|
||||
this._didPinch = false
|
||||
this.once('tick', () => {
|
||||
if (!this._didPinch) {
|
||||
this.setSelectedShapes(_selectedShapeIdsAtPointerDown, { squashing: true })
|
||||
}
|
||||
})
|
||||
|
||||
return // Stop here!
|
||||
}
|
||||
case 'pinch_end': {
|
||||
if (!inputs.isPinching) return this
|
||||
|
||||
inputs.isPinching = false
|
||||
const { _selectedShapeIdsAtPointerDown } = this
|
||||
this.setSelectedShapes(this._selectedShapeIdsAtPointerDown, { squashing: true })
|
||||
this._selectedShapeIdsAtPointerDown = []
|
||||
|
||||
if (this._didPinch) {
|
||||
this._didPinch = false
|
||||
requestAnimationFrame(() => {
|
||||
if (!this._didPinch) {
|
||||
this.setSelectedShapes(_selectedShapeIdsAtPointerDown, { squashing: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return // Stop here!
|
||||
}
|
||||
return // Stop here!
|
||||
}
|
||||
}
|
||||
case 'wheel': {
|
||||
if (!this.getInstanceState().canMoveCamera) return
|
||||
}
|
||||
case 'wheel': {
|
||||
if (!this.getInstanceState().canMoveCamera) return
|
||||
|
||||
this._updateInputsFromEvent(info)
|
||||
this._updateInputsFromEvent(info)
|
||||
|
||||
if (this.getIsMenuOpen()) {
|
||||
// noop
|
||||
} else {
|
||||
if (inputs.ctrlKey) {
|
||||
// todo: Start or update the zoom end interval
|
||||
if (this.getIsMenuOpen()) {
|
||||
// noop
|
||||
} else {
|
||||
this.stopCameraAnimation()
|
||||
if (this.getInstanceState().followingUserId) {
|
||||
this.stopFollowingUser()
|
||||
}
|
||||
if (inputs.ctrlKey) {
|
||||
// todo: Start or update the zoom end interval
|
||||
|
||||
// If the alt or ctrl keys are pressed,
|
||||
// zoom or pan the camera and then return.
|
||||
// If the alt or ctrl keys are pressed,
|
||||
// zoom or pan the camera and then return.
|
||||
|
||||
// Subtract the top left offset from the user's point
|
||||
// Subtract the top left offset from the user's point
|
||||
|
||||
const { x, y } = this.inputs.currentScreenPoint
|
||||
const { x, y } = this.inputs.currentScreenPoint
|
||||
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz))
|
||||
const zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, cz + (info.delta.z ?? 0) * cz))
|
||||
|
||||
this.setCamera({
|
||||
this._setCamera(
|
||||
{
|
||||
x: cx + (x / zoom - x) - (x / cz - x),
|
||||
y: cy + (y / zoom - y) - (y / cz - y),
|
||||
z: zoom,
|
||||
})
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
// We want to return here because none of the states in our
|
||||
// statechart should respond to this event (a camera zoom)
|
||||
// We want to return here because none of the states in our
|
||||
// statechart should respond to this event (a camera zoom)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the camera here, which will dispatch a pointer move...
|
||||
// this will also update the pointer position, etc
|
||||
const { x: cx, y: cy, z: cz } = this.getCamera()
|
||||
this._setCamera({ x: cx + info.delta.x / cz, y: cy + info.delta.y / cz, z: cz }, true)
|
||||
|
||||
if (
|
||||
!inputs.isDragging &&
|
||||
inputs.isPointing &&
|
||||
originPagePoint.dist(currentPagePoint) >
|
||||
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
|
||||
this.getZoomLevel()
|
||||
) {
|
||||
clearTimeout(this._longPressTimeout)
|
||||
inputs.isDragging = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'pointer': {
|
||||
// If we're pinching, return
|
||||
if (inputs.isPinching) return
|
||||
|
||||
this._updateInputsFromEvent(info)
|
||||
|
||||
const { isPen } = info
|
||||
|
||||
switch (info.name) {
|
||||
case 'pointer_down': {
|
||||
this.clearOpenMenus()
|
||||
|
||||
this._longPressTimeout = setTimeout(() => {
|
||||
this.dispatch({ ...info, name: 'long_press' })
|
||||
}, LONG_PRESS_DURATION)
|
||||
|
||||
this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
|
||||
|
||||
// Firefox bug fix...
|
||||
// If it's a left-mouse-click, we store the pointer id for later user
|
||||
if (info.button === 0) {
|
||||
this.capturedPointerId = info.pointerId
|
||||
}
|
||||
|
||||
// Add the button from the buttons set
|
||||
inputs.buttons.add(info.button)
|
||||
|
||||
inputs.isPointing = true
|
||||
inputs.isDragging = false
|
||||
|
||||
if (this.getInstanceState().isPenMode) {
|
||||
if (!isPen) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (isPen) {
|
||||
this.updateInstanceState({ isPenMode: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (info.button === 5) {
|
||||
// Eraser button activates eraser
|
||||
this._restoreToolId = this.getCurrentToolId()
|
||||
this.complete()
|
||||
this.setCurrentTool('eraser')
|
||||
} else if (info.button === 1) {
|
||||
// Middle mouse pan activates panning
|
||||
if (!this.inputs.isPanning) {
|
||||
this._prevCursor = this.getInstanceState().cursor.type
|
||||
}
|
||||
|
||||
this.inputs.isPanning = true
|
||||
}
|
||||
|
||||
if (this.inputs.isPanning) {
|
||||
this.stopCameraAnimation()
|
||||
this.setCursor({ type: 'grabbing', rotation: 0 })
|
||||
return this
|
||||
}
|
||||
|
||||
originScreenPoint.setTo(currentScreenPoint)
|
||||
originPagePoint.setTo(currentPagePoint)
|
||||
break
|
||||
}
|
||||
case 'pointer_move': {
|
||||
// If the user is in pen mode, but the pointer is not a pen, stop here.
|
||||
if (!isPen && this.getInstanceState().isPenMode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the camera here, which will dispatch a pointer move...
|
||||
// this will also update the pointer position, etc
|
||||
this.pan(info.delta)
|
||||
if (this.inputs.isPanning && this.inputs.isPointing) {
|
||||
// Handle panning
|
||||
const { currentScreenPoint, previousScreenPoint } = this.inputs
|
||||
this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint))
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!inputs.isDragging &&
|
||||
|
@ -8534,272 +8675,174 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|||
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
|
||||
this.getZoomLevel()
|
||||
) {
|
||||
clearTimeout(this._longPressTimeout)
|
||||
inputs.isDragging = true
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'pointer': {
|
||||
// If we're pinching, return
|
||||
if (inputs.isPinching) return
|
||||
case 'pointer_up': {
|
||||
// Remove the button from the buttons set
|
||||
inputs.buttons.delete(info.button)
|
||||
|
||||
this._updateInputsFromEvent(info)
|
||||
inputs.isPointing = false
|
||||
inputs.isDragging = false
|
||||
|
||||
const { isPen } = info
|
||||
|
||||
switch (info.name) {
|
||||
case 'pointer_down': {
|
||||
this.clearOpenMenus()
|
||||
|
||||
this._selectedShapeIdsAtPointerDown = this.getSelectedShapeIds()
|
||||
|
||||
// Firefox bug fix...
|
||||
// If it's a left-mouse-click, we store the pointer id for later user
|
||||
if (info.button === 0) {
|
||||
this.capturedPointerId = info.pointerId
|
||||
}
|
||||
|
||||
// Add the button from the buttons set
|
||||
inputs.buttons.add(info.button)
|
||||
|
||||
inputs.isPointing = true
|
||||
inputs.isDragging = false
|
||||
|
||||
if (this.getInstanceState().isPenMode) {
|
||||
if (!isPen) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (isPen) {
|
||||
this.updateInstanceState({ isPenMode: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (info.button === 5) {
|
||||
// Eraser button activates eraser
|
||||
this._restoreToolId = this.getCurrentToolId()
|
||||
this.complete()
|
||||
this.setCurrentTool('eraser')
|
||||
} else if (info.button === 1) {
|
||||
// Middle mouse pan activates panning
|
||||
if (!this.inputs.isPanning) {
|
||||
this._prevCursor = this.getInstanceState().cursor.type
|
||||
}
|
||||
|
||||
this.inputs.isPanning = true
|
||||
}
|
||||
|
||||
if (this.inputs.isPanning) {
|
||||
this.stopCameraAnimation()
|
||||
this.updateInstanceState({
|
||||
cursor: {
|
||||
type: 'grabbing',
|
||||
rotation: 0,
|
||||
},
|
||||
})
|
||||
return this
|
||||
}
|
||||
|
||||
originScreenPoint.setTo(currentScreenPoint)
|
||||
originPagePoint.setTo(currentPagePoint)
|
||||
break
|
||||
if (this.getIsMenuOpen()) {
|
||||
// Suppressing pointerup here as <ContextMenu/> doesn't seem to do what we what here.
|
||||
return
|
||||
}
|
||||
case 'pointer_move': {
|
||||
// If the user is in pen mode, but the pointer is not a pen, stop here.
|
||||
if (!isPen && this.getInstanceState().isPenMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.inputs.isPanning && this.inputs.isPointing) {
|
||||
// Handle panning
|
||||
const { currentScreenPoint, previousScreenPoint } = this.inputs
|
||||
this.pan(Vec.Sub(currentScreenPoint, previousScreenPoint))
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!inputs.isDragging &&
|
||||
inputs.isPointing &&
|
||||
originPagePoint.dist(currentPagePoint) >
|
||||
(this.getInstanceState().isCoarsePointer ? COARSE_DRAG_DISTANCE : DRAG_DISTANCE) /
|
||||
this.getZoomLevel()
|
||||
) {
|
||||
inputs.isDragging = true
|
||||
}
|
||||
break
|
||||
if (!isPen && this.getInstanceState().isPenMode) {
|
||||
return
|
||||
}
|
||||
case 'pointer_up': {
|
||||
// Remove the button from the buttons set
|
||||
inputs.buttons.delete(info.button)
|
||||
|
||||
inputs.isPointing = false
|
||||
inputs.isDragging = false
|
||||
// Firefox bug fix...
|
||||
// If it's the same pointer that we stored earlier...
|
||||
// ... then it's probably still a left-mouse-click!
|
||||
if (this.capturedPointerId === info.pointerId) {
|
||||
this.capturedPointerId = null
|
||||
info.button = 0
|
||||
}
|
||||
|
||||
if (this.getIsMenuOpen()) {
|
||||
// Suppressing pointerup here as <ContextMenu/> doesn't seem to do what we what here.
|
||||
return
|
||||
}
|
||||
if (inputs.isPanning) {
|
||||
if (info.button === 1) {
|
||||
if (!this.inputs.keys.has(' ')) {
|
||||
inputs.isPanning = false
|
||||
|
||||
if (!isPen && this.getInstanceState().isPenMode) {
|
||||
return
|
||||
}
|
||||
|
||||
// Firefox bug fix...
|
||||
// If it's the same pointer that we stored earlier...
|
||||
// ... then it's probably still a left-mouse-click!
|
||||
if (this.capturedPointerId === info.pointerId) {
|
||||
this.capturedPointerId = null
|
||||
info.button = 0
|
||||
}
|
||||
|
||||
if (inputs.isPanning) {
|
||||
if (info.button === 1) {
|
||||
if (!this.inputs.keys.has(' ')) {
|
||||
inputs.isPanning = false
|
||||
|
||||
this.slideCamera({
|
||||
speed: Math.min(2, this.inputs.pointerVelocity.len()),
|
||||
direction: this.inputs.pointerVelocity,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.updateInstanceState({
|
||||
cursor: { type: this._prevCursor, rotation: 0 },
|
||||
})
|
||||
} else {
|
||||
this.slideCamera({
|
||||
speed: Math.min(2, this.inputs.pointerVelocity.len()),
|
||||
direction: this.inputs.pointerVelocity,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.updateInstanceState({
|
||||
cursor: {
|
||||
type: 'grab',
|
||||
rotation: 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
} else if (info.button === 0) {
|
||||
this.slideCamera({
|
||||
speed: Math.min(2, this.inputs.pointerVelocity.len()),
|
||||
direction: this.inputs.pointerVelocity,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.updateInstanceState({
|
||||
cursor: {
|
||||
type: 'grab',
|
||||
rotation: 0,
|
||||
},
|
||||
this.setCursor({ type: this._prevCursor, rotation: 0 })
|
||||
} else {
|
||||
this.slideCamera({
|
||||
speed: Math.min(2, this.inputs.pointerVelocity.len()),
|
||||
direction: this.inputs.pointerVelocity,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.setCursor({
|
||||
type: 'grab',
|
||||
rotation: 0,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (info.button === 5) {
|
||||
// Eraser button activates eraser
|
||||
this.complete()
|
||||
this.setCurrentTool(this._restoreToolId)
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'keyboard': {
|
||||
// please, please
|
||||
if (info.key === 'ShiftRight') info.key = 'ShiftLeft'
|
||||
if (info.key === 'AltRight') info.key = 'AltLeft'
|
||||
if (info.code === 'ControlRight') info.code = 'ControlLeft'
|
||||
|
||||
switch (info.name) {
|
||||
case 'key_down': {
|
||||
// Add the key from the keys set
|
||||
inputs.keys.add(info.code)
|
||||
|
||||
// If the space key is pressed (but meta / control isn't!) activate panning
|
||||
if (!info.ctrlKey && info.code === 'Space') {
|
||||
if (!this.inputs.isPanning) {
|
||||
this._prevCursor = this.getInstanceState().cursor.type
|
||||
}
|
||||
|
||||
this.inputs.isPanning = true
|
||||
this.updateInstanceState({
|
||||
cursor: { type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 },
|
||||
} else if (info.button === 0) {
|
||||
this.slideCamera({
|
||||
speed: Math.min(2, this.inputs.pointerVelocity.len()),
|
||||
direction: this.inputs.pointerVelocity,
|
||||
friction: CAMERA_SLIDE_FRICTION,
|
||||
})
|
||||
this.setCursor({
|
||||
type: 'grab',
|
||||
rotation: 0,
|
||||
})
|
||||
}
|
||||
|
||||
break
|
||||
} else {
|
||||
if (info.button === 5) {
|
||||
// Eraser button activates eraser
|
||||
this.complete()
|
||||
this.setCurrentTool(this._restoreToolId)
|
||||
}
|
||||
}
|
||||
case 'key_up': {
|
||||
// Remove the key from the keys set
|
||||
inputs.keys.delete(info.code)
|
||||
|
||||
if (info.code === 'Space' && !this.inputs.buttons.has(1)) {
|
||||
this.inputs.isPanning = false
|
||||
this.updateInstanceState({
|
||||
cursor: { type: this._prevCursor, rotation: 0 },
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'keyboard': {
|
||||
// please, please
|
||||
if (info.key === 'ShiftRight') info.key = 'ShiftLeft'
|
||||
if (info.key === 'AltRight') info.key = 'AltLeft'
|
||||
if (info.code === 'ControlRight') info.code = 'ControlLeft'
|
||||
|
||||
switch (info.name) {
|
||||
case 'key_down': {
|
||||
// Add the key from the keys set
|
||||
inputs.keys.add(info.code)
|
||||
|
||||
// If the space key is pressed (but meta / control isn't!) activate panning
|
||||
if (!info.ctrlKey && info.code === 'Space') {
|
||||
if (!this.inputs.isPanning) {
|
||||
this._prevCursor = this.getInstanceState().cursor.type
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'key_repeat': {
|
||||
// noop
|
||||
break
|
||||
this.inputs.isPanning = true
|
||||
this.setCursor({ type: this.inputs.isPointing ? 'grabbing' : 'grab', rotation: 0 })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'key_up': {
|
||||
// Remove the key from the keys set
|
||||
inputs.keys.delete(info.code)
|
||||
|
||||
if (info.code === 'Space' && !this.inputs.buttons.has(1)) {
|
||||
this.inputs.isPanning = false
|
||||
this.setCursor({ type: this._prevCursor, rotation: 0 })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'key_repeat': {
|
||||
// noop
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Correct the info name for right / middle clicks
|
||||
if (info.type === 'pointer') {
|
||||
if (info.button === 1) {
|
||||
info.name = 'middle_click'
|
||||
} else if (info.button === 2) {
|
||||
info.name = 'right_click'
|
||||
}
|
||||
|
||||
// Correct the info name for right / middle clicks
|
||||
if (info.type === 'pointer') {
|
||||
if (info.button === 1) {
|
||||
info.name = 'middle_click'
|
||||
} else if (info.button === 2) {
|
||||
info.name = 'right_click'
|
||||
}
|
||||
|
||||
// If a pointer event, send the event to the click manager.
|
||||
if (info.isPen === this.getInstanceState().isPenMode) {
|
||||
switch (info.name) {
|
||||
case 'pointer_down': {
|
||||
const otherEvent = this._clickManager.transformPointerDownEvent(info)
|
||||
if (info.name !== otherEvent.name) {
|
||||
this.root.handleEvent(info)
|
||||
this.emit('event', info)
|
||||
this.root.handleEvent(otherEvent)
|
||||
this.emit('event', otherEvent)
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
// If a pointer event, send the event to the click manager.
|
||||
if (info.isPen === this.getInstanceState().isPenMode) {
|
||||
switch (info.name) {
|
||||
case 'pointer_down': {
|
||||
const otherEvent = this._clickManager.transformPointerDownEvent(info)
|
||||
if (info.name !== otherEvent.name) {
|
||||
this.root.handleEvent(info)
|
||||
this.emit('event', info)
|
||||
this.root.handleEvent(otherEvent)
|
||||
this.emit('event', otherEvent)
|
||||
return
|
||||
}
|
||||
case 'pointer_up': {
|
||||
const otherEvent = this._clickManager.transformPointerUpEvent(info)
|
||||
if (info.name !== otherEvent.name) {
|
||||
this.root.handleEvent(info)
|
||||
this.emit('event', info)
|
||||
this.root.handleEvent(otherEvent)
|
||||
this.emit('event', otherEvent)
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'pointer_move': {
|
||||
this._clickManager.handleMove()
|
||||
break
|
||||
break
|
||||
}
|
||||
case 'pointer_up': {
|
||||
clearTimeout(this._longPressTimeout)
|
||||
|
||||
const otherEvent = this._clickManager.transformPointerUpEvent(info)
|
||||
if (info.name !== otherEvent.name) {
|
||||
this.root.handleEvent(info)
|
||||
this.emit('event', info)
|
||||
this.root.handleEvent(otherEvent)
|
||||
this.emit('event', otherEvent)
|
||||
return
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'pointer_move': {
|
||||
this._clickManager.handleMove()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send the event to the statechart. It will be handled by all
|
||||
// active states, starting at the root.
|
||||
this.root.handleEvent(info)
|
||||
this.emit('event', info)
|
||||
})
|
||||
// Send the event to the statechart. It will be handled by all
|
||||
// active states, starting at the root.
|
||||
this.root.handleEvent(info)
|
||||
this.emit('event', info)
|
||||
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -334,16 +334,15 @@ export abstract class ShapeUtil<Shape extends TLUnknownShape = TLUnknownShape> {
|
|||
*
|
||||
* ```ts
|
||||
* onDragShapesOver = (shape, shapes) => {
|
||||
* return { shouldHint: true }
|
||||
* this.editor.reparentShapes(shapes, shape.id)
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param shape - The shape.
|
||||
* @param shapes - The shapes that are being dragged over this one.
|
||||
* @returns An object specifying whether the shape should hint that it can receive the dragged shapes.
|
||||
* @public
|
||||
*/
|
||||
onDragShapesOver?: TLOnDragHandler<Shape, { shouldHint: boolean }>
|
||||
onDragShapesOver?: TLOnDragHandler<Shape>
|
||||
|
||||
/**
|
||||
* A callback called when some other shapes are dragged out of this one.
|
||||
|
|
|
@ -198,6 +198,7 @@ export abstract class StateNode implements Partial<TLEventHandlers> {
|
|||
onWheel?: TLEventHandlers['onWheel']
|
||||
onPointerDown?: TLEventHandlers['onPointerDown']
|
||||
onPointerMove?: TLEventHandlers['onPointerMove']
|
||||
onLongPress?: TLEventHandlers['onLongPress']
|
||||
onPointerUp?: TLEventHandlers['onPointerUp']
|
||||
onDoubleClick?: TLEventHandlers['onDoubleClick']
|
||||
onTripleClick?: TLEventHandlers['onTripleClick']
|
||||
|
|
|
@ -16,6 +16,7 @@ export type TLPointerEventTarget =
|
|||
export type TLPointerEventName =
|
||||
| 'pointer_down'
|
||||
| 'pointer_move'
|
||||
| 'long_press'
|
||||
| 'pointer_up'
|
||||
| 'right_click'
|
||||
| 'middle_click'
|
||||
|
@ -152,6 +153,7 @@ export type TLExitEventHandler = (info: any, to: string) => void
|
|||
export interface TLEventHandlers {
|
||||
onPointerDown: TLPointerEvent
|
||||
onPointerMove: TLPointerEvent
|
||||
onLongPress: TLPointerEvent
|
||||
onRightClick: TLPointerEvent
|
||||
onDoubleClick: TLClickEvent
|
||||
onTripleClick: TLClickEvent
|
||||
|
@ -176,6 +178,7 @@ export const EVENT_NAME_MAP: Record<
|
|||
wheel: 'onWheel',
|
||||
pointer_down: 'onPointerDown',
|
||||
pointer_move: 'onPointerMove',
|
||||
long_press: 'onLongPress',
|
||||
pointer_up: 'onPointerUp',
|
||||
right_click: 'onRightClick',
|
||||
middle_click: 'onMiddleClick',
|
||||
|
|
|
@ -316,3 +316,19 @@ export function polygonsIntersect(a: VecLike[], b: VecLike[]) {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function polygonIntersectsPolyline(polygon: VecLike[], polyline: VecLike[]) {
|
||||
let a: VecLike, b: VecLike, c: VecLike, d: VecLike
|
||||
for (let i = 0, n = polygon.length; i < n; i++) {
|
||||
a = polygon[i]
|
||||
b = polygon[(i + 1) % n]
|
||||
|
||||
for (let j = 1, m = polyline.length; j < m; j++) {
|
||||
c = polyline[j - 1]
|
||||
d = polyline[j]
|
||||
if (linesIntersect(a, b, c, d)) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -62,7 +62,6 @@ import { TLBookmarkShape } from '@tldraw/editor';
|
|||
import { TLCancelEvent } from '@tldraw/editor';
|
||||
import { TLClickEvent } from '@tldraw/editor';
|
||||
import { TLClickEventInfo } from '@tldraw/editor';
|
||||
import { TLDefaultColorStyle } from '@tldraw/editor';
|
||||
import { TLDefaultColorTheme } from '@tldraw/editor';
|
||||
import { TLDefaultFillStyle } from '@tldraw/editor';
|
||||
import { TLDefaultFontStyle } from '@tldraw/editor';
|
||||
|
@ -94,7 +93,6 @@ import { TLOnBeforeUpdateHandler } from '@tldraw/editor';
|
|||
import { TLOnDoubleClickHandler } from '@tldraw/editor';
|
||||
import { TLOnEditEndHandler } from '@tldraw/editor';
|
||||
import { TLOnHandleDragHandler } from '@tldraw/editor';
|
||||
import { TLOnResizeEndHandler } from '@tldraw/editor';
|
||||
import { TLOnResizeHandler } from '@tldraw/editor';
|
||||
import { TLOnTranslateHandler } from '@tldraw/editor';
|
||||
import { TLOnTranslateStartHandler } from '@tldraw/editor';
|
||||
|
@ -659,14 +657,10 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
// (undocumented)
|
||||
onDragShapesOut: (_shape: TLFrameShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onDragShapesOver: (frame: TLFrameShape, shapes: TLShape[]) => {
|
||||
shouldHint: boolean;
|
||||
};
|
||||
onDragShapesOver: (frame: TLFrameShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onResize: TLOnResizeHandler<any>;
|
||||
// (undocumented)
|
||||
onResizeEnd: TLOnResizeEndHandler<TLFrameShape>;
|
||||
// (undocumented)
|
||||
static props: {
|
||||
w: Validator<number>;
|
||||
h: Validator<number>;
|
||||
|
@ -974,9 +968,15 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
// @public (undocumented)
|
||||
export function isGifAnimated(file: Blob): Promise<boolean>;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function isShapeOccluded(editor: Editor, occluder: TLShape, shape: TLShapeId): boolean;
|
||||
|
||||
// @public (undocumented)
|
||||
export function KeyboardShortcutsMenuItem(): JSX_2.Element | null;
|
||||
|
||||
// @internal (undocumented)
|
||||
export function kickoutOccludedShapes(editor: Editor, shapeIds: TLShapeId[]): void;
|
||||
|
||||
// @public (undocumented)
|
||||
export const LABEL_FONT_SIZES: Record<TLDefaultSizeStyle, number>;
|
||||
|
||||
|
@ -1096,21 +1096,23 @@ export class NoteShapeTool extends StateNode {
|
|||
|
||||
// @public (undocumented)
|
||||
export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
||||
// (undocumented)
|
||||
canDropShapes: (shape: TLNoteShape, _shapes: TLShape[]) => boolean;
|
||||
// (undocumented)
|
||||
canEdit: () => boolean;
|
||||
// (undocumented)
|
||||
canReceiveNewChildrenOfType: (shape: TLNoteShape, type: string) => boolean;
|
||||
// (undocumented)
|
||||
component(shape: TLNoteShape): JSX_2.Element;
|
||||
// (undocumented)
|
||||
doesAutoEditOnKeyStroke: () => boolean;
|
||||
// (undocumented)
|
||||
getDefaultProps(): TLNoteShape['props'];
|
||||
// (undocumented)
|
||||
getGeometry(shape: TLNoteShape): Rectangle2d;
|
||||
getGeometry(shape: TLNoteShape): Group2d;
|
||||
// (undocumented)
|
||||
getHandles(shape: TLNoteShape): TLHandle[];
|
||||
// (undocumented)
|
||||
getHeight(shape: TLNoteShape): number;
|
||||
// (undocumented)
|
||||
hideResizeHandles: () => boolean;
|
||||
// (undocumented)
|
||||
hideSelectionBoundsFg: () => boolean;
|
||||
|
@ -1169,12 +1171,18 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
typeName: "shape";
|
||||
} | undefined;
|
||||
// (undocumented)
|
||||
onDragShapesOut: (note: TLNoteShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onDragShapesOver: (note: TLNoteShape, shapes: TLShape[]) => void;
|
||||
// (undocumented)
|
||||
onEditEnd: TLOnEditEndHandler<TLNoteShape>;
|
||||
// (undocumented)
|
||||
onTranslateStart: (shape: TLNoteShape) => void;
|
||||
// (undocumented)
|
||||
static props: {
|
||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||
fontSizeAdjustment: Validator<number | undefined>;
|
||||
fontSizeAdjustment: Validator<number>;
|
||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
|
||||
verticalAlign: EnumStyleProp<"end" | "middle" | "start">;
|
||||
|
@ -2398,8 +2406,8 @@ export type TLUiToolsProviderProps = {
|
|||
export type TLUiTranslation = {
|
||||
readonly locale: string;
|
||||
readonly label: string;
|
||||
readonly isRTL?: boolean;
|
||||
readonly messages: Record<TLUiTranslationKey, string>;
|
||||
readonly dir: 'ltr' | 'rtl';
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
@ -2518,13 +2526,14 @@ export function useDialogs(): TLUiDialogsContextType;
|
|||
export function useEditableText(id: TLShapeId, type: string, text: string): {
|
||||
rInput: React_2.RefObject<HTMLTextAreaElement>;
|
||||
isEditing: boolean;
|
||||
handleFocus: () => void;
|
||||
handleFocus: typeof noop;
|
||||
handleBlur: () => void;
|
||||
handleKeyDown: (e: React_2.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
handleChange: (e: React_2.ChangeEvent<HTMLTextAreaElement>) => void;
|
||||
handleInputPointerDown: (e: React_2.PointerEvent) => void;
|
||||
handleDoubleClick: (e: any) => any;
|
||||
isEmpty: boolean;
|
||||
isEditingAnything: boolean;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -7611,7 +7611,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => {\n shouldHint: boolean;\n }"
|
||||
"text": "[]) => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -7665,50 +7665,6 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!FrameShapeUtil#onResizeEnd:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onResizeEnd: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLOnResizeEndHandler",
|
||||
"canonicalReference": "@tldraw/editor!TLOnResizeEndHandler:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<"
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLFrameShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLFrameShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onResizeEnd",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 5
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!FrameShapeUtil.props:member",
|
||||
|
@ -12824,6 +12780,54 @@
|
|||
"name": "NoteShapeUtil",
|
||||
"preserveMemberOrder": false,
|
||||
"members": [
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#canDropShapes:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "canDropShapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", _shapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "canDropShapes",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#canEdit:member",
|
||||
|
@ -12854,6 +12858,45 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#canReceiveNewChildrenOfType:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "canReceiveNewChildrenOfType: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", type: string) => boolean"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "canReceiveNewChildrenOfType",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#component:member(1)",
|
||||
|
@ -12994,8 +13037,8 @@
|
|||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Rectangle2d",
|
||||
"canonicalReference": "@tldraw/editor!Rectangle2d:class"
|
||||
"text": "Group2d",
|
||||
"canonicalReference": "@tldraw/editor!Group2d:class"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -13078,55 +13121,6 @@
|
|||
"isAbstract": false,
|
||||
"name": "getHandles"
|
||||
},
|
||||
{
|
||||
"kind": "Method",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#getHeight:member(1)",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "getHeight(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "): "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "number"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isStatic": false,
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 3,
|
||||
"endIndex": 4
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"isProtected": false,
|
||||
"overloadIndex": 1,
|
||||
"parameters": [
|
||||
{
|
||||
"parameterName": "shape",
|
||||
"parameterTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 2
|
||||
},
|
||||
"isOptional": false
|
||||
}
|
||||
],
|
||||
"isOptional": false,
|
||||
"isAbstract": false,
|
||||
"name": "getHeight"
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#hideResizeHandles:member",
|
||||
|
@ -13435,6 +13429,102 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onDragShapesOut:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onDragShapesOut: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(note: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", shapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onDragShapesOut",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onDragShapesOver:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onDragShapesOver: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(note: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", shapes: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "[]) => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onDragShapesOver",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 6
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onEditEnd:member",
|
||||
|
@ -13479,6 +13569,45 @@
|
|||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil#onTranslateStart:member",
|
||||
"docComment": "",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "onTranslateStart: "
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "(shape: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "TLNoteShape",
|
||||
"canonicalReference": "@tldraw/tlschema!TLNoteShape:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ") => void"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"isReadonly": false,
|
||||
"isOptional": false,
|
||||
"releaseTag": "Public",
|
||||
"name": "onTranslateStart",
|
||||
"propertyTypeTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 4
|
||||
},
|
||||
"isStatic": false,
|
||||
"isProtected": false,
|
||||
"isAbstract": false
|
||||
},
|
||||
{
|
||||
"kind": "Property",
|
||||
"canonicalReference": "tldraw!NoteShapeUtil.props:member",
|
||||
|
@ -13517,7 +13646,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<number | undefined>;\n font: import(\"@tldraw/editor\")."
|
||||
"text": "<number>;\n font: import(\"@tldraw/editor\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -26432,7 +26561,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "{\n readonly locale: string;\n readonly label: string;\n readonly isRTL?: boolean;\n readonly messages: "
|
||||
"text": "{\n readonly locale: string;\n readonly label: string;\n readonly messages: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -26450,7 +26579,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ", string>;\n}"
|
||||
"text": ", string>;\n readonly dir: 'ltr' | 'rtl';\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -27649,7 +27778,16 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ">;\n isEditing: boolean;\n handleFocus: () => void;\n handleBlur: () => void;\n handleKeyDown: (e: "
|
||||
"text": ">;\n isEditing: boolean;\n handleFocus: typeof "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "noop",
|
||||
"canonicalReference": "tldraw!~noop:function"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";\n handleBlur: () => void;\n handleKeyDown: (e: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -27694,7 +27832,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ") => void;\n handleDoubleClick: (e: any) => any;\n isEmpty: boolean;\n}"
|
||||
"text": ") => void;\n handleDoubleClick: (e: any) => any;\n isEmpty: boolean;\n isEditingAnything: boolean;\n}"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -27704,7 +27842,7 @@
|
|||
"fileUrlPath": "packages/tldraw/src/lib/shapes/shared/useEditableText.ts",
|
||||
"returnTypeTokenRange": {
|
||||
"startIndex": 7,
|
||||
"endIndex": 22
|
||||
"endIndex": 24
|
||||
},
|
||||
"releaseTag": "Public",
|
||||
"overloadIndex": 1,
|
||||
|
|
|
@ -41,6 +41,7 @@ export { EraserTool } from './lib/tools/EraserTool/EraserTool'
|
|||
export { HandTool } from './lib/tools/HandTool/HandTool'
|
||||
export { LaserTool } from './lib/tools/LaserTool/LaserTool'
|
||||
export { SelectTool } from './lib/tools/SelectTool/SelectTool'
|
||||
export { isShapeOccluded, kickoutOccludedShapes } from './lib/tools/SelectTool/selectHelpers'
|
||||
export { ZoomTool } from './lib/tools/ZoomTool/ZoomTool'
|
||||
// UI
|
||||
export { useEditableText } from './lib/shapes/shared/useEditableText'
|
||||
|
|
|
@ -306,15 +306,20 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
// If no bound shapes are in the selection, unbind any bound shapes
|
||||
|
||||
const selectedShapeIds = this.editor.getSelectedShapeIds()
|
||||
|
||||
if (
|
||||
(startBindingId &&
|
||||
(selectedShapeIds.includes(startBindingId) ||
|
||||
this.editor.isAncestorSelected(startBindingId))) ||
|
||||
(endBindingId &&
|
||||
(selectedShapeIds.includes(endBindingId) || this.editor.isAncestorSelected(endBindingId)))
|
||||
) {
|
||||
return
|
||||
const shapesToCheck = new Set<string>()
|
||||
if (startBindingId) {
|
||||
// Add shape and all ancestors to set
|
||||
shapesToCheck.add(startBindingId)
|
||||
this.editor.getShapeAncestors(startBindingId).forEach((a) => shapesToCheck.add(a.id))
|
||||
}
|
||||
if (endBindingId) {
|
||||
// Add shape and all ancestors to set
|
||||
shapesToCheck.add(endBindingId)
|
||||
this.editor.getShapeAncestors(endBindingId).forEach((a) => shapesToCheck.add(a.id))
|
||||
}
|
||||
// If any of the shapes are selected, return
|
||||
for (const id of selectedShapeIds) {
|
||||
if (shapesToCheck.has(id)) return
|
||||
}
|
||||
|
||||
let result = shape
|
||||
|
@ -530,6 +535,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
if (!info?.isValid) return null
|
||||
|
||||
const labelPosition = getArrowLabelPosition(this.editor, shape)
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
const isEditing = this.editor.getEditingShapeId() === shape.id
|
||||
const showArrowLabel = isEditing || shape.props.text
|
||||
|
||||
|
@ -549,6 +555,7 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
|
|||
size={shape.props.size}
|
||||
position={labelPosition.box.center}
|
||||
width={labelPosition.box.w}
|
||||
isSelected={isSelected}
|
||||
labelColor={shape.props.labelColor}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { TLArrowShape, TLDefaultColorStyle, TLShapeId, VecLike } from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { useDefaultColorTheme } from '../../shared/ShapeFill'
|
||||
import { TextLabel } from '../../shared/TextLabel'
|
||||
import { ARROW_LABEL_FONT_SIZES, TEXT_PROPS } from '../../shared/default-shape-constants'
|
||||
|
||||
|
@ -10,11 +11,16 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
|
|||
font,
|
||||
position,
|
||||
width,
|
||||
isSelected,
|
||||
labelColor,
|
||||
}: { id: TLShapeId; position: VecLike; width?: number; labelColor: TLDefaultColorStyle } & Pick<
|
||||
TLArrowShape['props'],
|
||||
'text' | 'size' | 'font'
|
||||
>) {
|
||||
}: {
|
||||
id: TLShapeId
|
||||
position: VecLike
|
||||
width?: number
|
||||
labelColor: TLDefaultColorStyle
|
||||
isSelected: boolean
|
||||
} & Pick<TLArrowShape['props'], 'text' | 'size' | 'font'>) {
|
||||
const theme = useDefaultColorTheme()
|
||||
return (
|
||||
<TextLabel
|
||||
id={id}
|
||||
|
@ -26,8 +32,9 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
|
|||
align="middle"
|
||||
verticalAlign="middle"
|
||||
text={text}
|
||||
labelColor={labelColor}
|
||||
labelColor={theme[labelColor].solid}
|
||||
textWidth={width}
|
||||
isSelected={isSelected}
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px)`,
|
||||
}}
|
||||
|
|
|
@ -62,9 +62,7 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const {
|
||||
editor: { inputs },
|
||||
} = this
|
||||
const { inputs } = this.editor
|
||||
|
||||
if (this.isPen !== inputs.isPen) {
|
||||
// The user made a palm gesture before starting a pen gesture;
|
||||
|
@ -282,8 +280,8 @@ export class Drawing extends StateNode {
|
|||
}
|
||||
|
||||
private updateShapes() {
|
||||
const { inputs } = this.editor
|
||||
const { initialShape } = this
|
||||
const { inputs } = this.editor
|
||||
|
||||
if (!initialShape) return
|
||||
|
||||
|
@ -440,7 +438,7 @@ export class Drawing extends StateNode {
|
|||
const newSegment = newSegments[newSegments.length - 1]
|
||||
|
||||
const { pagePointWhereCurrentSegmentChanged } = this
|
||||
const { currentPagePoint, ctrlKey } = this.editor.inputs
|
||||
const { ctrlKey, currentPagePoint } = this.editor.inputs
|
||||
|
||||
if (!pagePointWhereCurrentSegmentChanged)
|
||||
throw Error('We should have a point where the segment changed')
|
||||
|
@ -623,16 +621,14 @@ export class Drawing extends StateNode {
|
|||
if (newPoints.length > 500) {
|
||||
this.editor.updateShapes([{ id, type: this.shapeType, props: { isComplete: true } }])
|
||||
|
||||
const { currentPagePoint } = this.editor.inputs
|
||||
|
||||
const newShapeId = createShapeId()
|
||||
|
||||
this.editor.createShapes<DrawableShape>([
|
||||
{
|
||||
id: newShapeId,
|
||||
type: this.shapeType,
|
||||
x: toFixed(currentPagePoint.x),
|
||||
y: toFixed(currentPagePoint.y),
|
||||
x: toFixed(inputs.currentPagePoint.x),
|
||||
y: toFixed(inputs.currentPagePoint.y),
|
||||
props: {
|
||||
isPen: this.isPen,
|
||||
segments: [
|
||||
|
@ -647,7 +643,7 @@ export class Drawing extends StateNode {
|
|||
|
||||
this.initialShape = structuredClone(this.editor.getShape<DrawableShape>(newShapeId)!)
|
||||
this.mergeNextPoint = false
|
||||
this.lastRecordedPoint = this.editor.inputs.currentPagePoint.clone()
|
||||
this.lastRecordedPoint = inputs.currentPagePoint.clone()
|
||||
this.currentLineLength = 0
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,8 @@ import {
|
|||
SvgExportContext,
|
||||
TLFrameShape,
|
||||
TLGroupShape,
|
||||
TLOnResizeEndHandler,
|
||||
TLOnResizeHandler,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
canonicalizeRotation,
|
||||
frameShapeMigrations,
|
||||
frameShapeProps,
|
||||
|
@ -68,7 +66,7 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
const info = (resizingState as typeof resizingState & { info: { isCreating: boolean } })
|
||||
?.info
|
||||
if (!info) return false
|
||||
return info.isCreating && this.editor.getOnlySelectedShape()?.id === shape.id
|
||||
return info.isCreating && this.editor.getOnlySelectedShapeId() === shape.id
|
||||
},
|
||||
[shape.id]
|
||||
)
|
||||
|
@ -110,18 +108,18 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
|
||||
let labelTranslate: string
|
||||
switch (labelSide) {
|
||||
case 0:
|
||||
case 0: // top
|
||||
labelTranslate = ``
|
||||
break
|
||||
case 3:
|
||||
case 3: // right
|
||||
labelTranslate = `translate(${toDomPrecision(shape.props.w)}, 0) rotate(90)`
|
||||
break
|
||||
case 2:
|
||||
case 2: // bottom
|
||||
labelTranslate = `translate(${toDomPrecision(shape.props.w)}, ${toDomPrecision(
|
||||
shape.props.h
|
||||
)}) rotate(180)`
|
||||
break
|
||||
case 1:
|
||||
case 1: // left
|
||||
labelTranslate = `translate(0, ${toDomPrecision(shape.props.h)}) rotate(270)`
|
||||
break
|
||||
default:
|
||||
|
@ -207,15 +205,10 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
return !shape.isLocked
|
||||
}
|
||||
|
||||
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]): { shouldHint: boolean } => {
|
||||
override onDragShapesOver = (frame: TLFrameShape, shapes: TLShape[]) => {
|
||||
if (!shapes.every((child) => child.parentId === frame.id)) {
|
||||
this.editor.reparentShapes(
|
||||
shapes.map((shape) => shape.id),
|
||||
frame.id
|
||||
)
|
||||
return { shouldHint: true }
|
||||
this.editor.reparentShapes(shapes, frame.id)
|
||||
}
|
||||
return { shouldHint: false }
|
||||
}
|
||||
|
||||
override onDragShapesOut = (_shape: TLFrameShape, shapes: TLShape[]): void => {
|
||||
|
@ -232,24 +225,6 @@ export class FrameShapeUtil extends BaseBoxShapeUtil<TLFrameShape> {
|
|||
}
|
||||
}
|
||||
|
||||
override onResizeEnd: TLOnResizeEndHandler<TLFrameShape> = (shape) => {
|
||||
const bounds = this.editor.getShapePageBounds(shape)!
|
||||
const children = this.editor.getSortedChildIdsForParent(shape.id)
|
||||
|
||||
const shapesToReparent: TLShapeId[] = []
|
||||
|
||||
for (const childId of children) {
|
||||
const childBounds = this.editor.getShapePageBounds(childId)!
|
||||
if (!bounds.includes(childBounds)) {
|
||||
shapesToReparent.push(childId)
|
||||
}
|
||||
}
|
||||
|
||||
if (shapesToReparent.length > 0) {
|
||||
this.editor.reparentShapes(shapesToReparent, this.editor.getCurrentPageId())
|
||||
}
|
||||
}
|
||||
|
||||
override onResize: TLOnResizeHandler<any> = (shape, info) => {
|
||||
return resizeBox(shape, info)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import {
|
||||
BaseBoxShapeUtil,
|
||||
Editor,
|
||||
|
@ -26,6 +27,7 @@ import {
|
|||
} from '@tldraw/editor'
|
||||
|
||||
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import {
|
||||
|
@ -381,10 +383,11 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
|
||||
component(shape: TLGeoShape) {
|
||||
const { id, type, props } = shape
|
||||
const { labelColor, fill, font, align, verticalAlign, size, text } = props
|
||||
|
||||
const { fill, font, align, verticalAlign, size, text } = props
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
const theme = useDefaultColorTheme()
|
||||
const isEditing = this.editor.getEditingShapeId() === id
|
||||
const showHtmlContainer = isEditing || shape.props.url || shape.props.text
|
||||
const showHtmlContainer = isEditing || shape.props.text
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -410,15 +413,15 @@ export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
|
|||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
labelColor={labelColor}
|
||||
isSelected={isSelected}
|
||||
labelColor={theme[props.labelColor].solid}
|
||||
wrap
|
||||
bounds={props.geo === 'cloud' ? this.getGeometry(shape).bounds : undefined}
|
||||
/>
|
||||
{shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
||||
)}
|
||||
</HTMLContainer>
|
||||
)}
|
||||
{shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ export class ImageShapeUtil extends BaseBoxShapeUtil<TLImageShape> {
|
|||
|
||||
const asset = shape.props.assetId ? this.editor.getAsset(shape.props.assetId) : undefined
|
||||
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShape()?.id
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
|
||||
useEffect(() => {
|
||||
if (asset?.props.src && 'mimeType' in asset.props && asset?.props.mimeType === 'image/gif') {
|
||||
|
|
|
@ -204,17 +204,17 @@ describe('Grid placement helpers', () => {
|
|||
})
|
||||
})
|
||||
|
||||
it('Does note create a new sticky note in a sticky pit if a note is already there', () => {
|
||||
it('Does not create a new sticky note in a sticky pit if a note is already there', () => {
|
||||
editor
|
||||
.createShape({ type: 'note', x: 0, y: 0 })
|
||||
.createShape({ type: 'note', x: 228, y: 8 }) // make a shape kinda there already!
|
||||
.createShape({ type: 'note', x: 330, y: 8 }) // make a shape kinda there already!
|
||||
.setCurrentTool('note')
|
||||
.pointerMove(324, 104)
|
||||
.pointerMove(300, 104)
|
||||
.click()
|
||||
.expectShapeToMatch({
|
||||
...editor.getLastCreatedShape(),
|
||||
// outta da pit
|
||||
x: 224,
|
||||
x: 200,
|
||||
y: 4,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import {
|
||||
Editor,
|
||||
Group2d,
|
||||
IndexKey,
|
||||
Rectangle2d,
|
||||
ShapeUtil,
|
||||
SvgExportContext,
|
||||
TLGroupShape,
|
||||
TLHandle,
|
||||
TLNoteShape,
|
||||
TLOnEditEndHandler,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
Vec,
|
||||
WeakMapCache,
|
||||
getDefaultColorTheme,
|
||||
noteShapeMigrations,
|
||||
noteShapeProps,
|
||||
|
@ -24,16 +28,22 @@ import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { FONT_FAMILIES, LABEL_FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
import {
|
||||
FONT_FAMILIES,
|
||||
LABEL_FONT_SIZES,
|
||||
LABEL_PADDING,
|
||||
TEXT_PROPS,
|
||||
} from '../shared/default-shape-constants'
|
||||
import { getFontDefForExport } from '../shared/defaultStyleDefs'
|
||||
|
||||
import { startEditingShapeWithLabel } from '../shared/TextHelpers'
|
||||
import { useForceSolid } from '../shared/useForceSolid'
|
||||
import {
|
||||
ADJACENT_NOTE_MARGIN,
|
||||
CENTER_OFFSET,
|
||||
CLONE_HANDLE_MARGIN,
|
||||
NOTE_CENTER_OFFSET,
|
||||
NOTE_SIZE,
|
||||
getNoteShapeForAdjacentPosition,
|
||||
startEditingNoteShape,
|
||||
} from './noteHelpers'
|
||||
|
||||
/** @public */
|
||||
|
@ -47,6 +57,36 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
override hideResizeHandles = () => true
|
||||
override hideSelectionBoundsFg = () => false
|
||||
|
||||
override canReceiveNewChildrenOfType = (shape: TLNoteShape, type: string) => {
|
||||
return !shape.isLocked && type !== 'frame'
|
||||
}
|
||||
|
||||
override canDropShapes = (shape: TLNoteShape, _shapes: TLShape[]): boolean => {
|
||||
return !shape.isLocked
|
||||
}
|
||||
|
||||
override onDragShapesOver = (note: TLNoteShape, shapes: TLShape[]) => {
|
||||
if (!shapes.every((child) => child.parentId === note.id)) {
|
||||
const shapesWithoutFrames = shapes.filter(
|
||||
(shape) => !this.editor.isShapeOfType(shape, 'frame')
|
||||
)
|
||||
this.editor.reparentShapes(shapesWithoutFrames, note.id)
|
||||
}
|
||||
}
|
||||
|
||||
override onDragShapesOut = (note: TLNoteShape, shapes: TLShape[]) => {
|
||||
const parent = this.editor.getShape(note.parentId)
|
||||
const isInGroup = parent && this.editor.isShapeOfType<TLGroupShape>(parent, 'group')
|
||||
|
||||
// If sticky is in a group, keep the shape in that group
|
||||
|
||||
if (isInGroup) {
|
||||
this.editor.reparentShapes(shapes, parent.id)
|
||||
} else {
|
||||
this.editor.reparentShapes(shapes, this.editor.getCurrentPageId())
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultProps(): TLNoteShape['props'] {
|
||||
return {
|
||||
color: 'black',
|
||||
|
@ -61,18 +101,39 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
}
|
||||
}
|
||||
|
||||
getHeight(shape: TLNoteShape) {
|
||||
return NOTE_SIZE + shape.props.growY
|
||||
}
|
||||
|
||||
getGeometry(shape: TLNoteShape) {
|
||||
const height = this.getHeight(shape)
|
||||
return new Rectangle2d({ width: NOTE_SIZE, height, isFilled: true, isLabel: true })
|
||||
const noteHeight = getNoteHeight(shape)
|
||||
const { labelHeight, labelWidth } = getLabelSize(this.editor, shape)
|
||||
|
||||
return new Group2d({
|
||||
children: [
|
||||
new Rectangle2d({ width: NOTE_SIZE, height: noteHeight, isFilled: true }),
|
||||
new Rectangle2d({
|
||||
x:
|
||||
shape.props.align === 'start'
|
||||
? 0
|
||||
: shape.props.align === 'end'
|
||||
? NOTE_SIZE - labelWidth
|
||||
: (NOTE_SIZE - labelWidth) / 2,
|
||||
y:
|
||||
shape.props.verticalAlign === 'start'
|
||||
? 0
|
||||
: shape.props.verticalAlign === 'end'
|
||||
? noteHeight - labelHeight
|
||||
: (noteHeight - labelHeight) / 2,
|
||||
width: labelWidth,
|
||||
height: labelHeight,
|
||||
isFilled: true,
|
||||
isLabel: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
override getHandles(shape: TLNoteShape): TLHandle[] {
|
||||
const zoom = this.editor.getZoomLevel()
|
||||
const offset = CLONE_HANDLE_MARGIN / zoom
|
||||
const noteHeight = getNoteHeight(shape)
|
||||
|
||||
if (zoom < 0.25) return []
|
||||
|
||||
|
@ -89,21 +150,21 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
index: 'a2' as IndexKey,
|
||||
type: 'clone',
|
||||
x: NOTE_SIZE + offset,
|
||||
y: this.getHeight(shape) / 2,
|
||||
y: noteHeight / 2,
|
||||
},
|
||||
{
|
||||
id: 'bottom',
|
||||
index: 'a3' as IndexKey,
|
||||
type: 'clone',
|
||||
x: NOTE_SIZE / 2,
|
||||
y: this.getHeight(shape) + offset,
|
||||
y: noteHeight + offset,
|
||||
},
|
||||
{
|
||||
id: 'left',
|
||||
index: 'a4' as IndexKey,
|
||||
type: 'clone',
|
||||
x: -offset,
|
||||
y: this.getHeight(shape) / 2,
|
||||
y: noteHeight / 2,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -120,59 +181,67 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const theme = useDefaultColorTheme()
|
||||
const noteHeight = this.getHeight(shape)
|
||||
const noteHeight = getNoteHeight(shape)
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const rotation = useValue('shape rotation', () => this.editor.getShape(id)?.rotation ?? 0, [
|
||||
this.editor,
|
||||
])
|
||||
const rotation = useValue(
|
||||
'shape rotation',
|
||||
() => this.editor.getShapePageTransform(id)?.rotation() ?? 0,
|
||||
[this.editor]
|
||||
)
|
||||
|
||||
// todo: consider hiding shadows on dark mode if they're invisible anyway
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const hideShadows = useForceSolid()
|
||||
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
|
||||
// Shadow stuff
|
||||
const oy = Math.cos(rotation)
|
||||
const ox = Math.sin(rotation)
|
||||
const random = rng(id)
|
||||
const lift = 1 + random() * 0.5
|
||||
|
||||
const zoom = this.editor.getZoomLevel()
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className="tl-note__container"
|
||||
style={{
|
||||
width: NOTE_SIZE,
|
||||
height: noteHeight,
|
||||
color: theme[color].note.text,
|
||||
backgroundColor: theme[color].note.fill,
|
||||
borderBottom: hideShadows ? `3px solid rgb(144, 144, 144)` : 'none',
|
||||
opacity: hideShadows ? 1 : 0.99,
|
||||
boxShadow: hideShadows
|
||||
? 'none'
|
||||
: `${ox * 3}px ${4 - lift}px 4px -4px rgba(0,0,0,.8),
|
||||
${ox * 6}px ${(6 + lift * 8) * oy}px ${6 + lift * 8}px -${6 + lift * 6}px rgba(0,0,0,${0.3 + lift * 0.1}),
|
||||
0px 50px 8px -10px inset rgba(0,0,0,${0.0375 + 0.025 * random()})`,
|
||||
}}
|
||||
>
|
||||
<TextLabel
|
||||
<>
|
||||
<div
|
||||
id={id}
|
||||
type={type}
|
||||
font={font}
|
||||
fontSize={fontSizeAdjustment || LABEL_FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
isNote
|
||||
labelColor={color}
|
||||
wrap
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
className="tl-note__container"
|
||||
style={{
|
||||
width: NOTE_SIZE,
|
||||
height: noteHeight,
|
||||
backgroundColor: theme[color].note.fill,
|
||||
borderBottom: hideShadows ? `3px solid rgb(15, 23, 31, .2)` : 'none',
|
||||
opacity: hideShadows ? 1 : 0.99,
|
||||
boxShadow: hideShadows
|
||||
? 'none'
|
||||
: `${ox * 3}px ${4 - lift}px 5px -5px rgba(15, 23, 31,1),
|
||||
${ox * 6}px ${(4 + lift * 7) * Math.max(0, oy)}px ${6 + lift * 8}px -${4 + lift * 6}px rgba(15, 23, 31,${0.3 + lift * 0.1}),
|
||||
0px 48px 10px -10px inset rgba(15, 23, 31,${0.02 + random() * 0.005})`,
|
||||
}}
|
||||
>
|
||||
<TextLabel
|
||||
id={id}
|
||||
type={type}
|
||||
font={font}
|
||||
fontSize={fontSizeAdjustment || LABEL_FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align={align}
|
||||
verticalAlign={verticalAlign}
|
||||
text={text}
|
||||
isNote
|
||||
isSelected={isSelected}
|
||||
labelColor={theme[color].note.text}
|
||||
wrap
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
{'url' in shape.props && shape.props.url && (
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
|
||||
<HyperlinkButton url={shape.props.url} zoomLevel={zoom} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -181,7 +250,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
<rect
|
||||
rx="1"
|
||||
width={toDomPrecision(NOTE_SIZE)}
|
||||
height={toDomPrecision(this.getHeight(shape))}
|
||||
height={toDomPrecision(getNoteHeight(shape))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -219,7 +288,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
}
|
||||
|
||||
override onBeforeCreate = (next: TLNoteShape) => {
|
||||
return getGrowY(this.editor, next, next.props.growY)
|
||||
return getSizeAdjustments(this.editor, next)
|
||||
}
|
||||
|
||||
override onBeforeUpdate = (prev: TLNoteShape, next: TLNoteShape) => {
|
||||
|
@ -231,7 +300,11 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
return
|
||||
}
|
||||
|
||||
return getGrowY(this.editor, next, prev.props.growY)
|
||||
return getSizeAdjustments(this.editor, next)
|
||||
}
|
||||
|
||||
override onTranslateStart = (shape: TLNoteShape) => {
|
||||
this.editor.bringToFront([shape])
|
||||
}
|
||||
|
||||
override onEditEnd: TLOnEditEndHandler<TLNoteShape> = (shape) => {
|
||||
|
@ -255,62 +328,87 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
|
|||
}
|
||||
}
|
||||
|
||||
function getGrowY(editor: Editor, shape: TLNoteShape, prevGrowY = 0) {
|
||||
const BORDER = 1
|
||||
const PADDING = 16 + BORDER
|
||||
/**
|
||||
* Get the growY and fontSizeAdjustment for a shape.
|
||||
*/
|
||||
function getSizeAdjustments(editor: Editor, shape: TLNoteShape) {
|
||||
const { labelHeight, fontSizeAdjustment } = getLabelSize(editor, shape)
|
||||
// When the label height is more than the height of the shape, we add extra height to it
|
||||
const growY = Math.max(0, labelHeight - NOTE_SIZE)
|
||||
|
||||
if (growY !== shape.props.growY || fontSizeAdjustment !== shape.props.fontSizeAdjustment) {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
growY,
|
||||
fontSizeAdjustment,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the label size for a note.
|
||||
*/
|
||||
function _getLabelSize(editor: Editor, shape: TLNoteShape) {
|
||||
const text = shape.props.text
|
||||
|
||||
if (!text) {
|
||||
return { labelHeight: 0, labelWidth: 0, fontSizeAdjustment: 0 }
|
||||
}
|
||||
|
||||
const unadjustedFontSize = LABEL_FONT_SIZES[shape.props.size]
|
||||
|
||||
let fontSizeAdjustment = 0
|
||||
let iterations = 0
|
||||
let nextHeight = NOTE_SIZE
|
||||
let labelHeight = NOTE_SIZE
|
||||
let labelWidth = NOTE_SIZE
|
||||
|
||||
// We slightly make the font smaller if the text is too big for the note, width-wise.
|
||||
do {
|
||||
fontSizeAdjustment = Math.min(unadjustedFontSize, unadjustedFontSize - iterations)
|
||||
const nextTextSize = editor.textMeasure.measureText(shape.props.text, {
|
||||
const nextTextSize = editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: fontSizeAdjustment,
|
||||
maxWidth: NOTE_SIZE - PADDING * 2,
|
||||
maxWidth: NOTE_SIZE - LABEL_PADDING * 2,
|
||||
disableOverflowWrapBreaking: true,
|
||||
})
|
||||
|
||||
nextHeight = nextTextSize.h + PADDING * 2
|
||||
labelHeight = nextTextSize.h + LABEL_PADDING * 2
|
||||
labelWidth = nextTextSize.w + LABEL_PADDING * 2
|
||||
|
||||
if (fontSizeAdjustment <= 14) {
|
||||
// Too small, just rely now on CSS `overflow-wrap: break-word`
|
||||
// We need to recalculate the text measurement here with break-word enabled.
|
||||
const nextTextSizeWithOverflowBreak = editor.textMeasure.measureText(text, {
|
||||
...TEXT_PROPS,
|
||||
fontFamily: FONT_FAMILIES[shape.props.font],
|
||||
fontSize: fontSizeAdjustment,
|
||||
maxWidth: NOTE_SIZE - LABEL_PADDING * 2,
|
||||
})
|
||||
labelHeight = nextTextSizeWithOverflowBreak.h + LABEL_PADDING * 2
|
||||
labelWidth = nextTextSizeWithOverflowBreak.w + LABEL_PADDING * 2
|
||||
break
|
||||
}
|
||||
|
||||
if (nextTextSize.scrollWidth.toFixed(0) === nextTextSize.w.toFixed(0)) {
|
||||
break
|
||||
}
|
||||
} while (iterations++ < 50)
|
||||
|
||||
let growY: number | null = null
|
||||
|
||||
if (nextHeight > NOTE_SIZE) {
|
||||
growY = nextHeight - NOTE_SIZE
|
||||
} else {
|
||||
if (prevGrowY) {
|
||||
growY = 0
|
||||
}
|
||||
return {
|
||||
labelHeight,
|
||||
labelWidth,
|
||||
fontSizeAdjustment,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
growY !== null ||
|
||||
(shape.props.fontSizeAdjustment === 0
|
||||
? fontSizeAdjustment !== unadjustedFontSize
|
||||
: fontSizeAdjustment !== shape.props.fontSizeAdjustment)
|
||||
) {
|
||||
return {
|
||||
...shape,
|
||||
props: {
|
||||
...shape.props,
|
||||
growY: growY ?? 0,
|
||||
fontSizeAdjustment,
|
||||
},
|
||||
}
|
||||
}
|
||||
const labelSizesForNote = new WeakMapCache<TLShape, ReturnType<typeof _getLabelSize>>()
|
||||
|
||||
function getLabelSize(editor: Editor, shape: TLNoteShape) {
|
||||
return labelSizesForNote.get(shape, () => _getLabelSize(editor, shape))
|
||||
}
|
||||
|
||||
function useNoteKeydownHandler(id: TLShapeId) {
|
||||
|
@ -333,7 +431,7 @@ function useNoteKeydownHandler(id: TLShapeId) {
|
|||
// Based on the inputs, calculate the offset to the next note
|
||||
// tab controls x axis (shift inverts direction set by RTL)
|
||||
// cmd enter is the y axis (shift inverts direction)
|
||||
const isRTL = !!(translation.isRTL || isRightToLeftLanguage(shape.props.text))
|
||||
const isRTL = !!(translation.dir === 'rtl' || isRightToLeftLanguage(shape.props.text))
|
||||
|
||||
const offsetLength =
|
||||
NOTE_SIZE +
|
||||
|
@ -346,17 +444,21 @@ function useNoteKeydownHandler(id: TLShapeId) {
|
|||
isCmdEnter ? (e.shiftKey ? -1 : 1) : 0
|
||||
)
|
||||
.mul(offsetLength)
|
||||
.add(CENTER_OFFSET)
|
||||
.add(NOTE_CENTER_OFFSET)
|
||||
.rot(pageRotation)
|
||||
.add(pageTransform.point())
|
||||
|
||||
const newNote = getNoteShapeForAdjacentPosition(editor, shape, adjacentCenter, pageRotation)
|
||||
|
||||
if (newNote) {
|
||||
startEditingNoteShape(editor, newNote)
|
||||
startEditingShapeWithLabel(editor, newNote, true /* selectAll */)
|
||||
}
|
||||
}
|
||||
},
|
||||
[id, editor, translation.isRTL]
|
||||
[id, editor, translation.dir]
|
||||
)
|
||||
}
|
||||
|
||||
function getNoteHeight(shape: TLNoteShape) {
|
||||
return NOTE_SIZE + shape.props.growY
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { Vec } from '@tldraw/editor'
|
||||
import { Box, Vec } from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
// We don't want the camera to move when the shape gets created off screen
|
||||
editor.updateViewportScreenBounds(new Box(0, 0, 2000, 2000))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor?.dispose()
|
||||
})
|
||||
|
@ -24,8 +27,14 @@ function testCloneHandles(x: number, y: number, rotation: number) {
|
|||
)
|
||||
|
||||
handles.forEach((handle, i) => {
|
||||
const handleInPageSpace = editor.getShapePageTransform(shape).applyToPoint(handle)
|
||||
editor.select(shape.id)
|
||||
editor.pointerDown(handle.x, handle.y, {
|
||||
editor.pointerMove(handleInPageSpace.x, handleInPageSpace.y)
|
||||
expect(editor.inputs.currentPagePoint).toMatchObject({
|
||||
x: handleInPageSpace.x,
|
||||
y: handleInPageSpace.y,
|
||||
})
|
||||
editor.pointerDown(handleInPageSpace.x, handleInPageSpace.y, {
|
||||
target: 'handle',
|
||||
shape,
|
||||
handle,
|
||||
|
@ -50,25 +59,21 @@ function testCloneHandles(x: number, y: number, rotation: number) {
|
|||
|
||||
editor.expectToBeIn('select.editing_shape')
|
||||
|
||||
editor.cancel().undo()
|
||||
editor.cancel().undo().forceTick()
|
||||
})
|
||||
}
|
||||
|
||||
describe('Note clone handles', () => {
|
||||
it('Creates a new sticky note using handles', () => {
|
||||
testCloneHandles(0, 0, 0)
|
||||
})
|
||||
|
||||
it('Creates a new sticky note when translated', () => {
|
||||
testCloneHandles(100, 100, 0)
|
||||
testCloneHandles(1000, 1000, 0)
|
||||
})
|
||||
|
||||
it('Creates a new sticky when rotated', () => {
|
||||
testCloneHandles(0, 0, Math.PI / 2)
|
||||
testCloneHandles(1000, 1000, Math.PI / 2)
|
||||
})
|
||||
|
||||
it('Creates a new sticky when translated and rotated', () => {
|
||||
testCloneHandles(100, 100, Math.PI / 2)
|
||||
testCloneHandles(1000, 1000, Math.PI / 2)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -82,9 +87,10 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
|
|||
const handles = editor.getShapeHandles(shape.id)!
|
||||
|
||||
handles.forEach((handle) => {
|
||||
const handleInPageSpace = editor.getShapePageTransform(shape).applyToPoint(handle)
|
||||
editor.select(shape.id)
|
||||
editor.pointerMove(handle.x, handle.y)
|
||||
editor.pointerDown(handle.x, handle.y, {
|
||||
editor.pointerMove(handleInPageSpace.x, handleInPageSpace.y)
|
||||
editor.pointerDown(handleInPageSpace.x, handleInPageSpace.y, {
|
||||
target: 'handle',
|
||||
shape,
|
||||
handle,
|
||||
|
@ -92,7 +98,7 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
|
|||
|
||||
editor.expectToBeIn('select.pointing_handle')
|
||||
|
||||
editor.pointerMove(handle.x + 30, handle.y + 30)
|
||||
editor.pointerMove(handleInPageSpace.x + 30, handleInPageSpace.y + 30)
|
||||
|
||||
editor.expectToBeIn('select.translating')
|
||||
|
||||
|
@ -105,8 +111,8 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
|
|||
editor.expectShapeToMatch({
|
||||
id: newShape.id,
|
||||
type: 'note',
|
||||
x: handle.x + 30 - offset.x,
|
||||
y: handle.y + 30 - offset.y,
|
||||
x: handleInPageSpace.x + 30 - offset.x,
|
||||
y: handleInPageSpace.y + 30 - offset.y,
|
||||
})
|
||||
|
||||
editor.pointerUp()
|
||||
|
@ -119,27 +125,23 @@ function testDragCloneHandles(x: number, y: number, rotation: number) {
|
|||
|
||||
describe('Dragging clone handles', () => {
|
||||
it('Creates a new sticky note using handles', () => {
|
||||
testDragCloneHandles(0, 0, 0)
|
||||
})
|
||||
|
||||
it('Creates a new sticky note when translated', () => {
|
||||
testDragCloneHandles(100, 100, 0)
|
||||
testDragCloneHandles(1000, 1000, 0)
|
||||
})
|
||||
|
||||
it('Creates a new sticky when rotated', () => {
|
||||
testDragCloneHandles(0, 0, Math.PI / 2)
|
||||
testDragCloneHandles(1000, 1000, Math.PI / 2)
|
||||
})
|
||||
|
||||
it('Creates a new sticky when translated and rotated', () => {
|
||||
testDragCloneHandles(100, 100, Math.PI / 2)
|
||||
testDragCloneHandles(1000, 1000, Math.PI / 2)
|
||||
})
|
||||
})
|
||||
|
||||
it('Selects an adjacent note when clicking the clone handle', () => {
|
||||
editor.createShape({ type: 'note', x: 220, y: 0 })
|
||||
editor.createShape({ type: 'note', x: 1220, y: 1000 })
|
||||
const shapeA = editor.getLastCreatedShape()!
|
||||
|
||||
editor.createShape({ type: 'note', x: 0, y: 0 })
|
||||
editor.createShape({ type: 'note', x: 1000, y: 1000 })
|
||||
const shapeB = editor.getLastCreatedShape()!
|
||||
|
||||
editor.select(shapeB.id)
|
||||
|
@ -171,17 +173,17 @@ it('Selects an adjacent note when clicking the clone handle', () => {
|
|||
})
|
||||
|
||||
it('Creates an adjacent note when dragging the clone handle', () => {
|
||||
editor.createShape({ type: 'note', x: 220, y: 0 })
|
||||
editor.createShape({ type: 'note', x: 1220, y: 1000 })
|
||||
const shapeA = editor.getLastCreatedShape()!
|
||||
|
||||
editor.createShape({ type: 'note', x: 0, y: 0 })
|
||||
editor.createShape({ type: 'note', x: 1000, y: 1000 })
|
||||
const shapeB = editor.getLastCreatedShape()!
|
||||
|
||||
editor.select(shapeB.id)
|
||||
|
||||
const handles = editor.getShapeHandles(shapeB.id)!
|
||||
|
||||
const handle = handles[1]
|
||||
const handle = handles[0]
|
||||
|
||||
editor.select(shapeB.id)
|
||||
editor.pointerDown(handle.x, handle.y, {
|
||||
|
@ -214,10 +216,10 @@ it('Creates an adjacent note when dragging the clone handle', () => {
|
|||
})
|
||||
|
||||
it('Does not put the new shape into a frame if its center is not in the frame', () => {
|
||||
editor.createShape({ type: 'frame', x: 321, y: 100 }) // one pixel too far...
|
||||
editor.createShape({ type: 'frame', x: 1321, y: 1000 }) // one pixel too far...
|
||||
const frameA = editor.getLastCreatedShape()!
|
||||
// center no longer in the frame
|
||||
editor.createShape({ type: 'note', x: 0, y: 0 })
|
||||
editor.createShape({ type: 'note', x: 1000, y: 1000 })
|
||||
const shapeA = editor.getLastCreatedShape()!
|
||||
// to the right
|
||||
const handle = editor.getShapeHandles(shapeA.id)![1]
|
||||
|
@ -237,10 +239,10 @@ it('Does not put the new shape into a frame if its center is not in the frame',
|
|||
})
|
||||
|
||||
it('Puts the new shape into a frame based on its center', () => {
|
||||
editor.createShape({ type: 'frame', x: 320, y: 100 })
|
||||
editor.createShape({ type: 'frame', x: 1320, y: 1100 })
|
||||
const frameA = editor.getLastCreatedShape()!
|
||||
// top left won't be in the frame, but the center will (barely but yes)
|
||||
editor.createShape({ type: 'note', x: 0, y: 0 })
|
||||
editor.createShape({ type: 'note', x: 1000, y: 1000 })
|
||||
const shapeA = editor.getLastCreatedShape()!
|
||||
// to the right
|
||||
const handle = editor.getShapeHandles(shapeA.id)![1]
|
||||
|
@ -260,10 +262,10 @@ it('Puts the new shape into a frame based on its center', () => {
|
|||
})
|
||||
|
||||
function testNoteShapeFrameRotations(sourceRotation: number, rotation: number) {
|
||||
editor.createShape({ type: 'frame', x: 220, y: 0, rotation: rotation })
|
||||
editor.createShape({ type: 'frame', x: 1220, y: 1000, rotation: rotation })
|
||||
const frameA = editor.getLastCreatedShape()!
|
||||
// top left won't be in the frame, but the center will (barely but yes)
|
||||
editor.createShape({ type: 'note', x: 0, y: 0, rotation: sourceRotation })
|
||||
editor.createShape({ type: 'note', x: 1000, y: 1000, rotation: sourceRotation })
|
||||
const shapeA = editor.getLastCreatedShape()!
|
||||
// to the right
|
||||
const handle = editor.getShapeHandles(shapeA.id)![1]
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import {
|
||||
ANIMATION_MEDIUM_MS,
|
||||
Editor,
|
||||
TLNoteShape,
|
||||
TLShape,
|
||||
Vec,
|
||||
compact,
|
||||
createShapeId,
|
||||
} from '@tldraw/editor'
|
||||
import { Editor, TLNoteShape, TLShape, Vec, compact, createShapeId } from '@tldraw/editor'
|
||||
import { zoomToShapeIfOffscreen } from '../shared/TextHelpers'
|
||||
|
||||
/** @internal */
|
||||
export const ADJACENT_NOTE_MARGIN = 20
|
||||
|
@ -15,13 +8,11 @@ export const CLONE_HANDLE_MARGIN = 0
|
|||
/** @internal */
|
||||
export const NOTE_SIZE = 200
|
||||
/** @internal */
|
||||
export const CENTER_OFFSET = { x: NOTE_SIZE / 2, y: NOTE_SIZE / 2 }
|
||||
export const NOTE_CENTER_OFFSET = { x: NOTE_SIZE / 2, y: NOTE_SIZE / 2 }
|
||||
/** @internal */
|
||||
export const NOTE_PIT_RADIUS = 10
|
||||
/** @internal */
|
||||
export type NotePit = Vec
|
||||
|
||||
const DEFAULT_PITS: NotePit[] = [
|
||||
const DEFAULT_PITS = [
|
||||
new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * -0.5 - ADJACENT_NOTE_MARGIN), // t
|
||||
new Vec(NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN, NOTE_SIZE * 0.5), // r
|
||||
new Vec(NOTE_SIZE * 0.5, NOTE_SIZE * 1.5 + ADJACENT_NOTE_MARGIN), // b
|
||||
|
@ -159,7 +150,7 @@ export function getNoteShapeForAdjacentPosition(
|
|||
// space as the newly created shape (i.e its parent's space)
|
||||
const topLeft = editor.getPointInParentSpace(
|
||||
createdShape,
|
||||
Vec.Sub(center, Vec.Rot(CENTER_OFFSET, pageRotation))
|
||||
Vec.Sub(center, Vec.Rot(NOTE_CENTER_OFFSET, pageRotation))
|
||||
)
|
||||
|
||||
editor.updateShape({
|
||||
|
@ -172,36 +163,6 @@ export function getNoteShapeForAdjacentPosition(
|
|||
nextNote = editor.getShape(id)!
|
||||
}
|
||||
|
||||
// Animate to the next sticky if it would be off screen
|
||||
const selectionPageBounds = editor.getSelectionPageBounds()
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
|
||||
editor.centerOnPoint(selectionPageBounds.center, {
|
||||
duration: ANIMATION_MEDIUM_MS,
|
||||
})
|
||||
}
|
||||
|
||||
zoomToShapeIfOffscreen(editor)
|
||||
return nextNote
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function startEditingNoteShape(editor: Editor, shape: TLShape) {
|
||||
// Finish this sticky and start editing the next one
|
||||
editor.select(shape)
|
||||
editor.setEditingShape(shape)
|
||||
editor.setCurrentTool('select.editing_shape', {
|
||||
target: 'shape',
|
||||
shape: shape,
|
||||
})
|
||||
|
||||
// Select any text that's in the newly selected sticky
|
||||
;(document.getElementById(`text-input-${shape.id}`) as HTMLTextAreaElement)?.select()
|
||||
|
||||
const selectionPageBounds = editor.getSelectionPageBounds()
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
|
||||
editor.centerOnPoint(selectionPageBounds.center, {
|
||||
duration: ANIMATION_MEDIUM_MS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { TLShapeId } from '@tldraw/editor'
|
||||
import { TestEditor } from '../../../test/TestEditor'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor()
|
||||
})
|
||||
afterEach(() => {
|
||||
editor?.dispose()
|
||||
})
|
||||
|
||||
function clickCreate(tool: string, [x, y]: [number, number]): TLShapeId {
|
||||
editor.setCurrentTool('note')
|
||||
editor.pointerDown(x, y)
|
||||
editor.pointerUp(x, y)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const noteId = shapes[0].id
|
||||
return noteId
|
||||
}
|
||||
|
||||
function dragCreate(
|
||||
tool: string,
|
||||
{
|
||||
from,
|
||||
to,
|
||||
}: {
|
||||
from: [number, number]
|
||||
to: [number, number]
|
||||
}
|
||||
): TLShapeId {
|
||||
editor.setCurrentTool(tool)
|
||||
editor.pointerDown(...from)
|
||||
editor.pointerMove(...to)
|
||||
editor.pointerUp(...to)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const rectId = shapes[0].id
|
||||
return rectId
|
||||
}
|
||||
|
||||
describe('note parenting', () => {
|
||||
it('accepts shapes as children', () => {
|
||||
const noteId = clickCreate('note', [0, 0])
|
||||
const rectId = dragCreate('geo', { from: [-50, -50], to: [50, 50] })
|
||||
expect(editor.getShape(rectId)!.parentId).toBe(noteId)
|
||||
})
|
||||
|
||||
it("doesn't accept frames as children", () => {
|
||||
clickCreate('note', [0, 0])
|
||||
const frameId = dragCreate('frame', { from: [-50, -50], to: [50, 50] })
|
||||
expect(editor.getShape(frameId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it('parents shapes when you drag them onto the note', () => {
|
||||
const noteId = clickCreate('note', [0, 0])
|
||||
const rectId = dragCreate('geo', { from: [200, 200], to: [300, 300] })
|
||||
|
||||
expect(editor.getShape(rectId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerDown(250, 250)
|
||||
editor.pointerMove(0, 0)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(rectId)!.parentId).toBe(noteId)
|
||||
editor.pointerUp()
|
||||
})
|
||||
|
||||
it("doesn't parent frames when you drag them onto the note", () => {
|
||||
clickCreate('note', [0, 0])
|
||||
const frameId = dragCreate('frame', { from: [200, 200], to: [300, 300] })
|
||||
|
||||
expect(editor.getShape(frameId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerDown(250, 250)
|
||||
editor.pointerMove(0, 0)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(frameId)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerUp()
|
||||
})
|
||||
})
|
|
@ -4,6 +4,8 @@
|
|||
* Copyright (c) Federico Brigante <opensource@bfred.it> (bfred.it)
|
||||
*/
|
||||
|
||||
import { ANIMATION_MEDIUM_MS, Editor, TLShape } from '@tldraw/editor'
|
||||
|
||||
// TODO: Most of this file can be moved into a DOM utils library.
|
||||
|
||||
/** @internal */
|
||||
|
@ -290,3 +292,50 @@ function getCaretIndex(element: HTMLElement) {
|
|||
}
|
||||
return position
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function startEditingShapeWithLabel(
|
||||
editor: Editor,
|
||||
shape: TLShape,
|
||||
shouldSelectAll?: boolean
|
||||
) {
|
||||
// Finish this shape and start editing the next one
|
||||
editor.select(shape)
|
||||
editor.mark('editing shape')
|
||||
editor.setEditingShape(shape)
|
||||
editor.setCurrentTool('select.editing_shape', {
|
||||
target: 'shape',
|
||||
shape: shape,
|
||||
})
|
||||
|
||||
if (shouldSelectAll) {
|
||||
// Select any text that's in the newly selected sticky
|
||||
;(document.getElementById(`text-input-${shape.id}`) as HTMLTextAreaElement)?.select()
|
||||
}
|
||||
|
||||
zoomToShapeIfOffscreen(editor)
|
||||
}
|
||||
|
||||
const ZOOM_TO_SHAPE_PADDING = 16
|
||||
export function zoomToShapeIfOffscreen(editor: Editor) {
|
||||
const selectionPageBounds = editor.getSelectionPageBounds()
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
|
||||
const eb = selectionPageBounds
|
||||
.clone()
|
||||
// Expand the bounds by the padding
|
||||
.expandBy(ZOOM_TO_SHAPE_PADDING / editor.getZoomLevel())
|
||||
// then expand the bounds to include the viewport bounds
|
||||
.expand(viewportPageBounds)
|
||||
|
||||
// then use the difference between the centers to calculate the offset
|
||||
const nextBounds = viewportPageBounds.clone().translate({
|
||||
x: (eb.center.x - viewportPageBounds.center.x) * 2,
|
||||
y: (eb.center.y - viewportPageBounds.center.y) * 2,
|
||||
})
|
||||
editor.zoomToBounds(nextBounds, {
|
||||
duration: ANIMATION_MEDIUM_MS,
|
||||
inset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import {
|
||||
Box,
|
||||
TLDefaultColorStyle,
|
||||
TLDefaultFillStyle,
|
||||
TLDefaultFontStyle,
|
||||
TLDefaultHorizontalAlignStyle,
|
||||
TLDefaultVerticalAlignStyle,
|
||||
TLShapeId,
|
||||
getDefaultColorTheme,
|
||||
useIsDarkMode,
|
||||
} from '@tldraw/editor'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { TextArea } from '../text/TextArea'
|
||||
|
@ -26,9 +23,10 @@ type TextLabelProps = {
|
|||
verticalAlign: TLDefaultVerticalAlignStyle
|
||||
wrap?: boolean
|
||||
text: string
|
||||
labelColor: TLDefaultColorStyle
|
||||
labelColor: string
|
||||
bounds?: Box
|
||||
isNote?: boolean
|
||||
isSelected: boolean
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void
|
||||
classNamePrefix?: string
|
||||
style?: React.CSSProperties
|
||||
|
@ -48,15 +46,18 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
align,
|
||||
verticalAlign,
|
||||
wrap,
|
||||
bounds,
|
||||
isNote,
|
||||
isSelected,
|
||||
onKeyDown: handleKeyDownCustom,
|
||||
classNamePrefix,
|
||||
style,
|
||||
textWidth,
|
||||
textHeight,
|
||||
}: TextLabelProps) {
|
||||
const { rInput, isEmpty, isEditing, ...editableTextRest } = useEditableText(id, type, text)
|
||||
const { rInput, isEmpty, isEditing, isEditingAnything, ...editableTextRest } = useEditableText(
|
||||
id,
|
||||
type,
|
||||
text
|
||||
)
|
||||
|
||||
const [initialText, setInitialText] = useState(text)
|
||||
useEffect(() => {
|
||||
|
@ -69,7 +70,6 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
const hasText = finalText.length > 0
|
||||
|
||||
const legacyAlign = isLegacyAlign(align)
|
||||
const theme = getDefaultColorTheme({ isDarkMode: useIsDarkMode() })
|
||||
|
||||
if (!isEditing && !hasText) {
|
||||
return null
|
||||
|
@ -84,30 +84,23 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
data-align={align}
|
||||
data-hastext={!isEmpty}
|
||||
data-isediting={isEditing}
|
||||
data-iseditinganything={isEditingAnything}
|
||||
data-textwrap={!!wrap}
|
||||
data-isselected={isSelected}
|
||||
style={{
|
||||
justifyContent: align === 'middle' || legacyAlign ? 'center' : align,
|
||||
alignItems: verticalAlign === 'middle' ? 'center' : verticalAlign,
|
||||
...(bounds
|
||||
? {
|
||||
top: bounds.minY,
|
||||
left: bounds.minX,
|
||||
width: bounds.width,
|
||||
height: bounds.height,
|
||||
position: 'absolute',
|
||||
}
|
||||
: {}),
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`${cssPrefix}-label__inner`}
|
||||
className={`${cssPrefix}-label__inner tl-text-content__wrapper`}
|
||||
style={{
|
||||
fontSize,
|
||||
lineHeight: fontSize * lineHeight + 'px',
|
||||
minHeight: lineHeight + 32,
|
||||
minWidth: textWidth || 0,
|
||||
color: isNote ? theme[labelColor].note.text : 'solid',
|
||||
color: labelColor,
|
||||
width: textWidth,
|
||||
height: textHeight,
|
||||
}}
|
||||
|
@ -115,17 +108,19 @@ export const TextLabel = React.memo(function TextLabel({
|
|||
<div className={`${cssPrefix} tl-text tl-text-content`} dir="ltr">
|
||||
{finalText}
|
||||
</div>
|
||||
<TextArea
|
||||
id={`text-input-${id}`}
|
||||
ref={rInput}
|
||||
// We need to add the initial value as the key here because we need this component to
|
||||
// 'reset' when this state changes and grab the latest defaultValue.
|
||||
key={initialText}
|
||||
text={text}
|
||||
isEditing={isEditing}
|
||||
{...editableTextRest}
|
||||
handleKeyDown={handleKeyDownCustom ?? editableTextRest.handleKeyDown}
|
||||
/>
|
||||
{(isEditingAnything || isSelected) && (
|
||||
<TextArea
|
||||
id={`text-input-${id}`}
|
||||
ref={rInput}
|
||||
// We need to add the initial value as the key here because we need this component to
|
||||
// 'reset' when this state changes and grab the latest defaultValue.
|
||||
key={initialText}
|
||||
text={text}
|
||||
isEditing={isEditing}
|
||||
{...editableTextRest}
|
||||
handleKeyDown={handleKeyDownCustom ?? editableTextRest.handleKeyDown}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
TLShapeId,
|
||||
TLUnknownShape,
|
||||
getPointerInfo,
|
||||
setPointerCapture,
|
||||
stopEventPropagation,
|
||||
useEditor,
|
||||
useValue,
|
||||
|
@ -18,6 +19,12 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
|
|||
const rInput = useRef<HTMLTextAreaElement>(null)
|
||||
const rSelectionRanges = useRef<Range[] | null>()
|
||||
|
||||
const isEditingAnything = useValue(
|
||||
'isEditingAnything',
|
||||
() => editor.getEditingShapeId() !== null,
|
||||
[editor]
|
||||
)
|
||||
|
||||
const isEditing = useValue('isEditing', () => editor.getEditingShapeId() === id, [editor, id])
|
||||
|
||||
// If the shape is editing but the input element not focused, focus the element
|
||||
|
@ -38,10 +45,10 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
|
|||
const elm = rInput.current
|
||||
const editingShapeId = editor.getEditingShapeId()
|
||||
// Did we move to a different shape?
|
||||
if (elm && editingShapeId) {
|
||||
if (editingShapeId) {
|
||||
// important! these ^v are two different things
|
||||
// is that shape OUR shape?
|
||||
if (editingShapeId === id) {
|
||||
if (elm && editingShapeId === id) {
|
||||
if (ranges) {
|
||||
if (!ranges.length) {
|
||||
// If we don't have any ranges, restore selection
|
||||
|
@ -61,7 +68,6 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
|
|||
}
|
||||
} else {
|
||||
window.getSelection()?.removeAllRanges()
|
||||
editor.complete()
|
||||
}
|
||||
})
|
||||
}, [editor, id])
|
||||
|
@ -151,6 +157,10 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
|
|||
})
|
||||
|
||||
stopEventPropagation(e) // we need to prevent blurring the input
|
||||
|
||||
// This is important so that when dragging a shape using the text label,
|
||||
// the shape continues to be dragged, even if the cursor is over the UI.
|
||||
setPointerCapture(e.currentTarget, e)
|
||||
},
|
||||
[editor, id]
|
||||
)
|
||||
|
@ -160,14 +170,17 @@ export function useEditableText(id: TLShapeId, type: string, text: string) {
|
|||
return {
|
||||
rInput,
|
||||
isEditing,
|
||||
handleFocus: () => {
|
||||
/* noop */
|
||||
},
|
||||
handleFocus: noop,
|
||||
handleBlur,
|
||||
handleKeyDown,
|
||||
handleChange,
|
||||
handleInputPointerDown,
|
||||
handleDoubleClick,
|
||||
isEmpty,
|
||||
isEditingAnything,
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -52,9 +52,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
|
|||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
onTouchEnd={stopEventPropagation}
|
||||
onContextMenu={(e) => {
|
||||
isEditing && stopEventPropagation(e)
|
||||
}}
|
||||
onContextMenu={isEditing ? stopEventPropagation : undefined}
|
||||
onPointerDown={handleInputPointerDown}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import {
|
||||
Box,
|
||||
Editor,
|
||||
HTMLContainer,
|
||||
Rectangle2d,
|
||||
ShapeUtil,
|
||||
SvgExportContext,
|
||||
|
@ -17,6 +16,7 @@ import {
|
|||
toDomPrecision,
|
||||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import { useDefaultColorTheme } from '../shared/ShapeFill'
|
||||
import { SvgTextLabel } from '../shared/SvgTextLabel'
|
||||
import { TextLabel } from '../shared/TextLabel'
|
||||
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from '../shared/default-shape-constants'
|
||||
|
@ -70,29 +70,30 @@ export class TextShapeUtil extends ShapeUtil<TLTextShape> {
|
|||
} = shape
|
||||
|
||||
const { width, height } = this.getMinDimensions(shape)
|
||||
const isSelected = shape.id === this.editor.getOnlySelectedShapeId()
|
||||
const theme = useDefaultColorTheme()
|
||||
|
||||
return (
|
||||
<HTMLContainer id={shape.id}>
|
||||
<TextLabel
|
||||
id={id}
|
||||
classNamePrefix="tl-text-shape"
|
||||
type="text"
|
||||
font={font}
|
||||
fontSize={FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align={align}
|
||||
verticalAlign="middle"
|
||||
text={text}
|
||||
labelColor={color}
|
||||
textWidth={width}
|
||||
textHeight={height}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top left',
|
||||
}}
|
||||
wrap
|
||||
/>
|
||||
</HTMLContainer>
|
||||
<TextLabel
|
||||
id={id}
|
||||
classNamePrefix="tl-text-shape"
|
||||
type="text"
|
||||
font={font}
|
||||
fontSize={FONT_SIZES[size]}
|
||||
lineHeight={TEXT_PROPS.lineHeight}
|
||||
align={align}
|
||||
verticalAlign="middle"
|
||||
text={text}
|
||||
labelColor={theme[color].solid}
|
||||
isSelected={isSelected}
|
||||
textWidth={width}
|
||||
textHeight={height}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: 'top left',
|
||||
}}
|
||||
wrap
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { Editor, TLShape, TLShapeId, Vec, compact } from '@tldraw/editor'
|
||||
import { isShapeOccluded } from './selectHelpers'
|
||||
|
||||
const LAG_DURATION = 100
|
||||
const INITIAL_POINTER_LAG_DURATION = 20
|
||||
const FAST_POINTER_LAG_DURATION = 100
|
||||
|
||||
/** @public */
|
||||
export class DragAndDropManager {
|
||||
|
@ -16,6 +18,12 @@ export class DragAndDropManager {
|
|||
|
||||
updateDroppingNode(movingShapes: TLShape[], cb: () => void) {
|
||||
if (this.first) {
|
||||
this.editor.setHintingShapes(
|
||||
movingShapes
|
||||
.map((s) => this.editor.findShapeAncestor(s, (v) => v.type !== 'group'))
|
||||
.filter((s) => s) as TLShape[]
|
||||
)
|
||||
|
||||
this.prevDroppingShapeId =
|
||||
this.editor.getDroppingOverShape(this.editor.inputs.originPagePoint, movingShapes)?.id ??
|
||||
null
|
||||
|
@ -23,10 +31,10 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
if (this.droppingNodeTimer === null) {
|
||||
this.setDragTimer(movingShapes, LAG_DURATION * 10, cb)
|
||||
this.setDragTimer(movingShapes, INITIAL_POINTER_LAG_DURATION, cb)
|
||||
} else if (this.editor.inputs.pointerVelocity.len() > 0.5) {
|
||||
clearInterval(this.droppingNodeTimer)
|
||||
this.setDragTimer(movingShapes, LAG_DURATION, cb)
|
||||
this.setDragTimer(movingShapes, FAST_POINTER_LAG_DURATION, cb)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +54,7 @@ export class DragAndDropManager {
|
|||
|
||||
// is the next dropping shape id different than the last one?
|
||||
if (nextDroppingShapeId === this.prevDroppingShapeId) {
|
||||
this.hintParents(movingShapes)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -64,24 +73,42 @@ export class DragAndDropManager {
|
|||
}
|
||||
|
||||
if (nextDroppingShape) {
|
||||
const res = this.editor
|
||||
this.editor
|
||||
.getShapeUtil(nextDroppingShape)
|
||||
.onDragShapesOver?.(nextDroppingShape, movingShapes)
|
||||
|
||||
if (res && res.shouldHint) {
|
||||
this.editor.setHintingShapes([nextDroppingShape.id])
|
||||
}
|
||||
} else {
|
||||
// If we're dropping onto the page, then clear hinting ids
|
||||
this.editor.setHintingShapes([])
|
||||
}
|
||||
|
||||
this.hintParents(movingShapes)
|
||||
cb?.()
|
||||
|
||||
// next -> curr
|
||||
this.prevDroppingShapeId = nextDroppingShapeId
|
||||
}
|
||||
|
||||
hintParents(movingShapes: TLShape[]) {
|
||||
// Group moving shapes by their ancestor
|
||||
const shapesGroupedByAncestor = new Map<TLShapeId, TLShape[]>()
|
||||
for (const shape of movingShapes) {
|
||||
const ancestor = this.editor.findShapeAncestor(shape, (v) => v.type !== 'group')
|
||||
if (!ancestor) continue
|
||||
const shapes = shapesGroupedByAncestor.get(ancestor.id) ?? []
|
||||
shapes.push(shape)
|
||||
shapesGroupedByAncestor.set(ancestor.id, shapes)
|
||||
}
|
||||
|
||||
// Only hint an ancestor if some shapes will drop into it on pointer up
|
||||
const hintingShapes = []
|
||||
for (const [ancestorId, shapes] of shapesGroupedByAncestor) {
|
||||
const ancestor = this.editor.getShape(ancestorId)
|
||||
if (!ancestor) continue
|
||||
if (shapes.some((shape) => !isShapeOccluded(this.editor, ancestor, shape.id))) {
|
||||
hintingShapes.push(ancestor.id)
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.setHintingShapes(hintingShapes)
|
||||
}
|
||||
|
||||
dropShapes(shapes: TLShape[]) {
|
||||
const { prevDroppingShapeId } = this
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Vec,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
import { MIN_CROP_SIZE } from './Crop/crop-constants'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
|
||||
|
@ -206,6 +207,7 @@ export class Cropping extends StateNode {
|
|||
|
||||
private complete() {
|
||||
this.updateShapes()
|
||||
kickoutOccludedShapes(this.editor, [this.snapshot.shape.id])
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
sortByIndex,
|
||||
structuredClone,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
export class DraggingHandle extends StateNode {
|
||||
static override id = 'dragging_handle'
|
||||
|
@ -203,6 +204,7 @@ export class DraggingHandle extends StateNode {
|
|||
|
||||
private complete() {
|
||||
this.editor.snaps.clearIndicators()
|
||||
kickoutOccludedShapes(this.editor, [this.shapeId])
|
||||
|
||||
const { onInteractionEnd } = this.info
|
||||
if (this.editor.getInstanceState().isToolLocked && onInteractionEnd) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { StateNode, TLEventHandlers, TLFrameShape, TLTextShape } from '@tldraw/editor'
|
||||
import { StateNode, TLEventHandlers, TLFrameShape, TLShape, TLTextShape } from '@tldraw/editor'
|
||||
import { getTextLabels } from '../../../utils/shapes/shapes'
|
||||
import { getHitShapeOnCanvasPointerDown } from '../../selection-logic/getHitShapeOnCanvasPointerDown'
|
||||
import { updateHoveredId } from '../../selection-logic/updateHoveredId'
|
||||
|
@ -6,9 +6,12 @@ import { updateHoveredId } from '../../selection-logic/updateHoveredId'
|
|||
export class EditingShape extends StateNode {
|
||||
static override id = 'editing_shape'
|
||||
|
||||
hitShapeForPointerUp: TLShape | null = null
|
||||
|
||||
override onEnter = () => {
|
||||
const editingShape = this.editor.getEditingShape()
|
||||
if (!editingShape) throw Error('Entered editing state without an editing shape')
|
||||
this.hitShapeForPointerUp = null
|
||||
updateHoveredId(this.editor)
|
||||
this.editor.select(editingShape)
|
||||
}
|
||||
|
@ -28,6 +31,16 @@ export class EditingShape extends StateNode {
|
|||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
// In the case where on pointer down we hit a shape's label, we need to check if the user is dragging.
|
||||
// and if they are, we need to transition to translating instead.
|
||||
if (this.hitShapeForPointerUp && this.editor.inputs.isDragging) {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.editor.select(this.hitShapeForPointerUp)
|
||||
this.parent.transition('translating', info)
|
||||
this.hitShapeForPointerUp = null
|
||||
return
|
||||
}
|
||||
|
||||
switch (info.target) {
|
||||
case 'shape':
|
||||
case 'canvas': {
|
||||
|
@ -36,6 +49,7 @@ export class EditingShape extends StateNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override onPointerDown: TLEventHandlers['onPointerDown'] = (info) => {
|
||||
switch (info.target) {
|
||||
case 'canvas': {
|
||||
|
@ -80,22 +94,11 @@ export class EditingShape extends StateNode {
|
|||
// If we clicked on the editing geo / arrow shape's label, do nothing
|
||||
return
|
||||
} else {
|
||||
// Stay in edit mode to maintain flow of editing.
|
||||
this.editor.batch(() => {
|
||||
this.editor.mark('editing on pointer up')
|
||||
this.editor.select(selectingShape.id)
|
||||
this.hitShapeForPointerUp = selectingShape
|
||||
|
||||
const util = this.editor.getShapeUtil(selectingShape)
|
||||
if (this.editor.getInstanceState().isReadonly) {
|
||||
if (!util.canEditInReadOnly(selectingShape)) {
|
||||
this.parent.transition('pointing_shape', info)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.setEditingShape(selectingShape.id)
|
||||
this.editor.setCurrentTool('select.editing_shape')
|
||||
})
|
||||
// When clicking on a different shape's label, we need to clear the other selection
|
||||
// proactively until the pointer up happens.
|
||||
requestAnimationFrame(() => window.getSelection()?.removeAllRanges())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +126,32 @@ export class EditingShape extends StateNode {
|
|||
this.editor.root.handleEvent(info)
|
||||
}
|
||||
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = (info) => {
|
||||
// If we're not dragging, and it's a hit to the label, begin editing the shape.
|
||||
const hitShape = this.hitShapeForPointerUp
|
||||
if (hitShape) {
|
||||
// Stay in edit mode to maintain flow of editing.
|
||||
this.editor.batch(() => {
|
||||
if (!hitShape) return
|
||||
|
||||
this.editor.mark('editing on pointer up')
|
||||
this.editor.select(hitShape.id)
|
||||
|
||||
const util = this.editor.getShapeUtil(hitShape)
|
||||
if (this.editor.getInstanceState().isReadonly) {
|
||||
if (!util.canEditInReadOnly(hitShape)) {
|
||||
this.parent.transition('pointing_shape', info)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.setEditingShape(hitShape.id)
|
||||
this.editor.setCurrentTool('select.editing_shape')
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
override onComplete: TLEventHandlers['onComplete'] = (info) => {
|
||||
this.parent.transition('idle', info)
|
||||
}
|
||||
|
|
|
@ -15,10 +15,12 @@ import {
|
|||
createShapeId,
|
||||
pointInPolygon,
|
||||
} from '@tldraw/editor'
|
||||
import { startEditingShapeWithLabel } from '../../../shapes/shared/TextHelpers'
|
||||
import { getHitShapeOnCanvasPointerDown } from '../../selection-logic/getHitShapeOnCanvasPointerDown'
|
||||
import { getShouldEnterCropMode } from '../../selection-logic/getShouldEnterCropModeOnPointerDown'
|
||||
import { selectOnCanvasPointerUp } from '../../selection-logic/selectOnCanvasPointerUp'
|
||||
import { updateHoveredId } from '../../selection-logic/updateHoveredId'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
const SKIPPED_KEYS_FOR_AUTO_EDITING = [
|
||||
'Delete',
|
||||
|
@ -268,6 +270,7 @@ export class Idle extends StateNode {
|
|||
if (change) {
|
||||
this.editor.mark('double click edge')
|
||||
this.editor.updateShapes([change])
|
||||
kickoutOccludedShapes(this.editor, [onlySelectedShape.id])
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -282,7 +285,7 @@ export class Idle extends StateNode {
|
|||
}
|
||||
|
||||
if (this.shouldStartEditingShape(onlySelectedShape)) {
|
||||
this.startEditingShape(onlySelectedShape, info)
|
||||
this.startEditingShape(onlySelectedShape, info, true /* select all */)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
@ -316,7 +319,7 @@ export class Idle extends StateNode {
|
|||
|
||||
// If the shape can edit, then begin editing
|
||||
if (this.shouldStartEditingShape(shape)) {
|
||||
this.startEditingShape(shape, info)
|
||||
this.startEditingShape(shape, info, true /* select all */)
|
||||
} else {
|
||||
// If the shape's double click handler has not created a change,
|
||||
// and if the shape cannot edit, then create a text shape and
|
||||
|
@ -338,7 +341,7 @@ export class Idle extends StateNode {
|
|||
// If the shape's double click handler has not created a change,
|
||||
// and if the shape can edit, then begin editing the shape.
|
||||
if (this.shouldStartEditingShape(shape)) {
|
||||
this.startEditingShape(shape, info)
|
||||
this.startEditingShape(shape, info, true /* select all */)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -444,11 +447,15 @@ export class Idle extends StateNode {
|
|||
this.shouldStartEditingShape(onlySelectedShape) &&
|
||||
this.editor.getShapeUtil(onlySelectedShape).doesAutoEditOnKeyStroke(onlySelectedShape)
|
||||
) {
|
||||
this.startEditingShape(onlySelectedShape, {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
})
|
||||
this.startEditingShape(
|
||||
onlySelectedShape,
|
||||
{
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
},
|
||||
true /* select all */
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -484,17 +491,15 @@ export class Idle extends StateNode {
|
|||
// If the only selected shape is editable, then begin editing it
|
||||
const onlySelectedShape = this.editor.getOnlySelectedShape()
|
||||
if (onlySelectedShape && this.shouldStartEditingShape(onlySelectedShape)) {
|
||||
this.startEditingShape(onlySelectedShape, {
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
})
|
||||
|
||||
// XXX this is a hack to select the text in the textarea when we hit enter.
|
||||
// Open to other ideas! I don't see how else to currently do this in the codebase.
|
||||
;(
|
||||
document.getElementById(`text-input-${onlySelectedShape.id}`) as HTMLTextAreaElement
|
||||
)?.select()
|
||||
this.startEditingShape(
|
||||
onlySelectedShape,
|
||||
{
|
||||
...info,
|
||||
target: 'shape',
|
||||
shape: onlySelectedShape,
|
||||
},
|
||||
true /* select all */
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -516,10 +521,13 @@ export class Idle extends StateNode {
|
|||
return this.editor.getShapeUtil(shape).canEdit(shape)
|
||||
}
|
||||
|
||||
private startEditingShape(shape: TLShape, info: TLClickEventInfo | TLKeyboardEventInfo) {
|
||||
private startEditingShape(
|
||||
shape: TLShape,
|
||||
info: TLClickEventInfo | TLKeyboardEventInfo,
|
||||
shouldSelectAll?: boolean
|
||||
) {
|
||||
if (this.editor.isShapeOrAncestorLocked(shape) && shape.type !== 'embed') return
|
||||
this.editor.mark('editing shape')
|
||||
this.editor.setEditingShape(shape.id)
|
||||
startEditingShapeWithLabel(this.editor, shape, shouldSelectAll)
|
||||
this.parent.transition('editing_shape', info)
|
||||
}
|
||||
|
||||
|
@ -618,7 +626,9 @@ export class Idle extends StateNode {
|
|||
? MAJOR_NUDGE_FACTOR
|
||||
: MINOR_NUDGE_FACTOR
|
||||
|
||||
this.editor.nudgeShapes(this.editor.getSelectedShapeIds(), delta.mul(step))
|
||||
const selectedShapeIds = this.editor.getSelectedShapeIds()
|
||||
this.editor.nudgeShapes(selectedShapeIds, delta.mul(step))
|
||||
kickoutOccludedShapes(this.editor, selectedShapeIds)
|
||||
}
|
||||
|
||||
private canInteractWithShapeInReadOnly(shape: TLShape) {
|
||||
|
|
|
@ -41,7 +41,7 @@ export class PointingArrowLabel extends StateNode {
|
|||
this.info = info
|
||||
this.shapeId = shape.id
|
||||
this.didDrag = false
|
||||
this.wasAlreadySelected = this.editor.getOnlySelectedShape()?.id === shape.id
|
||||
this.wasAlreadySelected = this.editor.getOnlySelectedShapeId() === shape.id
|
||||
this.updateCursor()
|
||||
|
||||
const geometry = this.editor.getShapeGeometry<Group2d>(shape)
|
||||
|
|
|
@ -37,16 +37,23 @@ export class PointingCropHandle extends StateNode {
|
|||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const isDragging = this.editor.inputs.isDragging
|
||||
|
||||
if (isDragging) {
|
||||
this.parent.transition('cropping', {
|
||||
...this.info,
|
||||
onInteractionEnd: this.info.onInteractionEnd,
|
||||
})
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.startCropping()
|
||||
}
|
||||
}
|
||||
|
||||
override onLongPress: TLEventHandlers['onLongPress'] = () => {
|
||||
this.startCropping()
|
||||
}
|
||||
|
||||
private startCropping() {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('cropping', {
|
||||
...this.info,
|
||||
onInteractionEnd: this.info.onInteractionEnd,
|
||||
})
|
||||
}
|
||||
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
|
|
|
@ -9,11 +9,11 @@ import {
|
|||
Vec,
|
||||
} from '@tldraw/editor'
|
||||
import {
|
||||
CENTER_OFFSET,
|
||||
NOTE_CENTER_OFFSET,
|
||||
getNoteAdjacentPositions,
|
||||
getNoteShapeForAdjacentPosition,
|
||||
startEditingNoteShape,
|
||||
} from '../../../shapes/note/noteHelpers'
|
||||
import { startEditingShapeWithLabel } from '../../../shapes/shared/TextHelpers'
|
||||
|
||||
export class PointingHandle extends StateNode {
|
||||
static override id = 'pointing_handle'
|
||||
|
@ -53,7 +53,7 @@ export class PointingHandle extends StateNode {
|
|||
const { editor } = this
|
||||
const nextNote = getNoteForPit(editor, shape, handle, false)
|
||||
if (nextNote) {
|
||||
startEditingNoteShape(editor, nextNote)
|
||||
startEditingShapeWithLabel(editor, nextNote, true /* selectAll */)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,8 @@ export class PointingHandle extends StateNode {
|
|||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const { editor } = this
|
||||
if (editor.inputs.isDragging) {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
|
||||
const { shape, handle } = this.info
|
||||
|
||||
if (editor.isShapeOfType<TLNoteShape>(shape, 'note')) {
|
||||
|
@ -72,7 +74,7 @@ export class PointingHandle extends StateNode {
|
|||
// Center the shape on the current pointer
|
||||
const centeredOnPointer = editor
|
||||
.getPointInParentSpace(nextNote, editor.inputs.originPagePoint)
|
||||
.sub(Vec.Rot(CENTER_OFFSET, nextNote.rotation))
|
||||
.sub(Vec.Rot(NOTE_CENTER_OFFSET, nextNote.rotation))
|
||||
editor.updateShape({ ...nextNote, x: centeredOnPointer.x, y: centeredOnPointer.y })
|
||||
|
||||
// Then select and begin translating the shape
|
||||
|
@ -87,17 +89,26 @@ export class PointingHandle extends StateNode {
|
|||
isCreating: true,
|
||||
onCreate: () => {
|
||||
// When we're done, start editing it
|
||||
startEditingNoteShape(editor, nextNote)
|
||||
startEditingShapeWithLabel(editor, nextNote, true /* selectAll */)
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.parent.transition('dragging_handle', this.info)
|
||||
this.startDraggingHandle()
|
||||
}
|
||||
}
|
||||
|
||||
override onLongPress: TLEventHandlers['onLongPress'] = () => {
|
||||
this.startDraggingHandle()
|
||||
}
|
||||
|
||||
private startDraggingHandle() {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('dragging_handle', this.info)
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
this.cancel()
|
||||
}
|
||||
|
|
|
@ -48,13 +48,20 @@ export class PointingResizeHandle extends StateNode {
|
|||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
const isDragging = this.editor.inputs.isDragging
|
||||
|
||||
if (isDragging) {
|
||||
this.parent.transition('resizing', this.info)
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.startResizing()
|
||||
}
|
||||
}
|
||||
|
||||
override onLongPress: TLEventHandlers['onLongPress'] = () => {
|
||||
this.startResizing()
|
||||
}
|
||||
|
||||
private startResizing() {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('resizing', this.info)
|
||||
}
|
||||
|
||||
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
|
|
@ -33,14 +33,21 @@ export class PointingRotateHandle extends StateNode {
|
|||
)
|
||||
}
|
||||
|
||||
override onPointerMove = () => {
|
||||
const { isDragging } = this.editor.inputs
|
||||
|
||||
if (isDragging) {
|
||||
this.parent.transition('rotating', this.info)
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = () => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
this.startRotating()
|
||||
}
|
||||
}
|
||||
|
||||
override onLongPress: TLEventHandlers['onLongPress'] = () => {
|
||||
this.startRotating()
|
||||
}
|
||||
|
||||
private startRotating() {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('rotating', this.info)
|
||||
}
|
||||
|
||||
override onPointerUp = () => {
|
||||
this.complete()
|
||||
}
|
||||
|
|
|
@ -25,11 +25,19 @@ export class PointingSelection extends StateNode {
|
|||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('translating', info)
|
||||
this.startTranslating(info)
|
||||
}
|
||||
}
|
||||
|
||||
override onLongPress: TLEventHandlers['onLongPress'] = (info) => {
|
||||
this.startTranslating(info)
|
||||
}
|
||||
|
||||
private startTranslating(info: TLPointerEventInfo) {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('translating', info)
|
||||
}
|
||||
|
||||
override onDoubleClick?: TLClickEvent | undefined = (info) => {
|
||||
const hoveredShape = this.editor.getHoveredShape()
|
||||
const hitShape =
|
||||
|
|
|
@ -12,6 +12,7 @@ export class PointingShape extends StateNode {
|
|||
|
||||
hitShape = {} as TLShape
|
||||
hitShapeForPointerUp = {} as TLShape
|
||||
isDoubleClick = false
|
||||
|
||||
didSelectOnEnter = false
|
||||
|
||||
|
@ -24,7 +25,11 @@ export class PointingShape extends StateNode {
|
|||
} = this.editor
|
||||
|
||||
this.hitShape = info.shape
|
||||
this.isDoubleClick = false
|
||||
const outermostSelectingShape = this.editor.getOutermostSelectableShape(info.shape)
|
||||
const selectedAncestor = this.editor.findShapeAncestor(outermostSelectingShape, (parent) =>
|
||||
selectedShapeIds.includes(parent.id)
|
||||
)
|
||||
|
||||
if (
|
||||
// If the shape has an onClick handler
|
||||
|
@ -33,7 +38,9 @@ export class PointingShape extends StateNode {
|
|||
outermostSelectingShape.id === focusedGroupId ||
|
||||
// ...or if the shape is within the selection
|
||||
selectedShapeIds.includes(outermostSelectingShape.id) ||
|
||||
this.editor.isAncestorSelected(outermostSelectingShape.id) ||
|
||||
// ...or if an ancestor of the shape is selected (except note shapes)...
|
||||
// todo: Consider adding a flag for this hardcoded behaviour
|
||||
(selectedAncestor && selectedAncestor.type !== 'note') ||
|
||||
// ...or if the current point is NOT within the selection bounds
|
||||
(selectedShapeIds.length > 1 && selectionBounds?.containsPoint(currentPagePoint))
|
||||
) {
|
||||
|
@ -155,6 +162,16 @@ export class PointingShape extends StateNode {
|
|||
|
||||
this.editor.setEditingShape(selectingShape.id)
|
||||
this.editor.setCurrentTool('select.editing_shape')
|
||||
|
||||
if (this.isDoubleClick) {
|
||||
// XXX this is a hack to select the text in the textarea when we hit enter.
|
||||
// Open to other ideas! I don't see how else to currently do this in the codebase.
|
||||
;(
|
||||
document.getElementById(
|
||||
`text-input-${selectingShape.id}`
|
||||
) as HTMLTextAreaElement
|
||||
).select()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
@ -189,13 +206,25 @@ export class PointingShape extends StateNode {
|
|||
this.parent.transition('idle', info)
|
||||
}
|
||||
|
||||
override onDoubleClick: TLEventHandlers['onDoubleClick'] = () => {
|
||||
this.isDoubleClick = true
|
||||
}
|
||||
|
||||
override onPointerMove: TLEventHandlers['onPointerMove'] = (info) => {
|
||||
if (this.editor.inputs.isDragging) {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('translating', info)
|
||||
this.startTranslating(info)
|
||||
}
|
||||
}
|
||||
|
||||
override onLongPress: TLEventHandlers['onLongPress'] = (info) => {
|
||||
this.startTranslating(info)
|
||||
}
|
||||
|
||||
private startTranslating(info: TLPointerEventInfo) {
|
||||
if (this.editor.getInstanceState().isReadonly) return
|
||||
this.parent.transition('translating', info)
|
||||
}
|
||||
|
||||
override onCancel: TLEventHandlers['onCancel'] = () => {
|
||||
this.cancel()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
compact,
|
||||
moveCameraWhenCloseToEdge,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
type ResizingInfo = TLPointerEventInfo & {
|
||||
target: 'selection'
|
||||
|
@ -111,6 +112,8 @@ export class Resizing extends StateNode {
|
|||
}
|
||||
|
||||
private complete() {
|
||||
kickoutOccludedShapes(this.editor, this.snapshot.selectedShapeIds)
|
||||
|
||||
this.handleResizeEnd()
|
||||
|
||||
if (this.info.isCreating && this.info.onCreate) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
shortAngleDist,
|
||||
snapAngle,
|
||||
} from '@tldraw/editor'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
import { CursorTypeMap } from './PointingResizeHandle'
|
||||
|
||||
const ONE_DEGREE = Math.PI / 180
|
||||
|
@ -128,6 +129,10 @@ export class Rotating extends StateNode {
|
|||
snapshot: this.snapshot,
|
||||
stage: 'end',
|
||||
})
|
||||
kickoutOccludedShapes(
|
||||
this.editor,
|
||||
this.snapshot.shapeSnapshots.map((s) => s.shape.id)
|
||||
)
|
||||
if (this.info.onInteractionEnd) {
|
||||
this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
|
||||
} else {
|
||||
|
|
|
@ -42,9 +42,7 @@ export class ScribbleBrushing extends StateNode {
|
|||
|
||||
this.updateScribbleSelection(true)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.editor.updateInstanceState({ brush: null })
|
||||
})
|
||||
this.editor.updateInstanceState({ brush: null })
|
||||
}
|
||||
|
||||
override onExit = () => {
|
||||
|
|
|
@ -18,10 +18,10 @@ import {
|
|||
import {
|
||||
NOTE_PIT_RADIUS,
|
||||
NOTE_SIZE,
|
||||
NotePit,
|
||||
getAvailableNoteAdjacentPositions,
|
||||
} from '../../../shapes/note/noteHelpers'
|
||||
import { DragAndDropManager } from '../DragAndDropManager'
|
||||
import { kickoutOccludedShapes } from '../selectHelpers'
|
||||
|
||||
export class Translating extends StateNode {
|
||||
static override id = 'translating'
|
||||
|
@ -174,6 +174,10 @@ export class Translating extends StateNode {
|
|||
protected complete() {
|
||||
this.updateShapes()
|
||||
this.dragAndDropManager.dropShapes(this.snapshot.movingShapes)
|
||||
kickoutOccludedShapes(
|
||||
this.editor,
|
||||
this.snapshot.movingShapes.map((s) => s.id)
|
||||
)
|
||||
this.handleEnd()
|
||||
|
||||
if (this.editor.getInstanceState().isToolLocked && this.info.onInteractionEnd) {
|
||||
|
@ -349,24 +353,48 @@ function getTranslatingSnapshot(editor: Editor) {
|
|||
}
|
||||
}
|
||||
|
||||
let noteAdjacentPositions: NotePit[] | undefined
|
||||
let noteAdjacentPositions: Vec[] | undefined
|
||||
let noteSnapshot: MovingShapeSnapshot | undefined
|
||||
|
||||
const underCursor = editor.getHoveredShape()
|
||||
if (underCursor) {
|
||||
if (editor.isShapeOfType<TLNoteShape>(underCursor, 'note')) {
|
||||
const snapshot = shapeSnapshots.find((s) => s.shape.id === underCursor.id)
|
||||
if (snapshot) {
|
||||
noteSnapshot = snapshot
|
||||
noteAdjacentPositions = getAvailableNoteAdjacentPositions(
|
||||
editor,
|
||||
snapshot.pageRotation,
|
||||
underCursor.props.growY ?? 0
|
||||
)
|
||||
}
|
||||
const { originPagePoint } = editor.inputs
|
||||
|
||||
if (
|
||||
shapeSnapshots.length === 1 &&
|
||||
editor.isShapeOfType<TLNoteShape>(shapeSnapshots[0].shape, 'note')
|
||||
) {
|
||||
noteSnapshot = shapeSnapshots[0]
|
||||
} else {
|
||||
const allHoveredNotes = shapeSnapshots.filter(
|
||||
(s) =>
|
||||
editor.isShapeOfType<TLNoteShape>(s.shape, 'note') &&
|
||||
editor.isPointInShape(s.shape, originPagePoint)
|
||||
)
|
||||
|
||||
if (allHoveredNotes.length === 0) {
|
||||
// noop
|
||||
} else if (allHoveredNotes.length === 1) {
|
||||
// just one, easy
|
||||
noteSnapshot = allHoveredNotes[0]
|
||||
} else {
|
||||
// More than one under the cursor, so we need to find the highest shape in z-order
|
||||
const allShapesSorted = editor.getCurrentPageShapesSorted()
|
||||
noteSnapshot = allHoveredNotes
|
||||
.map((s) => ({
|
||||
snapshot: s,
|
||||
index: allShapesSorted.findIndex((shape) => shape.id === s.shape.id),
|
||||
}))
|
||||
.sort((a, b) => b.index - a.index)[0]?.snapshot // highest up first
|
||||
}
|
||||
}
|
||||
|
||||
if (noteSnapshot) {
|
||||
noteAdjacentPositions = getAvailableNoteAdjacentPositions(
|
||||
editor,
|
||||
noteSnapshot.pageRotation,
|
||||
(noteSnapshot.shape as TLNoteShape).props.growY ?? 0
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
averagePagePoint: Vec.Average(pagePoints),
|
||||
movingShapes,
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import {
|
||||
Editor,
|
||||
TLShape,
|
||||
TLShapeId,
|
||||
polygonIntersectsPolyline,
|
||||
polygonsIntersect,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
/** @internal */
|
||||
export function kickoutOccludedShapes(editor: Editor, shapeIds: TLShapeId[]) {
|
||||
const shapes = shapeIds.map((id) => editor.getShape(id)).filter((s) => s) as TLShape[]
|
||||
const effectedParents: TLShape[] = shapes
|
||||
.map((shape) => {
|
||||
const parent = editor.getShape(shape.parentId)
|
||||
if (!parent) return shape
|
||||
return parent
|
||||
})
|
||||
.filter((shape) => shape.type === 'frame' || shape.type === 'note')
|
||||
|
||||
const kickedOutChildren: TLShapeId[] = []
|
||||
for (const parent of effectedParents) {
|
||||
const childIds = editor.getSortedChildIdsForParent(parent.id)
|
||||
|
||||
// Get the bounds of the parent shape
|
||||
const parentPageBounds = editor.getShapePageBounds(parent)
|
||||
if (!parentPageBounds) continue
|
||||
|
||||
// For each child, check whether its bounds overlap with the parent's bounds
|
||||
for (const childId of childIds) {
|
||||
if (isShapeOccluded(editor, parent, childId)) {
|
||||
kickedOutChildren.push(childId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// now kick out the children
|
||||
// TODO: make this reparent to the parent's parent?
|
||||
editor.reparentShapes(kickedOutChildren, editor.getCurrentPageId())
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isShapeOccluded(editor: Editor, occluder: TLShape, shape: TLShapeId) {
|
||||
const occluderPageBounds = editor.getShapePageBounds(occluder)
|
||||
if (!occluderPageBounds) return false
|
||||
|
||||
const shapePageBounds = editor.getShapePageBounds(shape)
|
||||
if (!shapePageBounds) return true
|
||||
|
||||
// If the shape's bounds are completely inside the occluder, it's not occluded
|
||||
if (occluderPageBounds.contains(shapePageBounds)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the shape's bounds are completely outside the occluder, it's occluded
|
||||
if (!occluderPageBounds.includes(shapePageBounds)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we've made it this far, the shape's bounds must intersect the edge of the occluder
|
||||
// In this case, we need to look at the shape's geometry for a more fine-grained check
|
||||
const shapeGeometry = editor.getShapeGeometry(shape)
|
||||
const occluderCornersInShapeSpace = occluderPageBounds.corners.map((v) => {
|
||||
return editor.getPointInShapeSpace(shape, v)
|
||||
})
|
||||
|
||||
if (shapeGeometry.isClosed) {
|
||||
return !polygonsIntersect(occluderCornersInShapeSpace, shapeGeometry.vertices)
|
||||
}
|
||||
|
||||
return !polygonIntersectsPolyline(occluderCornersInShapeSpace, shapeGeometry.vertices)
|
||||
}
|
|
@ -22,6 +22,7 @@ import {
|
|||
} from '@tldraw/editor'
|
||||
import React from 'react'
|
||||
import { STYLES } from '../../../styles'
|
||||
import { kickoutOccludedShapes } from '../../../tools/SelectTool/selectHelpers'
|
||||
import { useUiEvents } from '../../context/events'
|
||||
import { useRelevantStyles } from '../../hooks/useRelevantStyles'
|
||||
import { useTranslation } from '../../hooks/useTranslation/useTranslation'
|
||||
|
@ -101,6 +102,7 @@ export function CommonStylePickerSet({
|
|||
theme: TLDefaultColorTheme
|
||||
}) {
|
||||
const msg = useTranslation()
|
||||
const editor = useEditor()
|
||||
|
||||
const handleValueChange = useStyleChangeCallback()
|
||||
|
||||
|
@ -163,7 +165,13 @@ export function CommonStylePickerSet({
|
|||
style={DefaultSizeStyle}
|
||||
items={STYLES.size}
|
||||
value={size}
|
||||
onValueChange={handleValueChange}
|
||||
onValueChange={(style, value, squashing) => {
|
||||
handleValueChange(style, value, squashing)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
if (selectedShapeIds.length > 0) {
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
}
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
useEditor,
|
||||
} from '@tldraw/editor'
|
||||
import * as React from 'react'
|
||||
import { kickoutOccludedShapes } from '../../tools/SelectTool/selectHelpers'
|
||||
import { getEmbedInfo } from '../../utils/embeds/embeds'
|
||||
import { fitFrameToContent, removeFrame } from '../../utils/frames/frames'
|
||||
import { EditLinkDialog } from '../components/EditLinkDialog'
|
||||
|
@ -321,24 +322,28 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('toggle-auto-size', { source })
|
||||
editor.mark('toggling auto size')
|
||||
const shapes = editor
|
||||
.getSelectedShapes()
|
||||
.filter(
|
||||
(shape): shape is TLTextShape =>
|
||||
editor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false
|
||||
)
|
||||
editor.updateShapes(
|
||||
editor
|
||||
.getSelectedShapes()
|
||||
.filter(
|
||||
(shape): shape is TLTextShape =>
|
||||
editor.isShapeOfType<TLTextShape>(shape, 'text') && shape.props.autoSize === false
|
||||
)
|
||||
.map((shape) => {
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
props: {
|
||||
...shape.props,
|
||||
w: 8,
|
||||
autoSize: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
shapes.map((shape) => {
|
||||
return {
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
props: {
|
||||
...shape.props,
|
||||
w: 8,
|
||||
autoSize: true,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
kickoutOccludedShapes(
|
||||
editor,
|
||||
shapes.map((shape) => shape.id)
|
||||
)
|
||||
},
|
||||
},
|
||||
|
@ -602,7 +607,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'left', source })
|
||||
editor.mark('align left')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'left')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'left')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -619,7 +626,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'center-horizontal', source })
|
||||
editor.mark('align center horizontal')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'center-horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'center-horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -633,7 +642,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'right', source })
|
||||
editor.mark('align right')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'right')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'right')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -650,7 +661,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'center-vertical', source })
|
||||
editor.mark('align center vertical')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'center-vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'center-vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -664,7 +677,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'top', source })
|
||||
editor.mark('align top')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'top')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'top')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -678,7 +693,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('align-shapes', { operation: 'bottom', source })
|
||||
editor.mark('align bottom')
|
||||
editor.alignShapes(editor.getSelectedShapeIds(), 'bottom')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.alignShapes(selectedShapeIds, 'bottom')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -695,7 +712,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('distribute-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('distribute horizontal')
|
||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.distributeShapes(selectedShapeIds, 'horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -712,7 +731,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('distribute-shapes', { operation: 'vertical', source })
|
||||
editor.mark('distribute vertical')
|
||||
editor.distributeShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.distributeShapes(selectedShapeIds, 'vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -728,7 +749,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stretch-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('stretch horizontal')
|
||||
editor.stretchShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stretchShapes(selectedShapeIds, 'horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -744,7 +767,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stretch-shapes', { operation: 'vertical', source })
|
||||
editor.mark('stretch vertical')
|
||||
editor.stretchShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stretchShapes(selectedShapeIds, 'vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -760,7 +785,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('flip-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('flip horizontal')
|
||||
editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.flipShapes(selectedShapeIds, 'horizontal')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -773,7 +800,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('flip-shapes', { operation: 'vertical', source })
|
||||
editor.mark('flip vertical')
|
||||
editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.flipShapes(selectedShapeIds, 'vertical')
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -786,7 +815,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('pack-shapes', { source })
|
||||
editor.mark('pack')
|
||||
editor.packShapes(editor.getSelectedShapeIds(), 16)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.packShapes(selectedShapeIds, 16)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -802,7 +833,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stack-shapes', { operation: 'vertical', source })
|
||||
editor.mark('stack-vertical')
|
||||
editor.stackShapes(editor.getSelectedShapeIds(), 'vertical', 16)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stackShapes(selectedShapeIds, 'vertical', 16)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -818,7 +851,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
|
||||
trackEvent('stack-shapes', { operation: 'horizontal', source })
|
||||
editor.mark('stack-horizontal')
|
||||
editor.stackShapes(editor.getSelectedShapeIds(), 'horizontal', 16)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.stackShapes(selectedShapeIds, 'horizontal', 16)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -970,10 +1005,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
editor.mark('rotate-cw')
|
||||
const offset = editor.getSelectionRotation() % (HALF_PI / 2)
|
||||
const dontUseOffset = approximately(offset, 0) || approximately(offset, HALF_PI / 2)
|
||||
editor.rotateShapesBy(
|
||||
editor.getSelectedShapeIds(),
|
||||
HALF_PI / 2 - (dontUseOffset ? 0 : offset)
|
||||
)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.rotateShapesBy(selectedShapeIds, HALF_PI / 2 - (dontUseOffset ? 0 : offset))
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -988,10 +1022,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
|
|||
editor.mark('rotate-ccw')
|
||||
const offset = editor.getSelectionRotation() % (HALF_PI / 2)
|
||||
const offsetCloseToZero = approximately(offset, 0)
|
||||
editor.rotateShapesBy(
|
||||
editor.getSelectedShapeIds(),
|
||||
offsetCloseToZero ? -(HALF_PI / 2) : -offset
|
||||
)
|
||||
const selectedShapeIds = editor.getSelectedShapeIds()
|
||||
editor.rotateShapesBy(selectedShapeIds, offsetCloseToZero ? -(HALF_PI / 2) : -offset)
|
||||
kickoutOccludedShapes(editor, selectedShapeIds)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -10,18 +10,21 @@ import { DEFAULT_TRANSLATION } from './defaultTranslation'
|
|||
|
||||
/* ----------------- (do not change) ---------------- */
|
||||
|
||||
export const RTL_LANGUAGES = new Set(['ar', 'fa', 'he', 'ur', 'ku'])
|
||||
|
||||
/** @public */
|
||||
export type TLUiTranslation = {
|
||||
readonly locale: string
|
||||
readonly label: string
|
||||
readonly isRTL?: boolean
|
||||
readonly messages: Record<TLUiTranslationKey, string>
|
||||
readonly dir: 'rtl' | 'ltr'
|
||||
}
|
||||
|
||||
const EN_TRANSLATION: TLUiTranslation = {
|
||||
locale: 'en',
|
||||
label: 'English',
|
||||
messages: DEFAULT_TRANSLATION as TLUiTranslation['messages'],
|
||||
dir: 'ltr',
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
@ -70,7 +73,7 @@ export async function fetchTranslation(
|
|||
return {
|
||||
locale,
|
||||
label: language.label,
|
||||
isRTL: 'isRTL' in language ? language.isRTL : false,
|
||||
dir: RTL_LANGUAGES.has(language.locale) ? 'rtl' : 'ltr',
|
||||
messages: { ...EN_TRANSLATION.messages, ...messages },
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ export const TranslationProvider = track(function TranslationProvider({
|
|||
return {
|
||||
locale: 'en',
|
||||
label: 'English',
|
||||
dir: 'ltr',
|
||||
messages: { ...DEFAULT_TRANSLATION, ...overrides['en'] },
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +56,7 @@ export const TranslationProvider = track(function TranslationProvider({
|
|||
return {
|
||||
locale: 'en',
|
||||
label: 'English',
|
||||
dir: 'ltr',
|
||||
messages: DEFAULT_TRANSLATION,
|
||||
}
|
||||
})
|
||||
|
|
|
@ -434,11 +434,11 @@ describe('When editing shapes', () => {
|
|||
// start editing the geo shape
|
||||
editor.doubleClick(50, 50, { target: 'shape', shape: editor.getShape(ids.geo1) })
|
||||
expect(editor.getEditingShapeId()).toBe(ids.geo1)
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.geo1)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.geo1)
|
||||
// point the text shape
|
||||
editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.text1) })
|
||||
expect(editor.getEditingShapeId()).toBe(null)
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.text1)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.text1)
|
||||
})
|
||||
|
||||
// The behavior described here will only work end to end, not with the library,
|
||||
|
@ -450,12 +450,12 @@ describe('When editing shapes', () => {
|
|||
// start editing the geo shape
|
||||
editor.doubleClick(50, 50, { target: 'shape', shape: editor.getShape(ids.geo1) })
|
||||
expect(editor.getEditingShapeId()).toBe(ids.geo1)
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.geo1)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.geo1)
|
||||
// point the other geo shape
|
||||
editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.geo2) })
|
||||
// that other shape should now be editing and selected!
|
||||
expect(editor.getEditingShapeId()).toBe(ids.geo2)
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.geo2)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.geo2)
|
||||
})
|
||||
|
||||
// This works but only end to end — the logic had to move to React
|
||||
|
@ -468,7 +468,7 @@ describe('When editing shapes', () => {
|
|||
editor.pointerDown(50, 50, { target: 'shape', shape: editor.getShape(ids.text2) })
|
||||
// that other shape should now be editing and selected!
|
||||
expect(editor.getEditingShapeId()).toBe(ids.text2)
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.text2)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.text2)
|
||||
})
|
||||
|
||||
it('Double clicking the canvas creates a new text shape', () => {
|
||||
|
@ -559,3 +559,14 @@ describe('When in readonly mode', () => {
|
|||
expect(editor.getEditingShapeId()).toBe(ids.embed1)
|
||||
})
|
||||
})
|
||||
|
||||
// This should be end to end, the problem is the blur handler of the react component
|
||||
it('goes into pointing canvas', () => {
|
||||
editor
|
||||
.createShape({ type: 'note' })
|
||||
.pointerMove(50, 50)
|
||||
.doubleClick()
|
||||
.expectToBeIn('select.editing_shape')
|
||||
.pointerDown(300, 300)
|
||||
.expectToBeIn('select.pointing_canvas')
|
||||
})
|
||||
|
|
|
@ -335,6 +335,17 @@ export class TestEditor extends Editor {
|
|||
|
||||
/* ------------------ Input Events ------------------ */
|
||||
|
||||
/**
|
||||
Some of our updates are not synchronous any longer. For example, drawing happens on tick instead of on pointer move.
|
||||
You can use this helper to force the tick, which will then process all the updates.
|
||||
*/
|
||||
forceTick = (count = 1) => {
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.emit('tick', 16)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
pointerMove = (
|
||||
x = this.inputs.currentScreenPoint.x,
|
||||
y = this.inputs.currentScreenPoint.y,
|
||||
|
@ -344,7 +355,7 @@ export class TestEditor extends Editor {
|
|||
this.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_move',
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -357,7 +368,7 @@ export class TestEditor extends Editor {
|
|||
this.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_down',
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -370,7 +381,7 @@ export class TestEditor extends Editor {
|
|||
this.dispatch({
|
||||
...this.getPointerEventInfo(x, y, options, modifiers),
|
||||
name: 'pointer_up',
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -404,17 +415,17 @@ export class TestEditor extends Editor {
|
|||
type: 'click',
|
||||
name: 'double_click',
|
||||
phase: 'up',
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
keyDown = (key: string, options = {} as Partial<Exclude<TLKeyboardEventInfo, 'key'>>) => {
|
||||
this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_down', options) })
|
||||
this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_down', options) }).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
keyRepeat = (key: string, options = {} as Partial<Exclude<TLKeyboardEventInfo, 'key'>>) => {
|
||||
this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_repeat', options) })
|
||||
this.dispatch({ ...this.getKeyboardEventInfo(key, 'key_repeat', options) }).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -426,7 +437,7 @@ export class TestEditor extends Editor {
|
|||
altKey: this.inputs.altKey && key !== 'Alt',
|
||||
...options,
|
||||
}),
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -440,7 +451,7 @@ export class TestEditor extends Editor {
|
|||
altKey: this.inputs.altKey,
|
||||
...options,
|
||||
delta: { x: dx, y: dy },
|
||||
})
|
||||
}).forceTick(2)
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -462,7 +473,7 @@ export class TestEditor extends Editor {
|
|||
...options,
|
||||
point: { x, y, z },
|
||||
delta: { x: dx, y: dy, z: dz },
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -506,7 +517,7 @@ export class TestEditor extends Editor {
|
|||
...options,
|
||||
point: { x, y, z },
|
||||
delta: { x: dx, y: dy, z: dz },
|
||||
})
|
||||
}).forceTick()
|
||||
return this
|
||||
}
|
||||
/* ------ Interaction Helpers ------ */
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
DefaultFillStyle,
|
||||
GeoShapeGeoStyle,
|
||||
TLArrowShape,
|
||||
TLFrameShape,
|
||||
TLShapeId,
|
||||
|
@ -275,8 +276,8 @@ describe('frame shapes', () => {
|
|||
expect(parentBefore).toBe(frameId)
|
||||
// resize the frame so the shape is partially out of bounds
|
||||
editor.pointerDown(100, 50, { target: 'selection', handle: 'right' })
|
||||
editor.pointerMove(70, 50)
|
||||
editor.pointerUp(70, 50)
|
||||
editor.pointerMove(80, 50)
|
||||
editor.pointerUp(80, 50)
|
||||
const parentAfter = editor.getShape(rectId)?.parentId
|
||||
expect(parentAfter).toBe(frameId)
|
||||
})
|
||||
|
@ -405,7 +406,7 @@ describe('frame shapes', () => {
|
|||
|
||||
expect(editor.getOnlySelectedShape()!.id).toBe(boxAid)
|
||||
expect(editor.getOnlySelectedShape()!.parentId).toBe(frameId)
|
||||
expect(editor.getHintingShapeIds()).toHaveLength(0)
|
||||
expect(editor.getHintingShapeIds()).toHaveLength(1)
|
||||
// box A should still be beneath box B
|
||||
expect(editor.getShape(boxAid)!.index.localeCompare(editor.getShape(boxBid)!.index)).toBe(-1)
|
||||
})
|
||||
|
@ -926,7 +927,7 @@ describe('When dragging a shape inside a group inside a frame', () => {
|
|||
|
||||
editor.pointerMove(100, 100).click().click()
|
||||
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.box1)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.box1)
|
||||
|
||||
editor.pointerMove(150, 150).pointerDown().pointerMove(140, 140)
|
||||
|
||||
|
@ -946,7 +947,7 @@ describe('When dragging a shape inside a group inside a frame', () => {
|
|||
|
||||
editor.pointerMove(100, 100).click().click()
|
||||
|
||||
expect(editor.getOnlySelectedShape()?.id).toBe(ids.box1)
|
||||
expect(editor.getOnlySelectedShapeId()).toBe(ids.box1)
|
||||
expect(editor.getFocusedGroupId()).toBe(ids.group1)
|
||||
|
||||
editor
|
||||
|
@ -1024,6 +1025,65 @@ function dragCreateFrame({
|
|||
return frameId
|
||||
}
|
||||
|
||||
function dragCreateRect({
|
||||
down,
|
||||
move,
|
||||
up,
|
||||
}: {
|
||||
down: [number, number]
|
||||
move: [number, number]
|
||||
up: [number, number]
|
||||
}): TLShapeId {
|
||||
editor.setCurrentTool('geo')
|
||||
editor.pointerDown(...down)
|
||||
editor.pointerMove(...move)
|
||||
editor.pointerUp(...up)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const rectId = shapes[0].id
|
||||
return rectId
|
||||
}
|
||||
|
||||
function dragCreateTriangle({
|
||||
down,
|
||||
move,
|
||||
up,
|
||||
}: {
|
||||
down: [number, number]
|
||||
move: [number, number]
|
||||
up: [number, number]
|
||||
}): TLShapeId {
|
||||
editor.setCurrentTool('geo')
|
||||
const originalStyle = editor.getStyleForNextShape(GeoShapeGeoStyle)
|
||||
editor.setStyleForNextShapes(GeoShapeGeoStyle, 'triangle')
|
||||
editor.pointerDown(...down)
|
||||
editor.pointerMove(...move)
|
||||
editor.pointerUp(...up)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
editor.selectNone()
|
||||
editor.setStyleForNextShapes(GeoShapeGeoStyle, originalStyle)
|
||||
const rectId = shapes[0].id
|
||||
editor.select(shapes[0].id)
|
||||
return rectId
|
||||
}
|
||||
|
||||
function dragCreateLine({
|
||||
down,
|
||||
move,
|
||||
up,
|
||||
}: {
|
||||
down: [number, number]
|
||||
move: [number, number]
|
||||
up: [number, number]
|
||||
}): TLShapeId {
|
||||
editor.setCurrentTool('line')
|
||||
editor.pointerDown(...down)
|
||||
editor.pointerMove(...move)
|
||||
editor.pointerUp(...up)
|
||||
const shapes = editor.getSelectedShapes()
|
||||
const lineId = shapes[0].id
|
||||
return lineId
|
||||
}
|
||||
|
||||
function createRect({ pos, size }: { pos: [number, number]; size: [number, number] }) {
|
||||
const rectId: TLShapeId = createShapeId()
|
||||
editor.createShapes([
|
||||
|
@ -1037,3 +1097,117 @@ function createRect({ pos, size }: { pos: [number, number]; size: [number, numbe
|
|||
])
|
||||
return rectId
|
||||
}
|
||||
|
||||
describe('Unparenting behavior', () => {
|
||||
it("unparents a shape when it's completely dragged out of a frame, even when the pointer doesn't move across the edge of the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [80, 50], move: [120, 60], up: [120, 60] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(110, 50)
|
||||
editor.pointerMove(140, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(140, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("doesn't unparent a shape when it's partially dragged out of a frame, when the pointer doesn't move across the edge of the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [80, 50], move: [120, 60], up: [120, 60] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(110, 50)
|
||||
editor.pointerMove(120, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(120, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
})
|
||||
|
||||
it('unparents a shape when the pointer drags across the edge of a frame, even if its geometry overlaps with the frame', () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [80, 50], move: [120, 60], up: [120, 60] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(90, 50)
|
||||
editor.pointerMove(110, 50)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerUp(110, 50)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("unparents a shape when it's rotated out of a frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [95, 10], move: [200, 20], up: [200, 20] })
|
||||
const [frame, rect] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(200, 20, {
|
||||
target: 'selection',
|
||||
handle: 'top_right_rotate',
|
||||
})
|
||||
editor.pointerMove(200, 200)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(200, 200)
|
||||
expect(editor.getShape(rect.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("unparents shapes if they're resized out of a frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateRect({ down: [10, 10], move: [20, 20], up: [20, 20] })
|
||||
dragCreateRect({ down: [80, 80], move: [90, 90], up: [90, 90] })
|
||||
const [frame, rect1, rect2] = editor.getLastCreatedShapes(3)
|
||||
|
||||
editor.select(rect1.id, rect2.id)
|
||||
editor.pointerDown(90, 90, { target: 'selection', handle: 'top_right' })
|
||||
expect(editor.getShape(rect2.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerMove(200, 200)
|
||||
expect(editor.getShape(rect2.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(200, 200)
|
||||
expect(editor.getShape(rect2.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("unparents a shape if its geometry doesn't overlap with the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateTriangle({ down: [80, 80], move: [120, 120], up: [120, 120] })
|
||||
const [frame, triangle] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(85, 85)
|
||||
editor.pointerMove(95, 95)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(95, 95)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it("only parents on pointer up if the shape's geometry overlaps with the frame", () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateTriangle({ down: [120, 120], move: [160, 160], up: [160, 160] })
|
||||
const [frame, triangle] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
editor.pointerDown(125, 125)
|
||||
editor.pointerMove(95, 95)
|
||||
jest.advanceTimersByTime(200)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(frame.id)
|
||||
expect(editor.getHintingShapeIds()).toHaveLength(0)
|
||||
editor.pointerUp(95, 95)
|
||||
expect(editor.getShape(triangle.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
|
||||
it('unparents an occluded shape after dragging a handle out of a frame', () => {
|
||||
dragCreateFrame({ down: [0, 0], move: [100, 100], up: [100, 100] })
|
||||
dragCreateLine({ down: [90, 90], move: [120, 120], up: [120, 120] })
|
||||
const [frame, line] = editor.getLastCreatedShapes(2)
|
||||
|
||||
expect(editor.getShape(line.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerDown(90, 90)
|
||||
editor.pointerMove(110, 110)
|
||||
expect(editor.getShape(line.id)!.parentId).toBe(frame.id)
|
||||
editor.pointerUp(110, 110)
|
||||
expect(editor.getShape(line.id)!.parentId).toBe(editor.getCurrentPageId())
|
||||
})
|
||||
})
|
||||
|
|
|
@ -15,7 +15,7 @@ describe(SelectTool, () => {
|
|||
describe('pointer down while shape is being edited', () => {
|
||||
it('captures the pointer down event if it is on the shape', () => {
|
||||
editor.setCurrentTool('geo').pointerDown(0, 0).pointerMove(100, 100).pointerUp(100, 100)
|
||||
const shapeId = editor.getOnlySelectedShape()?.id
|
||||
const shapeId = editor.getLastCreatedShape().id
|
||||
editor._transformPointerDownSpy.mockRestore()
|
||||
editor._transformPointerUpSpy.mockRestore()
|
||||
editor.setCurrentTool('select')
|
||||
|
@ -42,7 +42,7 @@ describe(SelectTool, () => {
|
|||
})
|
||||
it('does not allow pressing undo to end up in the editing state', () => {
|
||||
editor.setCurrentTool('geo').pointerDown(0, 0).pointerMove(100, 100).pointerUp(100, 100)
|
||||
const shapeId = editor.getOnlySelectedShape()?.id
|
||||
const shapeId = editor.getLastCreatedShape().id
|
||||
editor._transformPointerDownSpy.mockRestore()
|
||||
editor._transformPointerUpSpy.mockRestore()
|
||||
editor.setCurrentTool('select')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
GapsSnapIndicator,
|
||||
IndexKey,
|
||||
PointsSnapIndicator,
|
||||
SnapIndicator,
|
||||
TLArrowShape,
|
||||
|
@ -167,9 +168,9 @@ describe('When translating...', () => {
|
|||
.pointerMove(1080, 800)
|
||||
jest.advanceTimersByTime(100)
|
||||
editor
|
||||
.expectShapeToMatch({ id: ids.box1, x: 1300, y: 845.68 })
|
||||
.expectShapeToMatch({ id: ids.box1, x: 1320, y: 845.68 })
|
||||
.pointerUp()
|
||||
.expectShapeToMatch({ id: ids.box1, x: 1300, y: 845.68 })
|
||||
.expectShapeToMatch({ id: ids.box1, x: 1340, y: 857.92 })
|
||||
})
|
||||
|
||||
it('translates multiple shapes', () => {
|
||||
|
@ -2048,7 +2049,7 @@ describe('Note shape grid helper positions / pits', () => {
|
|||
it('Snaps multiple notes to the pit using the note under the cursor', () => {
|
||||
editor.createShape({ type: 'note' })
|
||||
editor.createShape({ type: 'note', x: 500, y: 500 })
|
||||
editor.createShape({ type: 'note', x: 700, y: 500 })
|
||||
editor.createShape({ type: 'note', x: 700, y: 500, parentId: editor.getCurrentPageId() })
|
||||
const [shapeB, shapeC] = editor.getLastCreatedShapes(2)
|
||||
|
||||
const pit = { x: 320, y: 100 } // right of shapeA
|
||||
|
@ -2063,7 +2064,8 @@ describe('Note shape grid helper positions / pits', () => {
|
|||
.pointerMove(pit.x - 4, pit.y - 4) // not exactly in the pit...
|
||||
|
||||
// B snaps the selection to the pit
|
||||
editor.expectShapeToMatch({ ...shapeB, x: 220, y: 0 })
|
||||
// (index is manually set because the sticky gets brought to front)
|
||||
editor.expectShapeToMatch({ ...shapeB, x: 220, y: 0, index: 'a4' as IndexKey })
|
||||
expect(editor.getSelectionPageBounds()).toMatchObject({ x: 220, y: 0, w: 400, h: 200 })
|
||||
|
||||
editor.cancel()
|
||||
|
@ -2083,7 +2085,49 @@ describe('Note shape grid helper positions / pits', () => {
|
|||
|
||||
// Even though B is in the same place as it was when it snapped (while dragging over B),
|
||||
// because our cursor is over C it won't fall into the pit—because it's not hovered
|
||||
editor.expectShapeToMatch({ ...shapeB, x: 216, y: -4 })
|
||||
// (index is manually set because the sticky gets brought to front)
|
||||
editor.expectShapeToMatch({ ...shapeB, x: 216, y: -4, index: 'a4' as IndexKey })
|
||||
expect(editor.getSelectionPageBounds()).toMatchObject({ x: 216, y: -4, w: 400, h: 200 })
|
||||
})
|
||||
|
||||
it('When multiple notes are under the cursor, uses the top-most one', () => {
|
||||
editor.createShape({ type: 'note' })
|
||||
editor.createShape({ type: 'note', x: 500, y: 500 })
|
||||
editor.createShape({ type: 'note', x: 501, y: 501 })
|
||||
const [shapeB, shapeC] = editor.getLastCreatedShapes(2)
|
||||
|
||||
// For the purposes of this test, let's leave the stickies unparented
|
||||
editor.reparentShapes([shapeC], editor.getCurrentPageId())
|
||||
|
||||
const pit = { x: 320, y: 100 } // right of shapeA
|
||||
|
||||
editor.select(shapeB, shapeC)
|
||||
|
||||
expect(editor.getSelectionPageBounds()).toMatchObject({ x: 500, y: 500, w: 201, h: 201 })
|
||||
|
||||
// First we do it with C in front
|
||||
editor.bringToFront([shapeC])
|
||||
editor
|
||||
.pointerMove(600, 600) // center of b but overlapping C
|
||||
.pointerDown()
|
||||
.pointerMove(pit.x - 4, pit.y - 4) // not exactly in the pit...
|
||||
|
||||
// B snaps the selection to the pit
|
||||
editor.expectShapeToMatch({ id: shapeB.id, x: 219, y: -1 }) // not snapped
|
||||
editor.expectShapeToMatch({ id: shapeC.id, x: 220, y: 0 }) // snapped
|
||||
|
||||
editor.cancel()
|
||||
|
||||
// Now let's do it with B in front
|
||||
editor.bringToFront([shapeB])
|
||||
|
||||
editor
|
||||
.pointerMove(600, 600) // center of b but overlapping C
|
||||
.pointerDown()
|
||||
.pointerMove(pit.x - 4, pit.y - 4) // not exactly in the pit...
|
||||
|
||||
// B snaps the selection to the pit
|
||||
editor.expectShapeToMatch({ id: shapeB.id, x: 220, y: 0 }) // snapped
|
||||
editor.expectShapeToMatch({ id: shapeC.id, x: 221, y: 1 }) // not snapped
|
||||
})
|
||||
})
|
||||
|
|
|
@ -636,19 +636,15 @@ export const LANGUAGES: readonly [{
|
|||
}, {
|
||||
readonly locale: "he";
|
||||
readonly label: "עברית";
|
||||
readonly isRTL: true;
|
||||
}, {
|
||||
readonly locale: "ar";
|
||||
readonly label: "عربي";
|
||||
readonly isRTL: true;
|
||||
}, {
|
||||
readonly locale: "fa";
|
||||
readonly label: "فارسی";
|
||||
readonly isRTL: true;
|
||||
}, {
|
||||
readonly locale: "ku";
|
||||
readonly label: "کوردی";
|
||||
readonly isRTL: true;
|
||||
}, {
|
||||
readonly locale: "ne";
|
||||
readonly label: "नेपाली";
|
||||
|
@ -705,7 +701,7 @@ export const noteShapeMigrations: Migrations;
|
|||
export const noteShapeProps: {
|
||||
color: EnumStyleProp<"black" | "blue" | "green" | "grey" | "light-blue" | "light-green" | "light-red" | "light-violet" | "orange" | "red" | "violet" | "white" | "yellow">;
|
||||
size: EnumStyleProp<"l" | "m" | "s" | "xl">;
|
||||
fontSizeAdjustment: T.Validator<number | undefined>;
|
||||
fontSizeAdjustment: T.Validator<number>;
|
||||
font: EnumStyleProp<"draw" | "mono" | "sans" | "serif">;
|
||||
align: EnumStyleProp<"end-legacy" | "end" | "middle-legacy" | "middle" | "start-legacy" | "start">;
|
||||
verticalAlign: EnumStyleProp<"end" | "middle" | "start">;
|
||||
|
@ -750,6 +746,11 @@ export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
|
|||
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type ShapePropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
|
||||
[K in keyof Config]: T.TypeOf<Config[K]>;
|
||||
}>;
|
||||
|
||||
// @public
|
||||
export class StyleProp<Type> implements T.Validatable<Type> {
|
||||
// @internal
|
||||
|
|
|
@ -2750,7 +2750,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "readonly [{\n readonly locale: \"ca\";\n readonly label: \"Català\";\n}, {\n readonly locale: \"cs\";\n readonly label: \"Čeština\";\n}, {\n readonly locale: \"da\";\n readonly label: \"Danish\";\n}, {\n readonly locale: \"de\";\n readonly label: \"Deutsch\";\n}, {\n readonly locale: \"en\";\n readonly label: \"English\";\n}, {\n readonly locale: \"es\";\n readonly label: \"Español\";\n}, {\n readonly locale: \"fr\";\n readonly label: \"Français\";\n}, {\n readonly locale: \"gl\";\n readonly label: \"Galego\";\n}, {\n readonly locale: \"hr\";\n readonly label: \"Hrvatski\";\n}, {\n readonly locale: \"it\";\n readonly label: \"Italiano\";\n}, {\n readonly locale: \"hu\";\n readonly label: \"Magyar\";\n}, {\n readonly locale: \"no\";\n readonly label: \"Norwegian\";\n}, {\n readonly locale: \"pl\";\n readonly label: \"Polski\";\n}, {\n readonly locale: \"pt-br\";\n readonly label: \"Português - Brasil\";\n}, {\n readonly locale: \"pt-pt\";\n readonly label: \"Português - Europeu\";\n}, {\n readonly locale: \"ro\";\n readonly label: \"Română\";\n}, {\n readonly locale: \"ru\";\n readonly label: \"Russian\";\n}, {\n readonly locale: \"sl\";\n readonly label: \"Slovenščina\";\n}, {\n readonly locale: \"fi\";\n readonly label: \"Suomi\";\n}, {\n readonly locale: \"sv\";\n readonly label: \"Svenska\";\n}, {\n readonly locale: \"vi\";\n readonly label: \"Tiếng Việt\";\n}, {\n readonly locale: \"tr\";\n readonly label: \"Türkçe\";\n}, {\n readonly locale: \"uk\";\n readonly label: \"Ukrainian\";\n}, {\n readonly locale: \"he\";\n readonly label: \"עברית\";\n readonly isRTL: true;\n}, {\n readonly locale: \"ar\";\n readonly label: \"عربي\";\n readonly isRTL: true;\n}, {\n readonly locale: \"fa\";\n readonly label: \"فارسی\";\n readonly isRTL: true;\n}, {\n readonly locale: \"ku\";\n readonly label: \"کوردی\";\n readonly isRTL: true;\n}, {\n readonly locale: \"ne\";\n readonly label: \"नेपाली\";\n}, {\n readonly locale: \"hi-in\";\n readonly label: \"हिन्दी\";\n}, {\n readonly locale: \"te\";\n readonly label: \"తెలుగు\";\n}, {\n readonly locale: \"th\";\n readonly label: \"ภาษาไทย\";\n}, {\n readonly locale: \"my\";\n readonly label: \"မြန်မာစာ\";\n}, {\n readonly locale: \"ko-kr\";\n readonly label: \"한국어\";\n}, {\n readonly locale: \"ja\";\n readonly label: \"日本語\";\n}, {\n readonly locale: \"zh-cn\";\n readonly label: \"简体中文\";\n}, {\n readonly locale: \"zh-tw\";\n readonly label: \"繁體中文 (台灣)\";\n}]"
|
||||
"text": "readonly [{\n readonly locale: \"ca\";\n readonly label: \"Català\";\n}, {\n readonly locale: \"cs\";\n readonly label: \"Čeština\";\n}, {\n readonly locale: \"da\";\n readonly label: \"Danish\";\n}, {\n readonly locale: \"de\";\n readonly label: \"Deutsch\";\n}, {\n readonly locale: \"en\";\n readonly label: \"English\";\n}, {\n readonly locale: \"es\";\n readonly label: \"Español\";\n}, {\n readonly locale: \"fr\";\n readonly label: \"Français\";\n}, {\n readonly locale: \"gl\";\n readonly label: \"Galego\";\n}, {\n readonly locale: \"hr\";\n readonly label: \"Hrvatski\";\n}, {\n readonly locale: \"it\";\n readonly label: \"Italiano\";\n}, {\n readonly locale: \"hu\";\n readonly label: \"Magyar\";\n}, {\n readonly locale: \"no\";\n readonly label: \"Norwegian\";\n}, {\n readonly locale: \"pl\";\n readonly label: \"Polski\";\n}, {\n readonly locale: \"pt-br\";\n readonly label: \"Português - Brasil\";\n}, {\n readonly locale: \"pt-pt\";\n readonly label: \"Português - Europeu\";\n}, {\n readonly locale: \"ro\";\n readonly label: \"Română\";\n}, {\n readonly locale: \"ru\";\n readonly label: \"Russian\";\n}, {\n readonly locale: \"sl\";\n readonly label: \"Slovenščina\";\n}, {\n readonly locale: \"fi\";\n readonly label: \"Suomi\";\n}, {\n readonly locale: \"sv\";\n readonly label: \"Svenska\";\n}, {\n readonly locale: \"vi\";\n readonly label: \"Tiếng Việt\";\n}, {\n readonly locale: \"tr\";\n readonly label: \"Türkçe\";\n}, {\n readonly locale: \"uk\";\n readonly label: \"Ukrainian\";\n}, {\n readonly locale: \"he\";\n readonly label: \"עברית\";\n}, {\n readonly locale: \"ar\";\n readonly label: \"عربي\";\n}, {\n readonly locale: \"fa\";\n readonly label: \"فارسی\";\n}, {\n readonly locale: \"ku\";\n readonly label: \"کوردی\";\n}, {\n readonly locale: \"ne\";\n readonly label: \"नेपाली\";\n}, {\n readonly locale: \"hi-in\";\n readonly label: \"हिन्दी\";\n}, {\n readonly locale: \"te\";\n readonly label: \"తెలుగు\";\n}, {\n readonly locale: \"th\";\n readonly label: \"ภาษาไทย\";\n}, {\n readonly locale: \"my\";\n readonly label: \"မြန်မာစာ\";\n}, {\n readonly locale: \"ko-kr\";\n readonly label: \"한국어\";\n}, {\n readonly locale: \"ja\";\n readonly label: \"日本語\";\n}, {\n readonly locale: \"zh-cn\";\n readonly label: \"简体中文\";\n}, {\n readonly locale: \"zh-tw\";\n readonly label: \"繁體中文 (台灣)\";\n}]"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tlschema/src/translations/languages.ts",
|
||||
|
@ -2909,7 +2909,7 @@
|
|||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<number | undefined>;\n font: import(\"..\")."
|
||||
"text": "<number>;\n font: import(\"..\")."
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
|
@ -3263,6 +3263,81 @@
|
|||
"endIndex": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "TypeAlias",
|
||||
"canonicalReference": "@tldraw/tlschema!ShapePropsType:type",
|
||||
"docComment": "/**\n * @public\n */\n",
|
||||
"excerptTokens": [
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "export type ShapePropsType<Config extends "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Record",
|
||||
"canonicalReference": "!Record:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<string, "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "T.Validatable",
|
||||
"canonicalReference": "@tldraw/validate!Validatable:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<any>>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "> = "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "Expand",
|
||||
"canonicalReference": "@tldraw/utils!Expand:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<{\n [K in keyof Config]: "
|
||||
},
|
||||
{
|
||||
"kind": "Reference",
|
||||
"text": "T.TypeOf",
|
||||
"canonicalReference": "@tldraw/validate!TypeOf:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": "<Config[K]>;\n}>"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
"text": ";"
|
||||
}
|
||||
],
|
||||
"fileUrlPath": "packages/tlschema/src/shapes/TLBaseShape.ts",
|
||||
"releaseTag": "Public",
|
||||
"name": "ShapePropsType",
|
||||
"typeParameters": [
|
||||
{
|
||||
"typeParameterName": "Config",
|
||||
"constraintTokenRange": {
|
||||
"startIndex": 1,
|
||||
"endIndex": 5
|
||||
},
|
||||
"defaultTypeTokenRange": {
|
||||
"startIndex": 0,
|
||||
"endIndex": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"typeTokenRange": {
|
||||
"startIndex": 6,
|
||||
"endIndex": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Class",
|
||||
"canonicalReference": "@tldraw/tlschema!StyleProp:class",
|
||||
|
@ -3977,7 +4052,7 @@
|
|||
{
|
||||
"kind": "Reference",
|
||||
"text": "ShapePropsType",
|
||||
"canonicalReference": "@tldraw/tlschema!~ShapePropsType:type"
|
||||
"canonicalReference": "@tldraw/tlschema!ShapePropsType:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -6576,7 +6651,7 @@
|
|||
{
|
||||
"kind": "Reference",
|
||||
"text": "ShapePropsType",
|
||||
"canonicalReference": "@tldraw/tlschema!~ShapePropsType:type"
|
||||
"canonicalReference": "@tldraw/tlschema!ShapePropsType:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
@ -9411,7 +9486,7 @@
|
|||
{
|
||||
"kind": "Reference",
|
||||
"text": "ShapePropsType",
|
||||
"canonicalReference": "@tldraw/tlschema!~ShapePropsType:type"
|
||||
"canonicalReference": "@tldraw/tlschema!ShapePropsType:type"
|
||||
},
|
||||
{
|
||||
"kind": "Content",
|
||||
|
|
|
@ -80,6 +80,7 @@ export {
|
|||
parentIdValidator,
|
||||
shapeIdValidator,
|
||||
type ShapeProps,
|
||||
type ShapePropsType,
|
||||
type TLBaseShape,
|
||||
} from './shapes/TLBaseShape'
|
||||
export {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { embedShapeMigrations } from './shapes/TLEmbedShape'
|
|||
import { GeoShapeVersions, geoShapeMigrations } from './shapes/TLGeoShape'
|
||||
import { imageShapeMigrations } from './shapes/TLImageShape'
|
||||
import { lineShapeMigrations, lineShapeVersions } from './shapes/TLLineShape'
|
||||
import { noteShapeMigrations } from './shapes/TLNoteShape'
|
||||
import { noteShapeMigrations, noteShapeVersions } from './shapes/TLNoteShape'
|
||||
import { textShapeMigrations } from './shapes/TLTextShape'
|
||||
import { videoShapeMigrations } from './shapes/TLVideoShape'
|
||||
import { storeMigrations, storeVersions } from './store-migrations'
|
||||
|
@ -2030,6 +2030,46 @@ describe('Fractional indexing for line points', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('Add font size adjustment to notes', () => {
|
||||
const { up, down } = noteShapeMigrations.migrators[noteShapeVersions.AddFontSizeAdjustment]
|
||||
|
||||
test('up works as expected', () => {
|
||||
expect(up({ props: {} })).toEqual({ props: { fontSizeAdjustment: 0 } })
|
||||
})
|
||||
|
||||
test('down works as expected', () => {
|
||||
expect(down({ props: { fontSizeAdjustment: 0 } })).toEqual({ props: {} })
|
||||
})
|
||||
})
|
||||
|
||||
describe('add white', () => {
|
||||
const { up, down } = rootShapeMigrations.migrators[rootShapeVersions.AddWhite]
|
||||
|
||||
test('up works as expected', () => {
|
||||
expect(
|
||||
up({
|
||||
props: {},
|
||||
})
|
||||
).toEqual({
|
||||
props: {},
|
||||
})
|
||||
})
|
||||
|
||||
test('down works as expected', () => {
|
||||
expect(
|
||||
down({
|
||||
props: {
|
||||
color: 'white',
|
||||
},
|
||||
})
|
||||
).toEqual({
|
||||
props: {
|
||||
color: 'black',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* --- PUT YOUR MIGRATIONS TESTS ABOVE HERE --- */
|
||||
|
||||
for (const migrator of allMigrators) {
|
||||
|
|
|
@ -87,11 +87,12 @@ export const rootShapeVersions = {
|
|||
AddIsLocked: 1,
|
||||
HoistOpacity: 2,
|
||||
AddMeta: 3,
|
||||
AddWhite: 4,
|
||||
} as const
|
||||
|
||||
/** @internal */
|
||||
export const rootShapeMigrations = defineMigrations({
|
||||
currentVersion: rootShapeVersions.AddMeta,
|
||||
currentVersion: rootShapeVersions.AddWhite,
|
||||
migrators: {
|
||||
[rootShapeVersions.AddIsLocked]: {
|
||||
up: (record) => {
|
||||
|
@ -147,6 +148,22 @@ export const rootShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
[rootShapeVersions.AddWhite]: {
|
||||
up: (record) => {
|
||||
return {
|
||||
...record,
|
||||
}
|
||||
},
|
||||
down: (record) => {
|
||||
return {
|
||||
...record,
|
||||
props: {
|
||||
...record.props,
|
||||
color: record.props.color === 'white' ? 'black' : record.props.color,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ export type ShapeProps<Shape extends TLBaseShape<any, any>> = {
|
|||
[K in keyof Shape['props']]: T.Validatable<Shape['props'][K]>
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export type ShapePropsType<Config extends Record<string, T.Validatable<any>>> = Expand<{
|
||||
[K in keyof Config]: T.TypeOf<Config[K]>
|
||||
}>
|
||||
|
|
|
@ -14,7 +14,7 @@ import { ShapePropsType, TLBaseShape } from './TLBaseShape'
|
|||
export const noteShapeProps = {
|
||||
color: DefaultColorStyle,
|
||||
size: DefaultSizeStyle,
|
||||
fontSizeAdjustment: T.optional(T.positiveNumber),
|
||||
fontSizeAdjustment: T.positiveNumber,
|
||||
font: DefaultFontStyle,
|
||||
align: DefaultHorizontalAlignStyle,
|
||||
verticalAlign: DefaultVerticalAlignStyle,
|
||||
|
@ -29,19 +29,20 @@ export type TLNoteShapeProps = ShapePropsType<typeof noteShapeProps>
|
|||
/** @public */
|
||||
export type TLNoteShape = TLBaseShape<'note', TLNoteShapeProps>
|
||||
|
||||
const Versions = {
|
||||
export const noteShapeVersions = {
|
||||
AddUrlProp: 1,
|
||||
RemoveJustify: 2,
|
||||
MigrateLegacyAlign: 3,
|
||||
AddVerticalAlign: 4,
|
||||
MakeUrlsValid: 5,
|
||||
AddFontSizeAdjustment: 6,
|
||||
} as const
|
||||
|
||||
/** @internal */
|
||||
export const noteShapeMigrations = defineMigrations({
|
||||
currentVersion: Versions.MakeUrlsValid,
|
||||
currentVersion: noteShapeVersions.AddFontSizeAdjustment,
|
||||
migrators: {
|
||||
[Versions.AddUrlProp]: {
|
||||
[noteShapeVersions.AddUrlProp]: {
|
||||
up: (shape) => {
|
||||
return { ...shape, props: { ...shape.props, url: '' } }
|
||||
},
|
||||
|
@ -50,7 +51,7 @@ export const noteShapeMigrations = defineMigrations({
|
|||
return { ...shape, props }
|
||||
},
|
||||
},
|
||||
[Versions.RemoveJustify]: {
|
||||
[noteShapeVersions.RemoveJustify]: {
|
||||
up: (shape) => {
|
||||
let newAlign = shape.props.align
|
||||
if (newAlign === 'justify') {
|
||||
|
@ -70,7 +71,7 @@ export const noteShapeMigrations = defineMigrations({
|
|||
},
|
||||
},
|
||||
|
||||
[Versions.MigrateLegacyAlign]: {
|
||||
[noteShapeVersions.MigrateLegacyAlign]: {
|
||||
up: (shape) => {
|
||||
let newAlign: TLDefaultHorizontalAlignStyle
|
||||
switch (shape.props.align) {
|
||||
|
@ -116,7 +117,7 @@ export const noteShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
[Versions.AddVerticalAlign]: {
|
||||
[noteShapeVersions.AddVerticalAlign]: {
|
||||
up: (shape) => {
|
||||
return {
|
||||
...shape,
|
||||
|
@ -135,7 +136,7 @@ export const noteShapeMigrations = defineMigrations({
|
|||
}
|
||||
},
|
||||
},
|
||||
[Versions.MakeUrlsValid]: {
|
||||
[noteShapeVersions.MakeUrlsValid]: {
|
||||
up: (shape) => {
|
||||
const url = shape.props.url
|
||||
if (url !== '' && !T.linkUrl.isValid(shape.props.url)) {
|
||||
|
@ -145,5 +146,14 @@ export const noteShapeMigrations = defineMigrations({
|
|||
},
|
||||
down: (shape) => shape,
|
||||
},
|
||||
[noteShapeVersions.AddFontSizeAdjustment]: {
|
||||
up: (shape) => {
|
||||
return { ...shape, props: { ...shape.props, fontSizeAdjustment: 0 } }
|
||||
},
|
||||
down: (shape) => {
|
||||
const { fontSizeAdjustment: _, ...props } = shape.props
|
||||
return { ...shape, props }
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -26,10 +26,10 @@ export const LANGUAGES = [
|
|||
{ locale: 'vi', label: 'Tiếng Việt' },
|
||||
{ locale: 'tr', label: 'Türkçe' },
|
||||
{ locale: 'uk', label: 'Ukrainian' },
|
||||
{ locale: 'he', label: 'עברית', isRTL: true },
|
||||
{ locale: 'ar', label: 'عربي', isRTL: true },
|
||||
{ locale: 'fa', label: 'فارسی', isRTL: true },
|
||||
{ locale: 'ku', label: 'کوردی', isRTL: true },
|
||||
{ locale: 'he', label: 'עברית' },
|
||||
{ locale: 'ar', label: 'عربي' },
|
||||
{ locale: 'fa', label: 'فارسی' },
|
||||
{ locale: 'ku', label: 'کوردی' },
|
||||
{ locale: 'ne', label: 'नेपाली' },
|
||||
{ locale: 'hi-in', label: 'हिन्दी' },
|
||||
{ locale: 'te', label: 'తెలుగు' },
|
||||
|
|
|
@ -37,7 +37,6 @@
|
|||
"lint": "yarn run -T tsx ../../scripts/lint.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"fast-check": "^3.16.0",
|
||||
"tldraw": "workspace:*",
|
||||
"typescript": "^5.3.3",
|
||||
"uuid-by-string": "^4.0.0",
|
||||
|
|
|
@ -49,43 +49,6 @@ export type TLPersistentClientSocket<R extends UnknownRecord = UnknownRecord> =
|
|||
const PING_INTERVAL = 5000
|
||||
const MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING_INTERVAL * 2
|
||||
|
||||
export function _applyNetworkDiffToStore<R extends UnknownRecord, S extends Store<R> = Store<R>>(
|
||||
diff: NetworkDiff<R>,
|
||||
store: S
|
||||
): RecordsDiff<R> | null {
|
||||
const changes: RecordsDiff<R> = { added: {} as any, updated: {} as any, removed: {} as any }
|
||||
type k = keyof typeof changes.updated
|
||||
let hasChanges = false
|
||||
for (const [id, op] of objectMapEntries(diff)) {
|
||||
if (op[0] === RecordOpType.Put) {
|
||||
const existing = store.get(id as RecordId<any>)
|
||||
if (existing && !isEqual(existing, op[1])) {
|
||||
hasChanges = true
|
||||
changes.updated[id as k] = [existing, op[1]]
|
||||
} else {
|
||||
hasChanges = true
|
||||
changes.added[id as k] = op[1]
|
||||
}
|
||||
} else if (op[0] === RecordOpType.Patch) {
|
||||
const record = store.get(id as RecordId<any>)
|
||||
if (!record) {
|
||||
// the record was removed upstream
|
||||
continue
|
||||
}
|
||||
const patched = applyObjectDiff(record, op[1])
|
||||
hasChanges = true
|
||||
changes.updated[id as k] = [record, patched]
|
||||
} else if (op[0] === RecordOpType.Remove) {
|
||||
if (store.has(id as RecordId<any>)) {
|
||||
hasChanges = true
|
||||
changes.removed[id as k] = store.get(id as RecordId<any>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges ? changes : null
|
||||
}
|
||||
|
||||
// Should connect support chunking the response to allow for large payloads?
|
||||
|
||||
/**
|
||||
|
@ -532,8 +495,36 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|||
*/
|
||||
private applyNetworkDiff(diff: NetworkDiff<R>, runCallbacks: boolean) {
|
||||
this.debug('applyNetworkDiff', diff)
|
||||
const changes = _applyNetworkDiffToStore(diff, this.store)
|
||||
if (changes !== null) {
|
||||
const changes: RecordsDiff<R> = { added: {} as any, updated: {} as any, removed: {} as any }
|
||||
type k = keyof typeof changes.updated
|
||||
let hasChanges = false
|
||||
for (const [id, op] of objectMapEntries(diff)) {
|
||||
if (op[0] === RecordOpType.Put) {
|
||||
const existing = this.store.get(id as RecordId<any>)
|
||||
if (existing && !isEqual(existing, op[1])) {
|
||||
hasChanges = true
|
||||
changes.updated[id as k] = [existing, op[1]]
|
||||
} else {
|
||||
hasChanges = true
|
||||
changes.added[id as k] = op[1]
|
||||
}
|
||||
} else if (op[0] === RecordOpType.Patch) {
|
||||
const record = this.store.get(id as RecordId<any>)
|
||||
if (!record) {
|
||||
// the record was removed upstream
|
||||
continue
|
||||
}
|
||||
const patched = applyObjectDiff(record, op[1])
|
||||
hasChanges = true
|
||||
changes.updated[id as k] = [record, patched]
|
||||
} else if (op[0] === RecordOpType.Remove) {
|
||||
if (this.store.has(id as RecordId<any>)) {
|
||||
hasChanges = true
|
||||
changes.removed[id as k] = this.store.get(id as RecordId<any>)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasChanges) {
|
||||
this.store.applyDiff(changes, runCallbacks)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ import {
|
|||
TLSocketServerSentDataEvent,
|
||||
TLSocketServerSentEvent,
|
||||
} from './protocol'
|
||||
import { squishDataEvents } from './squish'
|
||||
|
||||
/** @public */
|
||||
export type TLRoomSocket<R extends UnknownRecord> = {
|
||||
|
@ -457,10 +456,7 @@ export class TLSyncRoom<R extends UnknownRecord> {
|
|||
session.socket.sendMessage(message)
|
||||
}
|
||||
} else {
|
||||
session.socket.sendMessage({
|
||||
type: 'data',
|
||||
data: squishDataEvents(session.outstandingDataMessages),
|
||||
})
|
||||
session.socket.sendMessage({ type: 'data', data: session.outstandingDataMessages })
|
||||
}
|
||||
session.outstandingDataMessages.length = 0
|
||||
}
|
||||
|
|
|
@ -236,11 +236,7 @@ export function applyObjectDiff<T extends object>(object: T, objectDiff: ObjectD
|
|||
break
|
||||
}
|
||||
case ValueOpType.Patch: {
|
||||
if (
|
||||
object[key as keyof T] &&
|
||||
typeof object[key as keyof T] === 'object' &&
|
||||
!Array.isArray(object[key as keyof T])
|
||||
) {
|
||||
if (object[key as keyof T] && typeof object[key as keyof T] === 'object') {
|
||||
const diff = op[1]
|
||||
const patched = applyObjectDiff(object[key as keyof T] as object, diff)
|
||||
if (patched !== object[key as keyof T]) {
|
||||
|
|
|
@ -1,191 +0,0 @@
|
|||
import { UnknownRecord } from '@tldraw/store'
|
||||
import { exhaustiveSwitchError, objectMapEntries, structuredClone } from '@tldraw/utils'
|
||||
import {
|
||||
NetworkDiff,
|
||||
ObjectDiff,
|
||||
RecordOp,
|
||||
RecordOpType,
|
||||
ValueOpType,
|
||||
applyObjectDiff,
|
||||
} from './diff'
|
||||
import { TLSocketServerSentDataEvent } from './protocol'
|
||||
|
||||
interface State<R extends UnknownRecord> {
|
||||
lastPatch: (TLSocketServerSentDataEvent<R> & { type: 'patch' }) | null
|
||||
squished: TLSocketServerSentDataEvent<R>[]
|
||||
}
|
||||
|
||||
type Bailed = boolean
|
||||
|
||||
function patchThePatch(lastPatch: ObjectDiff, newPatch: ObjectDiff): Bailed {
|
||||
for (const [newKey, newOp] of Object.entries(newPatch)) {
|
||||
switch (newOp[0]) {
|
||||
case ValueOpType.Put:
|
||||
lastPatch[newKey] = newOp
|
||||
break
|
||||
case ValueOpType.Append:
|
||||
if (lastPatch[newKey] === undefined) {
|
||||
lastPatch[newKey] = newOp
|
||||
} else {
|
||||
const lastOp = lastPatch[newKey]
|
||||
switch (lastOp[0]) {
|
||||
case ValueOpType.Put: {
|
||||
const lastValues = lastOp[1]
|
||||
if (Array.isArray(lastValues)) {
|
||||
const newValues = newOp[1]
|
||||
lastValues.push(...newValues)
|
||||
} else {
|
||||
// we're trying to append to something that was put previously, but
|
||||
// is not an array; bail out
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
case ValueOpType.Append: {
|
||||
const lastValues = lastOp[1]
|
||||
const lastOffset = lastOp[2]
|
||||
const newValues = newOp[1]
|
||||
const newOffset = newOp[2]
|
||||
if (newOffset === lastOffset + lastValues.length) {
|
||||
lastValues.push(...newValues)
|
||||
} else {
|
||||
// something weird is going on, bail out
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
// trying to append to either a deletion or a patch, bail out
|
||||
return true
|
||||
}
|
||||
}
|
||||
break
|
||||
case ValueOpType.Patch:
|
||||
if (lastPatch[newKey] === undefined) {
|
||||
lastPatch[newKey] = newOp
|
||||
} else {
|
||||
// bail out, recursive patching is too hard
|
||||
return true
|
||||
}
|
||||
break
|
||||
case ValueOpType.Delete:
|
||||
// overwrite whatever was there previously, no point if it's going to be removed
|
||||
// todo: check if it was freshly put and don't add if it wasn't?
|
||||
lastPatch[newKey] = newOp
|
||||
break
|
||||
default:
|
||||
exhaustiveSwitchError(newOp[0])
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function patchTheOp<R extends UnknownRecord>(
|
||||
lastRecordOp: RecordOp<R>,
|
||||
newPatch: ObjectDiff
|
||||
): Bailed {
|
||||
switch (lastRecordOp[0]) {
|
||||
case RecordOpType.Put:
|
||||
// patching a freshly added value is easy, just patch as normal
|
||||
lastRecordOp[1] = applyObjectDiff(lastRecordOp[1], newPatch)
|
||||
break
|
||||
case RecordOpType.Patch: {
|
||||
// both are patches, merge them
|
||||
const bailed = patchThePatch(lastRecordOp[1], newPatch)
|
||||
if (bailed) {
|
||||
return true
|
||||
}
|
||||
break
|
||||
}
|
||||
case RecordOpType.Remove:
|
||||
// we're trying to patch an object that was removed, just disregard the update
|
||||
break
|
||||
default:
|
||||
exhaustiveSwitchError(lastRecordOp[0])
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function squishInto<R extends UnknownRecord>(
|
||||
lastDiff: NetworkDiff<R>,
|
||||
newDiff: NetworkDiff<R>
|
||||
): Bailed {
|
||||
for (const [newId, newOp] of objectMapEntries(newDiff)) {
|
||||
switch (newOp[0]) {
|
||||
case RecordOpType.Put:
|
||||
// we Put the same record several times, just overwrite whatever came previously
|
||||
lastDiff[newId] = newOp
|
||||
break
|
||||
case RecordOpType.Patch:
|
||||
if (lastDiff[newId] === undefined) {
|
||||
// this is the patch now
|
||||
lastDiff[newId] = newOp
|
||||
} else {
|
||||
// patch the previous RecordOp!
|
||||
const bailed = patchTheOp(lastDiff[newId], newOp[1])
|
||||
if (bailed) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
break
|
||||
case RecordOpType.Remove:
|
||||
// overwrite whatever was there previously
|
||||
// todo: check if it was freshly put and don't add if it wasn't?
|
||||
lastDiff[newId] = newOp
|
||||
break
|
||||
default:
|
||||
exhaustiveSwitchError(newOp[0])
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function squishDataEvents<R extends UnknownRecord>(
|
||||
dataEvents: TLSocketServerSentDataEvent<R>[]
|
||||
): TLSocketServerSentDataEvent<R>[] {
|
||||
if (dataEvents.length < 2) {
|
||||
// most common case
|
||||
return dataEvents
|
||||
}
|
||||
|
||||
const state: State<R> = { lastPatch: null, squished: [] }
|
||||
|
||||
for (const e of dataEvents) {
|
||||
switch (e.type) {
|
||||
case 'push_result':
|
||||
if (state.lastPatch !== null) {
|
||||
state.squished.push(state.lastPatch)
|
||||
state.lastPatch = null
|
||||
}
|
||||
state.squished.push(e)
|
||||
break
|
||||
case 'patch':
|
||||
if (state.lastPatch !== null) {
|
||||
// this structuredClone is necessary to avoid modifying the original list of events
|
||||
// (otherwise objects can get reused on put and then modified on patch)
|
||||
const bailed = squishInto(state.lastPatch.diff, structuredClone(e.diff))
|
||||
if (bailed) {
|
||||
// this is unfortunate, but some patches were too hard to patch, give up
|
||||
// and return the original list
|
||||
return dataEvents
|
||||
}
|
||||
|
||||
state.lastPatch.serverClock = e.serverClock
|
||||
} else {
|
||||
state.lastPatch = structuredClone(e)
|
||||
}
|
||||
break
|
||||
default:
|
||||
exhaustiveSwitchError(e, 'type')
|
||||
}
|
||||
}
|
||||
|
||||
if (state.lastPatch !== null) {
|
||||
state.squished.push(state.lastPatch)
|
||||
}
|
||||
|
||||
return state.squished
|
||||
}
|
|
@ -1,574 +0,0 @@
|
|||
import { createRecordType, IdOf, RecordId, Store, StoreSchema, UnknownRecord } from '@tldraw/store'
|
||||
import { assert, structuredClone } from '@tldraw/utils'
|
||||
import fc, { Arbitrary } from 'fast-check'
|
||||
import { NetworkDiff, ObjectDiff, RecordOpType, ValueOpType } from '../lib/diff'
|
||||
import { TLSocketServerSentDataEvent } from '../lib/protocol'
|
||||
import { squishDataEvents } from '../lib/squish'
|
||||
import { _applyNetworkDiffToStore } from '../lib/TLSyncClient'
|
||||
|
||||
test('basic squishing', () => {
|
||||
const capture = [
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 929.58203125,
|
||||
h: 500.14453125,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9237,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679590],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1526.07421875,
|
||||
y: 565.66796875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9238,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 916.046875,
|
||||
h: 494.20703125,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9239,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679599],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1519.26171875,
|
||||
y: 563.71875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9240,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 909.234375,
|
||||
h: 492.2578125,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9241,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679608],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1512.41015625,
|
||||
y: 562.23046875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9242,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 902.3828125,
|
||||
h: 490.76953125,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9243,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679617],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1506.71484375,
|
||||
y: 561.29296875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9244,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 896.6875,
|
||||
h: 489.83203125,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9245,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679625],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1501.734375,
|
||||
y: 560.88671875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9246,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 891.70703125,
|
||||
h: 489.42578125,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9247,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679633],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1497.22265625,
|
||||
y: 560.6875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9248,
|
||||
},
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 887.1953125,
|
||||
h: 489.2265625,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9249,
|
||||
},
|
||||
] as const satisfies TLSocketServerSentDataEvent<UnknownRecord>[]
|
||||
|
||||
const squished = squishDataEvents(capture)
|
||||
const manuallySquished = [
|
||||
{
|
||||
type: 'patch',
|
||||
diff: {
|
||||
'instance_presence:nlyxdltolNVL0VONRr9Bz': [
|
||||
'patch',
|
||||
{
|
||||
lastActivityTimestamp: ['put', 1710188679633],
|
||||
cursor: [
|
||||
'put',
|
||||
{
|
||||
x: 1497.22265625,
|
||||
y: 560.6875,
|
||||
rotation: 0,
|
||||
type: 'default',
|
||||
},
|
||||
],
|
||||
brush: [
|
||||
'put',
|
||||
{
|
||||
x: 610.02734375,
|
||||
y: 71.4609375,
|
||||
w: 887.1953125,
|
||||
h: 489.2265625,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
serverClock: 9249,
|
||||
},
|
||||
]
|
||||
|
||||
// see https://github.com/jestjs/jest/issues/14011 for why the second clone is needed
|
||||
expect(squished).toStrictEqual(structuredClone(manuallySquished))
|
||||
})
|
||||
|
||||
const TEST_RECORD_TYPENAME = 'testRecord' as const
|
||||
|
||||
interface TestRecord extends UnknownRecord {
|
||||
fieldA?: TestRecordValue
|
||||
fieldB?: TestRecordValue
|
||||
fieldC?: TestRecordValue
|
||||
}
|
||||
|
||||
type TestRecordValue =
|
||||
| string
|
||||
| number[]
|
||||
| { fieldA?: TestRecordValue; fieldB?: TestRecordValue; fieldC?: TestRecordValue }
|
||||
|
||||
const TestRecord = createRecordType<TestRecord>(TEST_RECORD_TYPENAME, {
|
||||
validator: {
|
||||
validate(value) {
|
||||
return value as TestRecord
|
||||
},
|
||||
},
|
||||
scope: 'document',
|
||||
})
|
||||
|
||||
class Model {
|
||||
diffs: NetworkDiff<TestRecord>[] = []
|
||||
idMap: IdOf<TestRecord>[]
|
||||
private readonly initialStoreData: Record<IdOf<TestRecord>, TestRecord>
|
||||
|
||||
constructor(public initialStoreContent: TestRecord[]) {
|
||||
this.idMap = initialStoreContent.map((r) => r.id)
|
||||
this.initialStoreData = Object.fromEntries(initialStoreContent.map((r) => [r.id, r]))
|
||||
}
|
||||
|
||||
trueIdx(idx: number) {
|
||||
return idx % this.idMap.length
|
||||
}
|
||||
|
||||
getId(idx: number) {
|
||||
return this.idMap[this.trueIdx(idx)]
|
||||
}
|
||||
|
||||
private getFreshStore(): Store<TestRecord> {
|
||||
return new Store({
|
||||
initialData: this.initialStoreData,
|
||||
schema: StoreSchema.create<TestRecord>({ testRecord: TestRecord }),
|
||||
props: {},
|
||||
})
|
||||
}
|
||||
|
||||
private getStoreWithDiffs(diffs: NetworkDiff<TestRecord>[]) {
|
||||
const store = this.getFreshStore()
|
||||
for (const diff of diffs) {
|
||||
const changes = _applyNetworkDiffToStore(diff, store)
|
||||
if (changes !== null) {
|
||||
store.applyDiff(changes, false)
|
||||
}
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
runTest() {
|
||||
const dataEvents = this.diffs.map((diff, idx) => ({
|
||||
type: 'patch' as const,
|
||||
diff,
|
||||
serverClock: idx,
|
||||
}))
|
||||
const squishedDiffs = squishDataEvents(dataEvents).map((e) => {
|
||||
assert(e.type === 'patch')
|
||||
return e.diff
|
||||
})
|
||||
|
||||
const baseStore = this.getStoreWithDiffs(this.diffs)
|
||||
const squishedStore = this.getStoreWithDiffs(squishedDiffs)
|
||||
|
||||
// see https://github.com/jestjs/jest/issues/14011 for the explanation for that structuredClone
|
||||
expect(squishedStore.serialize()).toEqual(structuredClone(baseStore.serialize()))
|
||||
}
|
||||
|
||||
// offsets are a MAJOR pain because they depend on the entire history of diffs so far, and
|
||||
// the store silently discards append patches if their offsets don't match, so they need
|
||||
// to be correct to exercise the squisher
|
||||
// NOTE: modifies the diff
|
||||
fixOffsets(recordId: IdOf<TestRecord>, fullDiff: ObjectDiff) {
|
||||
const fixed = structuredClone(fullDiff)
|
||||
|
||||
const store = this.getStoreWithDiffs(this.diffs)
|
||||
const record = store.get(recordId)
|
||||
if (record === undefined) {
|
||||
return fixed
|
||||
}
|
||||
|
||||
const fixer = (obj: any, diff: ObjectDiff) => {
|
||||
for (const [k, v] of Object.entries(diff)) {
|
||||
if (v[0] === ValueOpType.Append && Array.isArray(obj[k])) {
|
||||
v[2] = obj[k].length
|
||||
} else if (v[0] === ValueOpType.Patch && typeof obj[k] === 'object') {
|
||||
fixer(obj[k], v[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
fixer(record, fixed)
|
||||
|
||||
return fixed
|
||||
}
|
||||
}
|
||||
|
||||
type Real = 'whatever'
|
||||
|
||||
class RecordPut implements fc.Command<Model, Real> {
|
||||
constructor(readonly record: TestRecord) {}
|
||||
check(_m: Readonly<Model>) {
|
||||
return true
|
||||
}
|
||||
run(m: Model): void {
|
||||
m.diffs.push({ [this.record.id]: [RecordOpType.Put, this.record] })
|
||||
m.idMap.push(this.record.id)
|
||||
|
||||
m.runTest()
|
||||
}
|
||||
toString = () => `Put(${JSON.stringify(this.record)})`
|
||||
}
|
||||
|
||||
class RecordRemove implements fc.Command<Model, Real> {
|
||||
constructor(readonly idx: number) {}
|
||||
check(m: Readonly<Model>) {
|
||||
return m.idMap.length > 0
|
||||
}
|
||||
run(m: Model) {
|
||||
m.diffs.push({ [m.getId(this.idx)]: [RecordOpType.Remove] })
|
||||
m.idMap.splice(m.trueIdx(this.idx), 1)
|
||||
|
||||
m.runTest()
|
||||
}
|
||||
toString = () => `Remove(#${this.idx})`
|
||||
}
|
||||
|
||||
class RecordPatch implements fc.Command<Model, Real> {
|
||||
constructor(
|
||||
readonly idx: number,
|
||||
readonly patch: ObjectDiff
|
||||
) {}
|
||||
check(m: Readonly<Model>) {
|
||||
return m.idMap.length > 0
|
||||
}
|
||||
run(m: Model) {
|
||||
const fixedPatch = m.fixOffsets(m.getId(this.idx), this.patch)
|
||||
m.diffs.push({ [m.getId(this.idx)]: [RecordOpType.Patch, fixedPatch] })
|
||||
|
||||
m.runTest()
|
||||
}
|
||||
toString = () => `Patch(#${this.idx}, ${JSON.stringify(this.patch)})`
|
||||
}
|
||||
|
||||
const { TestRecordValueArb }: { TestRecordValueArb: Arbitrary<TestRecordValue> } = fc.letrec(
|
||||
(tie) => ({
|
||||
TestRecordValueArb: fc.oneof(
|
||||
fc.string(),
|
||||
fc.array(fc.integer()),
|
||||
fc.record(
|
||||
{
|
||||
fieldA: tie('TestRecordValueArb'),
|
||||
fieldB: tie('TestRecordValueArb'),
|
||||
fieldC: tie('TestRecordValueArb'),
|
||||
},
|
||||
{ requiredKeys: ['fieldA'] }
|
||||
)
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
const TestRecordKeyArb = fc.oneof(
|
||||
fc.constant('fieldA' as const),
|
||||
fc.constant('fieldB' as const),
|
||||
fc.constant('fieldC' as const)
|
||||
)
|
||||
|
||||
const TestRecordArb = fc.record(
|
||||
{
|
||||
id: fc.oneof(fc.constant('idA'), fc.constant('idB'), fc.constant('idC')) as Arbitrary<
|
||||
RecordId<TestRecord>
|
||||
>,
|
||||
typeName: fc.constant(TEST_RECORD_TYPENAME),
|
||||
fieldA: TestRecordValueArb,
|
||||
fieldB: TestRecordValueArb,
|
||||
fieldC: TestRecordValueArb,
|
||||
},
|
||||
{ requiredKeys: ['id', 'typeName'] }
|
||||
)
|
||||
|
||||
const { ObjectDiffArb }: { ObjectDiffArb: Arbitrary<ObjectDiff> } = fc.letrec((tie) => ({
|
||||
ObjectDiffArb: fc.dictionary(
|
||||
TestRecordKeyArb,
|
||||
fc.oneof(
|
||||
fc.tuple(fc.constant(ValueOpType.Put), TestRecordValueArb),
|
||||
// The offset is -1 because it depends on the length of the array *in the current state*,
|
||||
// so it can't be generated here. Instead, it's patched up in the command
|
||||
fc.tuple(fc.constant(ValueOpType.Append), fc.array(fc.integer()), fc.constant(-1)),
|
||||
fc.tuple(fc.constant(ValueOpType.Patch), tie('ObjectDiffArb')),
|
||||
fc.tuple(fc.constant(ValueOpType.Delete))
|
||||
),
|
||||
{ minKeys: 1, maxKeys: 3 }
|
||||
),
|
||||
}))
|
||||
|
||||
const allCommands = [
|
||||
TestRecordArb.map((r) => new RecordPut(r)),
|
||||
fc.nat(10).map((idx) => new RecordRemove(idx)),
|
||||
fc.tuple(fc.nat(), ObjectDiffArb).map(([idx, diff]) => new RecordPatch(idx, diff)),
|
||||
]
|
||||
|
||||
const initialStoreContentArb: Arbitrary<TestRecord[]> = fc.uniqueArray(TestRecordArb, {
|
||||
selector: (r) => r.id,
|
||||
maxLength: 3,
|
||||
})
|
||||
|
||||
test('fast-checking squish', () => {
|
||||
// If you see this test failing, to reproduce you need both seed and path in fc.assert,
|
||||
// and replayPath in fc.commands. See the next test for an examples
|
||||
fc.assert(
|
||||
fc.property(
|
||||
initialStoreContentArb,
|
||||
fc.commands(allCommands, {}),
|
||||
(initialStoreContent, cmds) => {
|
||||
fc.modelRun(
|
||||
() => ({
|
||||
model: new Model(initialStoreContent),
|
||||
real: 'whatever',
|
||||
}),
|
||||
cmds
|
||||
)
|
||||
}
|
||||
),
|
||||
{
|
||||
verbose: 1,
|
||||
numRuns: 1_000,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('problem: applying a patch to an array', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
initialStoreContentArb,
|
||||
fc.commands(allCommands, {
|
||||
replayPath: 'CDJ:F',
|
||||
}),
|
||||
(initialStoreContent, cmds) => {
|
||||
fc.modelRun(
|
||||
() => ({
|
||||
model: new Model(initialStoreContent),
|
||||
real: 'whatever',
|
||||
}),
|
||||
cmds
|
||||
)
|
||||
}
|
||||
),
|
||||
{ seed: -1883357795, path: '7653:1:2:2:4:3:3:3:3', endOnFailure: true }
|
||||
)
|
||||
})
|
|
@ -6,7 +6,7 @@ const isTest = () =>
|
|||
|
||||
const fpsQueue: Array<() => void> = []
|
||||
const targetFps = 60
|
||||
const targetTimePerFrame = 1000 / targetFps
|
||||
const targetTimePerFrame = Math.ceil(1000 / targetFps)
|
||||
let frame: number | undefined
|
||||
let time = 0
|
||||
let last = 0
|
||||
|
|
466
yarn.lock
466
yarn.lock
|
@ -2616,6 +2616,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/aix-ppc64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/aix-ppc64@npm:0.20.2"
|
||||
conditions: os=aix & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/android-arm64@npm:0.16.3"
|
||||
|
@ -2651,6 +2658,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/android-arm64@npm:0.20.2"
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/android-arm@npm:0.16.3"
|
||||
|
@ -2686,6 +2700,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-arm@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/android-arm@npm:0.20.2"
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/android-x64@npm:0.16.3"
|
||||
|
@ -2721,6 +2742,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/android-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/android-x64@npm:0.20.2"
|
||||
conditions: os=android & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-arm64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.16.3"
|
||||
|
@ -2756,6 +2784,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-arm64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/darwin-arm64@npm:0.20.2"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/darwin-x64@npm:0.16.3"
|
||||
|
@ -2791,6 +2826,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/darwin-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/darwin-x64@npm:0.20.2"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-arm64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.16.3"
|
||||
|
@ -2826,6 +2868,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-arm64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/freebsd-arm64@npm:0.20.2"
|
||||
conditions: os=freebsd & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.16.3"
|
||||
|
@ -2861,6 +2910,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/freebsd-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/freebsd-x64@npm:0.20.2"
|
||||
conditions: os=freebsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-arm64@npm:0.16.3"
|
||||
|
@ -2896,6 +2952,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-arm64@npm:0.20.2"
|
||||
conditions: os=linux & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-arm@npm:0.16.3"
|
||||
|
@ -2931,6 +2994,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-arm@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-arm@npm:0.20.2"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ia32@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-ia32@npm:0.16.3"
|
||||
|
@ -2966,6 +3036,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ia32@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-ia32@npm:0.20.2"
|
||||
conditions: os=linux & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-loong64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-loong64@npm:0.16.3"
|
||||
|
@ -3001,6 +3078,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-loong64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-loong64@npm:0.20.2"
|
||||
conditions: os=linux & cpu=loong64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-mips64el@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.16.3"
|
||||
|
@ -3036,6 +3120,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-mips64el@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-mips64el@npm:0.20.2"
|
||||
conditions: os=linux & cpu=mips64el
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ppc64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.16.3"
|
||||
|
@ -3071,6 +3162,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-ppc64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-ppc64@npm:0.20.2"
|
||||
conditions: os=linux & cpu=ppc64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-riscv64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.16.3"
|
||||
|
@ -3106,6 +3204,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-riscv64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-riscv64@npm:0.20.2"
|
||||
conditions: os=linux & cpu=riscv64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-s390x@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-s390x@npm:0.16.3"
|
||||
|
@ -3141,6 +3246,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-s390x@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-s390x@npm:0.20.2"
|
||||
conditions: os=linux & cpu=s390x
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/linux-x64@npm:0.16.3"
|
||||
|
@ -3176,6 +3288,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/linux-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/linux-x64@npm:0.20.2"
|
||||
conditions: os=linux & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.16.3"
|
||||
|
@ -3211,6 +3330,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/netbsd-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/netbsd-x64@npm:0.20.2"
|
||||
conditions: os=netbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.16.3"
|
||||
|
@ -3246,6 +3372,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/openbsd-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/openbsd-x64@npm:0.20.2"
|
||||
conditions: os=openbsd & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/sunos-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/sunos-x64@npm:0.16.3"
|
||||
|
@ -3281,6 +3414,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/sunos-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/sunos-x64@npm:0.20.2"
|
||||
conditions: os=sunos & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-arm64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/win32-arm64@npm:0.16.3"
|
||||
|
@ -3316,6 +3456,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-arm64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/win32-arm64@npm:0.20.2"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-ia32@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/win32-ia32@npm:0.16.3"
|
||||
|
@ -3351,6 +3498,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-ia32@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/win32-ia32@npm:0.20.2"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-x64@npm:0.16.3":
|
||||
version: 0.16.3
|
||||
resolution: "@esbuild/win32-x64@npm:0.16.3"
|
||||
|
@ -3386,6 +3540,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@esbuild/win32-x64@npm:0.20.2":
|
||||
version: 0.20.2
|
||||
resolution: "@esbuild/win32-x64@npm:0.20.2"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0":
|
||||
version: 4.4.0
|
||||
resolution: "@eslint-community/eslint-utils@npm:4.4.0"
|
||||
|
@ -5841,93 +6002,107 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-android-arm-eabi@npm:4.9.5"
|
||||
"@rollup/rollup-android-arm-eabi@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-android-arm-eabi@npm:4.13.2"
|
||||
conditions: os=android & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-android-arm64@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-android-arm64@npm:4.9.5"
|
||||
"@rollup/rollup-android-arm64@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-android-arm64@npm:4.13.2"
|
||||
conditions: os=android & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-darwin-arm64@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-darwin-arm64@npm:4.9.5"
|
||||
"@rollup/rollup-darwin-arm64@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-darwin-arm64@npm:4.13.2"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-darwin-x64@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-darwin-x64@npm:4.9.5"
|
||||
"@rollup/rollup-darwin-x64@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-darwin-x64@npm:4.13.2"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.9.5"
|
||||
"@rollup/rollup-linux-arm-gnueabihf@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.13.2"
|
||||
conditions: os=linux & cpu=arm
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.9.5"
|
||||
"@rollup/rollup-linux-arm64-gnu@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.13.2"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.9.5"
|
||||
"@rollup/rollup-linux-arm64-musl@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.13.2"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.9.5"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.13.2"
|
||||
conditions: os=linux & cpu=ppc64le & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.13.2"
|
||||
conditions: os=linux & cpu=riscv64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.9.5"
|
||||
"@rollup/rollup-linux-s390x-gnu@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.13.2"
|
||||
conditions: os=linux & cpu=s390x & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.13.2"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-linux-x64-musl@npm:4.9.5"
|
||||
"@rollup/rollup-linux-x64-musl@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-linux-x64-musl@npm:4.13.2"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.9.5"
|
||||
"@rollup/rollup-win32-arm64-msvc@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.13.2"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.9.5"
|
||||
"@rollup/rollup-win32-ia32-msvc@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.13.2"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@npm:4.9.5":
|
||||
version: 4.9.5
|
||||
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.9.5"
|
||||
"@rollup/rollup-win32-x64-msvc@npm:4.13.2":
|
||||
version: 4.13.2
|
||||
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.13.2"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
@ -7487,7 +7662,6 @@ __metadata:
|
|||
"@tldraw/store": "workspace:*"
|
||||
"@tldraw/tlschema": "workspace:*"
|
||||
"@tldraw/utils": "workspace:*"
|
||||
fast-check: "npm:^3.16.0"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
nanoevents: "npm:^7.0.1"
|
||||
nanoid: "npm:4.0.2"
|
||||
|
@ -9999,12 +10173,12 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"body-parser@npm:1.20.1":
|
||||
version: 1.20.1
|
||||
resolution: "body-parser@npm:1.20.1"
|
||||
"body-parser@npm:1.20.2":
|
||||
version: 1.20.2
|
||||
resolution: "body-parser@npm:1.20.2"
|
||||
dependencies:
|
||||
bytes: "npm:3.1.2"
|
||||
content-type: "npm:~1.0.4"
|
||||
content-type: "npm:~1.0.5"
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
destroy: "npm:1.2.0"
|
||||
|
@ -10012,10 +10186,10 @@ __metadata:
|
|||
iconv-lite: "npm:0.4.24"
|
||||
on-finished: "npm:2.4.1"
|
||||
qs: "npm:6.11.0"
|
||||
raw-body: "npm:2.5.1"
|
||||
raw-body: "npm:2.5.2"
|
||||
type-is: "npm:~1.6.18"
|
||||
unpipe: "npm:1.0.0"
|
||||
checksum: 5f8d128022a2fb8b6e7990d30878a0182f300b70e46b3f9d358a9433ad6275f0de46add6d63206da3637c01c3b38b6111a7480f7e7ac2e9f7b989f6133fe5510
|
||||
checksum: 3cf171b82190cf91495c262b073e425fc0d9e25cc2bf4540d43f7e7bbca27d6a9eae65ca367b6ef3993eea261159d9d2ab37ce444e8979323952e12eb3df319a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -10911,7 +11085,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"content-type@npm:~1.0.4":
|
||||
"content-type@npm:~1.0.4, content-type@npm:~1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "content-type@npm:1.0.5"
|
||||
checksum: 585847d98dc7fb8035c02ae2cb76c7a9bd7b25f84c447e5ed55c45c2175e83617c8813871b4ee22f368126af6b2b167df655829007b21aa10302873ea9c62662
|
||||
|
@ -10946,6 +11120,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "cookie@npm:0.6.0"
|
||||
checksum: c1f8f2ea7d443b9331680598b0ae4e6af18a618c37606d1bbdc75bec8361cce09fe93e727059a673f2ba24467131a9fb5a4eec76bb1b149c1b3e1ccb268dc583
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie@npm:^0.4.1":
|
||||
version: 0.4.2
|
||||
resolution: "cookie@npm:0.4.2"
|
||||
|
@ -12693,7 +12874,87 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:^0.19.3, esbuild@npm:~0.19.10":
|
||||
"esbuild@npm:^0.20.1":
|
||||
version: 0.20.2
|
||||
resolution: "esbuild@npm:0.20.2"
|
||||
dependencies:
|
||||
"@esbuild/aix-ppc64": "npm:0.20.2"
|
||||
"@esbuild/android-arm": "npm:0.20.2"
|
||||
"@esbuild/android-arm64": "npm:0.20.2"
|
||||
"@esbuild/android-x64": "npm:0.20.2"
|
||||
"@esbuild/darwin-arm64": "npm:0.20.2"
|
||||
"@esbuild/darwin-x64": "npm:0.20.2"
|
||||
"@esbuild/freebsd-arm64": "npm:0.20.2"
|
||||
"@esbuild/freebsd-x64": "npm:0.20.2"
|
||||
"@esbuild/linux-arm": "npm:0.20.2"
|
||||
"@esbuild/linux-arm64": "npm:0.20.2"
|
||||
"@esbuild/linux-ia32": "npm:0.20.2"
|
||||
"@esbuild/linux-loong64": "npm:0.20.2"
|
||||
"@esbuild/linux-mips64el": "npm:0.20.2"
|
||||
"@esbuild/linux-ppc64": "npm:0.20.2"
|
||||
"@esbuild/linux-riscv64": "npm:0.20.2"
|
||||
"@esbuild/linux-s390x": "npm:0.20.2"
|
||||
"@esbuild/linux-x64": "npm:0.20.2"
|
||||
"@esbuild/netbsd-x64": "npm:0.20.2"
|
||||
"@esbuild/openbsd-x64": "npm:0.20.2"
|
||||
"@esbuild/sunos-x64": "npm:0.20.2"
|
||||
"@esbuild/win32-arm64": "npm:0.20.2"
|
||||
"@esbuild/win32-ia32": "npm:0.20.2"
|
||||
"@esbuild/win32-x64": "npm:0.20.2"
|
||||
dependenciesMeta:
|
||||
"@esbuild/aix-ppc64":
|
||||
optional: true
|
||||
"@esbuild/android-arm":
|
||||
optional: true
|
||||
"@esbuild/android-arm64":
|
||||
optional: true
|
||||
"@esbuild/android-x64":
|
||||
optional: true
|
||||
"@esbuild/darwin-arm64":
|
||||
optional: true
|
||||
"@esbuild/darwin-x64":
|
||||
optional: true
|
||||
"@esbuild/freebsd-arm64":
|
||||
optional: true
|
||||
"@esbuild/freebsd-x64":
|
||||
optional: true
|
||||
"@esbuild/linux-arm":
|
||||
optional: true
|
||||
"@esbuild/linux-arm64":
|
||||
optional: true
|
||||
"@esbuild/linux-ia32":
|
||||
optional: true
|
||||
"@esbuild/linux-loong64":
|
||||
optional: true
|
||||
"@esbuild/linux-mips64el":
|
||||
optional: true
|
||||
"@esbuild/linux-ppc64":
|
||||
optional: true
|
||||
"@esbuild/linux-riscv64":
|
||||
optional: true
|
||||
"@esbuild/linux-s390x":
|
||||
optional: true
|
||||
"@esbuild/linux-x64":
|
||||
optional: true
|
||||
"@esbuild/netbsd-x64":
|
||||
optional: true
|
||||
"@esbuild/openbsd-x64":
|
||||
optional: true
|
||||
"@esbuild/sunos-x64":
|
||||
optional: true
|
||||
"@esbuild/win32-arm64":
|
||||
optional: true
|
||||
"@esbuild/win32-ia32":
|
||||
optional: true
|
||||
"@esbuild/win32-x64":
|
||||
optional: true
|
||||
bin:
|
||||
esbuild: bin/esbuild
|
||||
checksum: 663215ab7e599651e00d61b528a63136e1f1d397db8b9c3712540af928c9476d61da95aefa81b7a8dfc7a9fdd7616fcf08395c27be68be8c99953fb461863ce4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:~0.19.10":
|
||||
version: 0.19.11
|
||||
resolution: "esbuild@npm:0.19.11"
|
||||
dependencies:
|
||||
|
@ -13456,15 +13717,15 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"express@npm:^4.17.1":
|
||||
version: 4.18.2
|
||||
resolution: "express@npm:4.18.2"
|
||||
version: 4.19.2
|
||||
resolution: "express@npm:4.19.2"
|
||||
dependencies:
|
||||
accepts: "npm:~1.3.8"
|
||||
array-flatten: "npm:1.1.1"
|
||||
body-parser: "npm:1.20.1"
|
||||
body-parser: "npm:1.20.2"
|
||||
content-disposition: "npm:0.5.4"
|
||||
content-type: "npm:~1.0.4"
|
||||
cookie: "npm:0.5.0"
|
||||
cookie: "npm:0.6.0"
|
||||
cookie-signature: "npm:1.0.6"
|
||||
debug: "npm:2.6.9"
|
||||
depd: "npm:2.0.0"
|
||||
|
@ -13490,7 +13751,7 @@ __metadata:
|
|||
type-is: "npm:~1.6.18"
|
||||
utils-merge: "npm:1.0.1"
|
||||
vary: "npm:~1.1.2"
|
||||
checksum: 869ae89ed6ff4bed7b373079dc58e5dddcf2915a2669b36037ff78c99d675ae930e5fe052b35c24f56557d28a023bb1cbe3e2f2fb87eaab96a1cedd7e597809d
|
||||
checksum: 3fcd792536f802c059789ef48db3851b87e78fba103423e524144d79af37da7952a2b8d4e1a007f423329c7377d686d9476ac42e7d9ea413b80345d495e30a3a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -13553,15 +13814,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-check@npm:^3.16.0":
|
||||
version: 3.16.0
|
||||
resolution: "fast-check@npm:3.16.0"
|
||||
dependencies:
|
||||
pure-rand: "npm:^6.0.0"
|
||||
checksum: 4a14945b885ef2d75c3252a067a4cfa2440a2c0da18341d514be3803fafb616b0ec68806071f29e1267a85c7a9e4a5e192ae5e592727d8d2e66389f946be472c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3":
|
||||
version: 3.1.3
|
||||
resolution: "fast-deep-equal@npm:3.1.3"
|
||||
|
@ -20425,14 +20677,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss@npm:^8.4.19, postcss@npm:^8.4.27, postcss@npm:^8.4.35, postcss@npm:^8.4.4":
|
||||
version: 8.4.35
|
||||
resolution: "postcss@npm:8.4.35"
|
||||
"postcss@npm:^8.4.19, postcss@npm:^8.4.27, postcss@npm:^8.4.38, postcss@npm:^8.4.4":
|
||||
version: 8.4.38
|
||||
resolution: "postcss@npm:8.4.38"
|
||||
dependencies:
|
||||
nanoid: "npm:^3.3.7"
|
||||
picocolors: "npm:^1.0.0"
|
||||
source-map-js: "npm:^1.0.2"
|
||||
checksum: 93a7ce50cd6188f5f486a9ca98950ad27c19dfed996c45c414fa242944497e4d084a8760d3537f078630226f2bd3c6ab84b813b488740f4432e7c7039cd73a20
|
||||
source-map-js: "npm:^1.2.0"
|
||||
checksum: 6e44a7ed835ffa9a2b096e8d3e5dfc6bcf331a25c48aeb862dd54e3aaecadf814fa22be224fd308f87d08adf2299164f88c5fd5ab1c4ef6cbd693ceb295377f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -20833,19 +21085,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:2.5.1":
|
||||
version: 2.5.1
|
||||
resolution: "raw-body@npm:2.5.1"
|
||||
dependencies:
|
||||
bytes: "npm:3.1.2"
|
||||
http-errors: "npm:2.0.0"
|
||||
iconv-lite: "npm:0.4.24"
|
||||
unpipe: "npm:1.0.0"
|
||||
checksum: 280bedc12db3490ecd06f740bdcf66093a07535374b51331242382c0e130bb273ebb611b7bc4cba1b4b4e016cc7b1f4b05a6df885a6af39c2bc3b94c02291c84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"raw-body@npm:^2.2.0":
|
||||
"raw-body@npm:2.5.2, raw-body@npm:^2.2.0":
|
||||
version: 2.5.2
|
||||
resolution: "raw-body@npm:2.5.2"
|
||||
dependencies:
|
||||
|
@ -21838,23 +22078,25 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rollup@npm:^4.2.0":
|
||||
version: 4.9.5
|
||||
resolution: "rollup@npm:4.9.5"
|
||||
"rollup@npm:^4.13.0":
|
||||
version: 4.13.2
|
||||
resolution: "rollup@npm:4.13.2"
|
||||
dependencies:
|
||||
"@rollup/rollup-android-arm-eabi": "npm:4.9.5"
|
||||
"@rollup/rollup-android-arm64": "npm:4.9.5"
|
||||
"@rollup/rollup-darwin-arm64": "npm:4.9.5"
|
||||
"@rollup/rollup-darwin-x64": "npm:4.9.5"
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "npm:4.9.5"
|
||||
"@rollup/rollup-linux-arm64-gnu": "npm:4.9.5"
|
||||
"@rollup/rollup-linux-arm64-musl": "npm:4.9.5"
|
||||
"@rollup/rollup-linux-riscv64-gnu": "npm:4.9.5"
|
||||
"@rollup/rollup-linux-x64-gnu": "npm:4.9.5"
|
||||
"@rollup/rollup-linux-x64-musl": "npm:4.9.5"
|
||||
"@rollup/rollup-win32-arm64-msvc": "npm:4.9.5"
|
||||
"@rollup/rollup-win32-ia32-msvc": "npm:4.9.5"
|
||||
"@rollup/rollup-win32-x64-msvc": "npm:4.9.5"
|
||||
"@rollup/rollup-android-arm-eabi": "npm:4.13.2"
|
||||
"@rollup/rollup-android-arm64": "npm:4.13.2"
|
||||
"@rollup/rollup-darwin-arm64": "npm:4.13.2"
|
||||
"@rollup/rollup-darwin-x64": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-arm64-gnu": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-arm64-musl": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-riscv64-gnu": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-s390x-gnu": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-x64-gnu": "npm:4.13.2"
|
||||
"@rollup/rollup-linux-x64-musl": "npm:4.13.2"
|
||||
"@rollup/rollup-win32-arm64-msvc": "npm:4.13.2"
|
||||
"@rollup/rollup-win32-ia32-msvc": "npm:4.13.2"
|
||||
"@rollup/rollup-win32-x64-msvc": "npm:4.13.2"
|
||||
"@types/estree": "npm:1.0.5"
|
||||
fsevents: "npm:~2.3.2"
|
||||
dependenciesMeta:
|
||||
|
@ -21872,8 +22114,12 @@ __metadata:
|
|||
optional: true
|
||||
"@rollup/rollup-linux-arm64-musl":
|
||||
optional: true
|
||||
"@rollup/rollup-linux-powerpc64le-gnu":
|
||||
optional: true
|
||||
"@rollup/rollup-linux-riscv64-gnu":
|
||||
optional: true
|
||||
"@rollup/rollup-linux-s390x-gnu":
|
||||
optional: true
|
||||
"@rollup/rollup-linux-x64-gnu":
|
||||
optional: true
|
||||
"@rollup/rollup-linux-x64-musl":
|
||||
|
@ -21888,7 +22134,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
rollup: dist/bin/rollup
|
||||
checksum: 4debf528e63edea5c3f5d38e399c6dd7287e2977d90d2d3ce38d4b3412289e2081aff8f8488a11b1699c786f2e904e9e150f30d576fe9316b5b97df0e80b1bce
|
||||
checksum: 08ad387c16ac3595bbcc7d2cbb7c3cb4366306244b92145819e5671dbf6a7540760985b87656a7cd4238a507b6deaaa020065c688b2249d0baadce4594d0d0c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -22422,10 +22668,10 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "source-map-js@npm:1.0.2"
|
||||
checksum: 38e2d2dd18d2e331522001fc51b54127ef4a5d473f53b1349c5cca2123562400e0986648b52e9407e348eaaed53bce49248b6e2641e6d793ca57cb2c360d6d51
|
||||
"source-map-js@npm:^1.0.1, source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "source-map-js@npm:1.2.0"
|
||||
checksum: 74f331cfd2d121c50790c8dd6d3c9de6be21926de80583b23b37029b0f37aefc3e019fa91f9a10a5e120c08135297e1ecf312d561459c45908cb1e0e365f49e5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -24667,8 +24913,8 @@ __metadata:
|
|||
linkType: hard
|
||||
|
||||
"vite@npm:^3.0.0 || ^4.0.0, vite@npm:^4.1.4":
|
||||
version: 4.5.2
|
||||
resolution: "vite@npm:4.5.2"
|
||||
version: 4.5.3
|
||||
resolution: "vite@npm:4.5.3"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.18.10"
|
||||
fsevents: "npm:~2.3.2"
|
||||
|
@ -24702,18 +24948,18 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 3feb39f8da038fb2b1ad074c19a9579c263c1d7a872c5c6e0269b82d67805bb8c93cf9fc393e852807483ae9a918b1ac2861c72f73ee92fb3935ea68333a2cf7
|
||||
checksum: 82efe1bc6d6848f8c97b71f1dc5b2fba2c3f30b2207ef2451c8df1a0ed5903c55714d7cd8ecb75879b488661d97f6e01a4ad758b5ef6a50a14338f916233bfa4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^5.0.0":
|
||||
version: 5.1.6
|
||||
resolution: "vite@npm:5.1.6"
|
||||
version: 5.2.8
|
||||
resolution: "vite@npm:5.2.8"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.19.3"
|
||||
esbuild: "npm:^0.20.1"
|
||||
fsevents: "npm:~2.3.3"
|
||||
postcss: "npm:^8.4.35"
|
||||
rollup: "npm:^4.2.0"
|
||||
postcss: "npm:^8.4.38"
|
||||
rollup: "npm:^4.13.0"
|
||||
peerDependencies:
|
||||
"@types/node": ^18.0.0 || >=20.0.0
|
||||
less: "*"
|
||||
|
@ -24742,7 +24988,7 @@ __metadata:
|
|||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: f48073e93ead62fa58034398442de4517c824b3e50184f8b4059fb24077a26f2c04e910e29d7fb7ec51ea53eb61b9c7d94d56b14a38851de80c67480094cc79d
|
||||
checksum: caa40343c2c4e6d8e257fccb4c3029f62909c319a86063ce727ed550925c0a834460b0d1ca20c4d6c915f35302aa1052f6ec5193099a47ce21d74b9b817e69e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue