implemented xml type for new event system
This commit is contained in:
@@ -2,6 +2,16 @@ import Type from '../Struct/Type.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
|
||||
export default class YArray extends Type {
|
||||
_callObserver () {
|
||||
this._eventHandler.callEventListeners({})
|
||||
}
|
||||
get (i) {
|
||||
// TODO: This can be improved!
|
||||
return this.toArray()[i]
|
||||
}
|
||||
toArray () {
|
||||
return this.map(c => c)
|
||||
}
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
@@ -11,6 +21,7 @@ export default class YArray extends Type {
|
||||
return c.toString()
|
||||
}
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
map (f) {
|
||||
@@ -25,11 +36,15 @@ export default class YArray extends Type {
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
const content = n._content
|
||||
const contentLen = content.length
|
||||
for (let i = 0; i < contentLen; i++) {
|
||||
pos++
|
||||
f(content[i], pos, this)
|
||||
if (n instanceof Type) {
|
||||
f(n, pos++, this)
|
||||
} else {
|
||||
const content = n._content
|
||||
const contentLen = content.length
|
||||
for (let i = 0; i < contentLen; i++) {
|
||||
pos++
|
||||
f(content[i], pos, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
n = n._right
|
||||
@@ -42,14 +57,14 @@ export default class YArray extends Type {
|
||||
if (!n._deleted) {
|
||||
length += n._length
|
||||
}
|
||||
n = n._next
|
||||
n = n._right
|
||||
}
|
||||
return length
|
||||
}
|
||||
[Symbol.iterator] () {
|
||||
return {
|
||||
next: function () {
|
||||
while (this._item !== null && (this._item._deleted || this._item._content.length <= this._itemElement)) {
|
||||
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
||||
// item is deleted or itemElement does not exist (is deleted)
|
||||
this._item = this._item._right
|
||||
this._itemElement = 0
|
||||
@@ -58,11 +73,16 @@ export default class YArray extends Type {
|
||||
return {
|
||||
done: true
|
||||
}
|
||||
}
|
||||
let content
|
||||
if (this._item instanceof Type) {
|
||||
content = this._item
|
||||
} else {
|
||||
return {
|
||||
value: [this._count, this._item._content[this._itemElement++]],
|
||||
done: false
|
||||
}
|
||||
content = this._item._content[this._itemElement++]
|
||||
}
|
||||
return {
|
||||
value: [this._count, content],
|
||||
done: false
|
||||
}
|
||||
},
|
||||
_item: this._start,
|
||||
@@ -71,68 +91,99 @@ export default class YArray extends Type {
|
||||
}
|
||||
}
|
||||
delete (pos, length = 1) {
|
||||
let item = this._start
|
||||
let count = 0
|
||||
while (item !== null && length > 0) {
|
||||
if (count < pos && pos < count + item._length) {
|
||||
const diffDel = pos - count
|
||||
item = item
|
||||
._splitAt(this._y, diffDel)
|
||||
._splitAt(this._y, length)
|
||||
length -= item._length
|
||||
item._delete(this._y)
|
||||
this._y.transact(() => {
|
||||
let item = this._start
|
||||
let count = 0
|
||||
while (item !== null && length > 0) {
|
||||
if (count <= pos && pos < count + item._length) {
|
||||
const diffDel = pos - count
|
||||
item = item._splitAt(this._y, diffDel)
|
||||
item._splitAt(this._y, length)
|
||||
length -= item._length
|
||||
item._delete(this._y)
|
||||
}
|
||||
if (!item._deleted) {
|
||||
count += item._length
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
if (!item._deleted) {
|
||||
count += item._length
|
||||
if (length > 0) {
|
||||
throw new Error('Delete exceeds the range of the YArray')
|
||||
}
|
||||
})
|
||||
}
|
||||
insertAfter (left, content) {
|
||||
const apply = () => {
|
||||
let right
|
||||
if (left === null) {
|
||||
right = this._start
|
||||
} else {
|
||||
right = left._right
|
||||
}
|
||||
let prevJsonIns = null
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let c = content[i]
|
||||
if (c instanceof Type) {
|
||||
if (prevJsonIns !== null) {
|
||||
if (this._y !== null) {
|
||||
prevJsonIns._integrate(this._y)
|
||||
}
|
||||
left = prevJsonIns
|
||||
prevJsonIns = null
|
||||
}
|
||||
c._origin = left
|
||||
c._left = left
|
||||
c._right = right
|
||||
c._right_origin = right
|
||||
c._parent = this
|
||||
if (this._y !== null) {
|
||||
c._integrate(this._y)
|
||||
} else if (left === null) {
|
||||
this._start = c
|
||||
}
|
||||
left = c
|
||||
} else {
|
||||
if (prevJsonIns === null) {
|
||||
prevJsonIns = new ItemJSON()
|
||||
prevJsonIns._origin = left
|
||||
prevJsonIns._left = left
|
||||
prevJsonIns._right = right
|
||||
prevJsonIns._right_origin = right
|
||||
prevJsonIns._parent = this
|
||||
prevJsonIns._content = []
|
||||
}
|
||||
prevJsonIns._content.push(c)
|
||||
}
|
||||
}
|
||||
if (prevJsonIns !== null && this._y !== null) {
|
||||
prevJsonIns._integrate(this._y)
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
if (length > 0) {
|
||||
throw new Error('Delete exceeds the range of the YArray')
|
||||
if (this._y !== null) {
|
||||
this._y.transact(apply)
|
||||
} else {
|
||||
apply()
|
||||
}
|
||||
return content
|
||||
}
|
||||
insert (pos, content) {
|
||||
let left = this._start
|
||||
let right = null
|
||||
let left = null
|
||||
let right = this._start
|
||||
let count = 0
|
||||
while (left !== null) {
|
||||
if (count <= pos && pos < count + left._content.length) {
|
||||
right = left._splitAt(this.y, pos - count)
|
||||
while (right !== null) {
|
||||
if (count <= pos && pos < count + right._length) {
|
||||
right = right._splitAt(this._y, pos - count)
|
||||
left = right._left
|
||||
break
|
||||
}
|
||||
count += left._length
|
||||
left = left.right
|
||||
count += right._length
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
if (pos > count) {
|
||||
throw new Error('Position exceeds array range!')
|
||||
}
|
||||
let prevJsonIns = null
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let c = content[i]
|
||||
if (c instanceof Type) {
|
||||
if (prevJsonIns === null) {
|
||||
prevJsonIns._integrate(this._y)
|
||||
prevJsonIns = null
|
||||
}
|
||||
c._left = left
|
||||
c._origin = left
|
||||
c._right = right
|
||||
c._parent = this
|
||||
} else {
|
||||
if (prevJsonIns === null) {
|
||||
prevJsonIns = new ItemJSON()
|
||||
prevJsonIns._origin = left
|
||||
prevJsonIns._left = left
|
||||
prevJsonIns._right = right
|
||||
prevJsonIns._parent = this
|
||||
prevJsonIns._content = []
|
||||
}
|
||||
prevJsonIns._content.push(c)
|
||||
}
|
||||
}
|
||||
if (prevJsonIns !== null) {
|
||||
prevJsonIns._integrate(this._y)
|
||||
}
|
||||
this.insertAfter(left, content)
|
||||
}
|
||||
_logString () {
|
||||
let s = super._logString()
|
||||
|
||||
@@ -3,6 +3,11 @@ import Item from '../Struct/Item.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
|
||||
export default class YMap extends Type {
|
||||
_callObserver (parentSub) {
|
||||
this._eventHandler.callEventListeners({
|
||||
name: parentSub
|
||||
})
|
||||
}
|
||||
toJSON () {
|
||||
const map = {}
|
||||
for (let [key, item] of this._map) {
|
||||
@@ -22,22 +27,40 @@ export default class YMap extends Type {
|
||||
}
|
||||
return map
|
||||
}
|
||||
delete (key) {
|
||||
this._y.transact(() => {
|
||||
let c = this._map.get(key)
|
||||
if (c !== undefined) {
|
||||
c._delete(this._y)
|
||||
}
|
||||
})
|
||||
}
|
||||
set (key, value) {
|
||||
let old = this._map.get(key)
|
||||
let v
|
||||
if (value instanceof Item) {
|
||||
v = value
|
||||
} else {
|
||||
let v = new ItemJSON()
|
||||
v._content = JSON.stringify(value)
|
||||
}
|
||||
v._right = old
|
||||
v._parent = this
|
||||
v._parentSub = key
|
||||
v._integrate()
|
||||
this._y.transact(() => {
|
||||
const old = this._map.get(key) || null
|
||||
if (old !== null) {
|
||||
old._delete(this._y)
|
||||
}
|
||||
let v
|
||||
if (value instanceof Item) {
|
||||
v = value
|
||||
} else {
|
||||
v = new ItemJSON()
|
||||
v._content = [value]
|
||||
}
|
||||
v._right = old
|
||||
v._right_origin = old
|
||||
v._parent = this
|
||||
v._parentSub = key
|
||||
v._integrate(this._y)
|
||||
})
|
||||
return value
|
||||
}
|
||||
get (key) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
return undefined
|
||||
}
|
||||
if (v instanceof Type) {
|
||||
return v
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,53 @@
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
import YArray from './YArray.js'
|
||||
|
||||
export default class YText extends YArray {
|
||||
constructor (string) {
|
||||
super()
|
||||
if (typeof string === 'string') {
|
||||
const start = new ItemString()
|
||||
start._parent = this
|
||||
start._content = string
|
||||
this._start = start
|
||||
}
|
||||
}
|
||||
toString () {
|
||||
const strBuilder = []
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
strBuilder.push(n._content)
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
return strBuilder.join('')
|
||||
}
|
||||
insert (pos, text) {
|
||||
this._y.transact(() => {
|
||||
let left = null
|
||||
let right = this._start
|
||||
let count = 0
|
||||
while (right !== null) {
|
||||
if (count <= pos && pos < count + right._length) {
|
||||
right = right._splitAt(this._y, pos - count)
|
||||
left = right._left
|
||||
break
|
||||
}
|
||||
count += right._length
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
if (pos > count) {
|
||||
throw new Error('Position exceeds array range!')
|
||||
}
|
||||
let item = new ItemString()
|
||||
item._origin = left
|
||||
item._left = left
|
||||
item._right = right
|
||||
item._right_origin = right
|
||||
item._parent = this
|
||||
item._content = text
|
||||
item._integrate(this._y)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import YArray from './YArray.js'
|
||||
|
||||
export default class YXml extends YArray {
|
||||
setDomFilter () {
|
||||
// TODO
|
||||
}
|
||||
toString () {
|
||||
return '<nodeName></nodeName>'
|
||||
}
|
||||
}
|
||||
117
src/Type/y-xml/YXmlElement.js
Normal file
117
src/Type/y-xml/YXmlElement.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/* global MutationObserver */
|
||||
|
||||
// import diff from 'fast-diff'
|
||||
import { defaultDomFilter } from './utils.js'
|
||||
|
||||
import YMap from '../YMap.js'
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
|
||||
export default class YXmlElement extends YXmlFragment {
|
||||
constructor (arg1, arg2) {
|
||||
super()
|
||||
this.nodeName = null
|
||||
this._scrollElement = null
|
||||
if (typeof arg1 === 'string') {
|
||||
this.nodeName = arg1.toUpperCase()
|
||||
} else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === document.ELEMENT_NODE) {
|
||||
this.nodeName = arg1.nodeName
|
||||
this._setDom(arg1)
|
||||
} else {
|
||||
this.nodeName = 'UNDEFINED'
|
||||
}
|
||||
if (typeof arg2 === 'function') {
|
||||
this._domFilter = arg2
|
||||
}
|
||||
}
|
||||
_setDom (dom) {
|
||||
if (this._dom != null) {
|
||||
throw new Error('Only call this method if you know what you are doing ;)')
|
||||
} else if (dom.__yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
|
||||
throw new Error('Already bound to an YXml type')
|
||||
} else {
|
||||
dom.__yxml = this
|
||||
// tag is already set in constructor
|
||||
// set attributes
|
||||
let attrNames = []
|
||||
for (let i = 0; i < dom.attributes.length; i++) {
|
||||
attrNames.push(dom.attributes[i].name)
|
||||
}
|
||||
attrNames = this._domFilter(dom, attrNames)
|
||||
for (let i = 0; i < attrNames.length; i++) {
|
||||
let attrName = attrNames[i]
|
||||
let attrValue = dom.getAttribute(attrName)
|
||||
this.setAttribute(attrName, attrValue)
|
||||
}
|
||||
this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes))
|
||||
if (MutationObserver != null) {
|
||||
this._dom = this._bindToDom(dom)
|
||||
}
|
||||
return dom
|
||||
}
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
const missing = super._fromBinary(y, decoder)
|
||||
this.nodeName = decoder.readVarString()
|
||||
return missing
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
super._toBinary(encoder)
|
||||
encoder.writeVarString(this.nodeName)
|
||||
}
|
||||
_integrate (y) {
|
||||
if (this.nodeName === null) {
|
||||
throw new Error('nodeName must be defined!')
|
||||
}
|
||||
if (this._domFilter === defaultDomFilter && this._parent instanceof YXmlFragment) {
|
||||
this._domFilter = this._parent._domFilter
|
||||
}
|
||||
super._integrate(y)
|
||||
}
|
||||
toString () {
|
||||
const attrs = this.getAttributes()
|
||||
const stringBuilder = []
|
||||
for (let key in attrs) {
|
||||
stringBuilder.push(key + '="' + attrs[key] + '"')
|
||||
}
|
||||
const nodeName = this.nodeName.toLocaleLowerCase()
|
||||
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
|
||||
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
|
||||
}
|
||||
removeAttribute () {
|
||||
return YMap.prototype.delete.apply(this, arguments)
|
||||
}
|
||||
|
||||
setAttribute () {
|
||||
return YMap.prototype.set.apply(this, arguments)
|
||||
}
|
||||
|
||||
getAttribute () {
|
||||
return YMap.prototype.get.apply(this, arguments)
|
||||
}
|
||||
|
||||
getAttributes () {
|
||||
const obj = {}
|
||||
for (let [key, value] of this._map) {
|
||||
obj[key] = value._content[0]
|
||||
}
|
||||
return obj
|
||||
}
|
||||
getDom () {
|
||||
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())
|
||||
})
|
||||
if (MutationObserver !== null) {
|
||||
this._dom = this._bindToDom(dom)
|
||||
}
|
||||
}
|
||||
return dom
|
||||
}
|
||||
}
|
||||
167
src/Type/y-xml/YXmlFragment.js
Normal file
167
src/Type/y-xml/YXmlFragment.js
Normal file
@@ -0,0 +1,167 @@
|
||||
/* global MutationObserver */
|
||||
|
||||
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
||||
|
||||
import YArray from '../YArray.js'
|
||||
import YXmlText from './YXmlText.js'
|
||||
|
||||
function domToYXml (parent, doms) {
|
||||
const types = []
|
||||
doms.forEach(d => {
|
||||
if (d.__yxml != null && d.__yxml !== false) {
|
||||
d.__yxml._unbindFromDom()
|
||||
}
|
||||
if (parent._domFilter(d, []) !== null) {
|
||||
let type
|
||||
if (d.nodeType === document.TEXT_NODE) {
|
||||
type = new YXmlText(d)
|
||||
} else if (d.nodeType === document.ELEMENT_NODE) {
|
||||
type = new YXmlFragment._YXmlElement(d, parent._domFilter)
|
||||
} else {
|
||||
throw new Error('Unsupported node!')
|
||||
}
|
||||
type.enableSmartScrolling(parent._scrollElement)
|
||||
types.push(type)
|
||||
} else {
|
||||
d.__yxml = false
|
||||
}
|
||||
})
|
||||
return types
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
this._domObserver.takeRecords()
|
||||
token = true
|
||||
}
|
||||
}
|
||||
// Apply Y.Xml events to dom
|
||||
this.observe(reflectChangesOnDom)
|
||||
}
|
||||
enableSmartScrolling (scrollElement) {
|
||||
this._scrollElement = scrollElement
|
||||
this.forEach(xml => {
|
||||
xml.enableSmartScrolling(scrollElement)
|
||||
})
|
||||
}
|
||||
setDomFilter (f) {
|
||||
this._domFilter = f
|
||||
this.forEach(xml => {
|
||||
xml.setDomFilter(f)
|
||||
})
|
||||
}
|
||||
_callObserver (parentSub) {
|
||||
let event
|
||||
if (parentSub !== null) {
|
||||
event = {
|
||||
type: 'attributeChanged',
|
||||
name: parentSub,
|
||||
value: this.getAttribute(parentSub),
|
||||
target: this
|
||||
}
|
||||
} else {
|
||||
event = {
|
||||
type: 'contentChanged',
|
||||
target: this
|
||||
}
|
||||
}
|
||||
this._eventHandler.callEventListeners(event)
|
||||
}
|
||||
toString () {
|
||||
return this.map(xml => xml.toString()).join('')
|
||||
}
|
||||
_unbindFromDom () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserver.disconnect()
|
||||
this._domObserver = null
|
||||
}
|
||||
if (this._dom != null) {
|
||||
this._dom.__yxml = null
|
||||
this._dom = null
|
||||
}
|
||||
}
|
||||
insertDomElementsAfter (prev, doms) {
|
||||
const types = domToYXml(this, doms)
|
||||
return this.insertAfter(prev, types)
|
||||
}
|
||||
insertDomElements (pos, doms) {
|
||||
const types = domToYXml(this, doms)
|
||||
this.insert(pos, types)
|
||||
return types.length
|
||||
}
|
||||
bindToDom (dom) {
|
||||
if (this._dom != null) {
|
||||
this._unbindFromDom()
|
||||
}
|
||||
if (dom.__yxml != null) {
|
||||
dom.__yxml._unbindFromDom()
|
||||
}
|
||||
if (MutationObserver == null) {
|
||||
throw new Error('Not able to bind to a DOM element, because MutationObserver is not available!')
|
||||
}
|
||||
dom.innerHTML = ''
|
||||
this.forEach(t => {
|
||||
dom.insertBefore(t.getDom(), null)
|
||||
})
|
||||
this._dom = dom
|
||||
dom.__yxml = this
|
||||
this._bindToDom(dom)
|
||||
}
|
||||
// binds to a dom element
|
||||
// Only call if dom and YXml are isomorph
|
||||
_bindToDom (dom) {
|
||||
this._domObserverListener = mutations => {
|
||||
this._mutualExclude(() => {
|
||||
let diffChildren = false
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.type === 'attributes') {
|
||||
let name = mutation.attributeName
|
||||
// check if filter accepts attribute
|
||||
if (this._domFilter(this._dom, [name]).length > 0) {
|
||||
var val = mutation.target.getAttribute(name)
|
||||
if (this.getAttribute(name) !== val) {
|
||||
if (val == null) {
|
||||
this.removeAttribute(name)
|
||||
} else {
|
||||
this.setAttribute(name, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (mutation.type === 'childList') {
|
||||
diffChildren = true
|
||||
}
|
||||
})
|
||||
if (diffChildren) {
|
||||
applyChangesFromDom(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
||||
const observeOptions = { childList: true }
|
||||
if (this instanceof YXmlFragment._YXmlElement) {
|
||||
observeOptions.attributes = true
|
||||
}
|
||||
this._domObserver.observe(dom, observeOptions)
|
||||
return dom
|
||||
}
|
||||
_beforeChange () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserverListener(this._domObserver.takeRecords())
|
||||
}
|
||||
}
|
||||
}
|
||||
157
src/Type/y-xml/YXmlText.js
Normal file
157
src/Type/y-xml/YXmlText.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/* global getSelection, MutationObserver */
|
||||
|
||||
import diff from 'fast-diff'
|
||||
import YText from '../YText.js'
|
||||
import { getAnchorViewPosition, fixScrollPosition, getBoundingClientRect } from './utils.js'
|
||||
|
||||
function fixPosition (event, pos) {
|
||||
if (event.index <= pos) {
|
||||
if (event.type === 'delete') {
|
||||
return pos - Math.min(pos - event.index, event.length)
|
||||
} else {
|
||||
return pos + 1
|
||||
}
|
||||
} else {
|
||||
return pos
|
||||
}
|
||||
}
|
||||
|
||||
export default class YXmlText extends YText {
|
||||
constructor (arg1) {
|
||||
let dom = null
|
||||
let initialText = null
|
||||
if (arg1 != null && arg1.nodeType === document.TEXT_NODE) {
|
||||
dom = arg1
|
||||
initialText = dom.nodeValue
|
||||
}
|
||||
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 selection = null
|
||||
let shouldUpdateSelection = false
|
||||
let anchorNode = null
|
||||
let anchorOffset = null
|
||||
let focusNode = null
|
||||
let focusOffset = null
|
||||
if (typeof getSelection !== 'undefined') {
|
||||
selection = getSelection()
|
||||
if (selection.anchorNode === dom) {
|
||||
anchorNode = selection.anchorNode
|
||||
anchorOffset = fixPosition(event, selection.anchorOffset)
|
||||
shouldUpdateSelection = true
|
||||
}
|
||||
if (selection.focusNode === dom) {
|
||||
focusNode = selection.focusNode
|
||||
focusOffset = fixPosition(event, selection.focusOffset)
|
||||
shouldUpdateSelection = true
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
||||
if (shouldUpdateSelection) {
|
||||
selection.setBaseAndExtent(
|
||||
anchorNode || selection.anchorNode,
|
||||
anchorOffset || selection.anchorOffset,
|
||||
focusNode || selection.focusNode,
|
||||
focusOffset || selection.focusOffset
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
setDomFilter () {}
|
||||
enableSmartScrolling (scrollElement) {
|
||||
this._scrollElement = scrollElement
|
||||
}
|
||||
_setDom (dom) {
|
||||
if (this._dom != null) {
|
||||
this._unbindFromDom()
|
||||
}
|
||||
if (dom.__yxml != null) {
|
||||
dom.__yxml._unbindFromDom()
|
||||
}
|
||||
// set marker
|
||||
this._dom = dom
|
||||
dom.__yxml = this
|
||||
if (typeof MutationObserver === 'undefined') {
|
||||
return
|
||||
}
|
||||
this._domObserverListener = () => {
|
||||
this._mutualExclude(() => {
|
||||
var diffs = diff(this.toString(), dom.nodeValue)
|
||||
var pos = 0
|
||||
for (var i = 0; i < diffs.length; i++) {
|
||||
var d = diffs[i]
|
||||
if (d[0] === 0) { // EQUAL
|
||||
pos += d[1].length
|
||||
} else if (d[0] === -1) { // DELETE
|
||||
this.delete(pos, d[1].length)
|
||||
} else { // INSERT
|
||||
this.insert(pos, d[1])
|
||||
pos += d[1].length
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
this._domObserver = new MutationObserver(this._domObserverListener)
|
||||
this._domObserver.observe(this._dom, { characterData: true })
|
||||
}
|
||||
getDom () {
|
||||
if (this._dom == null) {
|
||||
const dom = document.createTextNode(this.toString())
|
||||
this._setDom(dom)
|
||||
return dom
|
||||
}
|
||||
return this._dom
|
||||
}
|
||||
_beforeChange () {
|
||||
if (this._domObserver != null && this._y !== null) { // TODO: do I need th y condition
|
||||
this._domObserverListener(this._domObserver.takeRecords())
|
||||
}
|
||||
}
|
||||
_delete (y, createDelete) {
|
||||
this._unbindFromDom()
|
||||
super._delete(y, createDelete)
|
||||
}
|
||||
_unbindFromDom () {
|
||||
if (this._domObserver != null) {
|
||||
this._domObserver.disconnect()
|
||||
this._domObserver = null
|
||||
}
|
||||
if (this._dom != null) {
|
||||
this._dom.__yxml = null
|
||||
this._dom = null
|
||||
}
|
||||
}
|
||||
}
|
||||
206
src/Type/y-xml/utils.js
Normal file
206
src/Type/y-xml/utils.js
Normal file
@@ -0,0 +1,206 @@
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function iterateUntilUndeleted (item) {
|
||||
while (item !== null && item._deleted) {
|
||||
item = item._right
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
/*
|
||||
* 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 function applyChangesFromDom (yxml) {
|
||||
const y = yxml._y
|
||||
let knownChildren =
|
||||
new Set(
|
||||
Array.prototype.map.call(yxml._dom.childNodes, child => child.__yxml)
|
||||
.filter(id => id !== undefined)
|
||||
)
|
||||
// 1. Check if any of the nodes was deleted
|
||||
yxml.forEach(function (childType, i) {
|
||||
if (!knownChildren.has(childType)) {
|
||||
childType._delete(y)
|
||||
}
|
||||
})
|
||||
// 2. iterate
|
||||
let childNodes = yxml._dom.childNodes
|
||||
let len = childNodes.length
|
||||
let prevExpectedNode = null
|
||||
let expectedNode = iterateUntilUndeleted(yxml._start)
|
||||
for (let domCnt = 0; domCnt < len; domCnt++) {
|
||||
const child = childNodes[domCnt]
|
||||
const childYXml = child.__yxml
|
||||
if (childYXml != null) {
|
||||
if (childYXml === false) {
|
||||
// should be ignored or is going to be deleted
|
||||
continue
|
||||
}
|
||||
if (expectedNode !== null) {
|
||||
if (expectedNode !== childYXml) {
|
||||
// 2.3 Not expected node
|
||||
if (childYXml._parent !== this) {
|
||||
// element is going to be deleted by its previous parent
|
||||
child.__yxml = null
|
||||
} else {
|
||||
childYXml._delete(y)
|
||||
}
|
||||
prevExpectedNode = yxml.insertDomElementsAfter(prevExpectedNode, [child])[0]
|
||||
} else {
|
||||
prevExpectedNode = expectedNode
|
||||
expectedNode = iterateUntilUndeleted(expectedNode._right)
|
||||
}
|
||||
// if this is the expected node id, just continue
|
||||
} else {
|
||||
// 2.2 fill _conten with child nodes
|
||||
prevExpectedNode = yxml.insertDomElementsAfter(prevExpectedNode, [child])[0]
|
||||
}
|
||||
} else {
|
||||
// 2.1 A new node was found
|
||||
prevExpectedNode = yxml.insertDomElementsAfter(prevExpectedNode, [child])[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function reflectChangesOnDom (event) {
|
||||
const yxml = event.target
|
||||
const dom = yxml._dom
|
||||
if (dom != null) {
|
||||
yxml._mutualExclude(() => {
|
||||
// TODO: do this once before applying stuff
|
||||
// let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
|
||||
if (event.type === 'attributeChanged') {
|
||||
if (event.value === undefined) {
|
||||
dom.removeAttribute(event.name)
|
||||
} else {
|
||||
dom.setAttribute(event.name, event.value)
|
||||
}
|
||||
} else if (event.type === 'contentChanged') {
|
||||
// create fragment of undeleted nodes
|
||||
const fragment = document.createDocumentFragment()
|
||||
yxml.forEach(function (t) {
|
||||
fragment.append(t.getDom())
|
||||
})
|
||||
// remove remainding nodes
|
||||
let lastChild = dom.lastChild
|
||||
while (lastChild !== null) {
|
||||
dom.removeChild(lastChild)
|
||||
lastChild = dom.lastChild
|
||||
}
|
||||
// insert fragment of undeleted nodes
|
||||
dom.append(fragment)
|
||||
}
|
||||
/* 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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
})
|
||||
}
|
||||
}
|
||||
9
src/Type/y-xml/y-xml.js
Normal file
9
src/Type/y-xml/y-xml.js
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
import YXmlElement from './YXmlElement.js'
|
||||
|
||||
export { default as YXmlFragment } from './YXmlFragment.js'
|
||||
export { default as YXmlElement } from './YXmlElement.js'
|
||||
export { default as YXmlText } from './YXmlText.js'
|
||||
|
||||
YXmlFragment._YXmlElement = YXmlElement
|
||||
Reference in New Issue
Block a user