Compare commits

..

1 Commits

Author SHA1 Message Date
Kevin Jahns
ed963dca41 v13.0.0-60 -- distribution files 2018-05-08 13:46:43 +02:00
27 changed files with 34267 additions and 957 deletions

View File

@@ -70,6 +70,7 @@ missing modules.
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
// .. // ..
@@ -88,6 +89,7 @@ var Y = require('yjs')
require('y-array')(Y) // add the y-array type to Yjs require('y-array')(Y) // add the y-array type to Yjs
require('y-websockets-client')(Y) require('y-websockets-client')(Y)
require('y-memory')(Y) require('y-memory')(Y)
require('y-array')(Y)
require('y-map')(Y) require('y-map')(Y)
require('y-text')(Y) require('y-text')(Y)
// .. // ..
@@ -100,6 +102,7 @@ import Y from 'yjs'
import yArray from 'y-array' import yArray from 'y-array'
import yWebsocketsClient from 'y-webrtc' import yWebsocketsClient from 'y-webrtc'
import yMemory from 'y-memory' import yMemory from 'y-memory'
import yArray from 'y-array'
import yMap from 'y-map' import yMap from 'y-map'
import yText from 'y-text' import yText from 'y-text'
// .. // ..

View File

@@ -20,7 +20,7 @@ let quill = new Quill('#quill-container', {
[{ header: [1, 2, false] }], [{ header: [1, 2, false] }],
['bold', 'italic', 'underline'], ['bold', 'italic', 'underline'],
['image', 'code-block'], ['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values [{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }], [{ script: 'sub' }, { script: 'super' }],
['link', 'image'], ['link', 'image'],
['link', 'code-block'], ['link', 'code-block'],
@@ -31,7 +31,7 @@ let quill = new Quill('#quill-container', {
} }
}, },
placeholder: 'Compose an epic...', placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble' theme: 'snow' // or 'bubble'
}) })
let cursors = quill.getModule('cursors') let cursors = quill.getModule('cursors')

View File

@@ -13,7 +13,7 @@ let quill = new Quill('#quill-container', {
[{ header: [1, 2, false] }], [{ header: [1, 2, false] }],
['bold', 'italic', 'underline'], ['bold', 'italic', 'underline'],
['image', 'code-block'], ['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values [{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }], [{ script: 'sub' }, { script: 'super' }],
['link', 'image'], ['link', 'image'],
['link', 'code-block'], ['link', 'code-block'],
@@ -21,7 +21,7 @@ let quill = new Quill('#quill-container', {
] ]
}, },
placeholder: 'Compose an epic...', placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble' theme: 'snow' // or 'bubble'
}) })
let yText = y.define('quill', Y.Text) let yText = y.define('quill', Y.Text)

View File

@@ -35,7 +35,7 @@ Y({
toolbar: [ toolbar: [
[{ size: ['small', false, 'large', 'huge'] }], [{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'], ['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values [{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }], [{ script: 'sub' }, { script: 'super' }],
['link', 'image'], ['link', 'image'],
['link', 'code-block'], ['link', 'code-block'],

28
package-lock.json generated
View File

@@ -1,21 +1,9 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-63", "version": "13.0.0-60",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@types/estree": {
"version": "0.0.38",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz",
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
"dev": true
},
"@types/node": {
"version": "6.0.110",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
"dev": true
},
"abab": { "abab": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -1510,7 +1498,6 @@
"version": "2.6.8", "version": "2.6.8",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
"dev": true,
"requires": { "requires": {
"ms": "2.0.0" "ms": "2.0.0"
} }
@@ -4819,8 +4806,7 @@
"ms": { "ms": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"dev": true
}, },
"mute-stream": { "mute-stream": {
"version": "0.0.5", "version": "0.0.5",
@@ -5643,16 +5629,6 @@
"glob": "7.1.2" "glob": "7.1.2"
} }
}, },
"rollup": {
"version": "0.58.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-0.58.2.tgz",
"integrity": "sha512-RZVvCWm9BHOYloaE6LLiE/ibpjv1CmI8F8k0B0Cp+q1eezo3cswszJH1DN0djgzSlo0hjuuCmyeI+1XOYLl4wg==",
"dev": true,
"requires": {
"@types/estree": "0.0.38",
"@types/node": "6.0.110"
}
},
"rollup-plugin-babel": { "rollup-plugin-babel": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz", "resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz",

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-63", "version": "13.0.0-60",
"description": "A framework for real-time p2p shared editing on any data", "description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js", "main": "./y.node.js",
"browser": "./y.js", "browser": "./y.js",
@@ -61,7 +61,6 @@
"esdoc-standard-plugin": "^1.0.0", "esdoc-standard-plugin": "^1.0.0",
"quill": "^1.3.5", "quill": "^1.3.5",
"quill-cursors": "^1.0.2", "quill-cursors": "^1.0.2",
"rollup": "^0.58.2",
"rollup-plugin-babel": "^2.7.1", "rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-inject": "^2.0.0", "rollup-plugin-inject": "^2.0.0",

View File

@@ -1,9 +1,8 @@
/* global MutationObserver, getSelection */ /* global MutationObserver */
import { fromRelativePosition } from '../../Util/relativePosition.js'
import Binding from '../Binding.js' import Binding from '../Binding.js'
import { createAssociation, removeAssociation } from './util.js' import { createAssociation, removeAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js' import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js' import { defaultFilter, applyFilterOnType } from './filter.js'
import typeObserver from './typeObserver.js' import typeObserver from './typeObserver.js'
import domObserver from './domObserver.js' import domObserver from './domObserver.js'
@@ -68,25 +67,16 @@ export default class DomBinding extends Binding {
characterData: true, characterData: true,
subtree: true subtree: true
}) })
this._currentSel = null
document.addEventListener('selectionchange', () => {
this._currentSel = getCurrentRelativeSelection(this)
})
const y = type._y const y = type._y
this.y = y
// Force flush dom changes before Type changes are applied (they might // Force flush dom changes before Type changes are applied (they might
// modify the dom) // modify the dom)
this._beforeTransactionHandler = (y, transaction, remote) => { this._beforeTransactionHandler = (y, transaction, remote) => {
this._domObserver(this._mutationObserver.takeRecords()) this._domObserver(this._mutationObserver.takeRecords())
this._mutualExclude(() => { beforeTransactionSelectionFixer(y, this, transaction, remote)
beforeTransactionSelectionFixer(this, remote)
})
} }
y.on('beforeTransaction', this._beforeTransactionHandler) y.on('beforeTransaction', this._beforeTransactionHandler)
this._afterTransactionHandler = (y, transaction, remote) => { this._afterTransactionHandler = (y, transaction, remote) => {
this._mutualExclude(() => { afterTransactionSelectionFixer(y, this, transaction, remote)
afterTransactionSelectionFixer(this, remote)
})
// remove associations // remove associations
// TODO: this could be done more efficiently // TODO: this could be done more efficiently
// e.g. Always delete using the following approach, or removeAssociation // e.g. Always delete using the following approach, or removeAssociation
@@ -125,86 +115,27 @@ export default class DomBinding extends Binding {
// TODO: apply filter to all elements // TODO: apply filter to all elements
} }
_getUndoStackInfo () {
return this.getSelection()
}
_restoreUndoStackInfo (info) {
this.restoreSelection(info)
}
getSelection () {
return this._currentSel
}
restoreSelection (selection) {
if (selection !== null) {
const { to, from } = selection
let shouldUpdate = false
/**
* There is little information on the difference between anchor/focus and base/extent.
* MDN doesn't even mention base/extent anymore.. though you still have to call
* setBaseAndExtent to change the selection..
* I can observe that base/extend refer to notes higher up in the xml hierachy.
* Espesially for undo/redo this is preferred. If this becomes a problem in the future,
* we should probably go back to anchor/focus.
*/
const browserSelection = getSelection()
let { baseNode, baseOffset, extentNode, extentOffset } = browserSelection
if (from !== null) {
let sel = fromRelativePosition(this.y, from)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== baseNode || offset !== baseOffset) {
baseNode = node
baseOffset = offset
shouldUpdate = true
}
}
}
if (to !== null) {
let sel = fromRelativePosition(this.y, to)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== extentNode || offset !== extentOffset) {
extentNode = node
extentOffset = offset
shouldUpdate = true
}
}
}
if (shouldUpdate) {
browserSelection.setBaseAndExtent(
baseNode,
baseOffset,
extentNode,
extentOffset
)
}
}
}
/** /**
* Remove all properties that are handled by this class. * Remove all properties that are handled by this class.
*/ */
destroy () { destroy () {
this.domToType = null this.domToType = null
this.typeToDom = null this.typeToDom = null
this.type.unobserveDeep(this._typeObserver) this.type.unobserve(this._typeObserver)
this._mutationObserver.disconnect() this._mutationObserver.disconnect()
const y = this.type._y const y = this.type._y
y.off('beforeTransaction', this._beforeTransactionHandler) y.off('beforeTransaction', this._beforeTransactionHandler)
y.off('beforeObserverCalls', this._beforeObserverCallsHandler) y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
y.off('afterObserverCalls', this._afterObserverCallsHandler)
y.off('afterTransaction', this._afterTransactionHandler) y.off('afterTransaction', this._afterTransactionHandler)
super.destroy() super.destroy()
} }
} }
/**
* A filter defines which elements and attributes to share. /**
* Return null if the node should be filtered. Otherwise return the Map of * A filter defines which elements and attributes to share.
* accepted attributes. * Return null if the node should be filtered. Otherwise return the Map of
* * accepted attributes.
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction *
*/ * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
*/

View File

@@ -94,7 +94,7 @@ export default function domObserver (mutations, _document) {
let parent = dom let parent = dom
let yParent let yParent
do { do {
parent = parent.parentElement parent = parent.parentNode
yParent = this.domToType.get(parent) yParent = this.domToType.get(parent)
} while (yParent === undefined && parent !== null) } while (yParent === undefined && parent !== null)
if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) { if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {

View File

@@ -1,7 +1,5 @@
import YXmlText from '../../Types/YXml/YXmlText.js' import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import YXmlElement from '../../Types/YXml/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js' import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js' import { filterDomAttributes, defaultFilter } from './filter.js'

View File

@@ -1,35 +1,84 @@
/* globals getSelection */ /* globals getSelection */
import { getRelativePosition } from '../../Util/relativePosition.js' import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
let browserSelection = null
let relativeSelection = null let relativeSelection = null
function _getCurrentRelativeSelection (domBinding) { /**
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection() * @private
const baseNodeType = domBinding.domToType.get(baseNode) */
const extentNodeType = domBinding.domToType.get(extentNode) export let beforeTransactionSelectionFixer
if (baseNodeType !== undefined && extentNodeType !== undefined) { if (typeof getSelection !== 'undefined') {
return { beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
from: getRelativePosition(baseNodeType, baseOffset), if (!remote) {
to: getRelativePosition(extentNodeType, extentOffset) return
}
relativeSelection = { from: null, to: null, fromY: null, toY: null }
browserSelection = getSelection()
const anchorNode = browserSelection.anchorNode
const anchorNodeType = domBinding.domToType.get(anchorNode)
if (anchorNode !== null && anchorNodeType !== undefined) {
relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
relativeSelection.fromY = anchorNodeType._y
}
const focusNode = browserSelection.focusNode
const focusNodeType = domBinding.domToType.get(focusNode)
if (focusNode !== null && focusNodeType !== undefined) {
relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
relativeSelection.toY = focusNodeType._y
} }
} }
return null } else {
} beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : () => null
export function beforeTransactionSelectionFixer (domBinding, remote) {
if (remote) {
relativeSelection = getCurrentRelativeSelection(domBinding)
}
} }
/** /**
* @private * @private
*/ */
export function afterTransactionSelectionFixer (domBinding, remote) { export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
if (relativeSelection !== null && remote) { if (relativeSelection === null || !remote) {
domBinding.restoreSelection(relativeSelection) 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) {
let node = domBinding.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== anchorNode || offset !== anchorOffset) {
anchorNode = node
anchorOffset = offset
shouldUpdate = true
}
}
}
if (to !== null) {
let sel = fromRelativePosition(toY, to)
if (sel !== null) {
let node = domBinding.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== focusNode || offset !== focusOffset) {
focusNode = node
focusOffset = offset
shouldUpdate = true
}
}
}
if (shouldUpdate) {
browserSelection.setBaseAndExtent(
anchorNode,
anchorOffset,
focusNode,
focusOffset
)
} }
} }

View File

@@ -50,6 +50,7 @@ export default function typeObserver (events) {
if (dom !== undefined && dom !== false) { if (dom !== undefined && dom !== false) {
if (yxml.constructor === YXmlText) { if (yxml.constructor === YXmlText) {
dom.nodeValue = yxml.toString() dom.nodeValue = yxml.toString()
// TODO: use hasOwnProperty instead of === undefined check
} else if (event.attributesChanged !== undefined) { } else if (event.attributesChanged !== undefined) {
// update attributes // update attributes
event.attributesChanged.forEach(attributeName => { event.attributesChanged.forEach(attributeName => {

View File

@@ -61,5 +61,5 @@ export function logID (id) {
export function logItemHelper (name, item, append) { export function logItemHelper (name, item, append) {
const left = item._left !== null ? item._left._lastId : null const left = item._left !== null ? item._left._lastId : null
const origin = item._origin !== null ? item._origin._lastId : null const origin = item._origin !== null ? item._origin._lastId : null
return `${name}(id:${logID(item._id)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})` return `${name}(id:${logID(item._id)},start:${logID(item._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
} }

View File

@@ -87,7 +87,7 @@ export default class Item {
* If the parent refers to this item with some kind of key (e.g. YMap, the * If the parent refers to this item with some kind of key (e.g. YMap, the
* key is specified here. The key is then used to refer to the list in which * key is specified here. The key is then used to refer to the list in which
* to insert this item. If `parentSub = null` type._start is the list in * to insert this item. If `parentSub = null` type._start is the list in
* which to insert to. Otherwise it is `parent._map`. * which to insert to. Otherwise it is `parent._start`.
* @type {String} * @type {String}
*/ */
this._parentSub = null this._parentSub = null
@@ -120,29 +120,17 @@ export default class Item {
* *
* @private * @private
*/ */
_redo (y, redoitems) { _redo (y) {
if (this._redone !== null) { if (this._redone !== null) {
return this._redone return this._redone
} }
let struct = this._copy() let struct = this._copy()
let left, right let left = this._left
if (this._parentSub === null) { let right = this
// Is an array item. Insert at the old position
left = this._left
right = this
} else {
// Is a map item. Insert at the start
left = null
right = this._parent._map.get(this._parentSub)
right._delete(y)
}
let parent = this._parent let parent = this._parent
// make sure that parent is redone // make sure that parent is redone
if (parent._deleted === true && parent._redone === null) { if (parent._deleted === true && parent._redone === null) {
// try to undo parent if it will be undone anyway parent._redo(y)
if (!redoitems.has(parent) || !parent._redo(y, redoitems)) {
return false
}
} }
if (parent._redone !== null) { if (parent._redone !== null) {
parent = parent._redone parent = parent._redone
@@ -169,7 +157,7 @@ export default class Item {
struct._parentSub = this._parentSub struct._parentSub = this._parentSub
struct._integrate(y) struct._integrate(y)
this._redone = struct this._redone = struct
return true return struct
} }
/** /**

View File

@@ -155,7 +155,7 @@ function insertAttributes (y, parent, left, right, attributes, currentAttributes
*/ */
function insertText (y, text, parent, left, right, currentAttributes, attributes) { function insertText (y, text, parent, left, right, currentAttributes, attributes) {
for (let [key] of currentAttributes) { for (let [key] of currentAttributes) {
if (attributes[key] === undefined) { if (attributes.hasOwnProperty(key) === false) {
attributes[key] = null attributes[key] = null
} }
} }
@@ -189,9 +189,8 @@ function formatText (y, length, parent, left, right, currentAttributes, attribut
if (right._deleted === false) { if (right._deleted === false) {
switch (right.constructor) { switch (right.constructor) {
case ItemFormat: case ItemFormat:
const attr = attributes[right.key] if (attributes.hasOwnProperty(right.key)) {
if (attr !== undefined) { if (attributes[right.key] === right.value) {
if (attr === right.value) {
negatedAttributes.delete(right.key) negatedAttributes.delete(right.key)
} else { } else {
negatedAttributes.set(right.key, right.value) negatedAttributes.set(right.key, right.value)
@@ -254,7 +253,7 @@ function deleteText (y, length, parent, left, right, currentAttributes) {
* @typedef {Array<Object>} Delta * @typedef {Array<Object>} Delta
*/ */
/** /**
* Attributes that can be assigned to a selection of text. * Attributes that can be assigned to a selection of text.
* *
* @example * @example
@@ -406,9 +405,8 @@ class YTextEvent extends YArrayEvent {
} }
} else if (item._deleted === false) { } else if (item._deleted === false) {
oldAttributes.set(item.key, item.value) oldAttributes.set(item.key, item.value)
const attr = attributes[item.key] if (attributes.hasOwnProperty(item.key)) {
if (attr !== undefined) { if (attributes[item.key] !== item.value) {
if (attr !== item.value) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
@@ -435,7 +433,7 @@ class YTextEvent extends YArrayEvent {
addOp() addOp()
while (this._delta.length > 0) { while (this._delta.length > 0) {
let lastOp = this._delta[this._delta.length - 1] let lastOp = this._delta[this._delta.length - 1]
if (lastOp.retain !== undefined && lastOp.attributes === undefined) { if (lastOp.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) {
// retain delta's if they don't assign attributes // retain delta's if they don't assign attributes
this._delta.pop() this._delta.pop()
} else { } else {
@@ -507,11 +505,11 @@ export default class YText extends YArray {
const currentAttributes = new Map() const currentAttributes = new Map()
for (let i = 0; i < delta.length; i++) { for (let i = 0; i < delta.length; i++) {
let op = delta[i] let op = delta[i]
if (op.insert !== undefined) { if (op.hasOwnProperty('insert')) {
;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {}) ;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {})
} else if (op.retain !== undefined) { } else if (op.hasOwnProperty('retain')) {
;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {}) ;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {})
} else if (op.delete !== undefined) { } else if (op.hasOwnProperty('delete')) {
;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes) ;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes)
} }
} }

12
src/Types/YXml/YXml.js Normal file
View File

@@ -0,0 +1,12 @@
import YXmlFragment from './YXmlFragment.js'
import YXmlElement from './YXmlElement.js'
import YXmlHook from './YXmlHook.js'
export { default as YXmlFragment } from './YXmlFragment.js'
export { default as YXmlElement } from './YXmlElement.js'
export { default as YXmlText } from './YXmlText.js'
export { default as YXmlHook } from './YXmlHook.js'
YXmlFragment._YXmlElement = YXmlElement
YXmlFragment._YXmlHook = YXmlHook

View File

@@ -1,5 +1,5 @@
import YMap from '../YMap/YMap.js' import YMap from '../YMap/YMap.js'
import YXmlFragment from './YXmlFragment.js' import { YXmlFragment } from './YXml.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js' import { createAssociation } from '../../Bindings/DomBinding/util.js'
/** /**
@@ -186,5 +186,3 @@ export default class YXmlElement extends YXmlFragment {
return dom return dom
} }
} }
YXmlFragment._YXmlElement = YXmlElement

View File

@@ -2,7 +2,7 @@ import ID from './ID/ID.js'
import isParentOf from './isParentOf.js' import isParentOf from './isParentOf.js'
class ReverseOperation { class ReverseOperation {
constructor (y, transaction, bindingInfos) { constructor (y, transaction) {
this.created = new Date() this.created = new Date()
const beforeState = transaction.beforeState const beforeState = transaction.beforeState
if (beforeState.has(y.userID)) { if (beforeState.has(y.userID)) {
@@ -12,26 +12,15 @@ class ReverseOperation {
this.toState = null this.toState = null
this.fromState = null this.fromState = null
} }
this.deletedStructs = new Set() this.deletedStructs = transaction.deletedStructs
transaction.deletedStructs.forEach(struct => {
this.deletedStructs.add({
from: struct._id,
len: struct._length
})
})
/**
* Maps from binding to binding information (e.g. cursor information)
*/
this.bindingInfos = bindingInfos
} }
} }
function applyReverseOperation (y, scope, reverseBuffer) { function applyReverseOperation (y, scope, reverseBuffer) {
let performedUndo = false let performedUndo = false
let undoOp
y.transact(() => { y.transact(() => {
while (!performedUndo && reverseBuffer.length > 0) { while (!performedUndo && reverseBuffer.length > 0) {
undoOp = reverseBuffer.pop() let undoOp = reverseBuffer.pop()
// make sure that it is possible to iterate {from}-{to} // make sure that it is possible to iterate {from}-{to}
if (undoOp.fromState !== null) { if (undoOp.fromState !== null) {
y.os.getItemCleanStart(undoOp.fromState) y.os.getItemCleanStart(undoOp.fromState)
@@ -46,39 +35,23 @@ function applyReverseOperation (y, scope, reverseBuffer) {
} }
}) })
} }
const redoitems = new Set() for (let op of undoOp.deletedStructs) {
for (let del of undoOp.deletedStructs) { if (
const fromState = del.from isParentOf(scope, op) &&
const toState = new ID(fromState.user, fromState.clock + del.len - 1) op._parent !== y &&
y.os.getItemCleanStart(fromState) (
y.os.getItemCleanEnd(toState) op._id.user !== y.userID ||
y.os.iterate(fromState, toState, op => { undoOp.fromState === null ||
if ( op._id.clock < undoOp.fromState.clock ||
isParentOf(scope, op) && op._id.clock > undoOp.toState.clock
op._parent !== y && )
( ) {
op._id.user !== y.userID || performedUndo = true
undoOp.fromState === null || op._redo(y)
op._id.clock < undoOp.fromState.clock || }
op._id.clock > undoOp.toState.clock
)
) {
redoitems.add(op)
}
})
} }
redoitems.forEach(op => {
const opUndone = op._redo(y, redoitems)
performedUndo = performedUndo || opUndone
})
} }
}) })
if (performedUndo) {
// should be performed after the undo transaction
undoOp.bindingInfos.forEach((info, binding) => {
binding._restoreUndoStackInfo(info)
})
}
return performedUndo return performedUndo
} }
@@ -93,7 +66,6 @@ export default class UndoManager {
*/ */
constructor (scope, options = {}) { constructor (scope, options = {}) {
this.options = options this.options = options
this._bindings = new Set(options.bindings)
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
this._undoBuffer = [] this._undoBuffer = []
this._redoBuffer = [] this._redoBuffer = []
@@ -104,28 +76,16 @@ export default class UndoManager {
const y = scope._y const y = scope._y
this.y = y this.y = y
y._hasUndoManager = true y._hasUndoManager = true
let bindingInfos
y.on('beforeTransaction', (y, transaction, remote) => {
if (!remote) {
// Store binding information before transaction is executed
// By restoring the binding information, we can make sure that the state
// before the transaction can be recovered
bindingInfos = new Map()
this._bindings.forEach(binding => {
bindingInfos.set(binding, binding._getUndoStackInfo())
})
}
})
y.on('afterTransaction', (y, transaction, remote) => { y.on('afterTransaction', (y, transaction, remote) => {
if (!remote && transaction.changedParentTypes.has(scope)) { if (!remote && transaction.changedParentTypes.has(scope)) {
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos) let reverseOperation = new ReverseOperation(y, transaction)
if (!this._undoing) { if (!this._undoing) {
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
if ( if (
this._redoing === false && this._redoing === false &&
this._lastTransactionWasUndo === false && this._lastTransactionWasUndo === false &&
lastUndoOp !== null && lastUndoOp !== null &&
(options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout) reverseOperation.created - lastUndoOp.created <= options.captureTimeout
) { ) {
lastUndoOp.created = reverseOperation.created lastUndoOp.created = reverseOperation.created
if (reverseOperation.toState !== null) { if (reverseOperation.toState !== null) {
@@ -150,13 +110,6 @@ export default class UndoManager {
}) })
} }
/**
* Enforce that the next change is created as a separate item in the undo stack
*/
flushChanges () {
this._lastTransactionWasUndo = true
}
/** /**
* Undo the last locally created change. * Undo the last locally created change.
*/ */

View File

@@ -76,10 +76,7 @@ export function fromRelativePosition (y, rpos) {
} else { } else {
id = new RootID(rpos[3], rpos[4]) id = new RootID(rpos[3], rpos[4])
} }
let type = y.os.get(id) const type = y.os.get(id)
while (type._redone !== null) {
type = type._redone
}
if (type === null || type.constructor === GC) { if (type === null || type.constructor === GC) {
return null return null
} }
@@ -90,16 +87,12 @@ export function fromRelativePosition (y, rpos) {
} else { } else {
let offset = 0 let offset = 0
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
const diff = rpos[1] - struct._id.clock
while (struct._redone !== null) {
struct = struct._redone
}
const parent = struct._parent const parent = struct._parent
if (struct.constructor === GC || parent._deleted) { if (struct.constructor === GC || parent._deleted) {
return null return null
} }
if (!struct._deleted) { if (!struct._deleted) {
offset = diff offset = rpos[1] - struct._id.clock
} }
struct = struct._left struct = struct._left
while (struct !== null) { while (struct !== null) {

View File

@@ -1,10 +1,7 @@
import YArray from '../Types/YArray/YArray.js' import YArray from '../Types/YArray/YArray.js'
import YMap from '../Types/YMap/YMap.js' import YMap from '../Types/YMap/YMap.js'
import YText from '../Types/YText/YText.js' import YText from '../Types/YText/YText.js'
import YXmlText from '../Types/YXml/YXmlText.js' import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Types/YXml/YXml.js'
import YXmlHook from '../Types/YXml/YXmlHook.js'
import YXmlFragment from '../Types/YXml/YXmlFragment.js'
import YXmlElement from '../Types/YXml/YXmlElement.js'
import Delete from '../Struct/Delete.js' import Delete from '../Struct/Delete.js'
import ItemJSON from '../Struct/ItemJSON.js' import ItemJSON from '../Struct/ItemJSON.js'

View File

@@ -10,10 +10,7 @@ import Persistence from './Persistence.js'
import YArray from './Types/YArray/YArray.js' import YArray from './Types/YArray/YArray.js'
import YMap from './Types/YMap/YMap.js' import YMap from './Types/YMap/YMap.js'
import YText from './Types/YText/YText.js' import YText from './Types/YText/YText.js'
import YXmlText from './Types/YXml/YXmlText.js' import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Types/YXml/YXml.js'
import YXmlHook from './Types/YXml/YXmlHook.js'
import YXmlFragment from './Types/YXml/YXmlFragment.js'
import YXmlElement from './Types/YXml/YXmlElement.js'
import BinaryDecoder from './Util/Binary/Decoder.js' import BinaryDecoder from './Util/Binary/Decoder.js'
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js' import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
import { registerStruct } from './Util/structReferences.js' import { registerStruct } from './Util/structReferences.js'

View File

@@ -111,7 +111,7 @@ function compareEvent (t, is, should) {
t.assert( t.assert(
should[key] === is[key] || should[key] === is[key] ||
JSON.stringify(should[key]) === JSON.stringify(is[key]) JSON.stringify(should[key]) === JSON.stringify(is[key])
, 'event works as expected' , 'event works as expected'
) )
} }
} }

8
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1285
y.node.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

33515
y.test.js Normal file

File diff suppressed because one or more lines are too long

1
y.test.js.map Normal file

File diff suppressed because one or more lines are too long