diff --git a/examples/quill/index.html b/examples/quill/index.html index 4a8128b3..0f881d27 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..8e2ab6e9 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('htmleditor10', { 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/package-lock.json b/package-lock.json index d698c8a1..8bc1546c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1039,7 +1039,6 @@ "requires": { "anymatch": "1.3.0", "async-each": "1.0.1", - "fsevents": "1.1.3", "glob-parent": "2.0.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", @@ -1080,6 +1079,12 @@ "wordwrap": "0.0.2" } }, + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1310,6 +1315,12 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1777,6 +1788,12 @@ "es5-ext": "0.10.23" } }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=", + "dev": true + }, "exit-hook": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", @@ -1810,6 +1827,12 @@ "os-homedir": "1.0.2" } }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, "extend-shallow": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", @@ -1834,6 +1857,12 @@ "integrity": "sha1-ysNCuPqJAm7+c6Jg/p9rgE9J5H8=", "dev": true }, + "fast-diff": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz", + "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==", + "dev": true + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -1991,910 +2020,6 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "fsevents": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", - "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", - "dev": true, - "optional": true, - "requires": { - "nan": "2.8.0", - "node-pre-gyp": "0.6.39" - }, - "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, - "ajv": { - "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "co": "4.6.0", - "json-stable-stringify": "1.0.1" - } - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "assert-plus": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws-sign2": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "balanced-match": { - "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "boom": { - "version": "2.10.1", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "cryptiles": { - "version": "2.0.5", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1" - } - }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "extsprintf": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true - }, - "form-data": { - "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, - "har-schema": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "har-validator": { - "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "hawk": { - "version": "3.1.3", - "bundled": true, - "dev": true, - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" - } - }, - "hoek": { - "version": "2.16.3", - "bundled": true, - "dev": true - }, - "http-signature": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true - }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } - }, - "request": { - "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aws-sign2": "0.6.0", - "aws4": "1.6.0", - "caseless": "0.12.0", - "combined-stream": "1.0.5", - "extend": "3.0.1", - "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", - "is-typedarray": "1.0.0", - "isstream": "0.1.2", - "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", - "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", - "safe-buffer": "5.0.1", - "stringstream": "0.0.5", - "tough-cookie": "2.3.2", - "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" - } - }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, - "semver": { - "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "sntp": { - "version": "1.0.9", - "bundled": true, - "dev": true, - "requires": { - "hoek": "2.16.3" - } - }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "requires": { - "ansi-regex": "2.1.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "punycode": "1.4.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "extsprintf": "1.0.2" - } - }, - "wide-align": { - "version": "1.1.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "string-width": "1.0.2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true - } - } - }, "function-bind": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", @@ -3843,13 +2968,6 @@ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", "dev": true }, - "nan": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", - "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", - "dev": true, - "optional": true - }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -4012,6 +3130,12 @@ "p-limit": "1.1.0" } }, + "parchment": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.3.tgz", + "integrity": "sha512-41Y+F8FejGa+URCuDTlS1zzzlYCwoZFTWpVwiQWDL82LFAAlIIiAo3JGJSLMiSPDeM3avFUivdXN3iY/i4mBXg==", + "dev": true + }, "parse-glob": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", @@ -4199,6 +3323,31 @@ "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", "dev": true }, + "quill": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.5.tgz", + "integrity": "sha512-08P1DqKz4OZPJSlwSiZQxQ1a0F56+KEz6MttlpDNE42+WpjGuOyvsEQepScpdeyilHWrQwh61M5C1KelP8I8IA==", + "dev": true, + "requires": { + "clone": "2.1.1", + "deep-equal": "1.0.1", + "eventemitter3": "2.0.3", + "extend": "3.0.1", + "parchment": "1.1.3", + "quill-delta": "3.6.2" + } + }, + "quill-delta": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.2.tgz", + "integrity": "sha512-grWEQq9woEidPDogtDNxQKmy2LFf9zBC0EU/YTSw6TwKmMjtihTxdnPtPRfrqazB2MSJ7YdCWxmsJ7aQKRSEgg==", + "dev": true, + "requires": { + "deep-equal": "1.0.1", + "extend": "3.0.1", + "fast-diff": "1.1.2" + } + }, "randomatic": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", diff --git a/package.json b/package.json index d50e4c54..2e5cbd06 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "chance": "^1.0.9", "concurrently": "^3.4.0", "cutest": "^0.1.9", + "quill": "^1.3.5", "rollup-plugin-babel": "^2.7.1", "rollup-plugin-commonjs": "^8.0.2", "rollup-plugin-inject": "^2.0.0", diff --git a/rollup.browser.js b/rollup.browser.js index 53149fdf..dfea0f74 100644 --- a/rollup.browser.js +++ b/rollup.browser.js @@ -20,7 +20,7 @@ export default { }), commonjs(), babel(), - uglify({ + /*uglify({ mangle: { except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item'] }, @@ -34,7 +34,7 @@ export default { } } } - }) + })*/ ], banner: ` /** diff --git a/rollup.test.js b/rollup.test.js index f4072276..7e948a42 100644 --- a/rollup.test.js +++ b/rollup.test.js @@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs' import multiEntry from 'rollup-plugin-multi-entry' export default { - input: 'test/index.js', + input: 'test/y-text.tests.js', name: 'y-tests', sourcemap: true, output: { diff --git a/src/Binary/Decoder.js b/src/Binary/Decoder.js index 7208c4dd..fcc18b72 100644 --- a/src/Binary/Decoder.js +++ b/src/Binary/Decoder.js @@ -90,7 +90,7 @@ export default class BinaryDecoder { for (let i = 0; i < len; i++) { bytes[i] = this.uint8arr[this.pos++] } - let encodedString = String.fromCodePoint(...bytes) + let encodedString = bytes.map(b => String.fromCodePoint(b)).join('') return decodeURIComponent(escape(encodedString)) } /** diff --git a/src/Binding/QuillBinding.js b/src/Binding/QuillBinding.js new file mode 100644 index 00000000..8920f20f --- /dev/null +++ b/src/Binding/QuillBinding.js @@ -0,0 +1,37 @@ + +import Binding from './Binding.js' + +function typeObserver (event) { + const quill = this.target + quill.update('yjs') + this._mutualExclude(function () { + quill.updateContents(event.delta, 'yjs') + quill.update('yjs') // ignore applied changes + }) +} + +function quillObserver (delta) { + this._mutualExclude(() => { + this.type.applyDelta(delta.ops) + }) +} + +export default class QuillBinding extends Binding { + constructor (textType, quillInstance) { + // Binding handles textType as this.type and quillInstance as this.target + super(textType, quillInstance) + // set initial value + quillInstance.setContents(textType.toDelta(), 'yjs') + // Observers are handled by this class + this._typeObserver = typeObserver.bind(this) + this._quillObserver = quillObserver.bind(this) + textType.observe(this._typeObserver) + quillInstance.on('text-change', this._quillObserver) + } + destroy () { + // Remove everything that is handled by this class + this.type.unobserve(this._typeObserver) + this.target.unobserve(this._quillObserver) + super.destroy() + } +} diff --git a/src/Struct/Item.js b/src/Struct/Item.js index 96a75cc8..d6fd420b 100644 --- a/src/Struct/Item.js +++ b/src/Struct/Item.js @@ -39,6 +39,11 @@ 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) + } } export default class Item { diff --git a/src/Struct/ItemEmbed.js b/src/Struct/ItemEmbed.js new file mode 100644 index 00000000..22c9fc2f --- /dev/null +++ b/src/Struct/ItemEmbed.js @@ -0,0 +1,31 @@ +import { default as Item } from './Item.js' +import { logID } from '../MessageHandler/messageToString.js' + +export default class ItemEmbed extends Item { + constructor () { + super() + this.embed = null + } + _copy (undeleteChildren, copyPosition) { + let struct = super._copy(undeleteChildren, copyPosition) + struct.embed = this.embed + return struct + } + get _length () { + return 1 + } + _fromBinary (y, decoder) { + const missing = super._fromBinary(y, decoder) + this.embed = JSON.parse(decoder.readVarString()) + return missing + } + _toBinary (encoder) { + super._toBinary(encoder) + encoder.writeVarString(JSON.stringify(this.embed)) + } + _logString () { + const left = this._left !== null ? this._left._lastId : null + const origin = this._origin !== null ? this._origin._lastId : null + return `ItemEmbed(id:${logID(this._id)},embed:${JSON.stringify(this.embed)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` + } +} diff --git a/src/Struct/ItemFormat.js b/src/Struct/ItemFormat.js index de33f8f5..b384406b 100644 --- a/src/Struct/ItemFormat.js +++ b/src/Struct/ItemFormat.js @@ -1,7 +1,7 @@ import { default as Item } from './Item.js' import { logID } from '../MessageHandler/messageToString.js' -export default class ItemString extends Item { +export default class ItemFormat extends Item { constructor () { super() this.key = null @@ -20,26 +20,19 @@ export default class ItemString extends Item { return false } _fromBinary (y, decoder) { - let missing = super._fromBinary(y, decoder) + const missing = super._fromBinary(y, decoder) this.key = decoder.readVarString() - this.value = decoder.readVarString() + this.value = JSON.parse(decoder.readVarString()) return missing } _toBinary (encoder) { super._toBinary(encoder) encoder.writeVarString(this.key) - encoder.writeVarString(this.value) + encoder.writeVarString(JSON.stringify(this.value)) } _logString () { const left = this._left !== null ? this._left._lastId : null const origin = this._origin !== null ? this._origin._lastId : null return `ItemFormat(id:${logID(this._id)},key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` } - _splitAt (y, diff) { - if (diff === 0) { - return this - } else { - return this._right - } - } } diff --git a/src/Type/YArray.js b/src/Type/YArray.js index 886af1af..90835566 100644 --- a/src/Type/YArray.js +++ b/src/Type/YArray.js @@ -4,12 +4,13 @@ import ItemString from '../Struct/ItemString.js' import { logID } from '../MessageHandler/messageToString.js' import YEvent from '../Util/YEvent.js' -class YArrayEvent extends YEvent { +export class YArrayEvent extends YEvent { constructor (yarray, remote, transaction) { super(yarray) this.remote = remote this._transaction = transaction this._addedElements = null + this._removedElements = null } get addedElements () { if (this._addedElements === null) { @@ -26,15 +27,18 @@ class YArrayEvent extends YEvent { return this._addedElements } get removedElements () { - const target = this.target - const transaction = this._transaction - const removedElements = new Set() - transaction.deletedStructs.forEach(function (struct) { - if (struct._parent === target && !transaction.newTypes.has(struct)) { - removedElements.add(struct) - } - }) - return removedElements + if (this._removedElements === null) { + const target = this.target + const transaction = this._transaction + const removedElements = new Set() + transaction.deletedStructs.forEach(function (struct) { + if (struct._parent === target && !transaction.newTypes.has(struct)) { + removedElements.add(struct) + } + }) + this._removedElements = removedElements + } + return this._removedElements } } diff --git a/src/Type/YText.js b/src/Type/YText.js index 02650ea2..926d8ef7 100644 --- a/src/Type/YText.js +++ b/src/Type/YText.js @@ -1,7 +1,8 @@ import ItemString from '../Struct/ItemString.js' +import ItemEmbed from '../Struct/ItemEmbed.js' import ItemFormat from '../Struct/ItemFormat.js' -import YArray from './YArray.js' import { logID } from '../MessageHandler/messageToString.js' +import { YArrayEvent, default as YArray } from './YArray.js' function integrateItem (item, parent, y, left, right) { item._origin = left @@ -10,7 +11,7 @@ function integrateItem (item, parent, y, left, right) { item._right_origin = right item._parent = parent if (y !== null) { - item._integrate(this._y) + item._integrate(y) } else if (left === null) { parent._start = item } else { @@ -18,49 +19,365 @@ function integrateItem (item, parent, y, left, right) { } } -function findPosition (parent, pos, attributes) { - let currentAttributes = new Map() - let left = null - let right = parent._start - let count = 0 - while (right !== null) { +function findNextPosition (currentAttributes, parent, left, right, count) { + while (right !== null && count > 0) { switch (right.constructor) { - // case ItemBlockFormat: do not break.. + case ItemEmbed: case ItemString: const rightLen = right._deleted ? 0 : (right._length - 1) - if (count <= pos && pos <= count + rightLen) { - const splitDiff = pos - count - right = right._splitAt(parent._y, splitDiff) + if (count <= rightLen) { + right = right._splitAt(parent._y, count) left = right._left - count += splitDiff - break + return [left, right, currentAttributes] } - if (!right._deleted) { - count += right._length + if (right._deleted === false) { + count -= right._length } break case ItemFormat: if (right._deleted === false) { - const key = right.key - const value = right.value - if (value === null) { - currentAttributes.delete(key) - } else if (attributes.hasOwnProperty(key)) { - // only set if relevant - currentAttributes.set(key, value) - } + updateCurrentAttributes(currentAttributes, right) } break } left = right right = right._right } - if (pos > count) { - throw new Error('Position exceeds array range!') - } return [left, right, currentAttributes] } +function findPosition (parent, pos) { + let currentAttributes = new Map() + let left = null + let right = parent._start + return findNextPosition(currentAttributes, parent, left, right, pos) +} + +// negate applied formats +function insertNegatedAttributes (y, parent, left, right, negatedAttributes) { + // check if we really need to remove attributes + while ( + right !== null && ( + right._deleted === true || ( + right.constructor === ItemFormat && + (negatedAttributes.get(right.key) === right.value) + ) + ) + ) { + if (right._deleted === false) { + negatedAttributes.delete(right.key) + } + left = right + right = right._right + } + for (let [key, val] of negatedAttributes) { + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, parent, y, left, right) + left = format + } + return [left, right] +} + +function updateCurrentAttributes (currentAttributes, item) { + const value = item.value + const key = item.key + if (value === null) { + currentAttributes.delete(key) + } else { + currentAttributes.set(key, value) + } +} + +function minimizeAttributeChanges (left, right, currentAttributes, attributes) { + // go right while attributes[right.key] === right.value (or right is deleted) + while (true) { + if (right === null) { + break + } else if (right._deleted === true) { + // continue + } else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) { + // found a format, update currentAttributes and continue + updateCurrentAttributes(currentAttributes, right) + } else { + break + } + left = right + right = right._right + } + return [left, right] +} + +function insertText (y, text, parent, left, right, currentAttributes, attributes) { + for (let [key] of currentAttributes) { + if (attributes.hasOwnProperty(key) === false) { + attributes[key] = null + } + } + [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) + let negatedAttributes = new Map() + // insert format-start items + for (let key in attributes) { + const val = attributes[key] + const currentVal = currentAttributes.get(key) + if (currentVal !== val) { + // save negated attribute (set null if currentVal undefined) + negatedAttributes.set(key, currentVal || null) + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, parent, y, left, right) + left = format + } + } + // insert content + let item + if (text.constructor === String) { + item = new ItemString() + item._content = text + } else { + item = new ItemEmbed() + item.embed = text + } + integrateItem(item, parent, y, left, right) + left = item + return insertNegatedAttributes(y, parent, left, right, negatedAttributes) +} + +function formatText (y, length, parent, left, right, currentAttributes, attributes) { + [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) + let negatedAttributes = new Map() + // insert format-start items + for (let key in attributes) { + const val = attributes[key] + const currentVal = currentAttributes.get(key) + if (currentVal !== val) { + // save negated attribute (set null if currentVal undefined) + negatedAttributes.set(key, currentVal || null) + let format = new ItemFormat() + format.key = key + format.value = val + integrateItem(format, parent, y, left, right) + left = format + } + } + // iterate until first non-format or null is found + // delete all formats with attributes[format.key] != null + while (length > 0 && right !== null) { + if (right._deleted === false) { + switch (right.constructor) { + case ItemFormat: + if (attributes.hasOwnProperty(right.key)) { + if (attributes[right.key] === right.value) { + negatedAttributes.delete(right.key) + } else { + negatedAttributes.set(right.key, right.value) + } + right._delete(y) + } + updateCurrentAttributes(currentAttributes, right) + break + case ItemEmbed: + case ItemString: + right._splitAt(y, length) + length -= right._length + break + } + } + left = right + right = right._right + } + return insertNegatedAttributes(y, parent, left, right, negatedAttributes) +} + +function deleteText (y, length, parent, left, right, currentAttributes) { + while (length > 0 && right !== null) { + if (right._deleted === false) { + switch (right.constructor) { + case ItemFormat: + updateCurrentAttributes(currentAttributes, right) + break + case ItemEmbed: + case ItemString: + right._splitAt(y, length) + length -= right._length + right._delete(y) + break + } + } + left = right + right = right._right + } + return [left, right] +} + +class YTextEvent extends YArrayEvent { + constructor (ytext, remote, transaction) { + super(ytext, remote, transaction) + this._delta = null + } + get delta () { + if (this._delta === null) { + const y = this.target._y + y.transact(() => { + let item = this.target._start + const delta = [] + const added = this.addedElements + const removed = this.removedElements + this._delta = delta + let action = null + let attributes = {} // counts added or removed new attributes for retain + const currentAttributes = new Map() // saves all current attributes for insert + const oldAttributes = new Map() + let insert = '' + let retain = 0 + let deleteLen = 0 + const addOp = function addOp () { + if (action !== null) { + let op + switch (action) { + case 'delete': + op = { delete: deleteLen } + deleteLen = 0 + break + case 'insert': + op = { insert } + if (currentAttributes.size > 0) { + op.attributes = {} + for (let [key, value] of currentAttributes) { + if (value !== null) { + op.attributes[key] = value + } + } + } + insert = '' + break + case 'retain': + op = { retain } + if (Object.keys(attributes).length > 0) { + op.attributes = {} + for (let key in attributes) { + op.attributes[key] = attributes[key] + } + } + retain = 0 + break + } + delta.push(op) + action = null + } + } + while (item !== null) { + switch (item.constructor) { + case ItemEmbed: + if (added.has(item)) { + addOp() + action = 'insert' + insert = item.embed + addOp() + } else if (removed.has(item)) { + if (action !== 'delete') { + addOp() + action = 'delete' + } + deleteLen += 1 + } else if (item._deleted === false) { + if (action !== 'retain') { + addOp() + action = 'retain' + } + retain += 1 + } + break + case ItemString: + if (added.has(item)) { + if (action !== 'insert') { + addOp() + action = 'insert' + } + insert += item._content + } else if (removed.has(item)) { + if (action !== 'delete') { + addOp() + action = 'delete' + } + deleteLen += item._length + } else if (item._deleted === false) { + if (action !== 'retain') { + addOp() + action = 'retain' + } + retain += item._length + } + break + case ItemFormat: + if (added.has(item)) { + const curVal = currentAttributes.get(item.key) || null + if (curVal !== item.value) { + if (action === 'retain') { + addOp() + } + if (item.value === (oldAttributes.get(item.key) || null)) { + delete attributes[item.key] + } else { + attributes[item.key] = item.value + } + } else { + item._delete(y) + } + } else if (removed.has(item)) { + oldAttributes.set(item.key, item.value) + const curVal = currentAttributes.get(item.key) || null + if (curVal !== item.value) { + if (action === 'retain') { + addOp() + } + attributes[item.key] = curVal + } + } else if (item._deleted === false) { + oldAttributes.set(item.key, item.value) + if (attributes.hasOwnProperty(item.key)) { + if (attributes[item.key] !== item.value) { + if (action === 'retain') { + addOp() + } + if (item.value === null) { + attributes[item.key] = item.value + } else { + delete attributes[item.key] + } + } else { + item._delete(y) + } + } + } + if (item._deleted === false) { + if (action === 'insert') { + addOp() + } + updateCurrentAttributes(currentAttributes, item) + } + break + } + item = item._right + } + addOp() + while (this._delta.length > 0) { + let lastOp = this._delta[this._delta.length - 1] + if (lastOp.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) { + // retain delta's if they don't assign attributes + this._delta.pop() + } else { + break + } + } + }) + } + return this._delta + } +} + export default class YText extends YArray { constructor (string) { super() @@ -71,6 +388,9 @@ export default class YText extends YArray { this._start = start } } + _callObserver (transaction, parentSubs, remote) { + this._callEventHandler(transaction, new YTextEvent(this, remote, transaction)) + } toString () { let str = '' let n = this._start @@ -82,10 +402,27 @@ export default class YText extends YArray { } return str } + applyDelta (delta) { + this._transact(y => { + let left = null + let right = this._start + const currentAttributes = new Map() + for (let i = 0; i < delta.length; i++) { + let op = delta[i] + if (op.hasOwnProperty('insert')) { + ;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {}) + } else if (op.hasOwnProperty('retain')) { + ;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {}) + } else if (op.hasOwnProperty('delete')) { + ;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes) + } + } + }) + } /** * As defined by Quilljs - https://quilljs.com/docs/delta/ */ - toRichtextDelta () { + toDelta () { let ops = [] let currentAttributes = new Map() let str = '' @@ -94,10 +431,16 @@ export default class YText extends YArray { if (str.length > 0) { // pack str with attributes to ops let attributes = {} + let addAttributes = false for (let [key, value] of currentAttributes) { + addAttributes = true attributes[key] = value } - ops.push({ insert: str, attributes }) + let op = { insert: str } + if (addAttributes) { + op.attributes = attributes + } + ops.push(op) str = '' } } @@ -109,13 +452,7 @@ export default class YText extends YArray { break case ItemFormat: packStr() - const value = n.value - const key = n.key - if (value === null) { - currentAttributes.delete(key) - } else { - currentAttributes.set(key, value) - } + updateCurrentAttributes(currentAttributes, n) break } } @@ -129,72 +466,35 @@ export default class YText extends YArray { return } this._transact(y => { - let [left, right, currentAttributes] = findPosition(this, pos, attributes) - let negatedAttributes = new Map() - // insert format-start items - for (let key in attributes) { - const val = attributes[key] - const currentVal = currentAttributes.get(key) - if (currentVal !== val) { - // save negated attribute (set null if currentVal undefined) - negatedAttributes.set(key, currentVal || null) - let format = new ItemFormat() - format.key = key - format.value = val - integrateItem(format, this, y, left, right) - left = format - } - } - // insert text content - let item = new ItemString() - item._content = text - integrateItem(item, this, y, left, right) - left = item - // negate applied formats - for (let [key, value] of negatedAttributes) { - let format = new ItemFormat() - format.key = key - format.value = value - integrateItem(format, this, y, left, right) - left = format - } + let [left, right, currentAttributes] = findPosition(this, pos) + insertText(y, text, this, left, right, currentAttributes, attributes) + }) + } + insertEmbed (pos, embed, attributes = {}) { + if (embed.constructor !== Object) { + throw new Error('Embed must be an Object') + } + this._transact(y => { + let [left, right, currentAttributes] = findPosition(this, pos) + insertText(y, embed, this, left, right, currentAttributes, attributes) + }) + } + delete (pos, length) { + if (length === 0) { + return + } + this._transact(y => { + let [left, right, currentAttributes] = findPosition(this, pos) + deleteText(y, length, this, left, right, currentAttributes) }) } format (pos, length, attributes) { this._transact(y => { - let [left, _right, currentAttributes] = findPosition(this, pos, attributes) - if (_right === null) { + let [left, right, currentAttributes] = findPosition(this, pos) + if (right === null) { return } - let negatedAttributes = new Map() - // insert format-start items - for (let key in attributes) { - const val = attributes[key] - const currentVal = currentAttributes.get(key) - if (currentVal !== val) { - // save negated attribute (set null if currentVal undefined) - negatedAttributes.set(key, currentVal || null) - let format = new ItemFormat() - format.key = key - format.value = val - integrateItem(format, this, y, left, _right) - left = format - } - } - // iterate until first non-format or null is found - // delete all formats with attributes[format.key] != null - while (length > 0 && left !== null) { - if (left._deleted === false) { - if (left.constructor === ItemFormat) { - if (attributes[left.key] != null) { - left.delete(y) - } - } else if (length < left._length) { - - } - } - left = left._right - } + formatText(y, length, this, left, right, currentAttributes, attributes) }) } _logString () { diff --git a/src/Util/structReferences.js b/src/Util/structReferences.js index 53d82f48..9a09c7a0 100644 --- a/src/Util/structReferences.js +++ b/src/Util/structReferences.js @@ -6,6 +6,8 @@ import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Type/y-xml/y-x import Delete from '../Struct/Delete.js' import ItemJSON from '../Struct/ItemJSON.js' import ItemString from '../Struct/ItemString.js' +import ItemFormat from '../Struct/ItemFormat.js' +import ItemEmbed from '../Struct/ItemEmbed.js' const structs = new Map() const references = new Map() @@ -23,8 +25,11 @@ export function getReference (typeConstructor) { return references.get(typeConstructor) } +// TODO: reorder (Item* should have low numbers) addStruct(0, ItemJSON) addStruct(1, ItemString) +addStruct(10, ItemFormat) +addStruct(11, ItemEmbed) addStruct(2, Delete) addStruct(3, YArray) diff --git a/src/Y.js b/src/Y.js index 568e36ff..fd29a6e9 100644 --- a/src/Y.js +++ b/src/Y.js @@ -23,6 +23,7 @@ import debug from 'debug' import Transaction from './Transaction.js' import TextareaBinding from './Binding/TextareaBinding.js' +import QuillBinding from './Binding/QuillBinding.js' import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' @@ -202,6 +203,7 @@ Y.XmlText = YXmlText Y.XmlHook = YXmlHook Y.TextareaBinding = TextareaBinding +Y.QuillBinding = QuillBinding Y.utils = { BinaryDecoder, 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/tests-lib/helper.js b/tests-lib/helper.js index ebacea27..a760ab19 100644 --- a/tests-lib/helper.js +++ b/tests-lib/helper.js @@ -6,6 +6,7 @@ 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' export const Y = _Y @@ -92,9 +93,14 @@ 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) @@ -124,6 +130,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') @@ -153,6 +161,13 @@ export async function initArrays (t, opts) { result['array' + i] = y.define('array', Y.Array) result['map' + i] = y.define('map', Y.Map) result['xml' + i] = y.define('xml', Y.XmlElement) + const textType = y.define('text', Y.Text) + result['text' + i] = textType + const quill = new Quill(document.createElement('div')) + const quillBinding = new Y.QuillBinding(textType, quill) + result['quill' + i] = quill + result['quillBinding' + i] = quillBinding + y.quill = quill // put quill on the y object (so we can use it later) y.get('xml').setDomFilter(function (nodeName, attrs) { if (nodeName === 'HIDDEN') { return null