Skip to content

feat: change event allows getting a list of the changes made #1585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 17, 2025

Conversation

nperez0111
Copy link
Contributor

@nperez0111 nperez0111 commented Apr 4, 2025

This implements a getChanges API which will derive the changes made within a transaction as a list of: inserts, updates and deletions of blocknote blocks

To test this you can update an example like so:

diff --git i/examples/01-basic/01-minimal/App.tsx w/examples/01-basic/01-minimal/App.tsx
index a3b92bafd..d5c43ac2b 100644
--- i/examples/01-basic/01-minimal/App.tsx
+++ w/examples/01-basic/01-minimal/App.tsx
@@ -8,5 +8,12 @@ export default function App() {
   const editor = useCreateBlockNote();
 
   // Renders the editor instance using a React component.
-  return <BlockNoteView editor={editor} />;
+  return (
+    <BlockNoteView
+      editor={editor}
+      onChange={(t, ctx) => {
+        console.log("changes", ctx.getChanges());
+      }}
+    />
+  );
 }

Things left:

  • add tests

Resolves #1541 #482 #1566

Copy link

vercel bot commented Apr 4, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
blocknote ✅ Ready (Inspect) Visit Preview Apr 17, 2025 7:55am
blocknote-website ✅ Ready (Inspect) Visit Preview Apr 17, 2025 7:55am

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

Copy link

pkg-pr-new bot commented Apr 7, 2025

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/ariakit@1585

@blocknote/code-block

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@1585

@blocknote/core

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@1585

@blocknote/mantine

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@1585

@blocknote/react

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@1585

@blocknote/server-util

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@1585

@blocknote/shadcn

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/shadcn@1585

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@1585

@blocknote/xl-multi-column

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@1585

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@1585

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@1585

commit: 91c1d3d

Copy link
Collaborator

@YousefED YousefED left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so awesome, and implementation actually cleaner / clearer than expected! really cool

const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];
// TODO when we upgrade to Tiptap v3, we can get the appendedTransactions which would give us things like the actual inserted Block IDs.
// since they are appended to the transaction via the unique-id plugin
const combinedTransaction = combineTransactionSteps(transaction.before, [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't completely understand this. How do we get the block ids now then, if we don't yet support the uniqueid transactions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiptap V2 gives the root tr that was applied which does not include any appended transactions (since it is only the root tr).
Tiptap V3 gives both root tr & any appended transaction in that callback which means you can see the appended transactions & re-combine them to get the "actual" changed ranges.

Technically, the editor state is up to date by the time we are reading this, but we can't know what transactions have been appended so our transaction ranges can be incorrect.

The reason that we are getting ids out of this right now is that nodeToBlock will generate an ID if it does not find one. But, those IDs are not the same ID that actually would have been persisted into the document.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we actually need to need the UniqueId transactions for? Since this is more for tracking user changes, so giving a new block an ID doesn't seem like smth we need to listen for. So is the issue just that if a new block is created, when we try to get it with nodeToBlock, it will generate a random ID as the UniqueId transaction is not detected here?

Copy link
Contributor Author

@nperez0111 nperez0111 Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason the unique-id appended transaction is important is that if you do getChanges, any inserted block technically hasn't been assigned an ID yet, it just generates one at random (in nodeToBlock as you described). So, you can't actually trust that ID to do lookups anywhere (in the document or otherwise). So, in the future, we can actually append that transaction to get the correct ID and not have that issue - making the ID reported by getChanges accurate to what is actually in the document.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason that we are getting ids out of this right now is that nodeToBlock will generate an ID if it does not find one. But, those IDs are not the same ID that actually would have been persisted into the document.

is this a blocker for the PR? it makes the "insert" changes not very useful then, right?

Copy link
Contributor Author

@nperez0111 nperez0111 Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It depends on what they are using this for. If this is just to inspect the content to do things like remove image uploads, then it won't matter.

I'm uncertain of what you'd need the insert changes for, much less whether it is a hard requirement to have the ID be correct. I get that correctness is important, in general. But, I hope that Tiptap V3 is around the corner and alleviates us of this.

Thinking out loud: One way to get around this would be to get the previous editor state & apply the transaction again (which would run the plugins again). But, the ID would still be incorrect since it was randomly generated. So, not helpful either.
Another way would be to try to find the node afterward, but technically any plugin could append a transaction that makes the position no longer valid. So not helpful in the general case, but probably fine just for blocknote.
Or, on inserting a node, we just already give it an ID

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, those workarounds sound painful. I figured a quick and easy fix be to update insertBlocks to add the id before issueing the transaction. However, then we'd still have scenarios where user operations like enter doesn't show the right id

It depends on what they are using this for. If this is just to inspect the content to do things like remove image uploads, then it won't matter.

Thinking of two scenarios atm that will be difficult now:

  • Table of contents: detect inserted headings and turn them into a ToC. While this would now work, you can't make clicking the ToC heading work because you don't know to which block in the document you need to scroll
  • post-processing: maybe you'd want to use this API to do some kind of post-processing / validation and update the block to make sure it always satisfies "some" requirements - (ofc; doing this after a change is still rather hacky; we should probably know more about these use-cases and expose a better API for this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've solved it. We just override the Tiptap dispatchTransaction method & re-implement it for ourselves.

I made this into a new event type to not disrupt any callers (even though it only adds a new field to the context). It also makes it clear that we should clean this up when we get off of Tiptap V2.

The switch to v3 then becomes only having to switch which event type to listen for, simple!

06b4306

const changes: BlocksChanged<BSchema, ISchema, SSchema> = [];
// TODO when we upgrade to Tiptap v3, we can get the appendedTransactions which would give us things like the actual inserted Block IDs.
// since they are appended to the transaction via the unique-id plugin
const combinedTransaction = combineTransactionSteps(transaction.before, [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we actually need to need the UniqueId transactions for? Since this is more for tracking user changes, so giving a new block an ID doesn't seem like smth we need to listen for. So is the issue just that if a new block is created, when we try to get it with nodeToBlock, it will generate a random ID as the UniqueId transaction is not detected here?

removedBlockIds.forEach((blockId) => {
changes.push({
type: "delete",
block: prevAffectedBlocks.find((block) => block.id === blockId)!,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't prevBlock be set instead of block? Since the block previously existed and no longer does

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of doing that too. But felt that if someone just wanted, "list of all changed blocks", it'd be just getChanges().map(c=>c.block), whereas branching on the type is more cumbersome.

I think an argument can be made for either way (i.e. the type is 'deleted' so obviously block is the deleted block)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep fair, I don't have a strong opinion one way or another, just wanted to make sure it was intentional

Copy link
Collaborator

@YousefED YousefED left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small last notes added!

@@ -0,0 +1,190 @@
[
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the case we discussed. The we want it like this (can we clearly explain this), or prefer only to share the update children?

Let's at least make sure it's clearly documented :)

Copy link
Contributor Author

@nperez0111 nperez0111 Apr 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to clean this one up & filter out the parent from the change

7ed62d8

/**
* Whether the change is from this client or another client.
*/
isChangeOrigin: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if this is false, shouldn't we just trigger type: "local" instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isChangeOrigin is never false for transactions emitted by y-sync plugin. So I just removed it.

If you are wondering what it is for, ysync plugin can tell if a change is made within the set of origins that the undo manager is looking for. It stores this in it's state but never sets it to anything other than true on a prosemirror transaction

Copy link

sentry-io bot commented Apr 17, 2025

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ InvalidStateError: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state. /docs/collaboration/comments View Issue
  • ‼️ RangeError: Position 5945 outside of fragment (<blockGroup(blockContainer(paragraph))>) / View Issue
  • ‼️ RangeError: Index 1 out of range for <tableParagraph("c")> / View Issue
  • ‼️ RangeError: Index 1 out of range for <tableParagraph("c")> / View Issue
  • ‼️ ShikiError: Language typescriptreact not found, you may need to load it first /examples/theming/custom-code-block View Issue

Did you find this useful? React with a 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Putting a watcher on blocks add / edition / removal
3 participants