diff --git a/.esdoc.json b/.esdoc.json new file mode 100644 index 00000000..90752511 --- /dev/null +++ b/.esdoc.json @@ -0,0 +1,10 @@ +{ + "source": "./src", + "destination": "./docs", + "plugins": [{ + "name": "esdoc-standard-plugin", + "option": { + "accessor": {"access": ["public"], "autoPrivate": true} + } + }] +} diff --git a/.gitignore b/.gitignore index 1173b99b..d784cd40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules bower_components +docs /y.* /examples/yjs-dist.js* diff --git a/README.md b/README.md index 48e7c555..94c0b770 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,6 @@ text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides most of the complexity of concurrent editing. For additional information, demos, and tutorials visit [y-js.org](http://y-js.org/). ->**If you ever felt like giving back - now is the time! Me and a group of friends have organized a fundraiser to bring heathy food to unprivileged children in Vegas. Good food is often hard to come by. Thus some children don’t eat vegetables on a regular basis. We are offering free daily meals with fresh produce and we are going to build a self-sustainable garden at an elementary school to educate children how to live healthy. https://urbanseedfoundation.networkforgood.com/projects/48182-kevin-jahns-s-fundraiser** -> -> Your support on my funding page would mean the world to me :raised_hands: -> -> Also check the description in the link above: If we get to 2500$, I'm going to publish a premium Yjs documentation for the upcoming v13 release! There are also some other goals. The fundraising campaign ends very soon! - - ### Extensions Yjs only knows how to resolve conflicts on shared data. You have to choose a .. * *Connector* - a communication protocol that propagates changes to the clients diff --git a/examples/html-editor-drawing-hook/index.js b/examples/html-editor-drawing-hook/index.js index 27b58a10..7da324bc 100644 --- a/examples/html-editor-drawing-hook/index.js +++ b/examples/html-editor-drawing-hook/index.js @@ -1,12 +1,25 @@ /* global Y, d3 */ +const hooks = { + 'magic-drawing': { + fillType: function (dom, type) { + initDrawingBindings(type, dom) + }, + createDom: function (type) { + const dom = document.createElement('magic-drawing') + initDrawingBindings(type, dom) + return dom + } + } +} + window.onload = function () { - window.yXmlType.bindToDom(document.body) + window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks }) } window.addMagicDrawing = function addMagicDrawing () { let mt = document.createElement('magic-drawing') - mt.dataset.yjsHook = 'magic-drawing' + mt.setAttribute('data-yjs-hook', 'magic-drawing') document.body.append(mt) } @@ -17,7 +30,7 @@ var renderPath = d3.svg.line() function initDrawingBindings (type, dom) { dom.contentEditable = 'false' - dom.dataset.yjsHook = 'magic-drawing' + dom.setAttribute('data-yjs-hook', 'magic-drawing') var drawing = type.get('drawing') if (drawing === undefined) { drawing = type.set('drawing', new Y.Array()) @@ -96,17 +109,6 @@ function initDrawingBindings (type, dom) { } } -Y.XmlHook.addHook('magic-drawing', { - fillType: function (dom, type) { - initDrawingBindings(type, dom) - }, - createDom: function (type) { - const dom = document.createElement('magic-drawing') - initDrawingBindings(type, dom) - return dom - } -}) - let y = new Y('html-editor-drawing-hook-example', { connector: { name: 'websockets-client', diff --git a/examples/html-editor/index.js b/examples/html-editor/index.js index 946fdb3a..80623031 100644 --- a/examples/html-editor/index.js +++ b/examples/html-editor/index.js @@ -1,7 +1,7 @@ /* global Y */ window.onload = function () { - window.yXmlType.bindToDom(document.body) + window.domBinding = new Y.DomBinding(window.yXmlType, document.body) } let y = new Y('htmleditor', { diff --git a/examples/quill-cursors/index.html b/examples/quill-cursors/index.html new file mode 100644 index 00000000..1e63961c --- /dev/null +++ b/examples/quill-cursors/index.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + +
+
+
+
+ + + diff --git a/examples/quill-cursors/index.js b/examples/quill-cursors/index.js new file mode 100644 index 00000000..3b14ac9f --- /dev/null +++ b/examples/quill-cursors/index.js @@ -0,0 +1,78 @@ +/* global Y, Quill, QuillCursors */ + +Quill.register('modules/cursors', QuillCursors) + +let y = new Y('quill-0', { + connector: { + name: 'websockets-client', + url: 'http://127.0.0.1:1234' + } +}) +let users = y.define('users', Y.Array) +let myUserInfo = new Y.Map() +myUserInfo.set('name', 'dada') +myUserInfo.set('color', 'red') +users.push([myUserInfo]) + +let quill = new Quill('#quill-container', { + modules: { + toolbar: [ + [{ header: [1, 2, false] }], + ['bold', 'italic', 'underline'], + ['image', 'code-block'], + [{ color: [] }, { background: [] }], // Snow theme fills in values + [{ script: 'sub' }, { script: 'super' }], + ['link', 'image'], + ['link', 'code-block'], + [{ list: 'ordered' }, { list: 'bullet' }] + ], + cursors: { + hideDelay: 500 + } + }, + placeholder: 'Compose an epic...', + theme: 'snow' // or 'bubble' +}) + +let cursors = quill.getModule('cursors') + +function drawCursors () { + cursors.clearCursors() + users.map((user, userId) => { + if (user !== myUserInfo) { + let relativeRange = user.get('range') + let lastUpdated = new Date(user.get('last updated')) + if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) { + let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset + let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset + let range = { index: start, length: end - start } + cursors.setCursor(userId + '', range, user.get('name'), user.get('color')) + } + } + }) +} + +users.observeDeep(drawCursors) +drawCursors() + +quill.on('selection-change', function (range) { + if (range != null) { + myUserInfo.set('range', { + start: Y.utils.getRelativePosition(yText, range.index), + end: Y.utils.getRelativePosition(yText, range.index + range.length) + }) + } else { + myUserInfo.delete('range') + } + myUserInfo.set('last updated', new Date().toString()) +}) + +let yText = y.define('quill', Y.Text) +let quillBinding = new Y.QuillBinding(yText, quill) + +window.quillBinding = quillBinding +window.yText = yText +window.y = y +window.quill = quill +window.users = users +window.cursors = cursors diff --git a/examples/quill/index.html b/examples/quill/index.html index 4a8128b3..27a1964b 100644 --- a/examples/quill/index.html +++ b/examples/quill/index.html @@ -1,32 +1,18 @@ - - - - - - + + + + + +
- - - - - - - diff --git a/examples/quill/index.js b/examples/quill/index.js index f472cb7b..87ec0de9 100644 --- a/examples/quill/index.js +++ b/examples/quill/index.js @@ -1,40 +1,33 @@ /* global Y, Quill */ -// initialize a shared object. This function call returns a promise! - -Y({ - db: { - name: 'memory' - }, +let y = new Y('quill-cursors-0', { connector: { name: 'websockets-client', - room: 'richtext-example-quill-1.0-test', - url: 'http://localhost:1234' - }, - sourceDir: '/bower_components', - share: { - richtext: 'Richtext' // y.share.richtext is of type Y.Richtext + url: 'http://127.0.0.1:1234' } -}).then(function (y) { - window.yQuill = y - - // create quill element - window.quill = new Quill('#quill', { - modules: { - formula: true, - syntax: true, - toolbar: [ - [{ size: ['small', false, 'large', 'huge'] }], - ['bold', 'italic', 'underline'], - [{ color: [] }, { background: [] }], // Snow theme fills in values - [{ script: 'sub' }, { script: 'super' }], - ['link', 'image'], - ['link', 'code-block'], - [{ list: 'ordered' }] - ] - }, - theme: 'snow' - }) - // bind quill to richtext type - y.share.richtext.bind(window.quill) }) + +let quill = new Quill('#quill-container', { + modules: { + toolbar: [ + [{ header: [1, 2, false] }], + ['bold', 'italic', 'underline'], + ['image', 'code-block'], + [{ color: [] }, { background: [] }], // Snow theme fills in values + [{ script: 'sub' }, { script: 'super' }], + ['link', 'image'], + ['link', 'code-block'], + [{ list: 'ordered' }, { list: 'bullet' }] + ] + }, + placeholder: 'Compose an epic...', + theme: 'snow' // or 'bubble' +}) + +let yText = y.define('quill', Y.Text) + +let quillBinding = new Y.QuillBinding(yText, quill) +window.quillBinding = quillBinding +window.yText = yText +window.y = y +window.quill = quill diff --git a/examples/xml/index.html b/examples/xml/index.html index 366ed69a..78403a40 100644 --- a/examples/xml/index.html +++ b/examples/xml/index.html @@ -1,7 +1,7 @@ - + @@ -24,14 +24,16 @@ + diff --git a/test/index.js b/test/index.js index f59d2a8b..1ef3afc8 100644 --- a/test/index.js +++ b/test/index.js @@ -1,5 +1,6 @@ import './red-black-tree.js' import './y-array.tests.js' +import './y-text.tests.js' import './y-map.tests.js' import './y-xml.tests.js' import './encode-decode.tests.js' diff --git a/test/red-black-tree.js b/test/red-black-tree.js index 63cb6f08..05ac3599 100644 --- a/test/red-black-tree.js +++ b/test/red-black-tree.js @@ -1,5 +1,5 @@ import RedBlackTree from '../src/Util/Tree.js' -import ID from '../src/Util/ID.js' +import ID from '../src/Util/ID/ID.js' import Chance from 'chance' import { test, proxyConsole } from 'cutest' diff --git a/test/y-text.tests.js b/test/y-text.tests.js new file mode 100644 index 00000000..4b73ec11 --- /dev/null +++ b/test/y-text.tests.js @@ -0,0 +1,102 @@ +import { initArrays, compareUsers, flushAll } from '../tests-lib/helper.js' +import { test, proxyConsole } from 'cutest' + +proxyConsole() + +test('basic insert delete', async function text0 (t) { + let { users, text0 } = await initArrays(t, { users: 2 }) + let delta + + text0.observe(function (event) { + delta = event.delta + }) + + text0.delete(0, 0) + t.assert(true, 'Does not throw when deleting zero elements with position 0') + + text0.insert(0, 'abc') + t.assert(text0.toString() === 'abc', 'Basic insert works') + t.compare(delta, [{ insert: 'abc' }]) + + text0.delete(0, 1) + t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)') + t.compare(delta, [{ delete: 1 }]) + + text0.delete(1, 1) + t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') + t.compare(delta, [{ retain: 1 }, { delete: 1 }]) + + await compareUsers(t, users) +}) + +test('basic format', async function text1 (t) { + let { users, text0 } = await initArrays(t, { users: 2 }) + let delta + text0.observe(function (event) { + delta = event.delta + }) + text0.insert(0, 'abc', { bold: true }) + t.assert(text0.toString() === 'abc', 'Basic insert with attributes works') + t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }]) + t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }]) + text0.delete(0, 1) + t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)') + t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }]) + t.compare(delta, [{ delete: 1 }]) + text0.delete(1, 1) + t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') + t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }]) + t.compare(delta, [{ retain: 1 }, { delete: 1 }]) + text0.insert(0, 'z', {bold: true}) + t.assert(text0.toString() === 'zb') + t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }]) + t.compare(delta, [{ insert: 'z', attributes: { bold: true } }]) + t.assert(text0._start._right._right._right._content === 'b', 'Does not insert duplicate attribute marker') + text0.insert(0, 'y') + t.assert(text0.toString() === 'yzb') + t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }]) + t.compare(delta, [{ insert: 'y' }]) + text0.format(0, 2, { bold: null }) + t.assert(text0.toString() === 'yzb') + t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }]) + t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }]) + await compareUsers(t, users) +}) + +test('quill issue 1', async function quill1 (t) { + let { users, quill0 } = await initArrays(t, { users: 2 }) + quill0.insertText(0, 'x') + await flushAll(t, users) + quill0.insertText(1, '\n', 'list', 'ordered') + await flushAll(t, users) + quill0.insertText(1, '\n', 'list', 'ordered') + await compareUsers(t, users) +}) + +test('quill issue 2', async function quill2 (t) { + let { users, quill0, text0 } = await initArrays(t, { users: 2 }) + let delta + text0.observe(function (event) { + delta = event.delta + }) + quill0.insertText(0, 'abc', 'bold', true) + await flushAll(t, users) + quill0.insertText(1, 'x') + quill0.update() + t.compare(delta, [{ retain: 1 }, { insert: 'x', attributes: { bold: true } }]) + await compareUsers(t, users) +}) + +test('quill issue 3', async function quill3 (t) { + let { users, quill0, text0 } = await initArrays(t, { users: 2 }) + quill0.insertText(0, 'a') + quill0.insertText(1, '\n\n', 'list', 'ordered') + quill0.insertText(2, 'b') + t.compare(text0.toDelta(), [ + { insert: 'a' }, + { insert: '\n', attributes: { list: 'ordered' } }, + { insert: 'b' }, + { insert: '\n', attributes: { list: 'ordered' } } + ]) + await compareUsers(t, users) +}) diff --git a/test/y-xml.tests.js b/test/y-xml.tests.js index efe8d949..6d77515c 100644 --- a/test/y-xml.tests.js +++ b/test/y-xml.tests.js @@ -3,19 +3,17 @@ import { test } from 'cutest' test('set property', async function xml0 (t) { var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) - xml0.setAttribute('height', 10) - t.assert(xml0.getAttribute('height') === 10, 'Simple set+get works') + xml0.setAttribute('height', '10') + t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works') await flushAll(t, users) - t.assert(xml1.getAttribute('height') === 10, 'Simple set+get works (remote)') + t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)') await compareUsers(t, users) }) -/* TODO: Test YXml events! test('events', async function xml1 (t) { var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) var event var remoteEvent - let expectedEvent xml0.observe(function (e) { delete e._content delete e.nodes @@ -29,48 +27,28 @@ test('events', async function xml1 (t) { remoteEvent = e }) xml0.setAttribute('key', 'value') - expectedEvent = { - type: 'attributeChanged', - value: 'value', - name: 'key' - } - t.compare(event, expectedEvent, 'attribute changed event') + t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key') await flushAll(t, users) - t.compare(remoteEvent, expectedEvent, 'attribute changed event (remote)') + t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key (remote)') // check attributeRemoved xml0.removeAttribute('key') - expectedEvent = { - type: 'attributeRemoved', - name: 'key' - } - t.compare(event, expectedEvent, 'attribute deleted event') + t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute') await flushAll(t, users) - t.compare(remoteEvent, expectedEvent, 'attribute deleted event (remote)') - // test childInserted event - expectedEvent = { - type: 'childInserted', - index: 0 - } + t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)') xml0.insert(0, [new Y.XmlText('some text')]) - t.compare(event, expectedEvent, 'child inserted event') + t.assert(event.childListChanged, 'YXmlEvent.childListChanged on inserted element') await flushAll(t, users) - t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)') + t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)') // test childRemoved xml0.delete(0) - expectedEvent = { - type: 'childRemoved', - index: 0 - } - t.compare(event, expectedEvent, 'child deleted event') + t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element') await flushAll(t, users) - t.compare(remoteEvent, expectedEvent, 'child deleted event (remote)') + t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on deleted element (remote)') await compareUsers(t, users) }) -*/ test('attribute modifications (y -> dom)', async function xml2 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.setAttribute('height', '100px') await wait() t.assert(dom0.getAttribute('height') === '100px', 'setAttribute') @@ -84,8 +62,7 @@ test('attribute modifications (y -> dom)', async function xml2 (t) { }) test('attribute modifications (dom -> y)', async function xml3 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) dom0.setAttribute('height', '100px') await wait() t.assert(xml0.getAttribute('height') === '100px', 'setAttribute') @@ -99,8 +76,7 @@ test('attribute modifications (dom -> y)', async function xml3 (t) { }) test('element insert (dom -> y)', async function xml4 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) dom0.insertBefore(document.createTextNode('some text'), null) dom0.insertBefore(document.createElement('p'), null) await wait() @@ -110,8 +86,7 @@ test('element insert (dom -> y)', async function xml4 (t) { }) test('element insert (y -> dom)', async function xml5 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.insert(0, [new Y.XmlText('some text')]) xml0.insert(1, [new Y.XmlElement('p')]) t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node') @@ -120,8 +95,7 @@ test('element insert (y -> dom)', async function xml5 (t) { }) test('y on insert, then delete (dom -> y)', async function xml6 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) dom0.insertBefore(document.createElement('p'), null) await wait() t.assert(xml0.length === 1, 'one node present') @@ -132,8 +106,7 @@ test('y on insert, then delete (dom -> y)', async function xml6 (t) { }) test('y on insert, then delete (y -> dom)', async function xml7 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.insert(0, [new Y.XmlElement('p')]) t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom') xml0.delete(0, 1) @@ -142,8 +115,7 @@ test('y on insert, then delete (y -> dom)', async function xml7 (t) { }) test('delete consecutive (1) (Text)', async function xml8 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')]) await wait() xml0.delete(1, 2) @@ -155,8 +127,7 @@ test('delete consecutive (1) (Text)', async function xml8 (t) { }) test('delete consecutive (2) (Text)', async function xml9 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')]) await wait() xml0.delete(0, 1) @@ -169,8 +140,7 @@ test('delete consecutive (2) (Text)', async function xml9 (t) { }) test('delete consecutive (1) (Element)', async function xml10 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) await wait() xml0.delete(1, 2) @@ -182,8 +152,7 @@ test('delete consecutive (1) (Element)', async function xml10 (t) { }) test('delete consecutive (2) (Element)', async function xml11 (t) { - var { users, xml0 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() + var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) await wait() xml0.delete(0, 1) @@ -196,9 +165,7 @@ test('delete consecutive (2) (Element)', async function xml11 (t) { }) test('Receive a bunch of elements (with disconnect)', async function xml12 (t) { - var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() - let dom1 = xml1.getDom() + var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 }) users[1].disconnect() xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) xml0.insert(0, [new Y.XmlElement('X'), new Y.XmlElement('Y'), new Y.XmlElement('Z')]) @@ -212,9 +179,7 @@ test('Receive a bunch of elements (with disconnect)', async function xml12 (t) { }) test('move element to a different position', async function xml13 (t) { - var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() - let dom1 = xml1.getDom() + var { users, dom0, dom1 } = await initArrays(t, { users: 3 }) dom0.append(document.createElement('div')) dom0.append(document.createElement('h1')) await flushAll(t, users) @@ -227,9 +192,7 @@ test('move element to a different position', async function xml13 (t) { }) test('filter node', async function xml14 (t) { - var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() - let dom1 = xml1.getDom() + var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) let domFilter = (nodeName, attrs) => { if (nodeName === 'H1') { return null @@ -237,8 +200,8 @@ test('filter node', async function xml14 (t) { return attrs } } - xml0.setDomFilter(domFilter) - xml1.setDomFilter(domFilter) + domBinding0.setFilter(domFilter) + domBinding1.setFilter(domFilter) dom0.append(document.createElement('div')) dom0.append(document.createElement('h1')) await flushAll(t, users) @@ -248,15 +211,13 @@ test('filter node', async function xml14 (t) { }) test('filter attribute', async function xml15 (t) { - var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() - let dom1 = xml1.getDom() + var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) let domFilter = (nodeName, attrs) => { attrs.delete('hidden') return attrs } - xml0.setDomFilter(domFilter) - xml1.setDomFilter(domFilter) + domBinding0.setFilter(domFilter) + domBinding1.setFilter(domFilter) dom0.setAttribute('hidden', 'true') dom0.setAttribute('style', 'height: 30px') dom0.setAttribute('data-me', '77') @@ -269,9 +230,7 @@ test('filter attribute', async function xml15 (t) { }) test('deep element insert', async function xml16 (t) { - var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) - let dom0 = xml0.getDom() - let dom1 = xml1.getDom() + var { users, dom0, dom1 } = await initArrays(t, { users: 3 }) let deepElement = document.createElement('p') let boldElement = document.createElement('b') let attrElement = document.createElement('img') @@ -291,8 +250,8 @@ test('treeWalker', async function xml17 (t) { var { users, xml0 } = await initArrays(t, { users: 3 }) let paragraph1 = new Y.XmlElement('p') let paragraph2 = new Y.XmlElement('p') - let text1 = new Y.Text('init') - let text2 = new Y.Text('text') + let text1 = new Y.XmlText('init') + let text2 = new Y.XmlText('text') paragraph1.insert(0, [text1, text2]) xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')]) let allParagraphs = xml0.querySelectorAll('p') @@ -309,8 +268,8 @@ test('treeWalker', async function xml17 (t) { * Incoming changes that contain malicious attributes should be deleted. */ test('Filtering remote changes', async function xmlFilteringRemote (t) { - var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) - xml0.setDomFilter(function (nodeName, attributes) { + var { users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 }) + domBinding0.setFilter(function (nodeName, attributes) { attributes.delete('malicious') if (nodeName === 'HIDEME') { return null @@ -320,10 +279,6 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { return attributes } }) - // make sure that dom filters are active - // TODO: do not rely on .getDom for domFilters - xml0.getDom() - xml1.getDom() let paragraph = new Y.XmlElement('p') let hideMe = new Y.XmlElement('hideMe') let span = new Y.XmlElement('span') @@ -337,8 +292,8 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { paragraph.insert(0, [tag2]) await flushAll(t, users) // check dom - paragraph.getDom().setAttribute('malicious', 'true') - span.getDom().setAttribute('malicious', 'true') + domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true') + domBinding0.typeToDom.get(span).setAttribute('malicious', 'true') // check incoming attributes xml1.get(0).get(0).setAttribute('malicious', 'true') xml1.insert(0, [new Y.XmlElement('hideMe')]) @@ -350,35 +305,35 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { // TODO: move elements var xmlTransactions = [ function attributeChange (t, user, chance) { - user.get('xml', Y.XmlElement).getDom().setAttribute(chance.word(), chance.word()) + user.dom.setAttribute(chance.word(), chance.word()) }, function attributeChangeHidden (t, user, chance) { - user.get('xml', Y.XmlElement).getDom().setAttribute('hidden', chance.word()) + user.dom.setAttribute('hidden', chance.word()) }, function insertText (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null dom.insertBefore(document.createTextNode(chance.word()), succ) }, function insertHiddenDom (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null dom.insertBefore(document.createElement('hidden'), succ) }, function insertDom (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null dom.insertBefore(document.createElement(chance.word()), succ) }, function deleteChild (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom if (dom.childNodes.length > 0) { var d = chance.pickone(dom.childNodes) d.remove() } }, function insertTextSecondLayer (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom if (dom.children.length > 0) { let dom2 = chance.pickone(dom.children) let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null @@ -386,7 +341,7 @@ var xmlTransactions = [ } }, function insertDomSecondLayer (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom if (dom.children.length > 0) { let dom2 = chance.pickone(dom.children) let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null @@ -394,7 +349,7 @@ var xmlTransactions = [ } }, function deleteChildSecondLayer (t, user, chance) { - let dom = user.get('xml', Y.XmlElement).getDom() + let dom = user.dom if (dom.children.length > 0) { let dom2 = chance.pickone(dom.children) if (dom2.childNodes.length > 0) { diff --git a/tests-lib/helper.js b/tests-lib/helper.js index ebacea27..fe59c55f 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -1,19 +1,22 @@ -import _Y from '../src/Y.js' -import yTest from './test-connector.js' +import _Y from '../src/Y.dist.js' +import { DomBinding } from '../src/Y.js' +import TestConnector from './test-connector.js' import Chance from 'chance' import ItemJSON from '../src/Struct/ItemJSON.js' import ItemString from '../src/Struct/ItemString.js' import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' +import Quill from 'quill' +import GC from '../src/Struct/GC.js' export const Y = _Y -Y.extend(yTest) - export const database = { name: 'memory' } export const connector = { name: 'test', url: 'http://localhost:1234' } +Y.test = TestConnector + function getStateSet (y) { let ss = {} for (let [user, clock] of y.ss.state) { @@ -39,39 +42,6 @@ function getDeleteSet (y) { return ds } -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 -} - -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 * 2. user 0 gc @@ -92,22 +62,36 @@ export async function compareUsers (t, users) { await wait() await flushAll(t, users) - var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON().map(val => JSON.stringify(val))) - var userMapValues = users.map(u => u.get('map', Y.Map).toJSON()) - var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString()) + var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val))) + var userMapValues = users.map(u => u.define('map', Y.Map).toJSON()) + var userXmlValues = users.map(u => u.define('xml', Y.Xml).toString()) + var userTextValues = users.map(u => u.define('text', Y.Text).toDelta()) + var userQuillValues = users.map(u => { + u.quill.update('yjs') // get latest changes + return u.quill.getContents().ops + }) var data = users.map(u => { defragmentItemContent(u) var data = {} let ops = [] u.os.iterate(null, null, function (op) { - const json = { - id: op._id, - left: op._left === null ? null : op._left._lastId, - right: op._right === null ? null : op._right._id, - length: op._length, - deleted: op._deleted, - parent: op._parent._id + let json + if (op.constructor === GC) { + json = { + type: 'GC', + id: op._id, + length: op._length + } + } else { + json = { + id: op._id, + left: op._left === null ? null : op._left._lastId, + right: op._right === null ? null : op._right._id, + length: op._length, + deleted: op._deleted, + parent: op._parent._id + } } if (op instanceof ItemJSON || op instanceof ItemString) { json.content = op._content @@ -124,6 +108,8 @@ export async function compareUsers (t, users) { t.compare(userArrayValues[i], userArrayValues[i + 1], 'array types') t.compare(userMapValues[i], userMapValues[i + 1], 'map types') t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml types') + t.compare(userTextValues[i], userTextValues[i + 1], 'text types') + t.compare(userQuillValues[i], userQuillValues[i + 1], 'quill delta content') t.compare(data[i].os, data[i + 1].os, 'os') t.compare(data[i].ds, data[i + 1].ds, 'ds') t.compare(data[i].ss, data[i + 1].ss, 'ss') @@ -132,12 +118,20 @@ export async function compareUsers (t, users) { users.map(u => u.destroy()) } +function domFilter (nodeName, attrs) { + if (nodeName === 'HIDDEN') { + return null + } + attrs.delete('hidden') + return attrs +} + export async function initArrays (t, opts) { var result = { users: [] } var chance = opts.chance || new Chance(t.getSeed() * 1000000000) - var conn = Object.assign({ room: 'debugging_' + t.name, generateUserId: false, testContext: t, chance }, connector) + var conn = Object.assign({ room: 'debugging_' + t.name, testContext: t, chance }, connector) for (let i = 0; i < opts.users; i++) { let connOpts if (i === 0) { @@ -146,23 +140,28 @@ export async function initArrays (t, opts) { connOpts = Object.assign({ role: 'slave' }, conn) } let y = new Y(connOpts.room, { - _userID: i, // evil hackery, don't try this at home + userID: i, // evil hackery, don't try this at home connector: connOpts }) result.users.push(y) result['array' + i] = y.define('array', Y.Array) result['map' + i] = y.define('map', Y.Map) - result['xml' + i] = y.define('xml', Y.XmlElement) - y.get('xml').setDomFilter(function (nodeName, attrs) { - if (nodeName === 'HIDDEN') { - return null - } - attrs.delete('hidden') - return attrs - }) + const yxml = y.define('xml', Y.XmlElement) + result['xml' + i] = yxml + const dom = document.createElement('my-dom') + const domBinding = new DomBinding(yxml, dom, { domFilter }) + result['domBinding' + i] = domBinding + result['dom' + i] = dom + const textType = y.define('text', Y.Text) + result['text' + i] = textType + const quill = new Quill(document.createElement('div')) + result['quillBinding' + i] = new Y.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 y.on('afterTransaction', function () { for (let missing of y._missingStructs.values()) { - if (Array.from(missing.values()).length > 0) { + if (missing.size > 0) { console.error(new Error('Test check in "afterTransaction": missing should be empty!')) } } diff --git a/tests-lib/test-connector.js b/tests-lib/test-connector.js index ec24f71d..b83f2777 100644 --- a/tests-lib/test-connector.js +++ b/tests-lib/test-connector.js @@ -1,6 +1,6 @@ -/* global Y */ import { wait } from './helper' import { messageToString } from '../src/MessageHandler/messageToString' +import AbstractConnector from '../src/Connector.js' var rooms = {} @@ -64,107 +64,99 @@ function getTestRoom (roomname) { return rooms[roomname] } -export default function extendTestConnector (Y) { - class TestConnector extends Y.AbstractConnector { - constructor (y, options) { - if (options === undefined) { - throw new Error('Options must not be undefined!') - } - if (options.room == null) { - throw new Error('You must define a room name!') - } - options.forwardAppliedOperations = options.role === 'master' - super(y, options) - this.options = options - this.room = options.room - this.chance = options.chance - this.testRoom = getTestRoom(this.room) - this.testRoom.join(this) +export default class TestConnector extends AbstractConnector { + constructor (y, options) { + if (options === undefined) { + throw new Error('Options must not be undefined!') } - disconnect () { - this.testRoom.leave(this) - return super.disconnect() + if (options.room == null) { + throw new Error('You must define a room name!') } - logBufferParsed () { - console.log(' === Logging buffer of user ' + this.y.userID + ' === ') - for (let [user, conn] of this.connections) { - console.log(` ${user}:`) - for (let i = 0; i < conn.buffer.length; i++) { - console.log(messageToString(conn.buffer[i])) - } + options.forwardAppliedOperations = options.role === 'master' + super(y, options) + this.options = options + this.room = options.room + this.chance = options.chance + this.testRoom = getTestRoom(this.room) + this.testRoom.join(this) + } + disconnect () { + this.testRoom.leave(this) + return super.disconnect() + } + logBufferParsed () { + console.log(' === Logging buffer of user ' + this.y.userID + ' === ') + for (let [user, conn] of this.connections) { + console.log(` ${user}:`) + for (let i = 0; i < conn.buffer.length; i++) { + console.log(messageToString(conn.buffer[i])) } } - reconnect () { - this.testRoom.join(this) - super.reconnect() - return new Promise(resolve => { - this.whenSynced(resolve) - }) - } - send (uid, message) { - super.send(uid, message) - this.testRoom.send(this.y.userID, uid, message) - } - broadcast (message) { - super.broadcast(message) - this.testRoom.broadcast(this.y.userID, message) - } - async whenSynced (f) { - var synced = false - var periodicFlushTillSync = () => { - if (synced) { - f() - } else { - this.testRoom.flushAll([this.y]).then(function () { - setTimeout(periodicFlushTillSync, 10) - }) - } - } - periodicFlushTillSync() - return super.whenSynced(function () { - synced = true - }) - } - receiveMessage (sender, m) { - if (this.y.userID !== sender && this.connections.has(sender)) { - var buffer = this.connections.get(sender).buffer - if (buffer == null) { - buffer = this.connections.get(sender).buffer = [] - } - buffer.push(m) - if (this.chance.bool({likelihood: 30})) { - // flush 1/2 with 30% chance - var flushLength = Math.round(buffer.length / 2) - buffer.splice(0, flushLength).forEach(m => { - super.receiveMessage(sender, m) - }) - } + } + reconnect () { + this.testRoom.join(this) + super.reconnect() + return new Promise(resolve => { + this.whenSynced(resolve) + }) + } + send (uid, message) { + super.send(uid, message) + this.testRoom.send(this.y.userID, uid, message) + } + broadcast (message) { + super.broadcast(message) + this.testRoom.broadcast(this.y.userID, message) + } + async whenSynced (f) { + var synced = false + var periodicFlushTillSync = () => { + if (synced) { + f() + } else { + this.testRoom.flushAll([this.y]).then(function () { + setTimeout(periodicFlushTillSync, 10) + }) } } - async _flushAll (flushUsers) { - if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) { - // this one needs to sync with every other user - flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y) + periodicFlushTillSync() + return super.whenSynced(function () { + synced = true + }) + } + receiveMessage (sender, m) { + if (this.y.userID !== sender && this.connections.has(sender)) { + var buffer = this.connections.get(sender).buffer + if (buffer == null) { + buffer = this.connections.get(sender).buffer = [] } - for (let i = 0; i < flushUsers.length; i++) { - let userID = flushUsers[i].connector.y.userID - if (userID !== this.y.userID && this.connections.has(userID)) { - let buffer = this.connections.get(userID).buffer - if (buffer != null) { - var messages = buffer.splice(0) - for (let j = 0; j < messages.length; j++) { - super.receiveMessage(userID, messages[j]) - } + buffer.push(m) + if (this.chance.bool({likelihood: 30})) { + // flush 1/2 with 30% chance + var flushLength = Math.round(buffer.length / 2) + buffer.splice(0, flushLength).forEach(m => { + super.receiveMessage(sender, m) + }) + } + } + } + async _flushAll (flushUsers) { + if (flushUsers.some(u => u.connector.y.userID === this.y.userID)) { + // this one needs to sync with every other user + flushUsers = Array.from(this.connections.keys()).map(uid => this.testRoom.users.get(uid).y) + } + for (let i = 0; i < flushUsers.length; i++) { + let userID = flushUsers[i].connector.y.userID + if (userID !== this.y.userID && this.connections.has(userID)) { + let buffer = this.connections.get(userID).buffer + if (buffer != null) { + var messages = buffer.splice(0) + for (let j = 0; j < messages.length; j++) { + super.receiveMessage(userID, messages[j]) } } } - return 'done' } + return 'done' } - // TODO: this should be moved to a separate module (dont work on Y) - Y.test = TestConnector -} - -if (typeof Y !== 'undefined') { - extendTestConnector(Y) }