Skip to content
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

Cusrsors with multiple prose mirror views in one yjs doc and awearness #19

Closed
wants to merge 8 commits into from
81 changes: 65 additions & 16 deletions src/plugins/cursor-plugin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import * as Y from 'yjs'
import { Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
import { Plugin, PluginKey } from 'prosemirror-state' // eslint-disable-line
Expand Down Expand Up @@ -33,14 +32,17 @@ export const defaultCursorBuilder = user => {
}

/**
* @param {string} cursorId
* @param {any} state
* @param {Awareness} awareness
* @param {Function} createCursor
* @return {any} DecorationSet
*/
export const createDecorations = (state, awareness, createCursor) => {
export const createDecorations = (cursorId, state, awareness, createCursor) => {
const ystate = ySyncPluginKey.getState(state)
const y = ystate.doc
const decorations = []

if (ystate.snapshot != null || ystate.prevSnapshot != null || ystate.binding === null) {
// do not render cursors while snapshot is active
return DecorationSet.create(state.doc, [])
Expand All @@ -49,24 +51,32 @@ export const createDecorations = (state, awareness, createCursor) => {
if (clientId === y.clientID) {
return
}
if (aw.cursor != null) {
const cursorInfo = aw.cursor
if (cursorInfo != null && cursorInfo.cursorId === cursorId) {
const user = aw.user || {}
if (user.color == null) {
user.color = '#ffa500'
}
if (user.name == null) {
user.name = `User: ${clientId}`
}
let anchor = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.anchor), ystate.binding.mapping)
let head = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.head), ystate.binding.mapping)
let anchor = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(cursorInfo.anchor), ystate.binding.mapping)
let head = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(cursorInfo.head), ystate.binding.mapping)
if (anchor !== null && head !== null) {
const maxsize = math.max(state.doc.content.size - 1, 0)
anchor = math.min(anchor, maxsize)
head = math.min(head, maxsize)
decorations.push(Decoration.widget(head, () => createCursor(user), { key: clientId + '', side: 10 }))
const from = math.min(anchor, head)
const to = math.max(anchor, head)
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}70` }, { inclusiveEnd: true, inclusiveStart: false }))
decorations.push(
Decoration.inline(from, to, {
style: `background-color: ${user.color}70`
}, {
inclusiveEnd: true,
inclusiveStart: false
})
)
}
}
})
Expand All @@ -82,19 +92,27 @@ export const createDecorations = (state, awareness, createCursor) => {
* @param {object} [opts]
* @param {function(any):HTMLElement} [opts.cursorBuilder]
* @param {function(any):any} [opts.getSelection]
* @param {any} [opts.cursorId]
* @return {any}
*/
export const yCursorPlugin = (awareness, { cursorBuilder = defaultCursorBuilder, getSelection = state => state.selection } = {}) => new Plugin({
export const yCursorPlugin = (
awareness,
{
cursorBuilder = defaultCursorBuilder,
getSelection = state => state.selection,
cursorId = null
} = {}
) => new Plugin({
key: yCursorPluginKey,
state: {
init (_, state) {
return createDecorations(state, awareness, cursorBuilder)
return createDecorations(cursorId, state, awareness, cursorBuilder)
},
apply (tr, prevState, oldState, newState) {
const ystate = ySyncPluginKey.getState(newState)
const yCursorState = tr.getMeta(yCursorPluginKey)
if ((ystate && ystate.isChangeOrigin) || (yCursorState && yCursorState.awarenessUpdated)) {
return createDecorations(newState, awareness, cursorBuilder)
return createDecorations(cursorId, newState, awareness, cursorBuilder)
}
return prevState.map(tr.mapping, tr.doc)
}
Expand All @@ -113,25 +131,48 @@ export const yCursorPlugin = (awareness, { cursorBuilder = defaultCursorBuilder,
}
const updateCursorInfo = () => {
const ystate = ySyncPluginKey.getState(view.state)

// @note We make implicit checks when checking for the cursor property
const current = awareness.getLocalState() || {}
const currentCursorInfo = current.cursor

if (view.hasFocus() && ystate.binding !== null) {
let shouldUpdateCursor = currentCursorInfo == null
const updateCursorInfo = {}

if (shouldUpdateCursor || currentCursorInfo.cursorId !== cursorId) {
updateCursorInfo.cursorId = cursorId
shouldUpdateCursor = true
}

const selection = getSelection(view.state)

/**
* @type {Y.RelativePosition}
*/
const anchor = absolutePositionToRelativePosition(selection.anchor, ystate.type, ystate.binding.mapping)

/**
* @type {Y.RelativePosition}
*/
const head = absolutePositionToRelativePosition(selection.head, ystate.type, ystate.binding.mapping)
if (current.cursor == null || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.anchor), anchor) || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.head), head)) {
awareness.setLocalStateField('cursor', {
anchor, head
})

if (shouldUpdateCursor ||
!Y.compareRelativePositions(Y.createRelativePositionFromJSON(currentCursorInfo.head), head) ||
!Y.compareRelativePositions(Y.createRelativePositionFromJSON(currentCursorInfo.anchor), anchor)
) {
updateCursorInfo.anchor = anchor
updateCursorInfo.head = head
shouldUpdateCursor = true
}

if (shouldUpdateCursor) {
awareness.setLocalStateField('cursor', updateCursorInfo)
}
} else if (currentCursorInfo != null) {
if (currentCursorInfo.cursorId === cursorId) {
awareness.setLocalStateField('cursor', null)
}
} else if (current.cursor != null) {
awareness.setLocalStateField('cursor', null)
}
}
awareness.on('change', awarenessListener)
Expand All @@ -141,7 +182,15 @@ export const yCursorPlugin = (awareness, { cursorBuilder = defaultCursorBuilder,
update: updateCursorInfo,
destroy: () => {
awareness.off('change', awarenessListener)
awareness.setLocalStateField('cursor', null)
view.dom.removeEventListener('focusin', updateCursorInfo)
view.dom.removeEventListener('focusout', updateCursorInfo)

const current = awareness.getLocalState() || {}
const currentCursorInfo = current.cursor

if (currentCursorInfo.cursorId === cursorId) {
awareness.setLocalStateField('cursor', null)
}
}
}
}
Expand Down