You're Invited:Meet the Socket Team at BlackHat and DEF CON in Las Vegas, Aug 7-8.RSVP
Socket
Socket
Sign inDemoInstall

y-prosemirror

Package Overview
Dependencies
Maintainers
1
Versions
68
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.0.5 to 0.0.6

src/lib.js

549

dist/y-prosemirror.js

@@ -5,6 +5,8 @@ 'use strict';

var Y = require('yjs');
var prosemirrorView = require('prosemirror-view');
var prosemirrorState = require('prosemirror-state');
require('y-protocols/awareness.js');
var mutex_js = require('lib0/dist/mutex.js');
var PModel = require('prosemirror-model');
var prosemirrorView = require('prosemirror-view');
var prosemirrorState = require('prosemirror-state');
var math = require('lib0/dist/math.js');

@@ -14,201 +16,4 @@ var object = require('lib0/dist/object.js');

var error = require('lib0/dist/error.js');
var Y = require('yjs');
require('y-protocols/awareness.js');
/**
* @module bindings/prosemirror
*/
const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id));
/**
* @typedef {Map<Y.AbstractType, Object>} ProsemirrorMapping
*/
/**
* The unique prosemirror plugin key for prosemirrorPlugin.
*
* @public
*/
const prosemirrorPluginKey = new prosemirrorState.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 {Y.XmlFragment} yXmlFragment
* @return {Plugin} Returns a prosemirror plugin that binds to this type
*/
const prosemirrorPlugin = (yXmlFragment) => {
let changedInitialContent = false;
const plugin = new prosemirrorState.Plugin({
props: {
editable: (state) => prosemirrorPluginKey.getState(state).snapshot == null
},
key: prosemirrorPluginKey,
state: {
init: (initargs, state) => {
return {
type: yXmlFragment,
doc: yXmlFragment.doc,
binding: null,
snapshot: null,
isChangeOrigin: false
}
},
apply: (tr, pluginState) => {
const change = tr.getMeta(prosemirrorPluginKey);
if (change !== undefined) {
pluginState = Object.assign({}, pluginState);
for (let key in change) {
pluginState[key] = change[key];
}
}
// always set isChangeOrigin. If undefined, this is not change origin.
pluginState.isChangeOrigin = change !== undefined && !!change.isChangeOrigin;
if (pluginState.binding !== null) {
if (change !== undefined && change.snapshot !== undefined) {
// snapshot changed, rerender next
setTimeout(() => {
if (change.restore == null) {
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot);
} else {
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot);
// reset to current prosemirror state
delete pluginState.restore;
delete pluginState.snapshot;
delete pluginState.prevSnapshot;
pluginState.binding._prosemirrorChanged(pluginState.binding.prosemirrorView.state.doc);
}
}, 0);
}
}
return pluginState
}
},
view: view => {
const binding = new ProsemirrorBinding(yXmlFragment, view);
view.dispatch(view.state.tr.setMeta(prosemirrorPluginKey, { binding }));
return {
update: () => {
const pluginState = plugin.getState(view.state);
if (pluginState.snapshot == null) {
if (changedInitialContent || view.state.doc.content.size > 2) {
changedInitialContent = true;
binding._prosemirrorChanged(view.state.doc);
}
}
},
destroy: () => {
binding.destroy();
}
}
}
});
return plugin
};
/**
* The unique prosemirror plugin key for cursorPlugin.type
*
* @public
*/
const cursorPluginKey = new prosemirrorState.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
* @param {Awareness} awareness
* @return {Plugin}
*/
const cursorPlugin = awareness => new prosemirrorState.Plugin({
key: cursorPluginKey,
props: {
decorations: state => {
const ystate = prosemirrorPluginKey.getState(state);
const y = ystate.doc;
const decorations = [];
if (ystate.snapshot != null || ystate.binding === null) {
// do not render cursors while snapshot is active
return
}
awareness.getStates().forEach((aw, clientId) => {
if (clientId === y.clientID) {
return
}
if (aw.cursor != null) {
let user = aw.user || {};
if (user.color == null) {
user.color = '#ffa500';
}
if (user.name == null) {
user.name = `User: ${clientId}`;
}
let anchor = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.anchor), ystate.binding.mapping);
let head = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.head), ystate.binding.mapping);
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(prosemirrorView.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: clientId + '' }));
const from = math.min(anchor, head);
const to = math.max(anchor, head);
decorations.push(prosemirrorView.Decoration.inline(from, to, { style: `background-color: ${user.color}70` }));
}
}
});
return prosemirrorView.DecorationSet.create(state.doc, decorations)
}
},
view: view => {
const ystate = prosemirrorPluginKey.getState(view.state);
const awarenessListener = () => {
view.updateState(view.state);
};
const updateCursorInfo = () => {
const current = awareness.getLocalState() || {};
if (view.hasFocus() && ystate.binding !== null) {
/**
* @type {Y.RelativePosition}
*/
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping);
/**
* @type {Y.RelativePosition}
*/
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping);
if (current.cursor == null || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.anchor), anchor) || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.head), head)) {
awareness.setLocalStateField('cursor', {
anchor, head
});
}
} else if (current.cursor !== null) {
awareness.setLocalStateField('cursor', null);
}
};
awareness.on('change', awarenessListener);
view.dom.addEventListener('focusin', updateCursorInfo);
view.dom.addEventListener('focusout', updateCursorInfo);
return {
update: updateCursorInfo,
destroy: () => {
const y = prosemirrorPluginKey.getState(view.state).doc;
y.setAwarenessField('cursor', null);
y.off('change', awarenessListener);
}
}
}
});
/**
* Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model).

@@ -303,10 +108,12 @@ *

while (i < type._length && i < decodedPos.index && n !== null) {
i++;
const t = /** @type {Y.ContentType} */ (n.content).type;
if (t.constructor === Y.XmlText) {
pos += t._length;
} else {
pos += mapping.get(t).nodeSize;
if (!n.deleted) {
const t = /** @type {Y.ContentType} */ (n.content).type;
i++;
if (t.constructor === Y.XmlText) {
pos += t._length;
} else {
pos += mapping.get(t).nodeSize;
}
}
n = /** @type {Y.Item} */ (n.next);
n = /** @type {Y.Item} */ (n.right);
}

@@ -328,8 +135,10 @@ pos += 1; // increase because we go out of n

}
if (contentType.constructor === Y.XmlText) {
pos += contentType._length;
} else {
pos += mapping.get(contentType).nodeSize;
if (!n.deleted) {
if (contentType.constructor === Y.XmlText) {
pos += contentType._length;
} else {
pos += mapping.get(contentType).nodeSize;
}
}
n = n.next;
n = n.right;
}

@@ -343,2 +152,115 @@ }

/**
* @module bindings/prosemirror
*/
const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id));
/**
* @typedef {Map<Y.AbstractType, Object>} ProsemirrorMapping
*/
/**
* The unique prosemirror plugin key for prosemirrorPlugin.
*
* @public
*/
const ySyncPluginKey = new prosemirrorState.PluginKey('y-sync');
/**
* 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 {Y.XmlFragment} yXmlFragment
* @return {Plugin} Returns a prosemirror plugin that binds to this type
*/
const ySyncPlugin = (yXmlFragment) => {
let changedInitialContent = false;
const plugin = new prosemirrorState.Plugin({
props: {
editable: (state) => ySyncPluginKey.getState(state).snapshot == null
},
key: ySyncPluginKey,
state: {
init: (initargs, state) => {
return {
type: yXmlFragment,
doc: yXmlFragment.doc,
binding: null,
snapshot: null,
isChangeOrigin: false
}
},
apply: (tr, pluginState) => {
const change = tr.getMeta(ySyncPluginKey);
if (change !== undefined) {
pluginState = Object.assign({}, pluginState);
for (let key in change) {
pluginState[key] = change[key];
}
}
// always set isChangeOrigin. If undefined, this is not change origin.
pluginState.isChangeOrigin = change !== undefined && !!change.isChangeOrigin;
if (pluginState.binding !== null) {
if (change !== undefined && change.snapshot !== undefined) {
// snapshot changed, rerender next
setTimeout(() => {
if (change.restore == null) {
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot);
} else {
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot);
// reset to current prosemirror state
delete pluginState.restore;
delete pluginState.snapshot;
delete pluginState.prevSnapshot;
pluginState.binding._prosemirrorChanged(pluginState.binding.prosemirrorView.state.doc);
}
}, 0);
}
}
return pluginState
}
},
view: view => {
const binding = new ProsemirrorBinding(yXmlFragment, view);
view.dispatch(view.state.tr.setMeta(ySyncPluginKey, { binding }));
return {
update: () => {
const pluginState = plugin.getState(view.state);
if (pluginState.snapshot == null) {
if (changedInitialContent || view.state.doc.content.size > 2) {
changedInitialContent = true;
binding._prosemirrorChanged(view.state.doc);
}
}
},
destroy: () => {
binding.destroy();
}
}
}
});
return plugin
};
/**
* @param {any} tr
* @param {any} relSel
* @param {ProsemirrorBinding} binding
*/
const restoreRelativeSelection = (tr, relSel, binding) => {
if (relSel !== null && relSel.anchor !== null && relSel.head !== null) {
const anchor = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.anchor, binding.mapping);
const head = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.head, binding.mapping);
if (anchor !== null && head !== null) {
tr = tr.setSelection(prosemirrorState.TextSelection.create(tr.doc, anchor, head));
}
}
};
const getRelativeSelection = (pmbinding, state) => ({
anchor: absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping),
head: absolutePositionToRelativePosition(state.selection.head, pmbinding.type, pmbinding.mapping)
});
/**
* Binding for prosemirror.

@@ -370,8 +292,5 @@ *

*/
this._relSelection = null;
this.beforeTransactionSelection = null;
this.doc.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)
};
this.beforeTransactionSelection = getRelativeSelection(this, prosemirrorView.state);
});

@@ -407,3 +326,3 @@ yXmlFragment.observeDeep(this._observeFunction);

_typeChanged (events, transaction) {
if (events.length === 0 || prosemirrorPluginKey.getState(this.prosemirrorView.state).snapshot != null) {
if (events.length === 0 || ySyncPluginKey.getState(this.prosemirrorView.state).snapshot != null) {
// drop out if snapshot is active

@@ -414,3 +333,3 @@ return

const delStruct = (_, struct) => this.mapping.delete(struct);
Y.iterateDeletedStructs(transaction.deleteSet, this.doc.store, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.Item} */ (struct).content.type));
Y.iterateDeletedStructs(transaction, transaction.deleteSet, this.doc.store, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type));
transaction.changed.forEach(delStruct);

@@ -420,11 +339,7 @@ transaction.changedParentTypes.forEach(delStruct);

let 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.doc, this.type, relSel.anchor, this.mapping);
const head = relativePositionToAbsolutePosition(this.doc, this.type, relSel.head, this.mapping);
if (anchor !== null && head !== null) {
tr = tr.setSelection(prosemirrorState.TextSelection.create(tr.doc, anchor, head));
}
restoreRelativeSelection(tr, this.beforeTransactionSelection, this);
tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true });
if (this.beforeTransactionSelection !== null) {
tr.scrollIntoView();
}
tr = tr.setMeta(prosemirrorPluginKey, { isChangeOrigin: true });
this.prosemirrorView.dispatch(tr);

@@ -802,4 +717,170 @@ });

/**
* The unique prosemirror plugin key for cursorPlugin.type
*
* @public
*/
const yCursorPluginKey = new prosemirrorState.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
* @param {Awareness} awareness
* @return {Plugin}
*/
const yCursorPlugin = awareness => new prosemirrorState.Plugin({
key: yCursorPluginKey,
props: {
decorations: state => {
const ystate = ySyncPluginKey.getState(state);
const y = ystate.doc;
const decorations = [];
if (ystate.snapshot != null || ystate.binding === null) {
// do not render cursors while snapshot is active
return
}
awareness.getStates().forEach((aw, clientId) => {
if (clientId === y.clientID) {
return
}
if (aw.cursor != null) {
let user = aw.user || {};
if (user.color == null) {
user.color = '#ffa500';
}
if (user.name == null) {
user.name = `User: ${clientId}`;
}
let anchor = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.anchor), ystate.binding.mapping);
let head = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.head), ystate.binding.mapping);
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(prosemirrorView.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: clientId + '' }));
const from = math.min(anchor, head);
const to = math.max(anchor, head);
decorations.push(prosemirrorView.Decoration.inline(from, to, { style: `background-color: ${user.color}70` }));
}
}
});
return prosemirrorView.DecorationSet.create(state.doc, decorations)
}
},
view: view => {
const ystate = ySyncPluginKey.getState(view.state);
const awarenessListener = () => {
view.updateState(view.state);
};
const updateCursorInfo = () => {
const current = awareness.getLocalState() || {};
if (view.hasFocus() && ystate.binding !== null) {
/**
* @type {Y.RelativePosition}
*/
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping);
/**
* @type {Y.RelativePosition}
*/
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping);
if (current.cursor == null || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.anchor), anchor) || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.head), head)) {
awareness.setLocalStateField('cursor', {
anchor, head
});
}
} else if (current.cursor !== null) {
awareness.setLocalStateField('cursor', null);
}
};
awareness.on('change', awarenessListener);
view.dom.addEventListener('focusin', updateCursorInfo);
view.dom.addEventListener('focusout', updateCursorInfo);
return {
update: updateCursorInfo,
destroy: () => {
const y = ySyncPluginKey.getState(view.state).doc;
y.setAwarenessField('cursor', null);
y.off('change', awarenessListener);
}
}
}
});
const undo = state => {
const undoManager = yUndoPluginKey.getState(state).undoManager;
if (undoManager != null) {
undoManager.undo();
return true
}
};
const redo = state => {
const undoManager = yUndoPluginKey.getState(state).undoManager;
if (undoManager != null) {
undoManager.redo();
return true
}
};
const yUndoPluginKey = new prosemirrorState.PluginKey('y-undo');
const yUndoPlugin = new prosemirrorState.Plugin({
key: yUndoPluginKey,
state: {
init: (initargs, state) => {
// TODO: check if plugin order matches and fix
const ystate = ySyncPluginKey.getState(state);
const undoManager = new Y.UndoManager(ystate.type, new Set([null, ySyncPluginKey]));
return {
undoManager,
prevSel: null
}
},
apply: (tr, val, oldState, state) => {
const binding = ySyncPluginKey.getState(state).binding;
if (binding) {
return {
undoManager: val.undoManager,
prevSel: getRelativeSelection(binding, oldState)
}
} else {
return val
}
}
},
view: view => {
const ystate = ySyncPluginKey.getState(view.state);
const undoManager = yUndoPluginKey.getState(view.state).undoManager;
undoManager.on('stack-item-added', ({ stackItem }) => {
const binding = ystate.binding;
if (binding) {
stackItem.meta.set(binding, yUndoPluginKey.getState(view.state).prevSel);
}
});
undoManager.on('stack-item-popped', ({ stackItem }) => {
const binding = ystate.binding;
if (binding) {
binding.beforeTransactionSelection = stackItem.meta.get(binding) || binding.beforeTransactionSelection;
}
});
return {
destroy: () => {
undoManager.destroy();
}
}
}
});
exports.ProsemirrorBinding = ProsemirrorBinding;
exports.absolutePositionToRelativePosition = absolutePositionToRelativePosition;
exports.createNodeFromYElement = createNodeFromYElement;

@@ -809,8 +890,12 @@ exports.createNodeIfNotExists = createNodeIfNotExists;

exports.createTypeFromNode = createTypeFromNode;
exports.cursorPlugin = cursorPlugin;
exports.cursorPluginKey = cursorPluginKey;
exports.getRelativeSelection = getRelativeSelection;
exports.isVisible = isVisible;
exports.prosemirrorPlugin = prosemirrorPlugin;
exports.prosemirrorPluginKey = prosemirrorPluginKey;
exports.relativePositionToAbsolutePosition = relativePositionToAbsolutePosition;
exports.redo = redo;
exports.undo = undo;
exports.yCursorPlugin = yCursorPlugin;
exports.yCursorPluginKey = yCursorPluginKey;
exports.ySyncPlugin = ySyncPlugin;
exports.ySyncPluginKey = ySyncPluginKey;
exports.yUndoPlugin = yUndoPlugin;
exports.yUndoPluginKey = yUndoPluginKey;
//# sourceMappingURL=y-prosemirror.js.map
{
"name": "y-prosemirror",
"version": "0.0.5",
"version": "0.0.6",
"description": "Prosemirror bindings for Yjs",

@@ -44,3 +44,3 @@ "main": "./dist/y-prosemirror.js",

"peerDependencies": {
"yjs": ">=13.0.0-90",
"yjs": ">=13.0.0-93",
"y-protocols": ">=0.0.6",

@@ -53,15 +53,15 @@ "prosemirror-model": "^1.7.0",

"concurrently": "^4.1.0",
"jsdom": "^15.0.0",
"jsdom": "^15.1.1",
"live-server": "^1.2.1",
"prosemirror-model": "^1.7.0",
"prosemirror-model": "^1.7.1",
"prosemirror-schema-basic": "^1.0.1",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.7.1",
"rollup": "^1.10.1",
"prosemirror-state": "^1.2.3",
"prosemirror-view": "^1.9.10",
"rollup": "^1.16.2",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-node-resolve": "^4.2.3",
"rollup-plugin-node-resolve": "^4.2.4",
"standard": "^12.0.1",
"y-protocols": "0.0.6",
"yjs": "13.0.0-90"
"yjs": "13.0.0-93"
}
}

@@ -8,3 +8,5 @@ # y-prosemirror

* Sync ProseMirror state
* Shared Cursors
* Shared Undo / Redo (each client has its own undo-/redo-history)
* Successfully recovers when concurrents edit result in an invalid document schema

@@ -15,4 +17,5 @@

```js
import { prosemirrorPlugin, cursorPlugin } from 'y-prosemirror'
import { ySyncPlugin, yCursorPlugin, yUndoPlugin, undo, redo } from 'y-prosemirror'
import { exampleSetup } from 'prosemirror-example-setup'
import { keymap } from 'prosemirror-keymap'
..

@@ -25,3 +28,12 @@

schema,
plugins: exampleSetup({ schema }).concat([prosemirrorPlugin(type), cursorPlugin])
plugins: [
ySyncPlugin(type),
yCursorPlugin(provider.awareness),
yUndoPlugin,
keymap({
'Mod-z': undo,
'Mod-y': redo,
'Mod-Shift-z': redo
})
].concat(exampleSetup({ schema }))
})

@@ -28,0 +40,0 @@ })

@@ -1,787 +0,3 @@

/**
* @module bindings/prosemirror
*/
import { createMutex } from 'lib0/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 'lib0/math.js'
import * as object from 'lib0/object.js'
import { simpleDiff } from 'lib0/diff.js'
import * as error from 'lib0/error.js'
import * as Y from 'yjs'
import { Awareness } from 'y-protocols/awareness.js' // eslint-disable-line
export const isVisible = (item, snapshot) => snapshot === undefined ? !item._deleted : (snapshot.sm.has(item._id.user) && snapshot.sm.get(item._id.user) > item._id.clock && !snapshot.ds.isDeleted(item._id))
/**
* @typedef {Map<Y.AbstractType, Object>} 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 {Y.XmlFragment} yXmlFragment
* @return {Plugin} Returns a prosemirror plugin that binds to this type
*/
export const prosemirrorPlugin = (yXmlFragment) => {
let changedInitialContent = false
const plugin = new Plugin({
props: {
editable: (state) => prosemirrorPluginKey.getState(state).snapshot == null
},
key: prosemirrorPluginKey,
state: {
init: (initargs, state) => {
return {
type: yXmlFragment,
doc: yXmlFragment.doc,
binding: null,
snapshot: null,
isChangeOrigin: false
}
},
apply: (tr, pluginState) => {
const change = tr.getMeta(prosemirrorPluginKey)
if (change !== undefined) {
pluginState = Object.assign({}, pluginState)
for (let key in change) {
pluginState[key] = change[key]
}
}
// always set isChangeOrigin. If undefined, this is not change origin.
pluginState.isChangeOrigin = change !== undefined && !!change.isChangeOrigin
if (pluginState.binding !== null) {
if (change !== undefined && change.snapshot !== undefined) {
// snapshot changed, rerender next
setTimeout(() => {
if (change.restore == null) {
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot)
} else {
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot)
// reset to current prosemirror state
delete pluginState.restore
delete pluginState.snapshot
delete pluginState.prevSnapshot
pluginState.binding._prosemirrorChanged(pluginState.binding.prosemirrorView.state.doc)
}
}, 0)
}
}
return pluginState
}
},
view: view => {
const binding = new ProsemirrorBinding(yXmlFragment, view)
view.dispatch(view.state.tr.setMeta(prosemirrorPluginKey, { binding }))
return {
update: () => {
const pluginState = plugin.getState(view.state)
if (pluginState.snapshot == null) {
if (changedInitialContent || view.state.doc.content.size > 2) {
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
* @param {Awareness} awareness
* @return {Plugin}
*/
export const cursorPlugin = awareness => new Plugin({
key: cursorPluginKey,
props: {
decorations: state => {
const ystate = prosemirrorPluginKey.getState(state)
const y = ystate.doc
const decorations = []
if (ystate.snapshot != null || ystate.binding === null) {
// do not render cursors while snapshot is active
return
}
awareness.getStates().forEach((aw, clientId) => {
if (clientId === y.clientID) {
return
}
if (aw.cursor != null) {
let user = aw.user || {}
if (user.color == null) {
user.color = '#ffa500'
}
if (user.name == null) {
user.name = `User: ${clientId}`
}
let anchor = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.anchor), ystate.binding.mapping)
let head = relativePositionToAbsolutePosition(y, ystate.type, Y.createRelativePositionFromJSON(aw.cursor.head), ystate.binding.mapping)
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: clientId + '' }))
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 awarenessListener = () => {
view.updateState(view.state)
}
const updateCursorInfo = () => {
const current = awareness.getLocalState() || {}
if (view.hasFocus() && ystate.binding !== null) {
/**
* @type {Y.RelativePosition}
*/
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
/**
* @type {Y.RelativePosition}
*/
const head = absolutePositionToRelativePosition(view.state.selection.head, ystate.type, ystate.binding.mapping)
if (current.cursor == null || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.anchor), anchor) || !Y.compareRelativePositions(Y.createRelativePositionFromJSON(current.cursor.head), head)) {
awareness.setLocalStateField('cursor', {
anchor, head
})
}
} else if (current.cursor !== null) {
awareness.setLocalStateField('cursor', null)
}
}
awareness.on('change', awarenessListener)
view.dom.addEventListener('focusin', updateCursorInfo)
view.dom.addEventListener('focusout', updateCursorInfo)
return {
update: updateCursorInfo,
destroy: () => {
const y = prosemirrorPluginKey.getState(view.state).doc
y.setAwarenessField('cursor', null)
y.off('change', awarenessListener)
}
}
}
})
/**
* Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model).
*
* @param {number} pos
* @param {Y.XmlFragment} type
* @param {ProsemirrorMapping} mapping
* @return {any} relative position
*/
export const absolutePositionToRelativePosition = (pos, type, mapping) => {
if (pos === 0) {
return Y.createRelativePositionFromTypeIndex(type, 0)
}
let n = type._first === null ? null : /** @type {Y.ContentType} */ (type._first.content).type
while (n !== null && type !== n) {
const pNodeSize = (mapping.get(n) || { nodeSize: 0 }).nodeSize
if (n.constructor === Y.XmlText) {
if (n._length >= pos) {
return Y.createRelativePositionFromTypeIndex(n, pos)
} else {
pos -= n._length
}
if (n._item !== null && n._item.next !== null) {
n = /** @type {Y.ContentType} */ (n._item.next.content).type
} else {
do {
n = n._item === null ? null : n._item.parent
pos--
} while (n !== type && n !== null && n._item !== null && n._item.next === null)
if (n !== null && n !== type) {
// @ts-gnore we know that n.next !== null because of above loop conditition
n = n._item === null ? null : /** @type {Y.ContentType} */ (/** @type Y.Item */ (n._item.next).content).type
}
}
} else if (n._first !== null && pos < pNodeSize) {
n = /** @type {Y.ContentType} */ (n._first.content).type
pos--
} else {
if (pos === 1 && n._length === 0 && pNodeSize > 1) {
// edge case, should end in this paragraph
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
}
pos -= pNodeSize
if (n._item !== null && n._item.next !== null) {
n = /** @type {Y.ContentType} */ (n._item.next.content).type
} else {
if (pos === 0) {
// set to end of n.parent
n = n._item === null ? n : n._item.parent
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
}
do {
n = /** @type {Y.Item} */ (n._item).parent
pos--
} while (n !== type && /** @type {Y.Item} */ (n._item).next === null)
// if n is null at this point, we have an unexpected case
if (n !== type) {
// We know that n._item.next is defined because of above loop condition
n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type
}
}
}
if (n === null) {
throw error.unexpectedCase()
}
if (pos === 0 && n.constructor !== Y.XmlText && n !== type) { // TODO: set to <= 0
return new Y.RelativePosition(n._item === null ? null : n._item.id, n._item === null ? Y.findRootTypeKey(n) : null, null)
}
}
return Y.createRelativePositionFromTypeIndex(type, type._length)
}
/**
* @param {Y.Doc} y
* @param {Y.XmlFragment} yDoc Top level type that is bound to pView
* @param {any} relPos Encoded Yjs based relative position
* @param {ProsemirrorMapping} mapping
*/
export const relativePositionToAbsolutePosition = (y, yDoc, relPos, mapping) => {
const decodedPos = Y.createAbsolutePositionFromRelativePosition(relPos, y)
if (decodedPos === null) {
return null
}
let type = decodedPos.type
let pos = 0
if (type.constructor === Y.XmlText) {
pos = decodedPos.index
} else if (type._item === null || !type._item.deleted) {
let n = type._first
let i = 0
while (i < type._length && i < decodedPos.index && n !== null) {
i++
const t = /** @type {Y.ContentType} */ (n.content).type
if (t.constructor === Y.XmlText) {
pos += t._length
} else {
pos += mapping.get(t).nodeSize
}
n = /** @type {Y.Item} */ (n.next)
}
pos += 1 // increase because we go out of n
}
while (type !== yDoc) {
// @ts-ignore
const parent = type._item.parent
// @ts-ignore
if (parent._item === null || !parent._item.deleted) {
pos += 1 // the start tag
let n = parent._first
// now iterate until we found type
while (n !== null) {
const contentType = /** @type {Y.ContentType} */ (n.content).type
if (contentType === type) {
break
}
if (contentType.constructor === Y.XmlText) {
pos += contentType._length
} else {
pos += mapping.get(contentType).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 {Y.XmlFragment} 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)
/**
* @type {Y.Doc}
*/
// @ts-ignore
this.doc = yXmlFragment.doc
/**
* current selection as relative positions in the Yjs model
*/
this._relSelection = null
this.doc.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)
}
_forceRerender () {
this.mapping = new Map()
this.mux(() => {
const fragmentContent = this.type.toArray().map(t => createNodeFromYElement(/** @type {Y.XmlElement} */ (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))
this.prosemirrorView.dispatch(tr)
})
}
/**
*
* @param {Y.Snapshot} snapshot
* @param {Y.Snapshot} prevSnapshot
*/
_renderSnapshot (snapshot, prevSnapshot) {
// clear mapping because we are going to rerender
this.mapping = new Map()
this.mux(() => {
const fragmentContent = Y.typeListToArraySnapshot(this.type, new Y.Snapshot(prevSnapshot.ds, snapshot.sm)).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot, prevSnapshot)).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))
this.prosemirrorView.dispatch(tr)
})
}
/**
* @param {Array<Y.YEvent>} events
* @param {Y.Transaction} transaction
*/
_typeChanged (events, transaction) {
if (events.length === 0 || prosemirrorPluginKey.getState(this.prosemirrorView.state).snapshot != null) {
// drop out if snapshot is active
return
}
this.mux(() => {
const delStruct = (_, struct) => this.mapping.delete(struct)
Y.iterateDeletedStructs(transaction.deleteSet, this.doc.store, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.Item} */ (struct).content.type))
transaction.changed.forEach(delStruct)
transaction.changedParentTypes.forEach(delStruct)
const fragmentContent = this.type.toArray().map(t => createNodeIfNotExists(/** @type {Y.XmlElement | Y.XmlHook} */ (t), this.prosemirrorView.state.schema, this.mapping)).filter(n => n !== null)
let 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.doc, this.type, relSel.anchor, this.mapping)
const head = relativePositionToAbsolutePosition(this.doc, this.type, relSel.head, this.mapping)
if (anchor !== null && head !== null) {
tr = tr.setSelection(TextSelection.create(tr.doc, anchor, head))
}
}
tr = tr.setMeta(prosemirrorPluginKey, { isChangeOrigin: true })
this.prosemirrorView.dispatch(tr)
})
}
_prosemirrorChanged (doc) {
this.mux(() => {
updateYFragment(this.doc, this.type, doc.content, this.mapping)
})
}
destroy () {
this.type.unobserveDeep(this._observeFunction)
}
}
/**
* @private
* @param {Y.XmlElement | Y.XmlHook} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @param {Y.Snapshot} [snapshot]
* @param {Y.Snapshot} [prevSnapshot]
* @return {PModel.Node | null}
*/
export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => {
const node = mapping.get(el)
if (node === undefined) {
if (el instanceof Y.XmlElement) {
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot)
} else {
throw error.methodUnimplemented() // we are currently not handling hooks
}
}
return node
}
/**
* @private
* @param {Y.XmlElement} el
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @param {Y.Snapshot} [snapshot]
* @param {Y.Snapshot} [prevSnapshot]
* @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, snapshot, prevSnapshot) => {
let _snapshot = snapshot
let _prevSnapshot = prevSnapshot
if (snapshot !== undefined && prevSnapshot !== undefined) {
if (!isVisible(el, snapshot)) {
// if this element is already rendered as deleted (ychange), then do not render children as deleted
_snapshot = new Y.Snapshot(prevSnapshot.ds, snapshot.sm)
_prevSnapshot = _snapshot
} else if (!isVisible(el, prevSnapshot)) {
_prevSnapshot = _snapshot
}
}
const children = []
const createChildren = type => {
if (type.constructor === Y.XmlElement) {
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot)
if (n !== null) {
children.push(n)
}
} else {
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot)
if (ns !== null) {
ns.forEach(textchild => {
if (textchild !== null) {
children.push(textchild)
}
})
}
}
}
if (snapshot === undefined || prevSnapshot === undefined) {
el.toArray().forEach(createChildren)
} else {
Y.typeListToArraySnapshot(el, new Y.Snapshot(prevSnapshot.ds, snapshot.sm)).forEach(createChildren)
}
let node
try {
const attrs = el.getAttributes(_snapshot)
if (snapshot !== undefined) {
if (!isVisible(el, snapshot)) {
attrs.ychange = { client: /** @type {Y.Item} */ (el._item).id.client, state: 'removed' }
} else if (!isVisible(el, prevSnapshot)) {
attrs.ychange = { client: /** @type {Y.Item} */ (el._item).id.client, state: 'added' }
}
}
node = schema.node(el.nodeName, attrs, children)
} catch (e) {
// an error occured while creating the node. This is probably a result of a concurrent action.
/** @type {Y.Doc} */ (el.doc).transact(transaction => {
/** @type {Y.Item} */ (el._item).delete(transaction)
})
return null
}
mapping.set(el, node)
return node
}
/**
* @private
* @param {Y.XmlText} text
* @param {PModel.Schema} schema
* @param {ProsemirrorMapping} mapping
* @param {Y.Snapshot} [snapshot]
* @param {Y.Snapshot} [prevSnapshot]
* @return {Array<PModel.Node>|null}
*/
export const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot) => {
const nodes = []
const deltas = text.toDelta(snapshot, prevSnapshot)
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) {
// an error occured while creating the node. This is probably a result of a concurrent action.
/** @type {Y.Doc} */ (text.doc).transact(transaction => {
/** @type {Y.Item} */ (text._item).delete(transaction)
})
return null
}
// @ts-ignore
return nodes
}
/**
* @private
* @param {Object} node prosemirror node
* @param {ProsemirrorMapping} mapping
* @return {Y.XmlElement | Y.XmlText}
*/
export const createTypeFromNode = (node, mapping) => {
let type
if (node.isText) {
type = new Y.XmlText()
const attrs = {}
node.marks.forEach(mark => {
if (mark.type.name !== 'ychange') {
attrs[mark.type.name] = mark.attrs
}
})
type.insert(0, node.text, attrs)
} else {
type = new Y.XmlElement(node.type.name)
for (let key in node.attrs) {
const val = node.attrs[key]
if (val !== null && key !== 'ychange') {
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]
const l = pattrs[key]
const r = yattrs[key]
eq = key === 'ychange' || l === r || (typeof l === 'object' && typeof r === 'object' && equalAttrs(l, r))
}
return eq
}
const equalYTextPText = (ytext, ptext) => {
const delta = ytext.toDelta()
if (delta.length === 0) {
return ptext.text === ''
}
const d = delta[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 === Y.XmlText
? 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 {Y.Doc} y
* @param {Y.XmlFragment} yDomFragment
* @param {Object} pContent
* @param {ProsemirrorMapping} mapping
*/
const updateYFragment = (y, yDomFragment, pContent, mapping) => {
if (yDomFragment instanceof Y.XmlElement && yDomFragment.nodeName !== pContent.type.name) {
throw new Error('node name mismatch!')
}
mapping.set(yDomFragment, pContent)
// update attributes
if (yDomFragment instanceof Y.XmlElement) {
const yDomAttrs = yDomFragment.getAttributes()
const pAttrs = pContent.attrs
for (let key in pAttrs) {
if (pAttrs[key] !== null) {
if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
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
}
}
}
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 instanceof Y.XmlText && leftP.isText) {
if (!equalYTextPText(leftY, leftP)) {
// try to apply diff. Only if attrs don't match, delete insert
// TODO: use a single ytext to hold all following Prosemirror Text nodes
const pattrs = {}
leftP.marks.forEach(mark => {
if (mark.type.name !== 'ychange') {
pattrs[mark.type.name] = mark.attrs
}
})
const delta = leftY.toDelta()
if (delta.length === 1 && delta[0].insert && equalAttrs(pattrs, delta[0].attributes || {})) {
const diff = simpleDiff(delta[0].insert, leftP.text)
leftY.delete(diff.index, diff.remove)
leftY.insert(diff.index, diff.insert, delta[0].attributes || {})
} else {
yDomFragment.delete(left, 1)
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
}
}
left += 1
} else {
let updateLeft = leftY instanceof Y.XmlElement && matchNodeName(leftY, leftP)
let updateRight = rightY instanceof Y.XmlElement && 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(y, /** @type {Y.XmlFragment} */ (leftY), leftP, mapping)
left += 1
} else if (updateRight) {
updateYFragment(y, /** @type {Y.XmlFragment} */ (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 {Y.XmlElement} yElement
* @param {any} pNode Prosemirror Node
*/
const matchNodeName = (yElement, pNode) => yElement.nodeName === pNode.type.name
export * from './plugins/cursor-plugin.js'
export * from './plugins/sync-plugin.js'
export * from './plugins/undo-plugin.js'

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc