Tiptap prosemirror bindings for Yjs
npm install @tiptap/y-tiptapThis binding maps a Y.XmlFragment to the ProseMirror state.
We forked y-prosemirror to create a Tiptap-specific package with changes we needed for Tiptap-related features. These modifications were too specific to be merged upstream or would have added maintenance overhead for the y-prosemirror maintainers.
Where feasible, we contribute improvements and fixes back to y-prosemirror to support the broader ecosystem.
This package is designed for use with Tiptap and is not intended as a general-purpose Yjs binding for ProseMirror.
* Sync ProseMirror state
* Shared Cursors
* Shared Undo / Redo (each client has its own undo-/redo-history)
* Successfully recovers when concurrents edit result in an invalid document schema
``js
import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo, initProseMirrorDoc } from '@tiptap/y-tiptap'
import { exampleSetup } from 'prosemirror-example-setup'
import { keymap } from 'prosemirror-keymap'
..
const type = ydocument.get('prosemirror', Y.XmlFragment)
const { doc, mapping } = initProseMirrorDoc(type, schema)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc,
schema,
plugins: [
ySyncPlugin(type, { mapping }),
yCursorPlugin(provider.awareness),
yUndoPlugin(),
keymap({
'Mod-z': undo,
'Mod-y': redo,
'Mod-Shift-z': redo
})
].concat(exampleSetup({ schema }))
})
})
`
#### Remote Cursors
The shared cursors depend on the Awareness instance that is exported by most providers. The Awareness protocol handles non-permanent data like the number of users, their user names, their cursor location, and their colors. You can change the name and color of the user like this:
`js`
example.binding.awareness.setLocalStateField('user', { color: '#008833', name: 'My real name' })
In order to render cursor information you need to embed custom CSS for the user icon. This is a template that you can use for styling cursor information.
`css`
/ this is a rough fix for the first cursor position when the first paragraph is empty /
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
margin-top: 16px;
}
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
margin-top: 16px
}
/ This gives the remote user caret. The colors are automatically overwritten/
.ProseMirror-yjs-cursor {
position: relative;
margin-left: -1px;
margin-right: -1px;
border-left: 1px solid black;
border-right: 1px solid black;
border-color: orange;
word-break: normal;
pointer-events: none;
}
/ This renders the username above the caret /
.ProseMirror-yjs-cursor > div {
position: absolute;
top: -1.05em;
left: -1px;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
white-space: nowrap;
}
You can also overwrite the default Widget dom by specifying a cursor builder in the yCursorPlugin
`jsborder-color: ${user.color}
/**
* This function receives the remote users "user" awareness state.
*/
export const myCursorBuilder = user => {
const cursor = document.createElement('span')
cursor.classList.add('ProseMirror-yjs-cursor')
cursor.setAttribute('style', )background-color: ${user.color}
const userDiv = document.createElement('div')
userDiv.setAttribute('style', )
userDiv.insertBefore(document.createTextNode(user.name), null)
cursor.insertBefore(userDiv, null)
return cursor
}
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
schema,
plugins: [
ySyncPlugin(type),
yCursorPlugin(provider.awareness, { cursorBuilder: myCursorBuilder }),
yUndoPlugin(),
keymap({
'Mod-z': undo,
'Mod-y': redo,
'Mod-Shift-z': redo
})
].concat(exampleSetup({ schema }))
})
})
`
#### Utilities
The package includes a number of utility methods for converting back and forth between
a Y.Doc and Prosemirror compatible data structures. These can be useful for persisting
to a datastore or for importing existing documents.
> _Note_: Serializing and deserializing to JSON will not store collaboration history
> steps and as such should not be used as the primary storage. You will still need
> to store the Y.Doc binary update format.
`js
import { prosemirrorToYDoc } from '@tiptap/y-tiptap'
// Pass JSON previously output from Prosemirror
const doc = Node.fromJSON(schema, {
type: "doc",
content: [...]
})
const ydoc = prosemirrorToYDoc(doc)
`
Because JSON is a common usecase there is an equivalent method that skips the need
to create a Prosemirror Node.
`js
import { prosemirrorJSONToYDoc } from '@tiptap/y-tiptap'
// Pass JSON previously output from Prosemirror
const ydoc = prosemirrorJSONToYDoc(schema, {
type: "doc",
content: [...]
})
`
`js
import { yDocToProsemirror } from '@tiptap/y-tiptap'
// apply binary updates from elsewhere
const ydoc = new Y.Doc()
ydoc.applyUpdate(update)
const node = yDocToProsemirror(schema, ydoc)
`
Because JSON is a common usecase there is an equivalent method that outputs JSON
directly, this method does not require the Prosemirror schema.
`js
import { yDocToProsemirrorJSON } from '@tiptap/y-tiptap'
// apply binary updates from elsewhere
const ydoc = new Y.Doc()
ydoc.applyUpdate(update)
const node = yDocToProsemirrorJSON(ydoc)
`
The package exports undo and redo commands which can be used in place of
prosemirror-history by mapping the
mod-Z/Y keys - see ProseMirror
and Tiptap
examples.
Undo and redo are be scoped to the local client, so one peer won't undo another's
changes. See Y.UndoManager for more details.
Just like prosemirror-history, you can set a transaction's addToHistory meta property
to false to prevent that transaction from being rolled back by undo. This can be helpful for programmatic
document changes that aren't initiated by the user.
`js``
tr.setMeta("addToHistory", false);
The MIT License © Kevin Jahns, Tiptap GmbH