fixed selection handler befor/after transactions

This commit is contained in:
Kevin Jahns 2017-10-28 23:02:48 +02:00
parent c619aa33d9
commit c545118637
13 changed files with 177 additions and 95 deletions

View File

@ -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

View File

@ -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()

View File

@ -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) {

View File

@ -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)

View File

@ -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])

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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

View File

@ -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,

View File

@ -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))

View File

@ -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,