separate dom binding
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/Bindings/DomBinding/selection.js
Normal file
78
src/Bindings/DomBinding/selection.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/* globals getSelection */
|
||||
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
|
||||
let browserSelection = null
|
||||
let relativeSelection = null
|
||||
|
||||
export let beforeTransactionSelectionFixer
|
||||
if (typeof getSelection !== 'undefined') {
|
||||
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
|
||||
const anchorNodeType = domBinding.domToType.get(anchorNode)
|
||||
if (anchorNode !== null && anchorNodeType !== undefined) {
|
||||
relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
|
||||
relativeSelection.fromY = anchorNodeType._y
|
||||
}
|
||||
const focusNode = browserSelection.focusNode
|
||||
const focusNodeType = domBinding.domToType.get(focusNode)
|
||||
if (focusNode !== null && focusNodeType !== undefined) {
|
||||
relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
|
||||
relativeSelection.toY = focusNodeType._y
|
||||
}
|
||||
}
|
||||
} else {
|
||||
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
||||
}
|
||||
|
||||
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||
if (relativeSelection === null || !remote) {
|
||||
return
|
||||
}
|
||||
const to = relativeSelection.to
|
||||
const from = relativeSelection.from
|
||||
const fromY = relativeSelection.fromY
|
||||
const toY = relativeSelection.toY
|
||||
let shouldUpdate = false
|
||||
let anchorNode = browserSelection.anchorNode
|
||||
let anchorOffset = browserSelection.anchorOffset
|
||||
let focusNode = browserSelection.focusNode
|
||||
let focusOffset = browserSelection.focusOffset
|
||||
if (from !== null) {
|
||||
let sel = fromRelativePosition(fromY, from)
|
||||
if (sel !== null) {
|
||||
let node = domBinding.typeToDom.get(sel.type)
|
||||
let offset = sel.offset
|
||||
if (node !== anchorNode || offset !== anchorOffset) {
|
||||
anchorNode = node
|
||||
anchorOffset = offset
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (to !== null) {
|
||||
let sel = fromRelativePosition(toY, to)
|
||||
if (sel !== null) {
|
||||
let node = domBinding.typeToDom.get(sel.type)
|
||||
let offset = sel.offset
|
||||
if (node !== focusNode || offset !== focusOffset) {
|
||||
focusNode = node
|
||||
focusOffset = offset
|
||||
shouldUpdate = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
browserSelection.setBaseAndExtent(
|
||||
anchorNode,
|
||||
anchorOffset,
|
||||
focusNode,
|
||||
focusOffset
|
||||
)
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user