improved granularity of prosemirror binding
This commit is contained in:
		
							parent
							
								
									c9ea3a412e
								
							
						
					
					
						commit
						582095e5a3
					
				@ -2,16 +2,18 @@
 | 
			
		||||
 * @module bindings/prosemirror
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { BindMapping } from '../utils/BindMapping.js'
 | 
			
		||||
import { YText } from '../types/YText.js' // eslint-disable-line
 | 
			
		||||
import { YXmlElement, YXmlFragment } from '../types/YXmlElement.js' // eslint-disable-line
 | 
			
		||||
import { createMutex } from '../lib/mutex.js'
 | 
			
		||||
import * as PModel from 'prosemirror-model'
 | 
			
		||||
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,
 | 
			
		||||
    binding: null
 | 
			
		||||
  }
 | 
			
		||||
  let changedInitialContent = false
 | 
			
		||||
  const plugin = new Plugin({
 | 
			
		||||
    key: prosemirrorPluginKey,
 | 
			
		||||
    state: {
 | 
			
		||||
@ -41,6 +44,11 @@ export const prosemirrorPlugin = yXmlFragment => {
 | 
			
		||||
        return 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
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
@ -49,7 +57,10 @@ export const prosemirrorPlugin = yXmlFragment => {
 | 
			
		||||
      pluginState.binding = binding
 | 
			
		||||
      return {
 | 
			
		||||
        update: () => {
 | 
			
		||||
          binding._prosemirrorChanged()
 | 
			
		||||
          if (changedInitialContent || view.state.doc.content.size > 4) {
 | 
			
		||||
            changedInitialContent = true
 | 
			
		||||
            binding._prosemirrorChanged(view.state.doc)
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        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
 | 
			
		||||
 */
 | 
			
		||||
@ -77,44 +88,61 @@ export const cursorPlugin = new Plugin({
 | 
			
		||||
  key: cursorPluginKey,
 | 
			
		||||
  props: {
 | 
			
		||||
    decorations: state => {
 | 
			
		||||
      const y = prosemirrorPluginKey.getState(state).y
 | 
			
		||||
      const ystate = prosemirrorPluginKey.getState(state)
 | 
			
		||||
      const y = ystate.y
 | 
			
		||||
      const awareness = y.getAwarenessInfo()
 | 
			
		||||
      const decorations = []
 | 
			
		||||
      awareness.forEach((state, userID) => {
 | 
			
		||||
        if (state.cursor != null) {
 | 
			
		||||
      awareness.forEach((aw, userID) => {
 | 
			
		||||
        if (aw.cursor != null) {
 | 
			
		||||
          const username = `User: ${userID}`
 | 
			
		||||
          decorations.push(Decoration.widget(state.cursor.from, () => {
 | 
			
		||||
            const cursor = document.createElement('span')
 | 
			
		||||
            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 }))
 | 
			
		||||
          decorations.push(Decoration.inline(state.cursor.from, state.cursor.to, { style: 'background-color: #ffa50070' }))
 | 
			
		||||
          let anchor = relativePositionToAbsolutePosition(ystate.type, aw.cursor.anchor || null, ystate.binding.mapping)
 | 
			
		||||
          let head = relativePositionToAbsolutePosition(ystate.type, aw.cursor.head || null, ystate.binding.mapping)
 | 
			
		||||
          if (anchor !== null && head !== null) {
 | 
			
		||||
            let maxsize = math.max(state.doc.content.size - 1, 0)
 | 
			
		||||
            anchor = math.min(anchor, maxsize)
 | 
			
		||||
            head = math.min(head, maxsize)
 | 
			
		||||
            decorations.push(Decoration.widget(head, () => {
 | 
			
		||||
              const cursor = document.createElement('span')
 | 
			
		||||
              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)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  view: view => {
 | 
			
		||||
    const y = prosemirrorPluginKey.getState(view.state).y
 | 
			
		||||
    const ystate = prosemirrorPluginKey.getState(view.state)
 | 
			
		||||
    const y = ystate.y
 | 
			
		||||
    const awarenessListener = () => {
 | 
			
		||||
      view.updateState(view.state)
 | 
			
		||||
    }
 | 
			
		||||
    y.on('awareness', awarenessListener)
 | 
			
		||||
    return {
 | 
			
		||||
      update: () => {
 | 
			
		||||
        const y = prosemirrorPluginKey.getState(view.state).y
 | 
			
		||||
        const from = view.state.selection.from
 | 
			
		||||
        const to = view.state.selection.to
 | 
			
		||||
        const current = y.getLocalAwarenessInfo()
 | 
			
		||||
        if (current.cursor == null || current.cursor.to !== to || current.cursor.from !== from) {
 | 
			
		||||
    const updateCursorInfo = () => {
 | 
			
		||||
      const current = y.getLocalAwarenessInfo()
 | 
			
		||||
      if (view.hasFocus()) {
 | 
			
		||||
        const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
 | 
			
		||||
        const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
 | 
			
		||||
        if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
 | 
			
		||||
          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: () => {
 | 
			
		||||
        const y = prosemirrorPluginKey.getState(view.state).y
 | 
			
		||||
        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.
 | 
			
		||||
 *
 | 
			
		||||
@ -141,36 +278,47 @@ export class ProsemirrorBinding {
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {ProsemirrorMapping}
 | 
			
		||||
     */
 | 
			
		||||
    this.mapping = new BindMapping()
 | 
			
		||||
    this.mapping = new Map()
 | 
			
		||||
    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)
 | 
			
		||||
  }
 | 
			
		||||
  _typeChanged (events) {
 | 
			
		||||
  _typeChanged (events, transaction) {
 | 
			
		||||
    if (events.length === 0) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
 | 
			
		||||
    this.mux(() => {
 | 
			
		||||
      events.forEach(event => {
 | 
			
		||||
        // recompute node for each parent
 | 
			
		||||
        // except main node, compute main node in the end
 | 
			
		||||
        let target = event.target
 | 
			
		||||
        if (target !== this.type) {
 | 
			
		||||
          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 delStruct = (_, struct) => this.mapping.delete(struct)
 | 
			
		||||
      transaction.deletedStructs.forEach(struct => this.mapping.delete(struct))
 | 
			
		||||
      transaction.changedTypes.forEach(delStruct)
 | 
			
		||||
      transaction.changedParentTypes.forEach(delStruct)
 | 
			
		||||
      const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(t, this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
 | 
			
		||||
      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))
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  _prosemirrorChanged () {
 | 
			
		||||
  _prosemirrorChanged (doc) {
 | 
			
		||||
    this.mux(() => {
 | 
			
		||||
      updateYFragment(this.type, this.prosemirrorView.state, this.mapping)
 | 
			
		||||
      updateYFragment(this.type, doc.content, this.mapping)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  destroy () {
 | 
			
		||||
@ -179,14 +327,14 @@ export class ProsemirrorBinding {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * @param {Y.XmlElement} el
 | 
			
		||||
 * @privateMapping
 | 
			
		||||
 * @param {YXmlElement} el
 | 
			
		||||
 * @param {PModel.Schema} schema
 | 
			
		||||
 * @param {ProsemirrorMapping} mapping
 | 
			
		||||
 * @return {PModel.Node}
 | 
			
		||||
 */
 | 
			
		||||
export const createNodeIfNotExists = (el, schema, mapping) => {
 | 
			
		||||
  const node = mapping.getY(el)
 | 
			
		||||
  const node = mapping.get(el)
 | 
			
		||||
  if (node === undefined) {
 | 
			
		||||
    return createNodeFromYElement(el, schema, mapping)
 | 
			
		||||
  }
 | 
			
		||||
@ -195,28 +343,48 @@ export const createNodeIfNotExists = (el, schema, mapping) => {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * @param {Y.XmlElement} el
 | 
			
		||||
 * @param {YXmlElement} el
 | 
			
		||||
 * @param {PModel.Schema} schema
 | 
			
		||||
 * @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) => {
 | 
			
		||||
  const children = []
 | 
			
		||||
  el.toArray().forEach(type => {
 | 
			
		||||
    if (type.constructor === YXmlElement) {
 | 
			
		||||
      children.push(createNodeIfNotExists(type, schema, mapping))
 | 
			
		||||
      const n = createNodeIfNotExists(type, schema, mapping)
 | 
			
		||||
      if (n !== null) {
 | 
			
		||||
        children.push(n)
 | 
			
		||||
      }
 | 
			
		||||
    } 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)))
 | 
			
		||||
  mapping.bind(el, node)
 | 
			
		||||
  let 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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * @param {Y.Text} text
 | 
			
		||||
 * @param {YText} text
 | 
			
		||||
 * @param {PModel.Schema} schema
 | 
			
		||||
 * @param {ProsemirrorMapping} mapping
 | 
			
		||||
 * @return {Array<PModel.Node>}
 | 
			
		||||
@ -224,16 +392,23 @@ export const createNodeFromYElement = (el, schema, mapping) => {
 | 
			
		||||
export const createTextNodesFromYText = (text, schema, mapping) => {
 | 
			
		||||
  const nodes = []
 | 
			
		||||
  const deltas = text.toDelta()
 | 
			
		||||
  for (let i = 0; i < deltas.length; i++) {
 | 
			
		||||
    const delta = deltas[i]
 | 
			
		||||
    const marks = []
 | 
			
		||||
    for (let markName in delta.attributes) {
 | 
			
		||||
      marks.push(schema.mark(markName, delta.attributes[markName]))
 | 
			
		||||
  try {
 | 
			
		||||
    for (let i = 0; i < deltas.length; i++) {
 | 
			
		||||
      const delta = deltas[i]
 | 
			
		||||
      const marks = []
 | 
			
		||||
      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.bind(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
 | 
			
		||||
    if (nodes.length > 0) {
 | 
			
		||||
      mapping.set(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
 | 
			
		||||
}
 | 
			
		||||
@ -256,44 +431,176 @@ export const createTypeFromNode = (node, mapping) => {
 | 
			
		||||
    for (let key in node.attrs) {
 | 
			
		||||
      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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 * @param {YXmlFragment} yDomFragment
 | 
			
		||||
 * @param {EditorState} state
 | 
			
		||||
 * @param {BindMapping} mapping
 | 
			
		||||
 * @param {PModel.Node} pContent
 | 
			
		||||
 * @param {ProsemirrorMapping} mapping
 | 
			
		||||
 */
 | 
			
		||||
const updateYFragment = (yDomFragment, state, mapping) => {
 | 
			
		||||
  const pChildCnt = state.doc.content.childCount
 | 
			
		||||
const updateYFragment = (yDomFragment, pContent, mapping) => {
 | 
			
		||||
  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 yChildCnt = yChildren.length
 | 
			
		||||
  const minCnt = pChildCnt < yChildCnt ? pChildCnt : yChildCnt
 | 
			
		||||
  const minCnt = math.min(pChildCnt, yChildCnt)
 | 
			
		||||
  let left = 0
 | 
			
		||||
  let right = 0
 | 
			
		||||
  // find number of matching elements from left
 | 
			
		||||
  for (;left < minCnt; left++) {
 | 
			
		||||
    if (state.doc.content.child(left) !== mapping.getY(yChildren[left])) {
 | 
			
		||||
      break
 | 
			
		||||
    const leftY = yChildren[left]
 | 
			
		||||
    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
 | 
			
		||||
  for (;right < minCnt; right++) {
 | 
			
		||||
    if (state.doc.content.child(pChildCnt - right - 1) !== mapping.getY(yChildren[yChildCnt - right - 1])) {
 | 
			
		||||
      break
 | 
			
		||||
  for (;right + left < minCnt; right++) {
 | 
			
		||||
    const rightY = yChildren[yChildCnt - right - 1]
 | 
			
		||||
    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(() => {
 | 
			
		||||
    // now update y to match editor state
 | 
			
		||||
    yDomFragment.delete(left, yChildCnt - left - right)
 | 
			
		||||
    yDomFragment.insert(left, state.doc.content.content.slice(left, pChildCnt - right).map(node => createTypeFromNode(node, mapping)))
 | 
			
		||||
    // try to compare and update
 | 
			
		||||
    while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) {
 | 
			
		||||
      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;
 | 
			
		||||
      }
 | 
			
		||||
      .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 {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        border-left: black;
 | 
			
		||||
 | 
			
		||||
@ -58,8 +58,6 @@ export const until = (timeout, check) => createPromise((resolve, reject) => {
 | 
			
		||||
 | 
			
		||||
export const error = description => new Error(description)
 | 
			
		||||
 | 
			
		||||
export const max = (a, b) => a > b ? a : b
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {number} t Time to wait
 | 
			
		||||
 * @return {Promise} Promise that is resolved after t ms
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										24
									
								
								lib/math.js
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								lib/math.js
									
									
									
									
									
								
							@ -2,3 +2,27 @@
 | 
			
		||||
 * @module math
 | 
			
		||||
 */
 | 
			
		||||
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,
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "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",
 | 
			
		||||
    "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",
 | 
			
		||||
    "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
 | 
			
		||||
    "serve-docs": "npm run docs && serve ./docs/",
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import * as bc from '../../lib/broadcastchannel.js'
 | 
			
		||||
const messageSync = 0
 | 
			
		||||
const messageAwareness = 1
 | 
			
		||||
 | 
			
		||||
const reconnectTimeout = 100
 | 
			
		||||
const reconnectTimeout = 3000
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @param {WebsocketsSharedDocument} doc
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,7 @@ const afterTransaction = (doc, transaction) => {
 | 
			
		||||
 | 
			
		||||
class WSSharedDoc extends Y.Y {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
    super({ gc: true })
 | 
			
		||||
    this.mux = Y.createMutex()
 | 
			
		||||
    /**
 | 
			
		||||
     * 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({
 | 
			
		||||
        room: metakey.slice(5),
 | 
			
		||||
        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))
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,9 @@ import commonjs from 'rollup-plugin-commonjs'
 | 
			
		||||
import babel from 'rollup-plugin-babel'
 | 
			
		||||
import uglify from 'rollup-plugin-uglify-es'
 | 
			
		||||
 | 
			
		||||
// set this to [] to disable obfuscation
 | 
			
		||||
const minificationPlugins = process.env.PRODUCTION ? [babel(), uglify()] : []
 | 
			
		||||
 | 
			
		||||
export default [{
 | 
			
		||||
  input: './index.js',
 | 
			
		||||
  output: [{
 | 
			
		||||
@ -39,10 +42,8 @@ export default [{
 | 
			
		||||
      sourcemap: true,
 | 
			
		||||
      module: true
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs(),
 | 
			
		||||
    babel(),
 | 
			
		||||
    uglify()
 | 
			
		||||
  ]
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ].concat(minificationPlugins)
 | 
			
		||||
}, {
 | 
			
		||||
  input: './examples/dom.js',
 | 
			
		||||
  output: {
 | 
			
		||||
@ -51,10 +52,7 @@ export default [{
 | 
			
		||||
    format: 'iife',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    babel(),
 | 
			
		||||
    uglify()
 | 
			
		||||
  ]
 | 
			
		||||
  plugins: minificationPlugins
 | 
			
		||||
}, {
 | 
			
		||||
  input: './examples/textarea.js',
 | 
			
		||||
  output: {
 | 
			
		||||
@ -63,10 +61,7 @@ export default [{
 | 
			
		||||
    format: 'iife',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    babel(),
 | 
			
		||||
    uglify()
 | 
			
		||||
  ]
 | 
			
		||||
  plugins: minificationPlugins
 | 
			
		||||
}, {
 | 
			
		||||
  input: './examples/quill.js',
 | 
			
		||||
  output: {
 | 
			
		||||
@ -80,8 +75,6 @@ export default [{
 | 
			
		||||
      sourcemap: true,
 | 
			
		||||
      module: true
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs(),
 | 
			
		||||
    babel(),
 | 
			
		||||
    uglify()
 | 
			
		||||
  ]
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ].concat(minificationPlugins)
 | 
			
		||||
}]
 | 
			
		||||
 | 
			
		||||
@ -19,6 +19,10 @@ export class GC {
 | 
			
		||||
    this._length = 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get _redone () {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get _deleted () {
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -113,6 +113,30 @@ export class Item {
 | 
			
		||||
    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)
 | 
			
		||||
   *
 | 
			
		||||
@ -127,7 +151,7 @@ export class Item {
 | 
			
		||||
   * Redoes the effect of this operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance.
 | 
			
		||||
   * @param {Array<Item>} redoitems
 | 
			
		||||
   * @param {Set<Item>} redoitems
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
 | 
			
		||||
@ -57,6 +57,17 @@ export class Type extends Item {
 | 
			
		||||
    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.
 | 
			
		||||
   *
 | 
			
		||||
 | 
			
		||||
@ -95,10 +95,12 @@ export class YArray extends Type {
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted && n._countable) {
 | 
			
		||||
        if (index < n._length) {
 | 
			
		||||
          if (n.constructor === ItemJSON || n.constructor === ItemString) {
 | 
			
		||||
            return n._content[index]
 | 
			
		||||
          } else {
 | 
			
		||||
            return n
 | 
			
		||||
          switch (n.constructor) {
 | 
			
		||||
            case ItemJSON:
 | 
			
		||||
            case ItemString:
 | 
			
		||||
              return n._content[index]
 | 
			
		||||
            default:
 | 
			
		||||
              return n
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        index -= n._length
 | 
			
		||||
 | 
			
		||||
@ -485,6 +485,10 @@ export class YText extends YArray {
 | 
			
		||||
    this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toDom () {
 | 
			
		||||
    return document.createTextNode(this.toString())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the unformatted string representation of this YText type.
 | 
			
		||||
   *
 | 
			
		||||
 | 
			
		||||
@ -49,14 +49,14 @@ export class EventHandler {
 | 
			
		||||
   * Call all event listeners that were added via
 | 
			
		||||
   * {@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.
 | 
			
		||||
   */
 | 
			
		||||
  callEventListeners (transaction, event) {
 | 
			
		||||
    for (var i = 0; i < this.eventListeners.length; i++) {
 | 
			
		||||
      try {
 | 
			
		||||
        const f = this.eventListeners[i]
 | 
			
		||||
        f(event)
 | 
			
		||||
        f(event, transaction)
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        /*
 | 
			
		||||
          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 {
 | 
			
		||||
  /**
 | 
			
		||||
   * @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()
 | 
			
		||||
    this.gcEnabled = conf.gc || false
 | 
			
		||||
    /**
 | 
			
		||||
     * The room name that this Yjs instance connects to.
 | 
			
		||||
     * @type {String}
 | 
			
		||||
     */
 | 
			
		||||
    this.room = room
 | 
			
		||||
    this._contentReady = false
 | 
			
		||||
    this.userID = generateRandomUint32()
 | 
			
		||||
    // 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
 | 
			
		||||
  let t = type._start
 | 
			
		||||
  while (t !== null) {
 | 
			
		||||
    if (t._deleted === false) {
 | 
			
		||||
    if (!t._deleted && t._countable) {
 | 
			
		||||
      if (t._length > 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}
 | 
			
		||||
 * @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).
 | 
			
		||||
 */
 | 
			
		||||
export const fromRelativePosition = (y, rpos) => {
 | 
			
		||||
  if (rpos === null) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  if (rpos[0] === 'endof') {
 | 
			
		||||
    let id
 | 
			
		||||
    if (rpos[3] === null) {
 | 
			
		||||
@ -80,6 +83,9 @@ export const fromRelativePosition = (y, rpos) => {
 | 
			
		||||
      id = ID.createRootID(rpos[3], rpos[4])
 | 
			
		||||
    }
 | 
			
		||||
    let type = y.os.get(id)
 | 
			
		||||
    if (type === null) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    while (type._redone !== null) {
 | 
			
		||||
      type = type._redone
 | 
			
		||||
    }
 | 
			
		||||
@ -93,6 +99,9 @@ export const fromRelativePosition = (y, rpos) => {
 | 
			
		||||
  } else {
 | 
			
		||||
    let offset = 0
 | 
			
		||||
    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
 | 
			
		||||
    while (struct._redone !== null) {
 | 
			
		||||
      struct = struct._redone
 | 
			
		||||
@ -101,12 +110,12 @@ export const fromRelativePosition = (y, rpos) => {
 | 
			
		||||
    if (struct.constructor === GC || parent._deleted) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    if (!struct._deleted) {
 | 
			
		||||
    if (!struct._deleted && struct._countable) {
 | 
			
		||||
      offset = diff
 | 
			
		||||
    }
 | 
			
		||||
    struct = struct._left
 | 
			
		||||
    while (struct !== null) {
 | 
			
		||||
      if (!struct._deleted) {
 | 
			
		||||
      if (!struct._deleted && struct._countable) {
 | 
			
		||||
        offset += struct._length
 | 
			
		||||
      }
 | 
			
		||||
      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