Skip to content

Commit

Permalink
Make sure that plugins are only called once for each change - fixes #121
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Aug 24, 2022
1 parent 13028bb commit 5f55313
Show file tree
Hide file tree
Showing 11 changed files with 3,467 additions and 1,985 deletions.
3,982 changes: 2,458 additions & 1,524 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@
"lib0": "^0.2.42"
},
"peerDependencies": {
"yjs": "^13.5.38",
"y-protocols": "^1.0.1",
"prosemirror-model": "^1.7.1",
"prosemirror-state": "^1.2.3",
"prosemirror-view": "^1.9.10"
"prosemirror-view": "^1.9.10",
"y-protocols": "^1.0.1",
"yjs": "^13.5.38"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
Expand All @@ -74,7 +74,7 @@
"prosemirror-state": "^1.3.4",
"prosemirror-view": "^1.22.0",
"rollup": "^2.59.0",
"standard": "^12.0.1",
"standard": "^17.0.0",
"typescript": "^3.9.10",
"y-protocols": "^1.0.5",
"y-webrtc": "^10.2.0",
Expand Down
4 changes: 2 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default [{
}],
external: id => /^(lib0|y-protocols|prosemirror|yjs)/.test(id)
}, {
input: './test/index.js',
input: './tests/index.js',
output: {
name: 'test',
file: 'dist/test.js',
Expand Down Expand Up @@ -53,7 +53,7 @@ export default [{
commonjs()
]
}, {
input: './test/index.node.js',
input: './tests/index.node.js',
output: {
name: 'test',
file: 'dist/test.cjs',
Expand Down
232 changes: 157 additions & 75 deletions src/plugins/cursor-plugin.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@

import * as Y from 'yjs'
import { Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
import { Plugin } from 'prosemirror-state' // eslint-disable-line
import { Awareness } from 'y-protocols/awareness' // eslint-disable-line
import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, setMeta } from '../lib.js'
import { Decoration, DecorationSet } from "prosemirror-view"; // eslint-disable-line
import { Plugin } from "prosemirror-state"; // eslint-disable-line
import { Awareness } from "y-protocols/awareness"; // eslint-disable-line
import {
absolutePositionToRelativePosition,
relativePositionToAbsolutePosition,
setMeta
} from '../lib.js'
import { yCursorPluginKey, ySyncPluginKey } from './keys.js'

import * as math from 'lib0/math'
Expand All @@ -14,7 +17,7 @@ import * as math from 'lib0/math'
* @param {any} user user data
* @return {HTMLElement}
*/
export const defaultCursorBuilder = user => {
export const defaultCursorBuilder = (user) => {
const cursor = document.createElement('span')
cursor.classList.add('ProseMirror-yjs-cursor')
cursor.setAttribute('style', `border-color: ${user.color}`)
Expand All @@ -35,10 +38,10 @@ export const defaultCursorBuilder = user => {
* @param {any} user user data
* @return {import('prosemirror-view').DecorationAttrs}
*/
export const defaultSelectionBuilder = user => {
export const defaultSelectionBuilder = (user) => {
return {
style: `background-color: ${user.color}70`,
class: `ProseMirror-yjs-selection`
class: 'ProseMirror-yjs-selection'
}
}

Expand All @@ -47,13 +50,23 @@ const rxValidColor = /^#[0-9a-fA-F]{6}$/
/**
* @param {any} state
* @param {Awareness} awareness
* @param {function({ name: string, color: string }):Element} createCursor
* @param {function({ name: string, color: string }):import('prosemirror-view').DecorationAttrs} createSelection
* @return {any} DecorationSet
*/
export const createDecorations = (state, awareness, createCursor, createSelection) => {
export const createDecorations = (
state,
awareness,
createCursor,
createSelection
) => {
const ystate = ySyncPluginKey.getState(state)
const y = ystate.doc
const decorations = []
if (ystate.snapshot != null || ystate.prevSnapshot != null || ystate.binding === null) {
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 @@ -72,16 +85,36 @@ export const createDecorations = (state, awareness, createCursor, createSelectio
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(aw.cursor.anchor),
ystate.binding.mapping
)
let head = relativePositionToAbsolutePosition(
y,
ystate.type,
Y.createRelativePositionFromJSON(aw.cursor.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 }))
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, createSelection(user), { inclusiveEnd: true, inclusiveStart: false }))
decorations.push(
Decoration.inline(from, to, createSelection(user), {
inclusiveEnd: true,
inclusiveStart: false
})
)
}
}
})
Expand All @@ -101,71 +134,120 @@ export const createDecorations = (state, awareness, createCursor, createSelectio
* @param {string} [cursorStateField] By default all editor bindings use the awareness 'cursor' field to propagate cursor information.
* @return {any}
*/
export const yCursorPlugin = (awareness, { cursorBuilder = defaultCursorBuilder, selectionBuilder = defaultSelectionBuilder, getSelection = state => state.selection } = {}, cursorStateField = 'cursor') => new Plugin({
key: yCursorPluginKey,
state: {
init (_, state) {
return createDecorations(state, awareness, cursorBuilder, selectionBuilder)
},
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, selectionBuilder)
export const yCursorPlugin = (
awareness,
{
cursorBuilder = defaultCursorBuilder,
selectionBuilder = defaultSelectionBuilder,
getSelection = (state) => state.selection
} = {},
cursorStateField = 'cursor'
) =>
new Plugin({
key: yCursorPluginKey,
state: {
init (_, state) {
return createDecorations(
state,
awareness,
cursorBuilder,
selectionBuilder
)
},
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,
selectionBuilder
)
}
return prevState.map(tr.mapping, tr.doc)
}
return prevState.map(tr.mapping, tr.doc)
}
},
props: {
decorations: state => {
return yCursorPluginKey.getState(state)
}
},
view: view => {
const awarenessListener = () => {
// @ts-ignore
if (view.docView) {
setMeta(view, yCursorPluginKey, { awarenessUpdated: true })
},
props: {
decorations: (state) => {
return yCursorPluginKey.getState(state)
}
}
const updateCursorInfo = () => {
const ystate = ySyncPluginKey.getState(view.state)
// @note We make implicit checks when checking for the cursor property
const current = awareness.getLocalState() || {}
if (ystate.binding == null) {
return
},
view: (view) => {
const awarenessListener = () => {
// @ts-ignore
if (view.docView) {
setMeta(view, yCursorPluginKey, { awarenessUpdated: true })
}
}
if (view.hasFocus()) {
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(cursorStateField, {
anchor, head
})
const updateCursorInfo = () => {
const ystate = ySyncPluginKey.getState(view.state)
// @note We make implicit checks when checking for the cursor property
const current = awareness.getLocalState() || {}
if (ystate.binding == null) {
return
}
if (view.hasFocus()) {
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(cursorStateField, {
anchor,
head
})
}
} else if (
current.cursor != null &&
relativePositionToAbsolutePosition(
ystate.doc,
ystate.type,
Y.createRelativePositionFromJSON(current.cursor.anchor),
ystate.binding.mapping
) !== null
) {
// delete cursor information if current cursor information is owned by this editor binding
awareness.setLocalStateField(cursorStateField, null)
}
} else if (current.cursor != null && relativePositionToAbsolutePosition(ystate.doc, ystate.type, Y.createRelativePositionFromJSON(current.cursor.anchor), ystate.binding.mapping) !== null) {
// delete cursor information if current cursor information is owned by this editor binding
awareness.setLocalStateField(cursorStateField, null)
}
}
awareness.on('change', awarenessListener)
view.dom.addEventListener('focusin', updateCursorInfo)
view.dom.addEventListener('focusout', updateCursorInfo)
return {
update: updateCursorInfo,
destroy: () => {
view.dom.removeEventListener('focusin', updateCursorInfo)
view.dom.removeEventListener('focusout', updateCursorInfo)
awareness.off('change', awarenessListener)
awareness.setLocalStateField(cursorStateField, null)
awareness.on('change', awarenessListener)
view.dom.addEventListener('focusin', updateCursorInfo)
view.dom.addEventListener('focusout', updateCursorInfo)
return {
update: updateCursorInfo,
destroy: () => {
view.dom.removeEventListener('focusin', updateCursorInfo)
view.dom.removeEventListener('focusout', updateCursorInfo)
awareness.off('change', awarenessListener)
awareness.setLocalStateField(cursorStateField, null)
}
}
}
}
})
})
Loading

0 comments on commit 5f55313

Please sign in to comment.