diff --git a/bindings/Binding.js b/bindings/Binding.js index 34addfa3..5a97eb08 100644 --- a/bindings/Binding.js +++ b/bindings/Binding.js @@ -1,5 +1,5 @@ -import { createMutex } from '../../lib/mutex.js' +import { createMutex } from '../lib/mutex.js' /** * Abstract class for bindings. diff --git a/bindings/DomBinding/DomBinding.js b/bindings/DomBinding/DomBinding.js index fc68179c..d486ffc7 100644 --- a/bindings/DomBinding/DomBinding.js +++ b/bindings/DomBinding/DomBinding.js @@ -1,6 +1,6 @@ /* global MutationObserver, getSelection */ -import { fromRelativePosition } from '../../Util/relativePosition.js' +import { fromRelativePosition } from '../../src/Util/relativePosition.js' import Binding from '../Binding.js' import { createAssociation, removeAssociation } from './util.js' import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js' @@ -121,8 +121,15 @@ export default class DomBinding extends Binding { createAssociation(this, target, type) } + flushDomChanges () { + this._domObserver(this._mutationObserver.takeRecords()) + } + /** - * NOTE: currently does not apply filter to existing elements! + * NOTE: + * * does not apply filter to existing elements! + * * only guarantees that changes are filtered locally. Remote sites may see different content. + * * @param {DomFilter} filter The filter function to use from now on. */ setFilter (filter) { diff --git a/bindings/DomBinding/domObserver.js b/bindings/DomBinding/domObserver.js index 0c27443b..b162b288 100644 --- a/bindings/DomBinding/domObserver.js +++ b/bindings/DomBinding/domObserver.js @@ -1,11 +1,11 @@ -import YXmlHook from '../../Types/YXml/YXmlHook.js' +import YXmlHook from '../../src/Types/YXml/YXmlHook.js' import { iterateUntilUndeleted, removeAssociation, insertNodeHelper } from './util.js' -import diff from '../../../lib/simpleDiff.js' -import YXmlFragment from '../../Types/YXml/YXmlFragment.js' +import diff from '../../lib/simpleDiff.js' +import YXmlFragment from '../../src/Types/YXml/YXmlFragment.js' /** * 1. Check if any of the nodes was deleted diff --git a/src/Struct/Item.js b/src/Struct/Item.js index c344ba8a..081c0188 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -47,10 +47,12 @@ export function splitHelper (y, a, b, diff) { o = o._right } y.os.put(b) - if (y._transaction.newTypes.has(a)) { - y._transaction.newTypes.add(b) - } else if (y._transaction.deletedStructs.has(a)) { - y._transaction.deletedStructs.add(b) + if (y._transaction !== null) { + if (y._transaction.newTypes.has(a)) { + y._transaction.newTypes.add(b) + } else if (y._transaction.deletedStructs.has(a)) { + y._transaction.deletedStructs.add(b) + } } } @@ -117,7 +119,7 @@ export default class Item { */ _copy () { const C = this.constructor - return C() + return new C() } /** diff --git a/src/protocols/syncProtocol.js b/src/protocols/syncProtocol.js index c779bcc4..7a8ceae8 100644 --- a/src/protocols/syncProtocol.js +++ b/src/protocols/syncProtocol.js @@ -54,7 +54,7 @@ export const stringifyDeleteSet = (decoder) => { const dsLength = decoding.readUint32(decoder) for (let i = 0; i < dsLength; i++) { str += ' -' + decoding.readVarUint(decoder) + ':\n' // decodes user - const dvLength = decoding.readVarUint(decoder) + const dvLength = decoding.readUint32(decoder) for (let j = 0; j < dvLength; j++) { str += `clock: ${decoding.readVarUint(decoder)}, length: ${decoding.readVarUint(decoder)}, gc: ${decoding.readUint8(decoder) === 1}\n` } @@ -192,8 +192,8 @@ export const readDeleteSet = (decoder, y) => { */ export const stringifyStateSet = decoder => { let s = 'State Set: ' - readStateSet(decoder).forEach((user, userState) => { - s += `(${user}: ${userState}), ` + readStateSet(decoder).forEach((clock, user) => { + s += `(${user}: ${clock}), ` }) return s } @@ -209,7 +209,7 @@ export const writeStateSet = (encoder, y) => { // write as fixed-size number to stay consistent with the other encode functions. // => anytime we write the number of objects that follow, encode as fixed-size number. encoding.writeUint32(encoder, state.size) - state.forEach((user, clock) => { + state.forEach((clock, user) => { encoding.writeVarUint(encoder, user) encoding.writeVarUint(encoder, clock) }) @@ -317,7 +317,9 @@ export const writeStructs = (encoder, y, ss) => { const overlappingLeft = y.os.findPrev(minBound) const rightID = overlappingLeft === null ? null : overlappingLeft._id if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) { - const struct = overlappingLeft._clonePartial(clock - rightID.clock) + // TODO: only write partial content (only missing content) + // const struct = overlappingLeft._clonePartial(clock - rightID.clock) + const struct = overlappingLeft struct._toBinary(encoder) len++ } diff --git a/test/encode-decode.tests.js b/test/encode-decode.tests.js index ad668245..f0c37854 100644 --- a/test/encode-decode.tests.js +++ b/test/encode-decode.tests.js @@ -13,8 +13,8 @@ function testEncoding (t, write, read, val) { t.compare(val, result, 'Compare results') } -const writeVarUint = (encoder, val) => encoder.writeVarUint(val) -const readVarUint = decoder => decoder.readVarUint() +const writeVarUint = (encoder, val) => encoding.writeVarUint(encoder, val) +const readVarUint = decoder => decoding.readVarUint(decoder) test('varUint 1 byte', async function varUint1 (t) { testEncoding(t, writeVarUint, readVarUint, 42) @@ -46,8 +46,8 @@ test('varUint random user id', async function varUintRandomUserId (t) { testEncoding(t, writeVarUint, readVarUint, generateRandomUint32()) }) -const writeVarString = (encoder, val) => encoder.writeVarString(val) -const readVarString = decoder => decoder.readVarString() +const writeVarString = (encoder, val) => encoding.writeVarString(encoder, val) +const readVarString = decoder => decoding.readVarString(decoder) test('varString', async function varString (t) { testEncoding(t, writeVarString, readVarString, 'hello') diff --git a/test/y-array.tests.js b/test/y-array.tests.js index 26a0d909..8c57444b 100644 --- a/test/y-array.tests.js +++ b/test/y-array.tests.js @@ -65,6 +65,7 @@ test('insertions work in late sync', async function array4 (t) { array2.insert(1, ['user2']) await users[1].connect() await users[2].connect() + testConnector.flushAllMessages() await compareUsers(t, users) }) @@ -215,7 +216,7 @@ function getUniqueNumber () { var arrayTransactions = [ function insert (t, user, prng) { - const yarray = user.get('array', Y.Array) + const yarray = user.define('array', Y.Array) var uniqueNumber = getUniqueNumber() var content = [] var len = random.int32(prng, 1, 4) @@ -226,14 +227,14 @@ var arrayTransactions = [ yarray.insert(pos, content) }, function insertTypeArray (t, user, prng) { - const yarray = user.get('array', Y.Array) + const yarray = user.define('array', Y.Array) var pos = random.int32(prng, 0, yarray.length) yarray.insert(pos, [Y.Array]) var array2 = yarray.get(pos) array2.insert(0, [1, 2, 3, 4]) }, function insertTypeMap (t, user, prng) { - const yarray = user.get('array', Y.Array) + const yarray = user.define('array', Y.Array) var pos = random.int32(prng, 0, yarray.length) yarray.insert(pos, [Y.Map]) var map = yarray.get(pos) @@ -242,7 +243,7 @@ var arrayTransactions = [ map.set('someprop', 44) }, function _delete (t, user, prng) { - const yarray = user.get('array', Y.Array) + const yarray = user.define('array', Y.Array) var length = yarray.length if (length > 0) { var somePos = random.int32(prng, 0, length - 1) diff --git a/test/y-map.tests.js b/test/y-map.tests.js index ffb3b8c5..e3518383 100644 --- a/test/y-map.tests.js +++ b/test/y-map.tests.js @@ -50,7 +50,7 @@ test('Basic get&set of Map property (converge via sync)', async function map1 (t testConnector.flushAllMessages() for (let user of users) { - var u = user.get('map', Y.Map) + var u = user.define('map', Y.Map) t.compare(u.get('stuff'), 'stuffy') t.assert(u.get('undefined') === undefined, 'undefined') t.compare(u.get('null'), null, 'null') @@ -94,7 +94,7 @@ test('Basic get&set of Map property (converge via update)', async function map5 testConnector.flushAllMessages() for (let user of users) { - var u = user.get('map', Y.Map) + var u = user.define('map', Y.Map) t.compare(u.get('stuff'), 'stuffy') } await compareUsers(t, users) @@ -108,7 +108,7 @@ test('Basic get&set of Map property (handle conflict)', async function map6 (t) testConnector.flushAllMessages() for (let user of users) { - var u = user.get('map', Y.Map) + var u = user.define('map', Y.Map) t.compare(u.get('stuff'), 'c0') } await compareUsers(t, users) @@ -121,7 +121,7 @@ test('Basic get&set&delete of Map property (handle conflict)', async function ma map1.set('stuff', 'c1') testConnector.flushAllMessages() for (let user of users) { - var u = user.get('map', Y.Map) + var u = user.define('map', Y.Map) t.assert(u.get('stuff') === undefined) } await compareUsers(t, users) @@ -135,7 +135,7 @@ test('Basic get&set of Map property (handle three conflicts)', async function ma map2.set('stuff', 'c3') testConnector.flushAllMessages() for (let user of users) { - var u = user.get('map', Y.Map) + var u = user.define('map', Y.Map) t.compare(u.get('stuff'), 'c0') } await compareUsers(t, users) @@ -155,7 +155,7 @@ test('Basic get&set&delete of Map property (handle three conflicts)', async func map3.set('stuff', 'c3') testConnector.flushAllMessages() for (let user of users) { - var u = user.get('map', Y.Map) + var u = user.define('map', Y.Map) t.assert(u.get('stuff') === undefined) } await compareUsers(t, users) @@ -303,12 +303,12 @@ var mapTransactions = [ function set (t, user, prng) { let key = random.oneOf(prng, ['one', 'two']) var value = random.utf16String(prng) - user.get('map', Y.Map).set(key, value) + user.define('map', Y.Map).set(key, value) }, function setType (t, user, prng) { let key = random.oneOf(prng, ['one', 'two']) var type = random.oneOf(prng, [new Y.Array(), new Y.Map()]) - user.get('map', Y.Map).set(key, type) + user.define('map', Y.Map).set(key, type) if (type instanceof Y.Array) { type.insert(0, [1, 2, 3, 4]) } else { @@ -317,7 +317,7 @@ var mapTransactions = [ }, function _delete (t, user, prng) { let key = random.oneOf(prng, ['one', 'two']) - user.get('map', Y.Map).delete(key) + user.define('map', Y.Map).delete(key) } ] diff --git a/test/y-xml.tests.js b/test/y-xml.tests.js index 4bccabba..6fba7470 100644 --- a/test/y-xml.tests.js +++ b/test/y-xml.tests.js @@ -61,20 +61,24 @@ test('attribute modifications (y -> dom)', async function xml2 (t) { }) test('attribute modifications (dom -> y)', async function xml3 (t) { - var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) + var { users, xml0, dom0, domBinding0 } = await initArrays(t, { users: 3 }) dom0.setAttribute('height', '100px') + domBinding0.flushDomChanges() t.assert(xml0.getAttribute('height') === '100px', 'setAttribute') dom0.removeAttribute('height') + domBinding0.flushDomChanges() t.assert(xml0.getAttribute('height') == null, 'removeAttribute') dom0.setAttribute('class', 'stuffy stuff') + domBinding0.flushDomChanges() t.assert(xml0.getAttribute('class') === 'stuffy stuff', 'set class attribute') await compareUsers(t, users) }) test('element insert (dom -> y)', async function xml4 (t) { - var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) + var { users, xml0, dom0, domBinding0 } = await initArrays(t, { users: 3 }) dom0.insertBefore(document.createTextNode('some text'), null) dom0.insertBefore(document.createElement('p'), null) + domBinding0.flushDomChanges() t.assert(xml0.get(0).toString() === 'some text', 'Retrieve Text Node') t.assert(xml0.get(1).nodeName === 'P', 'Retrieve Element node') await compareUsers(t, users) @@ -90,10 +94,12 @@ test('element insert (y -> dom)', async function xml5 (t) { }) test('y on insert, then delete (dom -> y)', async function xml6 (t) { - var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) + var { users, xml0, dom0, domBinding0 } = await initArrays(t, { users: 3 }) dom0.insertBefore(document.createElement('p'), null) + domBinding0.flushDomChanges() t.assert(xml0.length === 1, 'one node present') dom0.childNodes[0].remove() + domBinding0.flushDomChanges() t.assert(xml0.length === 0, 'no node present after delete') await compareUsers(t, users) }) @@ -164,11 +170,13 @@ test('Receive a bunch of elements (with disconnect)', async function xml12 (t) { }) test('move element to a different position', async function xml13 (t) { - var { testConnector, users, dom0, dom1 } = await initArrays(t, { users: 3 }) + var { testConnector, users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) dom0.append(document.createElement('div')) dom0.append(document.createElement('h1')) + domBinding0.flushDomChanges() testConnector.flushAllMessages() dom1.insertBefore(dom1.childNodes[0], null) + domBinding1.flushDomChanges() t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 0)') t.assert(dom1.childNodes[1].nodeName === 'DIV', 'div was moved to the correct position (user 0)') t.assert(dom1.childNodes[0].nodeName === 'H1', 'div was deleted (user 1)') @@ -189,6 +197,7 @@ test('filter node', async function xml14 (t) { domBinding1.setFilter(domFilter) dom0.append(document.createElement('div')) dom0.append(document.createElement('h1')) + domBinding0.flushDomChanges() testConnector.flushAllMessages() t.assert(dom1.childNodes.length === 1, 'Only one node was not transmitted') t.assert(dom1.childNodes[0].nodeName === 'DIV', 'div node was transmitted') @@ -206,6 +215,7 @@ test('filter attribute', async function xml15 (t) { dom0.setAttribute('hidden', 'true') dom0.setAttribute('style', 'height: 30px') dom0.setAttribute('data-me', '77') + domBinding0.flushDomChanges() testConnector.flushAllMessages() t.assert(dom0.getAttribute('hidden') === 'true', 'User 0 still has the attribute') t.assert(dom1.getAttribute('hidden') == null, 'User 1 did not receive update') @@ -215,7 +225,7 @@ test('filter attribute', async function xml15 (t) { }) test('deep element insert', async function xml16 (t) { - var { testConnector, users, dom0, dom1 } = await initArrays(t, { users: 3 }) + var { testConnector, users, dom0, dom1, domBinding0 } = await initArrays(t, { users: 3 }) let deepElement = document.createElement('p') let boldElement = document.createElement('b') let attrElement = document.createElement('img') @@ -225,6 +235,7 @@ test('deep element insert', async function xml16 (t) { deepElement.append(attrElement) dom0.append(deepElement) let str0 = dom0.outerHTML + domBinding0.flushDomChanges() testConnector.flushAllMessages() let str1 = dom1.outerHTML t.compare(str0, str1, 'Dom string representation matches') @@ -253,8 +264,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 { testConnector, users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 }) - domBinding0.setFilter(function (nodeName, attributes) { + var { testConnector, users, xml0, xml1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) + const filter = (nodeName, attributes) => { attributes.delete('malicious') if (nodeName === 'HIDEME') { return null @@ -263,7 +274,9 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { } else { return attributes } - }) + } + domBinding0.setFilter(filter) + domBinding1.setFilter(filter) let paragraph = new Y.XmlElement('p') let hideMe = new Y.XmlElement('hideMe') let span = new Y.XmlElement('span') @@ -275,13 +288,16 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { let tag2 = new Y.XmlElement('tag') tag2.setAttribute('isHidden', 'true') paragraph.insert(0, [tag2]) + domBinding0.flushDomChanges() testConnector.flushAllMessages() // check dom domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true') domBinding0.typeToDom.get(span).setAttribute('malicious', 'true') + domBinding0.flushDomChanges() // check incoming attributes xml1.get(0).get(0).setAttribute('malicious', 'true') xml1.insert(0, [new Y.XmlElement('hideMe')]) + domBinding0.flushDomChanges() testConnector.flushAllMessages() await compareUsers(t, users) @@ -292,30 +308,36 @@ var xmlTransactions = [ function attributeChange (t, user, prng) { // random.word generates non-empty words. prepend something user.dom.setAttribute('_' + random.word(prng), random.word(prng)) + user.domBinding.flushDomChanges() }, function attributeChangeHidden (t, user, prng) { user.dom.setAttribute('hidden', random.word(prng)) + user.domBinding.flushDomChanges() }, function insertText (t, user, prng) { let dom = user.dom var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null dom.insertBefore(document.createTextNode(random.word(prng)), succ) + user.domBinding.flushDomChanges() }, function insertHiddenDom (t, user, prng) { let dom = user.dom var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null dom.insertBefore(document.createElement('hidden'), succ) + user.domBinding.flushDomChanges() }, function insertDom (t, user, prng) { let dom = user.dom var succ = dom.children.length > 0 ? random.oneOf(prng, dom.children) : null dom.insertBefore(document.createElement('my-' + random.word(prng)), succ) + user.domBinding.flushDomChanges() }, function deleteChild (t, user, prng) { let dom = user.dom if (dom.childNodes.length > 0) { var d = random.oneOf(prng, dom.childNodes) d.remove() + user.domBinding.flushDomChanges() } }, function insertTextSecondLayer (t, user, prng) { @@ -324,6 +346,7 @@ var xmlTransactions = [ let dom2 = random.oneOf(prng, dom.children) let succ = dom2.childNodes.length > 0 ? random.oneOf(prng, dom2.childNodes) : null dom2.insertBefore(document.createTextNode(random.word(prng)), succ) + user.domBinding.flushDomChanges() } }, function insertDomSecondLayer (t, user, prng) { @@ -332,6 +355,7 @@ var xmlTransactions = [ let dom2 = random.oneOf(prng, dom.children) let succ = dom2.childNodes.length > 0 ? random.oneOf(prng, dom2.childNodes) : null dom2.insertBefore(document.createElement('my-' + random.word(prng)), succ) + user.domBinding.flushDomChanges() } }, function deleteChildSecondLayer (t, user, prng) { @@ -342,6 +366,7 @@ var xmlTransactions = [ let d = random.oneOf(prng, dom2.childNodes) d.remove() } + user.domBinding.flushDomChanges() } } ] diff --git a/tests-lib/helper.js b/tests-lib/helper.js index 35738540..ad28d4dc 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -6,10 +6,12 @@ import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' import Quill from 'quill' import GC from '../src/Struct/GC.js' import * as random from '../lib/random/random.js' -import * as message from '../src/message.js' +import * as syncProtocol from '../src/protocols/syncProtocol.js' import * as encoding from '../lib/encoding.js' import * as decoding from '../lib/decoding.js' import { createMutex } from '../lib/mutex.js' +import QuillBinding from '../bindings/QuillBinding/QuillBinding.js' +import DomBinding from '../bindings/DomBinding/DomBinding.js' export * from '../src/index.js' @@ -21,7 +23,7 @@ const afterTransaction = (y, transaction) => { y.mMux(() => { if (transaction.encodedStructsLen > 0) { const encoder = encoding.createEncoder() - message.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs) + syncProtocol.writeUpdate(encoder, transaction.encodedStructsLen, transaction.encodedStructs) broadcastMessage(y, encoding.toBuffer(encoder)) } }) @@ -31,8 +33,9 @@ export class TestYInstance extends Y.Y { /** * @param {TestConnector} testConnector */ - constructor (testConnector) { + constructor (testConnector, clientID) { super() + this.userID = clientID // overwriting clientID /** * @type {TestConnector} */ @@ -64,17 +67,19 @@ export class TestYInstance extends Y.Y { */ connect () { if (!this.tc.onlineConns.has(this)) { + this.tc.onlineConns.add(this) const encoder = encoding.createEncoder() - message.writeSyncStep1(encoder, this) + syncProtocol.writeSyncStep1(encoder, this) // publish SyncStep1 broadcastMessage(this, encoding.toBuffer(encoder)) this.tc.onlineConns.forEach(remoteYInstance => { - // remote instance sends instance to this instance - const encoder = encoding.createEncoder() - message.writeSyncStep1(encoder, remoteYInstance) - this._receive(encoding.toBuffer(encoder), remoteYInstance) + if (remoteYInstance !== this) { + // remote instance sends instance to this instance + const encoder = encoding.createEncoder() + syncProtocol.writeSyncStep1(encoder, remoteYInstance) + this._receive(encoding.toBuffer(encoder), remoteYInstance) + } }) - this.tc.onlineConns.add(this) } } /** @@ -117,9 +122,10 @@ export class TestConnector { } /** * Create a new Y instance and add it to the list of connections + * @param {number} clientID */ - createY () { - return new TestYInstance(this) + createY (clientID) { + return new TestYInstance(this, clientID) } /** * Choose random connection and flush a random message from a random sender. @@ -139,8 +145,9 @@ export class TestConnector { } const encoder = encoding.createEncoder() receiver.mMux(() => { + console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver)) // do not publish data created when this function is executed (could be ss2 or update message) - message.readMessage(decoding.createDecoder(m), encoder, receiver) + syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver) }) if (encoding.length(encoder) > 0) { // send reply message @@ -202,12 +209,15 @@ export class TestConnector { * @param {TestYInstance} y // publish message created by `y` to all other online clients * @param {ArrayBuffer} m */ -const broadcastMessage = (y, m) => - y.tc.onlineConns.forEach(remoteYInstance => { - if (remoteYInstance !== y) { - remoteYInstance._receive(m, y) - } - }) +const broadcastMessage = (y, m) => { + if (y.tc.onlineConns.has(y)) { + y.tc.onlineConns.forEach(remoteYInstance => { + if (remoteYInstance !== y) { + remoteYInstance._receive(m, y) + } + }) + } +} /** * Convert DS to a proper DeleteSet of Map. @@ -295,7 +305,7 @@ export function compareUsers (t, users) { data.os = ops data.ds = getDeleteSet(u) const ss = {} - u.ss.state.forEach((user, clock) => { + u.ss.state.forEach((clock, user) => { ss[user] = clock }) data.ss = ss @@ -347,20 +357,20 @@ export function initArrays (t, opts) { const testConnector = new TestConnector(prng) result.testConnector = testConnector for (let i = 0; i < opts.users; i++) { - let y = testConnector.createY() + let y = testConnector.createY(i) result.users.push(y) result['array' + i] = y.define('array', Y.Array) result['map' + i] = y.define('map', Y.Map) const yxml = y.define('xml', Y.XmlElement) result['xml' + i] = yxml const dom = document.createElement('my-dom') - const domBinding = new Y.DomBinding(yxml, dom, { filter }) + const domBinding = new DomBinding(yxml, dom, { filter }) 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')) - result['quillBinding' + i] = new Y.QuillBinding(textType, quill) + result['quillBinding' + i] = new QuillBinding(textType, quill) result['quill' + i] = quill y.quill = quill // put quill on the y object (so we can use it later) y.dom = dom