Compare commits

..

24 Commits

Author SHA1 Message Date
Kevin Jahns
240cf64841 v13.0.0-65 -- distribution files 2018-07-16 18:38:27 +02:00
Kevin Jahns
548125a944 13.0.0-65 2018-07-16 18:38:09 +02:00
Kevin Jahns
a7b124ca6e 13.0.0-64 2018-07-16 18:19:36 +02:00
Kevin Jahns
4022374620 dombinding: always set browser range after change 2018-07-16 18:15:24 +02:00
Kevin Jahns
860e4d7af6 13.0.0-63 2018-06-23 00:30:45 +02:00
Kevin Jahns
6376d69b58 fix undo of map update 2018-06-23 00:29:44 +02:00
Kevin Jahns
5cf6f45f19 13.0.0-62 2018-06-13 00:08:01 +02:00
Kevin Jahns
967903673b fixed undo/redo issues and implemented ability to manually flush the UndoManager 2018-06-13 00:06:38 +02:00
Kevin Jahns
db5312443e 13.0.0-61 2018-05-18 02:02:44 +02:00
Kevin Jahns
dbda07424b fix DomBinding destroy 2018-05-18 02:01:53 +02:00
Kevin Jahns
684d38d6c8 make compatible with webpack and less sophisticated module bundlers 2018-05-13 13:07:24 +02:00
Kevin Jahns
44fa064eb2 Merge branch 'master' of github.com:y-js/yjs 2018-05-12 16:43:28 +02:00
Kevin Jahns
9b6fffd880 Add rollup dependency - closes #110 2018-05-12 16:43:05 +02:00
Kevin Jahns
e9993b2643 Merge pull request #111 from larskarbo/patch-1
(DOCS) Remove y-array duplicates
2018-05-12 16:41:14 +02:00
Kevin Jahns
762e9e8a3a do not log _start in logItemHelper. Fixes #114 2018-05-12 15:42:31 +02:00
Kevin Jahns
6ddeb788c7 Merge branch 'master' of github.com:y-js/yjs 2018-05-09 16:28:07 +02:00
Kevin Jahns
b9245f323c prefer !== undefined check instead of hasOwnProperty 2018-05-09 16:27:55 +02:00
Kevin Jahns
c0e630b635 prefer parentElement instead of parentNode 2018-05-09 14:42:24 +02:00
Kevin Jahns
e56457a0ef 13.0.0-60 2018-05-08 13:46:27 +02:00
Kevin Jahns
ca13849828 fix domBinding infinite loop 2018-05-08 13:45:51 +02:00
Kevin Jahns
92c2fbd6d3 remove debug dependency 2018-05-07 11:52:37 +02:00
Kevin Jahns
65b8921f05 13.0.0-59 2018-05-07 11:45:39 +02:00
Kevin Jahns
1ace7f4b73 fix parentNode binding in case parent doesn't fire MO 2018-05-07 11:44:22 +02:00
Lars Karbo
ed2273e2ed Remove y-array duplicates 2018-04-24 12:59:51 -07:00
27 changed files with 972 additions and 34065 deletions

View File

@@ -70,7 +70,6 @@ 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>
// .. // ..
@@ -89,7 +88,6 @@ 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)
// .. // ..
@@ -102,7 +100,6 @@ 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,9 +1,21 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-58", "version": "13.0.0-65",
"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",
@@ -1498,6 +1510,7 @@
"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"
} }
@@ -4806,7 +4819,8 @@
"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",
@@ -5629,6 +5643,16 @@
"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-58", "version": "13.0.0-65",
"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,6 +61,7 @@
"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",
@@ -71,8 +72,5 @@
"rollup-watch": "^3.2.2", "rollup-watch": "^3.2.2",
"standard": "^10.0.2", "standard": "^10.0.2",
"tag-dist-files": "^0.1.6" "tag-dist-files": "^0.1.6"
},
"dependencies": {
"debug": "^2.6.8"
} }
} }

View File

@@ -1,8 +1,9 @@
/* global MutationObserver */ /* global MutationObserver, getSelection */
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 } from './selection.js' import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } 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'
@@ -67,16 +68,25 @@ 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())
beforeTransactionSelectionFixer(y, this, transaction, remote) this._mutualExclude(() => {
beforeTransactionSelectionFixer(this, remote)
})
} }
y.on('beforeTransaction', this._beforeTransactionHandler) y.on('beforeTransaction', this._beforeTransactionHandler)
this._afterTransactionHandler = (y, transaction, remote) => { this._afterTransactionHandler = (y, transaction, remote) => {
afterTransactionSelectionFixer(y, this, transaction, remote) this._mutualExclude(() => {
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
@@ -115,27 +125,81 @@ 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
/**
* 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
}
}
}
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
}
}
}
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.unobserve(this._typeObserver) this.type.unobserveDeep(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.
* A filter defines which elements and attributes to share. * Return null if the node should be filtered. Otherwise return the Map of
* Return null if the node should be filtered. Otherwise return the Map of * accepted attributes.
* accepted attributes. *
* * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
* @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction */
*/

View File

@@ -90,21 +90,20 @@ export default function domObserver (mutations, _document) {
mutations.forEach(mutation => { mutations.forEach(mutation => {
const dom = mutation.target const dom = mutation.target
const yxml = this.domToType.get(dom) const yxml = this.domToType.get(dom)
if (yxml === false || yxml === undefined || yxml.constructor === YXmlHook) { if (yxml === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom
// dom element is filtered let parent = dom
if (yxml === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom let yParent
console.error('Yjs DomBinding: Reconstructing DomBinding, please report how to reproduce this.') do {
let parent parent = parent.parentElement
let yParent yParent = this.domToType.get(parent)
do { } while (yParent === undefined && parent !== null)
parent = dom.parentNode if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
yParent = this.domToType.get(parent) diffChildren.add(parent)
} while (yParent === undefined)
if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
diffChildren.add(parent)
}
} }
return return
} else if (yxml === false || yxml.constructor === YXmlHook) {
// dom element is filtered / a dom hook
return
} }
switch (mutation.type) { switch (mutation.type) {
case 'characterData': case 'characterData':

View File

@@ -1,5 +1,7 @@
import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js' import YXmlText from '../../Types/YXml/YXmlText.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,84 +1,35 @@
/* globals getSelection */ /* globals getSelection */
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js' import { getRelativePosition } from '../../Util/relativePosition.js'
let browserSelection = null
let relativeSelection = null let relativeSelection = null
/** function _getCurrentRelativeSelection (domBinding) {
* @private const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
*/ const baseNodeType = domBinding.domToType.get(baseNode)
export let beforeTransactionSelectionFixer const extentNodeType = domBinding.domToType.get(extentNode)
if (typeof getSelection !== 'undefined') { if (baseNodeType !== undefined && extentNodeType !== undefined) {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) { return {
if (!remote) { from: getRelativePosition(baseNodeType, baseOffset),
return to: getRelativePosition(extentNodeType, extentOffset)
}
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
} }
} }
} else { return null
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 (y, domBinding, transaction, remote) { export function afterTransactionSelectionFixer (domBinding, remote) {
if (relativeSelection === null || !remote) { if (relativeSelection !== null && remote) {
return domBinding.restoreSelection(relativeSelection)
}
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,7 +50,6 @@ 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)},start:${logID(item._start)},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)},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._start`. * which to insert to. Otherwise it is `parent._map`.
* @type {String} * @type {String}
*/ */
this._parentSub = null this._parentSub = null
@@ -120,17 +120,29 @@ export default class Item {
* *
* @private * @private
*/ */
_redo (y) { _redo (y, redoitems) {
if (this._redone !== null) { if (this._redone !== null) {
return this._redone return this._redone
} }
let struct = this._copy() let struct = this._copy()
let left = this._left let left, right
let right = this if (this._parentSub === null) {
// 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) {
parent._redo(y) // try to undo parent if it will be undone anyway
if (!redoitems.has(parent) || !parent._redo(y, redoitems)) {
return false
}
} }
if (parent._redone !== null) { if (parent._redone !== null) {
parent = parent._redone parent = parent._redone
@@ -157,7 +169,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 struct return true
} }
/** /**

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.hasOwnProperty(key) === false) { if (attributes[key] === undefined) {
attributes[key] = null attributes[key] = null
} }
} }
@@ -189,8 +189,9 @@ 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:
if (attributes.hasOwnProperty(right.key)) { const attr = attributes[right.key]
if (attributes[right.key] === right.value) { if (attr !== undefined) {
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)
@@ -253,7 +254,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
@@ -405,8 +406,9 @@ 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)
if (attributes.hasOwnProperty(item.key)) { const attr = attributes[item.key]
if (attributes[item.key] !== item.value) { if (attr !== undefined) {
if (attr !== item.value) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
@@ -433,7 +435,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.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) { if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// 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 {
@@ -505,11 +507,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.hasOwnProperty('insert')) { if (op.insert !== undefined) {
;[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.hasOwnProperty('retain')) { } else if (op.retain !== undefined) {
;[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.hasOwnProperty('delete')) { } else if (op.delete !== undefined) {
;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes) ;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes)
} }
} }

View File

@@ -1,12 +0,0 @@
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 './YXml.js' import YXmlFragment from './YXmlFragment.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js' import { createAssociation } from '../../Bindings/DomBinding/util.js'
/** /**
@@ -186,3 +186,5 @@ 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) { constructor (y, transaction, bindingInfos) {
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,15 +12,26 @@ class ReverseOperation {
this.toState = null this.toState = null
this.fromState = null this.fromState = null
} }
this.deletedStructs = transaction.deletedStructs this.deletedStructs = new Set()
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) {
let undoOp = reverseBuffer.pop() 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)
@@ -35,23 +46,39 @@ function applyReverseOperation (y, scope, reverseBuffer) {
} }
}) })
} }
for (let op of undoOp.deletedStructs) { const redoitems = new Set()
if ( for (let del of undoOp.deletedStructs) {
isParentOf(scope, op) && const fromState = del.from
op._parent !== y && const toState = new ID(fromState.user, fromState.clock + del.len - 1)
( y.os.getItemCleanStart(fromState)
op._id.user !== y.userID || y.os.getItemCleanEnd(toState)
undoOp.fromState === null || y.os.iterate(fromState, toState, op => {
op._id.clock < undoOp.fromState.clock || if (
op._id.clock > undoOp.toState.clock isParentOf(scope, op) &&
) op._parent !== y &&
) { (
performedUndo = true op._id.user !== y.userID ||
op._redo(y) undoOp.fromState === null ||
} 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
} }
@@ -66,6 +93,7 @@ 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 = []
@@ -76,16 +104,28 @@ 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) let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
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 &&
reverseOperation.created - lastUndoOp.created <= options.captureTimeout (options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout)
) { ) {
lastUndoOp.created = reverseOperation.created lastUndoOp.created = reverseOperation.created
if (reverseOperation.toState !== null) { if (reverseOperation.toState !== null) {
@@ -110,6 +150,13 @@ 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,7 +76,10 @@ export function fromRelativePosition (y, rpos) {
} else { } else {
id = new RootID(rpos[3], rpos[4]) id = new RootID(rpos[3], rpos[4])
} }
const type = y.os.get(id) let 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
} }
@@ -87,12 +90,16 @@ 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 = rpos[1] - struct._id.clock offset = diff
} }
struct = struct._left struct = struct._left
while (struct !== null) { while (struct !== null) {

View File

@@ -1,7 +1,10 @@
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 { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Types/YXml/YXml.js' import YXmlText from '../Types/YXml/YXmlText.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,7 +10,10 @@ 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 { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Types/YXml/YXml.js' import YXmlText from './Types/YXml/YXmlText.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

1309
y.node.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

33283
y.test.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long