634 lines
20 KiB
JavaScript
634 lines
20 KiB
JavaScript
/**
|
|
* @module bindings/prosemirror
|
|
*/
|
|
|
|
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, 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 {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
|
*/
|
|
|
|
/**
|
|
* The unique prosemirror plugin key for prosemirrorPlugin.
|
|
*
|
|
* @public
|
|
*/
|
|
export const prosemirrorPluginKey = new PluginKey('yjs')
|
|
|
|
/**
|
|
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync.
|
|
*
|
|
* This plugin also keeps references to the type and the shared document so other plugins can access it.
|
|
* @param {YXmlFragment} yXmlFragment
|
|
* @return {Plugin} Returns a prosemirror plugin that binds to this type
|
|
*/
|
|
export const prosemirrorPlugin = yXmlFragment => {
|
|
const pluginState = {
|
|
type: yXmlFragment,
|
|
y: yXmlFragment._y,
|
|
binding: null
|
|
}
|
|
let changedInitialContent = false
|
|
const plugin = new Plugin({
|
|
key: prosemirrorPluginKey,
|
|
state: {
|
|
init: (initargs, state) => {
|
|
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
|
|
}
|
|
},
|
|
view: view => {
|
|
const binding = new ProsemirrorBinding(yXmlFragment, view)
|
|
pluginState.binding = binding
|
|
return {
|
|
update: () => {
|
|
if (changedInitialContent || view.state.doc.content.size > 4) {
|
|
changedInitialContent = true
|
|
binding._prosemirrorChanged(view.state.doc)
|
|
}
|
|
},
|
|
destroy: () => {
|
|
binding.destroy()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
return plugin
|
|
}
|
|
|
|
/**
|
|
* The unique prosemirror plugin key for cursorPlugin.type
|
|
*
|
|
* @public
|
|
*/
|
|
export const cursorPluginKey = new PluginKey('yjs-cursor')
|
|
|
|
/**
|
|
* A prosemirror plugin that listens to awareness information on Yjs.
|
|
* This requires that a `prosemirrorPlugin` is also bound to the prosemirror.
|
|
*
|
|
* @public
|
|
*/
|
|
export const cursorPlugin = new Plugin({
|
|
key: cursorPluginKey,
|
|
props: {
|
|
decorations: state => {
|
|
const ystate = prosemirrorPluginKey.getState(state)
|
|
const y = ystate.y
|
|
const awareness = y.getAwarenessInfo()
|
|
const decorations = []
|
|
awareness.forEach((aw, userID) => {
|
|
if (aw.cursor != null) {
|
|
let user = aw.user || {}
|
|
if (user.color == null) {
|
|
user.color = '#ffa500'
|
|
}
|
|
if (user.name == null) {
|
|
user.name = `User: ${userID}`
|
|
}
|
|
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')
|
|
cursor.setAttribute('style', `border-color: ${user.color}`)
|
|
const userDiv = document.createElement('div')
|
|
userDiv.setAttribute('style', `background-color: ${user.color}`)
|
|
userDiv.insertBefore(document.createTextNode(user.name), null)
|
|
cursor.insertBefore(userDiv, null)
|
|
return cursor
|
|
}, { key: userID + '' }))
|
|
const from = math.min(anchor, head)
|
|
const to = math.max(anchor, head)
|
|
decorations.push(Decoration.inline(from, to, { style: `background-color: ${user.color}70` }))
|
|
}
|
|
}
|
|
})
|
|
return DecorationSet.create(state.doc, decorations)
|
|
}
|
|
},
|
|
view: view => {
|
|
const ystate = prosemirrorPluginKey.getState(view.state)
|
|
const y = ystate.y
|
|
const awarenessListener = () => {
|
|
view.updateState(view.state)
|
|
}
|
|
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', {
|
|
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)
|
|
y.off('awareness', awarenessListener)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @protected
|
|
*/
|
|
export class ProsemirrorBinding {
|
|
/**
|
|
* @param {YXmlFragment} yXmlFragment The bind source
|
|
* @param {EditorView} prosemirrorView The target binding
|
|
*/
|
|
constructor (yXmlFragment, prosemirrorView) {
|
|
this.type = yXmlFragment
|
|
this.prosemirrorView = prosemirrorView
|
|
this.mux = createMutex()
|
|
/**
|
|
* @type {ProsemirrorMapping}
|
|
*/
|
|
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, transaction) {
|
|
if (events.length === 0) {
|
|
return
|
|
}
|
|
console.info('new types:', transaction.newTypes.size, 'deleted types:', transaction.deletedStructs.size, transaction.newTypes, transaction.deletedStructs)
|
|
this.mux(() => {
|
|
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 (doc) {
|
|
this.mux(() => {
|
|
updateYFragment(this.type, doc.content, this.mapping)
|
|
})
|
|
}
|
|
destroy () {
|
|
this.type.unobserveDeep(this._observeFunction)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @privateMapping
|
|
* @param {YXmlElement} el
|
|
* @param {PModel.Schema} schema
|
|
* @param {ProsemirrorMapping} mapping
|
|
* @return {PModel.Node}
|
|
*/
|
|
export const createNodeIfNotExists = (el, schema, mapping) => {
|
|
const node = mapping.get(el)
|
|
if (node === undefined) {
|
|
return createNodeFromYElement(el, schema, mapping)
|
|
}
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {YXmlElement} el
|
|
* @param {PModel.Schema} schema
|
|
* @param {ProsemirrorMapping} mapping
|
|
* @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) {
|
|
const n = createNodeIfNotExists(type, schema, mapping)
|
|
if (n !== null) {
|
|
children.push(n)
|
|
}
|
|
} else {
|
|
const ns = createTextNodesFromYText(type, schema, mapping)
|
|
if (ns !== null) {
|
|
ns.forEach(textchild => {
|
|
if (textchild !== null) {
|
|
children.push(textchild)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
})
|
|
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 {YText} text
|
|
* @param {PModel.Schema} schema
|
|
* @param {ProsemirrorMapping} mapping
|
|
* @return {Array<PModel.Node>}
|
|
*/
|
|
export const createTextNodesFromYText = (text, schema, mapping) => {
|
|
const nodes = []
|
|
const deltas = text.toDelta()
|
|
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))
|
|
}
|
|
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
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
* @param {PModel.Node} node
|
|
* @param {ProsemirrorMapping} mapping
|
|
* @return {YXmlElement | YText}
|
|
*/
|
|
export const createTypeFromNode = (node, mapping) => {
|
|
let type
|
|
if (node.isText) {
|
|
type = new YText()
|
|
const attrs = {}
|
|
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
|
type.insert(0, node.text, attrs)
|
|
} else {
|
|
type = new YXmlElement(node.type.name)
|
|
for (let key in node.attrs) {
|
|
const val = node.attrs[key]
|
|
if (val !== null) {
|
|
type.setAttribute(key, val)
|
|
}
|
|
}
|
|
const ins = []
|
|
for (let i = 0; i < node.childCount; i++) {
|
|
ins.push(createTypeFromNode(node.child(i), mapping))
|
|
}
|
|
type.insert(0, ins)
|
|
}
|
|
mapping.set(type, node)
|
|
return type
|
|
}
|
|
|
|
const equalAttrs = (pattrs, yattrs) => {
|
|
const keys = Object.keys(pattrs).filter(key => pattrs[key] === null)
|
|
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
|
|
for (let i = 0; i < keys.length && eq; i++) {
|
|
const key = keys[i]
|
|
eq = pattrs[key] === yattrs[key]
|
|
}
|
|
return eq
|
|
}
|
|
|
|
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 => equalAttrs(d.attributes[mark.type.name], mark.attrs))
|
|
}
|
|
|
|
const equalYTypePNode = (ytype, pnode) =>
|
|
ytype.constructor === YText
|
|
? equalYTextPText(ytype, pnode)
|
|
: (matchNodeName(ytype, pnode) && ytype.length === pnode.childCount && equalAttrs(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 {PModel.Node} pContent
|
|
* @param {ProsemirrorMapping} mapping
|
|
*/
|
|
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()
|
|
const pAttrs = pContent.attrs
|
|
for (let key in pAttrs) {
|
|
if (pAttrs[key] !== null) {
|
|
if (yDomAttrs[key] !== pAttrs[key]) {
|
|
yDomFragment.setAttribute(key, pAttrs[key])
|
|
}
|
|
} else {
|
|
yDomFragment.removeAttribute(key)
|
|
}
|
|
}
|
|
// remove all keys that are no longer in pAttrs
|
|
for (let key in yDomAttrs) {
|
|
if (pAttrs[key] === undefined) {
|
|
yDomFragment.removeAttribute(key)
|
|
}
|
|
}
|
|
}
|
|
// update children
|
|
const pChildCnt = pContent.childCount
|
|
const yChildren = yDomFragment.toArray()
|
|
const yChildCnt = yChildren.length
|
|
const minCnt = math.min(pChildCnt, yChildCnt)
|
|
let left = 0
|
|
let right = 0
|
|
// find number of matching elements from left
|
|
for (;left < minCnt; left++) {
|
|
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 + 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
|
|
}
|
|
}
|
|
}
|
|
yDomFragment._y.transact(() => {
|
|
// 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()
|