separate dom binding

This commit is contained in:
Kevin Jahns 2018-03-23 01:55:47 +01:00
parent acf443aacb
commit 026675b438
28 changed files with 1802 additions and 1212 deletions

View File

@ -1,7 +1,7 @@
/* global Y */
window.onload = function () {
window.yXmlType.bindToDom(document.body)
window.domBinding = new Y.DomBinding(window.yXmlType, document.body)
}
let y = new Y('htmleditor', {

1240
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@
"test": "npm run lint",
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
"lint": "standard",
"docs": "esdocs",
"docs": "esdoc",
"serve-docs": "npm run docs && serve ./docs/",
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
@ -56,6 +56,8 @@
"chance": "^1.0.9",
"concurrently": "^3.4.0",
"cutest": "^0.1.9",
"esdoc": "^1.0.4",
"esdoc-standard-plugin": "^1.0.0",
"quill": "^1.3.5",
"quill-cursors": "^1.0.2",
"rollup-plugin-babel": "^2.7.1",
@ -67,9 +69,7 @@
"rollup-regenerator-runtime": "^6.23.1",
"rollup-watch": "^3.2.2",
"standard": "^10.0.2",
"tag-dist-files": "^0.1.6",
"esdoc": "^1.0.4",
"esdoc-standard-plugin": "^1.0.0"
"tag-dist-files": "^0.1.6"
},
"dependencies": {
"debug": "^2.6.8"

View File

@ -19,7 +19,8 @@ export default {
browser: true
}),
commonjs(),
babel(),
// babel(),
/*
uglify({
mangle: {
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: `
/**

View File

@ -1,109 +1,15 @@
/* global MutationObserver */
import Binding from './Binding.js'
import diff from '../Util/simpleDiff.js'
import YXmlFragment from '../../Type/YXml/YXmlFragment.js'
import YXmlHook from '../../Type/YXml/YXmlHook.js'
function defaultFilter (nodeName, attrs) {
return attrs
}
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)
}
})
})
}
import Binding from '../Binding.js'
import diff from '../../Util/simpleDiff.js'
import YXmlFragment from '../../Types/YXml/YXmlFragment.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import { removeDomChildrenUntilElementFound, createAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js'
import typeObserver from './typeObserver.js'
import domObserver from './domObserver.js'
import { removeAssociation } from './util.js'
/**
* A binding that binds the children of a YXmlFragment to a DOM element.
@ -122,7 +28,7 @@ export default class DomBinding extends Binding {
* truth.
* @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
super(type, target)
this.domToType = new Map()
@ -134,20 +40,73 @@ export default class DomBinding extends Binding {
target.insertBefore(child.toDom(this.domToType, this.typeToDom), null)
}
this._typeObserver = typeObserver.bind(this)
this._domObserver = domObserver.bind(this)
type.observe(this._typeObserver)
this._domObserver = domObserver.bind(this)
this._mutationObserver = new MutationObserver(this._domObserver())
this._domObserver = (mutations) => {
domObserver.call(this, mutations, opts._document)
}
type.observeDeep(this._typeObserver)
this._mutationObserver = new MutationObserver(this._domObserver)
this._mutationObserver.observe(target, {
childList: true,
attributes: true,
characterData: true,
subtree: true
})
this._beforeTransactionHandler = () => {
this._domObserverListener(this._domObserver.takeRecords())
const y = type._y
// 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.type.unobserve(this._typeObserver)
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()
}
}

View File

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

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

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

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

View File

@ -7,30 +7,30 @@ let relativeSelection = null
export let beforeTransactionSelectionFixer
if (typeof getSelection !== 'undefined') {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) {
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
if (!remote) {
return
}
relativeSelection = { from: null, to: null, fromY: null, toY: null }
browserSelection = getSelection()
const anchorNode = browserSelection.anchorNode
if (anchorNode !== null && anchorNode._yxml != null) {
const yxml = anchorNode._yxml
relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset)
relativeSelection.fromY = yxml._y
const anchorNodeType = domBinding.domToType.get(anchorNode)
if (anchorNode !== null && anchorNodeType !== undefined) {
relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
relativeSelection.fromY = anchorNodeType._y
}
const focusNode = browserSelection.focusNode
if (focusNode !== null && focusNode._yxml != null) {
const yxml = focusNode._yxml
relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset)
relativeSelection.toY = yxml._y
const focusNodeType = domBinding.domToType.get(focusNode)
if (focusNode !== null && focusNodeType !== undefined) {
relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
relativeSelection.toY = focusNodeType._y
}
}
} else {
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
}
export function afterTransactionSelectionFixer (y, transaction, remote) {
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
if (relativeSelection === null || !remote) {
return
}
@ -46,7 +46,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
if (from !== null) {
let sel = fromRelativePosition(fromY, from)
if (sel !== null) {
let node = sel.type.getDom()
let node = domBinding.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== anchorNode || offset !== anchorOffset) {
anchorNode = node
@ -58,7 +58,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
if (to !== null) {
let sel = fromRelativePosition(toY, to)
if (sel !== null) {
let node = sel.type.getDom()
let node = domBinding.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== focusNode || offset !== focusOffset) {
focusNode = node

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

View File

@ -1,4 +1,6 @@
import domToType from './domToType.js'
export function iterateUntilUndeleted (item) {
while (item !== null && item._deleted) {
item = item._right
@ -12,15 +14,51 @@ export function removeAssociation (domBinding, dom, type) {
}
export function createAssociation (domBinding, dom, type) {
domBinding.domToType.set(dom, type)
domBinding.typeToDom.set(type, dom)
if (domBinding !== undefined) {
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) {
return insertedNodes[0]
} else {
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)
}
}

View File

@ -1,11 +1,14 @@
import Binding from './Binding.js'
import Binding from '../Binding.js'
function typeObserver (event) {
const quill = this.target
// Force flush Quill changes.
quill.update('yjs')
this._mutualExclude(function () {
// Apply computed delta.
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
* const quill = new Quill(document.createElement('div'))
* const type = y.define('quill', Y.Text)
* const binding = new Y.QuillBinding(quill, type)
* const quill = new Quill(document.createElement('div'))
* const type = y.define('quill', Y.Text)
* 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 {
/**
@ -29,18 +34,18 @@ export default class QuillBinding extends Binding {
* @param {Quill} 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)
// set initial value
// Set initial value.
quill.setContents(textType.toDelta(), 'yjs')
// Observers are handled by this class
// Observers are handled by this class.
this._typeObserver = typeObserver.bind(this)
this._quillObserver = quillObserver.bind(this)
textType.observe(this._typeObserver)
quill.on('text-change', this._quillObserver)
}
destroy () {
// Remove everything that is handled by this class
// Remove everything that is handled by this class.
this.type.unobserve(this._typeObserver)
this.target.off('text-change', this._quillObserver)
super.destroy()

View File

@ -1,7 +1,7 @@
import Binding from './Binding.js'
import simpleDiff from '../Util/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../Util/relativePosition.js'
import Binding from '../Binding.js'
import simpleDiff from '../../Util/simpleDiff.js'
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
function typeObserver () {
this._mutualExclude(() => {

View File

@ -302,6 +302,7 @@ export default class YArray extends Type {
}
}
})
return content
}
/**

View File

@ -1,7 +1,6 @@
import { defaultDomFilter } from './utils.js'
import YMap from '../YMap/YMap.js'
import { YXmlFragment } from './YXml.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
/**
* 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 childElements that must inherit from YXmlElement
*
* @param {String} arg1 Node name
* @param {Function} arg2 Dom filter
* @param {String} nodeName Node name
*/
export default class YXmlElement extends YXmlFragment {
constructor (arg1, arg2, _document) {
constructor (nodeName = 'UNDEFINED') {
super()
this.nodeName = null
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
}
this.nodeName = nodeName.toUpperCase()
}
/**
@ -41,48 +27,6 @@ export default class YXmlElement extends YXmlFragment {
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
* 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) {
throw new Error('nodeName must be defined!')
}
if (this._domFilter === defaultDomFilter && this._parent._domFilter !== undefined) {
this._domFilter = this._parent._domFilter
}
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}
*/
getDom (_document) {
_document = _document || document
let dom = this._dom
if (dom == null) {
dom = _document.createElement(this.nodeName)
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)
toDom (_document = document, binding) {
const dom = _document.createElement(this.nodeName)
let attrs = this.getAttributes()
for (let key in attrs) {
dom.setAttribute(key, attrs[key])
}
this.forEach(yxml => {
dom.appendChild(yxml.toDom(_document, binding))
})
createAssociation(binding, dom, this)
return dom
}
}

View File

@ -4,8 +4,9 @@ import YEvent from '../../Util/YEvent.js'
* An Event that describes changes on a YXml Element or Yxml Fragment
*/
export default class YXmlEvent extends YEvent {
constructor (target, subs, remote) {
constructor (target, subs, remote, transaction) {
super(target)
this._transaction = transaction
this.childListChanged = false
this.attributesChanged = new Set()
this.remote = remote

View File

@ -1,7 +1,7 @@
/* global MutationObserver */
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
import YXmlTreeWalker from './YXmlTreeWalker.js'
import YArray from '../YArray/YArray.js'
import YXmlEvent from './YXmlEvent.js'
@ -9,33 +9,6 @@ import { YXmlText, YXmlHook } from './YXml.js'
import { logID } from '../../MessageHandler/messageToString.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.
* {@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
*/
/**
* 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}.
@ -113,32 +28,6 @@ class YXmlTreeWalker {
* Therefore it also must not be added as a childElement.
*/
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.
*
@ -189,22 +78,6 @@ export default class YXmlFragment extends YArray {
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.
*
@ -214,44 +87,12 @@ export default class YXmlFragment extends YArray {
* @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
* Creates YArray Event and calls observers.
*/
_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.
*/
_delete (y, createDelete) {
this._unbindFromDom()
super._delete(y, createDelete)
}
/**
* @private
* Unbind this YXmlFragment from the Dom.
* @return {DocumentFragment} The dom representation of this
*/
_unbindFromDom () {
if (this._domObserver != null) {
this._domObserver.disconnect()
this._domObserver = 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)
toDom (_document = document, binding) {
const fragment = _document.createDocumentFragment()
createAssociation(binding, fragment, this)
this.forEach(xmlType => {
fragment.insertBefore(xmlType.toDom(_document, binding), 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
* Transform this YXml Type to a readable format.

View File

@ -7,16 +7,12 @@ import { getHook, addHook } from './hooks.js'
* @param {String} hookName nodeName of the Dom Node.
*/
export default class YXmlHook extends YMap {
constructor (hookName, dom) {
constructor (hookName) {
super()
this._dom = null
this.hookName = null
if (hookName !== undefined) {
this.hookName = hookName
this._dom = dom
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) {
_document = _document || document
if (this._dom === null) {
const dom = getHook(this.hookName).createDom(this)
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?
toDom (_document = document) {
const dom = getHook(this.hookName).createDom(this)
dom._yjsHook = this.hookName
return dom
}
/**
@ -99,11 +82,5 @@ export default class YXmlHook extends YMap {
}
super._integrate(y)
}
setDomFilter () {
// TODO: implement new modfilter method!
}
enableSmartScrolling () {
// TODO: implement new smartscrolling method!
}
}
YXmlHook.addHook = addHook

View File

@ -1,4 +1,5 @@
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
@ -7,93 +8,16 @@ import YText from '../YText/YText.js'
* @param {String} arg1 Initial value.
*/
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
* Set Dom element / Text Node that represents the same content as this
* YXmlElement.
* Creates a TextNode with the same textual content.
*
* @param {Element} dom The Dom Element / Text Node that is set to be
* equivalent to this Type.
* @return TextNode
*/
_setDom (dom) {
if (this._dom != null) {
this._unbindFromDom()
}
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
toDom (_document = document, binding) {
const dom = _document.createTextNode(this.toString())
createAssociation(binding, dom, this)
return dom
}
/**
@ -105,22 +29,6 @@ export default class YXmlText extends YText {
* Type was deleted.
*/
_delete (y, createDelete) {
this._unbindFromDom()
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
}
}
}

View File

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

View File

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

View File

@ -14,8 +14,9 @@ import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Types/YXml/YXml
import BinaryDecoder from './Util/Binary/Decoder.js'
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
import { registerStruct } from './Util/structReferences.js'
import TextareaBinding from './Bindings/TextareaBinding.js'
import QuillBinding from './Bindings/QuillBinding.js'
import TextareaBinding from './Bindings/TextareaBinding/TextareaBinding.js'
import QuillBinding from './Bindings/QuillBinding/QuillBinding.js'
import DomBinding from './Bindings/DomBinding/DomBinding.js'
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
import debug from 'debug'
@ -33,6 +34,7 @@ Y.XmlHook = YXmlHook
Y.TextareaBinding = TextareaBinding
Y.QuillBinding = QuillBinding
Y.DomBinding = DomBinding
Y.utils = {
BinaryDecoder,

View File

@ -6,6 +6,8 @@ import RootID from './Util/ID/RootID.js'
import NamedEventHandler from './Util/NamedEventHandler.js'
import Transaction from './Transaction.js'
export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js'
/**
* A positive natural number including zero: 0, 1, 2, ..
*
@ -44,7 +46,11 @@ export default class Y extends NamedEventHandler {
}
this._contentReady = false
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
this.share = {}
this.ds = new DeleteStore(this)
@ -77,6 +83,8 @@ export default class Y extends NamedEventHandler {
} else {
initConnection()
}
// for compatibility with isParentOf
this._parent = null
}
_setContentReady () {
if (!this._contentReady) {

View File

@ -3,14 +3,13 @@ import { test } from 'cutest'
test('set property', async function xml0 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
xml0.setAttribute('height', 10)
t.assert(xml0.getAttribute('height') === 10, 'Simple set+get works')
xml0.setAttribute('height', '10')
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
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)
})
/* TODO: Test YXml events!
test('events', async function xml1 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 2 })
var event
@ -29,48 +28,28 @@ test('events', async function xml1 (t) {
remoteEvent = e
})
xml0.setAttribute('key', 'value')
expectedEvent = {
type: 'attributeChanged',
value: 'value',
name: 'key'
}
t.compare(event, expectedEvent, 'attribute changed event')
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key')
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
xml0.removeAttribute('key')
expectedEvent = {
type: 'attributeRemoved',
name: 'key'
}
t.compare(event, expectedEvent, 'attribute deleted event')
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute')
await flushAll(t, users)
t.compare(remoteEvent, expectedEvent, 'attribute deleted event (remote)')
// test childInserted event
expectedEvent = {
type: 'childInserted',
index: 0
}
t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)')
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)
t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)')
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)')
// test childRemoved
xml0.delete(0)
expectedEvent = {
type: 'childRemoved',
index: 0
}
t.compare(event, expectedEvent, 'child deleted event')
t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element')
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)
})
*/
test('attribute modifications (y -> dom)', async function xml2 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.setAttribute('height', '100px')
await wait()
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
dom0.setAttribute('height', '100px')
await wait()
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
dom0.insertBefore(document.createTextNode('some text'), null)
dom0.insertBefore(document.createElement('p'), null)
await wait()
@ -110,8 +87,7 @@ test('element insert (dom -> y)', async function xml4 (t) {
})
test('element insert (y -> dom)', async function xml5 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.insert(0, [new Y.XmlText('some text')])
xml0.insert(1, [new Y.XmlElement('p')])
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
dom0.insertBefore(document.createElement('p'), null)
await wait()
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.insert(0, [new Y.XmlElement('p')])
t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom')
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
await wait()
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')])
await wait()
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
await wait()
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) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
var { users, xml0, dom0 } = await initArrays(t, { users: 3 })
xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')])
await wait()
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) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
users[1].disconnect()
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')])
@ -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) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
dom0.append(document.createElement('div'))
dom0.append(document.createElement('h1'))
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) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
var { users, xml0, xml1, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
let domFilter = (nodeName, attrs) => {
if (nodeName === 'H1') {
return null
@ -237,8 +201,8 @@ test('filter node', async function xml14 (t) {
return attrs
}
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
domBinding0.setFilter(domFilter)
domBinding1.setFilter(domFilter)
dom0.append(document.createElement('div'))
dom0.append(document.createElement('h1'))
await flushAll(t, users)
@ -248,15 +212,13 @@ test('filter node', async function xml14 (t) {
})
test('filter attribute', async function xml15 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
var { users, xml0, xml1, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 })
let domFilter = (nodeName, attrs) => {
attrs.delete('hidden')
return attrs
}
xml0.setDomFilter(domFilter)
xml1.setDomFilter(domFilter)
domBinding0.setFilter(domFilter)
domBinding1.setFilter(domFilter)
dom0.setAttribute('hidden', 'true')
dom0.setAttribute('style', 'height: 30px')
dom0.setAttribute('data-me', '77')
@ -269,9 +231,7 @@ test('filter attribute', async function xml15 (t) {
})
test('deep element insert', async function xml16 (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
let dom0 = xml0.getDom()
let dom1 = xml1.getDom()
var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 })
let deepElement = document.createElement('p')
let boldElement = document.createElement('b')
let attrElement = document.createElement('img')
@ -291,8 +251,8 @@ test('treeWalker', async function xml17 (t) {
var { users, xml0 } = await initArrays(t, { users: 3 })
let paragraph1 = new Y.XmlElement('p')
let paragraph2 = new Y.XmlElement('p')
let text1 = new Y.Text('init')
let text2 = new Y.Text('text')
let text1 = new Y.XmlText('init')
let text2 = new Y.XmlText('text')
paragraph1.insert(0, [text1, text2])
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
let allParagraphs = xml0.querySelectorAll('p')
@ -309,8 +269,8 @@ test('treeWalker', async function xml17 (t) {
* Incoming changes that contain malicious attributes should be deleted.
*/
test('Filtering remote changes', async function xmlFilteringRemote (t) {
var { users, xml0, xml1 } = await initArrays(t, { users: 3 })
xml0.setDomFilter(function (nodeName, attributes) {
var { users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 })
domBinding0.setFilter(function (nodeName, attributes) {
attributes.delete('malicious')
if (nodeName === 'HIDEME') {
return null
@ -320,10 +280,6 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
return attributes
}
})
// make sure that dom filters are active
// TODO: do not rely on .getDom for domFilters
xml0.getDom()
xml1.getDom()
let paragraph = new Y.XmlElement('p')
let hideMe = new Y.XmlElement('hideMe')
let span = new Y.XmlElement('span')
@ -337,8 +293,8 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
paragraph.insert(0, [tag2])
await flushAll(t, users)
// check dom
paragraph.getDom().setAttribute('malicious', 'true')
span.getDom().setAttribute('malicious', 'true')
domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true')
domBinding0.typeToDom.get(span).setAttribute('malicious', 'true')
// check incoming attributes
xml1.get(0).get(0).setAttribute('malicious', 'true')
xml1.insert(0, [new Y.XmlElement('hideMe')])
@ -350,35 +306,35 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
// TODO: move elements
var xmlTransactions = [
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) {
user.get('xml', Y.XmlElement).getDom().setAttribute('hidden', chance.word())
user.dom.setAttribute('hidden', chance.word())
},
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
dom.insertBefore(document.createTextNode(chance.word()), succ)
},
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
dom.insertBefore(document.createElement('hidden'), succ)
},
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
dom.insertBefore(document.createElement(chance.word()), succ)
},
function deleteChild (t, user, chance) {
let dom = user.get('xml', Y.XmlElement).getDom()
let dom = user.dom
if (dom.childNodes.length > 0) {
var d = chance.pickone(dom.childNodes)
d.remove()
}
},
function insertTextSecondLayer (t, user, chance) {
let dom = user.get('xml', Y.XmlElement).getDom()
let dom = user.dom
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
@ -386,7 +342,7 @@ var xmlTransactions = [
}
},
function insertDomSecondLayer (t, user, chance) {
let dom = user.get('xml', Y.XmlElement).getDom()
let dom = user.dom
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null
@ -394,7 +350,7 @@ var xmlTransactions = [
}
},
function deleteChildSecondLayer (t, user, chance) {
let dom = user.get('xml', Y.XmlElement).getDom()
let dom = user.dom
if (dom.children.length > 0) {
let dom2 = chance.pickone(dom.children)
if (dom2.childNodes.length > 0) {

View File

@ -1,6 +1,7 @@
import _Y from '../src/Y.js'
import yTest from './test-connector.js'
import _Y from '../src/Y.dist.js'
import { DomBinding } from '../src/Y.js'
import TestConnector from './test-connector.js'
import Chance from 'chance'
import ItemJSON from '../src/Struct/ItemJSON.js'
@ -10,11 +11,11 @@ import Quill from 'quill'
export const Y = _Y
Y.extend(yTest)
export const database = { name: 'memory' }
export const connector = { name: 'test', url: 'http://localhost:1234' }
Y.test = TestConnector
function getStateSet (y) {
let ss = {}
for (let [user, clock] of y.ss.state) {
@ -40,6 +41,7 @@ function getDeleteSet (y) {
return ds
}
// TODO: remove?
export function attrsObject (dom) {
let keys = []
let yxml = dom._yxml
@ -55,6 +57,7 @@ export function attrsObject (dom) {
return obj
}
// TODO: remove?
export function domToJson (dom) {
if (dom.nodeType === document.TEXT_NODE) {
return dom.textContent
@ -140,6 +143,14 @@ export async function compareUsers (t, users) {
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) {
var result = {
users: []
@ -154,27 +165,25 @@ export async function initArrays (t, opts) {
connOpts = Object.assign({ role: 'slave' }, conn)
}
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
})
result.users.push(y)
result['array' + i] = y.define('array', Y.Array)
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)
result['text' + i] = textType
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['quillBinding' + i] = quillBinding
y.quill = quill // put quill on the y object (so we can use it later)
y.get('xml').setDomFilter(function (nodeName, attrs) {
if (nodeName === 'HIDDEN') {
return null
}
attrs.delete('hidden')
return attrs
})
y.dom = dom
y.on('afterTransaction', function () {
for (let missing of y._missingStructs.values()) {
if (Array.from(missing.values()).length > 0) {

View File

@ -1,6 +1,7 @@
/* global Y */
import { wait } from './helper'
import { messageToString } from '../src/MessageHandler/messageToString'
import AbstractConnector from '../src/Connector.js'
var rooms = {}
@ -64,107 +65,99 @@ function getTestRoom (roomname) {
return rooms[roomname]
}
export default function extendTestConnector (Y) {
class TestConnector extends Y.AbstractConnector {
constructor (y, options) {
if (options === 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)
export default class TestConnector extends AbstractConnector {
constructor (y, options) {
if (options === undefined) {
throw new Error('Options must not be undefined!')
}
disconnect () {
this.testRoom.leave(this)
return super.disconnect()
if (options.room == null) {
throw new Error('You must define a room name!')
}
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]))
}
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 () {
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)
super.reconnect()
return new Promise(resolve => {
this.whenSynced(resolve)
})
}
send (uid, message) {
super.send(uid, message)
this.testRoom.send(this.y.userID, uid, message)
}
broadcast (message) {
super.broadcast(message)
this.testRoom.broadcast(this.y.userID, message)
}
async whenSynced (f) {
var synced = false
var periodicFlushTillSync = () => {
if (synced) {
f()
} else {
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)
})
}
}
reconnect () {
this.testRoom.join(this)
super.reconnect()
return new Promise(resolve => {
this.whenSynced(resolve)
})
}
send (uid, message) {
super.send(uid, message)
this.testRoom.send(this.y.userID, uid, message)
}
broadcast (message) {
super.broadcast(message)
this.testRoom.broadcast(this.y.userID, message)
}
async whenSynced (f) {
var synced = false
var periodicFlushTillSync = () => {
if (synced) {
f()
} else {
this.testRoom.flushAll([this.y]).then(function () {
setTimeout(periodicFlushTillSync, 10)
})
}
}
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)
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 = []
}
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])
}
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) {
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)
}