diff --git a/src/MessageHandler/integrateRemoteStructs.js b/src/MessageHandler/integrateRemoteStructs.js index d561b65e..3b027e3f 100644 --- a/src/MessageHandler/integrateRemoteStructs.js +++ b/src/MessageHandler/integrateRemoteStructs.js @@ -1,6 +1,5 @@ import { getStruct } from '../Util/structReferences.js' import BinaryDecoder from '../Binary/Decoder.js' -import Delete from '../Struct/Delete.js' import { logID } from './messageToString.js' class MissingEntry { @@ -17,9 +16,14 @@ class MissingEntry { * integrate. */ function _integrateRemoteStructHelper (y, struct) { - struct._integrate(y) - if (struct.constructor !== Delete) { - const id = struct._id + const id = struct._id + if (id === undefined) { + struct._integrate(y) + } else { + if (y.ss.getState(id.user) > id.clock) { + return + } + struct._integrate(y) let msu = y._missingStructs.get(id.user) if (msu != null) { let clock = id.clock diff --git a/src/Struct/Type.js b/src/Struct/Type.js index 69f0f17d..a865d4b3 100644 --- a/src/Struct/Type.js +++ b/src/Struct/Type.js @@ -1,5 +1,6 @@ import Item from './Item.js' import EventHandler from '../Util/EventHandler.js' +import ID from '../Util/ID.js' // restructure children as if they were inserted one after another function integrateChildren (y, start) { @@ -14,6 +15,21 @@ function integrateChildren (y, start) { } while (right !== null) } +export function getListItemIDByPosition (type, i) { + let pos = 0 + let n = type._start + while (n !== null) { + if (!n._deleted) { + if (pos <= i && i < pos + n._length) { + const id = n._id + return new ID(id.user, id.clock + i - pos) + } + pos++ + } + n = n._right + } +} + export default class Type extends Item { constructor () { super() diff --git a/src/Transaction.js b/src/Transaction.js index a99b2b47..95cd3a5c 100644 --- a/src/Transaction.js +++ b/src/Transaction.js @@ -11,7 +11,7 @@ export default class Transaction { } export function transactionTypeChanged (y, type, sub) { - if (type !== y && !type._deleted) { + if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) { const changedTypes = y._transaction.changedTypes let subs = changedTypes.get(type) if (subs === undefined) { diff --git a/src/Type/YArray.js b/src/Type/YArray.js index d90c9788..58b60f15 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -13,9 +13,17 @@ export default class YArray extends Type { _callObserver (parentSubs, remote) { this._eventHandler.callEventListeners(new YArrayEvent(this, remote)) } - get (i) { - // TODO: This can be improved! - return this.toArray()[i] + get (pos) { + let n = this._start + while (n !== null) { + if (!n._deleted) { + if (pos < n._length) { + return n._content[n._length - pos] + } + pos -= n._length + } + n = n._right + } } toArray () { return this.map(c => c) diff --git a/src/Type/y-xml/YXmlElement.js b/src/Type/y-xml/YXmlElement.js index b0f7955c..9cc664d7 100644 --- a/src/Type/y-xml/YXmlElement.js +++ b/src/Type/y-xml/YXmlElement.js @@ -26,10 +26,10 @@ export default class YXmlElement extends YXmlFragment { _setDom (dom) { if (this._dom != null) { 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') } else { - dom.__yxml = this + dom._yxml = this // tag is already set in constructor // set attributes let attrNames = [] @@ -112,7 +112,7 @@ export default class YXmlElement extends YXmlFragment { let dom = this._dom if (dom == null) { dom = document.createElement(this.nodeName) - dom.__yxml = this + dom._yxml = this let attrs = this.getAttributes() for (let key in attrs) { dom.setAttribute(key, attrs[key]) diff --git a/src/Type/y-xml/YXmlFragment.js b/src/Type/y-xml/YXmlFragment.js index c098ddf1..7f6519fc 100644 --- a/src/Type/y-xml/YXmlFragment.js +++ b/src/Type/y-xml/YXmlFragment.js @@ -10,8 +10,8 @@ import { logID } from '../../MessageHandler/messageToString.js' function domToYXml (parent, doms) { const types = [] doms.forEach(d => { - if (d.__yxml != null && d.__yxml !== false) { - d.__yxml._unbindFromDom() + if (d._yxml != null && d._yxml !== false) { + d._yxml._unbindFromDom() } if (parent._domFilter(d, []) !== null) { let type @@ -25,7 +25,7 @@ function domToYXml (parent, doms) { type.enableSmartScrolling(parent._scrollElement) types.push(type) } else { - d.__yxml = false + d._yxml = false } }) return types @@ -83,7 +83,7 @@ export default class YXmlFragment extends YArray { this._domObserver = null } if (this._dom != null) { - this._dom.__yxml = null + this._dom._yxml = null this._dom = null } } @@ -97,12 +97,15 @@ export default class YXmlFragment extends YArray { this.insert(pos, types) return types } + getDom () { + return this._dom + } bindToDom (dom) { if (this._dom != null) { this._unbindFromDom() } - if (dom.__yxml != null) { - dom.__yxml._unbindFromDom() + if (dom._yxml != null) { + dom._yxml._unbindFromDom() } if (MutationObserver == null) { throw new Error('Not able to bind to a DOM element, because MutationObserver is not available!') @@ -112,7 +115,7 @@ export default class YXmlFragment extends YArray { dom.insertBefore(t.getDom(), null) }) this._dom = dom - dom.__yxml = this + dom._yxml = this this._bindToDom(dom) } // binds to a dom element diff --git a/src/Type/y-xml/YXmlText.js b/src/Type/y-xml/YXmlText.js index b3651f58..cff8f487 100644 --- a/src/Type/y-xml/YXmlText.js +++ b/src/Type/y-xml/YXmlText.js @@ -1,20 +1,9 @@ -/* global getSelection, MutationObserver */ +/* global MutationObserver */ import diff from 'fast-diff' import YText from '../YText.js' import { getAnchorViewPosition, fixScrollPosition, getBoundingClientRect } from './utils.js' - -function fixPosition (event, pos) { - if (event.index <= pos) { - if (event.type === 'delete') { - return pos - Math.min(pos - event.index, event.length) - } else { - return pos + 1 - } - } else { - return pos - } -} +import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js' export default class YXmlText extends YText { constructor (arg1) { @@ -53,25 +42,6 @@ export default class YXmlText extends YText { if (this._dom != null) { const dom = this._dom this._mutualExclude(() => { - let selection = null - let shouldUpdateSelection = false - let anchorNode = null - let anchorOffset = null - let focusNode = null - let focusOffset = null - if (typeof getSelection !== 'undefined') { - selection = getSelection() - if (selection.anchorNode === dom) { - anchorNode = selection.anchorNode - anchorOffset = fixPosition(event, selection.anchorOffset) - shouldUpdateSelection = true - } - if (selection.focusNode === dom) { - focusNode = selection.focusNode - focusOffset = fixPosition(event, selection.focusOffset) - shouldUpdateSelection = true - } - } let anchorViewPosition = getAnchorViewPosition(this._scrollElement) let anchorViewFix if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) { @@ -81,19 +51,15 @@ export default class YXmlText extends YText { } dom.nodeValue = this.toString() fixScrollPosition(this._scrollElement, anchorViewFix) - - if (shouldUpdateSelection) { - selection.setBaseAndExtent( - anchorNode || selection.anchorNode, - anchorOffset || selection.anchorOffset, - focusNode || selection.focusNode, - focusOffset || selection.focusOffset - ) - } }) } }) } + _integrate (y) { + super._integrate(y) + y.on('beforeTransaction', beforeTransactionSelectionFixer) + y.on('afterTransaction', afterTransactionSelectionFixer) + } setDomFilter () {} enableSmartScrolling (scrollElement) { this._scrollElement = scrollElement @@ -102,12 +68,12 @@ export default class YXmlText extends YText { if (this._dom != null) { this._unbindFromDom() } - if (dom.__yxml != null) { - dom.__yxml._unbindFromDom() + if (dom._yxml != null) { + dom._yxml._unbindFromDom() } // set marker this._dom = dom - dom.__yxml = this + dom._yxml = this if (typeof MutationObserver === 'undefined') { return } @@ -154,7 +120,7 @@ export default class YXmlText extends YText { this._domObserver = null } if (this._dom != null) { - this._dom.__yxml = null + this._dom._yxml = null this._dom = null } } diff --git a/src/Type/y-xml/selection.js b/src/Type/y-xml/selection.js new file mode 100644 index 00000000..52ae5332 --- /dev/null +++ b/src/Type/y-xml/selection.js @@ -0,0 +1,73 @@ +/* globals getSelection */ + +import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js' + +let browserSelection = null +let relativeSelection = null + +export let beforeTransactionSelectionFixer +if (typeof getSelection !== 'undefined') { + beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, remote) { + if (!remote) { + return + } + relativeSelection = { from: null, to: null, fromY: null, toY: null } + browserSelection = getSelection() + const anchorNode = browserSelection.anchorNode + if (anchorNode !== null && anchorNode._yxml != null) { + const yxml = anchorNode._yxml + relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset) + relativeSelection.fromY = yxml._y + } + const focusNode = browserSelection.focusNode + if (focusNode !== null && focusNode._yxml != null) { + const yxml = anchorNode._yxml + relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset) + relativeSelection.toY = yxml._y + } + } +} else { + beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {} +} + +export function afterTransactionSelectionFixer (y, remote) { + if (relativeSelection === null || !remote) { + return + } + const to = relativeSelection.to + const from = relativeSelection.from + const fromY = relativeSelection.fromY + const toY = relativeSelection.toY + let shouldUpdate = false + let anchorNode = browserSelection.anchorNode + let anchorOffset = browserSelection.anchorOffset + let focusNode = browserSelection.focusNode + let focusOffset = browserSelection.focusOffset + if (from !== null) { + let sel = fromRelativePosition(fromY, from) + if (sel !== null) { + shouldUpdate = true + anchorNode = sel.type.getDom() + anchorOffset = sel.offset + } + } + if (to !== null) { + let sel = fromRelativePosition(toY, to) + if (sel !== null) { + focusNode = sel.type.getDom() + focusOffset = sel.offset + shouldUpdate = true + } + } + if (shouldUpdate) { + browserSelection.setBaseAndExtent( + anchorNode, + anchorOffset, + focusNode, + focusOffset + ) + } + // delete, so the objects can be gc'd + relativeSelection = null + browserSelection = null +} diff --git a/src/Type/y-xml/utils.js b/src/Type/y-xml/utils.js index 821bbcc5..0f035323 100644 --- a/src/Type/y-xml/utils.js +++ b/src/Type/y-xml/utils.js @@ -73,7 +73,7 @@ function _insertNodeHelper (yxml, prevExpectedNode, child) { /* * 1. Check if any of the nodes was deleted * 2. Iterate over the children. - * 2.1 If a node exists without __yxml property, insert a new node + * 2.1 If a node exists without _yxml property, insert a new node * 2.2 If _contents.length < dom.childNodes.length, fill the * rest of _content with childNodes * 2.3 If a node was moved, delete it and @@ -85,7 +85,7 @@ export function applyChangesFromDom (yxml) { const y = yxml._y let knownChildren = new Set( - Array.prototype.map.call(yxml._dom.childNodes, child => child.__yxml) + Array.prototype.map.call(yxml._dom.childNodes, child => child._yxml) .filter(id => id !== undefined) ) // 1. Check if any of the nodes was deleted @@ -101,7 +101,7 @@ export function applyChangesFromDom (yxml) { let expectedNode = iterateUntilUndeleted(yxml._start) for (let domCnt = 0; domCnt < len; domCnt++) { const child = childNodes[domCnt] - const childYXml = child.__yxml + const childYXml = child._yxml if (childYXml != null) { if (childYXml === false) { // should be ignored or is going to be deleted @@ -112,7 +112,7 @@ export function applyChangesFromDom (yxml) { // 2.3 Not expected node if (childYXml._parent !== this) { // element is going to be deleted by its previous parent - child.__yxml = null + child._yxml = null } else { childYXml._delete(y) } diff --git a/src/Util/NamedEventHandler.js b/src/Util/NamedEventHandler.js index c69631c7..cbb07c9d 100644 --- a/src/Util/NamedEventHandler.js +++ b/src/Util/NamedEventHandler.js @@ -1,26 +1,31 @@ export default class NamedEventHandler { constructor () { - this._eventListener = {} + this._eventListener = new Map() } on (name, f) { - if (this._eventListener[name] == null) { - this._eventListener[name] = [] + let fSet = this._eventListener.get(name) + if (fSet === undefined) { + fSet = new Set() + this._eventListener.set(name, fSet) } - this._eventListener[name].push(f) + fSet.add(f) } off (name, f) { if (name == null || f == null) { throw new Error('You must specify event name and function!') } - let listener = this._eventListener[name] || [] - this._eventListener[name] = listener.filter(e => e !== f) - } - emit (name, value) { - let listener = this._eventListener[name] || [] - if (name === 'error' && listener.length === 0) { - console.error(value) + const listener = this._eventListener.get(name) + if (listener !== undefined) { + listener.remove(f) + } + } + emit (name, ...args) { + const listener = this._eventListener.get(name) + if (listener !== undefined) { + listener.forEach(f => f.apply(null, args)) + } else if (name === 'error') { + console.error(args[0]) } - listener.forEach(l => l(value)) } destroy () { this._eventListener = null diff --git a/src/Util/relativePosition.js b/src/Util/relativePosition.js index 11aab809..b5788065 100644 --- a/src/Util/relativePosition.js +++ b/src/Util/relativePosition.js @@ -4,14 +4,20 @@ export function getRelativePosition (type, offset) { if (offset === 0) { return ['startof', type._id.user, type._id.clock] } else { - let t = type.start - while (t !== null && t.length < offset) { + let t = type._start + while (t !== null) { + if (t._length >= offset) { + return [t._id.user, t._id.clock + offset - 1] + } + if (t._right === null) { + return [t._id.user, t._id.clock + t._length - 1] + } if (!t._deleted) { - offset -= t.length + offset -= t._length } t = t._right } - return [t._id.user, t._id.clock + offset - 1] + return null } } @@ -23,19 +29,20 @@ export function fromRelativePosition (y, rpos) { } } else { let offset = 0 - let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])) - let parent = struct._parent + let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val + const parent = struct._parent if (parent._deleted) { return null } - if (!struct.deleted) { - offset = rpos[1] - struct._id.clock + if (!struct._deleted) { + offset = rpos[1] - struct._id.clock + 1 } - while (struct.left !== null) { - struct = struct.left - if (!struct.deleted) { + struct = struct._left + while (struct !== null) { + if (!struct._deleted) { offset += struct._length } + struct = struct._left } return { type: parent, diff --git a/src/Y.js b/src/Y.js index d05c3b9d..87b4866f 100644 --- a/src/Y.js +++ b/src/Y.js @@ -36,13 +36,13 @@ export default class Y extends NamedEventHandler { this.connected = true this._missingStructs = new Map() this._readyToIntegrate = [] - this._transactionsInProgress = 0 this._transaction = null } _beforeChange () {} transact (f, remote = false) { let initialCall = this._transaction === null if (initialCall) { + this.emit('beforeTransaction', this, remote) this._transaction = new Transaction(this) } try { @@ -55,9 +55,9 @@ export default class Y extends NamedEventHandler { this._transaction.changedTypes.forEach(function (subs, type) { type._callObserver(subs, remote) }) - this._transaction = null // when all changes & events are processed, emit afterTransaction event - this.emit('afterTransaction', this) + this.emit('afterTransaction', this, remote) + this._transaction = null } } // fake _start for root properties (y.set('name', type)) diff --git a/tests-lib/helper.js b/tests-lib/helper.js index cbef8ea5..bee1111e 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -41,7 +41,7 @@ function getDeleteSet (y) { export function attrsObject (dom) { let keys = [] - let yxml = dom.__yxml + let yxml = dom._yxml for (let i = 0; i < dom.attributes.length; i++) { keys.push(dom.attributes[i].name) } @@ -60,7 +60,7 @@ export function domToJson (dom) { } else if (dom.nodeType === document.ELEMENT_NODE) { let attributes = attrsObject(dom) let children = Array.from(dom.childNodes.values()) - .filter(d => d.__yxml !== false) + .filter(d => d._yxml !== false) .map(domToJson) return { name: dom.nodeName,