implemented undo 🙌
This commit is contained in:
parent
c545118637
commit
0208d83f91
@ -10,8 +10,23 @@ let y = new Y({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
window.yXml = y
|
window.yXml = y
|
||||||
|
window.yXmlType = y.get('xml', Y.XmlFragment)
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
console.log('start!')
|
console.log('start!')
|
||||||
// Bind children of XmlFragment to the document.body
|
// Bind children of XmlFragment to the document.body
|
||||||
y.get('xml', Y.XmlFragment).bindToDom(document.body)
|
window.yXmlType.bindToDom(document.body)
|
||||||
|
}
|
||||||
|
window.undoManager = new Y.utils.UndoManager(window.yXmlType)
|
||||||
|
|
||||||
|
document.onkeydown = function interceptUndoRedo (e) {
|
||||||
|
if (e.keyCode === 90 && e.ctrlKey) {
|
||||||
|
if (!e.shiftKey) {
|
||||||
|
console.info('Undo!')
|
||||||
|
window.undoManager.undo()
|
||||||
|
} else {
|
||||||
|
console.info('Redo!')
|
||||||
|
window.undoManager.redo()
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,10 @@ export default class StateStore {
|
|||||||
}
|
}
|
||||||
setState (user, state) {
|
setState (user, state) {
|
||||||
// TODO: modify missingi structs here
|
// TODO: modify missingi structs here
|
||||||
|
const beforeState = this.y._transaction.beforeState
|
||||||
|
if (!beforeState.has(user)) {
|
||||||
|
beforeState.set(user, this.getState(user))
|
||||||
|
}
|
||||||
this.state.set(user, state)
|
this.state.set(user, state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,19 @@ export default class Item {
|
|||||||
this._parentSub = null
|
this._parentSub = null
|
||||||
this._deleted = false
|
this._deleted = false
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Copy the effect of struct
|
||||||
|
*/
|
||||||
|
_copy () {
|
||||||
|
let struct = new this.constructor()
|
||||||
|
struct._origin = this._left
|
||||||
|
struct._left = this._left
|
||||||
|
struct._right = this
|
||||||
|
struct._right_origin = this
|
||||||
|
struct._parent = this._parent
|
||||||
|
struct._parentSub = this._parentSub
|
||||||
|
return struct
|
||||||
|
}
|
||||||
get _lastId () {
|
get _lastId () {
|
||||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||||
}
|
}
|
||||||
@ -83,6 +96,7 @@ export default class Item {
|
|||||||
del._integrate(y, true)
|
del._integrate(y, true)
|
||||||
}
|
}
|
||||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||||
|
y._transaction.deletedStructs.add(this)
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This is called right before this struct receives any children.
|
* This is called right before this struct receives any children.
|
||||||
|
@ -6,6 +6,11 @@ export default class ItemJSON extends Item {
|
|||||||
super()
|
super()
|
||||||
this._content = null
|
this._content = null
|
||||||
}
|
}
|
||||||
|
_copy () {
|
||||||
|
let struct = super._copy()
|
||||||
|
struct._content = this._content
|
||||||
|
return struct
|
||||||
|
}
|
||||||
get _length () {
|
get _length () {
|
||||||
return this._content.length
|
return this._content.length
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,11 @@ export default class ItemString extends Item {
|
|||||||
super()
|
super()
|
||||||
this._content = null
|
this._content = null
|
||||||
}
|
}
|
||||||
|
_copy () {
|
||||||
|
let struct = super._copy()
|
||||||
|
struct._content = this._content
|
||||||
|
return struct
|
||||||
|
}
|
||||||
get _length () {
|
get _length () {
|
||||||
return this._content.length
|
return this._content.length
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,48 @@ export default class Type extends Item {
|
|||||||
this._start = null
|
this._start = null
|
||||||
this._y = null
|
this._y = null
|
||||||
this._eventHandler = new EventHandler()
|
this._eventHandler = new EventHandler()
|
||||||
|
this._deepEventHandler = new EventHandler()
|
||||||
|
}
|
||||||
|
_callEventHandler (event) {
|
||||||
|
this._eventHandler.callEventListeners(event)
|
||||||
|
let type = this
|
||||||
|
while (type !== this._y) {
|
||||||
|
type._deepEventHandler.callEventListeners(event)
|
||||||
|
type = type._parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_copy (undeleteChildren) {
|
||||||
|
let copy = super._copy()
|
||||||
|
let map = new Map()
|
||||||
|
copy._map = map
|
||||||
|
for (let [key, value] of this._map) {
|
||||||
|
if (undeleteChildren.has(value) || !value.deleted) {
|
||||||
|
let _item = value._copy(undeleteChildren)
|
||||||
|
_item._parent = copy
|
||||||
|
map.set(key, value._copy(undeleteChildren))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let prevUndeleted = null
|
||||||
|
copy._start = null
|
||||||
|
let item = this._start
|
||||||
|
while (item !== null) {
|
||||||
|
if (undeleteChildren.has(item) || !item.deleted) {
|
||||||
|
let _item = item._copy(undeleteChildren)
|
||||||
|
_item._left = prevUndeleted
|
||||||
|
_item._origin = prevUndeleted
|
||||||
|
_item._right = null
|
||||||
|
_item._right_origin = null
|
||||||
|
_item._parent = copy
|
||||||
|
if (prevUndeleted === null) {
|
||||||
|
copy._start = _item
|
||||||
|
} else {
|
||||||
|
prevUndeleted._right = _item
|
||||||
|
}
|
||||||
|
prevUndeleted = _item
|
||||||
|
}
|
||||||
|
item = item._right
|
||||||
|
}
|
||||||
|
return copy
|
||||||
}
|
}
|
||||||
_transact (f) {
|
_transact (f) {
|
||||||
const y = this._y
|
const y = this._y
|
||||||
@ -49,9 +91,15 @@ export default class Type extends Item {
|
|||||||
observe (f) {
|
observe (f) {
|
||||||
this._eventHandler.addEventListener(f)
|
this._eventHandler.addEventListener(f)
|
||||||
}
|
}
|
||||||
|
observeDeep (f) {
|
||||||
|
this._deepEventHandler.addEventListener(f)
|
||||||
|
}
|
||||||
unobserve (f) {
|
unobserve (f) {
|
||||||
this._eventHandler.removeEventListener(f)
|
this._eventHandler.removeEventListener(f)
|
||||||
}
|
}
|
||||||
|
unobserveDeep (f) {
|
||||||
|
this._deepEventHandler.removeEventListener(f)
|
||||||
|
}
|
||||||
_integrate (y) {
|
_integrate (y) {
|
||||||
y._transaction.newTypes.add(this)
|
y._transaction.newTypes.add(this)
|
||||||
super._integrate(y)
|
super._integrate(y)
|
||||||
|
@ -5,8 +5,10 @@ export default class Transaction {
|
|||||||
// types added during transaction
|
// types added during transaction
|
||||||
this.newTypes = new Set()
|
this.newTypes = new Set()
|
||||||
// changed types (does not include new types)
|
// changed types (does not include new types)
|
||||||
// maps from type to parentSubs (item.parentSub = null for array elements)
|
// maps from type to parentSubs (item._parentSub = null for array elements)
|
||||||
this.changedTypes = new Map()
|
this.changedTypes = new Map()
|
||||||
|
this.deletedStructs = new Set()
|
||||||
|
this.beforeState = new Map()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class YArrayEvent {
|
|||||||
|
|
||||||
export default class YArray extends Type {
|
export default class YArray extends Type {
|
||||||
_callObserver (parentSubs, remote) {
|
_callObserver (parentSubs, remote) {
|
||||||
this._eventHandler.callEventListeners(new YArrayEvent(this, remote))
|
this._callEventHandler(new YArrayEvent(this, remote))
|
||||||
}
|
}
|
||||||
get (pos) {
|
get (pos) {
|
||||||
let n = this._start
|
let n = this._start
|
||||||
|
@ -13,7 +13,7 @@ class YMapEvent {
|
|||||||
|
|
||||||
export default class YMap extends Type {
|
export default class YMap extends Type {
|
||||||
_callObserver (parentSubs, remote) {
|
_callObserver (parentSubs, remote) {
|
||||||
this._eventHandler.callEventListeners(new YMapEvent(this, parentSubs, remote))
|
this._callEventHandler(new YMapEvent(this, parentSubs, remote))
|
||||||
}
|
}
|
||||||
toJSON () {
|
toJSON () {
|
||||||
const map = {}
|
const map = {}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
/* global MutationObserver */
|
|
||||||
|
|
||||||
// import diff from 'fast-diff'
|
// import diff from 'fast-diff'
|
||||||
import { defaultDomFilter } from './utils.js'
|
import { defaultDomFilter } from './utils.js'
|
||||||
|
|
||||||
@ -23,12 +21,18 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
this._domFilter = arg2
|
this._domFilter = arg2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_copy (undeleteChildren) {
|
||||||
|
let struct = super._copy(undeleteChildren)
|
||||||
|
struct.nodeName = this.nodeName
|
||||||
|
return struct
|
||||||
|
}
|
||||||
_setDom (dom) {
|
_setDom (dom) {
|
||||||
if (this._dom != null) {
|
if (this._dom != null) {
|
||||||
throw new Error('Only call this method if you know what you are doing ;)')
|
throw new Error('Only call this method if you know what you are doing ;)')
|
||||||
} else if (dom._yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
|
} else if (dom._yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
|
||||||
throw new Error('Already bound to an YXml type')
|
throw new Error('Already bound to an YXml type')
|
||||||
} else {
|
} else {
|
||||||
|
this._dom = dom
|
||||||
dom._yxml = this
|
dom._yxml = this
|
||||||
// tag is already set in constructor
|
// tag is already set in constructor
|
||||||
// set attributes
|
// set attributes
|
||||||
@ -43,9 +47,7 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
this.setAttribute(attrName, attrValue)
|
this.setAttribute(attrName, attrValue)
|
||||||
}
|
}
|
||||||
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes))
|
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes))
|
||||||
if (MutationObserver != null) {
|
this._bindToDom(dom)
|
||||||
this._dom = this._bindToDom(dom)
|
|
||||||
}
|
|
||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -112,6 +114,7 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
let dom = this._dom
|
let dom = this._dom
|
||||||
if (dom == null) {
|
if (dom == null) {
|
||||||
dom = document.createElement(this.nodeName)
|
dom = document.createElement(this.nodeName)
|
||||||
|
this._dom = dom
|
||||||
dom._yxml = this
|
dom._yxml = this
|
||||||
let attrs = this.getAttributes()
|
let attrs = this.getAttributes()
|
||||||
for (let key in attrs) {
|
for (let key in attrs) {
|
||||||
@ -120,9 +123,7 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
this.forEach(yxml => {
|
this.forEach(yxml => {
|
||||||
dom.appendChild(yxml.getDom())
|
dom.appendChild(yxml.getDom())
|
||||||
})
|
})
|
||||||
if (MutationObserver !== null) {
|
this._bindToDom(dom)
|
||||||
this._dom = this._bindToDom(dom)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
/* global MutationObserver */
|
/* global MutationObserver */
|
||||||
|
|
||||||
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
||||||
|
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
||||||
|
|
||||||
import YArray from '../YArray.js'
|
import YArray from '../YArray.js'
|
||||||
import YXmlText from './YXmlText.js'
|
import YXmlText from './YXmlText.js'
|
||||||
import YXmlEvent from './YXmlEvent.js'
|
import YXmlEvent from './YXmlEvent.js'
|
||||||
import { logID } from '../../MessageHandler/messageToString.js'
|
import { logID } from '../../MessageHandler/messageToString.js'
|
||||||
|
import diff from 'fast-diff'
|
||||||
|
|
||||||
function domToYXml (parent, doms) {
|
function domToYXml (parent, doms) {
|
||||||
const types = []
|
const types = []
|
||||||
@ -52,8 +54,6 @@ export default class YXmlFragment extends YArray {
|
|||||||
token = true
|
token = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Apply Y.Xml events to dom
|
|
||||||
this.observe(reflectChangesOnDom)
|
|
||||||
}
|
}
|
||||||
enableSmartScrolling (scrollElement) {
|
enableSmartScrolling (scrollElement) {
|
||||||
this._scrollElement = scrollElement
|
this._scrollElement = scrollElement
|
||||||
@ -68,7 +68,7 @@ export default class YXmlFragment extends YArray {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
_callObserver (parentSubs, remote) {
|
_callObserver (parentSubs, remote) {
|
||||||
this._eventHandler.callEventListeners(new YXmlEvent(this, parentSubs, remote))
|
this._callEventHandler(new YXmlEvent(this, parentSubs, remote))
|
||||||
}
|
}
|
||||||
toString () {
|
toString () {
|
||||||
return this.map(xml => xml.toString()).join('')
|
return this.map(xml => xml.toString()).join('')
|
||||||
@ -111,57 +111,91 @@ export default class YXmlFragment extends YArray {
|
|||||||
throw new Error('Not able to bind to a DOM element, because MutationObserver is not available!')
|
throw new Error('Not able to bind to a DOM element, because MutationObserver is not available!')
|
||||||
}
|
}
|
||||||
dom.innerHTML = ''
|
dom.innerHTML = ''
|
||||||
|
this._dom = dom
|
||||||
|
dom._yxml = this
|
||||||
this.forEach(t => {
|
this.forEach(t => {
|
||||||
dom.insertBefore(t.getDom(), null)
|
dom.insertBefore(t.getDom(), null)
|
||||||
})
|
})
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
this._bindToDom(dom)
|
this._bindToDom(dom)
|
||||||
}
|
}
|
||||||
// binds to a dom element
|
// binds to a dom element
|
||||||
// Only call if dom and YXml are isomorph
|
// Only call if dom and YXml are isomorph
|
||||||
_bindToDom (dom) {
|
_bindToDom (dom) {
|
||||||
|
if (this._parent === null || this._parent._dom != null || typeof MutationObserver === 'undefined') {
|
||||||
|
// only bind if parent did not already bind
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._y.on('beforeTransaction', () => {
|
||||||
|
this._domObserverListener(this._domObserver.takeRecords())
|
||||||
|
})
|
||||||
|
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
|
||||||
|
this._y.on('afterTransaction', afterTransactionSelectionFixer)
|
||||||
|
// Apply Y.Xml events to dom
|
||||||
|
this.observeDeep(reflectChangesOnDom.bind(this))
|
||||||
|
// Apply Dom changes on Y.Xml
|
||||||
this._domObserverListener = mutations => {
|
this._domObserverListener = mutations => {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
this._y.transact(() => {
|
this._y.transact(() => {
|
||||||
let diffChildren = false
|
let diffChildren = new Set()
|
||||||
mutations.forEach(mutation => {
|
mutations.forEach(mutation => {
|
||||||
if (mutation.type === 'attributes') {
|
const dom = mutation.target
|
||||||
|
const yxml = dom._yxml
|
||||||
|
if (yxml == null) {
|
||||||
|
// dom element is filtered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (mutation.type) {
|
||||||
|
case 'characterData':
|
||||||
|
var diffs = diff(yxml.toString(), dom.nodeValue)
|
||||||
|
var pos = 0
|
||||||
|
for (var i = 0; i < diffs.length; i++) {
|
||||||
|
var d = diffs[i]
|
||||||
|
if (d[0] === 0) { // EQUAL
|
||||||
|
pos += d[1].length
|
||||||
|
} else if (d[0] === -1) { // DELETE
|
||||||
|
yxml.delete(pos, d[1].length)
|
||||||
|
} else { // INSERT
|
||||||
|
yxml.insert(pos, d[1])
|
||||||
|
pos += d[1].length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'attributes':
|
||||||
let name = mutation.attributeName
|
let name = mutation.attributeName
|
||||||
// check if filter accepts attribute
|
// check if filter accepts attribute
|
||||||
if (this._domFilter(this._dom, [name]).length > 0) {
|
if (this._domFilter(dom, [name]).length > 0) {
|
||||||
var val = mutation.target.getAttribute(name)
|
var val = dom.getAttribute(name)
|
||||||
if (this.getAttribute(name) !== val) {
|
if (yxml.getAttribute(name) !== val) {
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
this.removeAttribute(name)
|
yxml.removeAttribute(name)
|
||||||
} else {
|
} else {
|
||||||
this.setAttribute(name, val)
|
yxml.setAttribute(name, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (mutation.type === 'childList') {
|
break
|
||||||
diffChildren = true
|
case 'childList':
|
||||||
|
diffChildren.add(mutation.target)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (diffChildren) {
|
for (let dom of diffChildren) {
|
||||||
applyChangesFromDom(this)
|
if (dom._yxml != null) {
|
||||||
|
applyChangesFromDom(dom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
this._domObserver = new MutationObserver(this._domObserverListener)
|
||||||
const observeOptions = { childList: true }
|
this._domObserver.observe(dom, {
|
||||||
if (this instanceof YXmlFragment._YXmlElement) {
|
childList: true,
|
||||||
observeOptions.attributes = true
|
attributes: true,
|
||||||
}
|
characterData: true,
|
||||||
this._domObserver.observe(dom, observeOptions)
|
subtree: true
|
||||||
|
})
|
||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
_beforeChange () {
|
|
||||||
if (this._domObserver != null) {
|
|
||||||
this._domObserverListener(this._domObserver.takeRecords())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_logString () {
|
_logString () {
|
||||||
const left = this._left !== null ? this._left._lastId : null
|
const left = this._left !== null ? this._left._lastId : null
|
||||||
const origin = this._origin !== null ? this._origin._lastId : null
|
const origin = this._origin !== null ? this._origin._lastId : null
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
/* global MutationObserver */
|
|
||||||
|
|
||||||
import diff from 'fast-diff'
|
|
||||||
import YText from '../YText.js'
|
import YText from '../YText.js'
|
||||||
import { getAnchorViewPosition, fixScrollPosition, getBoundingClientRect } from './utils.js'
|
|
||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
|
||||||
|
|
||||||
export default class YXmlText extends YText {
|
export default class YXmlText extends YText {
|
||||||
constructor (arg1) {
|
constructor (arg1) {
|
||||||
@ -25,6 +20,7 @@ export default class YXmlText extends YText {
|
|||||||
if (dom !== null) {
|
if (dom !== null) {
|
||||||
this._setDom(arg1)
|
this._setDom(arg1)
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
var token = true
|
var token = true
|
||||||
this._mutualExclude = f => {
|
this._mutualExclude = f => {
|
||||||
if (token) {
|
if (token) {
|
||||||
@ -54,11 +50,7 @@ export default class YXmlText extends YText {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
*/
|
||||||
_integrate (y) {
|
|
||||||
super._integrate(y)
|
|
||||||
y.on('beforeTransaction', beforeTransactionSelectionFixer)
|
|
||||||
y.on('afterTransaction', afterTransactionSelectionFixer)
|
|
||||||
}
|
}
|
||||||
setDomFilter () {}
|
setDomFilter () {}
|
||||||
enableSmartScrolling (scrollElement) {
|
enableSmartScrolling (scrollElement) {
|
||||||
@ -74,42 +66,15 @@ export default class YXmlText extends YText {
|
|||||||
// set marker
|
// set marker
|
||||||
this._dom = dom
|
this._dom = dom
|
||||||
dom._yxml = this
|
dom._yxml = this
|
||||||
if (typeof MutationObserver === 'undefined') {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._domObserverListener = () => {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
var diffs = diff(this.toString(), dom.nodeValue)
|
|
||||||
var pos = 0
|
|
||||||
for (var i = 0; i < diffs.length; i++) {
|
|
||||||
var d = diffs[i]
|
|
||||||
if (d[0] === 0) { // EQUAL
|
|
||||||
pos += d[1].length
|
|
||||||
} else if (d[0] === -1) { // DELETE
|
|
||||||
this.delete(pos, d[1].length)
|
|
||||||
} else { // INSERT
|
|
||||||
this.insert(pos, d[1])
|
|
||||||
pos += d[1].length
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
|
||||||
this._domObserver.observe(this._dom, { characterData: true })
|
|
||||||
}
|
}
|
||||||
getDom () {
|
getDom () {
|
||||||
if (this._dom == null) {
|
if (this._dom === null) {
|
||||||
const dom = document.createTextNode(this.toString())
|
const dom = document.createTextNode(this.toString())
|
||||||
this._setDom(dom)
|
this._setDom(dom)
|
||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
return this._dom
|
return this._dom
|
||||||
}
|
}
|
||||||
_beforeChange () {
|
|
||||||
if (this._domObserver != null && this._y !== null) { // TODO: do I need th y condition
|
|
||||||
this._domObserverListener(this._domObserver.takeRecords())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_delete (y, createDelete) {
|
_delete (y, createDelete) {
|
||||||
this._unbindFromDom()
|
this._unbindFromDom()
|
||||||
super._delete(y, createDelete)
|
super._delete(y, createDelete)
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import YXmlText from './YXmlText.js'
|
||||||
|
|
||||||
export function defaultDomFilter (node, attributes) {
|
export function defaultDomFilter (node, attributes) {
|
||||||
return attributes
|
return attributes
|
||||||
@ -81,11 +82,12 @@ function _insertNodeHelper (yxml, prevExpectedNode, child) {
|
|||||||
* You can detect that a node was moved because expectedId
|
* You can detect that a node was moved because expectedId
|
||||||
* !== actualId in the list
|
* !== actualId in the list
|
||||||
*/
|
*/
|
||||||
export function applyChangesFromDom (yxml) {
|
export function applyChangesFromDom (dom) {
|
||||||
|
const yxml = dom._yxml
|
||||||
const y = yxml._y
|
const y = yxml._y
|
||||||
let knownChildren =
|
let knownChildren =
|
||||||
new Set(
|
new Set(
|
||||||
Array.prototype.map.call(yxml._dom.childNodes, child => child._yxml)
|
Array.prototype.map.call(dom.childNodes, child => child._yxml)
|
||||||
.filter(id => id !== undefined)
|
.filter(id => id !== undefined)
|
||||||
)
|
)
|
||||||
// 1. Check if any of the nodes was deleted
|
// 1. Check if any of the nodes was deleted
|
||||||
@ -95,7 +97,7 @@ export function applyChangesFromDom (yxml) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
// 2. iterate
|
// 2. iterate
|
||||||
let childNodes = yxml._dom.childNodes
|
let childNodes = dom.childNodes
|
||||||
let len = childNodes.length
|
let len = childNodes.length
|
||||||
let prevExpectedNode = null
|
let prevExpectedNode = null
|
||||||
let expectedNode = iterateUntilUndeleted(yxml._start)
|
let expectedNode = iterateUntilUndeleted(yxml._start)
|
||||||
@ -137,10 +139,12 @@ export function reflectChangesOnDom (event) {
|
|||||||
const yxml = event.target
|
const yxml = event.target
|
||||||
const dom = yxml._dom
|
const dom = yxml._dom
|
||||||
if (dom != null) {
|
if (dom != null) {
|
||||||
yxml._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
// TODO: do this once before applying stuff
|
// TODO: do this once before applying stuff
|
||||||
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
||||||
|
if (yxml.constructor === YXmlText) {
|
||||||
|
yxml._dom.nodeValue = yxml.toString()
|
||||||
|
} else {
|
||||||
// update attributes
|
// update attributes
|
||||||
event.attributesChanged.forEach(attributeName => {
|
event.attributesChanged.forEach(attributeName => {
|
||||||
const value = yxml.getAttribute(attributeName)
|
const value = yxml.getAttribute(attributeName)
|
||||||
@ -165,6 +169,7 @@ export function reflectChangesOnDom (event) {
|
|||||||
// insert fragment of undeleted nodes
|
// insert fragment of undeleted nodes
|
||||||
dom.append(fragment)
|
dom.append(fragment)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
/* TODO: smartscrolling
|
/* TODO: smartscrolling
|
||||||
.. else if (event.type === 'childInserted' || event.type === 'insert') {
|
.. else if (event.type === 'childInserted' || event.type === 'insert') {
|
||||||
let nodes = event.values
|
let nodes = event.values
|
||||||
|
@ -20,7 +20,8 @@ export default class EventHandler {
|
|||||||
callEventListeners (event) {
|
callEventListeners (event) {
|
||||||
for (var i = 0; i < this.eventListeners.length; i++) {
|
for (var i = 0; i < this.eventListeners.length; i++) {
|
||||||
try {
|
try {
|
||||||
this.eventListeners[i](event)
|
const f = this.eventListeners[i]
|
||||||
|
f(event)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/*
|
/*
|
||||||
Your observer threw an error. This error was caught so that Yjs
|
Your observer threw an error. This error was caught so that Yjs
|
||||||
|
92
src/Util/UndoManager.js
Normal file
92
src/Util/UndoManager.js
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import ID from './ID.js'
|
||||||
|
|
||||||
|
class ReverseOperation {
|
||||||
|
constructor (y) {
|
||||||
|
const beforeState = y._transaction.beforeState
|
||||||
|
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1)
|
||||||
|
if (beforeState.has(y.userID)) {
|
||||||
|
this.fromState = new ID(y.userID, beforeState.get(y.userID))
|
||||||
|
} else {
|
||||||
|
this.fromState = this.toState
|
||||||
|
}
|
||||||
|
this.deletedStructs = y._transaction.deletedStructs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isStructInScope (y, struct, scope) {
|
||||||
|
while (struct !== y) {
|
||||||
|
if (struct === scope) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
struct = struct._parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class UndoManager {
|
||||||
|
constructor (scope) {
|
||||||
|
this._undoBuffer = []
|
||||||
|
this._redoBuffer = []
|
||||||
|
this._scope = scope
|
||||||
|
this._undoing = false
|
||||||
|
this._redoing = false
|
||||||
|
const y = scope._y
|
||||||
|
this.y = y
|
||||||
|
y.on('afterTransaction', (y, remote) => {
|
||||||
|
if (!remote && (y._transaction.beforeState.has(y.userID) || y._transaction.deletedStructs.size > 0)) {
|
||||||
|
let reverseOperation = new ReverseOperation(y)
|
||||||
|
if (!this._undoing) {
|
||||||
|
this._undoBuffer.push(reverseOperation)
|
||||||
|
if (!this._redoing) {
|
||||||
|
this._redoBuffer = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._redoBuffer.push(reverseOperation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
undo () {
|
||||||
|
this._undoing = true
|
||||||
|
this._applyReverseOperation(this._undoBuffer)
|
||||||
|
this._undoing = false
|
||||||
|
}
|
||||||
|
redo () {
|
||||||
|
this._redoing = true
|
||||||
|
this._applyReverseOperation(this._redoBuffer)
|
||||||
|
this._redoing = false
|
||||||
|
}
|
||||||
|
_applyReverseOperation (reverseBuffer) {
|
||||||
|
this.y.transact(() => {
|
||||||
|
let performedUndo = false
|
||||||
|
while (!performedUndo && reverseBuffer.length > 0) {
|
||||||
|
let undoOp = reverseBuffer.pop()
|
||||||
|
// make sure that it is possible to iterate {from}-{to}
|
||||||
|
this.y.os.getItemCleanStart(undoOp.fromState)
|
||||||
|
this.y.os.getItemCleanEnd(undoOp.toState)
|
||||||
|
this.y.os.iterate(undoOp.fromState, undoOp.toState, op => {
|
||||||
|
if (!op._deleted && isStructInScope(this.y, op, this._scope)) {
|
||||||
|
performedUndo = true
|
||||||
|
op._delete(this.y)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for (let op of undoOp.deletedStructs) {
|
||||||
|
if (
|
||||||
|
isStructInScope(this.y, op, this._scope) &&
|
||||||
|
op._parent !== this.y &&
|
||||||
|
!op._parent._deleted &&
|
||||||
|
(
|
||||||
|
op._parent._id.user !== this.y.userID ||
|
||||||
|
op._parent._id.clock < undoOp.fromState.clock ||
|
||||||
|
op._parent._id.clock > undoOp.fromState.clock
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
performedUndo = true
|
||||||
|
op = op._copy(undoOp.deletedStructs)
|
||||||
|
op._integrate(this.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
4
src/Y.js
4
src/Y.js
@ -4,6 +4,7 @@ import StateStore from './Store/StateStore.js'
|
|||||||
import { generateUserID } from './Util/generateUserID.js'
|
import { generateUserID } from './Util/generateUserID.js'
|
||||||
import RootID from './Util/RootID.js'
|
import RootID from './Util/RootID.js'
|
||||||
import NamedEventHandler from './Util/NamedEventHandler.js'
|
import NamedEventHandler from './Util/NamedEventHandler.js'
|
||||||
|
import UndoManager from './Util/UndoManager.js'
|
||||||
|
|
||||||
import { messageToString, messageToRoomname } from './MessageHandler/messageToString.js'
|
import { messageToString, messageToRoomname } from './MessageHandler/messageToString.js'
|
||||||
|
|
||||||
@ -132,7 +133,8 @@ Y.XmlFragment = YXmlFragment
|
|||||||
Y.XmlText = YXmlText
|
Y.XmlText = YXmlText
|
||||||
|
|
||||||
Y.utils = {
|
Y.utils = {
|
||||||
BinaryDecoder
|
BinaryDecoder,
|
||||||
|
UndoManager
|
||||||
}
|
}
|
||||||
|
|
||||||
Y.debug = debug
|
Y.debug = debug
|
||||||
|
Loading…
x
Reference in New Issue
Block a user