tiptap/docs/guide/collaborative-editing.md

307 lines
13 KiB
Markdown
Raw Normal View History

---
tableOfContents: true
---
2020-10-05 20:56:45 +08:00
# Collaborative editing
## Introduction
Real-time collaboration, syncing between different devices and working offline used to be hard. We provide everything you need to keep everything in sync with the power of [Y.js](https://github.com/yjs/yjs). The following guide helps you get started with collaborative editing in Tiptap. Dont worry, a production-grade setup doesnt require much code.
2020-10-05 20:56:45 +08:00
2021-02-03 17:41:37 +08:00
## Configure the editor
The underyling schema Tiptap uses is an excellent foundation to sync documents. With the [`Collaboration`](/api/extensions/collaboration) extension you can tell Tiptap to track changes to the document with [Y.js](https://github.com/yjs/yjs).
2020-10-05 20:56:45 +08:00
Y.js is a conflict-free replicated data types implementation, or in other words: Its really good in merging changes. And to achieve that, changes dont even have to come in order. Its totally fine to change a document while being offline and merge it with other changes when the device is online again.
2020-10-05 20:56:45 +08:00
Somehow, all clients need to interchange document modifications at some point. The most popular technologies to do that are [WebRTC](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) and [WebSockets](https://developer.mozilla.org/de/docs/Web/API/WebSocket), so lets have a closer look at those:
2020-11-28 00:23:04 +08:00
### WebRTC
WebRTC uses a server only to connect clients with each other. The actual data is then flowing between the clients, without the server knowing anything about it and thats great to take the first steps with collaborative editing.
First, install the dependencies:
2020-11-28 00:23:04 +08:00
```bash
npm install @tiptap/extension-collaboration yjs y-webrtc y-prosemirror
2020-11-28 00:23:04 +08:00
```
2021-10-20 04:30:45 +08:00
Now, create a new Y document, and register it with Tiptap:
2020-11-28 00:23:04 +08:00
```js
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
2020-11-28 00:23:04 +08:00
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
import { WebrtcProvider } from 'y-webrtc'
// A new Y document
const ydoc = new Y.Doc()
// Registered with a WebRTC provider
const provider = new WebrtcProvider('example-document', ydoc)
const editor = new Editor({
extensions: [
StarterKit.configure({
// The Collaboration extension comes with its own history handling
history: false,
}),
2021-10-20 04:30:45 +08:00
// Register the document with Tiptap
2020-11-28 00:23:04 +08:00
Collaboration.configure({
document: ydoc,
2020-11-28 00:23:04 +08:00
}),
],
})
```
2021-10-20 04:30:45 +08:00
This should be enough to create a collaborative instance of Tiptap. Crazy, isnt it? Try it out, and open the editor in two different browsers. Changes should be synced between different windows.
2020-11-28 00:23:04 +08:00
So how does this magic work? All clients need to connect with eachother, thats the job of a *provider*. The [WebRTC provider](https://github.com/yjs/y-webrtc) is the easiest way to get started with, as it uses a public server to connect clients directly with each other, but not to sync the actual changes. This has two downsides, though.
2020-11-28 00:23:04 +08:00
1. Browsers refuse to connect with too many clients. With Y.js its enough if all clients are connected indirectly, but even that isnt possible at some point. Or in other words, it doesnt scale well for more than 100+ concurrent clients in the same document.
2. Its likely you want to involve a server to persist changes anyway. But the WebRTC signaling server (which connects all clients with eachother) doesnt receive the changes and therefore doesnt know whats in the document.
2020-11-28 00:23:04 +08:00
2020-11-28 05:17:03 +08:00
Anyway, if you want to dive deeper, head over to [the Y WebRTC repository](https://github.com/yjs/y-webrtc) on GitHub.
2020-11-28 00:23:04 +08:00
### WebSocket (Recommended)
2023-01-18 23:13:27 +08:00
For most uses cases, a WebSocket provider is the recommended choice. Its very flexible and can scale very well. To make it even easier, we released [Hocuspocus](https://hocuspocus.dev) as an official backend for Tiptap.
For the client, the example is nearly the same, only the provider is different. First, lets install the dependencies:
2020-11-28 00:23:04 +08:00
```bash
npm install @tiptap/extension-collaboration @hocuspocus/provider y-prosemirror
2020-11-28 00:23:04 +08:00
```
2021-10-20 04:30:45 +08:00
And then register the WebSocket provider with Tiptap:
2020-11-28 00:23:04 +08:00
```js
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
2020-11-28 00:23:04 +08:00
import Collaboration from '@tiptap/extension-collaboration'
import { HocuspocusProvider } from '@hocuspocus/provider'
2020-11-28 00:23:04 +08:00
// Set up the Hocuspocus WebSocket provider
const provider = new HocuspocusProvider({
url: 'ws://127.0.0.1:1234',
name: 'example-document',
})
2020-11-28 00:23:04 +08:00
const editor = new Editor({
extensions: [
StarterKit.configure({
// The Collaboration extension comes with its own history handling
history: false,
}),
2021-10-20 04:30:45 +08:00
// Register the document with Tiptap
2020-11-28 00:23:04 +08:00
Collaboration.configure({
document: provider.document,
2020-11-28 00:23:04 +08:00
}),
],
})
```
This example doesnt work out of the box. As you can see, its configured to talk to a WebSocket server which is available under `ws://127.0.0.1:1234` (WebSocket protocol `ws://`, your local IP `127.0.0.1` and the port `1234`). You need to set this up, too.
#### The WebSocket backend
2023-01-20 22:53:54 +08:00
To make the server part as easy as possible, we provide [an opinionated server package, called Hocuspocus](http://hocuspocus.dev/). Its a flexible Node.js package, that you can use to build your custom backend.
2020-11-28 00:23:04 +08:00
For the purpose of that guide, lets just use the command-line interface which boots a minimal server literally in seconds:
2020-11-28 00:23:04 +08:00
```bash
npx @hocuspocus/cli --port 1234 --sqlite
2020-11-28 00:23:04 +08:00
```
This command downloads the Hocuspocus command-line interface, starts a server listening on port 1234 and stores changes in the memory (so its gone once you stop the command). The output should look like this:
2020-11-28 00:23:04 +08:00
```
Hocuspocus v1.0.0 running at:
2020-11-28 00:23:04 +08:00
> HTTP: http://127.0.0.1:1234
> WebSocket: ws://127.0.0.1:1234
2020-11-28 00:23:04 +08:00
Ready.
2020-11-28 00:23:04 +08:00
```
Try opening http://127.0.0.1:1234 in your browser. You should see a plain text `OK` if everything works fine.
Go back to your Tiptap editor and hit reload, it should now connect to the Hocuspocus WebSocket server and changes should sync with all other clients. Amazing, isnt it?
2020-10-05 20:56:45 +08:00
2023-05-12 18:14:35 +08:00
!!tiptap-collab-cta
### Multiple network providers
You can even combine multiple providers. Thats not needed, but could keep clients connected, even if one connection - for example the WebSocket server - goes down for a while. Here is an example:
```js
new WebrtcProvider('example-document', ydoc)
new HocuspocusProvider({
url: 'ws://127.0.0.1:1234',
name: 'example-document',
document: ydoc,
})
```
Yes, thats all.
Keep in mind that WebRTC needs a signaling server to connect clients. This signaling server doesnt receive the synced data, but helps to let clients find each other. You can [run your own signaling server](https://github.com/yjs/y-webrtc#signaling), if you like. Otherwise its using a default URL baked into the package.
### Show other cursors
To enable users to see the cursor and text selections of each other, add the [`CollaborationCursor`](/api/extensions/collaboration-cursor) extension.
2020-11-28 00:23:04 +08:00
```js
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
2020-11-28 00:23:04 +08:00
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import { HocuspocusProvider } from '@hocuspocus/provider'
2020-11-28 00:23:04 +08:00
// Set up the Hocuspocus WebSocket provider
const provider = new HocuspocusProvider({
url: 'ws://127.0.0.1:1234',
name: 'example-document',
})
2020-11-28 00:23:04 +08:00
const editor = new Editor({
extensions: [
StarterKit.configure({
// The Collaboration extension comes with its own history handling
history: false,
}),
Collaboration.configure({
document: provider.document,
}),
2020-11-28 00:23:04 +08:00
// Register the collaboration cursor extension
CollaborationCursor.configure({
provider: provider,
user: {
name: 'Cyndi Lauper',
color: '#f783ac',
},
2020-11-28 00:23:04 +08:00
}),
],
})
```
2020-11-30 21:44:18 +08:00
As you can see, you can pass a name and color for every user. Look at the [collaborative editing example](/examples/collaborative-editing), to see a more advanced example.
2020-10-05 20:56:45 +08:00
### Offline support
2020-11-28 05:17:03 +08:00
Adding offline support to your collaborative editor is basically a one-liner, thanks to the fantastic [Y IndexedDB adapter](https://github.com/yjs/y-indexeddb). Install it:
2020-11-28 00:23:04 +08:00
```bash
npm install y-indexeddb
```
And connect it with a Y document:
```js
import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import * as Y from 'yjs'
import { IndexeddbPersistence } from 'y-indexeddb'
const ydoc = new Y.Doc()
2020-11-28 00:23:04 +08:00
// Store the Y document in the browser
new IndexeddbPersistence('example-document', ydoc)
2020-11-28 00:23:04 +08:00
const editor = new Editor({
extensions: [
// …
Collaboration.configure({
document: ydoc,
2020-11-28 00:23:04 +08:00
}),
],
})
```
2020-11-28 05:17:03 +08:00
All changes will be stored in the browser then, even if you close the tab, go offline, or make changes while working offline. Next time you are online, the WebSocket provider will try to find a connection and eventually sync the changes.
2020-11-28 00:23:04 +08:00
2020-11-28 05:17:03 +08:00
Yes, its magic. As already mentioned, that is all based on the fantastic Y.js framework. And if youre using it, or our integration, you should definitely [sponsor Kevin Jahns on GitHub](https://github.com/dmonad), he is the brain behind Y.js.
2020-10-05 20:56:45 +08:00
## Our plug & play collaboration backend
2023-01-18 23:13:27 +08:00
Our collaborative editing backend [Hocuspocus](https://hocuspocus.dev) handles the syncing, authorization, persistence and scaling. Lets go through a few common use cases here!
2020-12-04 00:23:54 +08:00
2020-12-04 20:41:05 +08:00
### The document name
The document name is `'example-document'` in all examples here, but it could be any string. In a real-world app youd probably add the name of your entity and the ID of the entity. Here is how that could look like:
2020-12-04 20:41:05 +08:00
```js
const documentName = 'page.140'
2020-12-04 20:41:05 +08:00
```
In the backend, you can split the string to know the user is typing on a page with the ID 140 to manage authorization and such accordingly. New documents are created on the fly, no need to tell the backend about them, besides passing a string to the provider.
And if you would like to sync multiple fields with one Y.js document, just pass different fragment names to the collaboration extension:
```js
2021-10-20 04:30:45 +08:00
// a Tiptap instance for the field
Collaboration.configure({
document: ydoc,
field: 'title',
})
// and another instance for the summary, both in the same Y.js document
Collaboration.configure({
document: ydoc,
field: 'summary',
})
```
If your setup is somehow more complex, for example with nested fragments, you can pass a raw Y.js fragment too. `document` and `field` will be ignored then.
```js
// a raw Y.js fragment
Collaboration.configure({
fragment: ydoc.getXmlFragment('custom'),
})
```
2020-12-04 20:41:05 +08:00
2021-04-20 19:18:11 +08:00
### Authentication & Authorization
With the `onAuthenticate` hook you can check if a client is authenticated and authorized to view the current document. In a real world application this would probably be a request to an API, a database query or something else.
2020-12-02 22:35:41 +08:00
When throwing an error (or rejecting the returned Promise), the connection to the client will be terminated. If the client is authorized and authenticated you can also return contextual data which will be accessible in other hooks. But you dont need to.
2020-11-28 00:23:04 +08:00
```js
import { Server } from '@hocuspocus/server'
const server = Server.configure({
async onAuthenticate({ token }) {
// Example test if a user is authenticated
if (token !== 'super-secret-token') {
2021-04-20 19:18:11 +08:00
throw new Error('Not authorized!')
}
2021-04-20 19:18:11 +08:00
// You can set contextual data to use it in other hooks
return {
user: {
id: 1234,
name: 'John',
},
}
2020-11-28 00:23:04 +08:00
},
})
server.listen()
```
2023-05-12 18:14:35 +08:00
## Tiptap Collab our hosted solution
If you dont want the struggle of self-hosting and scaling Hocuspocus, make sure to check out our managed solution Tiptap Collab.
Its just a few clicks away, really.
!!tiptap-collab-cta
2020-12-04 22:12:38 +08:00
## Pitfalls
### Schema updates
feat(pm): new prosemirror package for dependency resolving * chore:(core): migrate to tsup * chore: migrate blockquote and bold to tsup * chore: migrated bubble-menu and bullet-list to tsup * chore: migrated more packages to tsup * chore: migrate code and character extensions to tsup * chore: update package.json to simplify build for all packages * chore: move all packages to tsup as a build process * chore: change ci build task * feat(pm): add prosemirror meta package * rfix: resolve issues with build paths & export mappings * docs: update documentation to include notes for @tiptap/pm * chore(pm): update tsconfig * chore(packages): update packages * fix(pm): add package export infos & fix dependencies * chore(general): start moving to pm package as deps * chore: move to tiptap pm package internally * fix(demos): fix demos working with new pm package * fix(tables): fix tables package * fix(tables): fix tables package * chore(demos): pinned typescript version * chore: remove unnecessary tsconfig * chore: fix netlify build * fix(demos): fix package resolving for pm packages * fix(tests): fix package resolving for pm packages * fix(tests): fix package resolving for pm packages * chore(tests): fix tests not running correctly after pm package * chore(pm): add files to files array * chore: update build workflow * chore(tests): increase timeout time back to 12s * chore(docs): update docs * chore(docs): update installation guides & pm information to docs * chore(docs): add link to prosemirror docs * fix(vue-3): add missing build step * chore(docs): comment out cdn link * chore(docs): remove semicolons from docs * chore(docs): remove unnecessary installation note * chore(docs): remove unnecessary installation note
2023-02-03 00:37:33 +08:00
Tiptap is very strict with the [schema](/api/schema), that means, if you add something thats not allowed according to the configured schema itll be thrown away. That can lead to a strange behaviour when multiple clients with different schemas share changes to a document.
2020-12-04 22:12:38 +08:00
2021-10-20 04:30:45 +08:00
Lets say you added an editor to your app and the first people use it already. They have all a loaded instance of Tiptap with all default extensions, and therefor a schema that only allows those. But you want to add task lists in the next update, so you add the extension and deploy again.
2020-12-04 22:12:38 +08:00
A new user opens your app and has the updated schema (with task lists), while all others still have the old schema (without task lists). The new user checks out the newly added tasks lists and adds it to a document to show that feature to other users in that document. But then, it magically disappears right after she added it. What happened?
2021-10-20 04:30:45 +08:00
When one user adds a new node (or mark), that change will be synced to all other connected clients. The other connected clients apply those changes to the editor, and Tiptap, strict as it is, removes the newly added node, because its not allowed according to their (old) schema. Those changes will be synced to other connected clients and oops, its removed everywhere. To avoid this you have a few options:
2020-12-04 22:12:38 +08:00
1. Never change the schema (not cool).
2. Force clients to update when you deploy a new schema (tough).
3. Keep track of the schema version and disable the editor for clients with an outdated schema (depends on your setup).
Its on our list to provide features to make that easier. If youve got an idea how to improve that, share it with us!