Compare commits

..

1 Commits

Author SHA1 Message Date
Kevin Jahns
ceb7328ec0 v13.0.0-25 -- distribution files 2017-11-08 17:31:57 -08:00
28 changed files with 796 additions and 1343 deletions

View File

@@ -1,15 +1,4 @@
/* global Y, HTMLElement, customElements */
class MagicTable extends HTMLElement {
constructor () {
super()
var shadow = this.attachShadow({mode: 'open'})
setTimeout(() => {
shadow.append(this.childNodes[0])
}, 1000)
}
}
customElements.define('magic-table', MagicTable)
/* global Y */
// initialize a shared object. This function call returns a promise!
let y = new Y({
@@ -28,14 +17,16 @@ window.onload = function () {
window.yXmlType.bindToDom(document.body)
}
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
captureTimeout: 500
captureTimeout: 1000
})
document.onkeydown = function interceptUndoRedo (e) {
if (e.keyCode === 90 && e.metaKey) {
if (e.keyCode === 90 && e.ctrlKey) {
if (!e.shiftKey) {
console.info('Undo!')
window.undoManager.undo()
} else {
console.info('Redo!')
window.undoManager.redo()
}
e.preventDefault()

View File

@@ -14,7 +14,10 @@
}
</style>
<script src="../bower_components/yjs/y.js"></script>
<script src="../bower_components/y-array/y-array.js"></script>
<script src="../bower_components/y-text/y-text.js"></script>
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
<script src="../bower_components/y-memory/y-memory.js"></script>
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
<script src="./index.js"></script>
</body>

36
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-32",
"version": "13.0.0-25",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -2808,6 +2808,14 @@
}
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"string-width": {
"version": "1.0.2",
"bundled": true,
@@ -2818,14 +2826,6 @@
"strip-ansi": "3.0.1"
}
},
"string_decoder": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"requires": {
"safe-buffer": "5.0.1"
}
},
"stringstream": {
"version": "0.0.5",
"bundled": true,
@@ -4868,6 +4868,15 @@
"duplexer": "0.1.1"
}
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -4889,15 +4898,6 @@
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz",
"integrity": "sha1-aybpvTr8qnvjtCabUm3huCAArHg="
},
"string_decoder": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
"integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-34",
"version": "13.0.0-25",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",
@@ -15,8 +15,7 @@
"postpublish": "tag-dist-files --overwrite-existing-tag"
},
"files": [
"y.*",
"src/*"
"y.*"
],
"standard": {
"ignore": [

View File

@@ -5,13 +5,9 @@ import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
input: 'src/Y.js',
name: 'Y',
sourcemap: true,
output: {
file: 'y.js',
format: 'umd'
},
entry: 'src/Y.js',
moduleName: 'Y',
format: 'umd',
plugins: [
nodeResolve({
main: true,
@@ -36,6 +32,8 @@ export default {
}
})
],
dest: 'y.js',
sourceMap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}

View File

@@ -3,13 +3,9 @@ import commonjs from 'rollup-plugin-commonjs'
var pkg = require('./package.json')
export default {
input: 'src/y-dist.cjs.js',
nameame: 'Y',
sourcemap: true,
output: {
file: 'y.node.js',
format: 'cjs'
},
entry: 'src/y-dist.cjs.js',
moduleName: 'Y',
format: 'cjs',
plugins: [
nodeResolve({
main: true,
@@ -18,6 +14,8 @@ export default {
}),
commonjs()
],
dest: 'y.node.js',
sourceMap: true,
banner: `
/**
* ${pkg.name} - ${pkg.description}

View File

@@ -3,13 +3,9 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry'
export default {
input: 'test/y-xml.tests.js',
name: 'y-tests',
sourcemap: true,
output: {
file: 'y.test.js',
format: 'umd'
},
entry: 'test/y-xml.tests.js',
moduleName: 'y-tests',
format: 'umd',
plugins: [
nodeResolve({
main: true,
@@ -18,5 +14,7 @@ export default {
}),
commonjs(),
multiEntry()
]
],
dest: 'y.test.js',
sourceMap: true
}

View File

@@ -20,13 +20,7 @@ export default class ItemJSON extends Item {
this._content = new Array(len)
for (let i = 0; i < len; i++) {
const ctnt = decoder.readVarString()
let parsed
if (ctnt === 'undefined') {
parsed = undefined
} else {
parsed = JSON.parse(ctnt)
}
this._content[i] = parsed
this._content[i] = JSON.parse(ctnt)
}
return missing
}
@@ -35,14 +29,7 @@ export default class ItemJSON extends Item {
let len = this._content.length
encoder.writeVarUint(len)
for (let i = 0; i < len; i++) {
let encoded
let content = this._content[i]
if (content === undefined) {
encoded = 'undefined'
} else {
encoded = JSON.stringify(content)
}
encoder.writeVarString(encoded)
encoder.writeVarString(JSON.stringify(this._content[i]))
}
}
_logString () {

View File

@@ -65,9 +65,9 @@ export default class Type extends Item {
}
return path
}
_callEventHandler (transaction, event) {
const changedParentTypes = transaction.changedParentTypes
this._eventHandler.callEventListeners(transaction, event)
_callEventHandler (event) {
const changedParentTypes = this._y._transaction.changedParentTypes
this._eventHandler.callEventListeners(event)
let type = this
while (type !== this._y) {
let events = changedParentTypes.get(type)

View File

@@ -1,6 +1,5 @@
import Type from '../Struct/Type.js'
import ItemJSON from '../Struct/ItemJSON.js'
import ItemString from '../Struct/ItemString.js'
import { logID } from '../MessageHandler/messageToString.js'
import YEvent from '../Util/YEvent.js'
@@ -12,19 +11,15 @@ class YArrayEvent extends YEvent {
}
export default class YArray extends Type {
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YArrayEvent(this, remote))
_callObserver (parentSubs, remote) {
this._callEventHandler(new YArrayEvent(this, remote))
}
get (pos) {
let n = this._start
while (n !== null) {
if (!n._deleted) {
if (pos < n._length) {
if (n.constructor === ItemJSON || n.constructor === ItemString) {
return n._content[pos]
} else {
return n
}
return n._content[n._length - pos]
}
pos -= n._length
}

View File

@@ -13,8 +13,8 @@ class YMapEvent extends YEvent {
}
export default class YMap extends Type {
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
_callObserver (parentSubs, remote) {
this._callEventHandler(new YMapEvent(this, parentSubs, remote))
}
toJSON () {
const map = {}

View File

@@ -5,7 +5,7 @@ import YMap from '../YMap.js'
import YXmlFragment from './YXmlFragment.js'
export default class YXmlElement extends YXmlFragment {
constructor (arg1, arg2, _document) {
constructor (arg1, arg2) {
super()
this.nodeName = null
this._scrollElement = null
@@ -13,7 +13,7 @@ export default class YXmlElement extends YXmlFragment {
this.nodeName = arg1.toUpperCase()
} else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === arg1.ELEMENT_NODE) {
this.nodeName = arg1.nodeName
this._setDom(arg1, _document)
this._setDom(arg1)
} else {
this.nodeName = 'UNDEFINED'
}
@@ -26,25 +26,28 @@ export default class YXmlElement extends YXmlFragment {
struct.nodeName = this.nodeName
return struct
}
_setDom (dom, _document) {
_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..
throw new Error('Already bound to an YXml type')
} else {
this._dom = dom
dom._yxml = this
// tag is already set in constructor
// set attributes
let attributes = new Map()
let attrNames = []
for (let i = 0; i < dom.attributes.length; i++) {
let attr = dom.attributes[i]
attributes.set(attr.name, attr.value)
attrNames.push(dom.attributes[i].name)
}
attributes = this._domFilter(dom, attributes)
attributes.forEach((value, name) => {
this.setAttribute(name, value)
})
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes), _document)
this._bindToDom(dom, _document)
attrNames = this._domFilter(dom, attrNames)
for (let i = 0; i < attrNames.length; i++) {
let attrName = attrNames[i]
let attrValue = dom.getAttribute(attrName)
this.setAttribute(attrName, attrValue)
}
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes))
this._bindToDom(dom)
return dom
}
}
@@ -103,9 +106,7 @@ export default class YXmlElement extends YXmlFragment {
getAttributes () {
const obj = {}
for (let [key, value] of this._map) {
if (!value._deleted) {
obj[key] = value._content[0]
}
obj[key] = value._content[0]
}
return obj
}
@@ -114,6 +115,7 @@ export default class YXmlElement extends YXmlFragment {
let dom = this._dom
if (dom == null) {
dom = _document.createElement(this.nodeName)
this._dom = dom
dom._yxml = this
let attrs = this.getAttributes()
for (let key in attrs) {
@@ -122,7 +124,7 @@ export default class YXmlElement extends YXmlFragment {
this.forEach(yxml => {
dom.appendChild(yxml.getDom(_document))
})
this._bindToDom(dom, _document)
this._bindToDom(dom)
}
return dom
}

View File

@@ -9,18 +9,18 @@ import YXmlEvent from './YXmlEvent.js'
import { logID } from '../../MessageHandler/messageToString.js'
import diff from 'fast-diff'
function domToYXml (parent, doms, _document) {
function domToYXml (parent, doms) {
const types = []
doms.forEach(d => {
if (d._yxml != null && d._yxml !== false) {
d._yxml._unbindFromDom()
}
if (parent._domFilter(d.nodeName, new Map()) !== null) {
if (parent._domFilter(d, []) !== null) {
let type
if (d.nodeType === d.TEXT_NODE) {
type = new YXmlText(d)
} else if (d.nodeType === d.ELEMENT_NODE) {
type = new YXmlFragment._YXmlElement(d, parent._domFilter, _document)
type = new YXmlFragment._YXmlElement(d, parent._domFilter)
} else {
throw new Error('Unsupported node!')
}
@@ -98,9 +98,7 @@ export default class YXmlFragment extends YArray {
} catch (e) {
console.error(e)
}
if (this._domObserver !== null) {
this._domObserver.takeRecords()
}
this._domObserver.takeRecords()
token = true
}
}
@@ -144,8 +142,8 @@ export default class YXmlFragment extends YArray {
xml.setDomFilter(f)
})
}
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote))
_callObserver (parentSubs, remote) {
this._callEventHandler(new YXmlEvent(this, parentSubs, remote))
}
toString () {
return this.map(xml => xml.toString()).join('')
@@ -164,167 +162,113 @@ export default class YXmlFragment extends YArray {
this._dom = null
}
}
insertDomElementsAfter (prev, doms, _document) {
const types = domToYXml(this, doms, _document)
insertDomElementsAfter (prev, doms) {
const types = domToYXml(this, doms)
this.insertAfter(prev, types)
return types
}
insertDomElements (pos, doms, _document) {
const types = domToYXml(this, doms, _document)
insertDomElements (pos, doms) {
const types = domToYXml(this, doms)
this.insert(pos, types)
return types
}
getDom () {
return this._dom
}
bindToDom (dom, _document) {
bindToDom (dom) {
if (this._dom != null) {
this._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!')
}
dom.innerHTML = ''
this._dom = dom
dom._yxml = this
this.forEach(t => {
dom.insertBefore(t.getDom(_document), null)
dom.insertBefore(t.getDom(), null)
})
this._bindToDom(dom, _document)
this._bindToDom(dom)
}
// binds to a dom element
// Only call if dom and YXml are isomorph
_bindToDom (dom, _document) {
_document = _document || document
this._dom = dom
dom._yxml = this
// TODO: refine this..
if ((this.constructor !== YXmlFragment && this._parent !== this._y) || this._parent === null) {
// TODO: only top level YXmlFragment can bind. Also allow YXmlElements..
_bindToDom (dom) {
if (this._parent === null || this._parent._dom != null || typeof MutationObserver === 'undefined') {
// only bind if parent did not already bind
return
}
this._y.on('beforeTransaction', () => {
this._domObserverListener(this._domObserver.takeRecords())
})
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
this._y.on('afterTransaction', afterTransactionSelectionFixer)
const applyFilter = (type) => {
if (type._deleted) {
return
}
// check if type is a child of this
let isChild = false
let p = type
while (p !== this._y) {
if (p === this) {
isChild = true
break
}
p = p._parent
}
if (!isChild) {
return
}
// filter attributes
let attributes = new Map()
if (type.getAttributes !== undefined) {
let attrs = type.getAttributes()
for (let key in attrs) {
attributes.set(key, attrs[key])
}
}
let result = this._domFilter(type.nodeName, new Map(attributes))
if (result === null) {
type._delete(this._y)
} else {
attributes.forEach((value, key) => {
if (!result.has(key)) {
type.removeAttribute(key)
}
})
}
}
this._y.on('beforeObserverCalls', function (y, transaction) {
// apply dom filter to new and changed types
transaction.changedTypes.forEach(function (subs, type) {
if (subs.size > 1 || !subs.has(null)) {
// only apply changes on attributes
applyFilter(type)
}
})
transaction.newTypes.forEach(applyFilter)
})
// Apply Y.Xml events to dom
this.observeDeep(events => {
reflectChangesOnDom.call(this, events, _document)
})
this.observeDeep(reflectChangesOnDom.bind(this))
// Apply Dom changes on Y.Xml
if (typeof MutationObserver !== 'undefined') {
this._y.on('beforeTransaction', () => {
this._domObserverListener(this._domObserver.takeRecords())
})
this._domObserverListener = mutations => {
this._mutualExclude(() => {
this._y.transact(() => {
let diffChildren = new Set()
mutations.forEach(mutation => {
const dom = mutation.target
const yxml = dom._yxml
if (yxml == null) {
// dom element is filtered
return
}
switch (mutation.type) {
case 'characterData':
var diffs = diff(yxml.toString(), dom.nodeValue)
var pos = 0
for (var i = 0; i < diffs.length; i++) {
var d = diffs[i]
if (d[0] === 0) { // EQUAL
pos += d[1].length
} else if (d[0] === -1) { // DELETE
yxml.delete(pos, d[1].length)
} else { // INSERT
yxml.insert(pos, d[1])
pos += d[1].length
this._domObserverListener = mutations => {
this._mutualExclude(() => {
this._y.transact(() => {
let diffChildren = new Set()
mutations.forEach(mutation => {
const dom = mutation.target
const yxml = dom._yxml
if (yxml == null) {
// dom element is filtered
return
}
switch (mutation.type) {
case 'characterData':
var diffs = diff(yxml.toString(), dom.nodeValue)
var pos = 0
for (var i = 0; i < diffs.length; i++) {
var d = diffs[i]
if (d[0] === 0) { // EQUAL
pos += d[1].length
} else if (d[0] === -1) { // DELETE
yxml.delete(pos, d[1].length)
} else { // INSERT
yxml.insert(pos, d[1])
pos += d[1].length
}
}
break
case 'attributes':
let name = mutation.attributeName
// check if filter accepts attribute
if (this._domFilter(dom, [name]).length > 0 && this.constructor !== YXmlFragment) {
var val = dom.getAttribute(name)
if (yxml.getAttribute(name) !== val) {
if (val == null) {
yxml.removeAttribute(name)
} else {
yxml.setAttribute(name, val)
}
}
break
case 'attributes':
if (yxml.constructor === YXmlFragment) {
break
}
let name = mutation.attributeName
let val = dom.getAttribute(name)
// check if filter accepts attribute
let attributes = new Map()
attributes.set(name, val)
if (this._domFilter(dom.nodeName, attributes).size > 0 && yxml.constructor !== YXmlFragment) {
if (yxml.getAttribute(name) !== val) {
if (val == null) {
yxml.removeAttribute(name)
} else {
yxml.setAttribute(name, val)
}
}
}
break
case 'childList':
diffChildren.add(mutation.target)
break
}
})
for (let dom of diffChildren) {
if (dom._yxml != null && dom._yxml !== false) {
applyChangesFromDom(dom)
}
}
break
case 'childList':
diffChildren.add(mutation.target)
break
}
})
for (let dom of diffChildren) {
if (dom._yxml != null) {
applyChangesFromDom(dom)
}
}
})
}
this._domObserver = new MutationObserver(this._domObserverListener)
this._domObserver.observe(dom, {
childList: true,
attributes: true,
characterData: true,
subtree: true
})
}
this._domObserver = new MutationObserver(this._domObserverListener)
this._domObserver.observe(dom, {
childList: true,
attributes: true,
characterData: true,
subtree: true
})
return dom
}
_logString () {

View File

@@ -7,7 +7,7 @@ let relativeSelection = null
export let beforeTransactionSelectionFixer
if (typeof getSelection !== 'undefined') {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, remote) {
if (!remote) {
return
}
@@ -30,7 +30,7 @@ if (typeof getSelection !== 'undefined') {
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
}
export function afterTransactionSelectionFixer (y, transaction, remote) {
export function afterTransactionSelectionFixer (y, remote) {
if (relativeSelection === null || !remote) {
return
}
@@ -46,25 +46,17 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
if (from !== null) {
let sel = fromRelativePosition(fromY, from)
if (sel !== null) {
let node = sel.type.getDom()
let offset = sel.offset
if (node !== anchorNode || offset !== anchorOffset) {
anchorNode = node
anchorOffset = offset
shouldUpdate = true
}
shouldUpdate = true
anchorNode = sel.type.getDom()
anchorOffset = sel.offset
}
}
if (to !== null) {
let sel = fromRelativePosition(toY, to)
if (sel !== null) {
let node = sel.type.getDom()
let offset = sel.offset
if (node !== focusNode || offset !== focusOffset) {
focusNode = node
focusOffset = offset
shouldUpdate = true
}
focusNode = sel.type.getDom()
focusOffset = sel.offset
shouldUpdate = true
}
}
if (shouldUpdate) {

View File

@@ -135,33 +135,7 @@ export function applyChangesFromDom (dom) {
}
}
export function reflectChangesOnDom (events, _document) {
// Make sure that no filtered attributes are applied to the structure
// if they were, delete them
/*
events.forEach(event => {
const target = event.target
if (event.attributesChanged === undefined) {
// event.target is Y.XmlText
return
}
const keys = this._domFilter(target.nodeName, Array.from(event.attributesChanged))
if (keys === null) {
target._delete()
} else {
const removeKeys = new Set() // is a copy of event.attributesChanged
event.attributesChanged.forEach(key => { removeKeys.add(key) })
keys.forEach(key => {
// remove all accepted keys from removeKeys
removeKeys.delete(key)
})
// remove the filtered attribute
removeKeys.forEach(key => {
target.removeAttribute(key)
})
}
})
*/
export function reflectChangesOnDom (events) {
this._mutualExclude(() => {
events.forEach(event => {
const yxml = event.target
@@ -183,9 +157,9 @@ export function reflectChangesOnDom (events, _document) {
})
if (event.childListChanged) {
// create fragment of undeleted nodes
const fragment = _document.createDocumentFragment()
const fragment = document.createDocumentFragment()
yxml.forEach(function (t) {
fragment.appendChild(t.getDom(_document))
fragment.append(t.getDom())
})
// remove remainding nodes
let lastChild = dom.lastChild
@@ -194,7 +168,7 @@ export function reflectChangesOnDom (events, _document) {
lastChild = dom.lastChild
}
// insert fragment of undeleted nodes
dom.appendChild(fragment)
dom.append(fragment)
}
}
/* TODO: smartscrolling

View File

@@ -17,7 +17,7 @@ export default class EventHandler {
removeAllEventListeners () {
this.eventListeners = []
}
callEventListeners (transaction, event) {
callEventListeners (event) {
for (var i = 0; i < this.eventListeners.length; i++) {
try {
const f = this.eventListeners[i]

View File

@@ -1,16 +1,16 @@
import ID from './ID.js'
class ReverseOperation {
constructor (y, transaction) {
constructor (y) {
this.created = new Date()
const beforeState = transaction.beforeState
const beforeState = y._transaction.beforeState
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1)
if (beforeState.has(y.userID)) {
this.fromState = new ID(y.userID, beforeState.get(y.userID))
} else {
this.fromState = this.toState
}
this.deletedStructs = transaction.deletedStructs
this.deletedStructs = y._transaction.deletedStructs
}
}
@@ -24,45 +24,10 @@ function isStructInScope (y, struct, scope) {
return false
}
function applyReverseOperation (y, scope, reverseBuffer) {
let performedUndo = false
y.transact(() => {
while (!performedUndo && reverseBuffer.length > 0) {
let undoOp = reverseBuffer.pop()
// make sure that it is possible to iterate {from}-{to}
y.os.getItemCleanStart(undoOp.fromState)
y.os.getItemCleanEnd(undoOp.toState)
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
if (!op._deleted && isStructInScope(y, op, scope)) {
performedUndo = true
op._delete(y)
}
})
for (let op of undoOp.deletedStructs) {
if (
isStructInScope(y, op, scope) &&
op._parent !== y &&
!op._parent._deleted &&
(
op._parent._id.user !== y.userID ||
op._parent._id.clock < undoOp.fromState.clock ||
op._parent._id.clock > undoOp.fromState.clock
)
) {
performedUndo = true
op = op._copy(undoOp.deletedStructs)
op._integrate(y)
}
}
}
})
return performedUndo
}
export default class UndoManager {
constructor (scope, options = {}) {
this.options = options
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
options.captureTimeout = options.captureTimeout || 0
this._undoBuffer = []
this._redoBuffer = []
this._scope = scope
@@ -70,12 +35,13 @@ export default class UndoManager {
this._redoing = false
const y = scope._y
this.y = y
y.on('afterTransaction', (y, transaction, remote) => {
if (!remote && transaction.changedParentTypes.has(scope)) {
let reverseOperation = new ReverseOperation(y, transaction)
y.on('afterTransaction', (y, remote) => {
if (!remote && (y._transaction.beforeState.has(y.userID) || y._transaction.deletedStructs.size > 0)) {
let reverseOperation = new ReverseOperation(y)
if (!this._undoing) {
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
if (lastUndoOp !== null && reverseOperation.created - lastUndoOp.created <= options.captureTimeout) {
if (lastUndoOp !== null && lastUndoOp.created - reverseOperation.created <= options.captureTimeout) {
console.log('appending', lastUndoOp, reverseOperation)
lastUndoOp.created = reverseOperation.created
lastUndoOp.toState = reverseOperation.toState
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
@@ -92,15 +58,47 @@ export default class UndoManager {
})
}
undo () {
console.log('undoing')
this._undoing = true
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer)
this._applyReverseOperation(this._undoBuffer)
this._undoing = false
return performedUndo
}
redo () {
this._redoing = true
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer)
this._applyReverseOperation(this._redoBuffer)
this._redoing = false
return performedRedo
}
_applyReverseOperation (reverseBuffer) {
this.y.transact(() => {
let performedUndo = false
while (!performedUndo && reverseBuffer.length > 0) {
let undoOp = reverseBuffer.pop()
// make sure that it is possible to iterate {from}-{to}
this.y.os.getItemCleanStart(undoOp.fromState)
this.y.os.getItemCleanEnd(undoOp.toState)
this.y.os.iterate(undoOp.fromState, undoOp.toState, op => {
if (!op._deleted && isStructInScope(this.y, op, this._scope)) {
performedUndo = true
op._delete(this.y)
}
})
for (let op of undoOp.deletedStructs) {
if (
isStructInScope(this.y, op, this._scope) &&
op._parent !== this.y &&
!op._parent._deleted &&
(
op._parent._id.user !== this.y.userID ||
op._parent._id.clock < undoOp.fromState.clock ||
op._parent._id.clock > undoOp.fromState.clock
)
) {
performedUndo = true
op = op._copy(undoOp.deletedStructs)
op._integrate(this.y)
}
}
}
})
}
}

View File

@@ -8,15 +8,15 @@ export default class YEvent {
const path = []
let type = this.target
const y = type._y
while (type !== this.currentTarget && type !== y) {
while (type._parent !== this._currentTarget && type._parent !== y) {
let parent = type._parent
if (type._parentSub !== null) {
path.unshift(type._parentSub)
path.push(type._parentSub)
} else {
// parent is array-ish
for (let [i, child] of parent) {
if (child === type) {
path.unshift(i)
path.push(i)
break
}
}

View File

@@ -15,7 +15,6 @@ import YMap from './Type/YMap.js'
import YText from './Type/YText.js'
import { YXmlFragment, YXmlElement, YXmlText } from './Type/y-xml/y-xml.js'
import BinaryDecoder from './Binary/Decoder.js'
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
import debug from 'debug'
import Transaction from './Transaction.js'
@@ -45,8 +44,8 @@ export default class Y extends NamedEventHandler {
transact (f, remote = false) {
let initialCall = this._transaction === null
if (initialCall) {
this.emit('beforeTransaction', this, remote)
this._transaction = new Transaction(this)
this.emit('beforeTransaction', this, this._transaction, remote)
}
try {
f(this)
@@ -54,16 +53,13 @@ export default class Y extends NamedEventHandler {
console.error(e)
}
if (initialCall) {
this.emit('beforeObserverCalls', this, this._transaction, remote)
const transaction = this._transaction
this._transaction = null
// emit change events on changed types
transaction.changedTypes.forEach(function (subs, type) {
this._transaction.changedTypes.forEach(function (subs, type) {
if (!type._deleted) {
type._callObserver(transaction, subs, remote)
type._callObserver(subs, remote)
}
})
transaction.changedParentTypes.forEach(function (events, type) {
this._transaction.changedParentTypes.forEach(function (events, type) {
if (!type._deleted) {
events = events
.filter(event =>
@@ -75,11 +71,12 @@ export default class Y extends NamedEventHandler {
})
// we don't have to check for events.length
// because there is no way events is empty..
type._deepEventHandler.callEventListeners(transaction, events)
type._deepEventHandler.callEventListeners(events)
}
})
// when all changes & events are processed, emit afterTransaction event
this.emit('afterTransaction', this, transaction, remote)
this.emit('afterTransaction', this, remote)
this._transaction = null
}
}
// fake _start for root properties (y.set('name', type))
@@ -95,10 +92,17 @@ export default class Y extends NamedEventHandler {
define (name, TypeConstructor) {
let id = new RootID(name, TypeConstructor)
let type = this.os.get(id)
if (type === null) {
type = new TypeConstructor()
type._id = id
type._parent = this
type._integrate(this)
if (this.share[name] !== undefined) {
throw new Error('Type is already defined with a different constructor!')
}
}
if (this.share[name] === undefined) {
this.share[name] = type
} else if (this.share[name] !== type) {
throw new Error('Type is already defined with a different constructor')
}
return type
}
@@ -164,9 +168,7 @@ Y.XmlText = YXmlText
Y.utils = {
BinaryDecoder,
UndoManager,
getRelativePosition,
fromRelativePosition
UndoManager
}
Y.debug = debug

View File

@@ -41,8 +41,6 @@ test('basic map tests', async function map0 (t) {
test('Basic get&set of Map property (converge via sync)', async function map1 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
map0.set('stuff', 'stuffy')
map0.set('undefined', undefined)
map0.set('null', null)
t.compare(map0.get('stuff'), 'stuffy')
await flushAll(t, users)
@@ -50,8 +48,6 @@ test('Basic get&set of Map property (converge via sync)', async function map1 (t
for (let user of users) {
var u = user.get('map', Y.Map)
t.compare(u.get('stuff'), 'stuffy')
t.assert(u.get('undefined') === undefined, 'undefined')
t.compare(u.get('null'), null, 'null')
}
await compareUsers(t, users)
})
@@ -176,36 +172,31 @@ test('observePath properties', async function map10 (t) {
})
*/
/* TODO: reimplement observeDeep
test('observe deep properties', async function map11 (t) {
let { users, map1, map2, map3 } = await initArrays(t, { users: 4 })
var _map1 = map1.set('map', new Y.Map())
var calls = 0
var dmapid
map1.observeDeep(function (events) {
events.forEach(event => {
calls++
t.assert(event.keysChanged.has('deepmap'))
t.assert(event.path.length === 1)
t.assert(event.path[0] === 'map')
dmapid = event.target.get('deepmap')._id
})
_map1.observe(function (event) {
calls++
t.compare(event.name, 'deepmap')
dmapid = event.target.opContents.deepmap
})
await flushAll(t, users)
await flushAll(t, users)
var _map3 = map3.get('map')
_map3.set('deepmap', new Y.Map())
await flushAll(t, users)
var _map2 = map2.get('map')
_map2.set('deepmap', new Y.Map())
await flushAll(t, users)
await flushAll(t, users)
var dmap1 = _map1.get('deepmap')
var dmap2 = _map2.get('deepmap')
var dmap3 = _map3.get('deepmap')
t.assert(calls > 0)
t.assert(dmap1._id.equals(dmap2._id))
t.assert(dmap1._id.equals(dmap3._id))
t.assert(dmap1._id.equals(dmapid))
t.compare(dmap1._model, dmap2._model)
t.compare(dmap1._model, dmap3._model)
t.compare(dmap1._model, dmapid)
await compareUsers(t, users)
})
@@ -213,10 +204,8 @@ test('observes using observeDeep', async function map12 (t) {
let { users, map0 } = await initArrays(t, { users: 2 })
var pathes = []
var calls = 0
map0.observeDeep(function (events) {
events.forEach(event => {
pathes.push(event.path)
})
map0.observeDeep(function (event) {
pathes.push(event.path)
calls++
})
map0.set('map', new Y.Map())
@@ -226,6 +215,7 @@ test('observes using observeDeep', async function map12 (t) {
t.compare(pathes, [[], ['map'], ['map', 'array']])
await compareUsers(t, users)
})
*/
/* TODO: Test events in Y.Map
function compareEvent (t, is, should) {

View File

@@ -230,8 +230,8 @@ test('filter node', async function xml14 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
let domFilter = (nodeName, attrs) => {
if (nodeName === 'H1') {
let domFilter = (node, attrs) => {
if (node.nodeName === 'H1') {
return null
} else {
return attrs
@@ -251,9 +251,8 @@ test('filter attribute', async function xml15 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
let domFilter = (nodeName, attrs) => {
attrs.delete('hidden')
return attrs
let domFilter = (node, attrs) => {
return attrs.filter(name => name !== 'hidden')
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
@@ -304,51 +303,6 @@ test('treeWalker', async function xml17 (t) {
await compareUsers(t, users)
})
/**
* The expected behavior is that changes on your own dom (e.g. malicious attributes) persist.
* Yjs should just ignore them, never propagate those attributes.
* Incoming changes that contain malicious attributes should be deleted.
*/
test('Filtering remote changes', async function xmlFilteringRemote (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
xml0.setDomFilter(function (nodeName, attributes) {
attributes.delete('malicious')
if (nodeName === 'HIDEME') {
return null
} else if (attributes.has('isHidden')) {
return null
} else {
return attributes
}
})
// make sure that dom filters are active
// TODO: do not rely on .getDom for domFilters
xml0.getDom()
xml1.getDom()
let paragraph = new Y.XmlElement('p')
let hideMe = new Y.XmlElement('hideMe')
let span = new Y.XmlElement('span')
span.setAttribute('malicious', 'alert("give me money")')
let tag = new Y.XmlElement('tag')
tag.setAttribute('isHidden', 'true')
paragraph.insert(0, [hideMe, span, tag])
xml0.insert(0, [paragraph])
let tag2 = new Y.XmlElement('tag')
tag2.setAttribute('isHidden', 'true')
paragraph.insert(0, [tag2])
await flushAll(t, users)
// check dom
paragraph.getDom().setAttribute('malicious', 'true')
span.getDom().setAttribute('malicious', 'true')
console.log(xml0.toString())
// check incoming attributes
xml1.get(0).get(0).setAttribute('malicious', 'true')
xml1.insert(0, [new Y.XmlElement('hideMe')])
await flushAll(t, users)
await compareUsers(t, users)
})
// TODO: move elements
var xmlTransactions = [
function attributeChange (t, user, chance) {

View File

@@ -153,12 +153,12 @@ export async function initArrays (t, opts) {
result['array' + i] = y.define('array', Y.Array)
result['map' + i] = y.define('map', Y.Map)
result['xml' + i] = y.define('xml', Y.XmlElement)
y.get('xml').setDomFilter(function (nodeName, attrs) {
if (nodeName === 'HIDDEN') {
y.get('xml', Y.Xml).setDomFilter(function (d, attrs) {
if (d.nodeName === 'HIDDEN') {
return null
} else {
return attrs.filter(a => a !== 'hidden')
}
attrs.delete('hidden')
return attrs
})
y.on('afterTransaction', function () {
for (let missing of y._missingStructs.values()) {
@@ -187,7 +187,7 @@ export async function flushAll (t, users) {
if (users.length === 0) {
return
}
await wait(10)
await wait(0)
if (users[0].connector.testRoom != null) {
// use flushAll method specified in Test Connector
await users[0].connector.testRoom.flushAll(users)

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

578
y.node.js
View File

@@ -1,7 +1,7 @@
/**
* yjs - A framework for real-time p2p shared editing on any data
* @version v13.0.0-34
* @version v13.0.0-25
* @license MIT
*/
@@ -687,6 +687,7 @@ if (!String.fromCodePoint) {
/*! http://mths.be/codepointat v0.2.0 by @mathias */
if (!String.prototype.codePointAt) {
(function() {
'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
var defineProperty = (function() {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
@@ -1899,7 +1900,7 @@ class EventHandler {
removeAllEventListeners () {
this.eventListeners = [];
}
callEventListeners (transaction, event) {
callEventListeners (event) {
for (var i = 0; i < this.eventListeners.length; i++) {
try {
const f = this.eventListeners[i];
@@ -1966,9 +1967,9 @@ class Type extends Item {
}
return path
}
_callEventHandler (transaction, event) {
const changedParentTypes = transaction.changedParentTypes;
this._eventHandler.callEventListeners(transaction, event);
_callEventHandler (event) {
const changedParentTypes = this._y._transaction.changedParentTypes;
this._eventHandler.callEventListeners(event);
let type = this;
while (type !== this._y) {
let events = changedParentTypes.get(type);
@@ -2091,13 +2092,7 @@ class ItemJSON extends Item {
this._content = new Array(len);
for (let i = 0; i < len; i++) {
const ctnt = decoder.readVarString();
let parsed;
if (ctnt === 'undefined') {
parsed = undefined;
} else {
parsed = JSON.parse(ctnt);
}
this._content[i] = parsed;
this._content[i] = JSON.parse(ctnt);
}
return missing
}
@@ -2106,14 +2101,7 @@ class ItemJSON extends Item {
let len = this._content.length;
encoder.writeVarUint(len);
for (let i = 0; i < len; i++) {
let encoded;
let content = this._content[i];
if (content === undefined) {
encoded = 'undefined';
} else {
encoded = JSON.stringify(content);
}
encoder.writeVarString(encoded);
encoder.writeVarString(JSON.stringify(this._content[i]));
}
}
_logString () {
@@ -2134,47 +2122,6 @@ class ItemJSON extends Item {
}
}
class ItemString extends Item {
constructor () {
super();
this._content = null;
}
_copy () {
let struct = super._copy();
struct._content = this._content;
return struct
}
get _length () {
return this._content.length
}
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder);
this._content = decoder.readVarString();
return missing
}
_toBinary (encoder) {
super._toBinary(encoder);
encoder.writeVarString(this._content);
}
_logString () {
const left = this._left !== null ? this._left._lastId : null;
const origin = this._origin !== null ? this._origin._lastId : null;
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
}
_splitAt (y, diff) {
if (diff === 0) {
return this
} else if (diff >= this._length) {
return this._right
}
let item = new ItemString();
item._content = this._content.slice(diff);
this._content = this._content.slice(0, diff);
splitHelper(y, this, item, diff);
return item
}
}
class YEvent {
constructor (target) {
this.target = target;
@@ -2184,15 +2131,15 @@ class YEvent {
const path = [];
let type = this.target;
const y = type._y;
while (type !== this.currentTarget && type !== y) {
while (type._parent !== this._currentTarget && type._parent !== y) {
let parent = type._parent;
if (type._parentSub !== null) {
path.unshift(type._parentSub);
path.push(type._parentSub);
} else {
// parent is array-ish
for (let [i, child] of parent) {
if (child === type) {
path.unshift(i);
path.push(i);
break
}
}
@@ -2211,19 +2158,15 @@ class YArrayEvent extends YEvent {
}
class YArray extends Type {
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YArrayEvent(this, remote));
_callObserver (parentSubs, remote) {
this._callEventHandler(new YArrayEvent(this, remote));
}
get (pos) {
let n = this._start;
while (n !== null) {
if (!n._deleted) {
if (pos < n._length) {
if (n.constructor === ItemJSON || n.constructor === ItemString) {
return n._content[pos]
} else {
return n
}
return n._content[n._length - pos]
}
pos -= n._length;
}
@@ -2429,8 +2372,8 @@ class YMapEvent extends YEvent {
}
class YMap extends Type {
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote));
_callObserver (parentSubs, remote) {
this._callEventHandler(new YMapEvent(this, parentSubs, remote));
}
toJSON () {
const map = {};
@@ -2529,6 +2472,47 @@ class YMap extends Type {
}
}
class ItemString extends Item {
constructor () {
super();
this._content = null;
}
_copy () {
let struct = super._copy();
struct._content = this._content;
return struct
}
get _length () {
return this._content.length
}
_fromBinary (y, decoder) {
let missing = super._fromBinary(y, decoder);
this._content = decoder.readVarString();
return missing
}
_toBinary (encoder) {
super._toBinary(encoder);
encoder.writeVarString(this._content);
}
_logString () {
const left = this._left !== null ? this._left._lastId : null;
const origin = this._origin !== null ? this._origin._lastId : null;
return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})`
}
_splitAt (y, diff) {
if (diff === 0) {
return this
} else if (diff >= this._length) {
return this._right
}
let item = new ItemString();
item._content = this._content.slice(diff);
this._content = this._content.slice(0, diff);
splitHelper(y, this, item, diff);
return item
}
}
class YText extends YArray {
constructor (string) {
super();
@@ -2776,33 +2760,7 @@ function applyChangesFromDom (dom) {
}
}
function reflectChangesOnDom (events, _document) {
// Make sure that no filtered attributes are applied to the structure
// if they were, delete them
/*
events.forEach(event => {
const target = event.target
if (event.attributesChanged === undefined) {
// event.target is Y.XmlText
return
}
const keys = this._domFilter(target.nodeName, Array.from(event.attributesChanged))
if (keys === null) {
target._delete()
} else {
const removeKeys = new Set() // is a copy of event.attributesChanged
event.attributesChanged.forEach(key => { removeKeys.add(key) })
keys.forEach(key => {
// remove all accepted keys from removeKeys
removeKeys.delete(key)
})
// remove the filtered attribute
removeKeys.forEach(key => {
target.removeAttribute(key)
})
}
})
*/
function reflectChangesOnDom (events) {
this._mutualExclude(() => {
events.forEach(event => {
const yxml = event.target;
@@ -2824,9 +2782,9 @@ function reflectChangesOnDom (events, _document) {
});
if (event.childListChanged) {
// create fragment of undeleted nodes
const fragment = _document.createDocumentFragment();
const fragment = document.createDocumentFragment();
yxml.forEach(function (t) {
fragment.appendChild(t.getDom(_document));
fragment.append(t.getDom());
});
// remove remainding nodes
let lastChild = dom.lastChild;
@@ -2835,7 +2793,7 @@ function reflectChangesOnDom (events, _document) {
lastChild = dom.lastChild;
}
// insert fragment of undeleted nodes
dom.appendChild(fragment);
dom.append(fragment);
}
}
/* TODO: smartscrolling
@@ -2956,7 +2914,7 @@ let relativeSelection = null;
let beforeTransactionSelectionFixer;
if (typeof getSelection !== 'undefined') {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, remote) {
if (!remote) {
return
}
@@ -2979,7 +2937,7 @@ if (typeof getSelection !== 'undefined') {
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {};
}
function afterTransactionSelectionFixer (y, transaction, remote) {
function afterTransactionSelectionFixer (y, remote) {
if (relativeSelection === null || !remote) {
return
}
@@ -2995,25 +2953,17 @@ function afterTransactionSelectionFixer (y, transaction, remote) {
if (from !== null) {
let sel = fromRelativePosition(fromY, from);
if (sel !== null) {
let node = sel.type.getDom();
let offset = sel.offset;
if (node !== anchorNode || offset !== anchorOffset) {
anchorNode = node;
anchorOffset = offset;
shouldUpdate = true;
}
shouldUpdate = true;
anchorNode = sel.type.getDom();
anchorOffset = sel.offset;
}
}
if (to !== null) {
let sel = fromRelativePosition(toY, to);
if (sel !== null) {
let node = sel.type.getDom();
let offset = sel.offset;
if (node !== focusNode || offset !== focusOffset) {
focusNode = node;
focusOffset = offset;
shouldUpdate = true;
}
focusNode = sel.type.getDom();
focusOffset = sel.offset;
shouldUpdate = true;
}
}
if (shouldUpdate) {
@@ -3786,18 +3736,18 @@ function merge_tuples (diffs, start, length) {
/* global MutationObserver */
function domToYXml (parent, doms, _document) {
function domToYXml (parent, doms) {
const types = [];
doms.forEach(d => {
if (d._yxml != null && d._yxml !== false) {
d._yxml._unbindFromDom();
}
if (parent._domFilter(d.nodeName, new Map()) !== null) {
if (parent._domFilter(d, []) !== null) {
let type;
if (d.nodeType === d.TEXT_NODE) {
type = new YXmlText(d);
} else if (d.nodeType === d.ELEMENT_NODE) {
type = new YXmlFragment._YXmlElement(d, parent._domFilter, _document);
type = new YXmlFragment._YXmlElement(d, parent._domFilter);
} else {
throw new Error('Unsupported node!')
}
@@ -3875,9 +3825,7 @@ class YXmlFragment extends YArray {
} catch (e) {
console.error(e);
}
if (this._domObserver !== null) {
this._domObserver.takeRecords();
}
this._domObserver.takeRecords();
token = true;
}
};
@@ -3921,8 +3869,8 @@ class YXmlFragment extends YArray {
xml.setDomFilter(f);
});
}
_callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote));
_callObserver (parentSubs, remote) {
this._callEventHandler(new YXmlEvent(this, parentSubs, remote));
}
toString () {
return this.map(xml => xml.toString()).join('')
@@ -3941,167 +3889,113 @@ class YXmlFragment extends YArray {
this._dom = null;
}
}
insertDomElementsAfter (prev, doms, _document) {
const types = domToYXml(this, doms, _document);
insertDomElementsAfter (prev, doms) {
const types = domToYXml(this, doms);
this.insertAfter(prev, types);
return types
}
insertDomElements (pos, doms, _document) {
const types = domToYXml(this, doms, _document);
insertDomElements (pos, doms) {
const types = domToYXml(this, doms);
this.insert(pos, types);
return types
}
getDom () {
return this._dom
}
bindToDom (dom, _document) {
bindToDom (dom) {
if (this._dom != null) {
this._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!')
}
dom.innerHTML = '';
this._dom = dom;
dom._yxml = this;
this.forEach(t => {
dom.insertBefore(t.getDom(_document), null);
dom.insertBefore(t.getDom(), null);
});
this._bindToDom(dom, _document);
this._bindToDom(dom);
}
// binds to a dom element
// Only call if dom and YXml are isomorph
_bindToDom (dom, _document) {
_document = _document || document;
this._dom = dom;
dom._yxml = this;
// TODO: refine this..
if ((this.constructor !== YXmlFragment && this._parent !== this._y) || this._parent === null) {
// TODO: only top level YXmlFragment can bind. Also allow YXmlElements..
_bindToDom (dom) {
if (this._parent === null || this._parent._dom != null || typeof MutationObserver === 'undefined') {
// only bind if parent did not already bind
return
}
this._y.on('beforeTransaction', () => {
this._domObserverListener(this._domObserver.takeRecords());
});
this._y.on('beforeTransaction', beforeTransactionSelectionFixer);
this._y.on('afterTransaction', afterTransactionSelectionFixer);
const applyFilter = (type) => {
if (type._deleted) {
return
}
// check if type is a child of this
let isChild = false;
let p = type;
while (p !== this._y) {
if (p === this) {
isChild = true;
break
}
p = p._parent;
}
if (!isChild) {
return
}
// filter attributes
let attributes = new Map();
if (type.getAttributes !== undefined) {
let attrs = type.getAttributes();
for (let key in attrs) {
attributes.set(key, attrs[key]);
}
}
let result = this._domFilter(type.nodeName, new Map(attributes));
if (result === null) {
type._delete(this._y);
} else {
attributes.forEach((value, key) => {
if (!result.has(key)) {
type.removeAttribute(key);
}
});
}
};
this._y.on('beforeObserverCalls', function (y, transaction) {
// apply dom filter to new and changed types
transaction.changedTypes.forEach(function (subs, type) {
if (subs.size > 1 || !subs.has(null)) {
// only apply changes on attributes
applyFilter(type);
}
});
transaction.newTypes.forEach(applyFilter);
});
// Apply Y.Xml events to dom
this.observeDeep(events => {
reflectChangesOnDom.call(this, events, _document);
});
this.observeDeep(reflectChangesOnDom.bind(this));
// Apply Dom changes on Y.Xml
if (typeof MutationObserver !== 'undefined') {
this._y.on('beforeTransaction', () => {
this._domObserverListener(this._domObserver.takeRecords());
});
this._domObserverListener = mutations => {
this._mutualExclude(() => {
this._y.transact(() => {
let diffChildren = new Set();
mutations.forEach(mutation => {
const dom = mutation.target;
const yxml = dom._yxml;
if (yxml == null) {
// dom element is filtered
return
}
switch (mutation.type) {
case 'characterData':
var diffs = diff_1(yxml.toString(), dom.nodeValue);
var pos = 0;
for (var i = 0; i < diffs.length; i++) {
var d = diffs[i];
if (d[0] === 0) { // EQUAL
pos += d[1].length;
} else if (d[0] === -1) { // DELETE
yxml.delete(pos, d[1].length);
} else { // INSERT
yxml.insert(pos, d[1]);
pos += d[1].length;
this._domObserverListener = mutations => {
this._mutualExclude(() => {
this._y.transact(() => {
let diffChildren = new Set();
mutations.forEach(mutation => {
const dom = mutation.target;
const yxml = dom._yxml;
if (yxml == null) {
// dom element is filtered
return
}
switch (mutation.type) {
case 'characterData':
var diffs = diff_1(yxml.toString(), dom.nodeValue);
var pos = 0;
for (var i = 0; i < diffs.length; i++) {
var d = diffs[i];
if (d[0] === 0) { // EQUAL
pos += d[1].length;
} else if (d[0] === -1) { // DELETE
yxml.delete(pos, d[1].length);
} else { // INSERT
yxml.insert(pos, d[1]);
pos += d[1].length;
}
}
break
case 'attributes':
let name = mutation.attributeName;
// check if filter accepts attribute
if (this._domFilter(dom, [name]).length > 0 && this.constructor !== YXmlFragment) {
var val = dom.getAttribute(name);
if (yxml.getAttribute(name) !== val) {
if (val == null) {
yxml.removeAttribute(name);
} else {
yxml.setAttribute(name, val);
}
}
break
case 'attributes':
if (yxml.constructor === YXmlFragment) {
break
}
let name = mutation.attributeName;
let val = dom.getAttribute(name);
// check if filter accepts attribute
let attributes = new Map();
attributes.set(name, val);
if (this._domFilter(dom.nodeName, attributes).size > 0 && yxml.constructor !== YXmlFragment) {
if (yxml.getAttribute(name) !== val) {
if (val == null) {
yxml.removeAttribute(name);
} else {
yxml.setAttribute(name, val);
}
}
}
break
case 'childList':
diffChildren.add(mutation.target);
break
}
});
for (let dom of diffChildren) {
if (dom._yxml != null && dom._yxml !== false) {
applyChangesFromDom(dom);
}
}
break
case 'childList':
diffChildren.add(mutation.target);
break
}
});
for (let dom of diffChildren) {
if (dom._yxml != null) {
applyChangesFromDom(dom);
}
}
});
};
this._domObserver = new MutationObserver(this._domObserverListener);
this._domObserver.observe(dom, {
childList: true,
attributes: true,
characterData: true,
subtree: true
});
}
};
this._domObserver = new MutationObserver(this._domObserverListener);
this._domObserver.observe(dom, {
childList: true,
attributes: true,
characterData: true,
subtree: true
});
return dom
}
_logString () {
@@ -4113,7 +4007,7 @@ class YXmlFragment extends YArray {
// import diff from 'fast-diff'
class YXmlElement extends YXmlFragment {
constructor (arg1, arg2, _document) {
constructor (arg1, arg2) {
super();
this.nodeName = null;
this._scrollElement = null;
@@ -4121,7 +4015,7 @@ class YXmlElement extends YXmlFragment {
this.nodeName = arg1.toUpperCase();
} else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === arg1.ELEMENT_NODE) {
this.nodeName = arg1.nodeName;
this._setDom(arg1, _document);
this._setDom(arg1);
} else {
this.nodeName = 'UNDEFINED';
}
@@ -4134,25 +4028,28 @@ class YXmlElement extends YXmlFragment {
struct.nodeName = this.nodeName;
return struct
}
_setDom (dom, _document) {
_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..
throw new Error('Already bound to an YXml type')
} else {
this._dom = dom;
dom._yxml = this;
// tag is already set in constructor
// set attributes
let attributes = new Map();
let attrNames = [];
for (let i = 0; i < dom.attributes.length; i++) {
let attr = dom.attributes[i];
attributes.set(attr.name, attr.value);
attrNames.push(dom.attributes[i].name);
}
attributes = this._domFilter(dom, attributes);
attributes.forEach((value, name) => {
this.setAttribute(name, value);
});
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes), _document);
this._bindToDom(dom, _document);
attrNames = this._domFilter(dom, attrNames);
for (let i = 0; i < attrNames.length; i++) {
let attrName = attrNames[i];
let attrValue = dom.getAttribute(attrName);
this.setAttribute(attrName, attrValue);
}
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes));
this._bindToDom(dom);
return dom
}
}
@@ -4211,9 +4108,7 @@ class YXmlElement extends YXmlFragment {
getAttributes () {
const obj = {};
for (let [key, value] of this._map) {
if (!value._deleted) {
obj[key] = value._content[0];
}
obj[key] = value._content[0];
}
return obj
}
@@ -4222,6 +4117,7 @@ class YXmlElement extends YXmlFragment {
let dom = this._dom;
if (dom == null) {
dom = _document.createElement(this.nodeName);
this._dom = dom;
dom._yxml = this;
let attrs = this.getAttributes();
for (let key in attrs) {
@@ -4230,7 +4126,7 @@ class YXmlElement extends YXmlFragment {
this.forEach(yxml => {
dom.appendChild(yxml.getDom(_document));
});
this._bindToDom(dom, _document);
this._bindToDom(dom);
}
return dom
}
@@ -4471,16 +4367,16 @@ class NamedEventHandler {
}
class ReverseOperation {
constructor (y, transaction) {
constructor (y) {
this.created = new Date();
const beforeState = transaction.beforeState;
const beforeState = y._transaction.beforeState;
this.toState = new ID(y.userID, y.ss.getState(y.userID) - 1);
if (beforeState.has(y.userID)) {
this.fromState = new ID(y.userID, beforeState.get(y.userID));
} else {
this.fromState = this.toState;
}
this.deletedStructs = transaction.deletedStructs;
this.deletedStructs = y._transaction.deletedStructs;
}
}
@@ -4494,45 +4390,10 @@ function isStructInScope (y, struct, scope) {
return false
}
function applyReverseOperation (y, scope, reverseBuffer) {
let performedUndo = false;
y.transact(() => {
while (!performedUndo && reverseBuffer.length > 0) {
let undoOp = reverseBuffer.pop();
// make sure that it is possible to iterate {from}-{to}
y.os.getItemCleanStart(undoOp.fromState);
y.os.getItemCleanEnd(undoOp.toState);
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
if (!op._deleted && isStructInScope(y, op, scope)) {
performedUndo = true;
op._delete(y);
}
});
for (let op of undoOp.deletedStructs) {
if (
isStructInScope(y, op, scope) &&
op._parent !== y &&
!op._parent._deleted &&
(
op._parent._id.user !== y.userID ||
op._parent._id.clock < undoOp.fromState.clock ||
op._parent._id.clock > undoOp.fromState.clock
)
) {
performedUndo = true;
op = op._copy(undoOp.deletedStructs);
op._integrate(y);
}
}
}
});
return performedUndo
}
class UndoManager {
constructor (scope, options = {}) {
this.options = options;
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout;
options.captureTimeout = options.captureTimeout || 0;
this._undoBuffer = [];
this._redoBuffer = [];
this._scope = scope;
@@ -4540,12 +4401,13 @@ class UndoManager {
this._redoing = false;
const y = scope._y;
this.y = y;
y.on('afterTransaction', (y, transaction, remote) => {
if (!remote && transaction.changedParentTypes.has(scope)) {
let reverseOperation = new ReverseOperation(y, transaction);
y.on('afterTransaction', (y, remote) => {
if (!remote && (y._transaction.beforeState.has(y.userID) || y._transaction.deletedStructs.size > 0)) {
let reverseOperation = new ReverseOperation(y);
if (!this._undoing) {
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null;
if (lastUndoOp !== null && reverseOperation.created - lastUndoOp.created <= options.captureTimeout) {
if (lastUndoOp !== null && lastUndoOp.created - reverseOperation.created <= options.captureTimeout) {
console.log('appending', lastUndoOp, reverseOperation);
lastUndoOp.created = reverseOperation.created;
lastUndoOp.toState = reverseOperation.toState;
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs);
@@ -4562,16 +4424,48 @@ class UndoManager {
});
}
undo () {
console.log('undoing');
this._undoing = true;
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer);
this._applyReverseOperation(this._undoBuffer);
this._undoing = false;
return performedUndo
}
redo () {
this._redoing = true;
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer);
this._applyReverseOperation(this._redoBuffer);
this._redoing = false;
return performedRedo
}
_applyReverseOperation (reverseBuffer) {
this.y.transact(() => {
let performedUndo = false;
while (!performedUndo && reverseBuffer.length > 0) {
let undoOp = reverseBuffer.pop();
// make sure that it is possible to iterate {from}-{to}
this.y.os.getItemCleanStart(undoOp.fromState);
this.y.os.getItemCleanEnd(undoOp.toState);
this.y.os.iterate(undoOp.fromState, undoOp.toState, op => {
if (!op._deleted && isStructInScope(this.y, op, this._scope)) {
performedUndo = true;
op._delete(this.y);
}
});
for (let op of undoOp.deletedStructs) {
if (
isStructInScope(this.y, op, this._scope) &&
op._parent !== this.y &&
!op._parent._deleted &&
(
op._parent._id.user !== this.y.userID ||
op._parent._id.clock < undoOp.fromState.clock ||
op._parent._id.clock > undoOp.fromState.clock
)
) {
performedUndo = true;
op = op._copy(undoOp.deletedStructs);
op._integrate(this.y);
}
}
}
});
}
}
@@ -4932,15 +4826,6 @@ function coerce(val) {
}
});
var debug_1 = debug$1.coerce;
var debug_2 = debug$1.disable;
var debug_3 = debug$1.enable;
var debug_4 = debug$1.enabled;
var debug_5 = debug$1.humanize;
var debug_6 = debug$1.names;
var debug_7 = debug$1.skips;
var debug_8 = debug$1.formatters;
var browser = createCommonjsModule(function (module, exports) {
/**
* This is the web browser implementation of `debug()`.
@@ -5129,14 +5014,6 @@ function localstorage() {
}
});
var browser_1 = browser.log;
var browser_2 = browser.formatArgs;
var browser_3 = browser.save;
var browser_4 = browser.load;
var browser_5 = browser.useColors;
var browser_6 = browser.storage;
var browser_7 = browser.colors;
class AbstractConnector {
constructor (y, opts) {
this.y = y;
@@ -5498,8 +5375,8 @@ class Y$1 extends NamedEventHandler {
transact (f, remote = false) {
let initialCall = this._transaction === null;
if (initialCall) {
this.emit('beforeTransaction', this, remote);
this._transaction = new Transaction(this);
this.emit('beforeTransaction', this, this._transaction, remote);
}
try {
f(this);
@@ -5507,16 +5384,13 @@ class Y$1 extends NamedEventHandler {
console.error(e);
}
if (initialCall) {
this.emit('beforeObserverCalls', this, this._transaction, remote);
const transaction = this._transaction;
this._transaction = null;
// emit change events on changed types
transaction.changedTypes.forEach(function (subs, type) {
this._transaction.changedTypes.forEach(function (subs, type) {
if (!type._deleted) {
type._callObserver(transaction, subs, remote);
type._callObserver(subs, remote);
}
});
transaction.changedParentTypes.forEach(function (events, type) {
this._transaction.changedParentTypes.forEach(function (events, type) {
if (!type._deleted) {
events = events
.filter(event =>
@@ -5528,11 +5402,12 @@ class Y$1 extends NamedEventHandler {
});
// we don't have to check for events.length
// because there is no way events is empty..
type._deepEventHandler.callEventListeners(transaction, events);
type._deepEventHandler.callEventListeners(events);
}
});
// when all changes & events are processed, emit afterTransaction event
this.emit('afterTransaction', this, transaction, remote);
this.emit('afterTransaction', this, remote);
this._transaction = null;
}
}
// fake _start for root properties (y.set('name', type))
@@ -5548,10 +5423,17 @@ class Y$1 extends NamedEventHandler {
define (name, TypeConstructor) {
let id = new RootID(name, TypeConstructor);
let type = this.os.get(id);
if (type === null) {
type = new TypeConstructor();
type._id = id;
type._parent = this;
type._integrate(this);
if (this.share[name] !== undefined) {
throw new Error('Type is already defined with a different constructor!')
}
}
if (this.share[name] === undefined) {
this.share[name] = type;
} else if (this.share[name] !== type) {
throw new Error('Type is already defined with a different constructor')
}
return type
}
@@ -5617,9 +5499,7 @@ Y$1.XmlText = YXmlText;
Y$1.utils = {
BinaryDecoder,
UndoManager,
getRelativePosition,
fromRelativePosition
UndoManager
};
Y$1.debug = browser;

File diff suppressed because one or more lines are too long

874
y.test.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long