Collaborative editing for ProseMirror using Firebase's Realtime Database. Features timestamped and attributed changes, version checkpointing, selection tracking, and more.
Assumes familiarity with ProseMirror and Firebase.
Call new FirebaseEditor
with an object containing:
firebaseRef
firebase.database.ReferencestateConfig
config (must contain schema
, doc
will be overwritten)view
Function that creates an editor view and returns it. Recieves an object containing:
stateConfig
config to be used for creating the state
for new Editor View
(or new MenuBarEditorView
)
updateCollab
Function that should be called in dispatchTransaction
with the transaction
and the newState
:
dispatchTransaction(transaction) {
let newState = view.state.apply(transaction)
view.updateState(newState)
updateCollab(transaction, newState)
},
selections
Object with keys being clientId
s and values being selections (useful for showing cursors and selections of other clients)
clientId
(optional, used to attribute changes and distinguish them from those of other clients, defaults to a unique 120-bit string identifier)Returns a Promise that resolves when the editor is ready (when existing content has been loaded and an editor view has been created). The resolved value is an object containing the properties and methods of the editor:
view
(see above)selections
(see above)destroy
Function that removes all database listeners that were added, removes the client's selection from the database, and calls editorView.destroy
new FirebaseEditor({
firebaseRef: /*database.Reference*/,
stateConfig: {
schema: /*Schema*/,
},
view({ stateConfig, updateCollab }) {
let view = new EditorView(/*dom.Node*/, {
state: EditorState.create(stateConfig),
dispatchTransaction(transaction) {
let newState = view.state.apply(transaction)
view.updateState(newState)
updateCollab(transaction, newState)
},
})
return view
},
})
function stringToColor(string, alpha = 1) {
let hue = string.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0) % 360
return `hsla(${hue}, 100%, 50%, ${alpha})`
}
new FirebaseEditor({
/*...*/
view({ stateConfig, updateCollab, selections }) {
let view = new EditorView(/*dom.Node*/, {
/*...*/
decorations({ doc }) {
return DecorationSet.create(doc, Object.entries(selections).map(
function ([ clientID, { from, to } ]) {
if (from === to) {
let elem = document.createElement('span')
elem.style.borderLeft = `1px solid ${stringToColor(clientID)}`
return Decoration.widget(from, elem)
} else {
return Decoration.inline(from, to, {
style: `background-color: ${stringToColor(clientID, 0.2)};`,
})
}
}
))
},
})
return view
},
})
new FirebaseEditor({
/*...*/
}).then(({ view, selections, destroy }) => {
console.log('editor ready')
}).catch(console.error)