This commit is contained in:
Kevin Jahns 2018-04-27 18:33:28 +02:00
parent 99f92cb9a0
commit a54d826d6d
20 changed files with 82 additions and 77 deletions

View File

@ -1,7 +1,7 @@
/* global Y, d3 */ /* global Y, d3 */
const hooks = { const hooks = {
"magic-drawing": { 'magic-drawing': {
fillType: function (dom, type) { fillType: function (dom, type) {
initDrawingBindings(type, dom) initDrawingBindings(type, dom)
}, },
@ -19,7 +19,7 @@ window.onload = function () {
window.addMagicDrawing = function addMagicDrawing () { window.addMagicDrawing = function addMagicDrawing () {
let mt = document.createElement('magic-drawing') let mt = document.createElement('magic-drawing')
mt.dataset.yjsHook = 'magic-drawing' mt.setAttribute('data-yjs-hook', 'magic-drawing')
document.body.append(mt) document.body.append(mt)
} }
@ -30,7 +30,7 @@ var renderPath = d3.svg.line()
function initDrawingBindings (type, dom) { function initDrawingBindings (type, dom) {
dom.contentEditable = 'false' dom.contentEditable = 'false'
dom.dataset.yjsHook = 'magic-drawing' dom.setAttribute('data-yjs-hook', 'magic-drawing')
var drawing = type.get('drawing') var drawing = type.get('drawing')
if (drawing === undefined) { if (drawing === undefined) {
drawing = type.set('drawing', new Y.Array()) drawing = type.set('drawing', new Y.Array())

View File

@ -9,5 +9,5 @@ let y = new Y('xml-example', {
window.yXml = y window.yXml = y
// bind xml type to a dom, and put it in body // bind xml type to a dom, and put it in body
window.sharedDom = y.define('xml', Y.XmlElement).getDom() window.sharedDom = y.define('xml', Y.XmlElement).toDom()
document.body.appendChild(window.sharedDom) document.body.appendChild(window.sharedDom)

View File

@ -19,7 +19,8 @@
"files": [ "files": [
"y.*", "y.*",
"src/*", "src/*",
".esdoc.json" ".esdoc.json",
"docs/*"
], ],
"standard": { "standard": {
"ignore": [ "ignore": [

View File

@ -51,9 +51,9 @@ export default class DomBinding extends Binding {
this.filter = opts.filter || defaultFilter this.filter = opts.filter || defaultFilter
// set initial value // set initial value
target.innerHTML = '' target.innerHTML = ''
for (let child of type) { type.forEach(child => {
target.insertBefore(child.toDom(opts.document, opts.hooks, this), null) target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
} })
this._typeObserver = typeObserver.bind(this) this._typeObserver = typeObserver.bind(this)
this._domObserver = (mutations) => { this._domObserver = (mutations) => {
domObserver.call(this, mutations, opts.document) domObserver.call(this, mutations, opts.document)

View File

@ -1,35 +1,37 @@
import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js' import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js'
import { createAssociation } from './util.js' import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes } from './filter.js' import { filterDomAttributes, defaultFilter } from './filter.js'
/** /**
* Creates a Yjs type (YXml) based on the contents of a DOM Element. * Creates a Yjs type (YXml) based on the contents of a DOM Element.
* *
* @param {Element|TextNode} element The DOM Element * @param {Element|TextNode} element The DOM Element
* @param {?Document} _document Optional. Provide the global document object. * @param {?Document} _document Optional. Provide the global document object
* @param {?DomBinding} binding This property should only be set if the type * @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks
* is going to be bound with the dom-binding. * @param {Filter} [filter=defaultFilter] Optional. Dom element filter
* @param {?DomBinding} binding Warning: This property is for internal use only!
* @return {YXmlElement | YXmlText} * @return {YXmlElement | YXmlText}
*/ */
export default function domToType (element, _document = document, binding) { export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
let type let type
switch (element.nodeType) { switch (element.nodeType) {
case _document.ELEMENT_NODE: case _document.ELEMENT_NODE:
let hookName = element.dataset.yjsHook let hookName = null
let hook let hook
// configure `hookName !== undefined` if element is a hook. // configure `hookName !== undefined` if element is a hook.
if (hookName !== undefined) { if (element.hasAttribute('data-yjs-hook')) {
hook = binding.opts.hooks[hookName] hookName = element.getAttribute('data-yjs-hook')
hook = hooks[hookName]
if (hook === undefined) { if (hook === undefined) {
console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`) console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
delete element.dataset.yjsHook delete element.removeAttribute('data-yjs-hook')
hookName = undefined hookName = null
} }
} }
if (hookName === undefined) { if (hookName === null) {
// Not a hook // Not a hook
const attrs = filterDomAttributes(element, binding.filter) const attrs = filterDomAttributes(element, filter)
if (attrs === null) { if (attrs === null) {
type = false type = false
} else { } else {
@ -37,14 +39,7 @@ export default function domToType (element, _document = document, binding) {
attrs.forEach((val, key) => { attrs.forEach((val, key) => {
type.setAttribute(key, val) type.setAttribute(key, val)
}) })
const children = [] type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
for (let elem of element.childNodes) {
const type = domToType(elem, _document, binding)
if (type !== false) {
children.push(type)
}
}
type.insert(0, children)
} }
} else { } else {
// Is a hook // Is a hook

View File

@ -14,7 +14,7 @@ export function defaultFilter (nodeName, attrs) {
} }
/** /**
* *
*/ */
export function filterDomAttributes (dom, filter) { export function filterDomAttributes (dom, filter) {
const attrs = new Map() const attrs = new Map()

View File

@ -17,7 +17,10 @@ export function iterateUntilUndeleted (item) {
* Removes an association (the information that a DOM element belongs to a * Removes an association (the information that a DOM element belongs to a
* type). * type).
* *
* @private * @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
*
*/ */
export function removeAssociation (domBinding, dom, type) { export function removeAssociation (domBinding, dom, type) {
domBinding.domToType.delete(dom) domBinding.domToType.delete(dom)
@ -28,7 +31,10 @@ export function removeAssociation (domBinding, dom, type) {
* Creates an association (the information that a DOM element belongs to a * Creates an association (the information that a DOM element belongs to a
* type). * type).
* *
* @private * @param {DomBinding} domBinding The binding object
* @param {Element} dom The dom that is to be associated with type
* @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
*
*/ */
export function createAssociation (domBinding, dom, type) { export function createAssociation (domBinding, dom, type) {
if (domBinding !== undefined) { if (domBinding !== undefined) {
@ -37,6 +43,24 @@ export function createAssociation (domBinding, dom, type) {
} }
} }
/**
* If oldDom is associated with a type, associate newDom with the type and
* forget about oldDom. If oldDom is not associated with any type, nothing happens.
*
* @param {DomBinding} domBinding The binding object
* @param {Element} oldDom The existing dom
* @param {Element} newDom The new dom object
*/
export function switchAssociation (domBinding, oldDom, newDom) {
if (domBinding !== undefined) {
const type = domBinding.domToType.get(oldDom)
if (type !== undefined) {
removeAssociation(domBinding, oldDom, type)
createAssociation(domBinding, newDom, type)
}
}
}
/** /**
* Insert Dom Elements after one of the children of this YXmlFragment. * 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 * The Dom elements will be bound to a new YXmlElement and inserted at the
@ -54,14 +78,19 @@ export function createAssociation (domBinding, dom, type) {
* @private * @private
*/ */
export function insertDomElementsAfter (type, prev, doms, _document, binding) { export function insertDomElementsAfter (type, prev, doms, _document, binding) {
const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
return type.insertAfter(prev, types)
}
export function domsToTypes (doms, _document, hooks, filter, binding) {
const types = [] const types = []
for (let dom of doms) { for (let dom of doms) {
const t = domToType(dom, _document, binding) const t = domToType(dom, _document, hooks, filter, binding)
if (t !== false) { if (t !== false) {
types.push(t) types.push(t)
} }
} }
return type.insertAfter(prev, types) return types
} }
/** /**

View File

@ -1,7 +1,7 @@
import { getStruct } from '../Util/structReferences.js' import { getStruct } from '../Util/structReferences.js'
import BinaryDecoder from '../Util/Binary/Decoder.js' import BinaryDecoder from '../Util/Binary/Decoder.js'
import { logID } from './messageToString.js' import { logID } from './messageToString.js'
import GC from '../Struct/GC.js'; import GC from '../Struct/GC.js'
class MissingEntry { class MissingEntry {
constructor (decoder, missing, struct) { constructor (decoder, missing, struct) {

View File

@ -91,4 +91,4 @@ export default class GC {
gc._length = this._length - diff gc._length = this._length - diff
return gc return gc
} }
} }

View File

@ -238,7 +238,7 @@ export default class Item {
} }
_gcChildren (y) {} _gcChildren (y) {}
_gc (y) { _gc (y) {
const gc = new GC() const gc = new GC()
gc._id = this._id gc._id = this._id

View File

@ -213,6 +213,8 @@ export default class Type extends Item {
* @param {Y} y The Yjs instance * @param {Y} y The Yjs instance
* @param {boolean} createDelete Whether to propagate a message that this * @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted. * Type was deleted.
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
* collect the children of this type.
*/ */
_delete (y, createDelete, gcChildren) { _delete (y, createDelete, gcChildren) {
if (gcChildren === undefined) { if (gcChildren === undefined) {

View File

@ -198,7 +198,7 @@ export default class YArray extends Type {
content = this._item._content[this._itemElement++] content = this._item._content[this._itemElement++]
} }
return { return {
value: [this._count, content], value: content,
done: false done: false
} }
}, },

View File

@ -123,6 +123,8 @@ export default class YXmlFragment extends YArray {
* @param {Y} y The Yjs instance * @param {Y} y The Yjs instance
* @param {boolean} createDelete Whether to propagate a message that this * @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted. * Type was deleted.
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
* collect the children of this type.
* *
* @private * @private
*/ */

View File

@ -52,7 +52,7 @@ export default class YXmlHook extends YMap {
} else { } else {
dom = document.createElement(this.hookName) dom = document.createElement(this.hookName)
} }
dom.dataset.yjsHook = this.hookName dom.setAttribute('data-yjs-hook', this.hookName)
createAssociation(binding, dom, this) createAssociation(binding, dom, this)
return dom return dom
} }

View File

@ -35,6 +35,8 @@ export default class YXmlText extends YText {
* @param {Y} y The Yjs instance * @param {Y} y The Yjs instance
* @param {boolean} createDelete Whether to propagate a message that this * @param {boolean} createDelete Whether to propagate a message that this
* Type was deleted. * Type was deleted.
* @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
* collect the children of this type.
* *
* @private * @private
*/ */

View File

@ -1,5 +1,6 @@
import ID from './ID/ID.js' import ID from './ID/ID.js'
import RootID from './ID/RootID.js' import RootID from './ID/RootID.js'
import GC from '../Struct/GC.js'
// TODO: Implement function to describe ranges // TODO: Implement function to describe ranges
@ -76,6 +77,9 @@ export function fromRelativePosition (y, rpos) {
id = new RootID(rpos[3], rpos[4]) id = new RootID(rpos[3], rpos[4])
} }
const type = y.os.get(id) const type = y.os.get(id)
if (type === null || type.constructor === GC) {
return null
}
return { return {
type, type,
offset: type.length offset: type.length
@ -84,7 +88,7 @@ export function fromRelativePosition (y, rpos) {
let offset = 0 let offset = 0
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
const parent = struct._parent const parent = struct._parent
if (parent._deleted) { if (struct.constructor === GC || parent._deleted) {
return null return null
} }
if (!struct._deleted) { if (!struct._deleted) {

View File

@ -56,4 +56,4 @@ registerStruct(7, YXmlElement)
registerStruct(8, YXmlText) registerStruct(8, YXmlText)
registerStruct(9, YXmlHook) registerStruct(9, YXmlHook)
registerStruct(12, GC) registerStruct(12, GC)

View File

@ -20,6 +20,8 @@ import DomBinding from './Bindings/DomBinding/DomBinding.js'
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js'
import debug from 'debug' import debug from 'debug'
import domToType from './Bindings/DomBinding/domToType.js'
import { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js'
// TODO: The following assignments should be moved to yjs-dist // TODO: The following assignments should be moved to yjs-dist
Y.AbstractConnector = Connector Y.AbstractConnector = Connector
@ -36,6 +38,10 @@ Y.TextareaBinding = TextareaBinding
Y.QuillBinding = QuillBinding Y.QuillBinding = QuillBinding
Y.DomBinding = DomBinding Y.DomBinding = DomBinding
DomBinding.domToType = domToType
DomBinding.domsToTypes = domsToTypes
DomBinding.switchAssociation = switchAssociation
Y.utils = { Y.utils = {
BinaryDecoder, BinaryDecoder,
UndoManager, UndoManager,

View File

@ -1,5 +1,4 @@
import { test } from '../node_modules/cutest/cutest.mjs' import { test } from '../node_modules/cutest/cutest.mjs'
import simpleDiff from '../src/Util/simpleDiff.js'
import Chance from 'chance' import Chance from 'chance'
import DeleteStore from '../src/Store/DeleteStore.js' import DeleteStore from '../src/Store/DeleteStore.js'
import ID from '../src/Util/ID/ID.js' import ID from '../src/Util/ID/ID.js'

View File

@ -8,7 +8,7 @@ import ItemJSON from '../src/Struct/ItemJSON.js'
import ItemString from '../src/Struct/ItemString.js' import ItemString from '../src/Struct/ItemString.js'
import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' import { defragmentItemContent } from '../src/Util/defragmentItemContent.js'
import Quill from 'quill' import Quill from 'quill'
import GC from '../src/Struct/GC.js'; import GC from '../src/Struct/GC.js'
export const Y = _Y export const Y = _Y
@ -42,41 +42,6 @@ function getDeleteSet (y) {
return ds return ds
} }
// TODO: remove?
export function attrsObject (dom) {
let keys = []
let yxml = dom._yxml
for (let i = 0; i < dom.attributes.length; i++) {
keys.push(dom.attributes[i].name)
}
keys = yxml._domFilter(dom, keys)
let obj = {}
for (let i = 0; i < keys.length; i++) {
let key = keys[i]
obj[key] = dom.getAttribute(key)
}
return obj
}
// TODO: remove?
export function domToJson (dom) {
if (dom.nodeType === document.TEXT_NODE) {
return dom.textContent
} else if (dom.nodeType === document.ELEMENT_NODE) {
let attributes = attrsObject(dom)
let children = Array.from(dom.childNodes.values())
.filter(d => d._yxml !== false)
.map(domToJson)
return {
name: dom.nodeName,
children: children,
attributes: attributes
}
} else {
throw new Error('Unsupported node type')
}
}
/* /*
* 1. reconnect and flush all * 1. reconnect and flush all
* 2. user 0 gc * 2. user 0 gc