improved granularity of prosemirror binding
This commit is contained in:
parent
c9ea3a412e
commit
582095e5a3
@ -2,16 +2,18 @@
|
|||||||
* @module bindings/prosemirror
|
* @module bindings/prosemirror
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BindMapping } from '../utils/BindMapping.js'
|
|
||||||
import { YText } from '../types/YText.js' // eslint-disable-line
|
import { YText } from '../types/YText.js' // eslint-disable-line
|
||||||
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
|
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
|
||||||
import { createMutex } from '../lib/mutex.js'
|
import { createMutex } from '../lib/mutex.js'
|
||||||
import * as PModel from 'prosemirror-model'
|
import * as PModel from 'prosemirror-model'
|
||||||
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
|
import { EditorView, Decoration, DecorationSet } from 'prosemirror-view' // eslint-disable-line
|
||||||
import { Plugin, PluginKey, EditorState } from 'prosemirror-state' // eslint-disable-line
|
import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state' // eslint-disable-line
|
||||||
|
import * as math from '../lib/math.js'
|
||||||
|
import * as object from '../lib/object.js'
|
||||||
|
import * as YPos from '../utils/relativePosition.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {BindMapping<YText | YXmlElement, PModel.Node>} ProsemirrorMapping
|
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -34,6 +36,7 @@ export const prosemirrorPlugin = yXmlFragment => {
|
|||||||
y: yXmlFragment._y,
|
y: yXmlFragment._y,
|
||||||
binding: null
|
binding: null
|
||||||
}
|
}
|
||||||
|
let changedInitialContent = false
|
||||||
const plugin = new Plugin({
|
const plugin = new Plugin({
|
||||||
key: prosemirrorPluginKey,
|
key: prosemirrorPluginKey,
|
||||||
state: {
|
state: {
|
||||||
@ -41,6 +44,11 @@ export const prosemirrorPlugin = yXmlFragment => {
|
|||||||
return pluginState
|
return pluginState
|
||||||
},
|
},
|
||||||
apply: (tr, pluginState) => {
|
apply: (tr, pluginState) => {
|
||||||
|
// update Yjs state when apply is called. We need to do this here to compute the correct cursor decorations with the cursor plugin
|
||||||
|
if (pluginState.binding !== null && (changedInitialContent || tr.doc.content.size > 4)) {
|
||||||
|
changedInitialContent = true
|
||||||
|
pluginState.binding._prosemirrorChanged(tr.doc)
|
||||||
|
}
|
||||||
return pluginState
|
return pluginState
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -49,7 +57,10 @@ export const prosemirrorPlugin = yXmlFragment => {
|
|||||||
pluginState.binding = binding
|
pluginState.binding = binding
|
||||||
return {
|
return {
|
||||||
update: () => {
|
update: () => {
|
||||||
binding._prosemirrorChanged()
|
if (changedInitialContent || view.state.doc.content.size > 4) {
|
||||||
|
changedInitialContent = true
|
||||||
|
binding._prosemirrorChanged(view.state.doc)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
binding.destroy()
|
binding.destroy()
|
||||||
@ -61,7 +72,7 @@ export const prosemirrorPlugin = yXmlFragment => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The unique prosemirror plugin key for cursorPlugin.
|
* The unique prosemirror plugin key for cursorPlugin.type
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -77,44 +88,61 @@ export const cursorPlugin = new Plugin({
|
|||||||
key: cursorPluginKey,
|
key: cursorPluginKey,
|
||||||
props: {
|
props: {
|
||||||
decorations: state => {
|
decorations: state => {
|
||||||
const y = prosemirrorPluginKey.getState(state).y
|
const ystate = prosemirrorPluginKey.getState(state)
|
||||||
|
const y = ystate.y
|
||||||
const awareness = y.getAwarenessInfo()
|
const awareness = y.getAwarenessInfo()
|
||||||
const decorations = []
|
const decorations = []
|
||||||
awareness.forEach((state, userID) => {
|
awareness.forEach((aw, userID) => {
|
||||||
if (state.cursor != null) {
|
if (aw.cursor != null) {
|
||||||
const username = `User: ${userID}`
|
const username = `User: ${userID}`
|
||||||
decorations.push(Decoration.widget(state.cursor.from, () => {
|
let anchor = relativePositionToAbsolutePosition(ystate.type, aw.cursor.anchor || null, ystate.binding.mapping)
|
||||||
const cursor = document.createElement('span')
|
let head = relativePositionToAbsolutePosition(ystate.type, aw.cursor.head || null, ystate.binding.mapping)
|
||||||
cursor.classList.add('ProseMirror-yjs-cursor')
|
if (anchor !== null && head !== null) {
|
||||||
const user = document.createElement('div')
|
let maxsize = math.max(state.doc.content.size - 1, 0)
|
||||||
user.insertBefore(document.createTextNode(username), null)
|
anchor = math.min(anchor, maxsize)
|
||||||
cursor.insertBefore(user, null)
|
head = math.min(head, maxsize)
|
||||||
return cursor
|
decorations.push(Decoration.widget(head, () => {
|
||||||
}, { key: username }))
|
const cursor = document.createElement('span')
|
||||||
decorations.push(Decoration.inline(state.cursor.from, state.cursor.to, { style: 'background-color: #ffa50070' }))
|
cursor.classList.add('ProseMirror-yjs-cursor')
|
||||||
|
const user = document.createElement('div')
|
||||||
|
user.insertBefore(document.createTextNode(username), null)
|
||||||
|
cursor.insertBefore(user, null)
|
||||||
|
return cursor
|
||||||
|
}, { key: username }))
|
||||||
|
const from = math.min(anchor, head)
|
||||||
|
const to = math.max(anchor, head)
|
||||||
|
decorations.push(Decoration.inline(from, to, { style: 'background-color: #ffa50070' }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return DecorationSet.create(state.doc, decorations)
|
return DecorationSet.create(state.doc, decorations)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
view: view => {
|
view: view => {
|
||||||
const y = prosemirrorPluginKey.getState(view.state).y
|
const ystate = prosemirrorPluginKey.getState(view.state)
|
||||||
|
const y = ystate.y
|
||||||
const awarenessListener = () => {
|
const awarenessListener = () => {
|
||||||
view.updateState(view.state)
|
view.updateState(view.state)
|
||||||
}
|
}
|
||||||
y.on('awareness', awarenessListener)
|
const updateCursorInfo = () => {
|
||||||
return {
|
const current = y.getLocalAwarenessInfo()
|
||||||
update: () => {
|
if (view.hasFocus()) {
|
||||||
const y = prosemirrorPluginKey.getState(view.state).y
|
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
|
||||||
const from = view.state.selection.from
|
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
|
||||||
const to = view.state.selection.to
|
if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
|
||||||
const current = y.getLocalAwarenessInfo()
|
|
||||||
if (current.cursor == null || current.cursor.to !== to || current.cursor.from !== from) {
|
|
||||||
y.setAwarenessField('cursor', {
|
y.setAwarenessField('cursor', {
|
||||||
from, to
|
anchor, head
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
} else if (current.cursor !== null) {
|
||||||
|
y.setAwarenessField('cursor', null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
y.on('awareness', awarenessListener)
|
||||||
|
view.dom.addEventListener('focusin', updateCursorInfo)
|
||||||
|
view.dom.addEventListener('focusout', updateCursorInfo)
|
||||||
|
return {
|
||||||
|
update: updateCursorInfo,
|
||||||
destroy: () => {
|
destroy: () => {
|
||||||
const y = prosemirrorPluginKey.getState(view.state).y
|
const y = prosemirrorPluginKey.getState(view.state).y
|
||||||
y.setAwarenessField('cursor', null)
|
y.setAwarenessField('cursor', null)
|
||||||
@ -124,6 +152,115 @@ export const cursorPlugin = new Plugin({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms a Prosemirror based absolute position to a Yjs based relative position.
|
||||||
|
*
|
||||||
|
* @param {number} pos
|
||||||
|
* @param {YXmlFragment} type
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
* @return {any} relative position
|
||||||
|
*/
|
||||||
|
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
|
||||||
|
if (pos === 0) {
|
||||||
|
return YPos.getRelativePosition(type, 0)
|
||||||
|
}
|
||||||
|
let n = type._first
|
||||||
|
if (n !== null) {
|
||||||
|
while (type !== n) {
|
||||||
|
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize
|
||||||
|
if (n.constructor === YText) {
|
||||||
|
if (n.length >= pos) {
|
||||||
|
return YPos.getRelativePosition(n, pos)
|
||||||
|
} else {
|
||||||
|
pos -= n.length
|
||||||
|
}
|
||||||
|
if (n._next !== null) {
|
||||||
|
n = n._next
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
n = n._parent
|
||||||
|
pos--
|
||||||
|
} while (n._next === null && n !== type)
|
||||||
|
if (n !== type) {
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (n._first !== null && pos < pNodeSize) {
|
||||||
|
n = n._first
|
||||||
|
pos--
|
||||||
|
} else {
|
||||||
|
if (pos === 1 && n.length === 0 && pNodeSize > 1) {
|
||||||
|
// edge case, should end in this paragraph
|
||||||
|
return ['endof', n._id.user, n._id.clock, null, null]
|
||||||
|
}
|
||||||
|
pos -= pNodeSize
|
||||||
|
if (n._next !== null) {
|
||||||
|
n = n._next
|
||||||
|
} else {
|
||||||
|
if (pos === 0) {
|
||||||
|
n = n._parent
|
||||||
|
return ['endof', n._id.user, n._id.clock || null, n._id.name || null, n._id.type || null]
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
n = n._parent
|
||||||
|
pos--
|
||||||
|
} while (n._next === null && n !== type)
|
||||||
|
if (n !== type) {
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos === 0 && n.constructor !== YText && n !== type) { // TODO: set to <= 0
|
||||||
|
return [n._id.user, n._id.clock]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return YPos.getRelativePosition(type, type.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {YXmlFragment} yDoc Top level type that is bound to pView
|
||||||
|
* @param {any} relPos Encoded Yjs based relative position
|
||||||
|
* @param {ProsemirrorMapping} mapping
|
||||||
|
*/
|
||||||
|
export const relativePositionToAbsolutePosition = (yDoc, relPos, mapping) => {
|
||||||
|
const decodedPos = YPos.fromRelativePosition(yDoc._y, relPos)
|
||||||
|
if (decodedPos === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let type = decodedPos.type
|
||||||
|
let pos = 0
|
||||||
|
if (type.constructor === YText) {
|
||||||
|
pos = decodedPos.offset
|
||||||
|
} else if (!type._deleted) {
|
||||||
|
let n = type._first
|
||||||
|
let i = 0
|
||||||
|
while (i < type.length && i < decodedPos.offset && n !== null) {
|
||||||
|
i++
|
||||||
|
pos += mapping.get(n).nodeSize
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
pos += 1 // increase because we go out of n
|
||||||
|
}
|
||||||
|
while (type !== yDoc) {
|
||||||
|
const parent = type._parent
|
||||||
|
if (!parent._deleted) {
|
||||||
|
pos += 1 // the start tag
|
||||||
|
let n = parent._first
|
||||||
|
// now iterate until we found type
|
||||||
|
while (n !== null) {
|
||||||
|
if (n === type) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pos += mapping.get(n).nodeSize
|
||||||
|
n = n._next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type = parent
|
||||||
|
}
|
||||||
|
return pos - 1 // we don't count the most outer tag, because it is a fragment
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binding for prosemirror.
|
* Binding for prosemirror.
|
||||||
*
|
*
|
||||||
@ -141,36 +278,47 @@ export class ProsemirrorBinding {
|
|||||||
/**
|
/**
|
||||||
* @type {ProsemirrorMapping}
|
* @type {ProsemirrorMapping}
|
||||||
*/
|
*/
|
||||||
this.mapping = new BindMapping()
|
this.mapping = new Map()
|
||||||
this._observeFunction = this._typeChanged.bind(this)
|
this._observeFunction = this._typeChanged.bind(this)
|
||||||
|
this.y = yXmlFragment._y
|
||||||
|
/**
|
||||||
|
* current selection as relative positions in the Yjs model
|
||||||
|
*/
|
||||||
|
this._relSelection = null
|
||||||
|
this.y.on('beforeTransaction', e => {
|
||||||
|
this._relSelection = {
|
||||||
|
anchor: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.anchor, yXmlFragment, this.mapping),
|
||||||
|
head: absolutePositionToRelativePosition(this.prosemirrorView.state.selection.head, yXmlFragment, this.mapping)
|
||||||
|
}
|
||||||
|
})
|
||||||
yXmlFragment.observeDeep(this._observeFunction)
|
yXmlFragment.observeDeep(this._observeFunction)
|
||||||
}
|
}
|
||||||
_typeChanged (events) {
|
_typeChanged (events, transaction) {
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
|
||||||
this.mux(() => {
|
this.mux(() => {
|
||||||
events.forEach(event => {
|
const delStruct = (_, struct) => this.mapping.delete(struct)
|
||||||
// recompute node for each parent
|
transaction.deletedStructs.forEach(struct => this.mapping.delete(struct))
|
||||||
// except main node, compute main node in the end
|
transaction.changedTypes.forEach(delStruct)
|
||||||
let target = event.target
|
transaction.changedParentTypes.forEach(delStruct)
|
||||||
if (target !== this.type) {
|
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
|
||||||
do {
|
|
||||||
if (target.constructor === YXmlElement) {
|
|
||||||
createNodeFromYElement(target, this.prosemirrorView.state.schema, this.mapping)
|
|
||||||
}
|
|
||||||
target = target._parent
|
|
||||||
} while (target._parent !== this.type)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping))
|
|
||||||
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
|
const tr = this.prosemirrorView.state.tr.replace(0, this.prosemirrorView.state.doc.content.size, new PModel.Slice(new PModel.Fragment(fragmentContent), 0, 0))
|
||||||
|
const relSel = this._relSelection
|
||||||
|
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
|
||||||
|
const anchor = relativePositionToAbsolutePosition(this.type, relSel.anchor, this.mapping)
|
||||||
|
const head = relativePositionToAbsolutePosition(this.type, relSel.head, this.mapping)
|
||||||
|
if (anchor !== null && head !== null) {
|
||||||
|
tr.setSelection(TextSelection.create(tr.doc, anchor, head))
|
||||||
|
}
|
||||||
|
}
|
||||||
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
|
this.prosemirrorView.updateState(this.prosemirrorView.state.apply(tr))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_prosemirrorChanged () {
|
_prosemirrorChanged (doc) {
|
||||||
this.mux(() => {
|
this.mux(() => {
|
||||||
updateYFragment(this.type, this.prosemirrorView.state, this.mapping)
|
updateYFragment(this.type, doc.content, this.mapping)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
destroy () {
|
destroy () {
|
||||||
@ -179,14 +327,14 @@ export class ProsemirrorBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @privateMapping
|
||||||
* @param {Y.XmlElement} el
|
* @param {YXmlElement} el
|
||||||
* @param {PModel.Schema} schema
|
* @param {PModel.Schema} schema
|
||||||
* @param {ProsemirrorMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
* @return {PModel.Node}
|
* @return {PModel.Node}
|
||||||
*/
|
*/
|
||||||
export const createNodeIfNotExists = (el, schema, mapping) => {
|
export const createNodeIfNotExists = (el, schema, mapping) => {
|
||||||
const node = mapping.getY(el)
|
const node = mapping.get(el)
|
||||||
if (node === undefined) {
|
if (node === undefined) {
|
||||||
return createNodeFromYElement(el, schema, mapping)
|
return createNodeFromYElement(el, schema, mapping)
|
||||||
}
|
}
|
||||||
@ -195,28 +343,48 @@ export const createNodeIfNotExists = (el, schema, mapping) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {Y.XmlElement} el
|
* @param {YXmlElement} el
|
||||||
* @param {PModel.Schema} schema
|
* @param {PModel.Schema} schema
|
||||||
* @param {ProsemirrorMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
* @return {PModel.Node}
|
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
|
||||||
*/
|
*/
|
||||||
export const createNodeFromYElement = (el, schema, mapping) => {
|
export const createNodeFromYElement = (el, schema, mapping) => {
|
||||||
const children = []
|
const children = []
|
||||||
el.toArray().forEach(type => {
|
el.toArray().forEach(type => {
|
||||||
if (type.constructor === YXmlElement) {
|
if (type.constructor === YXmlElement) {
|
||||||
children.push(createNodeIfNotExists(type, schema, mapping))
|
const n = createNodeIfNotExists(type, schema, mapping)
|
||||||
|
if (n !== null) {
|
||||||
|
children.push(n)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
children.concat(createTextNodesFromYText(type, schema, mapping)).forEach(textchild => children.push(textchild))
|
const ns = createTextNodesFromYText(type, schema, mapping)
|
||||||
|
if (ns !== null) {
|
||||||
|
ns.forEach(textchild => {
|
||||||
|
if (textchild !== null) {
|
||||||
|
children.push(textchild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), el.toArray().map(t => createNodeIfNotExists(t, schema, mapping)))
|
let node
|
||||||
mapping.bind(el, node)
|
try {
|
||||||
|
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(), children)
|
||||||
|
} catch (e) {
|
||||||
|
// an error occured while creating the node. This is probably a result because of a concurrent action.
|
||||||
|
// delete the node and do not push to children
|
||||||
|
el._y.transact(() => {
|
||||||
|
el._delete(el._y, true)
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
mapping.set(el, node)
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {Y.Text} text
|
* @param {YText} text
|
||||||
* @param {PModel.Schema} schema
|
* @param {PModel.Schema} schema
|
||||||
* @param {ProsemirrorMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
* @return {Array<PModel.Node>}
|
* @return {Array<PModel.Node>}
|
||||||
@ -224,16 +392,23 @@ export const createNodeFromYElement = (el, schema, mapping) => {
|
|||||||
export const createTextNodesFromYText = (text, schema, mapping) => {
|
export const createTextNodesFromYText = (text, schema, mapping) => {
|
||||||
const nodes = []
|
const nodes = []
|
||||||
const deltas = text.toDelta()
|
const deltas = text.toDelta()
|
||||||
for (let i = 0; i < deltas.length; i++) {
|
try {
|
||||||
const delta = deltas[i]
|
for (let i = 0; i < deltas.length; i++) {
|
||||||
const marks = []
|
const delta = deltas[i]
|
||||||
for (let markName in delta.attributes) {
|
const marks = []
|
||||||
marks.push(schema.mark(markName, delta.attributes[markName]))
|
for (let markName in delta.attributes) {
|
||||||
|
marks.push(schema.mark(markName, delta.attributes[markName]))
|
||||||
|
}
|
||||||
|
nodes.push(schema.text(delta.insert, marks))
|
||||||
}
|
}
|
||||||
nodes.push(schema.text(delta.insert, marks))
|
if (nodes.length > 0) {
|
||||||
}
|
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
||||||
if (nodes.length > 0) {
|
}
|
||||||
mapping.bind(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
} catch (e) {
|
||||||
|
text._y.transact(() => {
|
||||||
|
text._delete(text._y, true)
|
||||||
|
})
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
@ -256,44 +431,176 @@ export const createTypeFromNode = (node, mapping) => {
|
|||||||
for (let key in node.attrs) {
|
for (let key in node.attrs) {
|
||||||
type.setAttribute(key, node.attrs[key])
|
type.setAttribute(key, node.attrs[key])
|
||||||
}
|
}
|
||||||
type.insert(0, node.content.content.map(node => createTypeFromNode(node, mapping)))
|
const ins = []
|
||||||
|
for (let i = 0; i < node.childCount; i++) {
|
||||||
|
ins.push(createTypeFromNode(node.child(i), mapping))
|
||||||
|
}
|
||||||
|
type.insert(0, ins)
|
||||||
}
|
}
|
||||||
mapping.bind(type, node)
|
mapping.set(type, node)
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const equalYTextPText = (ytext, ptext) => {
|
||||||
|
const d = ytext.toDelta()[0]
|
||||||
|
return d.insert === ptext.text && object.keys(d.attributes || {}).length === ptext.marks.length && ptext.marks.every(mark => object.equalFlat(d.attributes[mark.type.name], mark.attrs))
|
||||||
|
}
|
||||||
|
|
||||||
|
const equalYTypePNode = (ytype, pnode) =>
|
||||||
|
ytype.constructor === YText
|
||||||
|
? equalYTextPText(ytype, pnode)
|
||||||
|
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && object.equalFlat(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, pnode.child(i))))
|
||||||
|
|
||||||
|
const computeChildEqualityFactor = (ytype, pnode, mapping) => {
|
||||||
|
const yChildren = ytype.toArray()
|
||||||
|
const pChildCnt = pnode.childCount
|
||||||
|
const yChildCnt = yChildren.length
|
||||||
|
const minCnt = math.min(yChildCnt, pChildCnt)
|
||||||
|
let left = 0
|
||||||
|
let right = 0
|
||||||
|
let foundMappedChild = false
|
||||||
|
for (; left < minCnt; left++) {
|
||||||
|
const leftY = yChildren[left]
|
||||||
|
const leftP = pnode.child(left)
|
||||||
|
if (mapping.get(leftY) === leftP) {
|
||||||
|
foundMappedChild = true// definite (good) match!
|
||||||
|
} else if (!equalYTypePNode(leftY, leftP)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (; left + right < minCnt; right++) {
|
||||||
|
const rightY = yChildren[yChildCnt - right - 1]
|
||||||
|
const rightP = pnode.child(pChildCnt - right - 1)
|
||||||
|
if (mapping.get(rightY) !== rightP) {
|
||||||
|
foundMappedChild = true
|
||||||
|
} else if (!equalYTypePNode(rightP, rightP)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
equalityFactor: left + right,
|
||||||
|
foundMappedChild
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {YXmlFragment} yDomFragment
|
* @param {YXmlFragment} yDomFragment
|
||||||
* @param {EditorState} state
|
* @param {PModel.Node} pContent
|
||||||
* @param {BindMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
*/
|
*/
|
||||||
const updateYFragment = (yDomFragment, state, mapping) => {
|
const updateYFragment = (yDomFragment, pContent, mapping) => {
|
||||||
const pChildCnt = state.doc.content.childCount
|
if (yDomFragment instanceof YXmlElement && yDomFragment.nodeName.toLowerCase() !== pContent.type.name) {
|
||||||
|
throw new Error('node name mismatch!')
|
||||||
|
}
|
||||||
|
mapping.set(yDomFragment, pContent)
|
||||||
|
// update attributes
|
||||||
|
if (yDomFragment instanceof YXmlElement) {
|
||||||
|
const yDomAttrs = yDomFragment.getAttributes()
|
||||||
|
for (let key in pContent.attrs) {
|
||||||
|
if (yDomAttrs[key] !== pContent.attrs[key]) {
|
||||||
|
yDomFragment.setAttribute(key, pContent.attrs[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let key in yDomAttrs) {
|
||||||
|
if (yDomAttrs[key] === undefined) {
|
||||||
|
yDomFragment.removeAttribute(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update children
|
||||||
|
const pChildCnt = pContent.childCount
|
||||||
const yChildren = yDomFragment.toArray()
|
const yChildren = yDomFragment.toArray()
|
||||||
const yChildCnt = yChildren.length
|
const yChildCnt = yChildren.length
|
||||||
const minCnt = pChildCnt < yChildCnt ? pChildCnt : yChildCnt
|
const minCnt = math.min(pChildCnt, yChildCnt)
|
||||||
let left = 0
|
let left = 0
|
||||||
let right = 0
|
let right = 0
|
||||||
// find number of matching elements from left
|
// find number of matching elements from left
|
||||||
for (;left < minCnt; left++) {
|
for (;left < minCnt; left++) {
|
||||||
if (state.doc.content.child(left) !== mapping.getY(yChildren[left])) {
|
const leftY = yChildren[left]
|
||||||
break
|
const leftP = pContent.child(left)
|
||||||
|
if (mapping.get(leftY) !== leftP) {
|
||||||
|
if (equalYTypePNode(leftY, leftP)) {
|
||||||
|
// update mapping
|
||||||
|
mapping.set(leftY, leftP)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// find number of matching elements from right
|
// find number of matching elements from right
|
||||||
for (;right < minCnt; right++) {
|
for (;right + left < minCnt; right++) {
|
||||||
if (state.doc.content.child(pChildCnt - right - 1) !== mapping.getY(yChildren[yChildCnt - right - 1])) {
|
const rightY = yChildren[yChildCnt - right - 1]
|
||||||
break
|
const rightP = pContent.child(pChildCnt - right - 1)
|
||||||
|
if (mapping.get(rightY) !== rightP) {
|
||||||
|
if (equalYTypePNode(rightY, rightP)) {
|
||||||
|
// update mapping
|
||||||
|
mapping.set(rightY, rightP)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (left + right > pChildCnt) {
|
|
||||||
// nothing changed
|
|
||||||
return
|
|
||||||
}
|
|
||||||
yDomFragment._y.transact(() => {
|
yDomFragment._y.transact(() => {
|
||||||
// now update y to match editor state
|
// try to compare and update
|
||||||
yDomFragment.delete(left, yChildCnt - left - right)
|
while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
|
||||||
yDomFragment.insert(left, state.doc.content.content.slice(left, pChildCnt - right).map(node => createTypeFromNode(node, mapping)))
|
const leftY = yChildren[left]
|
||||||
|
const leftP = pContent.child(left)
|
||||||
|
const rightY = yChildren[yChildCnt - right - 1]
|
||||||
|
const rightP = pContent.child(pChildCnt - right - 1)
|
||||||
|
if (leftY.constructor === YText && leftP.isText) {
|
||||||
|
if (!equalYTextPText(leftY, leftP)) {
|
||||||
|
yDomFragment.delete(left, 1)
|
||||||
|
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||||
|
}
|
||||||
|
left += 1
|
||||||
|
} else {
|
||||||
|
let updateLeft = matchNodeName(leftY, leftP)
|
||||||
|
let updateRight = matchNodeName(rightY, rightP)
|
||||||
|
if (updateLeft && updateRight) {
|
||||||
|
// decide which which element to update
|
||||||
|
const equalityLeft = computeChildEqualityFactor(leftY, leftP, mapping)
|
||||||
|
const equalityRight = computeChildEqualityFactor(rightY, rightP, mapping)
|
||||||
|
if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) {
|
||||||
|
updateRight = false
|
||||||
|
} else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) {
|
||||||
|
updateLeft = false
|
||||||
|
} else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) {
|
||||||
|
updateLeft = false
|
||||||
|
} else {
|
||||||
|
updateRight = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateLeft) {
|
||||||
|
updateYFragment(leftY, leftP, mapping)
|
||||||
|
left += 1
|
||||||
|
} else if (updateRight) {
|
||||||
|
updateYFragment(rightY, rightP, mapping)
|
||||||
|
right += 1
|
||||||
|
} else {
|
||||||
|
yDomFragment.delete(left, 1)
|
||||||
|
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||||
|
left += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const yDelLen = yChildCnt - left - right
|
||||||
|
if (yDelLen > 0) {
|
||||||
|
yDomFragment.delete(left, yDelLen)
|
||||||
|
}
|
||||||
|
if (left + right < pChildCnt) {
|
||||||
|
const ins = []
|
||||||
|
for (let i = left; i < pChildCnt - right; i++) {
|
||||||
|
ins.push(createTypeFromNode(pContent.child(i), mapping))
|
||||||
|
}
|
||||||
|
yDomFragment.insert(left, ins)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {YXmlElement} yElement
|
||||||
|
* @param {any} pNode Prosemirror Node
|
||||||
|
*/
|
||||||
|
const matchNodeName = (yElement, pNode) => yElement.nodeName === pNode.type.name.toUpperCase()
|
||||||
|
@ -16,6 +16,13 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.ProseMirror img { max-width: 100px }
|
.ProseMirror img { max-width: 100px }
|
||||||
|
/* 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
|
||||||
|
}
|
||||||
.ProseMirror-yjs-cursor {
|
.ProseMirror-yjs-cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-left: black;
|
border-left: black;
|
||||||
|
@ -58,8 +58,6 @@ export const until = (timeout, check) => createPromise((resolve, reject) => {
|
|||||||
|
|
||||||
export const error = description => new Error(description)
|
export const error = description => new Error(description)
|
||||||
|
|
||||||
export const max = (a, b) => a > b ? a : b
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} t Time to wait
|
* @param {number} t Time to wait
|
||||||
* @return {Promise} Promise that is resolved after t ms
|
* @return {Promise} Promise that is resolved after t ms
|
||||||
|
24
lib/math.js
24
lib/math.js
@ -2,3 +2,27 @@
|
|||||||
* @module math
|
* @module math
|
||||||
*/
|
*/
|
||||||
export const floor = Math.floor
|
export const floor = Math.floor
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @return {number} The sum of a and b
|
||||||
|
*/
|
||||||
|
export const add = (a, b) => a + b
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @return {number} The smaller element of a and b
|
||||||
|
*/
|
||||||
|
export const min = (a, b) => a < b ? a : b
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @return {number} The bigger element of a and b
|
||||||
|
*/
|
||||||
|
export const max = (a, b) => a > b ? a : b
|
||||||
|
14
lib/object.js
Normal file
14
lib/object.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
export const create = Object.create(null)
|
||||||
|
|
||||||
|
export const keys = Object.keys
|
||||||
|
|
||||||
|
export const equalFlat = (a, b) => {
|
||||||
|
const keys = Object.keys(a)
|
||||||
|
let eq = keys.length === Object.keys(b).length
|
||||||
|
for (let i = 0; i < keys.length && eq; i++) {
|
||||||
|
const key = keys[i]
|
||||||
|
eq = a[key] === b[key]
|
||||||
|
}
|
||||||
|
return eq
|
||||||
|
}
|
@ -7,9 +7,9 @@
|
|||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run lint",
|
"test": "npm run lint",
|
||||||
"build": "rm -rf build examples/build && rollup -c",
|
"build": "rm -rf build examples/build && PRODUCTION=1 rollup -c",
|
||||||
"watch": "rollup -wc",
|
"watch": "rollup -wc",
|
||||||
"debug": "concurrently 'rollup -wc' 'cutest-serve build/y.test.js -o'",
|
"debug": "concurrently 'npm run watch' 'cutest-serve build/y.test.js -o'",
|
||||||
"lint": "standard **/*.js",
|
"lint": "standard **/*.js",
|
||||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||||
"serve-docs": "npm run docs && serve ./docs/",
|
"serve-docs": "npm run docs && serve ./docs/",
|
||||||
|
@ -10,7 +10,7 @@ import * as bc from '../../lib/broadcastchannel.js'
|
|||||||
const messageSync = 0
|
const messageSync = 0
|
||||||
const messageAwareness = 1
|
const messageAwareness = 1
|
||||||
|
|
||||||
const reconnectTimeout = 100
|
const reconnectTimeout = 3000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {WebsocketsSharedDocument} doc
|
* @param {WebsocketsSharedDocument} doc
|
||||||
|
@ -32,7 +32,7 @@ const afterTransaction = (doc, transaction) => {
|
|||||||
|
|
||||||
class WSSharedDoc extends Y.Y {
|
class WSSharedDoc extends Y.Y {
|
||||||
constructor () {
|
constructor () {
|
||||||
super()
|
super({ gc: true })
|
||||||
this.mux = Y.createMutex()
|
this.mux = Y.createMutex()
|
||||||
/**
|
/**
|
||||||
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
||||||
|
@ -262,7 +262,7 @@ export const getRoomMetas = t => {
|
|||||||
result.push({
|
result.push({
|
||||||
room: metakey.slice(5),
|
room: metakey.slice(5),
|
||||||
rsid,
|
rsid,
|
||||||
offset: keys.reduce((cur, key) => globals.max(decodeHUKey(key).offset, cur), offset)
|
offset: keys.reduce((cur, key) => math.max(decodeHUKey(key).offset, cur), offset)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
).then(() => globals.presolve(result))
|
).then(() => globals.presolve(result))
|
||||||
|
@ -3,6 +3,9 @@ import commonjs from 'rollup-plugin-commonjs'
|
|||||||
import babel from 'rollup-plugin-babel'
|
import babel from 'rollup-plugin-babel'
|
||||||
import uglify from 'rollup-plugin-uglify-es'
|
import uglify from 'rollup-plugin-uglify-es'
|
||||||
|
|
||||||
|
// set this to [] to disable obfuscation
|
||||||
|
const minificationPlugins = process.env.PRODUCTION ? [babel(), uglify()] : []
|
||||||
|
|
||||||
export default [{
|
export default [{
|
||||||
input: './index.js',
|
input: './index.js',
|
||||||
output: [{
|
output: [{
|
||||||
@ -39,10 +42,8 @@ export default [{
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
module: true
|
module: true
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs()
|
||||||
babel(),
|
].concat(minificationPlugins)
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
input: './examples/dom.js',
|
input: './examples/dom.js',
|
||||||
output: {
|
output: {
|
||||||
@ -51,10 +52,7 @@ export default [{
|
|||||||
format: 'iife',
|
format: 'iife',
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: minificationPlugins
|
||||||
babel(),
|
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
input: './examples/textarea.js',
|
input: './examples/textarea.js',
|
||||||
output: {
|
output: {
|
||||||
@ -63,10 +61,7 @@ export default [{
|
|||||||
format: 'iife',
|
format: 'iife',
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: minificationPlugins
|
||||||
babel(),
|
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}, {
|
}, {
|
||||||
input: './examples/quill.js',
|
input: './examples/quill.js',
|
||||||
output: {
|
output: {
|
||||||
@ -80,8 +75,6 @@ export default [{
|
|||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
module: true
|
module: true
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs()
|
||||||
babel(),
|
].concat(minificationPlugins)
|
||||||
uglify()
|
|
||||||
]
|
|
||||||
}]
|
}]
|
||||||
|
@ -19,6 +19,10 @@ export class GC {
|
|||||||
this._length = 0
|
this._length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get _redone () {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
get _deleted () {
|
get _deleted () {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -113,6 +113,30 @@ export class Item {
|
|||||||
this._redone = null
|
this._redone = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the next non-deleted item
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
get _next () {
|
||||||
|
let n = this._right
|
||||||
|
while (n !== null && n._deleted) {
|
||||||
|
n = n._right
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the previous non-deleted item
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
get _prev () {
|
||||||
|
let n = this._left
|
||||||
|
while (n !== null && n._deleted) {
|
||||||
|
n = n._left
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an Item with the same effect as this Item (without position effect)
|
* Creates an Item with the same effect as this Item (without position effect)
|
||||||
*
|
*
|
||||||
@ -127,7 +151,7 @@ export class Item {
|
|||||||
* Redoes the effect of this operation.
|
* Redoes the effect of this operation.
|
||||||
*
|
*
|
||||||
* @param {Y} y The Yjs instance.
|
* @param {Y} y The Yjs instance.
|
||||||
* @param {Array<Item>} redoitems
|
* @param {Set<Item>} redoitems
|
||||||
*
|
*
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
|
@ -57,6 +57,17 @@ export class Type extends Item {
|
|||||||
this._deepEventHandler = new EventHandler()
|
this._deepEventHandler = new EventHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The first non-deleted item
|
||||||
|
*/
|
||||||
|
get _first () {
|
||||||
|
let n = this._start
|
||||||
|
while (n !== null && n._deleted) {
|
||||||
|
n = n._right
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the path from this type to the specified target.
|
* Compute the path from this type to the specified target.
|
||||||
*
|
*
|
||||||
|
@ -95,10 +95,12 @@ export class YArray extends Type {
|
|||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (!n._deleted && n._countable) {
|
if (!n._deleted && n._countable) {
|
||||||
if (index < n._length) {
|
if (index < n._length) {
|
||||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
switch (n.constructor) {
|
||||||
return n._content[index]
|
case ItemJSON:
|
||||||
} else {
|
case ItemString:
|
||||||
return n
|
return n._content[index]
|
||||||
|
default:
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
index -= n._length
|
index -= n._length
|
||||||
|
@ -485,6 +485,10 @@ export class YText extends YArray {
|
|||||||
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toDom () {
|
||||||
|
return document.createTextNode(this.toString())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the unformatted string representation of this YText type.
|
* Returns the unformatted string representation of this YText type.
|
||||||
*
|
*
|
||||||
|
@ -49,14 +49,14 @@ export class EventHandler {
|
|||||||
* Call all event listeners that were added via
|
* Call all event listeners that were added via
|
||||||
* {@link EventHandler#addEventListener}.
|
* {@link EventHandler#addEventListener}.
|
||||||
*
|
*
|
||||||
* @param {Transaction} transaction The transaction object // TODO: do we need this?
|
* @param {Transaction} transaction The transaction object
|
||||||
* @param {YEvent} event An event object that describes the change on a type.
|
* @param {YEvent} event An event object that describes the change on a type.
|
||||||
*/
|
*/
|
||||||
callEventListeners (transaction, event) {
|
callEventListeners (transaction, event) {
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||||
try {
|
try {
|
||||||
const f = this.eventListeners[i]
|
const f = this.eventListeners[i]
|
||||||
f(event)
|
f(event, transaction)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/*
|
/*
|
||||||
Your observer threw an error. This error was caught so that Yjs
|
Your observer threw an error. This error was caught so that Yjs
|
||||||
|
10
utils/Y.js
10
utils/Y.js
@ -28,17 +28,11 @@ import { Decoder } from '../lib/decoding.js' // eslint-disable-line
|
|||||||
*/
|
*/
|
||||||
export class Y extends NamedEventHandler {
|
export class Y extends NamedEventHandler {
|
||||||
/**
|
/**
|
||||||
* @param {string} room Users in the same room share the same content
|
* @param {Object} [conf] configuration
|
||||||
* @param {Object} conf configuration
|
|
||||||
*/
|
*/
|
||||||
constructor (room, conf = {}) {
|
constructor (conf = {}) {
|
||||||
super()
|
super()
|
||||||
this.gcEnabled = conf.gc || false
|
this.gcEnabled = conf.gc || false
|
||||||
/**
|
|
||||||
* The room name that this Yjs instance connects to.
|
|
||||||
* @type {String}
|
|
||||||
*/
|
|
||||||
this.room = room
|
|
||||||
this._contentReady = false
|
this._contentReady = false
|
||||||
this.userID = generateRandomUint32()
|
this.userID = generateRandomUint32()
|
||||||
// TODO: This should be a Map so we can use encodables as keys
|
// TODO: This should be a Map so we can use encodables as keys
|
||||||
|
@ -46,7 +46,7 @@ export const getRelativePosition = (type, offset) => {
|
|||||||
// TODO: rename to createRelativePosition
|
// TODO: rename to createRelativePosition
|
||||||
let t = type._start
|
let t = type._start
|
||||||
while (t !== null) {
|
while (t !== null) {
|
||||||
if (t._deleted === false) {
|
if (!t._deleted && t._countable) {
|
||||||
if (t._length > offset) {
|
if (t._length > offset) {
|
||||||
return [t._id.user, t._id.clock + offset]
|
return [t._id.user, t._id.clock + offset]
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ export const getRelativePosition = (type, offset) => {
|
|||||||
/**
|
/**
|
||||||
* @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition}
|
* @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition}
|
||||||
* @property {YType} type The type on which to apply the absolute position.
|
* @property {YType} type The type on which to apply the absolute position.
|
||||||
* @property {Integer} offset The absolute offset.r
|
* @property {number} offset The absolute offset.r
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -72,6 +72,9 @@ export const getRelativePosition = (type, offset) => {
|
|||||||
* (type + offset).
|
* (type + offset).
|
||||||
*/
|
*/
|
||||||
export const fromRelativePosition = (y, rpos) => {
|
export const fromRelativePosition = (y, rpos) => {
|
||||||
|
if (rpos === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
if (rpos[0] === 'endof') {
|
if (rpos[0] === 'endof') {
|
||||||
let id
|
let id
|
||||||
if (rpos[3] === null) {
|
if (rpos[3] === null) {
|
||||||
@ -80,6 +83,9 @@ export const fromRelativePosition = (y, rpos) => {
|
|||||||
id = ID.createRootID(rpos[3], rpos[4])
|
id = ID.createRootID(rpos[3], rpos[4])
|
||||||
}
|
}
|
||||||
let type = y.os.get(id)
|
let type = y.os.get(id)
|
||||||
|
if (type === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
while (type._redone !== null) {
|
while (type._redone !== null) {
|
||||||
type = type._redone
|
type = type._redone
|
||||||
}
|
}
|
||||||
@ -93,6 +99,9 @@ export const fromRelativePosition = (y, rpos) => {
|
|||||||
} else {
|
} else {
|
||||||
let offset = 0
|
let offset = 0
|
||||||
let struct = y.os.findNodeWithUpperBound(ID.createID(rpos[0], rpos[1])).val
|
let struct = y.os.findNodeWithUpperBound(ID.createID(rpos[0], rpos[1])).val
|
||||||
|
if (struct === null || struct._id.user === ID.RootFakeUserID) {
|
||||||
|
return null // TODO: support fake ids?
|
||||||
|
}
|
||||||
const diff = rpos[1] - struct._id.clock
|
const diff = rpos[1] - struct._id.clock
|
||||||
while (struct._redone !== null) {
|
while (struct._redone !== null) {
|
||||||
struct = struct._redone
|
struct = struct._redone
|
||||||
@ -101,12 +110,12 @@ export const fromRelativePosition = (y, rpos) => {
|
|||||||
if (struct.constructor === GC || parent._deleted) {
|
if (struct.constructor === GC || parent._deleted) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (!struct._deleted) {
|
if (!struct._deleted && struct._countable) {
|
||||||
offset = diff
|
offset = diff
|
||||||
}
|
}
|
||||||
struct = struct._left
|
struct = struct._left
|
||||||
while (struct !== null) {
|
while (struct !== null) {
|
||||||
if (!struct._deleted) {
|
if (!struct._deleted && struct._countable) {
|
||||||
offset += struct._length
|
offset += struct._length
|
||||||
}
|
}
|
||||||
struct = struct._left
|
struct = struct._left
|
||||||
@ -117,3 +126,5 @@ export const fromRelativePosition = (y, rpos) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const equal = (posa, posb) => posa === posb || (posa !== null && posb !== null && posa.length === posb.length && posa.every((v, i) => v === posb[i]))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user