y-prosemirror
Advanced tools
Comparing version 0.1.1 to 0.1.2
@@ -13,4 +13,6 @@ 'use strict'; | ||
var object = require('lib0/dist/object.js'); | ||
var set = require('lib0/dist/set.js'); | ||
var diff_js = require('lib0/dist/diff.js'); | ||
var error = require('lib0/dist/error.js'); | ||
var random = require('lib0/dist/random.js'); | ||
@@ -150,31 +152,2 @@ /** | ||
/* | ||
class PermanentUserData { | ||
/** | ||
* @param {Y.Doc} doc | ||
* @param {string} userid | ||
* | ||
constructor (doc, userid) { | ||
const users = doc.getMap('users') | ||
this.doc = doc | ||
this.users = users | ||
let user = users.get(userid) | ||
if (!user) { | ||
user = new Y.Map() | ||
const ids = new Y.Array() | ||
const ds = new Y.Array() | ||
ids.push([userid]) | ||
user.set('ids', ids) | ||
user.set('ds', ds) | ||
users.set(userid, user) | ||
} | ||
users.observe(event => { | ||
event.changes.added.forEach(item => { | ||
item.content.type | ||
}) | ||
}) | ||
} | ||
} | ||
*/ | ||
/** | ||
@@ -203,2 +176,39 @@ * @module bindings/prosemirror | ||
/** | ||
* @typedef {Object} ColorDef | ||
* @property {string} ColorDef.light | ||
* @property {string} ColorDef.dark | ||
*/ | ||
/** | ||
* @typedef {Object} YSyncOpts | ||
* @property {Array<ColorDef>} [YSyncOpts.colors] | ||
* @property {Map<string,ColorDef>} [YSyncOpts.colorMapping] | ||
* @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData] | ||
*/ | ||
/** | ||
* @type {Array<ColorDef>} | ||
*/ | ||
const defaultColors = [{ light: '#ecd44433', dark: '#ecd444' }]; | ||
/** | ||
* @param {Map<string,ColorDef>} colorMapping | ||
* @param {Array<ColorDef>} colors | ||
* @param {string} user | ||
* @return {ColorDef} | ||
*/ | ||
const getUserColor = (colorMapping, colors, user) => { | ||
// @todo do not hit the same color twice if possible | ||
if (!colorMapping.has(user)) { | ||
if (colorMapping.size < colors.length) { | ||
const usedColors = set.create(); | ||
colorMapping.forEach(color => usedColors.add(color)); | ||
colors = colors.filter(color => !usedColors.has(color)); | ||
} | ||
colorMapping.set(user, random.oneOf(colors)); | ||
} | ||
return /** @type {ColorDef} */ (colorMapping.get(user)) | ||
}; | ||
/** | ||
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync. | ||
@@ -208,5 +218,6 @@ * | ||
* @param {Y.XmlFragment} yXmlFragment | ||
* @param {YSyncOpts} opts | ||
* @return {Plugin} Returns a prosemirror plugin that binds to this type | ||
*/ | ||
const ySyncPlugin = yXmlFragment => { | ||
const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping = new Map(), permanentUserData = null } = {}) => { | ||
let changedInitialContent = false; | ||
@@ -226,3 +237,6 @@ const plugin = new prosemirrorState.Plugin({ | ||
prevSnapshot: null, | ||
isChangeOrigin: false | ||
isChangeOrigin: false, | ||
colors, | ||
colorMapping, | ||
permanentUserData | ||
} | ||
@@ -245,5 +259,5 @@ }, | ||
if (change.restore == null) { | ||
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot); | ||
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot, pluginState); | ||
} else { | ||
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot); | ||
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot, pluginState); | ||
// reset to current prosemirror state | ||
@@ -370,10 +384,30 @@ delete pluginState.restore; | ||
* @param {Y.Snapshot} prevSnapshot | ||
* @param {Object} pluginState | ||
*/ | ||
_renderSnapshot (snapshot, prevSnapshot) { | ||
_renderSnapshot (snapshot, prevSnapshot, pluginState) { | ||
// 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.sv)).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); | ||
this.doc.transact(transaction => { | ||
// before rendering, we are going to sanitize ops and split deleted ops | ||
// if they were deleted by seperate users. | ||
const pud = pluginState.permanentUserData; | ||
if (pud) { | ||
pud.dss.forEach(ds => { | ||
Y.iterateDeletedStructs(transaction, ds, item => {}); | ||
}); | ||
} | ||
const computeYChange = (type, id) => { | ||
const user = type === 'added' ? pud.getUserByClientId(id.client) : pud.getUserByDeletedId(id); | ||
return { | ||
user, | ||
type, | ||
color: getUserColor(pluginState.colorMapping, pluginState.colors, user) | ||
} | ||
}; | ||
// Create document fragment and render | ||
const fragmentContent = Y.typeListToArraySnapshot(this.type, new Y.Snapshot(prevSnapshot.ds, snapshot.sv)).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot, prevSnapshot, computeYChange)).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); | ||
}); | ||
}); | ||
@@ -396,3 +430,3 @@ } | ||
const delType = (_, type) => this.mapping.delete(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)); | ||
Y.iterateDeletedStructs(transaction, transaction.deleteSet, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type)); | ||
transaction.changed.forEach(delType); | ||
@@ -430,9 +464,10 @@ transaction.changedParentTypes.forEach(delType); | ||
* @param {Y.Snapshot} [prevSnapshot] | ||
* @param {function('removed' | 'added', Y.ID):any} [computeYChange] | ||
* @return {PModel.Node | null} | ||
*/ | ||
const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => { | ||
const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot, computeYChange) => { | ||
const node = /** @type {PModel.Node} */ (mapping.get(el)); | ||
if (node === undefined) { | ||
if (el instanceof Y.XmlElement) { | ||
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot) | ||
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot, computeYChange) | ||
} else { | ||
@@ -452,5 +487,6 @@ throw error.methodUnimplemented() // we are currently not handling hooks | ||
* @param {Y.Snapshot} [prevSnapshot] | ||
* @param {function('removed' | 'added', Y.ID):any} [computeYChange] | ||
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null | ||
*/ | ||
const createNodeFromYElement = (el, schema, mapping, snapshot, prevSnapshot) => { | ||
const createNodeFromYElement = (el, schema, mapping, snapshot, prevSnapshot, computeYChange) => { | ||
let _snapshot = snapshot; | ||
@@ -470,3 +506,3 @@ let _prevSnapshot = prevSnapshot; | ||
if (type.constructor === Y.XmlElement) { | ||
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot); | ||
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot, computeYChange); | ||
if (n !== null) { | ||
@@ -476,3 +512,3 @@ children.push(n); | ||
} else { | ||
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot); | ||
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot, computeYChange); | ||
if (ns !== null) { | ||
@@ -496,5 +532,5 @@ ns.forEach(textchild => { | ||
if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) { | ||
attrs.ychange = { user: /** @type {Y.Item} */ (el._item).id.client, state: 'removed' }; | ||
attrs.ychange = computeYChange ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id) : { type: 'removed' }; | ||
} else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) { | ||
attrs.ychange = { user: /** @type {Y.Item} */ (el._item).id.client, state: 'added' }; | ||
attrs.ychange = computeYChange ? computeYChange('added', /** @type {Y.Item} */ (el._item).id) : { type: 'added' }; | ||
} | ||
@@ -522,7 +558,8 @@ } | ||
* @param {Y.Snapshot} [prevSnapshot] | ||
* @param {function('removed' | 'added', Y.ID):any} [computeYChange] | ||
* @return {Array<PModel.Node>|null} | ||
*/ | ||
const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot) => { | ||
const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot, computeYChange) => { | ||
const nodes = []; | ||
const deltas = text.toDelta(snapshot, prevSnapshot); | ||
const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange); | ||
try { | ||
@@ -529,0 +566,0 @@ for (let i = 0; i < deltas.length; i++) { |
{ | ||
"name": "y-prosemirror", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "Prosemirror bindings for Yjs", | ||
@@ -41,6 +41,6 @@ "main": "./dist/y-prosemirror.js", | ||
"dependencies": { | ||
"lib0": "^0.1.0" | ||
"lib0": "^0.1.1" | ||
}, | ||
"peerDependencies": { | ||
"yjs": ">=13.0.0-97", | ||
"yjs": ">=13.0.0-98", | ||
"y-protocols": "^0.1.0", | ||
@@ -64,4 +64,4 @@ "prosemirror-model": "^1.7.0", | ||
"y-protocols": "^0.1.0", | ||
"yjs": "13.0.0-97" | ||
"yjs": "13.0.0-98" | ||
} | ||
} |
@@ -139,30 +139,1 @@ import { ProsemirrorMapping } from './plugins/sync-plugin.js' // eslint-disable-line | ||
} | ||
/* | ||
class PermanentUserData { | ||
/** | ||
* @param {Y.Doc} doc | ||
* @param {string} userid | ||
* | ||
constructor (doc, userid) { | ||
const users = doc.getMap('users') | ||
this.doc = doc | ||
this.users = users | ||
let user = users.get(userid) | ||
if (!user) { | ||
user = new Y.Map() | ||
const ids = new Y.Array() | ||
const ds = new Y.Array() | ||
ids.push([userid]) | ||
user.set('ids', ids) | ||
user.set('ds', ds) | ||
users.set(userid, user) | ||
} | ||
users.observe(event => { | ||
event.changes.added.forEach(item => { | ||
item.content.type | ||
}) | ||
}) | ||
} | ||
} | ||
*/ |
@@ -11,2 +11,3 @@ /** | ||
import * as object from 'lib0/object.js' | ||
import * as set from 'lib0/set.js' | ||
import { simpleDiff } from 'lib0/diff.js' | ||
@@ -16,2 +17,3 @@ import * as error from 'lib0/error.js' | ||
import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition } from '../lib.js' | ||
import * as random from 'lib0/random.js' | ||
@@ -37,2 +39,39 @@ /** | ||
/** | ||
* @typedef {Object} ColorDef | ||
* @property {string} ColorDef.light | ||
* @property {string} ColorDef.dark | ||
*/ | ||
/** | ||
* @typedef {Object} YSyncOpts | ||
* @property {Array<ColorDef>} [YSyncOpts.colors] | ||
* @property {Map<string,ColorDef>} [YSyncOpts.colorMapping] | ||
* @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData] | ||
*/ | ||
/** | ||
* @type {Array<ColorDef>} | ||
*/ | ||
const defaultColors = [{ light: '#ecd44433', dark: '#ecd444' }] | ||
/** | ||
* @param {Map<string,ColorDef>} colorMapping | ||
* @param {Array<ColorDef>} colors | ||
* @param {string} user | ||
* @return {ColorDef} | ||
*/ | ||
const getUserColor = (colorMapping, colors, user) => { | ||
// @todo do not hit the same color twice if possible | ||
if (!colorMapping.has(user)) { | ||
if (colorMapping.size < colors.length) { | ||
const usedColors = set.create() | ||
colorMapping.forEach(color => usedColors.add(color)) | ||
colors = colors.filter(color => !usedColors.has(color)) | ||
} | ||
colorMapping.set(user, random.oneOf(colors)) | ||
} | ||
return /** @type {ColorDef} */ (colorMapping.get(user)) | ||
} | ||
/** | ||
* This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync. | ||
@@ -42,5 +81,6 @@ * | ||
* @param {Y.XmlFragment} yXmlFragment | ||
* @param {YSyncOpts} opts | ||
* @return {Plugin} Returns a prosemirror plugin that binds to this type | ||
*/ | ||
export const ySyncPlugin = yXmlFragment => { | ||
export const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping = new Map(), permanentUserData = null } = {}) => { | ||
let changedInitialContent = false | ||
@@ -60,3 +100,6 @@ const plugin = new Plugin({ | ||
prevSnapshot: null, | ||
isChangeOrigin: false | ||
isChangeOrigin: false, | ||
colors, | ||
colorMapping, | ||
permanentUserData | ||
} | ||
@@ -79,5 +122,5 @@ }, | ||
if (change.restore == null) { | ||
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot) | ||
pluginState.binding._renderSnapshot(change.snapshot, change.prevSnapshot, pluginState) | ||
} else { | ||
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot) | ||
pluginState.binding._renderSnapshot(change.snapshot, change.snapshot, pluginState) | ||
// reset to current prosemirror state | ||
@@ -204,10 +247,30 @@ delete pluginState.restore | ||
* @param {Y.Snapshot} prevSnapshot | ||
* @param {Object} pluginState | ||
*/ | ||
_renderSnapshot (snapshot, prevSnapshot) { | ||
_renderSnapshot (snapshot, prevSnapshot, pluginState) { | ||
// 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.sv)).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) | ||
this.doc.transact(transaction => { | ||
// before rendering, we are going to sanitize ops and split deleted ops | ||
// if they were deleted by seperate users. | ||
const pud = pluginState.permanentUserData | ||
if (pud) { | ||
pud.dss.forEach(ds => { | ||
Y.iterateDeletedStructs(transaction, ds, item => {}) | ||
}) | ||
} | ||
const computeYChange = (type, id) => { | ||
const user = type === 'added' ? pud.getUserByClientId(id.client) : pud.getUserByDeletedId(id) | ||
return { | ||
user, | ||
type, | ||
color: getUserColor(pluginState.colorMapping, pluginState.colors, user) | ||
} | ||
} | ||
// Create document fragment and render | ||
const fragmentContent = Y.typeListToArraySnapshot(this.type, new Y.Snapshot(prevSnapshot.ds, snapshot.sv)).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot, prevSnapshot, computeYChange)).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) | ||
}) | ||
}) | ||
@@ -230,3 +293,3 @@ } | ||
const delType = (_, type) => this.mapping.delete(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)) | ||
Y.iterateDeletedStructs(transaction, transaction.deleteSet, struct => struct.constructor === Y.Item && this.mapping.delete(/** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type)) | ||
transaction.changed.forEach(delType) | ||
@@ -264,9 +327,10 @@ transaction.changedParentTypes.forEach(delType) | ||
* @param {Y.Snapshot} [prevSnapshot] | ||
* @param {function('removed' | 'added', Y.ID):any} [computeYChange] | ||
* @return {PModel.Node | null} | ||
*/ | ||
export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => { | ||
export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot, computeYChange) => { | ||
const node = /** @type {PModel.Node} */ (mapping.get(el)) | ||
if (node === undefined) { | ||
if (el instanceof Y.XmlElement) { | ||
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot) | ||
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot, computeYChange) | ||
} else { | ||
@@ -286,5 +350,6 @@ throw error.methodUnimplemented() // we are currently not handling hooks | ||
* @param {Y.Snapshot} [prevSnapshot] | ||
* @param {function('removed' | 'added', Y.ID):any} [computeYChange] | ||
* @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) => { | ||
export const createNodeFromYElement = (el, schema, mapping, snapshot, prevSnapshot, computeYChange) => { | ||
let _snapshot = snapshot | ||
@@ -304,3 +369,3 @@ let _prevSnapshot = prevSnapshot | ||
if (type.constructor === Y.XmlElement) { | ||
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot) | ||
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot, computeYChange) | ||
if (n !== null) { | ||
@@ -310,3 +375,3 @@ children.push(n) | ||
} else { | ||
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot) | ||
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot, computeYChange) | ||
if (ns !== null) { | ||
@@ -330,5 +395,5 @@ ns.forEach(textchild => { | ||
if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) { | ||
attrs.ychange = { user: /** @type {Y.Item} */ (el._item).id.client, state: 'removed' } | ||
attrs.ychange = computeYChange ? computeYChange('removed', /** @type {Y.Item} */ (el._item).id) : { type: 'removed' } | ||
} else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) { | ||
attrs.ychange = { user: /** @type {Y.Item} */ (el._item).id.client, state: 'added' } | ||
attrs.ychange = computeYChange ? computeYChange('added', /** @type {Y.Item} */ (el._item).id) : { type: 'added' } | ||
} | ||
@@ -356,7 +421,8 @@ } | ||
* @param {Y.Snapshot} [prevSnapshot] | ||
* @param {function('removed' | 'added', Y.ID):any} [computeYChange] | ||
* @return {Array<PModel.Node>|null} | ||
*/ | ||
export const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot) => { | ||
export const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot, computeYChange) => { | ||
const nodes = [] | ||
const deltas = text.toDelta(snapshot, prevSnapshot) | ||
const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange) | ||
try { | ||
@@ -363,0 +429,0 @@ for (let i = 0; i < deltas.length; i++) { |
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
2411577
21811
Updatedlib0@^0.1.1