separate dom binding
This commit is contained in:
parent
acf443aacb
commit
026675b438
@ -1,7 +1,7 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
|
|
||||||
window.onload = function () {
|
window.onload = function () {
|
||||||
window.yXmlType.bindToDom(document.body)
|
window.domBinding = new Y.DomBinding(window.yXmlType, document.body)
|
||||||
}
|
}
|
||||||
|
|
||||||
let y = new Y('htmleditor', {
|
let y = new Y('htmleditor', {
|
||||||
|
1240
package-lock.json
generated
1240
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
|||||||
"test": "npm run lint",
|
"test": "npm run lint",
|
||||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||||
"lint": "standard",
|
"lint": "standard",
|
||||||
"docs": "esdocs",
|
"docs": "esdoc",
|
||||||
"serve-docs": "npm run docs && serve ./docs/",
|
"serve-docs": "npm run docs && serve ./docs/",
|
||||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||||
@ -56,6 +56,8 @@
|
|||||||
"chance": "^1.0.9",
|
"chance": "^1.0.9",
|
||||||
"concurrently": "^3.4.0",
|
"concurrently": "^3.4.0",
|
||||||
"cutest": "^0.1.9",
|
"cutest": "^0.1.9",
|
||||||
|
"esdoc": "^1.0.4",
|
||||||
|
"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-plugin-babel": "^2.7.1",
|
"rollup-plugin-babel": "^2.7.1",
|
||||||
@ -67,9 +69,7 @@
|
|||||||
"rollup-regenerator-runtime": "^6.23.1",
|
"rollup-regenerator-runtime": "^6.23.1",
|
||||||
"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"
|
||||||
"esdoc": "^1.0.4",
|
|
||||||
"esdoc-standard-plugin": "^1.0.0"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^2.6.8"
|
"debug": "^2.6.8"
|
||||||
|
@ -19,7 +19,8 @@ export default {
|
|||||||
browser: true
|
browser: true
|
||||||
}),
|
}),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
babel(),
|
// babel(),
|
||||||
|
/*
|
||||||
uglify({
|
uglify({
|
||||||
mangle: {
|
mangle: {
|
||||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
||||||
@ -35,6 +36,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
*/
|
||||||
],
|
],
|
||||||
banner: `
|
banner: `
|
||||||
/**
|
/**
|
||||||
|
@ -1,109 +1,15 @@
|
|||||||
/* global MutationObserver */
|
/* global MutationObserver */
|
||||||
|
|
||||||
import Binding from './Binding.js'
|
import Binding from '../Binding.js'
|
||||||
import diff from '../Util/simpleDiff.js'
|
import diff from '../../Util/simpleDiff.js'
|
||||||
import YXmlFragment from '../../Type/YXml/YXmlFragment.js'
|
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
||||||
import YXmlHook from '../../Type/YXml/YXmlHook.js'
|
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||||
|
import { removeDomChildrenUntilElementFound, createAssociation } from './util.js'
|
||||||
|
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
||||||
function defaultFilter (nodeName, attrs) {
|
import { defaultFilter, applyFilterOnType } from './filter.js'
|
||||||
return attrs
|
import typeObserver from './typeObserver.js'
|
||||||
}
|
import domObserver from './domObserver.js'
|
||||||
|
import { removeAssociation } from './util.js'
|
||||||
function applyFilter (target, filter, type) {
|
|
||||||
if (type._deleted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if type is a child of this
|
|
||||||
let isChild = false
|
|
||||||
let p = type
|
|
||||||
while (p !== undefined) {
|
|
||||||
if (p === target) {
|
|
||||||
isChild = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
p = p._parent
|
|
||||||
}
|
|
||||||
if (!isChild) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// filter attributes
|
|
||||||
const attributes = new Map()
|
|
||||||
if (type.getAttributes !== undefined) {
|
|
||||||
let attrs = type.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let result = filter(type.nodeName, new Map(attributes))
|
|
||||||
if (result === null) {
|
|
||||||
type._delete(this._y)
|
|
||||||
} else {
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
if (!result.has(key)) {
|
|
||||||
type.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function typeObserver (events) {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
reflectChangesOnDom.call(this, events)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function domObserver (mutations) {
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
this._y.transact(() => {
|
|
||||||
let diffChildren = new Set()
|
|
||||||
mutations.forEach(mutation => {
|
|
||||||
const dom = mutation.target
|
|
||||||
const yxml = this.domToYXml.get(dom._yxml)
|
|
||||||
if (yxml == null || yxml.constructor === YXmlHook) {
|
|
||||||
// dom element is filtered
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch (mutation.type) {
|
|
||||||
case 'characterData':
|
|
||||||
var change = diff(yxml.toString(), dom.nodeValue)
|
|
||||||
yxml.delete(change.pos, change.remove)
|
|
||||||
yxml.insert(change.pos, change.insert)
|
|
||||||
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.filter(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.yOnChildrenChanged !== undefined) {
|
|
||||||
dom.yOnChildrenChanged()
|
|
||||||
}
|
|
||||||
const yxml = this.domToType.get(dom)
|
|
||||||
applyChangesFromDom(dom, yxml)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A binding that binds the children of a YXmlFragment to a DOM element.
|
* A binding that binds the children of a YXmlFragment to a DOM element.
|
||||||
@ -122,7 +28,7 @@ export default class DomBinding extends Binding {
|
|||||||
* truth.
|
* truth.
|
||||||
* @param {Element} target The bind target. Mirrors the target.
|
* @param {Element} target The bind target. Mirrors the target.
|
||||||
*/
|
*/
|
||||||
constructor (type, target, opts) {
|
constructor (type, target, opts = {}) {
|
||||||
// Binding handles textType as this.type and domTextarea as this.target
|
// Binding handles textType as this.type and domTextarea as this.target
|
||||||
super(type, target)
|
super(type, target)
|
||||||
this.domToType = new Map()
|
this.domToType = new Map()
|
||||||
@ -134,20 +40,73 @@ export default class DomBinding extends Binding {
|
|||||||
target.insertBefore(child.toDom(this.domToType, this.typeToDom), null)
|
target.insertBefore(child.toDom(this.domToType, this.typeToDom), null)
|
||||||
}
|
}
|
||||||
this._typeObserver = typeObserver.bind(this)
|
this._typeObserver = typeObserver.bind(this)
|
||||||
this._domObserver = domObserver.bind(this)
|
this._domObserver = (mutations) => {
|
||||||
type.observe(this._typeObserver)
|
domObserver.call(this, mutations, opts._document)
|
||||||
this._domObserver = domObserver.bind(this)
|
}
|
||||||
this._mutationObserver = new MutationObserver(this._domObserver())
|
type.observeDeep(this._typeObserver)
|
||||||
|
this._mutationObserver = new MutationObserver(this._domObserver)
|
||||||
this._mutationObserver.observe(target, {
|
this._mutationObserver.observe(target, {
|
||||||
childList: true,
|
childList: true,
|
||||||
attributes: true,
|
attributes: true,
|
||||||
characterData: true,
|
characterData: true,
|
||||||
subtree: true
|
subtree: true
|
||||||
})
|
})
|
||||||
this._beforeTransactionHandler = () => {
|
const y = type._y
|
||||||
this._domObserverListener(this._domObserver.takeRecords())
|
// Force flush dom changes before Type changes are applied (they might
|
||||||
|
// modify the dom)
|
||||||
|
this._beforeTransactionHandler = (y, transaction, remote) => {
|
||||||
|
this._domObserver(this._mutationObserver.takeRecords())
|
||||||
|
beforeTransactionSelectionFixer(y, this, transaction, remote)
|
||||||
}
|
}
|
||||||
this._y.on('beforeTransaction', this._beforeTransactionHandler)
|
y.on('beforeTransaction', this._beforeTransactionHandler)
|
||||||
|
this._afterTransactionHandler = (y, transaction, remote) => {
|
||||||
|
afterTransactionSelectionFixer(y, this, transaction, remote)
|
||||||
|
// remove associations
|
||||||
|
// TODO: this could be done more efficiently
|
||||||
|
// e.g. Always delete using the following approach, or removeAssociation
|
||||||
|
// in dom/type-observer..
|
||||||
|
transaction.deletedStructs.forEach(type => {
|
||||||
|
const dom = this.typeToDom.get(type)
|
||||||
|
if (dom !== undefined) {
|
||||||
|
removeAssociation(this, dom, type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
y.on('afterTransaction', this._afterTransactionHandler)
|
||||||
|
// Before calling observers, apply dom filter to all changed and new types.
|
||||||
|
this._beforeObserverCallsHandler = (y, transaction) => {
|
||||||
|
// Apply dom filter to new and changed types
|
||||||
|
transaction.changedTypes.forEach((subs, type) => {
|
||||||
|
// Only check attributes. New types are filtered below.
|
||||||
|
if ((subs.size > 1 || (subs.size === 1 && subs.has(null) === false))) {
|
||||||
|
applyFilterOnType(y, this, type)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
transaction.newTypes.forEach(type => {
|
||||||
|
applyFilterOnType(y, this, type)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
y.on('beforeObserverCalls', this._beforeObserverCallsHandler)
|
||||||
|
createAssociation(this, target, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables the smart scrolling functionality for a Dom Binding.
|
||||||
|
* This is useful when YXml is bound to a shared editor. When activated,
|
||||||
|
* the viewport will be changed to accommodate remote changes.
|
||||||
|
*
|
||||||
|
* @param {Element} scrollElement The node that is
|
||||||
|
*/
|
||||||
|
enableSmartScrolling (scrollElement) {
|
||||||
|
// @TODO: implement smart scrolling
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: currently does not apply filter to existing elements!
|
||||||
|
*/
|
||||||
|
setFilter (filter) {
|
||||||
|
this.filter = filter
|
||||||
|
// TODO: apply filter to all elements
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -158,7 +117,11 @@ export default class DomBinding extends Binding {
|
|||||||
this.typeToDom = null
|
this.typeToDom = null
|
||||||
this.type.unobserve(this._typeObserver)
|
this.type.unobserve(this._typeObserver)
|
||||||
this._mutationObserver.disconnect()
|
this._mutationObserver.disconnect()
|
||||||
this.type._y.off('beforeTransaction', this._beforeTransactionHandler)
|
const y = this.type._y
|
||||||
|
y.off('beforeTransaction', this._beforeTransactionHandler)
|
||||||
|
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
|
||||||
|
y.off('afterObserverCalls', this._afterObserverCallsHandler)
|
||||||
|
y.off('afterTransaction', this._afterTransactionHandler)
|
||||||
super.destroy()
|
super.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
import YXmlHook from '../../YXml/YXmlHook.js'
|
|
||||||
import {
|
|
||||||
iterateUntilUndeleted,
|
|
||||||
removeAssociation,
|
|
||||||
insertNodeHelper } from './util.js'
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 1. Check if any of the nodes was deleted
|
|
||||||
* 2. Iterate over the children.
|
|
||||||
* 2.1 If a node exists without _yxml property, insert a new node
|
|
||||||
* 2.2 If _contents.length < dom.childNodes.length, fill the
|
|
||||||
* rest of _content with childNodes
|
|
||||||
* 2.3 If a node was moved, delete it and
|
|
||||||
* recreate a new yxml element that is bound to that node.
|
|
||||||
* You can detect that a node was moved because expectedId
|
|
||||||
* !== actualId in the list
|
|
||||||
*/
|
|
||||||
export default function applyChangesFromDom (dom, yxml) {
|
|
||||||
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const y = yxml._y
|
|
||||||
const knownChildren = new Set()
|
|
||||||
for (let child in dom.childNodes) {
|
|
||||||
const type = knownChildren.get(child)
|
|
||||||
if (type !== undefined && type !== false) {
|
|
||||||
knownChildren.add(type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 1. Check if any of the nodes was deleted
|
|
||||||
yxml.forEach(function (childType) {
|
|
||||||
if (knownChildren.has(childType) === false) {
|
|
||||||
childType._delete(y)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// 2. iterate
|
|
||||||
const childNodes = dom.childNodes
|
|
||||||
const len = childNodes.length
|
|
||||||
let prevExpectedType = null
|
|
||||||
let expectedType = iterateUntilUndeleted(yxml._start)
|
|
||||||
for (let domCnt = 0; domCnt < len; domCnt++) {
|
|
||||||
const childNode = childNodes[domCnt]
|
|
||||||
const childType = this.domToYXml.get(childNode)
|
|
||||||
if (childType != null) {
|
|
||||||
if (childType === false) {
|
|
||||||
// should be ignored or is going to be deleted
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (expectedType !== null) {
|
|
||||||
if (expectedType !== childType) {
|
|
||||||
// 2.3 Not expected node
|
|
||||||
if (childType._parent !== yxml) {
|
|
||||||
// child was moved from another parent
|
|
||||||
// childType is going to be deleted by its previous parent
|
|
||||||
removeAssociation(this, childNode, this.domToYXml(childNode))
|
|
||||||
} else {
|
|
||||||
// child was moved to a different position.
|
|
||||||
childType._delete(y)
|
|
||||||
}
|
|
||||||
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode)
|
|
||||||
} else {
|
|
||||||
// Found expected node
|
|
||||||
prevExpectedType = expectedType
|
|
||||||
expectedType = iterateUntilUndeleted(expectedType._right)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 2.2 Fill _content with child nodes
|
|
||||||
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 2.1 A new node was found
|
|
||||||
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
132
src/Bindings/DomBinding/domObserver.js
Normal file
132
src/Bindings/DomBinding/domObserver.js
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
|
||||||
|
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||||
|
import {
|
||||||
|
iterateUntilUndeleted,
|
||||||
|
removeAssociation,
|
||||||
|
insertNodeHelper } from './util.js'
|
||||||
|
import diff from '../../Util/simpleDiff.js'
|
||||||
|
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 1. Check if any of the nodes was deleted
|
||||||
|
* 2. Iterate over the children.
|
||||||
|
* 2.1 If a node exists that is not yet bound to a type, insert a new node
|
||||||
|
* 2.2 If _contents.length < dom.childNodes.length, fill the
|
||||||
|
* rest of _content with childNodes
|
||||||
|
* 2.3 If a node was moved, delete it and
|
||||||
|
* recreate a new yxml element that is bound to that node.
|
||||||
|
* You can detect that a node was moved because expectedId
|
||||||
|
* !== actualId in the list
|
||||||
|
*/
|
||||||
|
function applyChangesFromDom (binding, dom, yxml, _document) {
|
||||||
|
if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const y = yxml._y
|
||||||
|
const knownChildren = new Set()
|
||||||
|
for (let i = dom.childNodes.length - 1; i >= 0; i--) {
|
||||||
|
const type = binding.domToType.get(dom.childNodes[i])
|
||||||
|
if (type !== undefined && type !== false) {
|
||||||
|
knownChildren.add(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 1. Check if any of the nodes was deleted
|
||||||
|
yxml.forEach(function (childType) {
|
||||||
|
if (knownChildren.has(childType) === false) {
|
||||||
|
childType._delete(y)
|
||||||
|
removeAssociation(binding, binding.typeToDom.get(childType), childType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 2. iterate
|
||||||
|
const childNodes = dom.childNodes
|
||||||
|
const len = childNodes.length
|
||||||
|
let prevExpectedType = null
|
||||||
|
let expectedType = iterateUntilUndeleted(yxml._start)
|
||||||
|
for (let domCnt = 0; domCnt < len; domCnt++) {
|
||||||
|
const childNode = childNodes[domCnt]
|
||||||
|
const childType = binding.domToType.get(childNode)
|
||||||
|
if (childType !== undefined) {
|
||||||
|
if (childType === false) {
|
||||||
|
// should be ignored or is going to be deleted
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (expectedType !== null) {
|
||||||
|
if (expectedType !== childType) {
|
||||||
|
// 2.3 Not expected node
|
||||||
|
if (childType._parent !== yxml) {
|
||||||
|
// child was moved from another parent
|
||||||
|
// childType is going to be deleted by its previous parent
|
||||||
|
removeAssociation(binding, childNode, childType)
|
||||||
|
} else {
|
||||||
|
// child was moved to a different position.
|
||||||
|
childType._delete(y)
|
||||||
|
removeAssociation(binding, childNode, childType)
|
||||||
|
}
|
||||||
|
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
|
||||||
|
} else {
|
||||||
|
// Found expected node. Continue.
|
||||||
|
prevExpectedType = expectedType
|
||||||
|
expectedType = iterateUntilUndeleted(expectedType._right)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2.2 Fill _content with child nodes
|
||||||
|
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 2.1 A new node was found
|
||||||
|
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function domObserver (mutations, _document) {
|
||||||
|
this._mutualExclude(() => {
|
||||||
|
this.type._y.transact(() => {
|
||||||
|
let diffChildren = new Set()
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
const dom = mutation.target
|
||||||
|
const yxml = this.domToType.get(dom)
|
||||||
|
if (yxml === false || yxml === undefined || yxml.constructor === YXmlHook) {
|
||||||
|
// dom element is filtered
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch (mutation.type) {
|
||||||
|
case 'characterData':
|
||||||
|
var change = diff(yxml.toString(), dom.nodeValue)
|
||||||
|
yxml.delete(change.pos, change.remove)
|
||||||
|
yxml.insert(change.pos, change.insert)
|
||||||
|
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 (yxml.constructor !== YXmlFragment && this.filter(dom.nodeName, attributes).size > 0) {
|
||||||
|
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.yOnChildrenChanged !== undefined) {
|
||||||
|
dom.yOnChildrenChanged()
|
||||||
|
}
|
||||||
|
const yxml = this.domToType.get(dom)
|
||||||
|
applyChangesFromDom(this, dom, yxml, _document)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
32
src/Bindings/DomBinding/domToType.js
Normal file
32
src/Bindings/DomBinding/domToType.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
|
||||||
|
import { YXmlText, YXmlElement } from '../../Types/YXml/YXml.js'
|
||||||
|
import { createAssociation } from './util.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Yjs type (YXml) based on the contents of a DOM Element.
|
||||||
|
*
|
||||||
|
* @param {Element|TextNode}
|
||||||
|
*/
|
||||||
|
export default function domToType (element, _document = document, binding) {
|
||||||
|
let type
|
||||||
|
switch (element.nodeType) {
|
||||||
|
case _document.ELEMENT_NODE:
|
||||||
|
type = new YXmlElement(element.nodeName)
|
||||||
|
const attrs = element.attributes
|
||||||
|
for (let i = attrs.length - 1; i >= 0; i--) {
|
||||||
|
const attr = attrs[i]
|
||||||
|
type.setAttribute(attr.name, attr.value)
|
||||||
|
}
|
||||||
|
const children = Array.from(element.childNodes).map(e => domToType(e, _document, binding))
|
||||||
|
type.insert(0, children)
|
||||||
|
break
|
||||||
|
case _document.TEXT_NODE:
|
||||||
|
type = new YXmlText()
|
||||||
|
type.insert(0, element.nodeValue)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
throw new Error('Can\'t transform this node type to a YXml type!')
|
||||||
|
}
|
||||||
|
createAssociation(binding, element, type)
|
||||||
|
return type
|
||||||
|
}
|
31
src/Bindings/DomBinding/filter.js
Normal file
31
src/Bindings/DomBinding/filter.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import isParentOf from '../../Util/isParentOf.js'
|
||||||
|
|
||||||
|
export function defaultFilter (nodeName, attrs) {
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function applyFilterOnType (y, binding, type) {
|
||||||
|
if (isParentOf(binding.type, type)) {
|
||||||
|
const nodeName = type.nodeName
|
||||||
|
let attributes = new Map()
|
||||||
|
if (type.getAttributes !== undefined) {
|
||||||
|
let attrs = type.getAttributes()
|
||||||
|
for (let key in attrs) {
|
||||||
|
attributes.set(key, attrs[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filteredAttributes = binding.filter(nodeName, new Map(attributes))
|
||||||
|
if (filteredAttributes === null) {
|
||||||
|
type._delete(y)
|
||||||
|
} else {
|
||||||
|
// iterate original attributes
|
||||||
|
attributes.forEach((value, key) => {
|
||||||
|
// delete all attributes that are not in filteredAttributes
|
||||||
|
if (filteredAttributes.has(key) === false) {
|
||||||
|
type.removeAttribute(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,30 +7,30 @@ let relativeSelection = null
|
|||||||
|
|
||||||
export let beforeTransactionSelectionFixer
|
export let beforeTransactionSelectionFixer
|
||||||
if (typeof getSelection !== 'undefined') {
|
if (typeof getSelection !== 'undefined') {
|
||||||
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) {
|
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||||
if (!remote) {
|
if (!remote) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
relativeSelection = { from: null, to: null, fromY: null, toY: null }
|
relativeSelection = { from: null, to: null, fromY: null, toY: null }
|
||||||
browserSelection = getSelection()
|
browserSelection = getSelection()
|
||||||
const anchorNode = browserSelection.anchorNode
|
const anchorNode = browserSelection.anchorNode
|
||||||
if (anchorNode !== null && anchorNode._yxml != null) {
|
const anchorNodeType = domBinding.domToType.get(anchorNode)
|
||||||
const yxml = anchorNode._yxml
|
if (anchorNode !== null && anchorNodeType !== undefined) {
|
||||||
relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset)
|
relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
|
||||||
relativeSelection.fromY = yxml._y
|
relativeSelection.fromY = anchorNodeType._y
|
||||||
}
|
}
|
||||||
const focusNode = browserSelection.focusNode
|
const focusNode = browserSelection.focusNode
|
||||||
if (focusNode !== null && focusNode._yxml != null) {
|
const focusNodeType = domBinding.domToType.get(focusNode)
|
||||||
const yxml = focusNode._yxml
|
if (focusNode !== null && focusNodeType !== undefined) {
|
||||||
relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset)
|
relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
|
||||||
relativeSelection.toY = yxml._y
|
relativeSelection.toY = focusNodeType._y
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function afterTransactionSelectionFixer (y, transaction, remote) {
|
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||||
if (relativeSelection === null || !remote) {
|
if (relativeSelection === null || !remote) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -46,7 +46,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
|||||||
if (from !== null) {
|
if (from !== null) {
|
||||||
let sel = fromRelativePosition(fromY, from)
|
let sel = fromRelativePosition(fromY, from)
|
||||||
if (sel !== null) {
|
if (sel !== null) {
|
||||||
let node = sel.type.getDom()
|
let node = domBinding.typeToDom.get(sel.type)
|
||||||
let offset = sel.offset
|
let offset = sel.offset
|
||||||
if (node !== anchorNode || offset !== anchorOffset) {
|
if (node !== anchorNode || offset !== anchorOffset) {
|
||||||
anchorNode = node
|
anchorNode = node
|
||||||
@ -58,7 +58,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
|||||||
if (to !== null) {
|
if (to !== null) {
|
||||||
let sel = fromRelativePosition(toY, to)
|
let sel = fromRelativePosition(toY, to)
|
||||||
if (sel !== null) {
|
if (sel !== null) {
|
||||||
let node = sel.type.getDom()
|
let node = domBinding.typeToDom.get(sel.type)
|
||||||
let offset = sel.offset
|
let offset = sel.offset
|
||||||
if (node !== focusNode || offset !== focusOffset) {
|
if (node !== focusNode || offset !== focusOffset) {
|
||||||
focusNode = node
|
focusNode = node
|
61
src/Bindings/DomBinding/typeObserver.js
Normal file
61
src/Bindings/DomBinding/typeObserver.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
|
||||||
|
import YXmlText from '../../Types/YXml/YXmlText.js'
|
||||||
|
import YXmlHook from '../../Types/YXml/YXmlHook.js'
|
||||||
|
import { removeDomChildrenUntilElementFound } from './util.js'
|
||||||
|
|
||||||
|
export default function typeObserver (events, _document) {
|
||||||
|
this._mutualExclude(() => {
|
||||||
|
events.forEach(event => {
|
||||||
|
const yxml = event.target
|
||||||
|
const dom = this.typeToDom.get(yxml)
|
||||||
|
if (dom !== undefined && dom !== false) {
|
||||||
|
if (yxml.constructor === YXmlText) {
|
||||||
|
dom.nodeValue = yxml.toString()
|
||||||
|
// TODO: use hasOwnProperty instead of === undefined check
|
||||||
|
} else if (event.attributesChanged !== undefined) {
|
||||||
|
// update attributes
|
||||||
|
event.attributesChanged.forEach(attributeName => {
|
||||||
|
const value = yxml.getAttribute(attributeName)
|
||||||
|
if (value === undefined) {
|
||||||
|
dom.removeAttribute(attributeName)
|
||||||
|
} else {
|
||||||
|
dom.setAttribute(attributeName, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
* TODO: instead of hard-checking the types, it would be best to
|
||||||
|
* specify the type's features. E.g.
|
||||||
|
* - _yxmlHasAttributes
|
||||||
|
* - _yxmlHasChildren
|
||||||
|
* Furthermore, the features shouldn't be encoded in the types,
|
||||||
|
* only in the attributes (above)
|
||||||
|
*/
|
||||||
|
if (event.childListChanged && yxml.constructor !== YXmlHook) {
|
||||||
|
let currentChild = dom.firstChild
|
||||||
|
yxml.forEach(childType => {
|
||||||
|
const childNode = this.typeToDom.get(childType)
|
||||||
|
const binding = this
|
||||||
|
switch (childNode) {
|
||||||
|
case undefined:
|
||||||
|
// Does not exist. Create it.
|
||||||
|
const node = childType.toDom(_document, binding)
|
||||||
|
dom.insertBefore(node, currentChild)
|
||||||
|
break
|
||||||
|
case false:
|
||||||
|
// nop
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// Is already attached to the dom.
|
||||||
|
// Find it and remove all dom nodes in-between.
|
||||||
|
removeDomChildrenUntilElementFound(dom, currentChild, childNode)
|
||||||
|
currentChild = childNode.nextSibling
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
removeDomChildrenUntilElementFound(dom, currentChild, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
@ -1,4 +1,6 @@
|
|||||||
|
|
||||||
|
import domToType from './domToType.js'
|
||||||
|
|
||||||
export function iterateUntilUndeleted (item) {
|
export function iterateUntilUndeleted (item) {
|
||||||
while (item !== null && item._deleted) {
|
while (item !== null && item._deleted) {
|
||||||
item = item._right
|
item = item._right
|
||||||
@ -12,15 +14,51 @@ export function removeAssociation (domBinding, dom, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createAssociation (domBinding, dom, type) {
|
export function createAssociation (domBinding, dom, type) {
|
||||||
domBinding.domToType.set(dom, type)
|
if (domBinding !== undefined) {
|
||||||
domBinding.typeToDom.set(type, dom)
|
domBinding.domToType.set(dom, type)
|
||||||
|
domBinding.typeToDom.set(type, dom)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertNodeHelper (yxml, prevExpectedNode, child) {
|
/**
|
||||||
let insertedNodes = yxml.insertDomElementsAfter(prevExpectedNode, [child])
|
* Insert Dom Elements after one of the children of this YXmlFragment.
|
||||||
|
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
||||||
|
* specified position.
|
||||||
|
*
|
||||||
|
* @param {YXmlElement} type The type in which to insert DOM elements.
|
||||||
|
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
||||||
|
* inserted after this node. Set null to insert at
|
||||||
|
* the beginning.
|
||||||
|
* @param {Array<Element>} doms The Dom elements to insert.
|
||||||
|
* @param {?Document} _document Optional. Provide the global document object.
|
||||||
|
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
||||||
|
*/
|
||||||
|
export function insertDomElementsAfter (type, prev, doms, _document, binding) {
|
||||||
|
return type.insertAfter(prev, doms.map(dom => domToType(dom, _document, binding)))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) {
|
||||||
|
let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
|
||||||
if (insertedNodes.length > 0) {
|
if (insertedNodes.length > 0) {
|
||||||
return insertedNodes[0]
|
return insertedNodes[0]
|
||||||
} else {
|
} else {
|
||||||
return prevExpectedNode
|
return prevExpectedNode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove children until `elem` is found.
|
||||||
|
*
|
||||||
|
* @param {Element} parent The parent of `elem` and `currentChild`.
|
||||||
|
* @param {Element} currentChild Start removing elements with `currentChild`. If
|
||||||
|
* `currentChild` is `elem` it won't be removed.
|
||||||
|
* @param {Element|null} elem The elemnt to look for.
|
||||||
|
*/
|
||||||
|
export function removeDomChildrenUntilElementFound (parent, currentChild, elem) {
|
||||||
|
while (currentChild !== elem) {
|
||||||
|
const del = currentChild
|
||||||
|
currentChild = currentChild.nextSibling
|
||||||
|
parent.removeChild(del)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import Binding from './Binding.js'
|
import Binding from '../Binding.js'
|
||||||
|
|
||||||
function typeObserver (event) {
|
function typeObserver (event) {
|
||||||
const quill = this.target
|
const quill = this.target
|
||||||
|
// Force flush Quill changes.
|
||||||
quill.update('yjs')
|
quill.update('yjs')
|
||||||
this._mutualExclude(function () {
|
this._mutualExclude(function () {
|
||||||
|
// Apply computed delta.
|
||||||
quill.updateContents(event.delta, 'yjs')
|
quill.updateContents(event.delta, 'yjs')
|
||||||
quill.update('yjs') // ignore applied changes
|
// Force flush Quill changes. Ignore applied changes.
|
||||||
|
quill.update('yjs')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,12 +19,14 @@ function quillObserver (delta) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Binding that binds a YText type to a Quill editor
|
* A Binding that binds a YText type to a Quill editor.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const quill = new Quill(document.createElement('div'))
|
* const quill = new Quill(document.createElement('div'))
|
||||||
* const type = y.define('quill', Y.Text)
|
* const type = y.define('quill', Y.Text)
|
||||||
* const binding = new Y.QuillBinding(quill, type)
|
* const binding = new Y.QuillBinding(quill, type)
|
||||||
|
* // Now modifications on the DOM will be reflected in the Type, and the other
|
||||||
|
* // way around!
|
||||||
*/
|
*/
|
||||||
export default class QuillBinding extends Binding {
|
export default class QuillBinding extends Binding {
|
||||||
/**
|
/**
|
||||||
@ -29,18 +34,18 @@ export default class QuillBinding extends Binding {
|
|||||||
* @param {Quill} quill
|
* @param {Quill} quill
|
||||||
*/
|
*/
|
||||||
constructor (textType, quill) {
|
constructor (textType, quill) {
|
||||||
// Binding handles textType as this.type and quill as this.target
|
// Binding handles textType as this.type and quill as this.target.
|
||||||
super(textType, quill)
|
super(textType, quill)
|
||||||
// set initial value
|
// Set initial value.
|
||||||
quill.setContents(textType.toDelta(), 'yjs')
|
quill.setContents(textType.toDelta(), 'yjs')
|
||||||
// Observers are handled by this class
|
// Observers are handled by this class.
|
||||||
this._typeObserver = typeObserver.bind(this)
|
this._typeObserver = typeObserver.bind(this)
|
||||||
this._quillObserver = quillObserver.bind(this)
|
this._quillObserver = quillObserver.bind(this)
|
||||||
textType.observe(this._typeObserver)
|
textType.observe(this._typeObserver)
|
||||||
quill.on('text-change', this._quillObserver)
|
quill.on('text-change', this._quillObserver)
|
||||||
}
|
}
|
||||||
destroy () {
|
destroy () {
|
||||||
// Remove everything that is handled by this class
|
// Remove everything that is handled by this class.
|
||||||
this.type.unobserve(this._typeObserver)
|
this.type.unobserve(this._typeObserver)
|
||||||
this.target.off('text-change', this._quillObserver)
|
this.target.off('text-change', this._quillObserver)
|
||||||
super.destroy()
|
super.destroy()
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import Binding from './Binding.js'
|
import Binding from '../Binding.js'
|
||||||
import simpleDiff from '../Util/simpleDiff.js'
|
import simpleDiff from '../../Util/simpleDiff.js'
|
||||||
import { getRelativePosition, fromRelativePosition } from '../Util/relativePosition.js'
|
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||||
|
|
||||||
function typeObserver () {
|
function typeObserver () {
|
||||||
this._mutualExclude(() => {
|
this._mutualExclude(() => {
|
||||||
|
@ -302,6 +302,7 @@ export default class YArray extends Type {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { defaultDomFilter } from './utils.js'
|
|
||||||
|
|
||||||
import YMap from '../YMap/YMap.js'
|
import YMap from '../YMap/YMap.js'
|
||||||
import { YXmlFragment } from './YXml.js'
|
import { YXmlFragment } from './YXml.js'
|
||||||
|
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An YXmlElement imitates the behavior of a
|
* An YXmlElement imitates the behavior of a
|
||||||
@ -10,25 +9,12 @@ import { YXmlFragment } from './YXml.js'
|
|||||||
* * An YXmlElement has attributes (key value pairs)
|
* * An YXmlElement has attributes (key value pairs)
|
||||||
* * An YXmlElement has childElements that must inherit from YXmlElement
|
* * An YXmlElement has childElements that must inherit from YXmlElement
|
||||||
*
|
*
|
||||||
* @param {String} arg1 Node name
|
* @param {String} nodeName Node name
|
||||||
* @param {Function} arg2 Dom filter
|
|
||||||
*/
|
*/
|
||||||
export default class YXmlElement extends YXmlFragment {
|
export default class YXmlElement extends YXmlFragment {
|
||||||
constructor (arg1, arg2, _document) {
|
constructor (nodeName = 'UNDEFINED') {
|
||||||
super()
|
super()
|
||||||
this.nodeName = null
|
this.nodeName = nodeName.toUpperCase()
|
||||||
this._scrollElement = null
|
|
||||||
if (typeof arg1 === 'string') {
|
|
||||||
this.nodeName = arg1.toUpperCase()
|
|
||||||
} else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === arg1.ELEMENT_NODE) {
|
|
||||||
this.nodeName = arg1.nodeName
|
|
||||||
this._setDom(arg1, _document)
|
|
||||||
} else {
|
|
||||||
this.nodeName = 'UNDEFINED'
|
|
||||||
}
|
|
||||||
if (typeof arg2 === 'function') {
|
|
||||||
this._domFilter = arg2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,48 +27,6 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
return struct
|
return struct
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Copies children and attributes from a dom node to this YXmlElement.
|
|
||||||
*/
|
|
||||||
_setDom (dom, _document) {
|
|
||||||
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 {
|
|
||||||
// tag is already set in constructor
|
|
||||||
// set attributes
|
|
||||||
let attributes = new Map()
|
|
||||||
for (let i = 0; i < dom.attributes.length; i++) {
|
|
||||||
let attr = dom.attributes[i]
|
|
||||||
// get attribute via getAttribute for custom element support (some write something different in attr.value)
|
|
||||||
attributes.set(attr.name, dom.getAttribute(attr.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)
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Bind a dom to to this YXmlElement. This means that the DOM changes when the
|
|
||||||
* YXmlElement is modified and that this YXmlElement changes when the DOM is
|
|
||||||
* modified.
|
|
||||||
*
|
|
||||||
* Currently only works in YXmlFragment.
|
|
||||||
*/
|
|
||||||
_bindToDom (dom, _document) {
|
|
||||||
_document = _document || document
|
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* Read the next Item in a Decoder and fill this Item with the read data.
|
* Read the next Item in a Decoder and fill this Item with the read data.
|
||||||
@ -127,9 +71,6 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
if (this.nodeName === null) {
|
if (this.nodeName === null) {
|
||||||
throw new Error('nodeName must be defined!')
|
throw new Error('nodeName must be defined!')
|
||||||
}
|
}
|
||||||
if (this._domFilter === defaultDomFilter && this._parent._domFilter !== undefined) {
|
|
||||||
this._domFilter = this._parent._domFilter
|
|
||||||
}
|
|
||||||
super._integrate(y)
|
super._integrate(y)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,21 +147,16 @@ export default class YXmlElement extends YXmlFragment {
|
|||||||
*
|
*
|
||||||
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
|
||||||
*/
|
*/
|
||||||
getDom (_document) {
|
toDom (_document = document, binding) {
|
||||||
_document = _document || document
|
const dom = _document.createElement(this.nodeName)
|
||||||
let dom = this._dom
|
let attrs = this.getAttributes()
|
||||||
if (dom == null) {
|
for (let key in attrs) {
|
||||||
dom = _document.createElement(this.nodeName)
|
dom.setAttribute(key, attrs[key])
|
||||||
dom._yxml = this
|
|
||||||
let attrs = this.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
dom.setAttribute(key, attrs[key])
|
|
||||||
}
|
|
||||||
this.forEach(yxml => {
|
|
||||||
dom.appendChild(yxml.getDom(_document))
|
|
||||||
})
|
|
||||||
this._bindToDom(dom, _document)
|
|
||||||
}
|
}
|
||||||
|
this.forEach(yxml => {
|
||||||
|
dom.appendChild(yxml.toDom(_document, binding))
|
||||||
|
})
|
||||||
|
createAssociation(binding, dom, this)
|
||||||
return dom
|
return dom
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,9 @@ import YEvent from '../../Util/YEvent.js'
|
|||||||
* An Event that describes changes on a YXml Element or Yxml Fragment
|
* An Event that describes changes on a YXml Element or Yxml Fragment
|
||||||
*/
|
*/
|
||||||
export default class YXmlEvent extends YEvent {
|
export default class YXmlEvent extends YEvent {
|
||||||
constructor (target, subs, remote) {
|
constructor (target, subs, remote, transaction) {
|
||||||
super(target)
|
super(target)
|
||||||
|
this._transaction = transaction
|
||||||
this.childListChanged = false
|
this.childListChanged = false
|
||||||
this.attributesChanged = new Set()
|
this.attributesChanged = new Set()
|
||||||
this.remote = remote
|
this.remote = remote
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/* global MutationObserver */
|
/* global MutationObserver */
|
||||||
|
|
||||||
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
import YXmlTreeWalker from './YXmlTreeWalker.js'
|
||||||
|
|
||||||
import YArray from '../YArray/YArray.js'
|
import YArray from '../YArray/YArray.js'
|
||||||
import YXmlEvent from './YXmlEvent.js'
|
import YXmlEvent from './YXmlEvent.js'
|
||||||
@ -9,33 +9,6 @@ import { YXmlText, YXmlHook } from './YXml.js'
|
|||||||
import { logID } from '../../MessageHandler/messageToString.js'
|
import { logID } from '../../MessageHandler/messageToString.js'
|
||||||
import diff from '../../Util/simpleDiff.js'
|
import diff from '../../Util/simpleDiff.js'
|
||||||
|
|
||||||
function domToYXml (parent, doms, _document) {
|
|
||||||
const types = []
|
|
||||||
doms.forEach(d => {
|
|
||||||
if (d._yxml != null && d._yxml !== false) {
|
|
||||||
d._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (parent._domFilter(d.nodeName, new Map()) !== null) {
|
|
||||||
let type
|
|
||||||
const hookName = d._yjsHook || (d.dataset != null ? d.dataset.yjsHook : undefined)
|
|
||||||
if (hookName !== undefined) {
|
|
||||||
type = new YXmlHook(hookName, d)
|
|
||||||
} else 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)
|
|
||||||
} else {
|
|
||||||
throw new Error('Unsupported node!')
|
|
||||||
}
|
|
||||||
// type.enableSmartScrolling(parent._scrollElement)
|
|
||||||
types.push(type)
|
|
||||||
} else {
|
|
||||||
d._yxml = false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the elements to which a set of CSS queries apply.
|
* Define the elements to which a set of CSS queries apply.
|
||||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
|
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
|
||||||
@ -48,64 +21,6 @@ function domToYXml (parent, doms, _document) {
|
|||||||
* @typedef {string} CSS_Selector
|
* @typedef {string} CSS_Selector
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
|
|
||||||
* position within them.
|
|
||||||
*
|
|
||||||
* Can be created with {@link YXmlFragment#createTreeWalker}
|
|
||||||
*/
|
|
||||||
class YXmlTreeWalker {
|
|
||||||
constructor (root, f) {
|
|
||||||
this._filter = f || (() => true)
|
|
||||||
this._root = root
|
|
||||||
this._currentNode = root
|
|
||||||
this._firstCall = true
|
|
||||||
}
|
|
||||||
[Symbol.iterator] () {
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Get the next node.
|
|
||||||
*
|
|
||||||
* @return {YXmlElement} The next node.
|
|
||||||
*/
|
|
||||||
next () {
|
|
||||||
let n = this._currentNode
|
|
||||||
if (this._firstCall) {
|
|
||||||
this._firstCall = false
|
|
||||||
if (!n._deleted && this._filter(n)) {
|
|
||||||
return { value: n, done: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
if (!n._deleted && (n.constructor === YXmlFragment._YXmlElement || n.constructor === YXmlFragment) && n._start !== null) {
|
|
||||||
// walk down in the tree
|
|
||||||
n = n._start
|
|
||||||
} else {
|
|
||||||
// walk right or up in the tree
|
|
||||||
while (n !== this._root) {
|
|
||||||
if (n._right !== null) {
|
|
||||||
n = n._right
|
|
||||||
break
|
|
||||||
}
|
|
||||||
n = n._parent
|
|
||||||
}
|
|
||||||
if (n === this._root) {
|
|
||||||
n = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (n === this._root) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} while (n !== null && (n._deleted || !this._filter(n)))
|
|
||||||
this._currentNode = n
|
|
||||||
if (n === null) {
|
|
||||||
return { done: true }
|
|
||||||
} else {
|
|
||||||
return { value: n, done: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a list of {@link YXmlElement}.
|
* Represents a list of {@link YXmlElement}.
|
||||||
@ -113,32 +28,6 @@ class YXmlTreeWalker {
|
|||||||
* Therefore it also must not be added as a childElement.
|
* Therefore it also must not be added as a childElement.
|
||||||
*/
|
*/
|
||||||
export default class YXmlFragment extends YArray {
|
export default class YXmlFragment extends YArray {
|
||||||
constructor () {
|
|
||||||
super()
|
|
||||||
this._dom = null
|
|
||||||
this._domFilter = defaultDomFilter
|
|
||||||
this._domObserver = null
|
|
||||||
// this function makes sure that either the
|
|
||||||
// dom event is executed, or the yjs observer is executed
|
|
||||||
var token = true
|
|
||||||
this._mutualExclude = f => {
|
|
||||||
if (token) {
|
|
||||||
token = false
|
|
||||||
try {
|
|
||||||
f()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
if (this._domObserver !== null) {
|
|
||||||
this._domObserver.takeRecords()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
token = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a subtree of childNodes.
|
* Create a subtree of childNodes.
|
||||||
*
|
*
|
||||||
@ -189,22 +78,6 @@ export default class YXmlFragment extends YArray {
|
|||||||
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables the smart scrolling functionality for a Dom Binding.
|
|
||||||
* This is useful when YXml is bound to a shared editor. When activated,
|
|
||||||
* the viewport will be changed to accommodate remote changes.
|
|
||||||
*
|
|
||||||
* @TODO: Disabled for now.
|
|
||||||
*
|
|
||||||
* @param {Element} scrollElement The node that is
|
|
||||||
*/
|
|
||||||
enableSmartScrolling (scrollElement) {
|
|
||||||
this._scrollElement = scrollElement
|
|
||||||
this.forEach(xml => {
|
|
||||||
xml.enableSmartScrolling(scrollElement)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dom filter function.
|
* Dom filter function.
|
||||||
*
|
*
|
||||||
@ -214,44 +87,12 @@ export default class YXmlFragment extends YArray {
|
|||||||
* @return {boolean} Whether to include the Dom node in the YXmlElement.
|
* @return {boolean} Whether to include the Dom node in the YXmlElement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter out Dom elements.
|
|
||||||
*
|
|
||||||
* @param {domFilter} f The filtering function that decides whether to include
|
|
||||||
* a Dom node.
|
|
||||||
*/
|
|
||||||
setDomFilter (f) {
|
|
||||||
this._domFilter = f
|
|
||||||
let attributes = new Map()
|
|
||||||
if (this.getAttributes !== undefined) {
|
|
||||||
let attrs = this.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._y.transact(() => {
|
|
||||||
let result = this._domFilter(this.nodeName, new Map(attributes))
|
|
||||||
if (result === null) {
|
|
||||||
this._delete(this._y)
|
|
||||||
} else {
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
if (!result.has(key)) {
|
|
||||||
this.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.forEach(xml => {
|
|
||||||
xml.setDomFilter(f)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* Creates YArray Event and calls observers.
|
* Creates YArray Event and calls observers.
|
||||||
*/
|
*/
|
||||||
_callObserver (transaction, parentSubs, remote) {
|
_callObserver (transaction, parentSubs, remote) {
|
||||||
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote))
|
this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote, transaction))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,119 +113,20 @@ export default class YXmlFragment extends YArray {
|
|||||||
* Type was deleted.
|
* Type was deleted.
|
||||||
*/
|
*/
|
||||||
_delete (y, createDelete) {
|
_delete (y, createDelete) {
|
||||||
this._unbindFromDom()
|
|
||||||
super._delete(y, createDelete)
|
super._delete(y, createDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @return {DocumentFragment} The dom representation of this
|
||||||
* Unbind this YXmlFragment from the Dom.
|
|
||||||
*/
|
*/
|
||||||
_unbindFromDom () {
|
toDom (_document = document, binding) {
|
||||||
if (this._domObserver != null) {
|
const fragment = _document.createDocumentFragment()
|
||||||
this._domObserver.disconnect()
|
createAssociation(binding, fragment, this)
|
||||||
this._domObserver = null
|
this.forEach(xmlType => {
|
||||||
}
|
fragment.insertBefore(xmlType.toDom(_document, binding), null)
|
||||||
if (this._dom != null) {
|
|
||||||
this._dom._yxml = null
|
|
||||||
this._dom = null
|
|
||||||
}
|
|
||||||
if (this._beforeTransactionHandler !== undefined) {
|
|
||||||
this._y.off('beforeTransaction', this._beforeTransactionHandler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert Dom Elements after one of the children of this YXmlFragment.
|
|
||||||
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
|
||||||
* specified position.
|
|
||||||
*
|
|
||||||
* @param {YXmlElement|null} prev The reference node. New YxmlElements are
|
|
||||||
* inserted after this node. Set null to insert at
|
|
||||||
* the beginning.
|
|
||||||
* @param {Array<Element>} doms The Dom elements to insert.
|
|
||||||
* @param {?Document} _document Optional. Provide the global document object.
|
|
||||||
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
|
||||||
*/
|
|
||||||
insertDomElementsAfter (prev, doms, _document) {
|
|
||||||
const types = domToYXml(this, doms, _document)
|
|
||||||
this.insertAfter(prev, types)
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert Dom Elements at a specified index.
|
|
||||||
* The Dom elements will be bound to a new YXmlElement and inserted at the
|
|
||||||
* specified position.
|
|
||||||
*
|
|
||||||
* @param {Integer} index The position to insert elements at.
|
|
||||||
* @param {Array<Element>} doms The Dom elements to insert.
|
|
||||||
* @param {?Document} _document Optional. Provide the global document object.
|
|
||||||
* @return {Array<YXmlElement>} The YxmlElements that are inserted.
|
|
||||||
*/
|
|
||||||
insertDomElements (index, doms, _document) {
|
|
||||||
const types = domToYXml(this, doms, _document)
|
|
||||||
this.insert(index, types)
|
|
||||||
return types
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the Dom representation of this YXml type..
|
|
||||||
*/
|
|
||||||
getDom () {
|
|
||||||
return this._dom
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bind this YXmlFragment and all its children to a Dom Element.
|
|
||||||
* The content of the Dom Element are replaced with the Dom representation of
|
|
||||||
* the children of this YXml Type.
|
|
||||||
*
|
|
||||||
* @param {Element} dom The Dom Element that should be bound to this Type.
|
|
||||||
* @param {?Document} _document Optional. Provide the global document object.
|
|
||||||
*/
|
|
||||||
bindToDom (dom, _document) {
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._unbindFromDom()
|
|
||||||
}
|
|
||||||
if (dom._yxml != null) {
|
|
||||||
dom._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
dom.innerHTML = ''
|
|
||||||
this.forEach(t => {
|
|
||||||
dom.insertBefore(t.getDom(_document), null)
|
|
||||||
})
|
})
|
||||||
this._bindToDom(dom, _document)
|
return fragment
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* 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
|
|
||||||
if (this._parent === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this._y.on('beforeTransaction', beforeTransactionSelectionFixer)
|
|
||||||
this._y.on('afterTransaction', afterTransactionSelectionFixer)
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* Transform this YXml Type to a readable format.
|
* Transform this YXml Type to a readable format.
|
||||||
|
@ -7,16 +7,12 @@ import { getHook, addHook } from './hooks.js'
|
|||||||
* @param {String} hookName nodeName of the Dom Node.
|
* @param {String} hookName nodeName of the Dom Node.
|
||||||
*/
|
*/
|
||||||
export default class YXmlHook extends YMap {
|
export default class YXmlHook extends YMap {
|
||||||
constructor (hookName, dom) {
|
constructor (hookName) {
|
||||||
super()
|
super()
|
||||||
this._dom = null
|
|
||||||
this.hookName = null
|
this.hookName = null
|
||||||
if (hookName !== undefined) {
|
if (hookName !== undefined) {
|
||||||
this.hookName = hookName
|
this.hookName = hookName
|
||||||
this._dom = dom
|
|
||||||
dom._yjsHook = hookName
|
dom._yjsHook = hookName
|
||||||
dom._yxml = this
|
|
||||||
getHook(hookName).fillType(dom, this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,27 +27,14 @@ export default class YXmlHook extends YMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Dom representation of this YXmlHook.
|
* Creates a DOM element that represents this YXmlHook.
|
||||||
|
*
|
||||||
|
* @return Element The DOM representation of this Type.
|
||||||
*/
|
*/
|
||||||
getDom (_document) {
|
toDom (_document = document) {
|
||||||
_document = _document || document
|
const dom = getHook(this.hookName).createDom(this)
|
||||||
if (this._dom === null) {
|
dom._yjsHook = this.hookName
|
||||||
const dom = getHook(this.hookName).createDom(this)
|
return dom
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
dom._yjsHook = this.hookName
|
|
||||||
}
|
|
||||||
return this._dom
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Removes the Dom binding.
|
|
||||||
*/
|
|
||||||
_unbindFromDom () {
|
|
||||||
this._dom._yxml = null
|
|
||||||
this._yxml = null
|
|
||||||
// TODO: cleanup hook?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -99,11 +82,5 @@ export default class YXmlHook extends YMap {
|
|||||||
}
|
}
|
||||||
super._integrate(y)
|
super._integrate(y)
|
||||||
}
|
}
|
||||||
setDomFilter () {
|
|
||||||
// TODO: implement new modfilter method!
|
|
||||||
}
|
|
||||||
enableSmartScrolling () {
|
|
||||||
// TODO: implement new smartscrolling method!
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
YXmlHook.addHook = addHook
|
YXmlHook.addHook = addHook
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import YText from '../YText/YText.js'
|
import YText from '../YText/YText.js'
|
||||||
|
import { createAssociation } from '../../Bindings/DomBinding/util.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents text in a Dom Element. In the future this type will also handle
|
* Represents text in a Dom Element. In the future this type will also handle
|
||||||
@ -7,93 +8,16 @@ import YText from '../YText/YText.js'
|
|||||||
* @param {String} arg1 Initial value.
|
* @param {String} arg1 Initial value.
|
||||||
*/
|
*/
|
||||||
export default class YXmlText extends YText {
|
export default class YXmlText extends YText {
|
||||||
constructor (arg1) {
|
|
||||||
let dom = null
|
|
||||||
let initialText = null
|
|
||||||
if (arg1 != null) {
|
|
||||||
if (arg1.nodeType != null && arg1.nodeType === arg1.TEXT_NODE) {
|
|
||||||
dom = arg1
|
|
||||||
initialText = dom.nodeValue
|
|
||||||
} else if (typeof arg1 === 'string') {
|
|
||||||
initialText = arg1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super(initialText)
|
|
||||||
this._dom = null
|
|
||||||
this._domObserver = null
|
|
||||||
this._domObserverListener = null
|
|
||||||
this._scrollElement = null
|
|
||||||
if (dom !== null) {
|
|
||||||
this._setDom(arg1)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
var token = true
|
|
||||||
this._mutualExclude = f => {
|
|
||||||
if (token) {
|
|
||||||
token = false
|
|
||||||
try {
|
|
||||||
f()
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
this._domObserver.takeRecords()
|
|
||||||
token = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.observe(event => {
|
|
||||||
if (this._dom != null) {
|
|
||||||
const dom = this._dom
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
let anchorViewPosition = getAnchorViewPosition(this._scrollElement)
|
|
||||||
let anchorViewFix
|
|
||||||
if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) {
|
|
||||||
anchorViewFix = anchorViewPosition
|
|
||||||
} else {
|
|
||||||
anchorViewFix = null
|
|
||||||
}
|
|
||||||
dom.nodeValue = this.toString()
|
|
||||||
fixScrollPosition(this._scrollElement, anchorViewFix)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
setDomFilter () {}
|
|
||||||
enableSmartScrolling (scrollElement) {
|
|
||||||
this._scrollElement = scrollElement
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* Creates a TextNode with the same textual content.
|
||||||
* Set Dom element / Text Node that represents the same content as this
|
|
||||||
* YXmlElement.
|
|
||||||
*
|
*
|
||||||
* @param {Element} dom The Dom Element / Text Node that is set to be
|
* @return TextNode
|
||||||
* equivalent to this Type.
|
|
||||||
*/
|
*/
|
||||||
_setDom (dom) {
|
toDom (_document = document, binding) {
|
||||||
if (this._dom != null) {
|
const dom = _document.createTextNode(this.toString())
|
||||||
this._unbindFromDom()
|
createAssociation(binding, dom, this)
|
||||||
}
|
return dom
|
||||||
if (dom._yxml != null) {
|
|
||||||
dom._yxml._unbindFromDom()
|
|
||||||
}
|
|
||||||
// set marker
|
|
||||||
this._dom = dom
|
|
||||||
dom._yxml = this
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Dom representation of this YXmlText.
|
|
||||||
*/
|
|
||||||
getDom (_document) {
|
|
||||||
_document = _document || document
|
|
||||||
if (this._dom === null) {
|
|
||||||
const dom = _document.createTextNode(this.toString())
|
|
||||||
this._setDom(dom)
|
|
||||||
return dom
|
|
||||||
}
|
|
||||||
return this._dom
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,22 +29,6 @@ export default class YXmlText extends YText {
|
|||||||
* Type was deleted.
|
* Type was deleted.
|
||||||
*/
|
*/
|
||||||
_delete (y, createDelete) {
|
_delete (y, createDelete) {
|
||||||
this._unbindFromDom()
|
|
||||||
super._delete(y, createDelete)
|
super._delete(y, createDelete)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Unbind this YXmlText from the Dom.
|
|
||||||
*/
|
|
||||||
_unbindFromDom () {
|
|
||||||
if (this._domObserver != null) {
|
|
||||||
this._domObserver.disconnect()
|
|
||||||
this._domObserver = null
|
|
||||||
}
|
|
||||||
if (this._dom != null) {
|
|
||||||
this._dom._yxml = null
|
|
||||||
this._dom = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
|
|
||||||
const filterMap = new Map()
|
|
||||||
|
|
||||||
export function addFilter (type, filter) {
|
|
||||||
if (!filterMap.has(type)) {
|
|
||||||
filterMap.set(type, new Set())
|
|
||||||
}
|
|
||||||
const filters = filterMap.get(type)
|
|
||||||
filters.add(filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function executeFilter (type) {
|
|
||||||
const y = type._y
|
|
||||||
let parent = type
|
|
||||||
const nodeName = type.nodeName
|
|
||||||
let attributes = new Map()
|
|
||||||
if (type.getAttributes !== undefined) {
|
|
||||||
let attrs = type.getAttributes()
|
|
||||||
for (let key in attrs) {
|
|
||||||
attributes.set(key, attrs[key])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let filteredAttributes = new Map(attributes)
|
|
||||||
// is not y, supports dom filtering
|
|
||||||
while (parent !== y && parent.setDomFilter != null) {
|
|
||||||
const filters = filterMap.get(parent)
|
|
||||||
if (filters !== undefined) {
|
|
||||||
for (let f of filters) {
|
|
||||||
filteredAttributes = f(nodeName, filteredAttributes)
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent = parent._parent
|
|
||||||
}
|
|
||||||
if (filteredAttributes === null) {
|
|
||||||
type._delete(y)
|
|
||||||
} else {
|
|
||||||
// iterate original attributes
|
|
||||||
attributes.forEach((value, key) => {
|
|
||||||
// delete all attributes that are not in filteredAttributes
|
|
||||||
if (!filteredAttributes.has(key)) {
|
|
||||||
type.removeAttribute(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,189 +0,0 @@
|
|||||||
import { YXmlText, YXmlHook } from './YXml.js'
|
|
||||||
|
|
||||||
export function defaultDomFilter (node, attributes) {
|
|
||||||
return attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAnchorViewPosition (scrollElement) {
|
|
||||||
if (scrollElement == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
let anchor = document.getSelection().anchorNode
|
|
||||||
if (anchor != null) {
|
|
||||||
let top = getBoundingClientRect(anchor).top
|
|
||||||
if (top >= 0 && top <= document.documentElement.clientHeight) {
|
|
||||||
return {
|
|
||||||
anchor: anchor,
|
|
||||||
top: top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
anchor: null,
|
|
||||||
scrollTop: scrollElement.scrollTop,
|
|
||||||
scrollHeight: scrollElement.scrollHeight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get BoundingClientRect that works on text nodes
|
|
||||||
export function getBoundingClientRect (element) {
|
|
||||||
if (element.getBoundingClientRect != null) {
|
|
||||||
// is element node
|
|
||||||
return element.getBoundingClientRect()
|
|
||||||
} else {
|
|
||||||
// is text node
|
|
||||||
if (element.parentNode == null) {
|
|
||||||
// range requires that text nodes have a parent
|
|
||||||
let span = document.createElement('span')
|
|
||||||
span.appendChild(element)
|
|
||||||
}
|
|
||||||
let range = document.createRange()
|
|
||||||
range.selectNode(element)
|
|
||||||
return range.getBoundingClientRect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fixScrollPosition (scrollElement, fix) {
|
|
||||||
if (scrollElement !== null && fix !== null) {
|
|
||||||
if (fix.anchor === null) {
|
|
||||||
if (scrollElement.scrollTop === fix.scrollTop) {
|
|
||||||
scrollElement.scrollTop = scrollElement.scrollHeight - fix.scrollHeight
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scrollElement.scrollTop = getBoundingClientRect(fix.anchor).top - fix.top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
this._mutualExclude(() => {
|
|
||||||
events.forEach(event => {
|
|
||||||
const yxml = event.target
|
|
||||||
const dom = yxml._dom
|
|
||||||
if (dom != null) {
|
|
||||||
// TODO: do this once before applying stuff
|
|
||||||
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
|
||||||
if (yxml.constructor === YXmlText) {
|
|
||||||
yxml._dom.nodeValue = yxml.toString()
|
|
||||||
} else if (event.attributesChanged !== undefined) {
|
|
||||||
// update attributes
|
|
||||||
event.attributesChanged.forEach(attributeName => {
|
|
||||||
const value = yxml.getAttribute(attributeName)
|
|
||||||
if (value === undefined) {
|
|
||||||
dom.removeAttribute(attributeName)
|
|
||||||
} else {
|
|
||||||
dom.setAttribute(attributeName, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
/*
|
|
||||||
* TODO: instead of hard-checking the types, it would be best to
|
|
||||||
* specify the type's features. E.g.
|
|
||||||
* - _yxmlHasAttributes
|
|
||||||
* - _yxmlHasChildren
|
|
||||||
* Furthermore, the features shouldn't be encoded in the types,
|
|
||||||
* only in the attributes (above)
|
|
||||||
*/
|
|
||||||
if (event.childListChanged && yxml.constructor !== YXmlHook) {
|
|
||||||
let currentChild = dom.firstChild
|
|
||||||
yxml.forEach(function (t) {
|
|
||||||
let expectedChild = t.getDom(_document)
|
|
||||||
if (expectedChild.parentNode === dom) {
|
|
||||||
// is already attached to the dom. Look for it
|
|
||||||
while (currentChild !== expectedChild) {
|
|
||||||
let del = currentChild
|
|
||||||
currentChild = currentChild.nextSibling
|
|
||||||
dom.removeChild(del)
|
|
||||||
}
|
|
||||||
currentChild = currentChild.nextSibling
|
|
||||||
} else {
|
|
||||||
// this dom is not yet attached to dom
|
|
||||||
dom.insertBefore(expectedChild, currentChild)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
while (currentChild !== null) {
|
|
||||||
let tmp = currentChild.nextSibling
|
|
||||||
dom.removeChild(currentChild)
|
|
||||||
currentChild = tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* TODO: smartscrolling
|
|
||||||
.. else if (event.type === 'childInserted' || event.type === 'insert') {
|
|
||||||
let nodes = event.values
|
|
||||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
||||||
let node = nodes[i]
|
|
||||||
node.setDomFilter(yxml._domFilter)
|
|
||||||
node.enableSmartScrolling(yxml._scrollElement)
|
|
||||||
let dom = node.getDom()
|
|
||||||
let fixPosition = null
|
|
||||||
let nextDom = null
|
|
||||||
if (yxml._content.length > event.index + i + 1) {
|
|
||||||
nextDom = yxml.get(event.index + i + 1).getDom()
|
|
||||||
}
|
|
||||||
yxml._dom.insertBefore(dom, nextDom)
|
|
||||||
if (anchorViewPosition === null) {
|
|
||||||
// nop
|
|
||||||
} else if (anchorViewPosition.anchor !== null) {
|
|
||||||
// no scrolling when current selection
|
|
||||||
if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
} else if (getBoundingClientRect(dom).top <= 0) {
|
|
||||||
// adjust scrolling if modified element is out of view,
|
|
||||||
// there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
fixScrollPosition(yxml._scrollElement, fixPosition)
|
|
||||||
}
|
|
||||||
} else if (event.type === 'childRemoved' || event.type === 'delete') {
|
|
||||||
for (let i = event.values.length - 1; i >= 0; i--) {
|
|
||||||
let dom = event.values[i]._dom
|
|
||||||
let fixPosition = null
|
|
||||||
if (anchorViewPosition === null) {
|
|
||||||
// nop
|
|
||||||
} else if (anchorViewPosition.anchor !== null) {
|
|
||||||
// no scrolling when current selection
|
|
||||||
if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) {
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
} else if (getBoundingClientRect(dom).top <= 0) {
|
|
||||||
// adjust scrolling if modified element is out of view,
|
|
||||||
// there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
|
|
||||||
fixPosition = anchorViewPosition
|
|
||||||
}
|
|
||||||
dom.remove()
|
|
||||||
fixScrollPosition(yxml._scrollElement, fixPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
18
src/Util/isParentOf.js
Normal file
18
src/Util/isParentOf.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Check if `parent` is a parent of `child`.
|
||||||
|
*
|
||||||
|
* @param {Type} parent
|
||||||
|
* @param {Type} child
|
||||||
|
* @return {Boolean} Whether `parent` is a parent of `child`.
|
||||||
|
*/
|
||||||
|
export default function isParentOf (parent, child) {
|
||||||
|
child = child._parent
|
||||||
|
while (child !== null) {
|
||||||
|
if (child === parent) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
child = child._parent
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -14,8 +14,9 @@ import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Types/YXml/YXml
|
|||||||
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'
|
||||||
import TextareaBinding from './Bindings/TextareaBinding.js'
|
import TextareaBinding from './Bindings/TextareaBinding/TextareaBinding.js'
|
||||||
import QuillBinding from './Bindings/QuillBinding.js'
|
import QuillBinding from './Bindings/QuillBinding/QuillBinding.js'
|
||||||
|
import DomBinding from './Bindings/DomBinding/DomBinding.js'
|
||||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
|
||||||
|
|
||||||
import debug from 'debug'
|
import debug from 'debug'
|
||||||
@ -33,6 +34,7 @@ Y.XmlHook = YXmlHook
|
|||||||
|
|
||||||
Y.TextareaBinding = TextareaBinding
|
Y.TextareaBinding = TextareaBinding
|
||||||
Y.QuillBinding = QuillBinding
|
Y.QuillBinding = QuillBinding
|
||||||
|
Y.DomBinding = DomBinding
|
||||||
|
|
||||||
Y.utils = {
|
Y.utils = {
|
||||||
BinaryDecoder,
|
BinaryDecoder,
|
||||||
|
10
src/Y.js
10
src/Y.js
@ -6,6 +6,8 @@ import RootID from './Util/ID/RootID.js'
|
|||||||
import NamedEventHandler from './Util/NamedEventHandler.js'
|
import NamedEventHandler from './Util/NamedEventHandler.js'
|
||||||
import Transaction from './Transaction.js'
|
import Transaction from './Transaction.js'
|
||||||
|
|
||||||
|
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A positive natural number including zero: 0, 1, 2, ..
|
* A positive natural number including zero: 0, 1, 2, ..
|
||||||
*
|
*
|
||||||
@ -44,7 +46,11 @@ export default class Y extends NamedEventHandler {
|
|||||||
}
|
}
|
||||||
this._contentReady = false
|
this._contentReady = false
|
||||||
this._opts = opts
|
this._opts = opts
|
||||||
this.userID = generateUserID()
|
if (typeof opts.userID !== 'number') {
|
||||||
|
this.userID = generateUserID()
|
||||||
|
} else {
|
||||||
|
this.userID = opts.userID
|
||||||
|
}
|
||||||
// TODO: This should be a Map so we can use encodables as keys
|
// TODO: This should be a Map so we can use encodables as keys
|
||||||
this.share = {}
|
this.share = {}
|
||||||
this.ds = new DeleteStore(this)
|
this.ds = new DeleteStore(this)
|
||||||
@ -77,6 +83,8 @@ export default class Y extends NamedEventHandler {
|
|||||||
} else {
|
} else {
|
||||||
initConnection()
|
initConnection()
|
||||||
}
|
}
|
||||||
|
// for compatibility with isParentOf
|
||||||
|
this._parent = null
|
||||||
}
|
}
|
||||||
_setContentReady () {
|
_setContentReady () {
|
||||||
if (!this._contentReady) {
|
if (!this._contentReady) {
|
||||||
|
@ -3,14 +3,13 @@ import { test } from 'cutest'
|
|||||||
|
|
||||||
test('set property', async function xml0 (t) {
|
test('set property', async function xml0 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||||
xml0.setAttribute('height', 10)
|
xml0.setAttribute('height', '10')
|
||||||
t.assert(xml0.getAttribute('height') === 10, 'Simple set+get works')
|
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
t.assert(xml1.getAttribute('height') === 10, 'Simple set+get works (remote)')
|
t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)')
|
||||||
await compareUsers(t, users)
|
await compareUsers(t, users)
|
||||||
})
|
})
|
||||||
|
|
||||||
/* TODO: Test YXml events!
|
|
||||||
test('events', async function xml1 (t) {
|
test('events', async function xml1 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
|
||||||
var event
|
var event
|
||||||
@ -29,48 +28,28 @@ test('events', async function xml1 (t) {
|
|||||||
remoteEvent = e
|
remoteEvent = e
|
||||||
})
|
})
|
||||||
xml0.setAttribute('key', 'value')
|
xml0.setAttribute('key', 'value')
|
||||||
expectedEvent = {
|
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key')
|
||||||
type: 'attributeChanged',
|
|
||||||
value: 'value',
|
|
||||||
name: 'key'
|
|
||||||
}
|
|
||||||
t.compare(event, expectedEvent, 'attribute changed event')
|
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
t.compare(remoteEvent, expectedEvent, 'attribute changed event (remote)')
|
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)')
|
||||||
// check attributeRemoved
|
// check attributeRemoved
|
||||||
xml0.removeAttribute('key')
|
xml0.removeAttribute('key')
|
||||||
expectedEvent = {
|
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute')
|
||||||
type: 'attributeRemoved',
|
|
||||||
name: 'key'
|
|
||||||
}
|
|
||||||
t.compare(event, expectedEvent, 'attribute deleted event')
|
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
t.compare(remoteEvent, expectedEvent, 'attribute deleted event (remote)')
|
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)')
|
||||||
// test childInserted event
|
|
||||||
expectedEvent = {
|
|
||||||
type: 'childInserted',
|
|
||||||
index: 0
|
|
||||||
}
|
|
||||||
xml0.insert(0, [new Y.XmlText('some text')])
|
xml0.insert(0, [new Y.XmlText('some text')])
|
||||||
t.compare(event, expectedEvent, 'child inserted event')
|
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element')
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)')
|
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)')
|
||||||
// test childRemoved
|
// test childRemoved
|
||||||
xml0.delete(0)
|
xml0.delete(0)
|
||||||
expectedEvent = {
|
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element')
|
||||||
type: 'childRemoved',
|
|
||||||
index: 0
|
|
||||||
}
|
|
||||||
t.compare(event, expectedEvent, 'child deleted event')
|
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
t.compare(remoteEvent, expectedEvent, 'child deleted event (remote)')
|
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)')
|
||||||
await compareUsers(t, users)
|
await compareUsers(t, users)
|
||||||
})
|
})
|
||||||
*/
|
|
||||||
|
|
||||||
test('attribute modifications (y -> dom)', async function xml2 (t) {
|
test('attribute modifications (y -> dom)', async function xml2 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.setAttribute('height', '100px')
|
xml0.setAttribute('height', '100px')
|
||||||
await wait()
|
await wait()
|
||||||
t.assert(dom0.getAttribute('height') === '100px', 'setAttribute')
|
t.assert(dom0.getAttribute('height') === '100px', 'setAttribute')
|
||||||
@ -84,8 +63,7 @@ test('attribute modifications (y -> dom)', async function xml2 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('attribute modifications (dom -> y)', async function xml3 (t) {
|
test('attribute modifications (dom -> y)', async function xml3 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
dom0.setAttribute('height', '100px')
|
dom0.setAttribute('height', '100px')
|
||||||
await wait()
|
await wait()
|
||||||
t.assert(xml0.getAttribute('height') === '100px', 'setAttribute')
|
t.assert(xml0.getAttribute('height') === '100px', 'setAttribute')
|
||||||
@ -99,8 +77,7 @@ test('attribute modifications (dom -> y)', async function xml3 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('element insert (dom -> y)', async function xml4 (t) {
|
test('element insert (dom -> y)', async function xml4 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
dom0.insertBefore(document.createTextNode('some text'), null)
|
dom0.insertBefore(document.createTextNode('some text'), null)
|
||||||
dom0.insertBefore(document.createElement('p'), null)
|
dom0.insertBefore(document.createElement('p'), null)
|
||||||
await wait()
|
await wait()
|
||||||
@ -110,8 +87,7 @@ test('element insert (dom -> y)', async function xml4 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('element insert (y -> dom)', async function xml5 (t) {
|
test('element insert (y -> dom)', async function xml5 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.insert(0, [new Y.XmlText('some text')])
|
xml0.insert(0, [new Y.XmlText('some text')])
|
||||||
xml0.insert(1, [new Y.XmlElement('p')])
|
xml0.insert(1, [new Y.XmlElement('p')])
|
||||||
t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node')
|
t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node')
|
||||||
@ -120,8 +96,7 @@ test('element insert (y -> dom)', async function xml5 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('y on insert, then delete (dom -> y)', async function xml6 (t) {
|
test('y on insert, then delete (dom -> y)', async function xml6 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
dom0.insertBefore(document.createElement('p'), null)
|
dom0.insertBefore(document.createElement('p'), null)
|
||||||
await wait()
|
await wait()
|
||||||
t.assert(xml0.length === 1, 'one node present')
|
t.assert(xml0.length === 1, 'one node present')
|
||||||
@ -132,8 +107,7 @@ test('y on insert, then delete (dom -> y)', async function xml6 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('y on insert, then delete (y -> dom)', async function xml7 (t) {
|
test('y on insert, then delete (y -> dom)', async function xml7 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.insert(0, [new Y.XmlElement('p')])
|
xml0.insert(0, [new Y.XmlElement('p')])
|
||||||
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
|
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
|
||||||
xml0.delete(0, 1)
|
xml0.delete(0, 1)
|
||||||
@ -142,8 +116,7 @@ test('y on insert, then delete (y -> dom)', async function xml7 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('delete consecutive (1) (Text)', async function xml8 (t) {
|
test('delete consecutive (1) (Text)', async function xml8 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
||||||
await wait()
|
await wait()
|
||||||
xml0.delete(1, 2)
|
xml0.delete(1, 2)
|
||||||
@ -155,8 +128,7 @@ test('delete consecutive (1) (Text)', async function xml8 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('delete consecutive (2) (Text)', async function xml9 (t) {
|
test('delete consecutive (2) (Text)', async function xml9 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
|
||||||
await wait()
|
await wait()
|
||||||
xml0.delete(0, 1)
|
xml0.delete(0, 1)
|
||||||
@ -169,8 +141,7 @@ test('delete consecutive (2) (Text)', async function xml9 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('delete consecutive (1) (Element)', async function xml10 (t) {
|
test('delete consecutive (1) (Element)', async function xml10 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||||
await wait()
|
await wait()
|
||||||
xml0.delete(1, 2)
|
xml0.delete(1, 2)
|
||||||
@ -182,8 +153,7 @@ test('delete consecutive (1) (Element)', async function xml10 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('delete consecutive (2) (Element)', async function xml11 (t) {
|
test('delete consecutive (2) (Element)', async function xml11 (t) {
|
||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||||
await wait()
|
await wait()
|
||||||
xml0.delete(0, 1)
|
xml0.delete(0, 1)
|
||||||
@ -196,9 +166,7 @@ test('delete consecutive (2) (Element)', async function xml11 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
let dom1 = xml1.getDom()
|
|
||||||
users[1].disconnect()
|
users[1].disconnect()
|
||||||
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
|
||||||
xml0.insert(0, [new Y.XmlElement('X'), new Y.XmlElement('Y'), new Y.XmlElement('Z')])
|
xml0.insert(0, [new Y.XmlElement('X'), new Y.XmlElement('Y'), new Y.XmlElement('Z')])
|
||||||
@ -212,9 +180,7 @@ test('Receive a bunch of elements (with disconnect)', async function xml12 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('move element to a different position', async function xml13 (t) {
|
test('move element to a different position', async function xml13 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
let dom1 = xml1.getDom()
|
|
||||||
dom0.append(document.createElement('div'))
|
dom0.append(document.createElement('div'))
|
||||||
dom0.append(document.createElement('h1'))
|
dom0.append(document.createElement('h1'))
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
@ -227,9 +193,7 @@ test('move element to a different position', async function xml13 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('filter node', async function xml14 (t) {
|
test('filter node', async function xml14 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
var { users, xml0, xml1, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
let dom1 = xml1.getDom()
|
|
||||||
let domFilter = (nodeName, attrs) => {
|
let domFilter = (nodeName, attrs) => {
|
||||||
if (nodeName === 'H1') {
|
if (nodeName === 'H1') {
|
||||||
return null
|
return null
|
||||||
@ -237,8 +201,8 @@ test('filter node', async function xml14 (t) {
|
|||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
xml0.setDomFilter(domFilter)
|
domBinding0.setFilter(domFilter)
|
||||||
xml1.setDomFilter(domFilter)
|
domBinding1.setFilter(domFilter)
|
||||||
dom0.append(document.createElement('div'))
|
dom0.append(document.createElement('div'))
|
||||||
dom0.append(document.createElement('h1'))
|
dom0.append(document.createElement('h1'))
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
@ -248,15 +212,13 @@ test('filter node', async function xml14 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('filter attribute', async function xml15 (t) {
|
test('filter attribute', async function xml15 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
var { users, xml0, xml1, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
let dom1 = xml1.getDom()
|
|
||||||
let domFilter = (nodeName, attrs) => {
|
let domFilter = (nodeName, attrs) => {
|
||||||
attrs.delete('hidden')
|
attrs.delete('hidden')
|
||||||
return attrs
|
return attrs
|
||||||
}
|
}
|
||||||
xml0.setDomFilter(domFilter)
|
domBinding0.setFilter(domFilter)
|
||||||
xml1.setDomFilter(domFilter)
|
domBinding1.setFilter(domFilter)
|
||||||
dom0.setAttribute('hidden', 'true')
|
dom0.setAttribute('hidden', 'true')
|
||||||
dom0.setAttribute('style', 'height: 30px')
|
dom0.setAttribute('style', 'height: 30px')
|
||||||
dom0.setAttribute('data-me', '77')
|
dom0.setAttribute('data-me', '77')
|
||||||
@ -269,9 +231,7 @@ test('filter attribute', async function xml15 (t) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('deep element insert', async function xml16 (t) {
|
test('deep element insert', async function xml16 (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
|
||||||
let dom0 = xml0.getDom()
|
|
||||||
let dom1 = xml1.getDom()
|
|
||||||
let deepElement = document.createElement('p')
|
let deepElement = document.createElement('p')
|
||||||
let boldElement = document.createElement('b')
|
let boldElement = document.createElement('b')
|
||||||
let attrElement = document.createElement('img')
|
let attrElement = document.createElement('img')
|
||||||
@ -291,8 +251,8 @@ test('treeWalker', async function xml17 (t) {
|
|||||||
var { users, xml0 } = await initArrays(t, { users: 3 })
|
var { users, xml0 } = await initArrays(t, { users: 3 })
|
||||||
let paragraph1 = new Y.XmlElement('p')
|
let paragraph1 = new Y.XmlElement('p')
|
||||||
let paragraph2 = new Y.XmlElement('p')
|
let paragraph2 = new Y.XmlElement('p')
|
||||||
let text1 = new Y.Text('init')
|
let text1 = new Y.XmlText('init')
|
||||||
let text2 = new Y.Text('text')
|
let text2 = new Y.XmlText('text')
|
||||||
paragraph1.insert(0, [text1, text2])
|
paragraph1.insert(0, [text1, text2])
|
||||||
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
|
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
|
||||||
let allParagraphs = xml0.querySelectorAll('p')
|
let allParagraphs = xml0.querySelectorAll('p')
|
||||||
@ -309,8 +269,8 @@ test('treeWalker', async function xml17 (t) {
|
|||||||
* Incoming changes that contain malicious attributes should be deleted.
|
* Incoming changes that contain malicious attributes should be deleted.
|
||||||
*/
|
*/
|
||||||
test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
||||||
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
|
var { users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 })
|
||||||
xml0.setDomFilter(function (nodeName, attributes) {
|
domBinding0.setFilter(function (nodeName, attributes) {
|
||||||
attributes.delete('malicious')
|
attributes.delete('malicious')
|
||||||
if (nodeName === 'HIDEME') {
|
if (nodeName === 'HIDEME') {
|
||||||
return null
|
return null
|
||||||
@ -320,10 +280,6 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
|||||||
return attributes
|
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 paragraph = new Y.XmlElement('p')
|
||||||
let hideMe = new Y.XmlElement('hideMe')
|
let hideMe = new Y.XmlElement('hideMe')
|
||||||
let span = new Y.XmlElement('span')
|
let span = new Y.XmlElement('span')
|
||||||
@ -337,8 +293,8 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
|||||||
paragraph.insert(0, [tag2])
|
paragraph.insert(0, [tag2])
|
||||||
await flushAll(t, users)
|
await flushAll(t, users)
|
||||||
// check dom
|
// check dom
|
||||||
paragraph.getDom().setAttribute('malicious', 'true')
|
domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true')
|
||||||
span.getDom().setAttribute('malicious', 'true')
|
domBinding0.typeToDom.get(span).setAttribute('malicious', 'true')
|
||||||
// check incoming attributes
|
// check incoming attributes
|
||||||
xml1.get(0).get(0).setAttribute('malicious', 'true')
|
xml1.get(0).get(0).setAttribute('malicious', 'true')
|
||||||
xml1.insert(0, [new Y.XmlElement('hideMe')])
|
xml1.insert(0, [new Y.XmlElement('hideMe')])
|
||||||
@ -350,35 +306,35 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
|
|||||||
// TODO: move elements
|
// TODO: move elements
|
||||||
var xmlTransactions = [
|
var xmlTransactions = [
|
||||||
function attributeChange (t, user, chance) {
|
function attributeChange (t, user, chance) {
|
||||||
user.get('xml', Y.XmlElement).getDom().setAttribute(chance.word(), chance.word())
|
user.dom.setAttribute(chance.word(), chance.word())
|
||||||
},
|
},
|
||||||
function attributeChangeHidden (t, user, chance) {
|
function attributeChangeHidden (t, user, chance) {
|
||||||
user.get('xml', Y.XmlElement).getDom().setAttribute('hidden', chance.word())
|
user.dom.setAttribute('hidden', chance.word())
|
||||||
},
|
},
|
||||||
function insertText (t, user, chance) {
|
function insertText (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||||
dom.insertBefore(document.createTextNode(chance.word()), succ)
|
dom.insertBefore(document.createTextNode(chance.word()), succ)
|
||||||
},
|
},
|
||||||
function insertHiddenDom (t, user, chance) {
|
function insertHiddenDom (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||||
dom.insertBefore(document.createElement('hidden'), succ)
|
dom.insertBefore(document.createElement('hidden'), succ)
|
||||||
},
|
},
|
||||||
function insertDom (t, user, chance) {
|
function insertDom (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null
|
||||||
dom.insertBefore(document.createElement(chance.word()), succ)
|
dom.insertBefore(document.createElement(chance.word()), succ)
|
||||||
},
|
},
|
||||||
function deleteChild (t, user, chance) {
|
function deleteChild (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
if (dom.childNodes.length > 0) {
|
if (dom.childNodes.length > 0) {
|
||||||
var d = chance.pickone(dom.childNodes)
|
var d = chance.pickone(dom.childNodes)
|
||||||
d.remove()
|
d.remove()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function insertTextSecondLayer (t, user, chance) {
|
function insertTextSecondLayer (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
if (dom.children.length > 0) {
|
if (dom.children.length > 0) {
|
||||||
let dom2 = chance.pickone(dom.children)
|
let dom2 = chance.pickone(dom.children)
|
||||||
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
||||||
@ -386,7 +342,7 @@ var xmlTransactions = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
function insertDomSecondLayer (t, user, chance) {
|
function insertDomSecondLayer (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
if (dom.children.length > 0) {
|
if (dom.children.length > 0) {
|
||||||
let dom2 = chance.pickone(dom.children)
|
let dom2 = chance.pickone(dom.children)
|
||||||
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
|
||||||
@ -394,7 +350,7 @@ var xmlTransactions = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
function deleteChildSecondLayer (t, user, chance) {
|
function deleteChildSecondLayer (t, user, chance) {
|
||||||
let dom = user.get('xml', Y.XmlElement).getDom()
|
let dom = user.dom
|
||||||
if (dom.children.length > 0) {
|
if (dom.children.length > 0) {
|
||||||
let dom2 = chance.pickone(dom.children)
|
let dom2 = chance.pickone(dom.children)
|
||||||
if (dom2.childNodes.length > 0) {
|
if (dom2.childNodes.length > 0) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import _Y from '../src/Y.js'
|
import _Y from '../src/Y.dist.js'
|
||||||
import yTest from './test-connector.js'
|
import { DomBinding } from '../src/Y.js'
|
||||||
|
import TestConnector from './test-connector.js'
|
||||||
|
|
||||||
import Chance from 'chance'
|
import Chance from 'chance'
|
||||||
import ItemJSON from '../src/Struct/ItemJSON.js'
|
import ItemJSON from '../src/Struct/ItemJSON.js'
|
||||||
@ -10,11 +11,11 @@ import Quill from 'quill'
|
|||||||
|
|
||||||
export const Y = _Y
|
export const Y = _Y
|
||||||
|
|
||||||
Y.extend(yTest)
|
|
||||||
|
|
||||||
export const database = { name: 'memory' }
|
export const database = { name: 'memory' }
|
||||||
export const connector = { name: 'test', url: 'http://localhost:1234' }
|
export const connector = { name: 'test', url: 'http://localhost:1234' }
|
||||||
|
|
||||||
|
Y.test = TestConnector
|
||||||
|
|
||||||
function getStateSet (y) {
|
function getStateSet (y) {
|
||||||
let ss = {}
|
let ss = {}
|
||||||
for (let [user, clock] of y.ss.state) {
|
for (let [user, clock] of y.ss.state) {
|
||||||
@ -40,6 +41,7 @@ function getDeleteSet (y) {
|
|||||||
return ds
|
return ds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove?
|
||||||
export function attrsObject (dom) {
|
export function attrsObject (dom) {
|
||||||
let keys = []
|
let keys = []
|
||||||
let yxml = dom._yxml
|
let yxml = dom._yxml
|
||||||
@ -55,6 +57,7 @@ export function attrsObject (dom) {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove?
|
||||||
export function domToJson (dom) {
|
export function domToJson (dom) {
|
||||||
if (dom.nodeType === document.TEXT_NODE) {
|
if (dom.nodeType === document.TEXT_NODE) {
|
||||||
return dom.textContent
|
return dom.textContent
|
||||||
@ -140,6 +143,14 @@ export async function compareUsers (t, users) {
|
|||||||
users.map(u => u.destroy())
|
users.map(u => u.destroy())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function domFilter (nodeName, attrs) {
|
||||||
|
if (nodeName === 'HIDDEN') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
attrs.delete('hidden')
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
export async function initArrays (t, opts) {
|
export async function initArrays (t, opts) {
|
||||||
var result = {
|
var result = {
|
||||||
users: []
|
users: []
|
||||||
@ -154,27 +165,25 @@ export async function initArrays (t, opts) {
|
|||||||
connOpts = Object.assign({ role: 'slave' }, conn)
|
connOpts = Object.assign({ role: 'slave' }, conn)
|
||||||
}
|
}
|
||||||
let y = new Y(connOpts.room, {
|
let y = new Y(connOpts.room, {
|
||||||
_userID: i, // evil hackery, don't try this at home
|
userID: i, // evil hackery, don't try this at home
|
||||||
connector: connOpts
|
connector: connOpts
|
||||||
})
|
})
|
||||||
result.users.push(y)
|
result.users.push(y)
|
||||||
result['array' + i] = y.define('array', Y.Array)
|
result['array' + i] = y.define('array', Y.Array)
|
||||||
result['map' + i] = y.define('map', Y.Map)
|
result['map' + i] = y.define('map', Y.Map)
|
||||||
result['xml' + i] = y.define('xml', Y.XmlElement)
|
const yxml = y.define('xml', Y.XmlElement)
|
||||||
|
result['xml' + i] = yxml
|
||||||
|
const dom = document.createElement('my-dom')
|
||||||
|
const domBinding = new DomBinding(yxml, dom, { domFilter })
|
||||||
|
result['domBinding' + i] = domBinding
|
||||||
|
result['dom' + i] = dom
|
||||||
const textType = y.define('text', Y.Text)
|
const textType = y.define('text', Y.Text)
|
||||||
result['text' + i] = textType
|
result['text' + i] = textType
|
||||||
const quill = new Quill(document.createElement('div'))
|
const quill = new Quill(document.createElement('div'))
|
||||||
const quillBinding = new Y.QuillBinding(textType, quill)
|
result['quillBinding' + i] = new Y.QuillBinding(textType, quill)
|
||||||
result['quill' + i] = quill
|
result['quill' + i] = quill
|
||||||
result['quillBinding' + i] = quillBinding
|
|
||||||
y.quill = quill // put quill on the y object (so we can use it later)
|
y.quill = quill // put quill on the y object (so we can use it later)
|
||||||
y.get('xml').setDomFilter(function (nodeName, attrs) {
|
y.dom = dom
|
||||||
if (nodeName === 'HIDDEN') {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
attrs.delete('hidden')
|
|
||||||
return attrs
|
|
||||||
})
|
|
||||||
y.on('afterTransaction', function () {
|
y.on('afterTransaction', function () {
|
||||||
for (let missing of y._missingStructs.values()) {
|
for (let missing of y._missingStructs.values()) {
|
||||||
if (Array.from(missing.values()).length > 0) {
|
if (Array.from(missing.values()).length > 0) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
/* global Y */
|
/* global Y */
|
||||||
import { wait } from './helper'
|
import { wait } from './helper'
|
||||||
import { messageToString } from '../src/MessageHandler/messageToString'
|
import { messageToString } from '../src/MessageHandler/messageToString'
|
||||||
|
import AbstractConnector from '../src/Connector.js'
|
||||||
|
|
||||||
var rooms = {}
|
var rooms = {}
|
||||||
|
|
||||||
@ -64,107 +65,99 @@ function getTestRoom (roomname) {
|
|||||||
return rooms[roomname]
|
return rooms[roomname]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function extendTestConnector (Y) {
|
export default class TestConnector extends AbstractConnector {
|
||||||
class TestConnector extends Y.AbstractConnector {
|
constructor (y, options) {
|
||||||
constructor (y, options) {
|
if (options === undefined) {
|
||||||
if (options === undefined) {
|
throw new Error('Options must not be undefined!')
|
||||||
throw new Error('Options must not be undefined!')
|
|
||||||
}
|
|
||||||
if (options.room == null) {
|
|
||||||
throw new Error('You must define a room name!')
|
|
||||||
}
|
|
||||||
options.forwardAppliedOperations = options.role === 'master'
|
|
||||||
super(y, options)
|
|
||||||
this.options = options
|
|
||||||
this.room = options.room
|
|
||||||
this.chance = options.chance
|
|
||||||
this.testRoom = getTestRoom(this.room)
|
|
||||||
this.testRoom.join(this)
|
|
||||||
}
|
}
|
||||||
disconnect () {
|
if (options.room == null) {
|
||||||
this.testRoom.leave(this)
|
throw new Error('You must define a room name!')
|
||||||
return super.disconnect()
|
|
||||||
}
|
}
|
||||||
logBufferParsed () {
|
options.forwardAppliedOperations = options.role === 'master'
|
||||||
console.log(' === Logging buffer of user ' + this.y.userID + ' === ')
|
super(y, options)
|
||||||
for (let [user, conn] of this.connections) {
|
this.options = options
|
||||||
console.log(` ${user}:`)
|
this.room = options.room
|
||||||
for (let i = 0; i < conn.buffer.length; i++) {
|
this.chance = options.chance
|
||||||
console.log(messageToString(conn.buffer[i]))
|
this.testRoom = getTestRoom(this.room)
|
||||||
}
|
this.testRoom.join(this)
|
||||||
|
}
|
||||||
|
disconnect () {
|
||||||
|
this.testRoom.leave(this)
|
||||||
|
return super.disconnect()
|
||||||
|
}
|
||||||
|
logBufferParsed () {
|
||||||
|
console.log(' === Logging buffer of user ' + this.y.userID + ' === ')
|
||||||
|
for (let [user, conn] of this.connections) {
|
||||||
|
console.log(` ${user}:`)
|
||||||
|
for (let i = 0; i < conn.buffer.length; i++) {
|
||||||
|
console.log(messageToString(conn.buffer[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
reconnect () {
|
}
|
||||||
this.testRoom.join(this)
|
reconnect () {
|
||||||
super.reconnect()
|
this.testRoom.join(this)
|
||||||
return new Promise(resolve => {
|
super.reconnect()
|
||||||
this.whenSynced(resolve)
|
return new Promise(resolve => {
|
||||||
})
|
this.whenSynced(resolve)
|
||||||
}
|
})
|
||||||
send (uid, message) {
|
}
|
||||||
super.send(uid, message)
|
send (uid, message) {
|
||||||
this.testRoom.send(this.y.userID, uid, message)
|
super.send(uid, message)
|
||||||
}
|
this.testRoom.send(this.y.userID, uid, message)
|
||||||
broadcast (message) {
|
}
|
||||||
super.broadcast(message)
|
broadcast (message) {
|
||||||
this.testRoom.broadcast(this.y.userID, message)
|
super.broadcast(message)
|
||||||
}
|
this.testRoom.broadcast(this.y.userID, message)
|
||||||
async whenSynced (f) {
|
}
|
||||||
var synced = false
|
async whenSynced (f) {
|
||||||
var periodicFlushTillSync = () => {
|
var synced = false
|
||||||
if (synced) {
|
var periodicFlushTillSync = () => {
|
||||||
f()
|
if (synced) {
|
||||||
} else {
|
f()
|
||||||
this.testRoom.flushAll([this.y]).then(function () {
|
} else {
|
||||||
setTimeout(periodicFlushTillSync, 10)
|
this.testRoom.flushAll([this.y]).then(function () {
|
||||||
})
|
setTimeout(periodicFlushTillSync, 10)
|
||||||
}
|
})
|
||||||
}
|
|
||||||
periodicFlushTillSync()
|
|
||||||
return super.whenSynced(function () {
|
|
||||||
synced = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
receiveMessage (sender, m) {
|
|
||||||
if (this.y.userID !== sender && this.connections.has(sender)) {
|
|
||||||
var buffer = this.connections.get(sender).buffer
|
|
||||||
if (buffer == null) {
|
|
||||||
buffer = this.connections.get(sender).buffer = []
|
|
||||||
}
|
|
||||||
buffer.push(m)
|
|
||||||
if (this.chance.bool({likelihood: 30})) {
|
|
||||||
// flush 1/2 with 30% chance
|
|
||||||
var flushLength = Math.round(buffer.length / 2)
|
|
||||||
buffer.splice(0, flushLength).forEach(m => {
|
|
||||||
super.receiveMessage(sender, m)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async _flushAll (flushUsers) {
|
periodicFlushTillSync()
|
||||||
if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) {
|
return super.whenSynced(function () {
|
||||||
// this one needs to sync with every other user
|
synced = true
|
||||||
flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y)
|
})
|
||||||
|
}
|
||||||
|
receiveMessage (sender, m) {
|
||||||
|
if (this.y.userID !== sender && this.connections.has(sender)) {
|
||||||
|
var buffer = this.connections.get(sender).buffer
|
||||||
|
if (buffer == null) {
|
||||||
|
buffer = this.connections.get(sender).buffer = []
|
||||||
}
|
}
|
||||||
for (let i = 0; i < flushUsers.length; i++) {
|
buffer.push(m)
|
||||||
let userID = flushUsers[i].connector.y.userID
|
if (this.chance.bool({likelihood: 30})) {
|
||||||
if (userID !== this.y.userID && this.connections.has(userID)) {
|
// flush 1/2 with 30% chance
|
||||||
let buffer = this.connections.get(userID).buffer
|
var flushLength = Math.round(buffer.length / 2)
|
||||||
if (buffer != null) {
|
buffer.splice(0, flushLength).forEach(m => {
|
||||||
var messages = buffer.splice(0)
|
super.receiveMessage(sender, m)
|
||||||
for (let j = 0; j < messages.length; j++) {
|
})
|
||||||
super.receiveMessage(userID, messages[j])
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
async _flushAll (flushUsers) {
|
||||||
|
if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) {
|
||||||
|
// this one needs to sync with every other user
|
||||||
|
flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y)
|
||||||
|
}
|
||||||
|
for (let i = 0; i < flushUsers.length; i++) {
|
||||||
|
let userID = flushUsers[i].connector.y.userID
|
||||||
|
if (userID !== this.y.userID && this.connections.has(userID)) {
|
||||||
|
let buffer = this.connections.get(userID).buffer
|
||||||
|
if (buffer != null) {
|
||||||
|
var messages = buffer.splice(0)
|
||||||
|
for (let j = 0; j < messages.length; j++) {
|
||||||
|
super.receiveMessage(userID, messages[j])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'done'
|
|
||||||
}
|
}
|
||||||
|
return 'done'
|
||||||
}
|
}
|
||||||
// TODO: this should be moved to a separate module (dont work on Y)
|
|
||||||
Y.test = TestConnector
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof Y !== 'undefined') {
|
|
||||||
extendTestConnector(Y)
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user