added utilities to make and recover snapshots
This commit is contained in:
parent
77e479c03b
commit
3a0694c35c
@ -11,6 +11,8 @@ import { Plugin, PluginKey, EditorState, TextSelection } from 'prosemirror-state
|
|||||||
import * as math from '../lib/math.js'
|
import * as math from '../lib/math.js'
|
||||||
import * as object from '../lib/object.js'
|
import * as object from '../lib/object.js'
|
||||||
import * as YPos from '../utils/relativePosition.js'
|
import * as YPos from '../utils/relativePosition.js'
|
||||||
|
import { isVisible } from '../utils/snapshot.js'
|
||||||
|
import { simpleDiff } from '../lib/diff.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
* @typedef {Map<YText | YXmlElement | YXmlFragment, PModel.Node>} ProsemirrorMapping
|
||||||
@ -33,6 +35,9 @@ export const prosemirrorPluginKey = new PluginKey('yjs')
|
|||||||
export const prosemirrorPlugin = yXmlFragment => {
|
export const prosemirrorPlugin = yXmlFragment => {
|
||||||
let changedInitialContent = false
|
let changedInitialContent = false
|
||||||
const plugin = new Plugin({
|
const plugin = new Plugin({
|
||||||
|
props: {
|
||||||
|
editable: (state) => prosemirrorPluginKey.getState(state).snapshot == null
|
||||||
|
},
|
||||||
key: prosemirrorPluginKey,
|
key: prosemirrorPluginKey,
|
||||||
state: {
|
state: {
|
||||||
init: (initargs, state) => {
|
init: (initargs, state) => {
|
||||||
@ -55,7 +60,16 @@ export const prosemirrorPlugin = yXmlFragment => {
|
|||||||
if (change !== undefined && change.snapshot !== undefined) {
|
if (change !== undefined && change.snapshot !== undefined) {
|
||||||
// snapshot changed, rerender next
|
// snapshot changed, rerender next
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
pluginState.binding._renderSnapshot(change.snapshot)
|
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)
|
}, 0)
|
||||||
} else if (pluginState.snapshot == null) {
|
} else if (pluginState.snapshot == null) {
|
||||||
// only apply if no snapshot active
|
// only apply if no snapshot active
|
||||||
@ -112,7 +126,14 @@ export const cursorPlugin = new Plugin({
|
|||||||
const y = ystate.y
|
const y = ystate.y
|
||||||
const awareness = y.getAwarenessInfo()
|
const awareness = y.getAwarenessInfo()
|
||||||
const decorations = []
|
const decorations = []
|
||||||
|
if (ystate.snapshot != null) {
|
||||||
|
// do not render cursors while snapshot is active
|
||||||
|
return
|
||||||
|
}
|
||||||
awareness.forEach((aw, userID) => {
|
awareness.forEach((aw, userID) => {
|
||||||
|
if (userID === y.userID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (aw.cursor != null) {
|
if (aw.cursor != null) {
|
||||||
let user = aw.user || {}
|
let user = aw.user || {}
|
||||||
if (user.color == null) {
|
if (user.color == null) {
|
||||||
@ -154,7 +175,7 @@ export const cursorPlugin = new Plugin({
|
|||||||
}
|
}
|
||||||
const updateCursorInfo = () => {
|
const updateCursorInfo = () => {
|
||||||
const current = y.getLocalAwarenessInfo()
|
const current = y.getLocalAwarenessInfo()
|
||||||
if (view.hasFocus()) {
|
if (view.hasFocus() && ystate.binding !== null) {
|
||||||
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
|
const anchor = absolutePositionToRelativePosition(view.state.selection.anchor, ystate.type, ystate.binding.mapping)
|
||||||
const head = absolutePositionToRelativePosition(view.state.selection.head, 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)) {
|
if (current.cursor == null || !YPos.equal(current.cursor.anchor, anchor) || !YPos.equal(current.cursor.head, head)) {
|
||||||
@ -321,11 +342,24 @@ export class ProsemirrorBinding {
|
|||||||
})
|
})
|
||||||
yXmlFragment.observeDeep(this._observeFunction)
|
yXmlFragment.observeDeep(this._observeFunction)
|
||||||
}
|
}
|
||||||
_renderSnapshot (snapshot) {
|
_forceRerender () {
|
||||||
|
this.mapping = new Map()
|
||||||
|
this.mux(() => {
|
||||||
|
const fragmentContent = this.type.toArray().map(t => createNodeFromYElement(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 {*} snapshot
|
||||||
|
* @param {*} prevSnapshot
|
||||||
|
*/
|
||||||
|
_renderSnapshot (snapshot, prevSnapshot) {
|
||||||
// clear mapping because we are going to rerender
|
// clear mapping because we are going to rerender
|
||||||
this.mapping = new Map()
|
this.mapping = new Map()
|
||||||
this.mux(() => {
|
this.mux(() => {
|
||||||
const fragmentContent = this.type.toArray(snapshot).map(t => createNodeFromYElement(t, this.prosemirrorView.state.schema, new Map(), snapshot)).filter(n => n !== null)
|
const fragmentContent = this.type.toArray({ sm: snapshot.sm, ds: prevSnapshot.ds}).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))
|
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.prosemirrorView.dispatch(tr)
|
||||||
})
|
})
|
||||||
@ -370,12 +404,13 @@ export class ProsemirrorBinding {
|
|||||||
* @param {PModel.Schema} schema
|
* @param {PModel.Schema} schema
|
||||||
* @param {ProsemirrorMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
* @param {HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
|
* @param {HistorySnapshot} [prevSnapshot]
|
||||||
* @return {PModel.Node}
|
* @return {PModel.Node}
|
||||||
*/
|
*/
|
||||||
export const createNodeIfNotExists = (el, schema, mapping, snapshot) => {
|
export const createNodeIfNotExists = (el, schema, mapping, snapshot, prevSnapshot) => {
|
||||||
const node = mapping.get(el)
|
const node = mapping.get(el)
|
||||||
if (node === undefined) {
|
if (node === undefined) {
|
||||||
return createNodeFromYElement(el, schema, mapping, snapshot)
|
return createNodeFromYElement(el, schema, mapping, snapshot, prevSnapshot)
|
||||||
}
|
}
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
@ -385,19 +420,31 @@ export const createNodeIfNotExists = (el, schema, mapping, snapshot) => {
|
|||||||
* @param {YXmlElement} el
|
* @param {YXmlElement} el
|
||||||
* @param {PModel.Schema} schema
|
* @param {PModel.Schema} schema
|
||||||
* @param {ProsemirrorMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} snapshot
|
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
||||||
|
* @param {import('../protocols/history.js').HistorySnapshot} [prevSnapshot]
|
||||||
* @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null
|
* @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) => {
|
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 = {sm: snapshot.sm, ds: prevSnapshot.ds}
|
||||||
|
_prevSnapshot = _snapshot
|
||||||
|
} else if (!isVisible(el, prevSnapshot)) {
|
||||||
|
_prevSnapshot = _snapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
const children = []
|
const children = []
|
||||||
el.toArray(snapshot).forEach(type => {
|
const createChildren = type => {
|
||||||
if (type.constructor === YXmlElement) {
|
if (type.constructor === YXmlElement) {
|
||||||
const n = createNodeIfNotExists(type, schema, mapping, snapshot)
|
const n = createNodeIfNotExists(type, schema, mapping, _snapshot, _prevSnapshot)
|
||||||
if (n !== null) {
|
if (n !== null) {
|
||||||
children.push(n)
|
children.push(n)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const ns = createTextNodesFromYText(type, schema, mapping, snapshot)
|
const ns = createTextNodesFromYText(type, schema, mapping, _snapshot, _prevSnapshot)
|
||||||
if (ns !== null) {
|
if (ns !== null) {
|
||||||
ns.forEach(textchild => {
|
ns.forEach(textchild => {
|
||||||
if (textchild !== null) {
|
if (textchild !== null) {
|
||||||
@ -406,16 +453,31 @@ export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
if (snapshot === undefined || prevSnapshot === undefined) {
|
||||||
|
el.toArray().forEach(createChildren)
|
||||||
|
} else {
|
||||||
|
el.toArray({sm: snapshot.sm, ds: prevSnapshot.ds}).forEach(createChildren)
|
||||||
|
}
|
||||||
let node
|
let node
|
||||||
try {
|
try {
|
||||||
node = schema.node(el.nodeName.toLowerCase(), el.getAttributes(snapshot), children)
|
const attrs = el.getAttributes(_snapshot)
|
||||||
|
if (snapshot !== undefined) {
|
||||||
|
if (!isVisible(el, snapshot)) {
|
||||||
|
attrs.ychange = { user: el._id.user, state: 'removed' }
|
||||||
|
} else if (!isVisible(el, prevSnapshot)) {
|
||||||
|
attrs.ychange = { user: el._id.user, state: 'added' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
node = schema.node(el.nodeName.toLowerCase(), attrs, children)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// an error occured while creating the node. This is probably a result because of a concurrent action.
|
// 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
|
// ignore the node while rendering
|
||||||
|
/* do not delete anymore
|
||||||
el._y.transact(() => {
|
el._y.transact(() => {
|
||||||
el._delete(el._y, true)
|
el._delete(el._y, true)
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
mapping.set(el, node)
|
mapping.set(el, node)
|
||||||
@ -428,11 +490,12 @@ export const createNodeFromYElement = (el, schema, mapping, snapshot) => {
|
|||||||
* @param {PModel.Schema} schema
|
* @param {PModel.Schema} schema
|
||||||
* @param {ProsemirrorMapping} mapping
|
* @param {ProsemirrorMapping} mapping
|
||||||
* @param {HistorySnapshot} [snapshot]
|
* @param {HistorySnapshot} [snapshot]
|
||||||
|
* @param {HistorySnapshot} [prevSnapshot]
|
||||||
* @return {Array<PModel.Node>}
|
* @return {Array<PModel.Node>}
|
||||||
*/
|
*/
|
||||||
export const createTextNodesFromYText = (text, schema, mapping, snapshot) => {
|
export const createTextNodesFromYText = (text, schema, mapping, snapshot, prevSnapshot) => {
|
||||||
const nodes = []
|
const nodes = []
|
||||||
const deltas = text.toDelta(snapshot)
|
const deltas = text.toDelta(snapshot, prevSnapshot)
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < deltas.length; i++) {
|
for (let i = 0; i < deltas.length; i++) {
|
||||||
const delta = deltas[i]
|
const delta = deltas[i]
|
||||||
@ -446,9 +509,11 @@ export const createTextNodesFromYText = (text, schema, mapping, snapshot) => {
|
|||||||
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
mapping.set(text, nodes[0]) // only map to first child, all following children are also considered bound to this type
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
/*
|
||||||
text._y.transact(() => {
|
text._y.transact(() => {
|
||||||
text._delete(text._y, true)
|
text._delete(text._y, true)
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return nodes
|
return nodes
|
||||||
@ -465,13 +530,17 @@ export const createTypeFromNode = (node, mapping) => {
|
|||||||
if (node.isText) {
|
if (node.isText) {
|
||||||
type = new YText()
|
type = new YText()
|
||||||
const attrs = {}
|
const attrs = {}
|
||||||
node.marks.forEach(mark => { attrs[mark.type.name] = mark.attrs })
|
node.marks.forEach(mark => {
|
||||||
|
if (mark.type.name !== 'ychange') {
|
||||||
|
attrs[mark.type.name] = mark.attrs
|
||||||
|
}
|
||||||
|
})
|
||||||
type.insert(0, node.text, attrs)
|
type.insert(0, node.text, attrs)
|
||||||
} else {
|
} else {
|
||||||
type = new YXmlElement(node.type.name)
|
type = new YXmlElement(node.type.name)
|
||||||
for (let key in node.attrs) {
|
for (let key in node.attrs) {
|
||||||
const val = node.attrs[key]
|
const val = node.attrs[key]
|
||||||
if (val !== null) {
|
if (val !== null && key !== 'ychange') {
|
||||||
type.setAttribute(key, val)
|
type.setAttribute(key, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -490,7 +559,9 @@ const equalAttrs = (pattrs, yattrs) => {
|
|||||||
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
|
let eq = keys.length === Object.keys(yattrs).filter(key => yattrs[key] === null).length
|
||||||
for (let i = 0; i < keys.length && eq; i++) {
|
for (let i = 0; i < keys.length && eq; i++) {
|
||||||
const key = keys[i]
|
const key = keys[i]
|
||||||
eq = pattrs[key] === yattrs[key]
|
const l = pattrs[key]
|
||||||
|
const r = yattrs[key]
|
||||||
|
eq = key === 'ychange' || l === r || (typeof l === 'object' && typeof r === 'object' && equalAttrs(l, r))
|
||||||
}
|
}
|
||||||
return eq
|
return eq
|
||||||
}
|
}
|
||||||
@ -554,7 +625,7 @@ const updateYFragment = (yDomFragment, pContent, mapping) => {
|
|||||||
const pAttrs = pContent.attrs
|
const pAttrs = pContent.attrs
|
||||||
for (let key in pAttrs) {
|
for (let key in pAttrs) {
|
||||||
if (pAttrs[key] !== null) {
|
if (pAttrs[key] !== null) {
|
||||||
if (yDomAttrs[key] !== pAttrs[key]) {
|
if (yDomAttrs[key] !== pAttrs[key] && key !== 'ychange') {
|
||||||
yDomFragment.setAttribute(key, pAttrs[key])
|
yDomFragment.setAttribute(key, pAttrs[key])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -610,8 +681,23 @@ const updateYFragment = (yDomFragment, pContent, mapping) => {
|
|||||||
const rightP = pContent.child(pChildCnt - right - 1)
|
const rightP = pContent.child(pChildCnt - right - 1)
|
||||||
if (leftY.constructor === YText && leftP.isText) {
|
if (leftY.constructor === YText && leftP.isText) {
|
||||||
if (!equalYTextPText(leftY, leftP)) {
|
if (!equalYTextPText(leftY, leftP)) {
|
||||||
yDomFragment.delete(left, 1)
|
// try to apply diff. Only if attrs don't match, delete insert
|
||||||
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
// 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.pos, diff.remove)
|
||||||
|
leftY.insert(diff.pos, diff.insert)
|
||||||
|
} else {
|
||||||
|
yDomFragment.delete(left, 1)
|
||||||
|
yDomFragment.insert(left, [createTypeFromNode(leftP, mapping)])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
left += 1
|
left += 1
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import {Plugin} from "prosemirror-state"
|
import {Plugin} from 'prosemirror-state'
|
||||||
import crel from 'crel'
|
import crel from 'crel'
|
||||||
import * as Y from '../index.js'
|
import * as Y from '../index.js'
|
||||||
import { prosemirrorPluginKey } from '../bindings/prosemirror.js'
|
import { prosemirrorPluginKey } from '../bindings/prosemirror.js'
|
||||||
@ -7,21 +7,53 @@ import * as encoding from '../lib/encoding.js'
|
|||||||
import * as decoding from '../lib/decoding.js'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import * as historyProtocol from '../protocols/history.js'
|
import * as historyProtocol from '../protocols/history.js'
|
||||||
|
|
||||||
|
const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
|
||||||
|
|
||||||
|
const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
|
||||||
|
[ychange_state][ychange_user="${userid}"]:hover::before {
|
||||||
|
content: "${username}" !important;
|
||||||
|
background-color: ${color} !important;
|
||||||
|
}
|
||||||
|
[ychange_state="added"][ychange_user="${userid}"] {
|
||||||
|
background-color: ${color2} !important;
|
||||||
|
}
|
||||||
|
[ychange_state="removed"][ychange_user="${userid}"] {
|
||||||
|
color: ${color} !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export const noteHistoryPlugin = new Plugin({
|
export const noteHistoryPlugin = new Plugin({
|
||||||
view (editorView) { return new NoteHistoryPlugin(editorView) }
|
state: {
|
||||||
|
init (initargs, state) {
|
||||||
|
return new NoteHistoryPlugin()
|
||||||
|
},
|
||||||
|
apply (tr, pluginState) {
|
||||||
|
return pluginState
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view (editorView) {
|
||||||
|
const hstate = noteHistoryPlugin.getState(editorView.state)
|
||||||
|
hstate.init(editorView)
|
||||||
|
return {
|
||||||
|
destroy: hstate.destroy.bind(hstate)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const createWrapper = () => {
|
const createWrapper = () => {
|
||||||
const wrapper = crel('div', { style: 'display: flex' })
|
const wrapper = crel('div', { style: 'display: flex;' })
|
||||||
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
|
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
|
||||||
wrapper.insertBefore(historyContainer, null)
|
wrapper.insertBefore(historyContainer, null)
|
||||||
return { wrapper, historyContainer }
|
const userStyleContainer = crel('style')
|
||||||
|
wrapper.insertBefore(userStyleContainer, null)
|
||||||
|
return { wrapper, historyContainer, userStyleContainer }
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteHistoryPlugin {
|
class NoteHistoryPlugin {
|
||||||
constructor(editorView) {
|
init (editorView) {
|
||||||
this.editorView = editorView
|
this.editorView = editorView
|
||||||
const { historyContainer, wrapper } = createWrapper()
|
const { historyContainer, wrapper, userStyleContainer } = createWrapper()
|
||||||
|
this.userStyleContainer = userStyleContainer
|
||||||
this.wrapper = wrapper
|
this.wrapper = wrapper
|
||||||
this.historyContainer = historyContainer
|
this.historyContainer = historyContainer
|
||||||
const n = editorView.dom.parentNode.parentNode
|
const n = editorView.dom.parentNode.parentNode
|
||||||
@ -33,35 +65,94 @@ class NoteHistoryPlugin {
|
|||||||
const history = y.define('history', Y.Array)
|
const history = y.define('history', Y.Array)
|
||||||
history.observe(this.render.bind(this))
|
history.observe(this.render.bind(this))
|
||||||
}
|
}
|
||||||
|
destroy () {
|
||||||
|
this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
|
||||||
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||||
|
const history = y.define('history', Y.Array)
|
||||||
|
history.unobserve(this.render)
|
||||||
|
}
|
||||||
render () {
|
render () {
|
||||||
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||||
const history = y.define('history', Y.Array).toArray()
|
const history = y.define('history', Y.Array).toArray()
|
||||||
const fragment = document.createDocumentFragment()
|
const fragment = document.createDocumentFragment()
|
||||||
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
|
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
|
||||||
snapshotBtn.addEventListener('click', this.snapshot.bind(this))
|
|
||||||
fragment.insertBefore(snapshotBtn, null)
|
fragment.insertBefore(snapshotBtn, null)
|
||||||
|
let _prevSnap = null // empty
|
||||||
|
snapshotBtn.addEventListener('click', () => {
|
||||||
|
const awareness = y.getAwarenessInfo()
|
||||||
|
const userMap = new Map()
|
||||||
|
const aw = y.getLocalAwarenessInfo()
|
||||||
|
userMap.set(y.userID, aw.name || 'unknown')
|
||||||
|
awareness.forEach((a, userID) => {
|
||||||
|
userMap.set(userID, a.name || 'Unknown')
|
||||||
|
})
|
||||||
|
this.snapshot(userMap)
|
||||||
|
})
|
||||||
history.forEach(buf => {
|
history.forEach(buf => {
|
||||||
const decoder = decoding.createDecoder(buf)
|
const decoder = decoding.createDecoder(buf)
|
||||||
const snapshot = historyProtocol.readHistorySnapshot(decoder)
|
const snapshot = historyProtocol.readHistorySnapshot(decoder)
|
||||||
const date = new Date(decoding.readUint32(decoder) * 1000)
|
const date = new Date(decoding.readUint32(decoder) * 1000)
|
||||||
|
const restoreBtn = crel('button', { type: 'button' }, ['restore'])
|
||||||
const a = crel('a', [
|
const a = crel('a', [
|
||||||
'• '+ date.toUTCString()
|
'• ' + date.toUTCString(), restoreBtn
|
||||||
])
|
])
|
||||||
const el = crel('div', [ a ])
|
const el = crel('div', [ a ])
|
||||||
|
let prevSnapshot = _prevSnap // rebind to new variable
|
||||||
|
restoreBtn.addEventListener('click', event => {
|
||||||
|
if (prevSnapshot === null) {
|
||||||
|
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
|
||||||
|
}
|
||||||
|
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
|
||||||
|
event.stopPropagation()
|
||||||
|
})
|
||||||
a.addEventListener('click', () => {
|
a.addEventListener('click', () => {
|
||||||
console.log('setting snapshot')
|
console.log('setting snapshot')
|
||||||
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot }))
|
if (prevSnapshot === null) {
|
||||||
|
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
|
||||||
|
}
|
||||||
|
this.renderSnapshot(snapshot, prevSnapshot)
|
||||||
})
|
})
|
||||||
fragment.insertBefore(el, null)
|
fragment.insertBefore(el, null)
|
||||||
|
_prevSnap = snapshot
|
||||||
})
|
})
|
||||||
this.historyContainer.innerHTML = ''
|
this.historyContainer.innerHTML = ''
|
||||||
this.historyContainer.insertBefore(fragment, null)
|
this.historyContainer.insertBefore(fragment, null)
|
||||||
}
|
}
|
||||||
snapshot () {
|
renderSnapshot (snapshot, prevSnapshot) {
|
||||||
|
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
|
||||||
|
/**
|
||||||
|
* @type {Array<string|null>}
|
||||||
|
*/
|
||||||
|
let colors = niceColors.slice()
|
||||||
|
let style = ''
|
||||||
|
snapshot.userMap.forEach((name, userid) => {
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
|
||||||
|
let color = null
|
||||||
|
let i = 0
|
||||||
|
for (; i < colors.length && color === null; i++) {
|
||||||
|
color = colors[(randInt + i) % colors.length]
|
||||||
|
}
|
||||||
|
if (color === null) {
|
||||||
|
colors = niceColors.slice()
|
||||||
|
i = 0
|
||||||
|
color = colors[randInt % colors.length]
|
||||||
|
}
|
||||||
|
colors[randInt % colors.length] = null
|
||||||
|
style += createUserCSS(userid, name, color, color + '69')
|
||||||
|
})
|
||||||
|
this.userStyleContainer.innerHTML = style
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
|
||||||
|
*/
|
||||||
|
snapshot (updatedUserMap = new Map()) {
|
||||||
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
const y = prosemirrorPluginKey.getState(this.editorView.state).y
|
||||||
const history = y.define('history', Y.Array)
|
const history = y.define('history', Y.Array)
|
||||||
const encoder = encoding.createEncoder()
|
const encoder = encoding.createEncoder()
|
||||||
historyProtocol.writeHistorySnapshot(encoder, y)
|
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
|
||||||
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
|
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
|
||||||
history.push([encoding.toBuffer(encoder)])
|
history.push([encoding.toBuffer(encoder)])
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Yjs Prosemirror Example</title>
|
<title>Yjs Prosemirror Example</title>
|
||||||
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
|
<link rel=stylesheet href="./prosemirror.css">
|
||||||
<style>
|
<style>
|
||||||
placeholder {
|
placeholder {
|
||||||
display: inline;
|
display: inline;
|
||||||
@ -30,6 +30,7 @@
|
|||||||
border-left-width: 2px;
|
border-left-width: 2px;
|
||||||
border-color: orange;
|
border-color: orange;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
|
word-break: normal;
|
||||||
}
|
}
|
||||||
.ProseMirror-yjs-cursor > div {
|
.ProseMirror-yjs-cursor > div {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -45,6 +46,36 @@
|
|||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
padding-right: 2px;
|
padding-right: 2px;
|
||||||
}
|
}
|
||||||
|
[ychange_state] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
[ychange_state]:hover::before {
|
||||||
|
content: attr(ychange_user);
|
||||||
|
background-color: #fa8100;
|
||||||
|
position: absolute;
|
||||||
|
top: -14px;
|
||||||
|
right: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 3px 3px 0 0;
|
||||||
|
color: #fdfdfe;
|
||||||
|
user-select: none;
|
||||||
|
word-break: normal;
|
||||||
|
}
|
||||||
|
*[ychange_state='added'] {
|
||||||
|
background-color: #fa810069;
|
||||||
|
}
|
||||||
|
ychange[ychange_state='removed'] {
|
||||||
|
color: rgb(250, 129, 0);
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
*:not(ychange)[ychange_state='removed'] {
|
||||||
|
background-color: #ff9494c9;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
img[ychange_state='removed'] {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -6,19 +6,19 @@ import * as conf from './exampleConfig.js'
|
|||||||
|
|
||||||
import { EditorState } from 'prosemirror-state'
|
import { EditorState } from 'prosemirror-state'
|
||||||
import { EditorView } from 'prosemirror-view'
|
import { EditorView } from 'prosemirror-view'
|
||||||
import { DOMParser } from 'prosemirror-model'
|
import { DOMParser, Schema } from 'prosemirror-model'
|
||||||
import { schema } from 'prosemirror-schema-basic'
|
import { schema } from './prosemirror-schema.js'
|
||||||
import { exampleSetup } from 'prosemirror-example-setup'
|
import { exampleSetup } from 'prosemirror-example-setup'
|
||||||
import { noteHistoryPlugin } from './prosemirror-history.js'
|
import { noteHistoryPlugin } from './prosemirror-history.js'
|
||||||
|
|
||||||
const provider = new WebsocketProvider(conf.serverAddress)
|
const provider = new WebsocketProvider(conf.serverAddress)
|
||||||
const ydocument = provider.get('prosemirror')
|
const ydocument = provider.get('prosemirror', { gc: false })
|
||||||
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
const type = ydocument.define('prosemirror', Y.XmlFragment)
|
||||||
|
|
||||||
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
|
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
|
||||||
state: EditorState.create({
|
state: EditorState.create({
|
||||||
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
|
||||||
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin, noteHistoryPlugin])
|
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin /* noteHistoryPlugin */])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
1
index.js
1
index.js
@ -55,4 +55,3 @@ registerStruct(10, YXmlText)
|
|||||||
registerStruct(11, YXmlHook)
|
registerStruct(11, YXmlHook)
|
||||||
registerStruct(12, ItemEmbed)
|
registerStruct(12, ItemEmbed)
|
||||||
registerStruct(13, ItemBinary)
|
registerStruct(13, ItemBinary)
|
||||||
|
|
||||||
|
3
package-lock.json
generated
3
package-lock.json
generated
@ -2173,7 +2173,8 @@
|
|||||||
"crel": {
|
"crel": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/crel/-/crel-3.1.0.tgz",
|
||||||
"integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg=="
|
"integrity": "sha512-VIGY44ERxx8lXVkOEfcB0A49OkjxkQNK+j+fHvoLy7GsGX1KKgAaQ+p9N0YgvQXu+X+ryUWGDeLx/fSI+w7+eg==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"cross-spawn": {
|
"cross-spawn": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
|
@ -48,17 +48,17 @@ const readEntry = (arr, ydocument) => mux(() =>
|
|||||||
* @param {string} docName
|
* @param {string} docName
|
||||||
* @param {Y.Y} ydocument
|
* @param {Y.Y} ydocument
|
||||||
*/
|
*/
|
||||||
const loadFromPersistence = (db, docName, ydocument) => new Promise((resolve, reject)=>
|
const loadFromPersistence = (db, docName, ydocument) => new Promise((resolve, reject) =>
|
||||||
db.createReadStream({
|
db.createReadStream({
|
||||||
gte: `${docName}#`,
|
gte: `${docName}#`,
|
||||||
lte: `${docName}#Z`,
|
lte: `${docName}#Z`,
|
||||||
keys: false,
|
keys: false,
|
||||||
values: true
|
values: true
|
||||||
})
|
})
|
||||||
.on('data', data => readEntry(data, ydocument))
|
.on('data', data => readEntry(data, ydocument))
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('end', resolve)
|
.on('end', resolve)
|
||||||
.on('close', resolve)
|
.on('close', resolve)
|
||||||
)
|
)
|
||||||
|
|
||||||
const persistState = (db, docName, ydocument) => {
|
const persistState = (db, docName, ydocument) => {
|
||||||
@ -68,9 +68,9 @@ const persistState = (db, docName, ydocument) => {
|
|||||||
const entryPromise = db.put(entryKey, Y.encoding.toBuffer(encoder))
|
const entryPromise = db.put(entryKey, Y.encoding.toBuffer(encoder))
|
||||||
const delOps = []
|
const delOps = []
|
||||||
return new Promise((resolve, reject) => db.createKeyStream({
|
return new Promise((resolve, reject) => db.createKeyStream({
|
||||||
gte: `${docName}#`,
|
gte: `${docName}#`,
|
||||||
lt: entryKey,
|
lt: entryKey
|
||||||
})
|
})
|
||||||
.on('data', key => delOps.push({ type: 'del', key }))
|
.on('data', key => delOps.push({ type: 'del', key }))
|
||||||
.on('error', reject)
|
.on('error', reject)
|
||||||
.on('end', resolve)
|
.on('end', resolve)
|
||||||
|
@ -40,24 +40,22 @@ export const readUsersStateChange = (decoder, y) => {
|
|||||||
const userID = decoding.readVarUint(decoder)
|
const userID = decoding.readVarUint(decoder)
|
||||||
const clock = decoding.readVarUint(decoder)
|
const clock = decoding.readVarUint(decoder)
|
||||||
const state = JSON.parse(decoding.readVarString(decoder))
|
const state = JSON.parse(decoding.readVarString(decoder))
|
||||||
if (userID !== y.userID) {
|
const uClock = y.awarenessClock.get(userID) || 0
|
||||||
const uClock = y.awarenessClock.get(userID) || 0
|
y.awarenessClock.set(userID, clock)
|
||||||
y.awarenessClock.set(userID, clock)
|
if (state === null) {
|
||||||
if (state === null) {
|
// only write if clock increases. cannot overwrite
|
||||||
// only write if clock increases. cannot overwrite
|
if (y.awareness.has(userID) && uClock < clock) {
|
||||||
if (y.awareness.has(userID) && uClock < clock) {
|
y.awareness.delete(userID)
|
||||||
y.awareness.delete(userID)
|
removed.push(userID)
|
||||||
removed.push(userID)
|
|
||||||
}
|
|
||||||
} else if (uClock <= clock) { // allow to overwrite (e.g. when client was on, then offline)
|
|
||||||
if (y.awareness.has(userID)) {
|
|
||||||
updated.push(userID)
|
|
||||||
} else {
|
|
||||||
added.push(userID)
|
|
||||||
}
|
|
||||||
y.awareness.set(userID, state)
|
|
||||||
y.awarenessClock.set(userID, clock)
|
|
||||||
}
|
}
|
||||||
|
} else if (uClock <= clock) { // allow to overwrite (e.g. when client was on, then offline)
|
||||||
|
if (y.awareness.has(userID)) {
|
||||||
|
updated.push(userID)
|
||||||
|
} else {
|
||||||
|
added.push(userID)
|
||||||
|
}
|
||||||
|
y.awareness.set(userID, state)
|
||||||
|
y.awarenessClock.set(userID, clock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
|
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
import * as encoding from '../lib/encoding.js'
|
import * as encoding from '../lib/encoding.js'
|
||||||
import * as decoding from '../lib/decoding.js'
|
import * as decoding from '../lib/decoding.js'
|
||||||
import { Y } from '../utils/Y.js' // eslint-disable-line
|
import { Y } from '../utils/Y.js' // eslint-disable-line
|
||||||
@ -10,15 +9,22 @@ import { writeStateMap, readStateMap } from '../utils/StateStore.js'
|
|||||||
* @typedef {Object} HistorySnapshot
|
* @typedef {Object} HistorySnapshot
|
||||||
* @property {DeleteStore} HistorySnapshot.ds
|
* @property {DeleteStore} HistorySnapshot.ds
|
||||||
* @property {Map<number,number>} HistorySnapshot.sm
|
* @property {Map<number,number>} HistorySnapshot.sm
|
||||||
|
* @property {Map<number,string>} HistorySnapshot.userMap
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {encoding.Encoder} encoder
|
* @param {encoding.Encoder} encoder
|
||||||
* @param {Y} y
|
* @param {Y} y
|
||||||
|
* @param {Map<number, string>} userMap
|
||||||
*/
|
*/
|
||||||
export const writeHistorySnapshot = (encoder, y) => {
|
export const writeHistorySnapshot = (encoder, y, userMap) => {
|
||||||
writeDeleteStore(encoder, y.ds)
|
writeDeleteStore(encoder, y.ds)
|
||||||
writeStateMap(encoder, y.ss.state)
|
writeStateMap(encoder, y.ss.state)
|
||||||
|
encoding.writeVarUint(encoder, userMap.size)
|
||||||
|
userMap.forEach((accountname, userid) => {
|
||||||
|
encoding.writeVarUint(encoder, userid)
|
||||||
|
encoding.writeVarString(encoder, accountname)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,8 +32,15 @@ export const writeHistorySnapshot = (encoder, y) => {
|
|||||||
* @param {decoding.Decoder} decoder
|
* @param {decoding.Decoder} decoder
|
||||||
* @return {HistorySnapshot}
|
* @return {HistorySnapshot}
|
||||||
*/
|
*/
|
||||||
export const readHistorySnapshot = (decoder) => {
|
export const readHistorySnapshot = decoder => {
|
||||||
const ds = readFreshDeleteStore(decoder)
|
const ds = readFreshDeleteStore(decoder)
|
||||||
const sm = readStateMap(decoder)
|
const sm = readStateMap(decoder)
|
||||||
return { ds, sm }
|
const size = decoding.readVarUint(decoder)
|
||||||
|
const userMap = new Map()
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
const userid = decoding.readVarUint(decoder)
|
||||||
|
const accountname = decoding.readVarString(decoder)
|
||||||
|
userMap.set(userid, accountname)
|
||||||
|
}
|
||||||
|
return { ds, sm, userMap }
|
||||||
}
|
}
|
@ -97,8 +97,8 @@ const broadcastUpdate = (y, transaction) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketsSharedDocument extends Y.Y {
|
class WebsocketsSharedDocument extends Y.Y {
|
||||||
constructor (url) {
|
constructor (url, opts) {
|
||||||
super()
|
super(opts)
|
||||||
this.url = url
|
this.url = url
|
||||||
this.wsconnected = false
|
this.wsconnected = false
|
||||||
this.mux = Y.createMutex()
|
this.mux = Y.createMutex()
|
||||||
@ -112,7 +112,7 @@ class WebsocketsSharedDocument extends Y.Y {
|
|||||||
const encoder = readMessage(this, data) // already muxed
|
const encoder = readMessage(this, data) // already muxed
|
||||||
this.mux(() => {
|
this.mux(() => {
|
||||||
if (Y.encoding.length(encoder) > 1) {
|
if (Y.encoding.length(encoder) > 1) {
|
||||||
bc.publish(url, Y.encoding.toBuffer(encoder))
|
bc.publish(url, Y.encoding.toBuffer(encoder))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -173,10 +173,10 @@ export class WebsocketProvider {
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @return {WebsocketsSharedDocument}
|
* @return {WebsocketsSharedDocument}
|
||||||
*/
|
*/
|
||||||
get (name) {
|
get (name, opts) {
|
||||||
let doc = this.docs.get(name)
|
let doc = this.docs.get(name)
|
||||||
if (doc === undefined) {
|
if (doc === undefined) {
|
||||||
doc = new WebsocketsSharedDocument(this.url + name)
|
doc = new WebsocketsSharedDocument(this.url + name, opts)
|
||||||
}
|
}
|
||||||
return doc
|
return doc
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ const http = require('http')
|
|||||||
|
|
||||||
const port = process.env.PORT || 1234
|
const port = process.env.PORT || 1234
|
||||||
|
|
||||||
|
// disable gc when using snapshots!
|
||||||
|
const gcEnabled = process.env.GC !== 'false' && process.env.GC !== '0'
|
||||||
const persistenceDir = process.env.YPERSISTENCE
|
const persistenceDir = process.env.YPERSISTENCE
|
||||||
let persistence = null
|
let persistence = null
|
||||||
if (typeof persistenceDir === 'string') {
|
if (typeof persistenceDir === 'string') {
|
||||||
@ -44,7 +46,7 @@ const afterTransaction = (doc, transaction) => {
|
|||||||
|
|
||||||
class WSSharedDoc extends Y.Y {
|
class WSSharedDoc extends Y.Y {
|
||||||
constructor () {
|
constructor () {
|
||||||
super({ gc: true })
|
super({ gc: gcEnabled })
|
||||||
this.mux = Y.createMutex()
|
this.mux = Y.createMutex()
|
||||||
/**
|
/**
|
||||||
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
* Maps from conn to set of controlled user ids. Delete all user ids from awareness when this conn is closed
|
||||||
|
BIN
tmp/000005.ldb
BIN
tmp/000005.ldb
Binary file not shown.
BIN
tmp/000006.log
BIN
tmp/000006.log
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
MANIFEST-000004
|
|
5
tmp/LOG
5
tmp/LOG
@ -1,5 +0,0 @@
|
|||||||
2018/12/22-13:33:44.376775 70000c740000 Recovering log #3
|
|
||||||
2018/12/22-13:33:44.377203 70000c740000 Level-0 table #5: started
|
|
||||||
2018/12/22-13:33:44.379580 70000c740000 Level-0 table #5: 4834 bytes OK
|
|
||||||
2018/12/22-13:33:44.380496 70000c740000 Delete type=0 #3
|
|
||||||
2018/12/22-13:33:44.380670 70000c740000 Delete type=3 #2
|
|
@ -1 +0,0 @@
|
|||||||
2018/12/22-13:23:54.104944 700007836000 Delete type=3 #1
|
|
Binary file not shown.
@ -8,7 +8,7 @@ import { ItemJSON } from '../structs/ItemJSON.js'
|
|||||||
import * as stringify from '../utils/structStringify.js'
|
import * as stringify from '../utils/structStringify.js'
|
||||||
import { YEvent } from '../utils/YEvent.js'
|
import { YEvent } from '../utils/YEvent.js'
|
||||||
import { ItemBinary } from '../structs/ItemBinary.js'
|
import { ItemBinary } from '../structs/ItemBinary.js'
|
||||||
import { isVisible } from '../utils/snapshot.js';
|
import { isVisible } from '../utils/snapshot.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that describes the changes on a YMap.
|
* Event that describes the changes on a YMap.
|
||||||
@ -82,11 +82,11 @@ export class YMap extends Type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let key in this._map) {
|
this._map.forEach((_, key) => {
|
||||||
if (this.has(key, snapshot)) {
|
if (YMap.prototype.has.call(this, key, snapshot)) {
|
||||||
keys.push(key)
|
keys.push(key)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
@ -572,11 +572,12 @@ export class YText extends YArray {
|
|||||||
* Returns the Delta representation of this YText type.
|
* Returns the Delta representation of this YText type.
|
||||||
*
|
*
|
||||||
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
* @param {import('../protocols/history.js').HistorySnapshot} [snapshot]
|
||||||
|
* @param {import('../protocols/history.js').HistorySnapshot} [prevSnapshot]
|
||||||
* @return {Delta} The Delta representation of this type.
|
* @return {Delta} The Delta representation of this type.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
toDelta (snapshot) {
|
toDelta (snapshot, prevSnapshot) {
|
||||||
let ops = []
|
let ops = []
|
||||||
let currentAttributes = new Map()
|
let currentAttributes = new Map()
|
||||||
let str = ''
|
let str = ''
|
||||||
@ -602,9 +603,24 @@ export class YText extends YArray {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (n !== null) {
|
while (n !== null) {
|
||||||
if (isVisible(n, snapshot)) {
|
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
|
||||||
switch (n.constructor) {
|
switch (n.constructor) {
|
||||||
case ItemString:
|
case ItemString:
|
||||||
|
const cur = currentAttributes.get('ychange')
|
||||||
|
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||||
|
if (cur === undefined || cur.user !== n._id.user || cur.state !== 'removed') {
|
||||||
|
packStr()
|
||||||
|
currentAttributes.set('ychange', { user: n._id.user, state: 'removed' })
|
||||||
|
}
|
||||||
|
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||||
|
if (cur === undefined || cur.user !== n._id.user || cur.state !== 'added') {
|
||||||
|
packStr()
|
||||||
|
currentAttributes.set('ychange', { user: n._id.user, state: 'added' })
|
||||||
|
}
|
||||||
|
} else if (cur !== undefined) {
|
||||||
|
packStr()
|
||||||
|
currentAttributes.delete('ychange')
|
||||||
|
}
|
||||||
str += n._content
|
str += n._content
|
||||||
break
|
break
|
||||||
case ItemFormat:
|
case ItemFormat:
|
||||||
|
@ -326,9 +326,9 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let key in this._map) {
|
YMap.prototype.keys.call(this, snapshot).forEach(key => {
|
||||||
return YMap.prototype.get.call(this, key, snapshot)
|
obj[key] = YMap.prototype.get.call(this, key, snapshot)
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,6 @@ export class DeleteStore extends Tree {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stringifies a message-encoded Delete Set.
|
* Stringifies a message-encoded Delete Set.
|
||||||
*
|
*
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Item} item
|
* @param {Item} item
|
||||||
|
Loading…
x
Reference in New Issue
Block a user