tiptap
tiptap copied to clipboard
Custom Document & TrailingNode extensions don't play well with Collaboration/Y.js
What’s the bug you are facing?
I have an editor that uses both CustomDocument and TrailingNode extensions, which works great (thanks!).
I recently added the Collaboration extension with an IndexeddbPersistence
provider to support offline access, as described here: https://docs.yjs.dev/getting-started/allowing-offline-editing
However, the combination of these causes a bug where extraneous nodes are appended to the editor every time the page is refreshed. I've narrowed the issue down to the interaction between these 3 extensions.
Which browser was this experienced in? Are any special extensions installed?
Chrome Version 109.0.5414.119
How can we reproduce the bug on our side?
Here's a minimal repro: https://codesandbox.io/s/tiptap-indexeddb-bug-rzv4cg?file=/src/App.js
I followed the Tiptap docs to enable these extensions:
- https://tiptap.dev/examples/custom-document
- https://tiptap.dev/api/extensions/collaboration
- https://tiptap.dev/experiments/trailing-node
Then added a Y.js provider for offline access:
- https://docs.yjs.dev/getting-started/allowing-offline-editing
On initial load, everything works. But every time the page is refreshed, extraneous nodes are appended to the end of the doc.
Interestingly, if you disable any of these 3 extensions, everything works, but the bug occurs reliably if all 3 are enabled.
I've tried changing order/priority, and nothing seems to fix it :-(
Can you provide a CodeSandbox?
No response
What did you expect to happen?
These extensions can be used together, without extraneous nodes being added.
Anything to add? (optional)
Happy to spend time debugging if you have any guess what might be causing this.
Did you update your dependencies?
- [X] Yes, I’ve updated my dependencies to use the latest version of all packages.
Are you sponsoring us?
- [ ] Yes, I’m a sponsor. 💖
Same here -ish. Custom node breaks my hocuspocus/yjs server implementation. Getting "Yjs update RangeError: Position X out of range".
This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days
After TipTap Editor is rendered with an empty const ydoc = new Y.Doc()
, The Trailing extension immediately inserts a new paragraph, and the moment after yDoc receives changes from a persister, it merges them with that new paragraph.
So there are probably 2 solutions:
- Do not render Editor before you receive changes from a persister.
- Register trailing plugin (should be prosemirror plugin, not TipTap extension) after you receive changes from a persister
import { trailingNode } from 'prosemirror-trailing-node'
// ...
editor.registerPlugin(trailingNode())
@holdenmatt I have been working through a similar issue and deduced this down to a side-effect of using any customization that modifies the document content on the initial load, for example:
- A CustomDocument with a non-standard content schema (in your example, forcing the first-line heading) will initialize the doc to match that schema...so it's not really empty.
- A TrailingNode extension will add an empty paragraph on initialization.
When you pass in an empty Y.Doc, the ProseMirror editor initializes it as empty and then inserts content to match the schema (for example, an empty heading and an empty block node is inserted or an extension adds an empty paragraph at the end). Then, when the doc synchronizes via a provider, the changes that are loaded from the provider are merged with the "empty" doc, replicating the "empty" heading and first block and causing "duplicate" content.
After researching this a bit, I stumbled upon an old ProseMirror post: https://discuss.yjs.dev/t/initial-offline-value-of-a-shared-document/465/13 describing how to set the initial value of a shared doc. I followed these suggestions, and instead of initializing an empty Y.Doc, I initialize it with a templated Y.Doc with the initial content the editor would normally insert (a heading + an empty paragraph or with a trailing node already added). Any client synchronizing via the provider will then have the same "base" document to work from, so no merges of that duplicate initial content will happen.
Feel free to shoot me a note if you need more details. Cheers!
@holdenmatt did you figure out the issue? I tried both @yan-yanishevsky and @jsherer solutions. But it doesn't seem to work.