merge with master
This commit is contained in:
		
						commit
						09a94f053e
					
				
							
								
								
									
										10
									
								
								.esdoc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.esdoc.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | { | ||||||
|  |   "source": "./src", | ||||||
|  |   "destination": "./docs", | ||||||
|  |   "plugins": [{ | ||||||
|  |     "name": "esdoc-standard-plugin", | ||||||
|  |     "option": { | ||||||
|  |       "accessor": {"access": ["public"], "autoPrivate": true} | ||||||
|  |     } | ||||||
|  |   }] | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,4 +1,5 @@ | |||||||
| node_modules | node_modules | ||||||
| bower_components | bower_components | ||||||
|  | docs | ||||||
| /y.* | /y.* | ||||||
| /examples/yjs-dist.js* | /examples/yjs-dist.js* | ||||||
|  | |||||||
| @ -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, | most of the complexity of concurrent editing. For additional information, demos, | ||||||
| and tutorials visit [y-js.org](http://y-js.org/). | 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 | ### Extensions | ||||||
| Yjs only knows how to resolve conflicts on shared data. You have to choose a .. | 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 | * *Connector* - a communication protocol that propagates changes to the clients | ||||||
|  | |||||||
| @ -1,12 +1,25 @@ | |||||||
| /* global Y, d3 */ | /* 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.onload = function () { | ||||||
|   window.yXmlType.bindToDom(document.body) |   window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| window.addMagicDrawing = function addMagicDrawing () { | window.addMagicDrawing = function addMagicDrawing () { | ||||||
|   let mt = document.createElement('magic-drawing') |   let mt = document.createElement('magic-drawing') | ||||||
|   mt.dataset.yjsHook = 'magic-drawing' |   mt.setAttribute('data-yjs-hook', 'magic-drawing') | ||||||
|   document.body.append(mt) |   document.body.append(mt) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -17,7 +30,7 @@ var renderPath = d3.svg.line() | |||||||
| 
 | 
 | ||||||
| function initDrawingBindings (type, dom) { | function initDrawingBindings (type, dom) { | ||||||
|   dom.contentEditable = 'false' |   dom.contentEditable = 'false' | ||||||
|   dom.dataset.yjsHook = 'magic-drawing' |   dom.setAttribute('data-yjs-hook', 'magic-drawing') | ||||||
|   var drawing = type.get('drawing') |   var drawing = type.get('drawing') | ||||||
|   if (drawing === undefined) { |   if (drawing === undefined) { | ||||||
|     drawing = type.set('drawing', new Y.Array()) |     drawing = type.set('drawing', new Y.Array()) | ||||||
| @ -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', { | let y = new Y('html-editor-drawing-hook-example', { | ||||||
|   connector: { |   connector: { | ||||||
|     name: 'websockets-client', |     name: 'websockets-client', | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| /* global Y */ | /* global Y */ | ||||||
| 
 | 
 | ||||||
| window.onload = function () { | window.onload = function () { | ||||||
|   window.yXmlType.bindToDom(document.body) |   window.domBinding = new Y.DomBinding(window.yXmlType, document.body) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| let y = new Y('htmleditor', { | let y = new Y('htmleditor', { | ||||||
|  | |||||||
							
								
								
									
										21
									
								
								examples/quill-cursors/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								examples/quill-cursors/index.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |   <!-- Main quill library --> | ||||||
|  |   <script src="../../node_modules/quill/dist/quill.min.js"></script> | ||||||
|  |   <link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet"> | ||||||
|  |   <!-- Quill cursors module --> | ||||||
|  |   <script src="../../node_modules/quill-cursors/dist/quill-cursors.min.js"></script> | ||||||
|  |   <link href="../../node_modules/quill-cursors/dist/quill-cursors.css" rel="stylesheet"> | ||||||
|  |   <!-- Yjs Library and connector --> | ||||||
|  |   <script src="../../y.js"></script> | ||||||
|  |   <script src='../../../y-websockets-client/y-websockets-client.js'></script> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |   <div id="quill-container"> | ||||||
|  |     <div id="quill"> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <script src="./index.js"></script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
							
								
								
									
										78
									
								
								examples/quill-cursors/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								examples/quill-cursors/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
| @ -1,32 +1,18 @@ | |||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html> | <html> | ||||||
| <head> | <head> | ||||||
|   <!-- quill does not include dist files! We are using the hosted version instead --> |   <!-- Main Quill library --> | ||||||
|   <!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /--> |   <script src="../../node_modules/quill/dist/quill.min.js"></script> | ||||||
|   <link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet"> |   <link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet"> | ||||||
|   <link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet"> |   <!-- Yjs Library and connector --> | ||||||
|   <link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet"> |   <script src="../../y.js"></script> | ||||||
|   <style> |   <script src='../../../y-websockets-client/y-websockets-client.js'></script> | ||||||
|     #quill-container { |  | ||||||
|       border: 1px solid gray; |  | ||||||
|       box-shadow: 0px 0px 10px gray; |  | ||||||
|     } |  | ||||||
|   </style> |  | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|   <div id="quill-container"> |   <div id="quill-container"> | ||||||
|     <div id="quill"> |     <div id="quill"> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| 
 |  | ||||||
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script> |  | ||||||
|   <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script> |  | ||||||
|   <script src="https://cdn.quilljs.com/1.0.4/quill.js"></script> |  | ||||||
|   <!-- quill does not include dist files! We are using the hosted version instead (see above) |  | ||||||
|   <script src="../bower_components/quill/dist/quill.js"></script> |  | ||||||
|   --> |  | ||||||
|   <script src="../../y.js"></script> |  | ||||||
|   <script src='../../../y-websockets-client/y-websockets-client.js'></script> |  | ||||||
|   <script src="./index.js"></script> |   <script src="./index.js"></script> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|  | |||||||
| @ -1,40 +1,33 @@ | |||||||
| /* global Y, Quill */ | /* global Y, Quill */ | ||||||
| 
 | 
 | ||||||
| // initialize a shared object. This function call returns a promise!
 | let y = new Y('quill-cursors-0', { | ||||||
| 
 |  | ||||||
| Y({ |  | ||||||
|   db: { |  | ||||||
|     name: 'memory' |  | ||||||
|   }, |  | ||||||
|   connector: { |   connector: { | ||||||
|     name: 'websockets-client', |     name: 'websockets-client', | ||||||
|     room: 'richtext-example-quill-1.0-test', |     url: 'http://127.0.0.1:1234' | ||||||
|     url: 'http://localhost:1234' |  | ||||||
|   }, |  | ||||||
|   sourceDir: '/bower_components', |  | ||||||
|   share: { |  | ||||||
|     richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
 |  | ||||||
|   } |   } | ||||||
| }).then(function (y) { | }) | ||||||
|   window.yQuill = y |  | ||||||
| 
 | 
 | ||||||
|   // create quill element
 | let quill = new Quill('#quill-container', { | ||||||
|   window.quill = new Quill('#quill', { |  | ||||||
|   modules: { |   modules: { | ||||||
|       formula: true, |  | ||||||
|       syntax: true, |  | ||||||
|     toolbar: [ |     toolbar: [ | ||||||
|         [{ size: ['small', false, 'large', 'huge'] }], |       [{ header: [1, 2, false] }], | ||||||
|       ['bold', 'italic', 'underline'], |       ['bold', 'italic', 'underline'], | ||||||
|  |       ['image', 'code-block'], | ||||||
|       [{ color: [] }, { background: [] }],    // Snow theme fills in values
 |       [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | ||||||
|       [{ script: 'sub' }, { script: 'super' }], |       [{ script: 'sub' }, { script: 'super' }], | ||||||
|       ['link', 'image'], |       ['link', 'image'], | ||||||
|       ['link', 'code-block'], |       ['link', 'code-block'], | ||||||
|         [{ list: 'ordered' }] |       [{ list: 'ordered' }, { list: 'bullet' }] | ||||||
|     ] |     ] | ||||||
|   }, |   }, | ||||||
|     theme: 'snow' |   placeholder: 'Compose an epic...', | ||||||
|   }) |   theme: 'snow'  // or 'bubble'
 | ||||||
|   // bind quill to richtext type
 |  | ||||||
|   y.share.richtext.bind(window.quill) |  | ||||||
| }) | }) | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| <!DOCTYPE html> | <!DOCTYPE html> | ||||||
| <html> | <html> | ||||||
| </head> | </head> | ||||||
|   <!-- jquery is not required for y-xml. It is just here for convenience, and to test batch operations. --> |   <!-- jquery is not required for YXml. It is just here for convenience, and to test batch operations. --> | ||||||
|   <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> |   <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> | ||||||
|   <script src="../../y.js"></script> |   <script src="../../y.js"></script> | ||||||
|   <script src='../../../y-websockets-client/y-websockets-client.js'></script> |   <script src='../../../y-websockets-client/y-websockets-client.js'></script> | ||||||
| @ -24,14 +24,16 @@ | |||||||
|   </div> |   </div> | ||||||
| 
 | 
 | ||||||
|   <script> |   <script> | ||||||
|     var commands = document.querySelectorAll(".command"); |     /* global $ */ | ||||||
|     Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) { |     var commands = document.querySelectorAll('.command') | ||||||
|  |     Array.prototype.forEach.call(commands, function (command) { | ||||||
|       var execute = function () { |       var execute = function () { | ||||||
|         eval(command.querySelector("input").value); |         // eslint-disable-next-line no-eval | ||||||
|  |         eval(command.querySelector('input').value) | ||||||
|       } |       } | ||||||
|       command.querySelector("button").onclick = execute |       command.querySelector('button').onclick = execute | ||||||
|       $(command.querySelector("input")).keyup(function (e) { |       $(command.querySelector('input')).keyup(function (e) { | ||||||
|         if (e.keyCode == 13) { |         if (e.keyCode === 13) { | ||||||
|           execute() |           execute() | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|  | |||||||
| @ -9,5 +9,5 @@ let y = new Y('xml-example', { | |||||||
| 
 | 
 | ||||||
| window.yXml = y | window.yXml = y | ||||||
| // bind xml type to a dom, and put it in body
 | // bind xml type to a dom, and put it in body
 | ||||||
| window.sharedDom = y.define('xml', Y.XmlElement).getDom() | window.sharedDom = y.define('xml', Y.XmlElement).toDom() | ||||||
| document.body.appendChild(window.sharedDom) | document.body.appendChild(window.sharedDom) | ||||||
|  | |||||||
							
								
								
									
										1255
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1255
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @ -9,6 +9,8 @@ | |||||||
|     "test": "npm run lint", |     "test": "npm run lint", | ||||||
|     "debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'", |     "debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'", | ||||||
|     "lint": "standard", |     "lint": "standard", | ||||||
|  |     "docs": "esdoc", | ||||||
|  |     "serve-docs": "npm run docs && serve ./docs/", | ||||||
|     "dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js", |     "dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js", | ||||||
|     "watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'", |     "watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'", | ||||||
|     "postversion": "npm run dist", |     "postversion": "npm run dist", | ||||||
| @ -16,7 +18,9 @@ | |||||||
|   }, |   }, | ||||||
|   "files": [ |   "files": [ | ||||||
|     "y.*", |     "y.*", | ||||||
|     "src/*" |     "src/*", | ||||||
|  |     ".esdoc.json", | ||||||
|  |     "docs/*" | ||||||
|   ], |   ], | ||||||
|   "standard": { |   "standard": { | ||||||
|     "ignore": [ |     "ignore": [ | ||||||
| @ -53,6 +57,10 @@ | |||||||
|     "chance": "^1.0.9", |     "chance": "^1.0.9", | ||||||
|     "concurrently": "^3.4.0", |     "concurrently": "^3.4.0", | ||||||
|     "cutest": "^0.1.9", |     "cutest": "^0.1.9", | ||||||
|  |     "esdoc": "^1.0.4", | ||||||
|  |     "esdoc-standard-plugin": "^1.0.0", | ||||||
|  |     "quill": "^1.3.5", | ||||||
|  |     "quill-cursors": "^1.0.2", | ||||||
|     "rollup-plugin-babel": "^2.7.1", |     "rollup-plugin-babel": "^2.7.1", | ||||||
|     "rollup-plugin-commonjs": "^8.0.2", |     "rollup-plugin-commonjs": "^8.0.2", | ||||||
|     "rollup-plugin-inject": "^2.0.0", |     "rollup-plugin-inject": "^2.0.0", | ||||||
|  | |||||||
| @ -5,7 +5,7 @@ import commonjs from 'rollup-plugin-commonjs' | |||||||
| var pkg = require('./package.json') | var pkg = require('./package.json') | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   input: 'src/Y.js', |   input: 'src/Y.dist.js', | ||||||
|   name: 'Y', |   name: 'Y', | ||||||
|   sourcemap: true, |   sourcemap: true, | ||||||
|   output: { |   output: { | ||||||
|  | |||||||
| @ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs' | |||||||
| var pkg = require('./package.json') | var pkg = require('./package.json') | ||||||
| 
 | 
 | ||||||
| export default { | export default { | ||||||
|   input: 'src/y-dist.cjs.js', |   input: 'src/Y.dist.js', | ||||||
|   nameame: 'Y', |   nameame: 'Y', | ||||||
|   sourcemap: true, |   sourcemap: true, | ||||||
|   output: { |   output: { | ||||||
|  | |||||||
| @ -1,83 +0,0 @@ | |||||||
| import { RootFakeUserID } from '../Util/RootID.js' |  | ||||||
| 
 |  | ||||||
| const bits7 = 0b1111111 |  | ||||||
| const bits8 = 0b11111111 |  | ||||||
| 
 |  | ||||||
| export default class BinaryEncoder { |  | ||||||
|   constructor () { |  | ||||||
|     // TODO: implement chained Uint8Array buffers instead of Array buffer
 |  | ||||||
|     this.data = [] |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   get length () { |  | ||||||
|     return this.data.length |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   get pos () { |  | ||||||
|     return this.data.length |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   createBuffer () { |  | ||||||
|     return Uint8Array.from(this.data).buffer |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   writeUint8 (num) { |  | ||||||
|     this.data.push(num & bits8) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setUint8 (pos, num) { |  | ||||||
|     this.data[pos] = num & bits8 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   writeUint16 (num) { |  | ||||||
|     this.data.push(num & bits8, (num >>> 8) & bits8) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setUint16 (pos, num) { |  | ||||||
|     this.data[pos] = num & bits8 |  | ||||||
|     this.data[pos + 1] = (num >>> 8) & bits8 |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   writeUint32 (num) { |  | ||||||
|     for (let i = 0; i < 4; i++) { |  | ||||||
|       this.data.push(num & bits8) |  | ||||||
|       num >>>= 8 |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setUint32 (pos, num) { |  | ||||||
|     for (let i = 0; i < 4; i++) { |  | ||||||
|       this.data[pos + i] = num & bits8 |  | ||||||
|       num >>>= 8 |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   writeVarUint (num) { |  | ||||||
|     while (num >= 0b10000000) { |  | ||||||
|       this.data.push(0b10000000 | (bits7 & num)) |  | ||||||
|       num >>>= 7 |  | ||||||
|     } |  | ||||||
|     this.data.push(bits7 & num) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   writeVarString (str) { |  | ||||||
|     let encodedString = unescape(encodeURIComponent(str)) |  | ||||||
|     let bytes = encodedString.split('').map(c => c.codePointAt()) |  | ||||||
|     let len = bytes.length |  | ||||||
|     this.writeVarUint(len) |  | ||||||
|     for (let i = 0; i < len; i++) { |  | ||||||
|       this.data.push(bytes[i]) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   writeID (id) { |  | ||||||
|     const user = id.user |  | ||||||
|     this.writeVarUint(user) |  | ||||||
|     if (user !== RootFakeUserID) { |  | ||||||
|       this.writeVarUint(id.clock) |  | ||||||
|     } else { |  | ||||||
|       this.writeVarString(id.name) |  | ||||||
|       this.writeVarUint(id.type) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,14 +0,0 @@ | |||||||
| 
 |  | ||||||
| import { createMutualExclude } from '../Util/mutualExclude.js' |  | ||||||
| 
 |  | ||||||
| export default class Binding { |  | ||||||
|   constructor (type, target) { |  | ||||||
|     this.type = type |  | ||||||
|     this.target = target |  | ||||||
|     this._mutualExclude = createMutualExclude() |  | ||||||
|   } |  | ||||||
|   destroy () { |  | ||||||
|     this.type = null |  | ||||||
|     this.target = null |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										47
									
								
								src/Bindings/Binding.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Bindings/Binding.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | 
 | ||||||
|  | import { createMutualExclude } from '../Util/mutualExclude.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstract class for bindings. | ||||||
|  |  * | ||||||
|  |  * A binding handles data binding from a Yjs type to a data object. For example, | ||||||
|  |  * you can bind a Quill editor instance to a YText instance with the `QuillBinding` class. | ||||||
|  |  * | ||||||
|  |  * It is expected that a concrete implementation accepts two parameters | ||||||
|  |  * (type and binding target). | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  *   const quill = new Quill(document.createElement('div')) | ||||||
|  |  *   const type = y.define('quill', Y.Text) | ||||||
|  |  *   const binding = new Y.QuillBinding(quill, type) | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | export default class Binding { | ||||||
|  |   /** | ||||||
|  |    * @param {YType} type Yjs type. | ||||||
|  |    * @param {any} target Binding Target. | ||||||
|  |    */ | ||||||
|  |   constructor (type, target) { | ||||||
|  |     /** | ||||||
|  |      * The Yjs type that is bound to `target` | ||||||
|  |      * @type {YType} | ||||||
|  |      */ | ||||||
|  |     this.type = type | ||||||
|  |     /** | ||||||
|  |      * The target that `type` is bound to. | ||||||
|  |      * @type {*} | ||||||
|  |      */ | ||||||
|  |     this.target = target | ||||||
|  |     /** | ||||||
|  |      * @private | ||||||
|  |      */ | ||||||
|  |     this._mutualExclude = createMutualExclude() | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * Remove all data observers (both from the type and the target). | ||||||
|  |    */ | ||||||
|  |   destroy () { | ||||||
|  |     this.type = null | ||||||
|  |     this.target = null | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										151
									
								
								src/Bindings/DomBinding/DomBinding.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/Bindings/DomBinding/DomBinding.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,151 @@ | |||||||
|  | /* global MutationObserver */ | ||||||
|  | 
 | ||||||
|  | import Binding from '../Binding.js' | ||||||
|  | import { createAssociation, removeAssociation } from './util.js' | ||||||
|  | import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js' | ||||||
|  | import { defaultFilter, applyFilterOnType } from './filter.js' | ||||||
|  | import typeObserver from './typeObserver.js' | ||||||
|  | import domObserver from './domObserver.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A binding that binds the children of a YXmlFragment to a DOM element. | ||||||
|  |  * | ||||||
|  |  * This binding is automatically destroyed when its parent is deleted. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * const div = document.createElement('div') | ||||||
|  |  * const type = y.define('xml', Y.XmlFragment) | ||||||
|  |  * const binding = new Y.QuillBinding(type, div) | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | export default class DomBinding extends Binding { | ||||||
|  |   /** | ||||||
|  |    * @param {YXmlFragment} type The bind source. This is the ultimate source of | ||||||
|  |    *                            truth. | ||||||
|  |    * @param {Element} target The bind target. Mirrors the target. | ||||||
|  |    * @param {Object} [opts] Optional configurations | ||||||
|  | 
 | ||||||
|  |    * @param {FilterFunction} [opts.filter=defaultFilter] The filter function to use. | ||||||
|  |    */ | ||||||
|  |   constructor (type, target, opts = {}) { | ||||||
|  |     // Binding handles textType as this.type and domTextarea as this.target
 | ||||||
|  |     super(type, target) | ||||||
|  |     this.opts = opts | ||||||
|  |     opts.document = opts.document || document | ||||||
|  |     opts.hooks = opts.hooks || {} | ||||||
|  |     /** | ||||||
|  |      * Maps each DOM element to the type that it is associated with. | ||||||
|  |      * @type {Map} | ||||||
|  |      */ | ||||||
|  |     this.domToType = new Map() | ||||||
|  |     /** | ||||||
|  |      * Maps each YXml type to the DOM element that it is associated with. | ||||||
|  |      * @type {Map} | ||||||
|  |      */ | ||||||
|  |     this.typeToDom = new Map() | ||||||
|  |     /** | ||||||
|  |      * Defines which DOM attributes and elements to filter out. | ||||||
|  |      * Also filters remote changes. | ||||||
|  |      * @type {FilterFunction} | ||||||
|  |      */ | ||||||
|  |     this.filter = opts.filter || defaultFilter | ||||||
|  |     // set initial value
 | ||||||
|  |     target.innerHTML = '' | ||||||
|  |     type.forEach(child => { | ||||||
|  |       target.insertBefore(child.toDom(opts.document, opts.hooks, this), null) | ||||||
|  |     }) | ||||||
|  |     this._typeObserver = typeObserver.bind(this) | ||||||
|  |     this._domObserver = (mutations) => { | ||||||
|  |       domObserver.call(this, mutations, opts.document) | ||||||
|  |     } | ||||||
|  |     type.observeDeep(this._typeObserver) | ||||||
|  |     this._mutationObserver = new MutationObserver(this._domObserver) | ||||||
|  |     this._mutationObserver.observe(target, { | ||||||
|  |       childList: true, | ||||||
|  |       attributes: true, | ||||||
|  |       characterData: true, | ||||||
|  |       subtree: true | ||||||
|  |     }) | ||||||
|  |     const y = type._y | ||||||
|  |     // Force flush dom changes before Type changes are applied (they might
 | ||||||
|  |     // modify the dom)
 | ||||||
|  |     this._beforeTransactionHandler = (y, transaction, remote) => { | ||||||
|  |       this._domObserver(this._mutationObserver.takeRecords()) | ||||||
|  |       beforeTransactionSelectionFixer(y, this, transaction, remote) | ||||||
|  |     } | ||||||
|  |     y.on('beforeTransaction', this._beforeTransactionHandler) | ||||||
|  |     this._afterTransactionHandler = (y, transaction, remote) => { | ||||||
|  |       afterTransactionSelectionFixer(y, this, transaction, remote) | ||||||
|  |       // remove associations
 | ||||||
|  |       // TODO: this could be done more efficiently
 | ||||||
|  |       // e.g. Always delete using the following approach, or removeAssociation
 | ||||||
|  |       // in dom/type-observer..
 | ||||||
|  |       transaction.deletedStructs.forEach(type => { | ||||||
|  |         const dom = this.typeToDom.get(type) | ||||||
|  |         if (dom !== undefined) { | ||||||
|  |           removeAssociation(this, dom, type) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |     y.on('afterTransaction', this._afterTransactionHandler) | ||||||
|  |     // Before calling observers, apply dom filter to all changed and new types.
 | ||||||
|  |     this._beforeObserverCallsHandler = (y, transaction) => { | ||||||
|  |       // Apply dom filter to new and changed types
 | ||||||
|  |       transaction.changedTypes.forEach((subs, type) => { | ||||||
|  |         // Only check attributes. New types are filtered below.
 | ||||||
|  |         if ((subs.size > 1 || (subs.size === 1 && subs.has(null) === false))) { | ||||||
|  |           applyFilterOnType(y, this, type) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       transaction.newTypes.forEach(type => { | ||||||
|  |         applyFilterOnType(y, this, type) | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |     y.on('beforeObserverCalls', this._beforeObserverCallsHandler) | ||||||
|  |     createAssociation(this, target, type) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Enables the smart scrolling functionality for a Dom Binding. | ||||||
|  |    * This is useful when YXml is bound to a shared editor. When activated, | ||||||
|  |    * the viewport will be changed to accommodate remote changes. | ||||||
|  |    * | ||||||
|  |    * @param {Element} scrollElement The node that is | ||||||
|  |    */ | ||||||
|  |   enableSmartScrolling (scrollElement) { | ||||||
|  |     // @TODO: implement smart scrolling
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * NOTE: currently does not apply filter to existing elements! | ||||||
|  |    * @param {FilterFunction} filter The filter function to use from now on. | ||||||
|  |    */ | ||||||
|  |   setFilter (filter) { | ||||||
|  |     this.filter = filter | ||||||
|  |     // TODO: apply filter to all elements
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Remove all properties that are handled by this class. | ||||||
|  |    */ | ||||||
|  |   destroy () { | ||||||
|  |     this.domToType = null | ||||||
|  |     this.typeToDom = null | ||||||
|  |     this.type.unobserve(this._typeObserver) | ||||||
|  |     this._mutationObserver.disconnect() | ||||||
|  |     const y = this.type._y | ||||||
|  |     y.off('beforeTransaction', this._beforeTransactionHandler) | ||||||
|  |     y.off('beforeObserverCalls', this._beforeObserverCallsHandler) | ||||||
|  |     y.off('afterObserverCalls', this._afterObserverCallsHandler) | ||||||
|  |     y.off('afterTransaction', this._afterTransactionHandler) | ||||||
|  |     super.destroy() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * A filter defines which elements and attributes to share. | ||||||
|  |    * Return null if the node should be filtered. Otherwise return the Map of | ||||||
|  |    * accepted attributes. | ||||||
|  |    * | ||||||
|  |    * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction | ||||||
|  |    */ | ||||||
							
								
								
									
										136
									
								
								src/Bindings/DomBinding/domObserver.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/Bindings/DomBinding/domObserver.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | |||||||
|  | 
 | ||||||
|  | import YXmlHook from '../../Types/YXml/YXmlHook.js' | ||||||
|  | import { | ||||||
|  |   iterateUntilUndeleted, | ||||||
|  |   removeAssociation, | ||||||
|  |   insertNodeHelper } from './util.js' | ||||||
|  | import diff from '../../Util/simpleDiff.js' | ||||||
|  | import YXmlFragment from '../../Types/YXml/YXmlFragment.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * 1. Check if any of the nodes was deleted | ||||||
|  |  * 2. Iterate over the children. | ||||||
|  |  *    2.1 If a node exists that is not yet bound to a type, insert a new node | ||||||
|  |  *    2.2 If _contents.length < dom.childNodes.length, fill the | ||||||
|  |  *        rest of _content with childNodes | ||||||
|  |  *    2.3 If a node was moved, delete it and | ||||||
|  |  *       recreate a new yxml element that is bound to that node. | ||||||
|  |  *       You can detect that a node was moved because expectedId | ||||||
|  |  *       !== actualId in the list | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function applyChangesFromDom (binding, dom, yxml, _document) { | ||||||
|  |   if (yxml == null || yxml === false || yxml.constructor === YXmlHook) { | ||||||
|  |     return | ||||||
|  |   } | ||||||
|  |   const y = yxml._y | ||||||
|  |   const knownChildren = new Set() | ||||||
|  |   for (let i = dom.childNodes.length - 1; i >= 0; i--) { | ||||||
|  |     const type = binding.domToType.get(dom.childNodes[i]) | ||||||
|  |     if (type !== undefined && type !== false) { | ||||||
|  |       knownChildren.add(type) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // 1. Check if any of the nodes was deleted
 | ||||||
|  |   yxml.forEach(function (childType) { | ||||||
|  |     if (knownChildren.has(childType) === false) { | ||||||
|  |       childType._delete(y) | ||||||
|  |       removeAssociation(binding, binding.typeToDom.get(childType), childType) | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   // 2. iterate
 | ||||||
|  |   const childNodes = dom.childNodes | ||||||
|  |   const len = childNodes.length | ||||||
|  |   let prevExpectedType = null | ||||||
|  |   let expectedType = iterateUntilUndeleted(yxml._start) | ||||||
|  |   for (let domCnt = 0; domCnt < len; domCnt++) { | ||||||
|  |     const childNode = childNodes[domCnt] | ||||||
|  |     const childType = binding.domToType.get(childNode) | ||||||
|  |     if (childType !== undefined) { | ||||||
|  |       if (childType === false) { | ||||||
|  |         // should be ignored or is going to be deleted
 | ||||||
|  |         continue | ||||||
|  |       } | ||||||
|  |       if (expectedType !== null) { | ||||||
|  |         if (expectedType !== childType) { | ||||||
|  |           // 2.3 Not expected node
 | ||||||
|  |           if (childType._parent !== yxml) { | ||||||
|  |             // child was moved from another parent
 | ||||||
|  |             // childType is going to be deleted by its previous parent
 | ||||||
|  |             removeAssociation(binding, childNode, childType) | ||||||
|  |           } else { | ||||||
|  |             // child was moved to a different position.
 | ||||||
|  |             childType._delete(y) | ||||||
|  |             removeAssociation(binding, childNode, childType) | ||||||
|  |           } | ||||||
|  |           prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding) | ||||||
|  |         } else { | ||||||
|  |           // Found expected node. Continue.
 | ||||||
|  |           prevExpectedType = expectedType | ||||||
|  |           expectedType = iterateUntilUndeleted(expectedType._right) | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         // 2.2 Fill _content with child nodes
 | ||||||
|  |         prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding) | ||||||
|  |       } | ||||||
|  |     } else { | ||||||
|  |       // 2.1 A new node was found
 | ||||||
|  |       prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export default function domObserver (mutations, _document) { | ||||||
|  |   this._mutualExclude(() => { | ||||||
|  |     this.type._y.transact(() => { | ||||||
|  |       let diffChildren = new Set() | ||||||
|  |       mutations.forEach(mutation => { | ||||||
|  |         const dom = mutation.target | ||||||
|  |         const yxml = this.domToType.get(dom) | ||||||
|  |         if (yxml === false || yxml === undefined || yxml.constructor === YXmlHook) { | ||||||
|  |           // dom element is filtered
 | ||||||
|  |           return | ||||||
|  |         } | ||||||
|  |         switch (mutation.type) { | ||||||
|  |           case 'characterData': | ||||||
|  |             var change = diff(yxml.toString(), dom.nodeValue) | ||||||
|  |             yxml.delete(change.pos, change.remove) | ||||||
|  |             yxml.insert(change.pos, change.insert) | ||||||
|  |             break | ||||||
|  |           case 'attributes': | ||||||
|  |             if (yxml.constructor === YXmlFragment) { | ||||||
|  |               break | ||||||
|  |             } | ||||||
|  |             let name = mutation.attributeName | ||||||
|  |             let val = dom.getAttribute(name) | ||||||
|  |             // check if filter accepts attribute
 | ||||||
|  |             let attributes = new Map() | ||||||
|  |             attributes.set(name, val) | ||||||
|  |             if (yxml.constructor !== YXmlFragment && this.filter(dom.nodeName, attributes).size > 0) { | ||||||
|  |               if (yxml.getAttribute(name) !== val) { | ||||||
|  |                 if (val == null) { | ||||||
|  |                   yxml.removeAttribute(name) | ||||||
|  |                 } else { | ||||||
|  |                   yxml.setAttribute(name, val) | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |             break | ||||||
|  |           case 'childList': | ||||||
|  |             diffChildren.add(mutation.target) | ||||||
|  |             break | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |       for (let dom of diffChildren) { | ||||||
|  |         if (dom.yOnChildrenChanged !== undefined) { | ||||||
|  |           dom.yOnChildrenChanged() | ||||||
|  |         } | ||||||
|  |         const yxml = this.domToType.get(dom) | ||||||
|  |         applyChangesFromDom(this, dom, yxml, _document) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								src/Bindings/DomBinding/domToType.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/Bindings/DomBinding/domToType.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | 
 | ||||||
|  | import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js' | ||||||
|  | import { createAssociation, domsToTypes } from './util.js' | ||||||
|  | import { filterDomAttributes, defaultFilter } from './filter.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Creates a Yjs type (YXml) based on the contents of a DOM Element. | ||||||
|  |  * | ||||||
|  |  * @param {Element|TextNode} element The DOM Element | ||||||
|  |  * @param {?Document} _document Optional. Provide the global document object | ||||||
|  |  * @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks | ||||||
|  |  * @param {Filter} [filter=defaultFilter] Optional. Dom element filter | ||||||
|  |  * @param {?DomBinding} binding Warning: This property is for internal use only! | ||||||
|  |  * @return {YXmlElement | YXmlText} | ||||||
|  |  */ | ||||||
|  | export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) { | ||||||
|  |   let type | ||||||
|  |   switch (element.nodeType) { | ||||||
|  |     case _document.ELEMENT_NODE: | ||||||
|  |       let hookName = null | ||||||
|  |       let hook | ||||||
|  |       // configure `hookName !== undefined` if element is a hook.
 | ||||||
|  |       if (element.hasAttribute('data-yjs-hook')) { | ||||||
|  |         hookName = element.getAttribute('data-yjs-hook') | ||||||
|  |         hook = hooks[hookName] | ||||||
|  |         if (hook === undefined) { | ||||||
|  |           console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`) | ||||||
|  |           delete element.removeAttribute('data-yjs-hook') | ||||||
|  |           hookName = null | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (hookName === null) { | ||||||
|  |         // Not a hook
 | ||||||
|  |         const attrs = filterDomAttributes(element, filter) | ||||||
|  |         if (attrs === null) { | ||||||
|  |           type = false | ||||||
|  |         } else { | ||||||
|  |           type = new YXmlElement(element.nodeName) | ||||||
|  |           attrs.forEach((val, key) => { | ||||||
|  |             type.setAttribute(key, val) | ||||||
|  |           }) | ||||||
|  |           type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding)) | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         // Is a hook
 | ||||||
|  |         type = new YXmlHook(hookName) | ||||||
|  |         hook.fillType(element, type) | ||||||
|  |       } | ||||||
|  |       break | ||||||
|  |     case _document.TEXT_NODE: | ||||||
|  |       type = new YXmlText() | ||||||
|  |       type.insert(0, element.nodeValue) | ||||||
|  |       break | ||||||
|  |     default: | ||||||
|  |       throw new Error('Can\'t transform this node type to a YXml type!') | ||||||
|  |   } | ||||||
|  |   createAssociation(binding, element, type) | ||||||
|  |   return type | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								src/Bindings/DomBinding/filter.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/Bindings/DomBinding/filter.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | |||||||
|  | import isParentOf from '../../Util/isParentOf.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Default filter method (does nothing). | ||||||
|  |  * | ||||||
|  |  * @param {String} nodeName The nodeName of the element | ||||||
|  |  * @param {Map} attrs Map of key-value pairs that are attributes of the node. | ||||||
|  |  * @return {Map | null} The allowed attributes or null, if the element should be | ||||||
|  |  *                      filtered. | ||||||
|  |  */ | ||||||
|  | export function defaultFilter (nodeName, attrs) { | ||||||
|  |   // TODO: implement basic filter that filters out dangerous properties!
 | ||||||
|  |   return attrs | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | export function filterDomAttributes (dom, filter) { | ||||||
|  |   const attrs = new Map() | ||||||
|  |   for (let i = dom.attributes.length - 1; i >= 0; i--) { | ||||||
|  |     const attr = dom.attributes[i] | ||||||
|  |     attrs.set(attr.name, attr.value) | ||||||
|  |   } | ||||||
|  |   return filter(dom.nodeName, attrs) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Applies a filter on a type. | ||||||
|  |  * | ||||||
|  |  * @param {Y} y The Yjs instance. | ||||||
|  |  * @param {DomBinding} binding The DOM binding instance that has the dom filter. | ||||||
|  |  * @param {YXmlElement | YXmlFragment } type The type to apply the filter to. | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function applyFilterOnType (y, binding, type) { | ||||||
|  |   if (isParentOf(binding.type, type)) { | ||||||
|  |     const nodeName = type.nodeName | ||||||
|  |     let attributes = new Map() | ||||||
|  |     if (type.getAttributes !== undefined) { | ||||||
|  |       let attrs = type.getAttributes() | ||||||
|  |       for (let key in attrs) { | ||||||
|  |         attributes.set(key, attrs[key]) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     const filteredAttributes = binding.filter(nodeName, new Map(attributes)) | ||||||
|  |     if (filteredAttributes === null) { | ||||||
|  |       type._delete(y) | ||||||
|  |     } else { | ||||||
|  |       // iterate original attributes
 | ||||||
|  |       attributes.forEach((value, key) => { | ||||||
|  |         // delete all attributes that are not in filteredAttributes
 | ||||||
|  |         if (filteredAttributes.has(key) === false) { | ||||||
|  |           type.removeAttribute(key) | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -5,32 +5,38 @@ import { getRelativePosition, fromRelativePosition } from '../../Util/relativePo | |||||||
| let browserSelection = null | let browserSelection = null | ||||||
| let relativeSelection = null | let relativeSelection = null | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
| export let beforeTransactionSelectionFixer | export let beforeTransactionSelectionFixer | ||||||
| if (typeof getSelection !== 'undefined') { | if (typeof getSelection !== 'undefined') { | ||||||
|   beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) { |   beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) { | ||||||
|     if (!remote) { |     if (!remote) { | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|     relativeSelection = { from: null, to: null, fromY: null, toY: null } |     relativeSelection = { from: null, to: null, fromY: null, toY: null } | ||||||
|     browserSelection = getSelection() |     browserSelection = getSelection() | ||||||
|     const anchorNode = browserSelection.anchorNode |     const anchorNode = browserSelection.anchorNode | ||||||
|     if (anchorNode !== null && anchorNode._yxml != null) { |     const anchorNodeType = domBinding.domToType.get(anchorNode) | ||||||
|       const yxml = anchorNode._yxml |     if (anchorNode !== null && anchorNodeType !== undefined) { | ||||||
|       relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset) |       relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset) | ||||||
|       relativeSelection.fromY = yxml._y |       relativeSelection.fromY = anchorNodeType._y | ||||||
|     } |     } | ||||||
|     const focusNode = browserSelection.focusNode |     const focusNode = browserSelection.focusNode | ||||||
|     if (focusNode !== null && focusNode._yxml != null) { |     const focusNodeType = domBinding.domToType.get(focusNode) | ||||||
|       const yxml = focusNode._yxml |     if (focusNode !== null && focusNodeType !== undefined) { | ||||||
|       relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset) |       relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset) | ||||||
|       relativeSelection.toY = yxml._y |       relativeSelection.toY = focusNodeType._y | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } else { | } else { | ||||||
|   beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {} |   beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function afterTransactionSelectionFixer (y, transaction, remote) { | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) { | ||||||
|   if (relativeSelection === null || !remote) { |   if (relativeSelection === null || !remote) { | ||||||
|     return |     return | ||||||
|   } |   } | ||||||
| @ -46,7 +52,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) { | |||||||
|   if (from !== null) { |   if (from !== null) { | ||||||
|     let sel = fromRelativePosition(fromY, from) |     let sel = fromRelativePosition(fromY, from) | ||||||
|     if (sel !== null) { |     if (sel !== null) { | ||||||
|       let node = sel.type.getDom() |       let node = domBinding.typeToDom.get(sel.type) | ||||||
|       let offset = sel.offset |       let offset = sel.offset | ||||||
|       if (node !== anchorNode || offset !== anchorOffset) { |       if (node !== anchorNode || offset !== anchorOffset) { | ||||||
|         anchorNode = node |         anchorNode = node | ||||||
| @ -58,7 +64,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) { | |||||||
|   if (to !== null) { |   if (to !== null) { | ||||||
|     let sel = fromRelativePosition(toY, to) |     let sel = fromRelativePosition(toY, to) | ||||||
|     if (sel !== null) { |     if (sel !== null) { | ||||||
|       let node = sel.type.getDom() |       let node = domBinding.typeToDom.get(sel.type) | ||||||
|       let offset = sel.offset |       let offset = sel.offset | ||||||
|       if (node !== focusNode || offset !== focusOffset) { |       if (node !== focusNode || offset !== focusOffset) { | ||||||
|         focusNode = node |         focusNode = node | ||||||
							
								
								
									
										63
									
								
								src/Bindings/DomBinding/typeObserver.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/Bindings/DomBinding/typeObserver.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,63 @@ | |||||||
|  | 
 | ||||||
|  | import YXmlText from '../../Types/YXml/YXmlText.js' | ||||||
|  | import YXmlHook from '../../Types/YXml/YXmlHook.js' | ||||||
|  | import { removeDomChildrenUntilElementFound } from './util.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export default function typeObserver (events) { | ||||||
|  |   this._mutualExclude(() => { | ||||||
|  |     events.forEach(event => { | ||||||
|  |       const yxml = event.target | ||||||
|  |       const dom = this.typeToDom.get(yxml) | ||||||
|  |       if (dom !== undefined && dom !== false) { | ||||||
|  |         if (yxml.constructor === YXmlText) { | ||||||
|  |           dom.nodeValue = yxml.toString() | ||||||
|  |           // TODO: use hasOwnProperty instead of === undefined check
 | ||||||
|  |         } else if (event.attributesChanged !== undefined) { | ||||||
|  |           // update attributes
 | ||||||
|  |           event.attributesChanged.forEach(attributeName => { | ||||||
|  |             const value = yxml.getAttribute(attributeName) | ||||||
|  |             if (value === undefined) { | ||||||
|  |               dom.removeAttribute(attributeName) | ||||||
|  |             } else { | ||||||
|  |               dom.setAttribute(attributeName, value) | ||||||
|  |             } | ||||||
|  |           }) | ||||||
|  |           /* | ||||||
|  |            * TODO: instead of hard-checking the types, it would be best to | ||||||
|  |            *       specify the type's features. E.g. | ||||||
|  |            *         - _yxmlHasAttributes | ||||||
|  |            *         - _yxmlHasChildren | ||||||
|  |            *       Furthermore, the features shouldn't be encoded in the types, | ||||||
|  |            *       only in the attributes (above) | ||||||
|  |            */ | ||||||
|  |           if (event.childListChanged && yxml.constructor !== YXmlHook) { | ||||||
|  |             let currentChild = dom.firstChild | ||||||
|  |             yxml.forEach(childType => { | ||||||
|  |               const childNode = this.typeToDom.get(childType) | ||||||
|  |               switch (childNode) { | ||||||
|  |                 case undefined: | ||||||
|  |                   // Does not exist. Create it.
 | ||||||
|  |                   const node = childType.toDom(this.opts.document, this.opts.hooks, this) | ||||||
|  |                   dom.insertBefore(node, currentChild) | ||||||
|  |                   break | ||||||
|  |                 case false: | ||||||
|  |                   // nop
 | ||||||
|  |                   break | ||||||
|  |                 default: | ||||||
|  |                   // Is already attached to the dom.
 | ||||||
|  |                   // Find it and remove all dom nodes in-between.
 | ||||||
|  |                   removeDomChildrenUntilElementFound(dom, currentChild, childNode) | ||||||
|  |                   currentChild = childNode.nextSibling | ||||||
|  |                   break | ||||||
|  |               } | ||||||
|  |             }) | ||||||
|  |             removeDomChildrenUntilElementFound(dom, currentChild, null) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | } | ||||||
							
								
								
									
										124
									
								
								src/Bindings/DomBinding/util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/Bindings/DomBinding/util.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | |||||||
|  | 
 | ||||||
|  | import domToType from './domToType.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Iterates items until an undeleted item is found. | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function iterateUntilUndeleted (item) { | ||||||
|  |   while (item !== null && item._deleted) { | ||||||
|  |     item = item._right | ||||||
|  |   } | ||||||
|  |   return item | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Removes an association (the information that a DOM element belongs to a | ||||||
|  |  * type). | ||||||
|  |  * | ||||||
|  |  * @param {DomBinding} domBinding The binding object | ||||||
|  |  * @param {Element} dom The dom that is to be associated with type | ||||||
|  |  * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | export function removeAssociation (domBinding, dom, type) { | ||||||
|  |   domBinding.domToType.delete(dom) | ||||||
|  |   domBinding.typeToDom.delete(type) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Creates an association (the information that a DOM element belongs to a | ||||||
|  |  * type). | ||||||
|  |  * | ||||||
|  |  * @param {DomBinding} domBinding The binding object | ||||||
|  |  * @param {Element} dom The dom that is to be associated with type | ||||||
|  |  * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | export function createAssociation (domBinding, dom, type) { | ||||||
|  |   if (domBinding !== undefined) { | ||||||
|  |     domBinding.domToType.set(dom, type) | ||||||
|  |     domBinding.typeToDom.set(type, dom) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * If oldDom is associated with a type, associate newDom with the type and | ||||||
|  |  * forget about oldDom. If oldDom is not associated with any type, nothing happens. | ||||||
|  |  * | ||||||
|  |  * @param {DomBinding} domBinding The binding object | ||||||
|  |  * @param {Element} oldDom The existing dom | ||||||
|  |  * @param {Element} newDom The new dom object | ||||||
|  |  */ | ||||||
|  | export function switchAssociation (domBinding, oldDom, newDom) { | ||||||
|  |   if (domBinding !== undefined) { | ||||||
|  |     const type = domBinding.domToType.get(oldDom) | ||||||
|  |     if (type !== undefined) { | ||||||
|  |       removeAssociation(domBinding, oldDom, type) | ||||||
|  |       createAssociation(domBinding, newDom, type) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Insert Dom Elements after one of the children of this YXmlFragment. | ||||||
|  |  * The Dom elements will be bound to a new YXmlElement and inserted at the | ||||||
|  |  * specified position. | ||||||
|  |  * | ||||||
|  |  * @param {YXmlElement} type The type in which to insert DOM elements. | ||||||
|  |  * @param {YXmlElement|null} prev The reference node. New YxmlElements are | ||||||
|  |  *                           inserted after this node. Set null to insert at | ||||||
|  |  *                           the beginning. | ||||||
|  |  * @param {Array<Element>} doms The Dom elements to insert. | ||||||
|  |  * @param {?Document} _document Optional. Provide the global document object. | ||||||
|  |  * @param {DomBinding} binding The dom binding | ||||||
|  |  * @return {Array<YXmlElement>} The YxmlElements that are inserted. | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function insertDomElementsAfter (type, prev, doms, _document, binding) { | ||||||
|  |   const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding) | ||||||
|  |   return type.insertAfter(prev, types) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function domsToTypes (doms, _document, hooks, filter, binding) { | ||||||
|  |   const types = [] | ||||||
|  |   for (let dom of doms) { | ||||||
|  |     const t = domToType(dom, _document, hooks, filter, binding) | ||||||
|  |     if (t !== false) { | ||||||
|  |       types.push(t) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return types | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) { | ||||||
|  |   let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding) | ||||||
|  |   if (insertedNodes.length > 0) { | ||||||
|  |     return insertedNodes[0] | ||||||
|  |   } else { | ||||||
|  |     return prevExpectedNode | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Remove children until `elem` is found. | ||||||
|  |  * | ||||||
|  |  * @param {Element} parent The parent of `elem` and `currentChild`. | ||||||
|  |  * @param {Element} currentChild Start removing elements with `currentChild`. If | ||||||
|  |  *                               `currentChild` is `elem` it won't be removed. | ||||||
|  |  * @param {Element|null} elem The elemnt to look for. | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function removeDomChildrenUntilElementFound (parent, currentChild, elem) { | ||||||
|  |   while (currentChild !== elem) { | ||||||
|  |     const del = currentChild | ||||||
|  |     currentChild = currentChild.nextSibling | ||||||
|  |     parent.removeChild(del) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								src/Bindings/QuillBinding/QuillBinding.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/Bindings/QuillBinding/QuillBinding.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | |||||||
|  | import Binding from '../Binding.js' | ||||||
|  | 
 | ||||||
|  | function typeObserver (event) { | ||||||
|  |   const quill = this.target | ||||||
|  |   // Force flush Quill changes.
 | ||||||
|  |   quill.update('yjs') | ||||||
|  |   this._mutualExclude(function () { | ||||||
|  |     // Apply computed delta.
 | ||||||
|  |     quill.updateContents(event.delta, 'yjs') | ||||||
|  |     // Force flush Quill changes. Ignore applied changes.
 | ||||||
|  |     quill.update('yjs') | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function quillObserver (delta) { | ||||||
|  |   this._mutualExclude(() => { | ||||||
|  |     this.type.applyDelta(delta.ops) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A Binding that binds a YText type to a Quill editor. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * const quill = new Quill(document.createElement('div')) | ||||||
|  |  * const type = y.define('quill', Y.Text) | ||||||
|  |  * const binding = new Y.QuillBinding(quill, type) | ||||||
|  |  * // Now modifications on the DOM will be reflected in the Type, and the other
 | ||||||
|  |  * // way around!
 | ||||||
|  |  */ | ||||||
|  | export default class QuillBinding extends Binding { | ||||||
|  |   /** | ||||||
|  |    * @param {YText} textType | ||||||
|  |    * @param {Quill} quill | ||||||
|  |    */ | ||||||
|  |   constructor (textType, quill) { | ||||||
|  |     // Binding handles textType as this.type and quill as this.target.
 | ||||||
|  |     super(textType, quill) | ||||||
|  |     // Set initial value.
 | ||||||
|  |     quill.setContents(textType.toDelta(), 'yjs') | ||||||
|  |     // Observers are handled by this class.
 | ||||||
|  |     this._typeObserver = typeObserver.bind(this) | ||||||
|  |     this._quillObserver = quillObserver.bind(this) | ||||||
|  |     textType.observe(this._typeObserver) | ||||||
|  |     quill.on('text-change', this._quillObserver) | ||||||
|  |   } | ||||||
|  |   destroy () { | ||||||
|  |     // Remove everything that is handled by this class.
 | ||||||
|  |     this.type.unobserve(this._typeObserver) | ||||||
|  |     this.target.off('text-change', this._quillObserver) | ||||||
|  |     super.destroy() | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,7 +1,7 @@ | |||||||
| 
 | 
 | ||||||
| import Binding from './Binding.js' | import Binding from '../Binding.js' | ||||||
| import simpleDiff from '../Util/simpleDiff.js' | import simpleDiff from '../../Util/simpleDiff.js' | ||||||
| import { getRelativePosition, fromRelativePosition } from '../Util/relativePosition.js' | import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js' | ||||||
| 
 | 
 | ||||||
| function typeObserver () { | function typeObserver () { | ||||||
|   this._mutualExclude(() => { |   this._mutualExclude(() => { | ||||||
| @ -24,6 +24,17 @@ function domObserver () { | |||||||
|   }) |   }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A binding that binds a YText to a dom textarea. | ||||||
|  |  * | ||||||
|  |  * This binding is automatically destroyed when its parent is deleted. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  *   const textare = document.createElement('textarea') | ||||||
|  |  *   const type = y.define('textarea', Y.Text) | ||||||
|  |  *   const binding = new Y.QuillBinding(type, textarea) | ||||||
|  |  * | ||||||
|  |  */ | ||||||
| export default class TextareaBinding extends Binding { | export default class TextareaBinding extends Binding { | ||||||
|   constructor (textType, domTextarea) { |   constructor (textType, domTextarea) { | ||||||
|     // Binding handles textType as this.type and domTextarea as this.target
 |     // Binding handles textType as this.type and domTextarea as this.target
 | ||||||
| @ -1,5 +1,5 @@ | |||||||
| import BinaryEncoder from './Binary/Encoder.js' | import BinaryEncoder from './Util/Binary/Encoder.js' | ||||||
| import BinaryDecoder from './Binary/Decoder.js' | import BinaryDecoder from './Util/Binary/Decoder.js' | ||||||
| 
 | 
 | ||||||
| import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js' | import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js' | ||||||
| import { readSyncStep2 } from './MessageHandler/syncStep2.js' | import { readSyncStep2 } from './MessageHandler/syncStep2.js' | ||||||
| @ -7,6 +7,8 @@ import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs. | |||||||
| 
 | 
 | ||||||
| import debug from 'debug' | import debug from 'debug' | ||||||
| 
 | 
 | ||||||
|  | // TODO: rename Connector
 | ||||||
|  | 
 | ||||||
| export default class AbstractConnector { | export default class AbstractConnector { | ||||||
|   constructor (y, opts) { |   constructor (y, opts) { | ||||||
|     this.y = y |     this.y = y | ||||||
|  | |||||||
| @ -1,8 +1,15 @@ | |||||||
|  | 
 | ||||||
| import { writeStructs } from './syncStep1.js' | import { writeStructs } from './syncStep1.js' | ||||||
| import { integrateRemoteStructs } from './integrateRemoteStructs.js' | import { integrateRemoteStructs } from './integrateRemoteStructs.js' | ||||||
| import { readDeleteSet, writeDeleteSet } from './deleteSet.js' | import { readDeleteSet, writeDeleteSet } from './deleteSet.js' | ||||||
| import BinaryEncoder from '../Binary/Encoder.js' | import BinaryEncoder from '../Util/Binary/Encoder.js' | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Read the Decoder and fill the Yjs instance with data in the decoder. | ||||||
|  |  * | ||||||
|  |  * @param {Y} y The Yjs instance | ||||||
|  |  * @param {BinaryDecoder} decoder The BinaryDecoder to read from. | ||||||
|  |  */ | ||||||
| export function fromBinary (y, decoder) { | export function fromBinary (y, decoder) { | ||||||
|   y.transact(function () { |   y.transact(function () { | ||||||
|     integrateRemoteStructs(y, decoder) |     integrateRemoteStructs(y, decoder) | ||||||
| @ -10,6 +17,13 @@ export function fromBinary (y, decoder) { | |||||||
|   }) |   }) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Encode the Yjs model to binary format. | ||||||
|  |  * | ||||||
|  |  * @param {Y} y The Yjs instance | ||||||
|  |  * @return {BinaryEncoder} The encoder instance that can be transformed | ||||||
|  |  *                         to ArrayBuffer or Buffer. | ||||||
|  |  */ | ||||||
| export function toBinary (y) { | export function toBinary (y) { | ||||||
|   let encoder = new BinaryEncoder() |   let encoder = new BinaryEncoder() | ||||||
|   writeStructs(y, encoder, new Map()) |   writeStructs(y, encoder, new Map()) | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { deleteItemRange } from '../Struct/Delete.js' | import { deleteItemRange } from '../Struct/Delete.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| 
 | 
 | ||||||
| export function stringifyDeleteSet (y, decoder, strBuilder) { | export function stringifyDeleteSet (y, decoder, strBuilder) { | ||||||
|   let dsLength = decoder.readUint32() |   let dsLength = decoder.readUint32() | ||||||
| @ -92,7 +92,7 @@ export function readDeleteSet (y, decoder) { | |||||||
|             // delete maximum the len of d
 |             // delete maximum the len of d
 | ||||||
|             // else delete as much as possible
 |             // else delete as much as possible
 | ||||||
|             diff = Math.min(n._id.clock - d[0], d[1]) |             diff = Math.min(n._id.clock - d[0], d[1]) | ||||||
|             // deleteItemRange(y, user, d[0], diff)
 |             // deleteItemRange(y, user, d[0], diff, true)
 | ||||||
|             deletions.push([user, d[0], diff]) |             deletions.push([user, d[0], diff]) | ||||||
|           } else { |           } else { | ||||||
|             // 3)
 |             // 3)
 | ||||||
| @ -100,7 +100,7 @@ export function readDeleteSet (y, decoder) { | |||||||
|             if (d[2] && !n.gc) { |             if (d[2] && !n.gc) { | ||||||
|               // d marks as gc'd but n does not
 |               // d marks as gc'd but n does not
 | ||||||
|               // then delete either way
 |               // then delete either way
 | ||||||
|               // deleteItemRange(y, user, d[0], Math.min(diff, d[1]))
 |               // deleteItemRange(y, user, d[0], Math.min(diff, d[1]), true)
 | ||||||
|               deletions.push([user, d[0], Math.min(diff, d[1])]) |               deletions.push([user, d[0], Math.min(diff, d[1])]) | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| @ -117,12 +117,12 @@ export function readDeleteSet (y, decoder) { | |||||||
|       // Adapt the Tree implementation to support delete while iterating
 |       // Adapt the Tree implementation to support delete while iterating
 | ||||||
|       for (let i = deletions.length - 1; i >= 0; i--) { |       for (let i = deletions.length - 1; i >= 0; i--) { | ||||||
|         const del = deletions[i] |         const del = deletions[i] | ||||||
|         deleteItemRange(y, del[0], del[1], del[2]) |         deleteItemRange(y, del[0], del[1], del[2], true) | ||||||
|       } |       } | ||||||
|       // for the rest.. just apply it
 |       // for the rest.. just apply it
 | ||||||
|       for (; pos < dv.length; pos++) { |       for (; pos < dv.length; pos++) { | ||||||
|         d = dv[pos] |         d = dv[pos] | ||||||
|         deleteItemRange(y, user, d[0], d[1]) |         deleteItemRange(y, user, d[0], d[1], true) | ||||||
|         // deletions.push([user, d[0], d[1], d[2]])
 |         // deletions.push([user, d[0], d[1], d[2]])
 | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { getStruct } from '../Util/structReferences.js' | import { getStruct } from '../Util/structReferences.js' | ||||||
| import BinaryDecoder from '../Binary/Decoder.js' | import BinaryDecoder from '../Util/Binary/Decoder.js' | ||||||
| import { logID } from './messageToString.js' | import { logID } from './messageToString.js' | ||||||
|  | import GC from '../Struct/GC.js' | ||||||
| 
 | 
 | ||||||
| class MissingEntry { | class MissingEntry { | ||||||
|   constructor (decoder, missing, struct) { |   constructor (decoder, missing, struct) { | ||||||
| @ -11,6 +12,7 @@ class MissingEntry { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  * @private | ||||||
|  * Integrate remote struct |  * Integrate remote struct | ||||||
|  * When a remote struct is integrated, other structs might be ready to ready to |  * When a remote struct is integrated, other structs might be ready to ready to | ||||||
|  * integrate. |  * integrate. | ||||||
| @ -23,7 +25,14 @@ function _integrateRemoteStructHelper (y, struct) { | |||||||
|     if (y.ss.getState(id.user) > id.clock) { |     if (y.ss.getState(id.user) > id.clock) { | ||||||
|       return |       return | ||||||
|     } |     } | ||||||
|  |     if (struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) { | ||||||
|  |       // Is either a GC or Item with an undeleted parent
 | ||||||
|  |       // save to integrate
 | ||||||
|       struct._integrate(y) |       struct._integrate(y) | ||||||
|  |     } else { | ||||||
|  |       // Is an Item. parent was deleted.
 | ||||||
|  |       struct._gc(y) | ||||||
|  |     } | ||||||
|     let msu = y._missingStructs.get(id.user) |     let msu = y._missingStructs.get(id.user) | ||||||
|     if (msu != null) { |     if (msu != null) { | ||||||
|       let clock = id.clock |       let clock = id.clock | ||||||
|  | |||||||
| @ -1,9 +1,9 @@ | |||||||
| import BinaryDecoder from '../Binary/Decoder.js' | import BinaryDecoder from '../Util/Binary/Decoder.js' | ||||||
| import { stringifyStructs } from './integrateRemoteStructs.js' | import { stringifyStructs } from './integrateRemoteStructs.js' | ||||||
| import { stringifySyncStep1 } from './syncStep1.js' | import { stringifySyncStep1 } from './syncStep1.js' | ||||||
| import { stringifySyncStep2 } from './syncStep2.js' | import { stringifySyncStep2 } from './syncStep2.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| import RootID from '../Util/RootID.js' | import RootID from '../Util/ID/RootID.js' | ||||||
| import Y from '../Y.js' | import Y from '../Y.js' | ||||||
| 
 | 
 | ||||||
| export function messageToString ([y, buffer]) { | export function messageToString ([y, buffer]) { | ||||||
| @ -46,3 +46,20 @@ export function logID (id) { | |||||||
|     throw new Error('This is not a valid ID!') |     throw new Error('This is not a valid ID!') | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Helper utility to convert an item to a readable format. | ||||||
|  |  * | ||||||
|  |  * @param {String} name The name of the item class (YText, ItemString, ..). | ||||||
|  |  * @param {Item} item The item instance. | ||||||
|  |  * @param {String} [append] Additional information to append to the returned | ||||||
|  |  *                          string. | ||||||
|  |  * @return {String} A readable string that represents the item object. | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function logItemHelper (name, item, append) { | ||||||
|  |   const left = item._left !== null ? item._left._lastId : null | ||||||
|  |   const origin = item._origin !== null ? item._origin._lastId : null | ||||||
|  |   return `${name}(id:${logID(item._id)},start:${logID(item._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})` | ||||||
|  | } | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
| import BinaryEncoder from '../Binary/Encoder.js' | import BinaryEncoder from '../Util/Binary/Encoder.js' | ||||||
| import { readStateSet, writeStateSet } from './stateSet.js' | import { readStateSet, writeStateSet } from './stateSet.js' | ||||||
| import { writeDeleteSet } from './deleteSet.js' | import { writeDeleteSet } from './deleteSet.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| import { RootFakeUserID } from '../Util/RootID.js' | import { RootFakeUserID } from '../Util/ID/RootID.js' | ||||||
| 
 | 
 | ||||||
| export function stringifySyncStep1 (y, decoder, strBuilder) { | export function stringifySyncStep1 (y, decoder, strBuilder) { | ||||||
|   let auth = decoder.readVarString() |   let auth = decoder.readVarString() | ||||||
| @ -30,6 +30,11 @@ export function sendSyncStep1 (connector, syncUser) { | |||||||
|   connector.send(syncUser, encoder.createBuffer()) |   connector.send(syncUser, encoder.createBuffer()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  * Write all Items that are not not included in ss to | ||||||
|  |  * the encoder object. | ||||||
|  |  */ | ||||||
| export function writeStructs (y, encoder, ss) { | export function writeStructs (y, encoder, ss) { | ||||||
|   const lenPos = encoder.pos |   const lenPos = encoder.pos | ||||||
|   encoder.writeUint32(0) |   encoder.writeUint32(0) | ||||||
| @ -37,7 +42,15 @@ export function writeStructs (y, encoder, ss) { | |||||||
|   for (let user of y.ss.state.keys()) { |   for (let user of y.ss.state.keys()) { | ||||||
|     let clock = ss.get(user) || 0 |     let clock = ss.get(user) || 0 | ||||||
|     if (user !== RootFakeUserID) { |     if (user !== RootFakeUserID) { | ||||||
|       y.os.iterate(new ID(user, clock), new ID(user, Number.MAX_VALUE), function (struct) { |       const minBound = new ID(user, clock) | ||||||
|  |       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) | ||||||
|  |         struct._toBinary(encoder) | ||||||
|  |         len++ | ||||||
|  |       } | ||||||
|  |       y.os.iterate(minBound, new ID(user, Number.MAX_VALUE), function (struct) { | ||||||
|         struct._toBinary(encoder) |         struct._toBinary(encoder) | ||||||
|         len++ |         len++ | ||||||
|       }) |       }) | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import BinaryEncoder from './Binary/Encoder.js' | import BinaryEncoder from './Util/Binary/Encoder.js' | ||||||
| import BinaryDecoder from './Binary/Decoder.js' | import BinaryDecoder from './Util/Binary/Decoder.js' | ||||||
| import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' | import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' | ||||||
| import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js' | import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js' | ||||||
| import { createMutualExclude } from './Util/mutualExclude.js' | import { createMutualExclude } from './Util/mutualExclude.js' | ||||||
| @ -13,6 +13,9 @@ function getFreshCnf () { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Abstract persistence class. | ||||||
|  |  */ | ||||||
| export default class AbstractPersistence { | export default class AbstractPersistence { | ||||||
|   constructor (opts) { |   constructor (opts) { | ||||||
|     this.opts = opts |     this.opts = opts | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
|  | 
 | ||||||
| import Tree from '../Util/Tree.js' | import Tree from '../Util/Tree.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| 
 | 
 | ||||||
| class DSNode { | class DSNode { | ||||||
|   constructor (id, len, gc) { |   constructor (id, len, gc) { | ||||||
| @ -29,97 +30,61 @@ export default class DeleteStore extends Tree { | |||||||
|     var n = this.findWithUpperBound(id) |     var n = this.findWithUpperBound(id) | ||||||
|     return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len |     return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len | ||||||
|   } |   } | ||||||
|   /* |   mark (id, length, gc) { | ||||||
|    * Mark an operation as deleted. returns the deleted node |     if (length === 0) return | ||||||
|    */ |     // Step 1. Unmark range
 | ||||||
|  |     const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1)) | ||||||
|  |     // Resize left DSNode if necessary
 | ||||||
|  |     if (leftD !== null && leftD._id.user === id.user) { | ||||||
|  |       if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) { | ||||||
|  |         // node is overlapping. need to resize
 | ||||||
|  |         if (id.clock + length < leftD._id.clock + leftD.len) { | ||||||
|  |           // overlaps new mark range and some more
 | ||||||
|  |           // create another DSNode to the right of new mark
 | ||||||
|  |           this.put(new DSNode(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc)) | ||||||
|  |         } | ||||||
|  |         // resize left DSNode
 | ||||||
|  |         leftD.len = id.clock - leftD._id.clock | ||||||
|  |       } // Otherwise there is no overlapping
 | ||||||
|  |     } | ||||||
|  |     // Resize right DSNode if necessary
 | ||||||
|  |     const upper = new ID(id.user, id.clock + length - 1) | ||||||
|  |     const rightD = this.findWithUpperBound(upper) | ||||||
|  |     if (rightD !== null && rightD._id.user === id.user) { | ||||||
|  |       if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
 | ||||||
|  |         const d = id.clock + length - rightD._id.clock | ||||||
|  |         rightD._id = new ID(rightD._id.user, rightD._id.clock + d) | ||||||
|  |         rightD.len -= d | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     // Now we only have to delete all inner marks
 | ||||||
|  |     const deleteNodeIds = [] | ||||||
|  |     this.iterate(id, upper, m => { | ||||||
|  |       deleteNodeIds.push(m._id) | ||||||
|  |     }) | ||||||
|  |     for (let i = deleteNodeIds.length - 1; i >= 0; i--) { | ||||||
|  |       this.delete(deleteNodeIds[i]) | ||||||
|  |     } | ||||||
|  |     let newMark = new DSNode(id, length, gc) | ||||||
|  |     // Step 2. Check if we can extend left or right
 | ||||||
|  |     if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) { | ||||||
|  |       // We can extend left
 | ||||||
|  |       leftD.len += length | ||||||
|  |       newMark = leftD | ||||||
|  |     } | ||||||
|  |     const rightNext = this.find(new ID(id.user, id.clock + length)) | ||||||
|  |     if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) { | ||||||
|  |       // We can merge newMark and rightNext
 | ||||||
|  |       newMark.len += rightNext.len | ||||||
|  |       this.delete(rightNext._id) | ||||||
|  |     } | ||||||
|  |     if (leftD !== newMark) { | ||||||
|  |       // only put if we didn't extend left
 | ||||||
|  |       this.put(newMark) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // TODO: exchange markDeleted for mark()
 | ||||||
|   markDeleted (id, length) { |   markDeleted (id, length) { | ||||||
|     if (length == null) { |     this.mark(id, length, false) | ||||||
|       throw new Error('length must be defined') |  | ||||||
|     } |  | ||||||
|     var n = this.findWithUpperBound(id) |  | ||||||
|     if (n != null && n._id.user === id.user) { |  | ||||||
|       if (n._id.clock <= id.clock && id.clock <= n._id.clock + n.len) { |  | ||||||
|         // id is in n's range
 |  | ||||||
|         var diff = id.clock + length - (n._id.clock + n.len) // overlapping right
 |  | ||||||
|         if (diff > 0) { |  | ||||||
|           // id+length overlaps n
 |  | ||||||
|           if (!n.gc) { |  | ||||||
|             n.len += diff |  | ||||||
|           } else { |  | ||||||
|             diff = n._id.clock + n.len - id.clock // overlapping left (id till n.end)
 |  | ||||||
|             if (diff < length) { |  | ||||||
|               // a partial deletion
 |  | ||||||
|               let nId = id.clone() |  | ||||||
|               nId.clock += diff |  | ||||||
|               n = new DSNode(nId, length - diff, false) |  | ||||||
|               this.put(n) |  | ||||||
|             } else { |  | ||||||
|               // already gc'd
 |  | ||||||
|               throw new Error( |  | ||||||
|                 'DS reached an inconsistent state. Please report this issue!' |  | ||||||
|               ) |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } else { |  | ||||||
|           // no overlapping, already deleted
 |  | ||||||
|           return n |  | ||||||
|         } |  | ||||||
|       } else { |  | ||||||
|         // cannot extend left (there is no left!)
 |  | ||||||
|         n = new DSNode(id, length, false) |  | ||||||
|         this.put(n) // TODO: you double-put !!
 |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       // cannot extend left
 |  | ||||||
|       n = new DSNode(id, length, false) |  | ||||||
|       this.put(n) |  | ||||||
|     } |  | ||||||
|     // can extend right?
 |  | ||||||
|     var next = this.findNext(n._id) |  | ||||||
|     if ( |  | ||||||
|       next != null && |  | ||||||
|       n._id.user === next._id.user && |  | ||||||
|       n._id.clock + n.len >= next._id.clock |  | ||||||
|     ) { |  | ||||||
|       diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
 |  | ||||||
|       while (diff >= 0) { |  | ||||||
|         // n overlaps with next
 |  | ||||||
|         if (next.gc) { |  | ||||||
|           // gc is stronger, so reduce length of n
 |  | ||||||
|           n.len -= diff |  | ||||||
|           if (diff >= next.len) { |  | ||||||
|             // delete the missing range after next
 |  | ||||||
|             diff = diff - next.len // missing range after next
 |  | ||||||
|             if (diff > 0) { |  | ||||||
|               this.put(n) // unneccessary? TODO!
 |  | ||||||
|               this.markDeleted(new ID(next._id.user, next._id.clock + next.len), diff) |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|           break |  | ||||||
|         } else { |  | ||||||
|           // we can extend n with next
 |  | ||||||
|           if (diff > next.len) { |  | ||||||
|             // n is even longer than next
 |  | ||||||
|             // get next.next, and try to extend it
 |  | ||||||
|             var _next = this.findNext(next._id) |  | ||||||
|             this.delete(next._id) |  | ||||||
|             if (_next == null || n._id.user !== _next._id.user) { |  | ||||||
|               break |  | ||||||
|             } else { |  | ||||||
|               next = _next |  | ||||||
|               diff = n._id.clock + n.len - next._id.clock // from next.start to n.end
 |  | ||||||
|               // continue!
 |  | ||||||
|             } |  | ||||||
|           } else { |  | ||||||
|             // n just partially overlaps with next. extend n, delete next, and break this loop
 |  | ||||||
|             n.len += next.len - diff |  | ||||||
|             this.delete(next._id) |  | ||||||
|             break |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this.put(n) |  | ||||||
|     return n |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,8 @@ | |||||||
| import Tree from '../Util/Tree.js' | import Tree from '../Util/Tree.js' | ||||||
| import RootID from '../Util/RootID.js' | import RootID from '../Util/ID/RootID.js' | ||||||
| import { getStruct } from '../Util/structReferences.js' | import { getStruct } from '../Util/structReferences.js' | ||||||
| import { logID } from '../MessageHandler/messageToString.js' | import { logID } from '../MessageHandler/messageToString.js' | ||||||
|  | import GC from '../Struct/GC.js' | ||||||
| 
 | 
 | ||||||
| export default class OperationStore extends Tree { | export default class OperationStore extends Tree { | ||||||
|   constructor (y) { |   constructor (y) { | ||||||
| @ -11,6 +12,13 @@ export default class OperationStore extends Tree { | |||||||
|   logTable () { |   logTable () { | ||||||
|     const items = [] |     const items = [] | ||||||
|     this.iterate(null, null, function (item) { |     this.iterate(null, null, function (item) { | ||||||
|  |       if (item.constructor === GC) { | ||||||
|  |         items.push({ | ||||||
|  |           id: logID(item), | ||||||
|  |           content: item._length, | ||||||
|  |           deleted: 'GC' | ||||||
|  |         }) | ||||||
|  |       } else { | ||||||
|         items.push({ |         items.push({ | ||||||
|           id: logID(item), |           id: logID(item), | ||||||
|           origin: logID(item._origin === null ? null : item._origin._lastId), |           origin: logID(item._origin === null ? null : item._origin._lastId), | ||||||
| @ -22,6 +30,7 @@ export default class OperationStore extends Tree { | |||||||
|           deleted: item._deleted, |           deleted: item._deleted, | ||||||
|           content: JSON.stringify(item._content) |           content: JSON.stringify(item._content) | ||||||
|         }) |         }) | ||||||
|  |       } | ||||||
|     }) |     }) | ||||||
|     console.table(items) |     console.table(items) | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| 
 | 
 | ||||||
| export default class StateStore { | export default class StateStore { | ||||||
|   constructor (y) { |   constructor (y) { | ||||||
|  | |||||||
| @ -1,29 +1,30 @@ | |||||||
| import { getReference } from '../Util/structReferences.js' | import { getStructReference } from '../Util/structReferences.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| import { logID } from '../MessageHandler/messageToString.js' | import { logID } from '../MessageHandler/messageToString.js' | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  * @private | ||||||
|  * Delete all items in an ID-range |  * Delete all items in an ID-range | ||||||
|  * TODO: implement getItemCleanStartNode for better performance (only one lookup) |  * TODO: implement getItemCleanStartNode for better performance (only one lookup) | ||||||
|  */ |  */ | ||||||
| export function deleteItemRange (y, user, clock, range) { | export function deleteItemRange (y, user, clock, range, gcChildren) { | ||||||
|   const createDelete = y.connector !== null && y.connector._forwardAppliedStructs |   const createDelete = y.connector !== null && y.connector._forwardAppliedStructs | ||||||
|   let item = y.os.getItemCleanStart(new ID(user, clock)) |   let item = y.os.getItemCleanStart(new ID(user, clock)) | ||||||
|   if (item !== null) { |   if (item !== null) { | ||||||
|     if (!item._deleted) { |     if (!item._deleted) { | ||||||
|       item._splitAt(y, range) |       item._splitAt(y, range) | ||||||
|       item._delete(y, createDelete) |       item._delete(y, createDelete, true) | ||||||
|     } |     } | ||||||
|     let itemLen = item._length |     let itemLen = item._length | ||||||
|     range -= itemLen |     range -= itemLen | ||||||
|     clock += itemLen |     clock += itemLen | ||||||
|     if (range > 0) { |     if (range > 0) { | ||||||
|       let node = y.os.findNode(new ID(user, clock)) |       let node = y.os.findNode(new ID(user, clock)) | ||||||
|       while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) { |       while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) { | ||||||
|         const nodeVal = node.val |         const nodeVal = node.val | ||||||
|         if (!nodeVal._deleted) { |         if (!nodeVal._deleted) { | ||||||
|           nodeVal._splitAt(y, range) |           nodeVal._splitAt(y, range) | ||||||
|           nodeVal._delete(y, createDelete) |           nodeVal._delete(y, createDelete, gcChildren) | ||||||
|         } |         } | ||||||
|         const nodeLen = nodeVal._length |         const nodeLen = nodeVal._length | ||||||
|         range -= nodeLen |         range -= nodeLen | ||||||
| @ -35,13 +36,26 @@ export function deleteItemRange (y, user, clock, range) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Delete is not a real struct. It will not be saved in OS |  * @private | ||||||
|  |  * A Delete change is not a real Item, but it provides the same interface as an | ||||||
|  |  * Item. The only difference is that it will not be saved in the ItemStore | ||||||
|  |  * (OperationStore), but instead it is safed in the DeleteStore. | ||||||
|  */ |  */ | ||||||
| export default class Delete { | export default class Delete { | ||||||
|   constructor () { |   constructor () { | ||||||
|     this._target = null |     this._target = null | ||||||
|     this._length = null |     this._length = null | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Read the next Item in a Decoder and fill this Item with the read data. | ||||||
|  |    * | ||||||
|  |    * This is called when data is received from a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance that this Item belongs to. | ||||||
|  |    * @param {BinaryDecoder} decoder The decoder object to read data from. | ||||||
|  |    */ | ||||||
|   _fromBinary (y, decoder) { |   _fromBinary (y, decoder) { | ||||||
|     // TODO: set target, and add it to missing if not found
 |     // TODO: set target, and add it to missing if not found
 | ||||||
|     // There is an edge case in p2p networks!
 |     // There is an edge case in p2p networks!
 | ||||||
| @ -54,22 +68,39 @@ export default class Delete { | |||||||
|       return [] |       return [] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Transform the properties of this type to binary and write it to an | ||||||
|  |    * BinaryEncoder. | ||||||
|  |    * | ||||||
|  |    * This is called when this Item is sent to a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {BinaryEncoder} encoder The encoder to write data to. | ||||||
|  |    */ | ||||||
|   _toBinary (encoder) { |   _toBinary (encoder) { | ||||||
|     encoder.writeUint8(getReference(this.constructor)) |     encoder.writeUint8(getStructReference(this.constructor)) | ||||||
|     encoder.writeID(this._targetID) |     encoder.writeID(this._targetID) | ||||||
|     encoder.writeVarUint(this._length) |     encoder.writeVarUint(this._length) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * - If created remotely (a remote user deleted something), |    * @private | ||||||
|  |    * Integrates this Item into the shared structure. | ||||||
|  |    * | ||||||
|  |    * This method actually applies the change to the Yjs instance. In the case of | ||||||
|  |    * Delete it marks the delete target as deleted. | ||||||
|  |    * | ||||||
|  |    * * If created remotely (a remote user deleted something), | ||||||
|    *   this Delete is applied to all structs in id-range. |    *   this Delete is applied to all structs in id-range. | ||||||
|    * - If created lokally (e.g. when y-array deletes a range of elements), |    * * If created lokally (e.g. when y-array deletes a range of elements), | ||||||
|    *   this struct is broadcasted only (it is already executed) |    *   this struct is broadcasted only (it is already executed) | ||||||
|    */ |    */ | ||||||
|   _integrate (y, locallyCreated = false) { |   _integrate (y, locallyCreated = false) { | ||||||
|     if (!locallyCreated) { |     if (!locallyCreated) { | ||||||
|       // from remote
 |       // from remote
 | ||||||
|       const id = this._targetID |       const id = this._targetID | ||||||
|       deleteItemRange(y, id.user, id.clock, this._length) |       deleteItemRange(y, id.user, id.clock, this._length, false) | ||||||
|     } else if (y.connector !== null) { |     } else if (y.connector !== null) { | ||||||
|       // from local
 |       // from local
 | ||||||
|       y.connector.broadcastStruct(this) |       y.connector.broadcastStruct(this) | ||||||
| @ -78,6 +109,13 @@ export default class Delete { | |||||||
|       y.persistence.saveStruct(y, this) |       y.persistence.saveStruct(y, this) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _logString () { |   _logString () { | ||||||
|     return `Delete - target: ${logID(this._targetID)}, len: ${this._length}` |     return `Delete - target: ${logID(this._targetID)}, len: ${this._length}` | ||||||
|   } |   } | ||||||
|  | |||||||
							
								
								
									
										94
									
								
								src/Struct/GC.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/Struct/GC.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,94 @@ | |||||||
|  | import { getStructReference } from '../Util/structReferences.js' | ||||||
|  | import { RootFakeUserID } from '../Util/ID/RootID.js' | ||||||
|  | import ID from '../Util/ID/ID.js' | ||||||
|  | 
 | ||||||
|  | // TODO should have the same base class as Item
 | ||||||
|  | export default class GC { | ||||||
|  |   constructor () { | ||||||
|  |     this._id = null | ||||||
|  |     this._length = 0 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get _deleted () { | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _integrate (y) { | ||||||
|  |     const id = this._id | ||||||
|  |     const userState = y.ss.getState(id.user) | ||||||
|  |     if (id.clock === userState) { | ||||||
|  |       y.ss.setState(id.user, id.clock + this._length) | ||||||
|  |     } | ||||||
|  |     y.ds.mark(this._id, this._length, true) | ||||||
|  |     let n = y.os.put(this) | ||||||
|  |     const prev = n.prev().val | ||||||
|  |     if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) { | ||||||
|  |       // TODO: do merging for all items!
 | ||||||
|  |       prev._length += n.val._length | ||||||
|  |       y.os.delete(n.val._id) | ||||||
|  |       n = prev | ||||||
|  |     } | ||||||
|  |     if (n.val) { | ||||||
|  |       n = n.val | ||||||
|  |     } | ||||||
|  |     const next = y.os.findNext(n._id) | ||||||
|  |     if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) { | ||||||
|  |       n._length += next._length | ||||||
|  |       y.os.delete(next._id) | ||||||
|  |     } | ||||||
|  |     if (id.user !== RootFakeUserID) { | ||||||
|  |       if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) { | ||||||
|  |         y.connector.broadcastStruct(this) | ||||||
|  |       } | ||||||
|  |       if (y.persistence !== null) { | ||||||
|  |         y.persistence.saveStruct(y, this) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform the properties of this type to binary and write it to an | ||||||
|  |    * BinaryEncoder. | ||||||
|  |    * | ||||||
|  |    * This is called when this Item is sent to a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {BinaryEncoder} encoder The encoder to write data to. | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _toBinary (encoder) { | ||||||
|  |     encoder.writeUint8(getStructReference(this.constructor)) | ||||||
|  |     encoder.writeID(this._id) | ||||||
|  |     encoder.writeVarUint(this._length) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Read the next Item in a Decoder and fill this Item with the read data. | ||||||
|  |    * | ||||||
|  |    * This is called when data is received from a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance that this Item belongs to. | ||||||
|  |    * @param {BinaryDecoder} decoder The decoder object to read data from. | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _fromBinary (y, decoder) { | ||||||
|  |     const id = decoder.readID() | ||||||
|  |     this._id = id | ||||||
|  |     this._length = decoder.readVarUint() | ||||||
|  |     const missing = [] | ||||||
|  |     if (y.ss.getState(id.user) < id.clock) { | ||||||
|  |       missing.push(new ID(id.user, id.clock - 1)) | ||||||
|  |     } | ||||||
|  |     return missing | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _splitAt () { | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _clonePartial (diff) { | ||||||
|  |     const gc = new GC() | ||||||
|  |     gc._id = new ID(this._id.user, this._id.clock + diff) | ||||||
|  |     gc._length = this._length - diff | ||||||
|  |     return gc | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,15 +1,17 @@ | |||||||
| import { getReference } from '../Util/structReferences.js' | import { getStructReference } from '../Util/structReferences.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| import { RootFakeUserID } from '../Util/RootID.js' | import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.js' | ||||||
| import Delete from './Delete.js' | import Delete from './Delete.js' | ||||||
| import { transactionTypeChanged } from '../Transaction.js' | import { transactionTypeChanged } from '../Transaction.js' | ||||||
|  | import GC from './GC.js' | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Helper utility to split an Item (see _splitAt) |  * @private | ||||||
|  * - copy all properties from a to b |  * Helper utility to split an Item (see {@link Item#_splitAt}) | ||||||
|  * - connect a to b |  * - copies all properties from a to b | ||||||
|  |  * - connects a to b | ||||||
|  * - assigns the correct _id |  * - assigns the correct _id | ||||||
|  * - save b to os |  * - saves b to os | ||||||
|  */ |  */ | ||||||
| export function splitHelper (y, a, b, diff) { | export function splitHelper (y, a, b, diff) { | ||||||
|   const aID = a._id |   const aID = a._id | ||||||
| @ -39,28 +41,84 @@ export function splitHelper (y, a, b, diff) { | |||||||
|     o = o._right |     o = o._right | ||||||
|   } |   } | ||||||
|   y.os.put(b) |   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) | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Abstract class that represents any content. | ||||||
|  |  */ | ||||||
| export default class Item { | export default class Item { | ||||||
|   constructor () { |   constructor () { | ||||||
|  |     /** | ||||||
|  |      * The uniqe identifier of this type. | ||||||
|  |      * @type {ID} | ||||||
|  |      */ | ||||||
|     this._id = null |     this._id = null | ||||||
|  |     /** | ||||||
|  |      * The item that was originally to the left of this item. | ||||||
|  |      * @type {Item} | ||||||
|  |      */ | ||||||
|     this._origin = null |     this._origin = null | ||||||
|  |     /** | ||||||
|  |      * The item that is currently to the left of this item. | ||||||
|  |      * @type {Item} | ||||||
|  |      */ | ||||||
|     this._left = null |     this._left = null | ||||||
|  |     /** | ||||||
|  |      * The item that is currently to the right of this item. | ||||||
|  |      * @type {Item} | ||||||
|  |      */ | ||||||
|     this._right = null |     this._right = null | ||||||
|  |     /** | ||||||
|  |      * The item that was originally to the right of this item. | ||||||
|  |      * @type {Item} | ||||||
|  |      */ | ||||||
|     this._right_origin = null |     this._right_origin = null | ||||||
|  |     /** | ||||||
|  |      * The parent type. | ||||||
|  |      * @type {Y|YType} | ||||||
|  |      */ | ||||||
|     this._parent = null |     this._parent = null | ||||||
|  |     /** | ||||||
|  |      * If the parent refers to this item with some kind of key (e.g. YMap, the | ||||||
|  |      * key is specified here. The key is then used to refer to the list in which | ||||||
|  |      * to insert this item. If `parentSub = null` type._start is the list in | ||||||
|  |      * which to insert to. Otherwise it is `parent._start`. | ||||||
|  |      * @type {String} | ||||||
|  |      */ | ||||||
|     this._parentSub = null |     this._parentSub = null | ||||||
|  |     /** | ||||||
|  |      * Whether this item was deleted or not. | ||||||
|  |      * @type {Boolean} | ||||||
|  |      */ | ||||||
|     this._deleted = false |     this._deleted = false | ||||||
|  |     /** | ||||||
|  |      * If this type's effect is reundone this type refers to the type that undid | ||||||
|  |      * this operation. | ||||||
|  |      * @type {Item} | ||||||
|  |      */ | ||||||
|     this._redone = null |     this._redone = null | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Create a operation with the same effect (without position effect) |    * Creates an Item with the same effect as this Item (without position effect) | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|    */ |    */ | ||||||
|   _copy () { |   _copy () { | ||||||
|     return new this.constructor() |     return new this.constructor() | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Redo the effect of this operation. |    * Redoes the effect of this operation. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|    */ |    */ | ||||||
|   _redo (y) { |   _redo (y) { | ||||||
|     if (this._redone !== null) { |     if (this._redone !== null) { | ||||||
| @ -102,20 +160,47 @@ export default class Item { | |||||||
|     return struct |     return struct | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Computes the last content address of this Item. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   get _lastId () { |   get _lastId () { | ||||||
|     return new ID(this._id.user, this._id.clock + this._length - 1) |     return new ID(this._id.user, this._id.clock + this._length - 1) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Computes the length of this Item. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   get _length () { |   get _length () { | ||||||
|     return 1 |     return 1 | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Splits this struct so that another struct can be inserted in-between. |    * Should return false if this Item is some kind of meta information | ||||||
|  |    * (e.g. format information). | ||||||
|  |    * | ||||||
|  |    * * Whether this Item should be addressable via `yarray.get(i)` | ||||||
|  |    * * Whether this Item should be counted when computing yarray.length | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   get _countable () { | ||||||
|  |     return true | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Splits this Item so that another Items can be inserted in-between. | ||||||
|    * This must be overwritten if _length > 1 |    * This must be overwritten if _length > 1 | ||||||
|    * Returns right part after split |    * Returns right part after split | ||||||
|    * - diff === 0 => this |    * * diff === 0 => this | ||||||
|    * - diff === length => this._right |    * * diff === length => this._right | ||||||
|    * - otherwise => split _content and return right part of split |    * * otherwise => split _content and return right part of split | ||||||
|    * (see ItemJSON/ItemString for implementation) |    * (see {@link ItemJSON}/{@link ItemString} for implementation) | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|    */ |    */ | ||||||
|   _splitAt (y, diff) { |   _splitAt (y, diff) { | ||||||
|     if (diff === 0) { |     if (diff === 0) { | ||||||
| @ -123,10 +208,20 @@ export default class Item { | |||||||
|     } |     } | ||||||
|     return this._right |     return this._right | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Mark this Item as deleted. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    * @param {boolean} createDelete Whether to propagate a message that this | ||||||
|  |    *                               Type was deleted. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _delete (y, createDelete = true) { |   _delete (y, createDelete = true) { | ||||||
|     if (!this._deleted) { |     if (!this._deleted) { | ||||||
|       this._deleted = true |       this._deleted = true | ||||||
|       y.ds.markDeleted(this._id, this._length) |       y.ds.mark(this._id, this._length, false) | ||||||
|       let del = new Delete() |       let del = new Delete() | ||||||
|       del._targetID = this._id |       del._targetID = this._id | ||||||
|       del._length = this._length |       del._length = this._length | ||||||
| @ -141,17 +236,39 @@ export default class Item { | |||||||
|       y._transaction.deletedStructs.add(this) |       y._transaction.deletedStructs.add(this) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   _gcChildren (y) {} | ||||||
|  | 
 | ||||||
|  |   _gc (y) { | ||||||
|  |     const gc = new GC() | ||||||
|  |     gc._id = this._id | ||||||
|  |     gc._length = this._length | ||||||
|  |     y.os.delete(this._id) | ||||||
|  |     gc._integrate(y) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * This is called right before this struct receives any children. |    * This is called right before this Item receives any children. | ||||||
|    * It can be overwritten to apply pending changes before applying remote changes |    * It can be overwritten to apply pending changes before applying remote changes | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|    */ |    */ | ||||||
|   _beforeChange () { |   _beforeChange () { | ||||||
|     // nop
 |     // nop
 | ||||||
|   } |   } | ||||||
|   /* | 
 | ||||||
|    * - Integrate the struct so that other types/structs can see it |   /** | ||||||
|    * - Add this struct to y.os |    * Integrates this Item into the shared structure. | ||||||
|    * - Check if this is struct deleted |    * | ||||||
|  |    * This method actually applies the change to the Yjs instance. In case of | ||||||
|  |    * Item it connects _left and _right to this Item and calls the | ||||||
|  |    * {@link Item#beforeChange} method. | ||||||
|  |    * | ||||||
|  |    * * Integrate the struct so that other types/structs can see it | ||||||
|  |    * * Add this struct to y.os | ||||||
|  |    * * Check if this is struct deleted | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|    */ |    */ | ||||||
|   _integrate (y) { |   _integrate (y) { | ||||||
|     y._transaction.newTypes.add(this) |     y._transaction.newTypes.add(this) | ||||||
| @ -177,6 +294,7 @@ export default class Item { | |||||||
|       // or this types is new
 |       // or this types is new
 | ||||||
|       this._parent._beforeChange() |       this._parent._beforeChange() | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     /* |     /* | ||||||
|     # $this has to find a unique position between origin and the next known character |     # $this has to find a unique position between origin and the next known character | ||||||
|     # case 1: $origin equals $o.origin: the $creator parameter decides if left or right |     # case 1: $origin equals $o.origin: the $creator parameter decides if left or right | ||||||
| @ -269,8 +387,19 @@ export default class Item { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform the properties of this type to binary and write it to an | ||||||
|  |    * BinaryEncoder. | ||||||
|  |    * | ||||||
|  |    * This is called when this Item is sent to a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {BinaryEncoder} encoder The encoder to write data to. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _toBinary (encoder) { |   _toBinary (encoder) { | ||||||
|     encoder.writeUint8(getReference(this.constructor)) |     encoder.writeUint8(getStructReference(this.constructor)) | ||||||
|     let info = 0 |     let info = 0 | ||||||
|     if (this._origin !== null) { |     if (this._origin !== null) { | ||||||
|       info += 0b1 // origin is defined
 |       info += 0b1 // origin is defined
 | ||||||
| @ -309,6 +438,17 @@ export default class Item { | |||||||
|       encoder.writeVarString(JSON.stringify(this._parentSub)) |       encoder.writeVarString(JSON.stringify(this._parentSub)) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Read the next Item in a Decoder and fill this Item with the read data. | ||||||
|  |    * | ||||||
|  |    * This is called when data is received from a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance that this Item belongs to. | ||||||
|  |    * @param {BinaryDecoder} decoder The decoder object to read data from. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _fromBinary (y, decoder) { |   _fromBinary (y, decoder) { | ||||||
|     let missing = [] |     let missing = [] | ||||||
|     const info = decoder.readUint8() |     const info = decoder.readUint8() | ||||||
| @ -346,7 +486,12 @@ export default class Item { | |||||||
|       const parentID = decoder.readID() |       const parentID = decoder.readID() | ||||||
|       // parent does not change, so we don't have to search for it again
 |       // parent does not change, so we don't have to search for it again
 | ||||||
|       if (this._parent === null) { |       if (this._parent === null) { | ||||||
|         const parent = y.os.get(parentID) |         let parent | ||||||
|  |         if (parentID.constructor === RootID) { | ||||||
|  |           parent = y.os.get(parentID) | ||||||
|  |         } else { | ||||||
|  |           parent = y.os.getItem(parentID) | ||||||
|  |         } | ||||||
|         if (parent === null) { |         if (parent === null) { | ||||||
|           missing.push(parentID) |           missing.push(parentID) | ||||||
|         } else { |         } else { | ||||||
| @ -355,11 +500,21 @@ export default class Item { | |||||||
|       } |       } | ||||||
|     } else if (this._parent === null) { |     } else if (this._parent === null) { | ||||||
|       if (this._origin !== null) { |       if (this._origin !== null) { | ||||||
|  |         if (this._origin.constructor === GC) { | ||||||
|  |           // if origin is a gc, set parent also gc'd
 | ||||||
|  |           this._parent = this._origin | ||||||
|  |         } else { | ||||||
|           this._parent = this._origin._parent |           this._parent = this._origin._parent | ||||||
|  |         } | ||||||
|       } else if (this._right_origin !== null) { |       } else if (this._right_origin !== null) { | ||||||
|  |         // if origin is a gc, set parent also gc'd
 | ||||||
|  |         if (this._right_origin.constructor === GC) { | ||||||
|  |           this._parent = this._right_origin | ||||||
|  |         } else { | ||||||
|           this._parent = this._right_origin._parent |           this._parent = this._right_origin._parent | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|     if (info & 0b1000) { |     if (info & 0b1000) { | ||||||
|       // TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
 |       // TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
 | ||||||
|       this._parentSub = JSON.parse(decoder.readVarString()) |       this._parentSub = JSON.parse(decoder.readVarString()) | ||||||
|  | |||||||
							
								
								
									
										35
									
								
								src/Struct/ItemEmbed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/Struct/ItemEmbed.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,35 @@ | |||||||
|  | import { default as Item } from './Item.js' | ||||||
|  | import { logItemHelper } 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)) | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _logString () { | ||||||
|  |     return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								src/Struct/ItemFormat.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/Struct/ItemFormat.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | |||||||
|  | import { default as Item } from './Item.js' | ||||||
|  | import { logItemHelper } from '../MessageHandler/messageToString.js' | ||||||
|  | 
 | ||||||
|  | export default class ItemFormat extends Item { | ||||||
|  |   constructor () { | ||||||
|  |     super() | ||||||
|  |     this.key = null | ||||||
|  |     this.value = null | ||||||
|  |   } | ||||||
|  |   _copy (undeleteChildren, copyPosition) { | ||||||
|  |     let struct = super._copy(undeleteChildren, copyPosition) | ||||||
|  |     struct.key = this.key | ||||||
|  |     struct.value = this.value | ||||||
|  |     return struct | ||||||
|  |   } | ||||||
|  |   get _length () { | ||||||
|  |     return 1 | ||||||
|  |   } | ||||||
|  |   get _countable () { | ||||||
|  |     return false | ||||||
|  |   } | ||||||
|  |   _fromBinary (y, decoder) { | ||||||
|  |     const missing = super._fromBinary(y, decoder) | ||||||
|  |     this.key = decoder.readVarString() | ||||||
|  |     this.value = JSON.parse(decoder.readVarString()) | ||||||
|  |     return missing | ||||||
|  |   } | ||||||
|  |   _toBinary (encoder) { | ||||||
|  |     super._toBinary(encoder) | ||||||
|  |     encoder.writeVarString(this.key) | ||||||
|  |     encoder.writeVarString(JSON.stringify(this.value)) | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _logString () { | ||||||
|  |     return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,5 +1,5 @@ | |||||||
| import { splitHelper, default as Item } from './Item.js' | import { splitHelper, default as Item } from './Item.js' | ||||||
| import { logID } from '../MessageHandler/messageToString.js' | import { logItemHelper } from '../MessageHandler/messageToString.js' | ||||||
| 
 | 
 | ||||||
| export default class ItemJSON extends Item { | export default class ItemJSON extends Item { | ||||||
|   constructor () { |   constructor () { | ||||||
| @ -45,10 +45,14 @@ export default class ItemJSON extends Item { | |||||||
|       encoder.writeVarString(encoded) |       encoder.writeVarString(encoded) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _logString () { |   _logString () { | ||||||
|     const left = this._left !== null ? this._left._lastId : null |     return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`) | ||||||
|     const origin = this._origin !== null ? this._origin._lastId : null |  | ||||||
|     return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` |  | ||||||
|   } |   } | ||||||
|   _splitAt (y, diff) { |   _splitAt (y, diff) { | ||||||
|     if (diff === 0) { |     if (diff === 0) { | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import { splitHelper, default as Item } from './Item.js' | import { splitHelper, default as Item } from './Item.js' | ||||||
| import { logID } from '../MessageHandler/messageToString.js' | import { logItemHelper } from '../MessageHandler/messageToString.js' | ||||||
| 
 | 
 | ||||||
| export default class ItemString extends Item { | export default class ItemString extends Item { | ||||||
|   constructor () { |   constructor () { | ||||||
| @ -23,10 +23,14 @@ export default class ItemString extends Item { | |||||||
|     super._toBinary(encoder) |     super._toBinary(encoder) | ||||||
|     encoder.writeVarString(this._content) |     encoder.writeVarString(this._content) | ||||||
|   } |   } | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _logString () { |   _logString () { | ||||||
|     const left = this._left !== null ? this._left._lastId : null |     return logItemHelper('ItemString', this, `content:"${this._content}"`) | ||||||
|     const origin = this._origin !== null ? this._origin._lastId : null |  | ||||||
|     return `ItemJSON(id:${logID(this._id)},content:${JSON.stringify(this._content)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` |  | ||||||
|   } |   } | ||||||
|   _splitAt (y, diff) { |   _splitAt (y, diff) { | ||||||
|     if (diff === 0) { |     if (diff === 0) { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import Item from './Item.js' | import Item from './Item.js' | ||||||
| import EventHandler from '../Util/EventHandler.js' | import EventHandler from '../Util/EventHandler.js' | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| 
 | 
 | ||||||
| // restructure children as if they were inserted one after another
 | // restructure children as if they were inserted one after another
 | ||||||
| function integrateChildren (y, start) { | function integrateChildren (y, start) { | ||||||
| @ -30,6 +30,17 @@ export function getListItemIDByPosition (type, i) { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function gcChildren (y, item) { | ||||||
|  |   while (item !== null) { | ||||||
|  |     item._delete(y, false, true) | ||||||
|  |     item._gc(y) | ||||||
|  |     item = item._right | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Abstract Yjs Type class | ||||||
|  |  */ | ||||||
| export default class Type extends Item { | export default class Type extends Item { | ||||||
|   constructor () { |   constructor () { | ||||||
|     super() |     super() | ||||||
| @ -39,32 +50,52 @@ export default class Type extends Item { | |||||||
|     this._eventHandler = new EventHandler() |     this._eventHandler = new EventHandler() | ||||||
|     this._deepEventHandler = new EventHandler() |     this._deepEventHandler = new EventHandler() | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Compute the path from this type to the specified target. | ||||||
|  |    * | ||||||
|  |    * @example | ||||||
|  |    * It should be accessible via `this.get(result[0]).get(result[1])..` | ||||||
|  |    * const path = type.getPathTo(child) | ||||||
|  |    * // assuming `type instanceof YArray`
 | ||||||
|  |    * console.log(path) // might look like => [2, 'key1']
 | ||||||
|  |    * child === type.get(path[0]).get(path[1]) | ||||||
|  |    * | ||||||
|  |    * @param {YType} type Type target | ||||||
|  |    * @return {Array<string>} Path to the target | ||||||
|  |    */ | ||||||
|   getPathTo (type) { |   getPathTo (type) { | ||||||
|     if (type === this) { |     if (type === this) { | ||||||
|       return [] |       return [] | ||||||
|     } |     } | ||||||
|     const path = [] |     const path = [] | ||||||
|     const y = this._y |     const y = this._y | ||||||
|     while (type._parent !== this && this._parent !== y) { |     while (type !== this && type !== y) { | ||||||
|       let parent = type._parent |       let parent = type._parent | ||||||
|       if (type._parentSub !== null) { |       if (type._parentSub !== null) { | ||||||
|         path.push(type._parentSub) |         path.unshift(type._parentSub) | ||||||
|       } else { |       } else { | ||||||
|         // parent is array-ish
 |         // parent is array-ish
 | ||||||
|         for (let [i, child] of parent) { |         for (let [i, child] of parent) { | ||||||
|           if (child === type) { |           if (child === type) { | ||||||
|             path.push(i) |             path.unshift(i) | ||||||
|             break |             break | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       type = parent |       type = parent | ||||||
|     } |     } | ||||||
|     if (this._parent !== this) { |     if (type !== this) { | ||||||
|       throw new Error('The type is not a child of this node') |       throw new Error('The type is not a child of this node') | ||||||
|     } |     } | ||||||
|     return path |     return path | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Call event listeners with an event. This will also add an event to all | ||||||
|  |    * parents (for `.observeDeep` handlers). | ||||||
|  |    */ | ||||||
|   _callEventHandler (transaction, event) { |   _callEventHandler (transaction, event) { | ||||||
|     const changedParentTypes = transaction.changedParentTypes |     const changedParentTypes = transaction.changedParentTypes | ||||||
|     this._eventHandler.callEventListeners(transaction, event) |     this._eventHandler.callEventListeners(transaction, event) | ||||||
| @ -79,6 +110,14 @@ export default class Type extends Item { | |||||||
|       type = type._parent |       type = type._parent | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Helper method to transact if the y instance is available. | ||||||
|  |    * | ||||||
|  |    * TODO: Currently event handlers are not thrown when a type is not registered | ||||||
|  |    *       with a Yjs instance. | ||||||
|  |    */ | ||||||
|   _transact (f) { |   _transact (f) { | ||||||
|     const y = this._y |     const y = this._y | ||||||
|     if (y !== null) { |     if (y !== null) { | ||||||
| @ -87,18 +126,53 @@ export default class Type extends Item { | |||||||
|       f(y) |       f(y) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Observe all events that are created on this type. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f Observer function | ||||||
|  |    */ | ||||||
|   observe (f) { |   observe (f) { | ||||||
|     this._eventHandler.addEventListener(f) |     this._eventHandler.addEventListener(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Observe all events that are created by this type and its children. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f Observer function | ||||||
|  |    */ | ||||||
|   observeDeep (f) { |   observeDeep (f) { | ||||||
|     this._deepEventHandler.addEventListener(f) |     this._deepEventHandler.addEventListener(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Unregister an observer function. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f Observer function | ||||||
|  |    */ | ||||||
|   unobserve (f) { |   unobserve (f) { | ||||||
|     this._eventHandler.removeEventListener(f) |     this._eventHandler.removeEventListener(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Unregister an observer function. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f Observer function | ||||||
|  |    */ | ||||||
|   unobserveDeep (f) { |   unobserveDeep (f) { | ||||||
|     this._deepEventHandler.removeEventListener(f) |     this._deepEventHandler.removeEventListener(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Integrate this type into the Yjs instance. | ||||||
|  |    * | ||||||
|  |    * * Save this struct in the os | ||||||
|  |    * * This type is sent to other client | ||||||
|  |    * * Observer functions are fired | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    */ | ||||||
|   _integrate (y) { |   _integrate (y) { | ||||||
|     super._integrate(y) |     super._integrate(y) | ||||||
|     this._y = y |     this._y = y | ||||||
| @ -117,22 +191,53 @@ export default class Type extends Item { | |||||||
|       integrateChildren(y, t) |       integrateChildren(y, t) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   _delete (y, createDelete) { | 
 | ||||||
|     super._delete(y, createDelete) |   _gcChildren (y) { | ||||||
|  |     gcChildren(y, this._start) | ||||||
|  |     this._start = null | ||||||
|  |     this._map.forEach(item => { | ||||||
|  |       gcChildren(y, item) | ||||||
|  |     }) | ||||||
|  |     this._map = new Map() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _gc (y) { | ||||||
|  |     this._gcChildren(y) | ||||||
|  |     super._gc(y) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Mark this Item as deleted. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    * @param {boolean} createDelete Whether to propagate a message that this | ||||||
|  |    *                               Type was deleted. | ||||||
|  |    * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage | ||||||
|  |    *                                         collect the children of this type. | ||||||
|  |    */ | ||||||
|  |   _delete (y, createDelete, gcChildren) { | ||||||
|  |     if (gcChildren === undefined) { | ||||||
|  |       gcChildren = y._hasUndoManager === false | ||||||
|  |     } | ||||||
|  |     super._delete(y, createDelete, gcChildren) | ||||||
|     y._transaction.changedTypes.delete(this) |     y._transaction.changedTypes.delete(this) | ||||||
|     // delete map types
 |     // delete map types
 | ||||||
|     for (let value of this._map.values()) { |     for (let value of this._map.values()) { | ||||||
|       if (value instanceof Item && !value._deleted) { |       if (value instanceof Item && !value._deleted) { | ||||||
|         value._delete(y, false) |         value._delete(y, false, gcChildren) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     // delete array types
 |     // delete array types
 | ||||||
|     let t = this._start |     let t = this._start | ||||||
|     while (t !== null) { |     while (t !== null) { | ||||||
|       if (!t._deleted) { |       if (!t._deleted) { | ||||||
|         t._delete(y, false) |         t._delete(y, false, gcChildren) | ||||||
|       } |       } | ||||||
|       t = t._right |       t = t._right | ||||||
|     } |     } | ||||||
|  |     if (gcChildren) { | ||||||
|  |       this._gcChildren(y) | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,18 +1,69 @@ | |||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A transaction is created for every change on the Yjs model. It is possible | ||||||
|  |  * to bundle changes on the Yjs model in a single transaction to | ||||||
|  |  * minimize the number on messages sent and the number of observer calls. | ||||||
|  |  * If possible the user of this library should bundle as many changes as | ||||||
|  |  * possible. Here is an example to illustrate the advantages of bundling: | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * const map = y.define('map', YMap) | ||||||
|  |  * // Log content when change is triggered
 | ||||||
|  |  * map.observe(function () { | ||||||
|  |  *   console.log('change triggered') | ||||||
|  |  * }) | ||||||
|  |  * // Each change on the map type triggers a log message:
 | ||||||
|  |  * map.set('a', 0) // => "change triggered"
 | ||||||
|  |  * map.set('b', 0) // => "change triggered"
 | ||||||
|  |  * // When put in a transaction, it will trigger the log after the transaction:
 | ||||||
|  |  * y.transact(function () { | ||||||
|  |  *   map.set('a', 1) | ||||||
|  |  *   map.set('b', 1) | ||||||
|  |  * }) // => "change triggered"
 | ||||||
|  |  * | ||||||
|  |  */ | ||||||
| export default class Transaction { | export default class Transaction { | ||||||
|   constructor (y) { |   constructor (y) { | ||||||
|  |     /** | ||||||
|  |      * @type {Y} The Yjs instance. | ||||||
|  |      */ | ||||||
|     this.y = y |     this.y = y | ||||||
|     // types added during transaction
 |     /** | ||||||
|  |      * All new types that are added during a transaction. | ||||||
|  |      * @type {Set<Item>} | ||||||
|  |      */ | ||||||
|     this.newTypes = new Set() |     this.newTypes = new Set() | ||||||
|     // changed types (does not include new types)
 |     /** | ||||||
|     // maps from type to parentSubs (item._parentSub = null for array elements)
 |      * All types that were directly modified (property added or child | ||||||
|  |      * inserted/deleted). New types are not included in this Set. | ||||||
|  |      * Maps from type to parentSubs (`item._parentSub = null` for YArray) | ||||||
|  |      * @type {Set<YType,String>} | ||||||
|  |      */ | ||||||
|     this.changedTypes = new Map() |     this.changedTypes = new Map() | ||||||
|  |     // TODO: rename deletedTypes
 | ||||||
|  |     /** | ||||||
|  |      * Set of all deleted Types and Structs. | ||||||
|  |      * @type {Set<Item>} | ||||||
|  |      */ | ||||||
|     this.deletedStructs = new Set() |     this.deletedStructs = new Set() | ||||||
|  |     /** | ||||||
|  |      * Saves the old state set of the Yjs instance. If a state was modified, | ||||||
|  |      * the original value is saved here. | ||||||
|  |      * @type {Map<Number,Number>} | ||||||
|  |      */ | ||||||
|     this.beforeState = new Map() |     this.beforeState = new Map() | ||||||
|  |     /** | ||||||
|  |      * Stores the events for the types that observe also child elements. | ||||||
|  |      * It is mainly used by `observeDeep`. | ||||||
|  |      * @type {Map<YType,Array<YEvent>>} | ||||||
|  |      */ | ||||||
|     this.changedParentTypes = new Map() |     this.changedParentTypes = new Map() | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
| export function transactionTypeChanged (y, type, sub) { | export function transactionTypeChanged (y, type, sub) { | ||||||
|   if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) { |   if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) { | ||||||
|     const changedTypes = y._transaction.changedTypes |     const changedTypes = y._transaction.changedTypes | ||||||
|  | |||||||
| @ -1,73 +0,0 @@ | |||||||
| import ItemString from '../Struct/ItemString.js' |  | ||||||
| import YArray from './YArray.js' |  | ||||||
| import { logID } from '../MessageHandler/messageToString.js' |  | ||||||
| 
 |  | ||||||
| export default class YText extends YArray { |  | ||||||
|   constructor (string) { |  | ||||||
|     super() |  | ||||||
|     if (typeof string === 'string') { |  | ||||||
|       const start = new ItemString() |  | ||||||
|       start._parent = this |  | ||||||
|       start._content = string |  | ||||||
|       this._start = start |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   toString () { |  | ||||||
|     const strBuilder = [] |  | ||||||
|     let n = this._start |  | ||||||
|     while (n !== null) { |  | ||||||
|       if (!n._deleted) { |  | ||||||
|         strBuilder.push(n._content) |  | ||||||
|       } |  | ||||||
|       n = n._right |  | ||||||
|     } |  | ||||||
|     return strBuilder.join('') |  | ||||||
|   } |  | ||||||
|   insert (pos, text) { |  | ||||||
|     if (text.length <= 0) { |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
|     this._transact(y => { |  | ||||||
|       let left = null |  | ||||||
|       let right = this._start |  | ||||||
|       let count = 0 |  | ||||||
|       while (right !== null) { |  | ||||||
|         const rightLen = right._deleted ? 0 : (right._length - 1) |  | ||||||
|         if (count <= pos && pos <= count + rightLen) { |  | ||||||
|           const splitDiff = pos - count |  | ||||||
|           right = right._splitAt(this._y, splitDiff) |  | ||||||
|           left = right._left |  | ||||||
|           count += splitDiff |  | ||||||
|           break |  | ||||||
|         } |  | ||||||
|         if (!right._deleted) { |  | ||||||
|           count += right._length |  | ||||||
|         } |  | ||||||
|         left = right |  | ||||||
|         right = right._right |  | ||||||
|       } |  | ||||||
|       if (pos > count) { |  | ||||||
|         throw new Error('Position exceeds array range!') |  | ||||||
|       } |  | ||||||
|       let item = new ItemString() |  | ||||||
|       item._origin = left |  | ||||||
|       item._left = left |  | ||||||
|       item._right = right |  | ||||||
|       item._right_origin = right |  | ||||||
|       item._parent = this |  | ||||||
|       item._content = text |  | ||||||
|       if (y !== null) { |  | ||||||
|         item._integrate(this._y) |  | ||||||
|       } else if (left === null) { |  | ||||||
|         this._start = item |  | ||||||
|       } else { |  | ||||||
|         left._right = item |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|   _logString () { |  | ||||||
|     const left = this._left !== null ? this._left._lastId : null |  | ||||||
|     const origin = this._origin !== null ? this._origin._lastId : null |  | ||||||
|     return `YText(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,134 +0,0 @@ | |||||||
| import { defaultDomFilter } from './utils.js' |  | ||||||
| 
 |  | ||||||
| import YMap from '../YMap.js' |  | ||||||
| import { YXmlFragment } from './y-xml.js' |  | ||||||
| 
 |  | ||||||
| export default class YXmlElement extends YXmlFragment { |  | ||||||
|   constructor (arg1, arg2, _document) { |  | ||||||
|     super() |  | ||||||
|     this.nodeName = null |  | ||||||
|     this._scrollElement = null |  | ||||||
|     if (typeof arg2 === 'function') { |  | ||||||
|       this._domFilter = arg2 |  | ||||||
|     } |  | ||||||
|     if (typeof arg1 === 'string') { |  | ||||||
|       this.nodeName = arg1.toUpperCase() |  | ||||||
|     } else if (arg1 != null && arg1.nodeType != null && arg1.nodeType === arg1.ELEMENT_NODE) { |  | ||||||
|       this.nodeName = arg1.nodeName |  | ||||||
|       this._setDom(arg1, _document) |  | ||||||
|     } else { |  | ||||||
|       this.nodeName = 'UNDEFINED' |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   _copy () { |  | ||||||
|     let struct = super._copy() |  | ||||||
|     struct.nodeName = this.nodeName |  | ||||||
|     return struct |  | ||||||
|   } |  | ||||||
|   _setDom (dom, _document) { |  | ||||||
|     if (this._dom != null) { |  | ||||||
|       throw new Error('Only call this method if you know what you are doing ;)') |  | ||||||
|     } else if (dom._yxml != null) { // TODO do i need to check this? - no.. but for dev purps..
 |  | ||||||
|       throw new Error('Already bound to an YXml type') |  | ||||||
|     } else { |  | ||||||
|       // tag is already set in constructor
 |  | ||||||
|       // set attributes
 |  | ||||||
|       let attributes = new Map() |  | ||||||
|       for (let i = 0; i < dom.attributes.length; i++) { |  | ||||||
|         let attr = dom.attributes[i] |  | ||||||
|         // get attribute via getAttribute for custom element support (some write something different in attr.value)
 |  | ||||||
|         attributes.set(attr.name, dom.getAttribute(attr.name)) |  | ||||||
|       } |  | ||||||
|       attributes = this._domFilter(dom.nodeName, attributes) |  | ||||||
|       attributes.forEach((value, name) => { |  | ||||||
|         this.setAttribute(name, value) |  | ||||||
|       }) |  | ||||||
|       this.insertDomElements(0, Array.prototype.slice.call(dom.childNodes), _document) |  | ||||||
|       this._bindToDom(dom, _document) |  | ||||||
|       return dom |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   _bindToDom (dom, _document) { |  | ||||||
|     _document = _document || document |  | ||||||
|     this._dom = dom |  | ||||||
|     dom._yxml = this |  | ||||||
|   } |  | ||||||
|   _fromBinary (y, decoder) { |  | ||||||
|     const missing = super._fromBinary(y, decoder) |  | ||||||
|     this.nodeName = decoder.readVarString() |  | ||||||
|     return missing |  | ||||||
|   } |  | ||||||
|   _toBinary (encoder) { |  | ||||||
|     super._toBinary(encoder) |  | ||||||
|     encoder.writeVarString(this.nodeName) |  | ||||||
|   } |  | ||||||
|   _integrate (y) { |  | ||||||
|     if (this.nodeName === null) { |  | ||||||
|       throw new Error('nodeName must be defined!') |  | ||||||
|     } |  | ||||||
|     if (this._domFilter === defaultDomFilter && this._parent._domFilter !== undefined) { |  | ||||||
|       this._domFilter = this._parent._domFilter |  | ||||||
|     } |  | ||||||
|     super._integrate(y) |  | ||||||
|   } |  | ||||||
|   /** |  | ||||||
|    * Returns the string representation of the XML document. |  | ||||||
|    * The attributes are ordered by attribute-name, so you can easily use this |  | ||||||
|    * method to compare YXmlElements |  | ||||||
|    */ |  | ||||||
|   toString () { |  | ||||||
|     const attrs = this.getAttributes() |  | ||||||
|     const stringBuilder = [] |  | ||||||
|     const keys = [] |  | ||||||
|     for (let key in attrs) { |  | ||||||
|       keys.push(key) |  | ||||||
|     } |  | ||||||
|     keys.sort() |  | ||||||
|     const keysLen = keys.length |  | ||||||
|     for (let i = 0; i < keysLen; i++) { |  | ||||||
|       const key = keys[i] |  | ||||||
|       stringBuilder.push(key + '="' + attrs[key] + '"') |  | ||||||
|     } |  | ||||||
|     const nodeName = this.nodeName.toLocaleLowerCase() |  | ||||||
|     const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : '' |  | ||||||
|     return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>` |  | ||||||
|   } |  | ||||||
|   removeAttribute () { |  | ||||||
|     return YMap.prototype.delete.apply(this, arguments) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   setAttribute () { |  | ||||||
|     return YMap.prototype.set.apply(this, arguments) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getAttribute () { |  | ||||||
|     return YMap.prototype.get.apply(this, arguments) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getAttributes () { |  | ||||||
|     const obj = {} |  | ||||||
|     for (let [key, value] of this._map) { |  | ||||||
|       if (!value._deleted) { |  | ||||||
|         obj[key] = value._content[0] |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return obj |  | ||||||
|   } |  | ||||||
|   getDom (_document) { |  | ||||||
|     _document = _document || document |  | ||||||
|     let dom = this._dom |  | ||||||
|     if (dom == null) { |  | ||||||
|       dom = _document.createElement(this.nodeName) |  | ||||||
|       dom._yxml = this |  | ||||||
|       let attrs = this.getAttributes() |  | ||||||
|       for (let key in attrs) { |  | ||||||
|         dom.setAttribute(key, attrs[key]) |  | ||||||
|       } |  | ||||||
|       this.forEach(yxml => { |  | ||||||
|         dom.appendChild(yxml.getDom(_document)) |  | ||||||
|       }) |  | ||||||
|       this._bindToDom(dom, _document) |  | ||||||
|     } |  | ||||||
|     return dom |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,17 +0,0 @@ | |||||||
| import YEvent from '../../Util/YEvent.js' |  | ||||||
| 
 |  | ||||||
| export default class YXmlEvent extends YEvent { |  | ||||||
|   constructor (target, subs, remote) { |  | ||||||
|     super(target) |  | ||||||
|     this.childListChanged = false |  | ||||||
|     this.attributesChanged = new Set() |  | ||||||
|     this.remote = remote |  | ||||||
|     subs.forEach((sub) => { |  | ||||||
|       if (sub === null) { |  | ||||||
|         this.childListChanged = true |  | ||||||
|       } else { |  | ||||||
|         this.attributesChanged.add(sub) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,354 +0,0 @@ | |||||||
| /* global MutationObserver */ |  | ||||||
| 
 |  | ||||||
| import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js' |  | ||||||
| import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js' |  | ||||||
| 
 |  | ||||||
| import YArray from '../YArray.js' |  | ||||||
| import YXmlEvent from './YXmlEvent.js' |  | ||||||
| import { YXmlText, YXmlHook } from './y-xml' |  | ||||||
| import { logID } from '../../MessageHandler/messageToString.js' |  | ||||||
| import diff from '../../Util/simpleDiff.js' |  | ||||||
| 
 |  | ||||||
| function domToYXml (parent, doms, _document) { |  | ||||||
|   const types = [] |  | ||||||
|   doms.forEach(d => { |  | ||||||
|     if (d._yxml != null && d._yxml !== false) { |  | ||||||
|       d._yxml._unbindFromDom() |  | ||||||
|     } |  | ||||||
|     if (parent._domFilter(d.nodeName, new Map()) !== null) { |  | ||||||
|       let type |  | ||||||
|       const hookName = d._yjsHook || (d.dataset != null ? d.dataset.yjsHook : undefined) |  | ||||||
|       if (hookName !== undefined) { |  | ||||||
|         type = new YXmlHook(hookName, d) |  | ||||||
|       } else if (d.nodeType === d.TEXT_NODE) { |  | ||||||
|         type = new YXmlText(d) |  | ||||||
|       } else if (d.nodeType === d.ELEMENT_NODE) { |  | ||||||
|         type = new YXmlFragment._YXmlElement(d, parent._domFilter, _document) |  | ||||||
|       } else { |  | ||||||
|         throw new Error('Unsupported node!') |  | ||||||
|       } |  | ||||||
|       // type.enableSmartScrolling(parent._scrollElement)
 |  | ||||||
|       types.push(type) |  | ||||||
|     } else { |  | ||||||
|       d._yxml = false |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   return types |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class YXmlTreeWalker { |  | ||||||
|   constructor (root, f) { |  | ||||||
|     this._filter = f || (() => true) |  | ||||||
|     this._root = root |  | ||||||
|     this._currentNode = root |  | ||||||
|     this._firstCall = true |  | ||||||
|   } |  | ||||||
|   [Symbol.iterator] () { |  | ||||||
|     return this |  | ||||||
|   } |  | ||||||
|   next () { |  | ||||||
|     let n = this._currentNode |  | ||||||
|     if (this._firstCall) { |  | ||||||
|       this._firstCall = false |  | ||||||
|       if (!n._deleted && this._filter(n)) { |  | ||||||
|         return { value: n, done: false } |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     do { |  | ||||||
|       if (!n._deleted && (n.constructor === YXmlFragment._YXmlElement || n.constructor === YXmlFragment) && n._start !== null) { |  | ||||||
|         // walk down in the tree
 |  | ||||||
|         n = n._start |  | ||||||
|       } else { |  | ||||||
|         // walk right or up in the tree
 |  | ||||||
|         while (n !== this._root) { |  | ||||||
|           if (n._right !== null) { |  | ||||||
|             n = n._right |  | ||||||
|             break |  | ||||||
|           } |  | ||||||
|           n = n._parent |  | ||||||
|         } |  | ||||||
|         if (n === this._root) { |  | ||||||
|           n = null |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (n === this._root) { |  | ||||||
|         break |  | ||||||
|       } |  | ||||||
|     } while (n !== null && (n._deleted || !this._filter(n))) |  | ||||||
|     this._currentNode = n |  | ||||||
|     if (n === null) { |  | ||||||
|       return { done: true } |  | ||||||
|     } else { |  | ||||||
|       return { value: n, done: false } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default class YXmlFragment extends YArray { |  | ||||||
|   constructor () { |  | ||||||
|     super() |  | ||||||
|     this._dom = null |  | ||||||
|     this._domFilter = defaultDomFilter |  | ||||||
|     this._domObserver = null |  | ||||||
|     // this function makes sure that either the
 |  | ||||||
|     // dom event is executed, or the yjs observer is executed
 |  | ||||||
|     var token = true |  | ||||||
|     this._mutualExclude = f => { |  | ||||||
|       if (token) { |  | ||||||
|         token = false |  | ||||||
|         try { |  | ||||||
|           f() |  | ||||||
|         } catch (e) { |  | ||||||
|           console.error(e) |  | ||||||
|         } |  | ||||||
|         /* |  | ||||||
|         if (this._domObserver !== null) { |  | ||||||
|           this._domObserver.takeRecords() |  | ||||||
|         } |  | ||||||
|         */ |  | ||||||
|         token = true |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   createTreeWalker (filter) { |  | ||||||
|     return new YXmlTreeWalker(this, filter) |  | ||||||
|   } |  | ||||||
|   /** |  | ||||||
|    * Retrieve first element that matches *query* |  | ||||||
|    * Similar to DOM's querySelector, but only accepts a subset of its queries |  | ||||||
|    * |  | ||||||
|    * Query support: |  | ||||||
|    *   - tagname |  | ||||||
|    * TODO: |  | ||||||
|    *   - id |  | ||||||
|    *   - attribute |  | ||||||
|    */ |  | ||||||
|   querySelector (query) { |  | ||||||
|     query = query.toUpperCase() |  | ||||||
|     const iterator = new YXmlTreeWalker(this, element => element.nodeName === query) |  | ||||||
|     const next = iterator.next() |  | ||||||
|     if (next.done) { |  | ||||||
|       return null |  | ||||||
|     } else { |  | ||||||
|       return next.value |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   querySelectorAll (query) { |  | ||||||
|     query = query.toUpperCase() |  | ||||||
|     return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query)) |  | ||||||
|   } |  | ||||||
|   enableSmartScrolling (scrollElement) { |  | ||||||
|     this._scrollElement = scrollElement |  | ||||||
|     this.forEach(xml => { |  | ||||||
|       xml.enableSmartScrolling(scrollElement) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|   setDomFilter (f) { |  | ||||||
|     this._domFilter = f |  | ||||||
|     let attributes = new Map() |  | ||||||
|     if (this.getAttributes !== undefined) { |  | ||||||
|       let attrs = this.getAttributes() |  | ||||||
|       for (let key in attrs) { |  | ||||||
|         attributes.set(key, attrs[key]) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this._y.transact(() => { |  | ||||||
|       let result = this._domFilter(this.nodeName, new Map(attributes)) |  | ||||||
|       if (result === null) { |  | ||||||
|         this._delete(this._y) |  | ||||||
|       } else { |  | ||||||
|         attributes.forEach((value, key) => { |  | ||||||
|           if (!result.has(key)) { |  | ||||||
|             this.removeAttribute(key) |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|       this.forEach(xml => { |  | ||||||
|         xml.setDomFilter(f) |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|   _callObserver (transaction, parentSubs, remote) { |  | ||||||
|     this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote)) |  | ||||||
|   } |  | ||||||
|   toString () { |  | ||||||
|     return this.map(xml => xml.toString()).join('') |  | ||||||
|   } |  | ||||||
|   _delete (y, createDelete) { |  | ||||||
|     this._unbindFromDom() |  | ||||||
|     super._delete(y, createDelete) |  | ||||||
|   } |  | ||||||
|   _unbindFromDom () { |  | ||||||
|     if (this._domObserver != null) { |  | ||||||
|       this._domObserver.disconnect() |  | ||||||
|       this._domObserver = null |  | ||||||
|     } |  | ||||||
|     if (this._dom != null) { |  | ||||||
|       this._dom._yxml = null |  | ||||||
|       this._dom = null |  | ||||||
|     } |  | ||||||
|     if (this._beforeTransactionHandler !== undefined) { |  | ||||||
|       this._y.off('beforeTransaction', this._beforeTransactionHandler) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   insertDomElementsAfter (prev, doms, _document) { |  | ||||||
|     const types = domToYXml(this, doms, _document) |  | ||||||
|     this.insertAfter(prev, types) |  | ||||||
|     return types |  | ||||||
|   } |  | ||||||
|   insertDomElements (pos, doms, _document) { |  | ||||||
|     const types = domToYXml(this, doms, _document) |  | ||||||
|     this.insert(pos, types) |  | ||||||
|     return types |  | ||||||
|   } |  | ||||||
|   getDom () { |  | ||||||
|     return this._dom |  | ||||||
|   } |  | ||||||
|   bindToDom (dom, _document) { |  | ||||||
|     if (this._dom != null) { |  | ||||||
|       this._unbindFromDom() |  | ||||||
|     } |  | ||||||
|     if (dom._yxml != null) { |  | ||||||
|       dom._yxml._unbindFromDom() |  | ||||||
|     } |  | ||||||
|     dom.innerHTML = '' |  | ||||||
|     this.forEach(t => { |  | ||||||
|       dom.insertBefore(t.getDom(_document), null) |  | ||||||
|     }) |  | ||||||
|     this._bindToDom(dom, _document) |  | ||||||
|   } |  | ||||||
|   // binds to a dom element
 |  | ||||||
|   // Only call if dom and YXml are isomorph
 |  | ||||||
|   _bindToDom (dom, _document) { |  | ||||||
|     _document = _document || document |  | ||||||
|     this._dom = dom |  | ||||||
|     dom._yxml = this |  | ||||||
|     if (this._parent === null) { |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
|     this._y.on('beforeTransaction', beforeTransactionSelectionFixer) |  | ||||||
|     this._y.on('afterTransaction', afterTransactionSelectionFixer) |  | ||||||
|     const applyFilter = (type) => { |  | ||||||
|       if (type._deleted) { |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       // check if type is a child of this
 |  | ||||||
|       let isChild = false |  | ||||||
|       let p = type |  | ||||||
|       while (p !== this._y) { |  | ||||||
|         if (p === this) { |  | ||||||
|           isChild = true |  | ||||||
|           break |  | ||||||
|         } |  | ||||||
|         p = p._parent |  | ||||||
|       } |  | ||||||
|       if (!isChild) { |  | ||||||
|         return |  | ||||||
|       } |  | ||||||
|       // filter attributes
 |  | ||||||
|       let attributes = new Map() |  | ||||||
|       if (type.getAttributes !== undefined) { |  | ||||||
|         let attrs = type.getAttributes() |  | ||||||
|         for (let key in attrs) { |  | ||||||
|           attributes.set(key, attrs[key]) |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       let result = this._domFilter(type.nodeName, new Map(attributes)) |  | ||||||
|       if (result === null) { |  | ||||||
|         type._delete(this._y) |  | ||||||
|       } else { |  | ||||||
|         attributes.forEach((value, key) => { |  | ||||||
|           if (!result.has(key)) { |  | ||||||
|             type.removeAttribute(key) |  | ||||||
|           } |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this._y.on('beforeObserverCalls', function (y, transaction) { |  | ||||||
|       // apply dom filter to new and changed types
 |  | ||||||
|       transaction.changedTypes.forEach(function (subs, type) { |  | ||||||
|         if (subs.size > 1 || !subs.has(null)) { |  | ||||||
|           // only apply changes on attributes
 |  | ||||||
|           applyFilter(type) |  | ||||||
|         } |  | ||||||
|       }) |  | ||||||
|       transaction.newTypes.forEach(applyFilter) |  | ||||||
|     }) |  | ||||||
|     // Apply Y.Xml events to dom
 |  | ||||||
|     this.observeDeep(events => { |  | ||||||
|       reflectChangesOnDom.call(this, events, _document) |  | ||||||
|     }) |  | ||||||
|     // Apply Dom changes on Y.Xml
 |  | ||||||
|     if (typeof MutationObserver !== 'undefined') { |  | ||||||
|       this._beforeTransactionHandler = () => { |  | ||||||
|         this._domObserverListener(this._domObserver.takeRecords()) |  | ||||||
|       } |  | ||||||
|       this._y.on('beforeTransaction', this._beforeTransactionHandler) |  | ||||||
|       this._domObserverListener = mutations => { |  | ||||||
|         this._mutualExclude(() => { |  | ||||||
|           this._y.transact(() => { |  | ||||||
|             let diffChildren = new Set() |  | ||||||
|             mutations.forEach(mutation => { |  | ||||||
|               const dom = mutation.target |  | ||||||
|               const yxml = dom._yxml |  | ||||||
|               if (yxml == null || yxml.constructor === YXmlHook) { |  | ||||||
|                 // dom element is filtered
 |  | ||||||
|                 return |  | ||||||
|               } |  | ||||||
|               switch (mutation.type) { |  | ||||||
|                 case 'characterData': |  | ||||||
|                   var change = diff(yxml.toString(), dom.nodeValue) |  | ||||||
|                   yxml.delete(change.pos, change.remove) |  | ||||||
|                   yxml.insert(change.pos, change.insert) |  | ||||||
|                   break |  | ||||||
|                 case 'attributes': |  | ||||||
|                   if (yxml.constructor === YXmlFragment) { |  | ||||||
|                     break |  | ||||||
|                   } |  | ||||||
|                   let name = mutation.attributeName |  | ||||||
|                   let val = dom.getAttribute(name) |  | ||||||
|                   // check if filter accepts attribute
 |  | ||||||
|                   let attributes = new Map() |  | ||||||
|                   attributes.set(name, val) |  | ||||||
|                   if (this._domFilter(dom.nodeName, attributes).size > 0 && yxml.constructor !== YXmlFragment) { |  | ||||||
|                     if (yxml.getAttribute(name) !== val) { |  | ||||||
|                       if (val == null) { |  | ||||||
|                         yxml.removeAttribute(name) |  | ||||||
|                       } else { |  | ||||||
|                         yxml.setAttribute(name, val) |  | ||||||
|                       } |  | ||||||
|                     } |  | ||||||
|                   } |  | ||||||
|                   break |  | ||||||
|                 case 'childList': |  | ||||||
|                   diffChildren.add(mutation.target) |  | ||||||
|                   break |  | ||||||
|               } |  | ||||||
|             }) |  | ||||||
|             for (let dom of diffChildren) { |  | ||||||
|               if (dom.yOnChildrenChanged !== undefined) { |  | ||||||
|                 dom.yOnChildrenChanged() |  | ||||||
|               } |  | ||||||
|               if (dom._yxml != null && dom._yxml !== false) { |  | ||||||
|                 applyChangesFromDom(dom) |  | ||||||
|               } |  | ||||||
|             } |  | ||||||
|           }) |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|       this._domObserver = new MutationObserver(this._domObserverListener) |  | ||||||
|       this._domObserver.observe(dom, { |  | ||||||
|         childList: true, |  | ||||||
|         attributes: true, |  | ||||||
|         characterData: true, |  | ||||||
|         subtree: true |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
|     return dom |  | ||||||
|   } |  | ||||||
|   _logString () { |  | ||||||
|     const left = this._left !== null ? this._left._lastId : null |  | ||||||
|     const origin = this._origin !== null ? this._origin._lastId : null |  | ||||||
|     return `YXml(id:${logID(this._id)},left:${logID(left)},origin:${logID(origin)},right:${this._right},parent:${logID(this._parent)},parentSub:${this._parentSub})` |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,59 +0,0 @@ | |||||||
| import YMap from '../YMap.js' |  | ||||||
| import { getHook, addHook } from './hooks.js' |  | ||||||
| 
 |  | ||||||
| export default class YXmlHook extends YMap { |  | ||||||
|   constructor (hookName, dom) { |  | ||||||
|     super() |  | ||||||
|     this._dom = null |  | ||||||
|     this.hookName = null |  | ||||||
|     if (hookName !== undefined) { |  | ||||||
|       this.hookName = hookName |  | ||||||
|       this._dom = dom |  | ||||||
|       dom._yjsHook = hookName |  | ||||||
|       dom._yxml = this |  | ||||||
|       getHook(hookName).fillType(dom, this) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   _copy () { |  | ||||||
|     const struct = super._copy() |  | ||||||
|     struct.hookName = this.hookName |  | ||||||
|     return struct |  | ||||||
|   } |  | ||||||
|   getDom (_document) { |  | ||||||
|     _document = _document || document |  | ||||||
|     if (this._dom === null) { |  | ||||||
|       const dom = getHook(this.hookName).createDom(this) |  | ||||||
|       this._dom = dom |  | ||||||
|       dom._yxml = this |  | ||||||
|       dom._yjsHook = this.hookName |  | ||||||
|     } |  | ||||||
|     return this._dom |  | ||||||
|   } |  | ||||||
|   _unbindFromDom () { |  | ||||||
|     this._dom._yxml = null |  | ||||||
|     this._yxml = null |  | ||||||
|     // TODO: cleanup hook?
 |  | ||||||
|   } |  | ||||||
|   _fromBinary (y, decoder) { |  | ||||||
|     const missing = super._fromBinary(y, decoder) |  | ||||||
|     this.hookName = decoder.readVarString() |  | ||||||
|     return missing |  | ||||||
|   } |  | ||||||
|   _toBinary (encoder) { |  | ||||||
|     super._toBinary(encoder) |  | ||||||
|     encoder.writeVarString(this.hookName) |  | ||||||
|   } |  | ||||||
|   _integrate (y) { |  | ||||||
|     if (this.hookName === null) { |  | ||||||
|       throw new Error('hookName must be defined!') |  | ||||||
|     } |  | ||||||
|     super._integrate(y) |  | ||||||
|   } |  | ||||||
|   setDomFilter () { |  | ||||||
|     // TODO: implement new modfilter method!
 |  | ||||||
|   } |  | ||||||
|   enableSmartScrolling () { |  | ||||||
|     // TODO: implement new smartscrolling method!
 |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| YXmlHook.addHook = addHook |  | ||||||
| @ -1,93 +0,0 @@ | |||||||
| import YText from '../YText.js' |  | ||||||
| 
 |  | ||||||
| export default class YXmlText extends YText { |  | ||||||
|   constructor (arg1) { |  | ||||||
|     let dom = null |  | ||||||
|     let initialText = null |  | ||||||
|     if (arg1 != null) { |  | ||||||
|       if (arg1.nodeType != null && arg1.nodeType === arg1.TEXT_NODE) { |  | ||||||
|         dom = arg1 |  | ||||||
|         initialText = dom.nodeValue |  | ||||||
|       } else if (typeof arg1 === 'string') { |  | ||||||
|         initialText = arg1 |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     super(initialText) |  | ||||||
|     this._dom = null |  | ||||||
|     this._domObserver = null |  | ||||||
|     this._domObserverListener = null |  | ||||||
|     this._scrollElement = null |  | ||||||
|     if (dom !== null) { |  | ||||||
|       this._setDom(arg1) |  | ||||||
|     } |  | ||||||
|     /* |  | ||||||
|     var token = true |  | ||||||
|     this._mutualExclude = f => { |  | ||||||
|       if (token) { |  | ||||||
|         token = false |  | ||||||
|         try { |  | ||||||
|           f() |  | ||||||
|         } catch (e) { |  | ||||||
|           console.error(e) |  | ||||||
|         } |  | ||||||
|         this._domObserver.takeRecords() |  | ||||||
|         token = true |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     this.observe(event => { |  | ||||||
|       if (this._dom != null) { |  | ||||||
|         const dom = this._dom |  | ||||||
|         this._mutualExclude(() => { |  | ||||||
|           let anchorViewPosition = getAnchorViewPosition(this._scrollElement) |  | ||||||
|           let anchorViewFix |  | ||||||
|           if (anchorViewPosition !== null && (anchorViewPosition.anchor !== null || getBoundingClientRect(this._dom).top <= 0)) { |  | ||||||
|             anchorViewFix = anchorViewPosition |  | ||||||
|           } else { |  | ||||||
|             anchorViewFix = null |  | ||||||
|           } |  | ||||||
|           dom.nodeValue = this.toString() |  | ||||||
|           fixScrollPosition(this._scrollElement, anchorViewFix) |  | ||||||
|         }) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|     */ |  | ||||||
|   } |  | ||||||
|   setDomFilter () {} |  | ||||||
|   enableSmartScrolling (scrollElement) { |  | ||||||
|     this._scrollElement = scrollElement |  | ||||||
|   } |  | ||||||
|   _setDom (dom) { |  | ||||||
|     if (this._dom != null) { |  | ||||||
|       this._unbindFromDom() |  | ||||||
|     } |  | ||||||
|     if (dom._yxml != null) { |  | ||||||
|       dom._yxml._unbindFromDom() |  | ||||||
|     } |  | ||||||
|     // set marker
 |  | ||||||
|     this._dom = dom |  | ||||||
|     dom._yxml = this |  | ||||||
|   } |  | ||||||
|   getDom (_document) { |  | ||||||
|     _document = _document || document |  | ||||||
|     if (this._dom === null) { |  | ||||||
|       const dom = _document.createTextNode(this.toString()) |  | ||||||
|       this._setDom(dom) |  | ||||||
|       return dom |  | ||||||
|     } |  | ||||||
|     return this._dom |  | ||||||
|   } |  | ||||||
|   _delete (y, createDelete) { |  | ||||||
|     this._unbindFromDom() |  | ||||||
|     super._delete(y, createDelete) |  | ||||||
|   } |  | ||||||
|   _unbindFromDom () { |  | ||||||
|     if (this._domObserver != null) { |  | ||||||
|       this._domObserver.disconnect() |  | ||||||
|       this._domObserver = null |  | ||||||
|     } |  | ||||||
|     if (this._dom != null) { |  | ||||||
|       this._dom._yxml = null |  | ||||||
|       this._dom = null |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,51 +0,0 @@ | |||||||
| 
 |  | ||||||
| const filterMap = new Map() |  | ||||||
| 
 |  | ||||||
| export function addFilter (type, filter) { |  | ||||||
|   if (!filterMap.has(type)) { |  | ||||||
|     filterMap.set(type, new Set()) |  | ||||||
|   } |  | ||||||
|   const filters = filterMap.get(type) |  | ||||||
|   filters.add(filter) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function executeFilter (type) { |  | ||||||
|   const y = type._y |  | ||||||
|   let parent = type |  | ||||||
|   const nodeName = type.nodeName |  | ||||||
|   let attributes = new Map() |  | ||||||
|   if (type.getAttributes !== undefined) { |  | ||||||
|     let attrs = type.getAttributes() |  | ||||||
|     for (let key in attrs) { |  | ||||||
|       attributes.set(key, attrs[key]) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   let filteredAttributes = new Map(attributes) |  | ||||||
|   // is not y, supports dom filtering
 |  | ||||||
|   while (parent !== y && parent.setDomFilter != null) { |  | ||||||
|     const filters = filterMap.get(parent) |  | ||||||
|     if (filters !== undefined) { |  | ||||||
|       for (let f of filters) { |  | ||||||
|         filteredAttributes = f(nodeName, filteredAttributes) |  | ||||||
|         if (filteredAttributes === null) { |  | ||||||
|           break |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       if (filteredAttributes === null) { |  | ||||||
|         break |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     parent = parent._parent |  | ||||||
|   } |  | ||||||
|   if (filteredAttributes === null) { |  | ||||||
|     type._delete(y) |  | ||||||
|   } else { |  | ||||||
|     // iterate original attributes
 |  | ||||||
|     attributes.forEach((value, key) => { |  | ||||||
|       // delete all attributes that are not in filteredAttributes
 |  | ||||||
|       if (!filteredAttributes.has(key)) { |  | ||||||
|         type.removeAttribute(key) |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,14 +0,0 @@ | |||||||
| 
 |  | ||||||
| const xmlHooks = {} |  | ||||||
| 
 |  | ||||||
| export function addHook (name, hook) { |  | ||||||
|   xmlHooks[name] = hook |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function getHook (name) { |  | ||||||
|   const hook = xmlHooks[name] |  | ||||||
|   if (hook === undefined) { |  | ||||||
|     throw new Error(`The hook "${name}" is not specified! You must not access this hook!`) |  | ||||||
|   } |  | ||||||
|   return hook |  | ||||||
| } |  | ||||||
| @ -1,271 +0,0 @@ | |||||||
| import { YXmlText, YXmlHook } from './y-xml.js' |  | ||||||
| 
 |  | ||||||
| export function defaultDomFilter (node, attributes) { |  | ||||||
|   return attributes |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function getAnchorViewPosition (scrollElement) { |  | ||||||
|   if (scrollElement == null) { |  | ||||||
|     return null |  | ||||||
|   } |  | ||||||
|   let anchor = document.getSelection().anchorNode |  | ||||||
|   if (anchor != null) { |  | ||||||
|     let top = getBoundingClientRect(anchor).top |  | ||||||
|     if (top >= 0 && top <= document.documentElement.clientHeight) { |  | ||||||
|       return { |  | ||||||
|         anchor: anchor, |  | ||||||
|         top: top |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   return { |  | ||||||
|     anchor: null, |  | ||||||
|     scrollTop: scrollElement.scrollTop, |  | ||||||
|     scrollHeight: scrollElement.scrollHeight |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // get BoundingClientRect that works on text nodes
 |  | ||||||
| export function getBoundingClientRect (element) { |  | ||||||
|   if (element.getBoundingClientRect != null) { |  | ||||||
|     // is element node
 |  | ||||||
|     return element.getBoundingClientRect() |  | ||||||
|   } else { |  | ||||||
|     // is text node
 |  | ||||||
|     if (element.parentNode == null) { |  | ||||||
|       // range requires that text nodes have a parent
 |  | ||||||
|       let span = document.createElement('span') |  | ||||||
|       span.appendChild(element) |  | ||||||
|     } |  | ||||||
|     let range = document.createRange() |  | ||||||
|     range.selectNode(element) |  | ||||||
|     return range.getBoundingClientRect() |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function fixScrollPosition (scrollElement, fix) { |  | ||||||
|   if (scrollElement !== null && fix !== null) { |  | ||||||
|     if (fix.anchor === null) { |  | ||||||
|       if (scrollElement.scrollTop === fix.scrollTop) { |  | ||||||
|         scrollElement.scrollTop = scrollElement.scrollHeight - fix.scrollHeight |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       scrollElement.scrollTop = getBoundingClientRect(fix.anchor).top - fix.top |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function iterateUntilUndeleted (item) { |  | ||||||
|   while (item !== null && item._deleted) { |  | ||||||
|     item = item._right |  | ||||||
|   } |  | ||||||
|   return item |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function _insertNodeHelper (yxml, prevExpectedNode, child) { |  | ||||||
|   let insertedNodes = yxml.insertDomElementsAfter(prevExpectedNode, [child]) |  | ||||||
|   if (insertedNodes.length > 0) { |  | ||||||
|     return insertedNodes[0] |  | ||||||
|   } else { |  | ||||||
|     return prevExpectedNode |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /* |  | ||||||
|  * 1. Check if any of the nodes was deleted |  | ||||||
|  * 2. Iterate over the children. |  | ||||||
|  *    2.1 If a node exists without _yxml property, insert a new node |  | ||||||
|  *    2.2 If _contents.length < dom.childNodes.length, fill the |  | ||||||
|  *        rest of _content with childNodes |  | ||||||
|  *    2.3 If a node was moved, delete it and |  | ||||||
|  *       recreate a new yxml element that is bound to that node. |  | ||||||
|  *       You can detect that a node was moved because expectedId |  | ||||||
|  *       !== actualId in the list |  | ||||||
|  */ |  | ||||||
| export function applyChangesFromDom (dom) { |  | ||||||
|   const yxml = dom._yxml |  | ||||||
|   if (yxml.constructor === YXmlHook) { |  | ||||||
|     return |  | ||||||
|   } |  | ||||||
|   const y = yxml._y |  | ||||||
|   let knownChildren = |  | ||||||
|     new Set( |  | ||||||
|       Array.prototype.map.call(dom.childNodes, child => child._yxml) |  | ||||||
|       .filter(id => id !== undefined) |  | ||||||
|     ) |  | ||||||
|   // 1. Check if any of the nodes was deleted
 |  | ||||||
|   yxml.forEach(function (childType, i) { |  | ||||||
|     if (!knownChildren.has(childType)) { |  | ||||||
|       childType._delete(y) |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   // 2. iterate
 |  | ||||||
|   let childNodes = dom.childNodes |  | ||||||
|   let len = childNodes.length |  | ||||||
|   let prevExpectedNode = null |  | ||||||
|   let expectedNode = iterateUntilUndeleted(yxml._start) |  | ||||||
|   for (let domCnt = 0; domCnt < len; domCnt++) { |  | ||||||
|     const child = childNodes[domCnt] |  | ||||||
|     const childYXml = child._yxml |  | ||||||
|     if (childYXml != null) { |  | ||||||
|       if (childYXml === false) { |  | ||||||
|         // should be ignored or is going to be deleted
 |  | ||||||
|         continue |  | ||||||
|       } |  | ||||||
|       if (expectedNode !== null) { |  | ||||||
|         if (expectedNode !== childYXml) { |  | ||||||
|           // 2.3 Not expected node
 |  | ||||||
|           if (childYXml._parent !== this) { |  | ||||||
|             // element is going to be deleted by its previous parent
 |  | ||||||
|             child._yxml = null |  | ||||||
|           } else { |  | ||||||
|             childYXml._delete(y) |  | ||||||
|           } |  | ||||||
|           prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child) |  | ||||||
|         } else { |  | ||||||
|           prevExpectedNode = expectedNode |  | ||||||
|           expectedNode = iterateUntilUndeleted(expectedNode._right) |  | ||||||
|         } |  | ||||||
|         // if this is the expected node id, just continue
 |  | ||||||
|       } else { |  | ||||||
|         // 2.2 fill _conten with child nodes
 |  | ||||||
|         prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child) |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       // 2.1 A new node was found
 |  | ||||||
|       prevExpectedNode = _insertNodeHelper(yxml, prevExpectedNode, child) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function reflectChangesOnDom (events, _document) { |  | ||||||
|   // Make sure that no filtered attributes are applied to the structure
 |  | ||||||
|   // if they were, delete them
 |  | ||||||
|   /* |  | ||||||
|   events.forEach(event => { |  | ||||||
|     const target = event.target |  | ||||||
|     if (event.attributesChanged === undefined) { |  | ||||||
|       // event.target is Y.XmlText
 |  | ||||||
|       return |  | ||||||
|     } |  | ||||||
|     const keys = this._domFilter(target.nodeName, Array.from(event.attributesChanged)) |  | ||||||
|     if (keys === null) { |  | ||||||
|       target._delete() |  | ||||||
|     } else { |  | ||||||
|       const removeKeys = new Set() // is a copy of event.attributesChanged
 |  | ||||||
|       event.attributesChanged.forEach(key => { removeKeys.add(key) }) |  | ||||||
|       keys.forEach(key => { |  | ||||||
|         // remove all accepted keys from removeKeys
 |  | ||||||
|         removeKeys.delete(key) |  | ||||||
|       }) |  | ||||||
|       // remove the filtered attribute
 |  | ||||||
|       removeKeys.forEach(key => { |  | ||||||
|         target.removeAttribute(key) |  | ||||||
|       }) |  | ||||||
|     } |  | ||||||
|   }) |  | ||||||
|   */ |  | ||||||
|   this._mutualExclude(() => { |  | ||||||
|     events.forEach(event => { |  | ||||||
|       const yxml = event.target |  | ||||||
|       const dom = yxml._dom |  | ||||||
|       if (dom != null) { |  | ||||||
|         // TODO: do this once before applying stuff
 |  | ||||||
|         // let anchorViewPosition = getAnchorViewPosition(yxml._scrollElement)
 |  | ||||||
|         if (yxml.constructor === YXmlText) { |  | ||||||
|           yxml._dom.nodeValue = yxml.toString() |  | ||||||
|         } else if (event.attributesChanged !== undefined) { |  | ||||||
|           // update attributes
 |  | ||||||
|           event.attributesChanged.forEach(attributeName => { |  | ||||||
|             const value = yxml.getAttribute(attributeName) |  | ||||||
|             if (value === undefined) { |  | ||||||
|               dom.removeAttribute(attributeName) |  | ||||||
|             } else { |  | ||||||
|               dom.setAttribute(attributeName, value) |  | ||||||
|             } |  | ||||||
|           }) |  | ||||||
|           /** |  | ||||||
|            * TODO: instead of chard-checking the types, it would be best to |  | ||||||
|            *       specify the type's features. E.g. |  | ||||||
|            *         - _yxmlHasAttributes |  | ||||||
|            *         - _yxmlHasChildren |  | ||||||
|            *       Furthermore, the features shouldn't be encoded in the types, |  | ||||||
|            *       only in the attributes (above) |  | ||||||
|            */ |  | ||||||
|           if (event.childListChanged && yxml.constructor !== YXmlHook) { |  | ||||||
|             let currentChild = dom.firstChild |  | ||||||
|             yxml.forEach(function (t) { |  | ||||||
|               let expectedChild = t.getDom(_document) |  | ||||||
|               if (expectedChild.parentNode === dom) { |  | ||||||
|                 // is already attached to the dom. Look for it
 |  | ||||||
|                 while (currentChild !== expectedChild) { |  | ||||||
|                   let del = currentChild |  | ||||||
|                   currentChild = currentChild.nextSibling |  | ||||||
|                   dom.removeChild(del) |  | ||||||
|                 } |  | ||||||
|                 currentChild = currentChild.nextSibling |  | ||||||
|               } else { |  | ||||||
|                 // this dom is not yet attached to dom
 |  | ||||||
|                 dom.insertBefore(expectedChild, currentChild) |  | ||||||
|               } |  | ||||||
|             }) |  | ||||||
|             while (currentChild !== null) { |  | ||||||
|               let tmp = currentChild.nextSibling |  | ||||||
|               dom.removeChild(currentChild) |  | ||||||
|               currentChild = tmp |  | ||||||
|             } |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         /* TODO: smartscrolling |  | ||||||
|         .. else if (event.type === 'childInserted' || event.type === 'insert') { |  | ||||||
|           let nodes = event.values |  | ||||||
|           for (let i = nodes.length - 1; i >= 0; i--) { |  | ||||||
|             let node = nodes[i] |  | ||||||
|             node.setDomFilter(yxml._domFilter) |  | ||||||
|             node.enableSmartScrolling(yxml._scrollElement) |  | ||||||
|             let dom = node.getDom() |  | ||||||
|             let fixPosition = null |  | ||||||
|             let nextDom = null |  | ||||||
|             if (yxml._content.length > event.index + i + 1) { |  | ||||||
|               nextDom = yxml.get(event.index + i + 1).getDom() |  | ||||||
|             } |  | ||||||
|             yxml._dom.insertBefore(dom, nextDom) |  | ||||||
|             if (anchorViewPosition === null) { |  | ||||||
|               // nop
 |  | ||||||
|             } else if (anchorViewPosition.anchor !== null) { |  | ||||||
|               // no scrolling when current selection
 |  | ||||||
|               if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) { |  | ||||||
|                 fixPosition = anchorViewPosition |  | ||||||
|               } |  | ||||||
|             } else if (getBoundingClientRect(dom).top <= 0) { |  | ||||||
|               // adjust scrolling if modified element is out of view,
 |  | ||||||
|               // there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
 |  | ||||||
|               fixPosition = anchorViewPosition |  | ||||||
|             } |  | ||||||
|             fixScrollPosition(yxml._scrollElement, fixPosition) |  | ||||||
|           } |  | ||||||
|         } else if (event.type === 'childRemoved' || event.type === 'delete') { |  | ||||||
|           for (let i = event.values.length - 1; i >= 0; i--) { |  | ||||||
|             let dom = event.values[i]._dom |  | ||||||
|             let fixPosition = null |  | ||||||
|             if (anchorViewPosition === null) { |  | ||||||
|               // nop
 |  | ||||||
|             } else if (anchorViewPosition.anchor !== null) { |  | ||||||
|               // no scrolling when current selection
 |  | ||||||
|               if (!dom.contains(anchorViewPosition.anchor) && !anchorViewPosition.anchor.contains(dom)) { |  | ||||||
|                 fixPosition = anchorViewPosition |  | ||||||
|               } |  | ||||||
|             } else if (getBoundingClientRect(dom).top <= 0) { |  | ||||||
|               // adjust scrolling if modified element is out of view,
 |  | ||||||
|               // there is no anchor element, and the browser did not adjust scrollTop (this is checked later)
 |  | ||||||
|               fixPosition = anchorViewPosition |  | ||||||
|             } |  | ||||||
|             dom.remove() |  | ||||||
|             fixScrollPosition(yxml._scrollElement, fixPosition) |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         */ |  | ||||||
|       } |  | ||||||
|     }) |  | ||||||
|   }) |  | ||||||
| } |  | ||||||
| @ -1,16 +1,30 @@ | |||||||
| import Type from '../Struct/Type.js' | import Type from '../../Struct/Type.js' | ||||||
| import ItemJSON from '../Struct/ItemJSON.js' | import ItemJSON from '../../Struct/ItemJSON.js' | ||||||
| import ItemString from '../Struct/ItemString.js' | import ItemString from '../../Struct/ItemString.js' | ||||||
| import { logID } from '../MessageHandler/messageToString.js' | import { logID, logItemHelper } from '../../MessageHandler/messageToString.js' | ||||||
| import YEvent from '../Util/YEvent.js' | import YEvent from '../../Util/YEvent.js' | ||||||
| 
 | 
 | ||||||
| class YArrayEvent extends YEvent { | /** | ||||||
|  |  * Event that describes the changes on a YArray | ||||||
|  |  * | ||||||
|  |  * @param {YArray} yarray The changed type | ||||||
|  |  * @param {Boolean} remote Whether the changed was caused by a remote peer | ||||||
|  |  * @param {Transaction} transaction The transaction object | ||||||
|  |  */ | ||||||
|  | export class YArrayEvent extends YEvent { | ||||||
|   constructor (yarray, remote, transaction) { |   constructor (yarray, remote, transaction) { | ||||||
|     super(yarray) |     super(yarray) | ||||||
|     this.remote = remote |     this.remote = remote | ||||||
|     this._transaction = transaction |     this._transaction = transaction | ||||||
|     this._addedElements = null |     this._addedElements = null | ||||||
|  |     this._removedElements = null | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Child elements that were added in this transaction. | ||||||
|  |    * | ||||||
|  |    * @return {Set} | ||||||
|  |    */ | ||||||
|   get addedElements () { |   get addedElements () { | ||||||
|     if (this._addedElements === null) { |     if (this._addedElements === null) { | ||||||
|       const target = this.target |       const target = this.target | ||||||
| @ -25,7 +39,14 @@ class YArrayEvent extends YEvent { | |||||||
|     } |     } | ||||||
|     return this._addedElements |     return this._addedElements | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Child elements that were removed in this transaction. | ||||||
|  |    * | ||||||
|  |    * @return {Set} | ||||||
|  |    */ | ||||||
|   get removedElements () { |   get removedElements () { | ||||||
|  |     if (this._removedElements === null) { | ||||||
|       const target = this.target |       const target = this.target | ||||||
|       const transaction = this._transaction |       const transaction = this._transaction | ||||||
|       const removedElements = new Set() |       const removedElements = new Set() | ||||||
| @ -34,33 +55,60 @@ class YArrayEvent extends YEvent { | |||||||
|           removedElements.add(struct) |           removedElements.add(struct) | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|     return removedElements |       this._removedElements = removedElements | ||||||
|  |     } | ||||||
|  |     return this._removedElements | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A shared Array implementation. | ||||||
|  |  */ | ||||||
| export default class YArray extends Type { | export default class YArray extends Type { | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Creates YArray Event and calls observers. | ||||||
|  |    */ | ||||||
|   _callObserver (transaction, parentSubs, remote) { |   _callObserver (transaction, parentSubs, remote) { | ||||||
|     this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction)) |     this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction)) | ||||||
|   } |   } | ||||||
|   get (pos) { | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the i-th element from a YArray. | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index The index of the element to return from the YArray | ||||||
|  |    */ | ||||||
|  |   get (index) { | ||||||
|     let n = this._start |     let n = this._start | ||||||
|     while (n !== null) { |     while (n !== null) { | ||||||
|       if (!n._deleted) { |       if (!n._deleted && n._countable) { | ||||||
|         if (pos < n._length) { |         if (index < n._length) { | ||||||
|           if (n.constructor === ItemJSON || n.constructor === ItemString) { |           if (n.constructor === ItemJSON || n.constructor === ItemString) { | ||||||
|             return n._content[pos] |             return n._content[index] | ||||||
|           } else { |           } else { | ||||||
|             return n |             return n | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         pos -= n._length |         index -= n._length | ||||||
|       } |       } | ||||||
|       n = n._right |       n = n._right | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transforms this YArray to a JavaScript Array. | ||||||
|  |    * | ||||||
|  |    * @return {Array} | ||||||
|  |    */ | ||||||
|   toArray () { |   toArray () { | ||||||
|     return this.map(c => c) |     return this.map(c => c) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transforms this Shared Type to a JSON object. | ||||||
|  |    * | ||||||
|  |    * @return {Array} | ||||||
|  |    */ | ||||||
|   toJSON () { |   toJSON () { | ||||||
|     return this.map(c => { |     return this.map(c => { | ||||||
|       if (c instanceof Type) { |       if (c instanceof Type) { | ||||||
| @ -73,6 +121,15 @@ export default class YArray extends Type { | |||||||
|       return c |       return c | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns an Array with the result of calling a provided function on every | ||||||
|  |    * element of this YArray. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f Function that produces an element of the new Array | ||||||
|  |    * @return {Array} A new array with each element being the result of the | ||||||
|  |    *                 callback function | ||||||
|  |    */ | ||||||
|   map (f) { |   map (f) { | ||||||
|     const res = [] |     const res = [] | ||||||
|     this.forEach((c, i) => { |     this.forEach((c, i) => { | ||||||
| @ -80,36 +137,47 @@ export default class YArray extends Type { | |||||||
|     }) |     }) | ||||||
|     return res |     return res | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Executes a provided function on once on overy element of this YArray. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f A function to execute on every element of this YArray. | ||||||
|  |    */ | ||||||
|   forEach (f) { |   forEach (f) { | ||||||
|     let pos = 0 |     let index = 0 | ||||||
|     let n = this._start |     let n = this._start | ||||||
|     while (n !== null) { |     while (n !== null) { | ||||||
|       if (!n._deleted) { |       if (!n._deleted && n._countable) { | ||||||
|         if (n instanceof Type) { |         if (n instanceof Type) { | ||||||
|           f(n, pos++, this) |           f(n, index++, this) | ||||||
|         } else { |         } else { | ||||||
|           const content = n._content |           const content = n._content | ||||||
|           const contentLen = content.length |           const contentLen = content.length | ||||||
|           for (let i = 0; i < contentLen; i++) { |           for (let i = 0; i < contentLen; i++) { | ||||||
|             pos++ |             index++ | ||||||
|             f(content[i], pos, this) |             f(content[i], index, this) | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       n = n._right |       n = n._right | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Computes the length of this YArray. | ||||||
|  |    */ | ||||||
|   get length () { |   get length () { | ||||||
|     let length = 0 |     let length = 0 | ||||||
|     let n = this._start |     let n = this._start | ||||||
|     while (n !== null) { |     while (n !== null) { | ||||||
|       if (!n._deleted) { |       if (!n._deleted && n._countable) { | ||||||
|         length += n._length |         length += n._length | ||||||
|       } |       } | ||||||
|       n = n._right |       n = n._right | ||||||
|     } |     } | ||||||
|     return length |     return length | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   [Symbol.iterator] () { |   [Symbol.iterator] () { | ||||||
|     return { |     return { | ||||||
|       next: function () { |       next: function () { | ||||||
| @ -130,7 +198,7 @@ export default class YArray extends Type { | |||||||
|           content = this._item._content[this._itemElement++] |           content = this._item._content[this._itemElement++] | ||||||
|         } |         } | ||||||
|         return { |         return { | ||||||
|           value: [this._count, content], |           value: content, | ||||||
|           done: false |           done: false | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
| @ -139,14 +207,21 @@ export default class YArray extends Type { | |||||||
|       _count: 0 |       _count: 0 | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   delete (pos, length = 1) { | 
 | ||||||
|  |   /** | ||||||
|  |    * Deletes elements starting from an index. | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index Index at which to start deleting elements | ||||||
|  |    * @param {Integer} length The number of elements to remove. Defaults to 1. | ||||||
|  |    */ | ||||||
|  |   delete (index, length = 1) { | ||||||
|     this._y.transact(() => { |     this._y.transact(() => { | ||||||
|       let item = this._start |       let item = this._start | ||||||
|       let count = 0 |       let count = 0 | ||||||
|       while (item !== null && length > 0) { |       while (item !== null && length > 0) { | ||||||
|         if (!item._deleted) { |         if (!item._deleted && item._countable) { | ||||||
|           if (count <= pos && pos < count + item._length) { |           if (count <= index && index < count + item._length) { | ||||||
|             const diffDel = pos - count |             const diffDel = index - count | ||||||
|             item = item._splitAt(this._y, diffDel) |             item = item._splitAt(this._y, diffDel) | ||||||
|             item._splitAt(this._y, length) |             item._splitAt(this._y, length) | ||||||
|             length -= item._length |             length -= item._length | ||||||
| @ -163,6 +238,14 @@ export default class YArray extends Type { | |||||||
|       throw new Error('Delete exceeds the range of the YArray') |       throw new Error('Delete exceeds the range of the YArray') | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Inserts content after an element container. | ||||||
|  |    * | ||||||
|  |    * @param {Item} left The element container to use as a reference. | ||||||
|  |    * @param {Array} content The Array of content to insert (see {@see insert}) | ||||||
|  |    */ | ||||||
|   insertAfter (left, content) { |   insertAfter (left, content) { | ||||||
|     this._transact(y => { |     this._transact(y => { | ||||||
|       let right |       let right | ||||||
| @ -219,16 +302,35 @@ export default class YArray extends Type { | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|  |     return content | ||||||
|   } |   } | ||||||
|   insert (pos, content) { | 
 | ||||||
|  |   /** | ||||||
|  |    * Inserts new content at an index. | ||||||
|  |    * | ||||||
|  |    * Important: This function expects an array of content. Not just a content | ||||||
|  |    * object. The reason for this "weirdness" is that inserting several elements | ||||||
|  |    * is very efficient when it is done as a single operation. | ||||||
|  |    * | ||||||
|  |    * @example | ||||||
|  |    *  // Insert character 'a' at position 0
 | ||||||
|  |    *  yarray.insert(0, ['a']) | ||||||
|  |    *  // Insert numbers 1, 2 at position 1
 | ||||||
|  |    *  yarray.insert(2, [1, 2]) | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index The index to insert content at. | ||||||
|  |    * @param {Array} content The array of content | ||||||
|  |    */ | ||||||
|  |   insert (index, content) { | ||||||
|  |     this._transact(() => { | ||||||
|       let left = null |       let left = null | ||||||
|       let right = this._start |       let right = this._start | ||||||
|       let count = 0 |       let count = 0 | ||||||
|       const y = this._y |       const y = this._y | ||||||
|       while (right !== null) { |       while (right !== null) { | ||||||
|         const rightLen = right._deleted ? 0 : (right._length - 1) |         const rightLen = right._deleted ? 0 : (right._length - 1) | ||||||
|       if (count <= pos && pos <= count + rightLen) { |         if (count <= index && index <= count + rightLen) { | ||||||
|         const splitDiff = pos - count |           const splitDiff = index - count | ||||||
|           right = right._splitAt(y, splitDiff) |           right = right._splitAt(y, splitDiff) | ||||||
|           left = right._left |           left = right._left | ||||||
|           count += splitDiff |           count += splitDiff | ||||||
| @ -240,11 +342,18 @@ export default class YArray extends Type { | |||||||
|         left = right |         left = right | ||||||
|         right = right._right |         right = right._right | ||||||
|       } |       } | ||||||
|     if (pos > count) { |       if (index > count) { | ||||||
|       throw new Error('Position exceeds array range!') |         throw new Error('Index exceeds array range!') | ||||||
|       } |       } | ||||||
|       this.insertAfter(left, content) |       this.insertAfter(left, content) | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Appends content to this YArray. | ||||||
|  |    * | ||||||
|  |    * @param {Array} content Array of content to append. | ||||||
|  |    */ | ||||||
|   push (content) { |   push (content) { | ||||||
|     let n = this._start |     let n = this._start | ||||||
|     let lastUndeleted = null |     let lastUndeleted = null | ||||||
| @ -256,9 +365,14 @@ export default class YArray extends Type { | |||||||
|     } |     } | ||||||
|     this.insertAfter(lastUndeleted, content) |     this.insertAfter(lastUndeleted, content) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _logString () { |   _logString () { | ||||||
|     const left = this._left !== null ? this._left._lastId : null |     return logItemHelper('YArray', this, `start:${logID(this._start)}"`) | ||||||
|     const origin = this._origin !== null ? this._origin._lastId : null |  | ||||||
|     return `YArray(id:${logID(this._id)},start:${logID(this._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -1,10 +1,17 @@ | |||||||
| import Type from '../Struct/Type.js' | import Type from '../../Struct/Type.js' | ||||||
| import Item from '../Struct/Item.js' | import Item from '../../Struct/Item.js' | ||||||
| import ItemJSON from '../Struct/ItemJSON.js' | import ItemJSON from '../../Struct/ItemJSON.js' | ||||||
| import { logID } from '../MessageHandler/messageToString.js' | import { logItemHelper } from '../../MessageHandler/messageToString.js' | ||||||
| import YEvent from '../Util/YEvent.js' | import YEvent from '../../Util/YEvent.js' | ||||||
| 
 | 
 | ||||||
| class YMapEvent extends YEvent { | /** | ||||||
|  |  * Event that describes the changes on a YMap. | ||||||
|  |  * | ||||||
|  |  * @param {YMap} ymap The YArray that changed. | ||||||
|  |  * @param {Set<any>} subs The keys that changed. | ||||||
|  |  * @param {boolean} remote Whether the change was created by a remote peer. | ||||||
|  |  */ | ||||||
|  | export class YMapEvent extends YEvent { | ||||||
|   constructor (ymap, subs, remote) { |   constructor (ymap, subs, remote) { | ||||||
|     super(ymap) |     super(ymap) | ||||||
|     this.keysChanged = subs |     this.keysChanged = subs | ||||||
| @ -12,10 +19,23 @@ class YMapEvent extends YEvent { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A shared Map implementation. | ||||||
|  |  */ | ||||||
| export default class YMap extends Type { | export default class YMap extends Type { | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Creates YMap Event and calls observers. | ||||||
|  |    */ | ||||||
|   _callObserver (transaction, parentSubs, remote) { |   _callObserver (transaction, parentSubs, remote) { | ||||||
|     this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote)) |     this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote)) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transforms this Shared Type to a JSON object. | ||||||
|  |    * | ||||||
|  |    * @return {Object} | ||||||
|  |    */ | ||||||
|   toJSON () { |   toJSON () { | ||||||
|     const map = {} |     const map = {} | ||||||
|     for (let [key, item] of this._map) { |     for (let [key, item] of this._map) { | ||||||
| @ -35,7 +55,14 @@ export default class YMap extends Type { | |||||||
|     } |     } | ||||||
|     return map |     return map | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the keys for each element in the YMap Type. | ||||||
|  |    * | ||||||
|  |    * @return {Array} | ||||||
|  |    */ | ||||||
|   keys () { |   keys () { | ||||||
|  |     // TODO: Should return either Iterator or Set!
 | ||||||
|     let keys = [] |     let keys = [] | ||||||
|     for (let [key, value] of this._map) { |     for (let [key, value] of this._map) { | ||||||
|       if (!value._deleted) { |       if (!value._deleted) { | ||||||
| @ -44,6 +71,12 @@ export default class YMap extends Type { | |||||||
|     } |     } | ||||||
|     return keys |     return keys | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Remove a specified element from this YMap. | ||||||
|  |    * | ||||||
|  |    * @param {encodable} key The key of the element to remove. | ||||||
|  |    */ | ||||||
|   delete (key) { |   delete (key) { | ||||||
|     this._transact((y) => { |     this._transact((y) => { | ||||||
|       let c = this._map.get(key) |       let c = this._map.get(key) | ||||||
| @ -52,11 +85,22 @@ export default class YMap extends Type { | |||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Adds or updates an element with a specified key and value. | ||||||
|  |    * | ||||||
|  |    * @param {encodable} key The key of the element to add to this YMap. | ||||||
|  |    * @param {encodable | YType} value The value of the element to add to this | ||||||
|  |    *                                  YMap. | ||||||
|  |    */ | ||||||
|   set (key, value) { |   set (key, value) { | ||||||
|     this._transact(y => { |     this._transact(y => { | ||||||
|       const old = this._map.get(key) || null |       const old = this._map.get(key) || null | ||||||
|       if (old !== null) { |       if (old !== null) { | ||||||
|         if (old.constructor === ItemJSON && !old._deleted && old._content[0] === value) { |         if ( | ||||||
|  |           old.constructor === ItemJSON && | ||||||
|  |           !old._deleted && old._content[0] === value | ||||||
|  |         ) { | ||||||
|           // Trying to overwrite with same value
 |           // Trying to overwrite with same value
 | ||||||
|           // break here
 |           // break here
 | ||||||
|           return value |           return value | ||||||
| @ -87,6 +131,12 @@ export default class YMap extends Type { | |||||||
|     }) |     }) | ||||||
|     return value |     return value | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns a specified element from this YMap. | ||||||
|  |    * | ||||||
|  |    * @param {encodable} key The key of the element to return. | ||||||
|  |    */ | ||||||
|   get (key) { |   get (key) { | ||||||
|     let v = this._map.get(key) |     let v = this._map.get(key) | ||||||
|     if (v === undefined || v._deleted) { |     if (v === undefined || v._deleted) { | ||||||
| @ -98,6 +148,12 @@ export default class YMap extends Type { | |||||||
|       return v._content[v._content.length - 1] |       return v._content[v._content.length - 1] | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns a boolean indicating whether the specified key exists or not. | ||||||
|  |    * | ||||||
|  |    * @param {encodable} key The key to test. | ||||||
|  |    */ | ||||||
|   has (key) { |   has (key) { | ||||||
|     let v = this._map.get(key) |     let v = this._map.get(key) | ||||||
|     if (v === undefined || v._deleted) { |     if (v === undefined || v._deleted) { | ||||||
| @ -106,9 +162,14 @@ export default class YMap extends Type { | |||||||
|       return true |       return true | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|   _logString () { |   _logString () { | ||||||
|     const left = this._left !== null ? this._left._lastId : null |     return logItemHelper('YMap', this, `mapSize:${this._map.size}`) | ||||||
|     const origin = this._origin !== null ? this._origin._lastId : null |  | ||||||
|     return `YMap(id:${logID(this._id)},mapSize:${this._map.size},left:${logID(left)},origin:${logID(origin)},right:${logID(this._right)},parent:${logID(this._parent)},parentSub:${this._parentSub})` |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
							
								
								
									
										654
									
								
								src/Types/YText/YText.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										654
									
								
								src/Types/YText/YText.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,654 @@ | |||||||
|  | import ItemString from '../../Struct/ItemString.js' | ||||||
|  | import ItemEmbed from '../../Struct/ItemEmbed.js' | ||||||
|  | import ItemFormat from '../../Struct/ItemFormat.js' | ||||||
|  | import { logItemHelper } from '../../MessageHandler/messageToString.js' | ||||||
|  | import { YArrayEvent, default as YArray } from '../YArray/YArray.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function integrateItem (item, parent, y, left, right) { | ||||||
|  |   item._origin = left | ||||||
|  |   item._left = left | ||||||
|  |   item._right = right | ||||||
|  |   item._right_origin = right | ||||||
|  |   item._parent = parent | ||||||
|  |   if (y !== null) { | ||||||
|  |     item._integrate(y) | ||||||
|  |   } else if (left === null) { | ||||||
|  |     parent._start = item | ||||||
|  |   } else { | ||||||
|  |     left._right = item | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function findNextPosition (currentAttributes, parent, left, right, count) { | ||||||
|  |   while (right !== null && count > 0) { | ||||||
|  |     switch (right.constructor) { | ||||||
|  |       case ItemEmbed: | ||||||
|  |       case ItemString: | ||||||
|  |         const rightLen = right._deleted ? 0 : (right._length - 1) | ||||||
|  |         if (count <= rightLen) { | ||||||
|  |           right = right._splitAt(parent._y, count) | ||||||
|  |           left = right._left | ||||||
|  |           return [left, right, currentAttributes] | ||||||
|  |         } | ||||||
|  |         if (right._deleted === false) { | ||||||
|  |           count -= right._length | ||||||
|  |         } | ||||||
|  |         break | ||||||
|  |       case ItemFormat: | ||||||
|  |         if (right._deleted === false) { | ||||||
|  |           updateCurrentAttributes(currentAttributes, right) | ||||||
|  |         } | ||||||
|  |         break | ||||||
|  |     } | ||||||
|  |     left = right | ||||||
|  |     right = right._right | ||||||
|  |   } | ||||||
|  |   return [left, right, currentAttributes] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function findPosition (parent, index) { | ||||||
|  |   let currentAttributes = new Map() | ||||||
|  |   let left = null | ||||||
|  |   let right = parent._start | ||||||
|  |   return findNextPosition(currentAttributes, parent, left, right, index) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Negate applied formats | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | 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] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function updateCurrentAttributes (currentAttributes, item) { | ||||||
|  |   const value = item.value | ||||||
|  |   const key = item.key | ||||||
|  |   if (value === null) { | ||||||
|  |     currentAttributes.delete(key) | ||||||
|  |   } else { | ||||||
|  |     currentAttributes.set(key, value) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | 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] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function insertAttributes (y, parent, left, right, attributes, currentAttributes) { | ||||||
|  |   const 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 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return [left, right, negatedAttributes] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | 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 | ||||||
|  |   [left, right, negatedAttributes] = insertAttributes(y, parent, left, right, attributes, currentAttributes) | ||||||
|  |   // 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) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | function formatText (y, length, parent, left, right, currentAttributes, attributes) { | ||||||
|  |   [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes) | ||||||
|  |   let negatedAttributes | ||||||
|  |   [left, right, negatedAttributes] = insertAttributes(y, parent, left, right, attributes, currentAttributes) | ||||||
|  |   // 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) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | 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] | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: In the quill delta representation we should also use the format {ops:[..]}
 | ||||||
|  | /** | ||||||
|  |  * The Quill Delta format represents changes on a text document with | ||||||
|  |  * formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
 | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  *   { | ||||||
|  |  *     ops: [ | ||||||
|  |  *       { insert: 'Gandalf', attributes: { bold: true } }, | ||||||
|  |  *       { insert: ' the ' }, | ||||||
|  |  *       { insert: 'Grey', attributes: { color: '#cccccc' } } | ||||||
|  |  *     ] | ||||||
|  |  *   } | ||||||
|  |  * | ||||||
|  |  * @typedef {Array<Object>} Delta | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  |  /** | ||||||
|  |   * Attributes that can be assigned to a selection of text. | ||||||
|  |   * | ||||||
|  |   * @example | ||||||
|  |   *   { | ||||||
|  |   *     bold: true, | ||||||
|  |   *     font-size: '40px' | ||||||
|  |   *   } | ||||||
|  |   * | ||||||
|  |   * @typedef {Object} TextAttributes | ||||||
|  |   */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Event that describes the changes on a YText type. | ||||||
|  |  * | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | class YTextEvent extends YArrayEvent { | ||||||
|  |   constructor (ytext, remote, transaction) { | ||||||
|  |     super(ytext, remote, transaction) | ||||||
|  |     this._delta = null | ||||||
|  |   } | ||||||
|  |   // TODO: Should put this in a separate function. toDelta shouldn't be included
 | ||||||
|  |   //       in every Yjs distribution
 | ||||||
|  |   /** | ||||||
|  |    * Compute the changes in the delta format. | ||||||
|  |    * | ||||||
|  |    * @return {Delta} A {@link https://quilljs.com/docs/delta/|Quill Delta}) that
 | ||||||
|  |    *                 represents the changes on the document. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   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 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Type that represents text with formatting information. | ||||||
|  |  * | ||||||
|  |  * This type replaces y-richtext as this implementation is able to handle | ||||||
|  |  * block formats (format information on a paragraph), embeds (complex elements | ||||||
|  |  * like pictures and videos), and text formats (**bold**, *italic*). | ||||||
|  |  * | ||||||
|  |  * @param {String} string The initial value of the YText. | ||||||
|  |  */ | ||||||
|  | export default class YText extends YArray { | ||||||
|  |   constructor (string) { | ||||||
|  |     super() | ||||||
|  |     if (typeof string === 'string') { | ||||||
|  |       const start = new ItemString() | ||||||
|  |       start._parent = this | ||||||
|  |       start._content = string | ||||||
|  |       this._start = start | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Creates YMap Event and calls observers. | ||||||
|  |    */ | ||||||
|  |   _callObserver (transaction, parentSubs, remote) { | ||||||
|  |     this._callEventHandler(transaction, new YTextEvent(this, remote, transaction)) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the unformatted string representation of this YText type. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toString () { | ||||||
|  |     let str = '' | ||||||
|  |     let n = this._start | ||||||
|  |     while (n !== null) { | ||||||
|  |       if (!n._deleted && n._countable) { | ||||||
|  |         str += n._content | ||||||
|  |       } | ||||||
|  |       n = n._right | ||||||
|  |     } | ||||||
|  |     return str | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Apply a {@link Delta} on this shared YText type. | ||||||
|  |    * | ||||||
|  |    * @param {Delta} delta The changes to apply on this element. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   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) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the Delta representation of this YText type. | ||||||
|  |    * | ||||||
|  |    * @return {Delta} The Delta representation of this type. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toDelta () { | ||||||
|  |     let ops = [] | ||||||
|  |     let currentAttributes = new Map() | ||||||
|  |     let str = '' | ||||||
|  |     let n = this._start | ||||||
|  |     function packStr () { | ||||||
|  |       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 | ||||||
|  |         } | ||||||
|  |         let op = { insert: str } | ||||||
|  |         if (addAttributes) { | ||||||
|  |           op.attributes = attributes | ||||||
|  |         } | ||||||
|  |         ops.push(op) | ||||||
|  |         str = '' | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     while (n !== null) { | ||||||
|  |       if (!n._deleted) { | ||||||
|  |         switch (n.constructor) { | ||||||
|  |           case ItemString: | ||||||
|  |             str += n._content | ||||||
|  |             break | ||||||
|  |           case ItemFormat: | ||||||
|  |             packStr() | ||||||
|  |             updateCurrentAttributes(currentAttributes, n) | ||||||
|  |             break | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       n = n._right | ||||||
|  |     } | ||||||
|  |     packStr() | ||||||
|  |     return ops | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Insert text at a given index. | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index The index at which to start inserting. | ||||||
|  |    * @param {String} text The text to insert at the specified position. | ||||||
|  |    * @param {TextAttributes} attributes Optionally define some formatting | ||||||
|  |    *                                    information to apply on the inserted | ||||||
|  |    *                                    Text. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   insert (index, text, attributes = {}) { | ||||||
|  |     if (text.length <= 0) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     this._transact(y => { | ||||||
|  |       let [left, right, currentAttributes] = findPosition(this, index) | ||||||
|  |       insertText(y, text, this, left, right, currentAttributes, attributes) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Inserts an embed at a index. | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index The index to insert the embed at. | ||||||
|  |    * @param {Object} embed The Object that represents the embed. | ||||||
|  |    * @param {TextAttributes} attributes Attribute information to apply on the | ||||||
|  |    *                                    embed | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   insertEmbed (index, embed, attributes = {}) { | ||||||
|  |     if (embed.constructor !== Object) { | ||||||
|  |       throw new Error('Embed must be an Object') | ||||||
|  |     } | ||||||
|  |     this._transact(y => { | ||||||
|  |       let [left, right, currentAttributes] = findPosition(this, index) | ||||||
|  |       insertText(y, embed, this, left, right, currentAttributes, attributes) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Deletes text starting from an index. | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index Index at which to start deleting. | ||||||
|  |    * @param {Integer} length The number of characters to remove. Defaults to 1. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   delete (index, length) { | ||||||
|  |     if (length === 0) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |     this._transact(y => { | ||||||
|  |       let [left, right, currentAttributes] = findPosition(this, index) | ||||||
|  |       deleteText(y, length, this, left, right, currentAttributes) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Assigns properties to a range of text. | ||||||
|  |    * | ||||||
|  |    * @param {Integer} index The position where to start formatting. | ||||||
|  |    * @param {Integer} length The amount of characters to assign properties to. | ||||||
|  |    * @param {TextAttributes} attributes Attribute information to apply on the | ||||||
|  |    *                                    text. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   format (index, length, attributes) { | ||||||
|  |     this._transact(y => { | ||||||
|  |       let [left, right, currentAttributes] = findPosition(this, index) | ||||||
|  |       if (right === null) { | ||||||
|  |         return | ||||||
|  |       } | ||||||
|  |       formatText(y, length, this, left, right, currentAttributes, attributes) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |   // TODO: De-duplicate code. The following code is in every type.
 | ||||||
|  |   /** | ||||||
|  |    * Transform this YText to a readable format. | ||||||
|  |    * Useful for logging as all Items implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _logString () { | ||||||
|  |     return logItemHelper('YText', this) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										188
									
								
								src/Types/YXml/YXmlElement.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src/Types/YXml/YXmlElement.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,188 @@ | |||||||
|  | import YMap from '../YMap/YMap.js' | ||||||
|  | import { YXmlFragment } from './YXml.js' | ||||||
|  | import { createAssociation } from '../../Bindings/DomBinding/util.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * An YXmlElement imitates the behavior of a | ||||||
|  |  * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
 | ||||||
|  |  * | ||||||
|  |  * * An YXmlElement has attributes (key value pairs) | ||||||
|  |  * * An YXmlElement has childElements that must inherit from YXmlElement | ||||||
|  |  * | ||||||
|  |  * @param {String} nodeName Node name | ||||||
|  |  */ | ||||||
|  | export default class YXmlElement extends YXmlFragment { | ||||||
|  |   constructor (nodeName = 'UNDEFINED') { | ||||||
|  |     super() | ||||||
|  |     this.nodeName = nodeName.toUpperCase() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Creates an Item with the same effect as this Item (without position effect) | ||||||
|  |    */ | ||||||
|  |   _copy () { | ||||||
|  |     let struct = super._copy() | ||||||
|  |     struct.nodeName = this.nodeName | ||||||
|  |     return struct | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Read the next Item in a Decoder and fill this Item with the read data. | ||||||
|  |    * | ||||||
|  |    * This is called when data is received from a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance that this Item belongs to. | ||||||
|  |    * @param {BinaryDecoder} decoder The decoder object to read data from. | ||||||
|  |    */ | ||||||
|  |   _fromBinary (y, decoder) { | ||||||
|  |     const missing = super._fromBinary(y, decoder) | ||||||
|  |     this.nodeName = decoder.readVarString() | ||||||
|  |     return missing | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform the properties of this type to binary and write it to an | ||||||
|  |    * BinaryEncoder. | ||||||
|  |    * | ||||||
|  |    * This is called when this Item is sent to a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {BinaryEncoder} encoder The encoder to write data to. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _toBinary (encoder) { | ||||||
|  |     super._toBinary(encoder) | ||||||
|  |     encoder.writeVarString(this.nodeName) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Integrates this Item into the shared structure. | ||||||
|  |    * | ||||||
|  |    * This method actually applies the change to the Yjs instance. In case of | ||||||
|  |    * Item it connects _left and _right to this Item and calls the | ||||||
|  |    * {@link Item#beforeChange} method. | ||||||
|  |    * | ||||||
|  |    * * Checks for nodeName | ||||||
|  |    * * Sets domFilter | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _integrate (y) { | ||||||
|  |     if (this.nodeName === null) { | ||||||
|  |       throw new Error('nodeName must be defined!') | ||||||
|  |     } | ||||||
|  |     super._integrate(y) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the string representation of this YXmlElement. | ||||||
|  |    * The attributes are ordered by attribute-name, so you can easily use this | ||||||
|  |    * method to compare YXmlElements | ||||||
|  |    * | ||||||
|  |    * @return {String} The string representation of this type. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toString () { | ||||||
|  |     const attrs = this.getAttributes() | ||||||
|  |     const stringBuilder = [] | ||||||
|  |     const keys = [] | ||||||
|  |     for (let key in attrs) { | ||||||
|  |       keys.push(key) | ||||||
|  |     } | ||||||
|  |     keys.sort() | ||||||
|  |     const keysLen = keys.length | ||||||
|  |     for (let i = 0; i < keysLen; i++) { | ||||||
|  |       const key = keys[i] | ||||||
|  |       stringBuilder.push(key + '="' + attrs[key] + '"') | ||||||
|  |     } | ||||||
|  |     const nodeName = this.nodeName.toLocaleLowerCase() | ||||||
|  |     const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : '' | ||||||
|  |     return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>` | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Removes an attribute from this YXmlElement. | ||||||
|  |    * | ||||||
|  |    * @param {String} attributeName The attribute name that is to be removed. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   removeAttribute (attributeName) { | ||||||
|  |     return YMap.prototype.delete.call(this, attributeName) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Sets or updates an attribute. | ||||||
|  |    * | ||||||
|  |    * @param {String} attributeName The attribute name that is to be set. | ||||||
|  |    * @param {String} attributeValue The attribute value that is to be set. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   setAttribute (attributeName, attributeValue) { | ||||||
|  |     return YMap.prototype.set.call(this, attributeName, attributeValue) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns an attribute value that belongs to the attribute name. | ||||||
|  |    * | ||||||
|  |    * @param {String} attributeName The attribute name that identifies the | ||||||
|  |    *                               queried value. | ||||||
|  |    * @return {String} The queried attribute value. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   getAttribute (attributeName) { | ||||||
|  |     return YMap.prototype.get.call(this, attributeName) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns all attribute name/value pairs in a JSON Object. | ||||||
|  |    * | ||||||
|  |    * @return {Object} A JSON Object that describes the attributes. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   getAttributes () { | ||||||
|  |     const obj = {} | ||||||
|  |     for (let [key, value] of this._map) { | ||||||
|  |       if (!value._deleted) { | ||||||
|  |         obj[key] = value._content[0] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return obj | ||||||
|  |   } | ||||||
|  |   // TODO: outsource the binding property.
 | ||||||
|  |   /** | ||||||
|  |    * Creates a Dom Element that mirrors this YXmlElement. | ||||||
|  |    * | ||||||
|  |    * @param {Document} [_document=document] The document object (you must define | ||||||
|  |    *                                        this when calling this method in | ||||||
|  |    *                                        nodejs) | ||||||
|  |    * @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks | ||||||
|  |    *                                             are presented in the DOM | ||||||
|  |    * @param {DomBinding} [binding] You should not set this property. This is | ||||||
|  |    *                               used if DomBinding wants to create a | ||||||
|  |    *                               association to the created DOM type. | ||||||
|  |    * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toDom (_document = document, hooks = {}, binding) { | ||||||
|  |     const dom = _document.createElement(this.nodeName) | ||||||
|  |     let attrs = this.getAttributes() | ||||||
|  |     for (let key in attrs) { | ||||||
|  |       dom.setAttribute(key, attrs[key]) | ||||||
|  |     } | ||||||
|  |     this.forEach(yxml => { | ||||||
|  |       dom.appendChild(yxml.toDom(_document, hooks, binding)) | ||||||
|  |     }) | ||||||
|  |     createAssociation(binding, dom, this) | ||||||
|  |     return dom | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										47
									
								
								src/Types/YXml/YXmlEvent.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Types/YXml/YXmlEvent.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | import YEvent from '../../Util/YEvent.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * An Event that describes changes on a YXml Element or Yxml Fragment | ||||||
|  |  * | ||||||
|  |  * @protected | ||||||
|  |  */ | ||||||
|  | export default class YXmlEvent extends YEvent { | ||||||
|  |   /** | ||||||
|  |    * @param {YType} target The target on which the event is created. | ||||||
|  |    * @param {Set} subs The set of changed attributes. `null` is included if the | ||||||
|  |    *                   child list changed. | ||||||
|  |    * @param {Boolean} remote Whether this change was created by a remote peer. | ||||||
|  |    * @param {Transaction} transaction The transaction instance with wich the | ||||||
|  |    *                                  change was created. | ||||||
|  |    */ | ||||||
|  |   constructor (target, subs, remote, transaction) { | ||||||
|  |     super(target) | ||||||
|  |     /** | ||||||
|  |      * The transaction instance for the computed change. | ||||||
|  |      * @type {Transaction} | ||||||
|  |      */ | ||||||
|  |     this._transaction = transaction | ||||||
|  |     /** | ||||||
|  |      * Whether the children changed. | ||||||
|  |      * @type {Boolean} | ||||||
|  |      */ | ||||||
|  |     this.childListChanged = false | ||||||
|  |     /** | ||||||
|  |      * Set of all changed attributes. | ||||||
|  |      * @type {Set} | ||||||
|  |      */ | ||||||
|  |     this.attributesChanged = new Set() | ||||||
|  |     /** | ||||||
|  |      * Whether this change was created by a remote peer. | ||||||
|  |      * @type {Boolean} | ||||||
|  |      */ | ||||||
|  |     this.remote = remote | ||||||
|  |     subs.forEach((sub) => { | ||||||
|  |       if (sub === null) { | ||||||
|  |         this.childListChanged = true | ||||||
|  |       } else { | ||||||
|  |         this.attributesChanged.add(sub) | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										167
									
								
								src/Types/YXml/YXmlFragment.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/Types/YXml/YXmlFragment.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,167 @@ | |||||||
|  | import { createAssociation } from '../../Bindings/DomBinding/util.js' | ||||||
|  | import YXmlTreeWalker from './YXmlTreeWalker.js' | ||||||
|  | 
 | ||||||
|  | import YArray from '../YArray/YArray.js' | ||||||
|  | import YXmlEvent from './YXmlEvent.js' | ||||||
|  | import { logItemHelper } from '../../MessageHandler/messageToString.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Dom filter function. | ||||||
|  |  * | ||||||
|  |  * @callback domFilter | ||||||
|  |  * @param {string} nodeName The nodeName of the element | ||||||
|  |  * @param {Map} attributes The map of attributes. | ||||||
|  |  * @return {boolean} Whether to include the Dom node in the YXmlElement. | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Define the elements to which a set of CSS queries apply. | ||||||
|  |  * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
 | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  *   query = '.classSelector' | ||||||
|  |  *   query = 'nodeSelector' | ||||||
|  |  *   query = '#idSelector' | ||||||
|  |  * | ||||||
|  |  * @typedef {string} CSS_Selector | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Represents a list of {@link YXmlElement}.and {@link YXmlText} types. | ||||||
|  |  * A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a | ||||||
|  |  * nodeName and it does not have attributes. Though it can be bound to a DOM | ||||||
|  |  * element - in this case the attributes and the nodeName are not shared. | ||||||
|  |  * | ||||||
|  |  * @public | ||||||
|  |  */ | ||||||
|  | export default class YXmlFragment extends YArray { | ||||||
|  |   /** | ||||||
|  |    * Create a subtree of childNodes. | ||||||
|  |    * | ||||||
|  |    * @example | ||||||
|  |    * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div') | ||||||
|  |    * for (let node in walker) { | ||||||
|  |    *   // `node` is a div node
 | ||||||
|  |    *   nop(node) | ||||||
|  |    * } | ||||||
|  |    * | ||||||
|  |    * @param {Function} filter Function that is called on each child element and | ||||||
|  |    *                          returns a Boolean indicating whether the child | ||||||
|  |    *                          is to be included in the subtree. | ||||||
|  |    * @return {TreeWalker} A subtree and a position within it. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   createTreeWalker (filter) { | ||||||
|  |     return new YXmlTreeWalker(this, filter) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns the first YXmlElement that matches the query. | ||||||
|  |    * Similar to DOM's {@link querySelector}. | ||||||
|  |    * | ||||||
|  |    * Query support: | ||||||
|  |    *   - tagname | ||||||
|  |    * TODO: | ||||||
|  |    *   - id | ||||||
|  |    *   - attribute | ||||||
|  |    * | ||||||
|  |    * @param {CSS_Selector} query The query on the children. | ||||||
|  |    * @return {?YXmlElement} The first element that matches the query or null. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   querySelector (query) { | ||||||
|  |     query = query.toUpperCase() | ||||||
|  |     const iterator = new YXmlTreeWalker(this, element => element.nodeName === query) | ||||||
|  |     const next = iterator.next() | ||||||
|  |     if (next.done) { | ||||||
|  |       return null | ||||||
|  |     } else { | ||||||
|  |       return next.value | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns all YXmlElements that match the query. | ||||||
|  |    * Similar to Dom's {@link querySelectorAll}. | ||||||
|  |    * | ||||||
|  |    * TODO: Does not yet support all queries. Currently only query by tagName. | ||||||
|  |    * | ||||||
|  |    * @param {CSS_Selector} query The query on the children | ||||||
|  |    * @return {Array<YXmlElement>} The elements that match this query. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   querySelectorAll (query) { | ||||||
|  |     query = query.toUpperCase() | ||||||
|  |     return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query)) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Creates YArray Event and calls observers. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _callObserver (transaction, parentSubs, remote) { | ||||||
|  |     this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote, transaction)) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get the string representation of all the children of this YXmlFragment. | ||||||
|  |    * | ||||||
|  |    * @return {string} The string representation of all children. | ||||||
|  |    */ | ||||||
|  |   toString () { | ||||||
|  |     return this.map(xml => xml.toString()).join('') | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Unbind from Dom and mark this Item as deleted. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    * @param {boolean} createDelete Whether to propagate a message that this | ||||||
|  |    *                               Type was deleted. | ||||||
|  |    * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage | ||||||
|  |    *                                         collect the children of this type. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _delete (y, createDelete, gcChildren) { | ||||||
|  |     super._delete(y, createDelete, gcChildren) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Creates a Dom Element that mirrors this YXmlElement. | ||||||
|  |    * | ||||||
|  |    * @param {Document} [_document=document] The document object (you must define | ||||||
|  |    *                                        this when calling this method in | ||||||
|  |    *                                        nodejs) | ||||||
|  |    * @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks | ||||||
|  |    *                                             are presented in the DOM | ||||||
|  |    * @param {DomBinding} [binding] You should not set this property. This is | ||||||
|  |    *                               used if DomBinding wants to create a | ||||||
|  |    *                               association to the created DOM type | ||||||
|  |    * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toDom (_document = document, hooks = {}, binding) { | ||||||
|  |     const fragment = _document.createDocumentFragment() | ||||||
|  |     createAssociation(binding, fragment, this) | ||||||
|  |     this.forEach(xmlType => { | ||||||
|  |       fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null) | ||||||
|  |     }) | ||||||
|  |     return fragment | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * Transform this YXml Type to a readable format. | ||||||
|  |    * Useful for logging as all Items and Delete implement this method. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _logString () { | ||||||
|  |     return logItemHelper('YXml', this) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										108
									
								
								src/Types/YXml/YXmlHook.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/Types/YXml/YXmlHook.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,108 @@ | |||||||
|  | import YMap from '../YMap/YMap.js' | ||||||
|  | import { createAssociation } from '../../Bindings/DomBinding/util.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * You can manage binding to a custom type with YXmlHook. | ||||||
|  |  * | ||||||
|  |  * @public | ||||||
|  |  */ | ||||||
|  | export default class YXmlHook extends YMap { | ||||||
|  |   /** | ||||||
|  |    * @param {String} hookName nodeName of the Dom Node. | ||||||
|  |    */ | ||||||
|  |   constructor (hookName) { | ||||||
|  |     super() | ||||||
|  |     this.hookName = null | ||||||
|  |     if (hookName !== undefined) { | ||||||
|  |       this.hookName = hookName | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Creates an Item with the same effect as this Item (without position effect) | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _copy () { | ||||||
|  |     const struct = super._copy() | ||||||
|  |     struct.hookName = this.hookName | ||||||
|  |     return struct | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Creates a Dom Element that mirrors this YXmlElement. | ||||||
|  |    * | ||||||
|  |    * @param {Document} [_document=document] The document object (you must define | ||||||
|  |    *                                        this when calling this method in | ||||||
|  |    *                                        nodejs) | ||||||
|  |    * @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks | ||||||
|  |    *                                             are presented in the DOM | ||||||
|  |    * @param {DomBinding} [binding] You should not set this property. This is | ||||||
|  |    *                               used if DomBinding wants to create a | ||||||
|  |    *                               association to the created DOM type | ||||||
|  |    * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toDom (_document = document, hooks = {}, binding) { | ||||||
|  |     const hook = hooks[this.hookName] | ||||||
|  |     let dom | ||||||
|  |     if (hook !== undefined) { | ||||||
|  |       dom = hook.createDom(this) | ||||||
|  |     } else { | ||||||
|  |       dom = document.createElement(this.hookName) | ||||||
|  |     } | ||||||
|  |     dom.setAttribute('data-yjs-hook', this.hookName) | ||||||
|  |     createAssociation(binding, dom, this) | ||||||
|  |     return dom | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Read the next Item in a Decoder and fill this Item with the read data. | ||||||
|  |    * | ||||||
|  |    * This is called when data is received from a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance that this Item belongs to. | ||||||
|  |    * @param {BinaryDecoder} decoder The decoder object to read data from. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _fromBinary (y, decoder) { | ||||||
|  |     const missing = super._fromBinary(y, decoder) | ||||||
|  |     this.hookName = decoder.readVarString() | ||||||
|  |     return missing | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Transform the properties of this type to binary and write it to an | ||||||
|  |    * BinaryEncoder. | ||||||
|  |    * | ||||||
|  |    * This is called when this Item is sent to a remote peer. | ||||||
|  |    * | ||||||
|  |    * @param {BinaryEncoder} encoder The encoder to write data to. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _toBinary (encoder) { | ||||||
|  |     super._toBinary(encoder) | ||||||
|  |     encoder.writeVarString(this.hookName) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Integrate this type into the Yjs instance. | ||||||
|  |    * | ||||||
|  |    * * Save this struct in the os | ||||||
|  |    * * This type is sent to other client | ||||||
|  |    * * Observer functions are fired | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _integrate (y) { | ||||||
|  |     if (this.hookName === null) { | ||||||
|  |       throw new Error('hookName must be defined!') | ||||||
|  |     } | ||||||
|  |     super._integrate(y) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										46
									
								
								src/Types/YXml/YXmlText.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Types/YXml/YXmlText.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,46 @@ | |||||||
|  | import YText from '../YText/YText.js' | ||||||
|  | import { createAssociation } from '../../Bindings/DomBinding/util.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Represents text in a Dom Element. In the future this type will also handle | ||||||
|  |  * simple formatting information like bold and italic. | ||||||
|  |  * | ||||||
|  |  * @param {String} arg1 Initial value. | ||||||
|  |  */ | ||||||
|  | export default class YXmlText extends YText { | ||||||
|  |   /** | ||||||
|  |    * Creates a Dom Element that mirrors this YXmlText. | ||||||
|  |    * | ||||||
|  |    * @param {Document} [_document=document] The document object (you must define | ||||||
|  |    *                                        this when calling this method in | ||||||
|  |    *                                        nodejs) | ||||||
|  |    * @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks | ||||||
|  |    *                                             are presented in the DOM | ||||||
|  |    * @param {DomBinding} [binding] You should not set this property. This is | ||||||
|  |    *                               used if DomBinding wants to create a | ||||||
|  |    *                               association to the created DOM type. | ||||||
|  |    * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   toDom (_document = document, hooks, binding) { | ||||||
|  |     const dom = _document.createTextNode(this.toString()) | ||||||
|  |     createAssociation(binding, dom, this) | ||||||
|  |     return dom | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Mark this Item as deleted. | ||||||
|  |    * | ||||||
|  |    * @param {Y} y The Yjs instance | ||||||
|  |    * @param {boolean} createDelete Whether to propagate a message that this | ||||||
|  |    *                               Type was deleted. | ||||||
|  |    * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage | ||||||
|  |    *                                         collect the children of this type. | ||||||
|  |    * | ||||||
|  |    * @private | ||||||
|  |    */ | ||||||
|  |   _delete (y, createDelete, gcChildren) { | ||||||
|  |     super._delete(y, createDelete, gcChildren) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										76
									
								
								src/Types/YXml/YXmlTreeWalker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/Types/YXml/YXmlTreeWalker.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | |||||||
|  | import YXmlFragment from './YXmlFragment.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Define the elements to which a set of CSS queries apply. | ||||||
|  |  * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
 | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  *   query = '.classSelector' | ||||||
|  |  *   query = 'nodeSelector' | ||||||
|  |  *   query = '#idSelector' | ||||||
|  |  * | ||||||
|  |  * @typedef {string} CSS_Selector | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Represents a subset of the nodes of a YXmlElement / YXmlFragment and a | ||||||
|  |  * position within them. | ||||||
|  |  * | ||||||
|  |  * Can be created with {@link YXmlFragment#createTreeWalker} | ||||||
|  |  * | ||||||
|  |  * @public | ||||||
|  |  */ | ||||||
|  | export default class YXmlTreeWalker { | ||||||
|  |   constructor (root, f) { | ||||||
|  |     this._filter = f || (() => true) | ||||||
|  |     this._root = root | ||||||
|  |     this._currentNode = root | ||||||
|  |     this._firstCall = true | ||||||
|  |   } | ||||||
|  |   [Symbol.iterator] () { | ||||||
|  |     return this | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * Get the next node. | ||||||
|  |    * | ||||||
|  |    * @return {YXmlElement} The next node. | ||||||
|  |    * | ||||||
|  |    * @public | ||||||
|  |    */ | ||||||
|  |   next () { | ||||||
|  |     let n = this._currentNode | ||||||
|  |     if (this._firstCall) { | ||||||
|  |       this._firstCall = false | ||||||
|  |       if (!n._deleted && this._filter(n)) { | ||||||
|  |         return { value: n, done: false } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     do { | ||||||
|  |       if (!n._deleted && (n.constructor === YXmlFragment._YXmlElement || n.constructor === YXmlFragment) && n._start !== null) { | ||||||
|  |         // walk down in the tree
 | ||||||
|  |         n = n._start | ||||||
|  |       } else { | ||||||
|  |         // walk right or up in the tree
 | ||||||
|  |         while (n !== this._root) { | ||||||
|  |           if (n._right !== null) { | ||||||
|  |             n = n._right | ||||||
|  |             break | ||||||
|  |           } | ||||||
|  |           n = n._parent | ||||||
|  |         } | ||||||
|  |         if (n === this._root) { | ||||||
|  |           n = null | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       if (n === this._root) { | ||||||
|  |         break | ||||||
|  |       } | ||||||
|  |     } while (n !== null && (n._deleted || !this._filter(n))) | ||||||
|  |     this._currentNode = n | ||||||
|  |     if (n === null) { | ||||||
|  |       return { done: true } | ||||||
|  |     } else { | ||||||
|  |       return { value: n, done: false } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,46 +1,65 @@ | |||||||
| import ID from '../Util/ID.js' | import ID from '../ID/ID.js' | ||||||
| import { default as RootID, RootFakeUserID } from '../Util/RootID.js' | import { default as RootID, RootFakeUserID } from '../ID/RootID.js' | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A BinaryDecoder handles the decoding of an ArrayBuffer. | ||||||
|  |  */ | ||||||
| export default class BinaryDecoder { | export default class BinaryDecoder { | ||||||
|  |   /** | ||||||
|  |    * @param {Uint8Array|Buffer} buffer The binary data that this instance | ||||||
|  |    *                                   decodes. | ||||||
|  |    */ | ||||||
|   constructor (buffer) { |   constructor (buffer) { | ||||||
|     if (buffer instanceof ArrayBuffer) { |     if (buffer instanceof ArrayBuffer) { | ||||||
|       this.uint8arr = new Uint8Array(buffer) |       this.uint8arr = new Uint8Array(buffer) | ||||||
|     } else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) { |     } else if ( | ||||||
|  |       buffer instanceof Uint8Array || | ||||||
|  |       ( | ||||||
|  |         typeof Buffer !== 'undefined' && buffer instanceof Buffer | ||||||
|  |       ) | ||||||
|  |     ) { | ||||||
|       this.uint8arr = buffer |       this.uint8arr = buffer | ||||||
|     } else { |     } else { | ||||||
|       throw new Error('Expected an ArrayBuffer or Uint8Array!') |       throw new Error('Expected an ArrayBuffer or Uint8Array!') | ||||||
|     } |     } | ||||||
|     this.pos = 0 |     this.pos = 0 | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Clone this decoder instance |    * Clone this decoder instance. | ||||||
|    * Optionally set a new position parameter |    * Optionally set a new position parameter. | ||||||
|    */ |    */ | ||||||
|   clone (newPos = this.pos) { |   clone (newPos = this.pos) { | ||||||
|     let decoder = new BinaryDecoder(this.uint8arr) |     let decoder = new BinaryDecoder(this.uint8arr) | ||||||
|     decoder.pos = newPos |     decoder.pos = newPos | ||||||
|     return decoder |     return decoder | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Number of bytes |    * Number of bytes. | ||||||
|    */ |    */ | ||||||
|   get length () { |   get length () { | ||||||
|     return this.uint8arr.length |     return this.uint8arr.length | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Skip one byte, jump to the next position |    * Skip one byte, jump to the next position. | ||||||
|    */ |    */ | ||||||
|   skip8 () { |   skip8 () { | ||||||
|     this.pos++ |     this.pos++ | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Read one byte as unsigned integer |    * Read one byte as unsigned integer. | ||||||
|    */ |    */ | ||||||
|   readUint8 () { |   readUint8 () { | ||||||
|     return this.uint8arr[this.pos++] |     return this.uint8arr[this.pos++] | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Read 4 bytes as unsigned integer |    * Read 4 bytes as unsigned integer. | ||||||
|  |    * | ||||||
|  |    * @return {number} An unsigned integer. | ||||||
|    */ |    */ | ||||||
|   readUint32 () { |   readUint32 () { | ||||||
|     let uint = |     let uint = | ||||||
| @ -51,19 +70,24 @@ export default class BinaryDecoder { | |||||||
|     this.pos += 4 |     this.pos += 4 | ||||||
|     return uint |     return uint | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Look ahead without incrementing position |    * Look ahead without incrementing position. | ||||||
|    * to the next byte and read it as unsigned integer |    * to the next byte and read it as unsigned integer. | ||||||
|  |    * | ||||||
|  |    * @return {number} An unsigned integer. | ||||||
|    */ |    */ | ||||||
|   peekUint8 () { |   peekUint8 () { | ||||||
|     return this.uint8arr[this.pos] |     return this.uint8arr[this.pos] | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Read unsigned integer (32bit) with variable length |    * Read unsigned integer (32bit) with variable length. | ||||||
|    * 1/8th of the storage is used as encoding overhead |    * 1/8th of the storage is used as encoding overhead. | ||||||
|    *  - numbers < 2^7 is stored in one byte |    *  * numbers < 2^7 is stored in one byte. | ||||||
|    *  - numbers < 2^14 is stored in two bytes |    *  * numbers < 2^14 is stored in two bytes. | ||||||
|    *  .. |    * | ||||||
|  |    * @return {number} An unsigned integer. | ||||||
|    */ |    */ | ||||||
|   readVarUint () { |   readVarUint () { | ||||||
|     let num = 0 |     let num = 0 | ||||||
| @ -80,9 +104,12 @@ export default class BinaryDecoder { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Read string of variable length |    * Read string of variable length | ||||||
|    * - varUint is used to store the length of the string |    * * varUint is used to store the length of the string | ||||||
|  |    * | ||||||
|  |    * @return {String} The read String. | ||||||
|    */ |    */ | ||||||
|   readVarString () { |   readVarString () { | ||||||
|     let len = this.readVarUint() |     let len = this.readVarUint() | ||||||
| @ -90,9 +117,10 @@ export default class BinaryDecoder { | |||||||
|     for (let i = 0; i < len; i++) { |     for (let i = 0; i < len; i++) { | ||||||
|       bytes[i] = this.uint8arr[this.pos++] |       bytes[i] = this.uint8arr[this.pos++] | ||||||
|     } |     } | ||||||
|     let encodedString = String.fromCodePoint(...bytes) |     let encodedString = bytes.map(b => String.fromCodePoint(b)).join('') | ||||||
|     return decodeURIComponent(escape(encodedString)) |     return decodeURIComponent(escape(encodedString)) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Look ahead and read varString without incrementing position |    * Look ahead and read varString without incrementing position | ||||||
|    */ |    */ | ||||||
| @ -102,10 +130,13 @@ export default class BinaryDecoder { | |||||||
|     this.pos = pos |     this.pos = pos | ||||||
|     return s |     return s | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Read ID |    * Read ID. | ||||||
|    * - If first varUint read is 0xFFFFFF a RootID is returned |    * * If first varUint read is 0xFFFFFF a RootID is returned. | ||||||
|    * - Otherwise an ID is returned |    * * Otherwise an ID is returned. | ||||||
|  |    * | ||||||
|  |    * @return ID | ||||||
|    */ |    */ | ||||||
|   readID () { |   readID () { | ||||||
|     let user = this.readVarUint() |     let user = this.readVarUint() | ||||||
							
								
								
									
										145
									
								
								src/Util/Binary/Encoder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/Util/Binary/Encoder.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | import { RootFakeUserID } from '../ID/RootID.js' | ||||||
|  | 
 | ||||||
|  | const bits7 = 0b1111111 | ||||||
|  | const bits8 = 0b11111111 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A BinaryEncoder handles the encoding to an ArrayBuffer. | ||||||
|  |  */ | ||||||
|  | export default class BinaryEncoder { | ||||||
|  |   constructor () { | ||||||
|  |     // TODO: implement chained Uint8Array buffers instead of Array buffer
 | ||||||
|  |     // TODO: Rewrite all methods as functions!
 | ||||||
|  |     this.data = [] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * The current length of the encoded data. | ||||||
|  |    */ | ||||||
|  |   get length () { | ||||||
|  |     return this.data.length | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * The current write pointer (the same as {@link length}). | ||||||
|  |    */ | ||||||
|  |   get pos () { | ||||||
|  |     return this.data.length | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Create an ArrayBuffer. | ||||||
|  |    * | ||||||
|  |    * @return {Uint8Array} A Uint8Array that represents the written data. | ||||||
|  |    */ | ||||||
|  |   createBuffer () { | ||||||
|  |     return Uint8Array.from(this.data).buffer | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write one byte as an unsigned integer. | ||||||
|  |    * | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   writeUint8 (num) { | ||||||
|  |     this.data.push(num & bits8) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write one byte as an unsigned Integer at a specific location. | ||||||
|  |    * | ||||||
|  |    * @param {number} pos The location where the data will be written. | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   setUint8 (pos, num) { | ||||||
|  |     this.data[pos] = num & bits8 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write two bytes as an unsigned integer. | ||||||
|  |    * | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   writeUint16 (num) { | ||||||
|  |     this.data.push(num & bits8, (num >>> 8) & bits8) | ||||||
|  |   } | ||||||
|  |   /** | ||||||
|  |    * Write two bytes as an unsigned integer at a specific location. | ||||||
|  |    * | ||||||
|  |    * @param {number} pos The location where the data will be written. | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   setUint16 (pos, num) { | ||||||
|  |     this.data[pos] = num & bits8 | ||||||
|  |     this.data[pos + 1] = (num >>> 8) & bits8 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write two bytes as an unsigned integer | ||||||
|  |    * | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   writeUint32 (num) { | ||||||
|  |     for (let i = 0; i < 4; i++) { | ||||||
|  |       this.data.push(num & bits8) | ||||||
|  |       num >>>= 8 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write two bytes as an unsigned integer at a specific location. | ||||||
|  |    * | ||||||
|  |    * @param {number} pos The location where the data will be written. | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   setUint32 (pos, num) { | ||||||
|  |     for (let i = 0; i < 4; i++) { | ||||||
|  |       this.data[pos + i] = num & bits8 | ||||||
|  |       num >>>= 8 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write a variable length unsigned integer. | ||||||
|  |    * | ||||||
|  |    * @param {number} num The number that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   writeVarUint (num) { | ||||||
|  |     while (num >= 0b10000000) { | ||||||
|  |       this.data.push(0b10000000 | (bits7 & num)) | ||||||
|  |       num >>>= 7 | ||||||
|  |     } | ||||||
|  |     this.data.push(bits7 & num) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write a variable length string. | ||||||
|  |    * | ||||||
|  |    * @param {String} str The string that is to be encoded. | ||||||
|  |    */ | ||||||
|  |   writeVarString (str) { | ||||||
|  |     let encodedString = unescape(encodeURIComponent(str)) | ||||||
|  |     let bytes = encodedString.split('').map(c => c.codePointAt()) | ||||||
|  |     let len = bytes.length | ||||||
|  |     this.writeVarUint(len) | ||||||
|  |     for (let i = 0; i < len; i++) { | ||||||
|  |       this.data.push(bytes[i]) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Write an ID at the current position. | ||||||
|  |    * | ||||||
|  |    * @param {ID} id The ID that is to be written. | ||||||
|  |    */ | ||||||
|  |   writeID (id) { | ||||||
|  |     const user = id.user | ||||||
|  |     this.writeVarUint(user) | ||||||
|  |     if (user !== RootFakeUserID) { | ||||||
|  |       this.writeVarUint(id.clock) | ||||||
|  |     } else { | ||||||
|  |       this.writeVarString(id.name) | ||||||
|  |       this.writeVarUint(id.type) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,22 +1,56 @@ | |||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * General event handler implementation. | ||||||
|  |  */ | ||||||
| export default class EventHandler { | export default class EventHandler { | ||||||
|   constructor () { |   constructor () { | ||||||
|     this.eventListeners = [] |     this.eventListeners = [] | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * To prevent memory leaks, call this method when the eventListeners won't be | ||||||
|  |    * used anymore. | ||||||
|  |    */ | ||||||
|   destroy () { |   destroy () { | ||||||
|     this.eventListeners = null |     this.eventListeners = null | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Adds an event listener that is called when | ||||||
|  |    * {@link EventHandler#callEventListeners} is called. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f The event handler. | ||||||
|  |    */ | ||||||
|   addEventListener (f) { |   addEventListener (f) { | ||||||
|     this.eventListeners.push(f) |     this.eventListeners.push(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Removes an event listener. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f The event handler that was added with | ||||||
|  |    *                     {@link EventHandler#addEventListener} | ||||||
|  |    */ | ||||||
|   removeEventListener (f) { |   removeEventListener (f) { | ||||||
|     this.eventListeners = this.eventListeners.filter(function (g) { |     this.eventListeners = this.eventListeners.filter(function (g) { | ||||||
|       return f !== g |       return f !== g | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Removes all event listeners. | ||||||
|  |    */ | ||||||
|   removeAllEventListeners () { |   removeAllEventListeners () { | ||||||
|     this.eventListeners = [] |     this.eventListeners = [] | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Call all event listeners that were added via | ||||||
|  |    * {@link EventHandler#addEventListener}. | ||||||
|  |    * | ||||||
|  |    * @param {Transaction} transaction The transaction object // TODO: do we need this?
 | ||||||
|  |    * @param {YEvent} event An event object that describes the change on a type. | ||||||
|  |    */ | ||||||
|   callEventListeners (transaction, event) { |   callEventListeners (transaction, event) { | ||||||
|     for (var i = 0; i < this.eventListeners.length; i++) { |     for (var i = 0; i < this.eventListeners.length; i++) { | ||||||
|       try { |       try { | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| 
 | 
 | ||||||
| export default class ID { | export default class ID { | ||||||
|   constructor (user, clock) { |   constructor (user, clock) { | ||||||
|     this.user = user |     this.user = user // TODO: rename to client
 | ||||||
|     this.clock = clock |     this.clock = clock | ||||||
|   } |   } | ||||||
|   clone () { |   clone () { | ||||||
| @ -1,4 +1,4 @@ | |||||||
| import { getReference } from './structReferences.js' | import { getStructReference } from '../structReferences.js' | ||||||
| 
 | 
 | ||||||
| export const RootFakeUserID = 0xFFFFFF | export const RootFakeUserID = 0xFFFFFF | ||||||
| 
 | 
 | ||||||
| @ -6,7 +6,7 @@ export default class RootID { | |||||||
|   constructor (name, typeConstructor) { |   constructor (name, typeConstructor) { | ||||||
|     this.user = RootFakeUserID |     this.user = RootFakeUserID | ||||||
|     this.name = name |     this.name = name | ||||||
|     this.type = getReference(typeConstructor) |     this.type = getStructReference(typeConstructor) | ||||||
|   } |   } | ||||||
|   equals (id) { |   equals (id) { | ||||||
|     return id !== null && id.user === this.user && id.name === this.name && id.type === this.type |     return id !== null && id.user === this.user && id.name === this.name && id.type === this.type | ||||||
| @ -1,8 +1,19 @@ | |||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Handles named events. | ||||||
|  |  */ | ||||||
| export default class NamedEventHandler { | export default class NamedEventHandler { | ||||||
|   constructor () { |   constructor () { | ||||||
|     this._eventListener = new Map() |     this._eventListener = new Map() | ||||||
|     this._stateListener = new Map() |     this._stateListener = new Map() | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Returns all listeners that listen to a specified name. | ||||||
|  |    * | ||||||
|  |    * @param {String} name The query event name. | ||||||
|  |    */ | ||||||
|   _getListener (name) { |   _getListener (name) { | ||||||
|     let listeners = this._eventListener.get(name) |     let listeners = this._eventListener.get(name) | ||||||
|     if (listeners === undefined) { |     if (listeners === undefined) { | ||||||
| @ -14,14 +25,34 @@ export default class NamedEventHandler { | |||||||
|     } |     } | ||||||
|     return listeners |     return listeners | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Adds a named event listener. The listener is removed after it has been | ||||||
|  |    * called once. | ||||||
|  |    * | ||||||
|  |    * @param {String} name The event name to listen to. | ||||||
|  |    * @param {Function} f The function that is executed when the event is fired. | ||||||
|  |    */ | ||||||
|   once (name, f) { |   once (name, f) { | ||||||
|     let listeners = this._getListener(name) |     let listeners = this._getListener(name) | ||||||
|     listeners.once.add(f) |     listeners.once.add(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Adds a named event listener. | ||||||
|  |    * | ||||||
|  |    * @param {String} name The event name to listen to. | ||||||
|  |    * @param {Function} f The function that is executed when the event is fired. | ||||||
|  |    */ | ||||||
|   on (name, f) { |   on (name, f) { | ||||||
|     let listeners = this._getListener(name) |     let listeners = this._getListener(name) | ||||||
|     listeners.on.add(f) |     listeners.on.add(f) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Init the saved state for an event name. | ||||||
|  |    */ | ||||||
|   _initStateListener (name) { |   _initStateListener (name) { | ||||||
|     let state = this._stateListener.get(name) |     let state = this._stateListener.get(name) | ||||||
|     if (state === undefined) { |     if (state === undefined) { | ||||||
| @ -33,9 +64,20 @@ export default class NamedEventHandler { | |||||||
|     } |     } | ||||||
|     return state |     return state | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Returns a Promise that is resolved when the event name is called. | ||||||
|  |    * The Promise is immediately resolved when the event name was called in the | ||||||
|  |    * past. | ||||||
|  |    */ | ||||||
|   when (name) { |   when (name) { | ||||||
|     return this._initStateListener(name).promise |     return this._initStateListener(name).promise | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Remove an event listener that was registered with either | ||||||
|  |    * {@link EventHandler#on} or {@link EventHandler#once}. | ||||||
|  |    */ | ||||||
|   off (name, f) { |   off (name, f) { | ||||||
|     if (name == null || f == null) { |     if (name == null || f == null) { | ||||||
|       throw new Error('You must specify event name and function!') |       throw new Error('You must specify event name and function!') | ||||||
| @ -46,6 +88,14 @@ export default class NamedEventHandler { | |||||||
|       listener.once.delete(f) |       listener.once.delete(f) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Emit a named event. All registered event listeners that listen to the | ||||||
|  |    * specified name will receive the event. | ||||||
|  |    * | ||||||
|  |    * @param {String} name The event name. | ||||||
|  |    * @param {Array} args The arguments that are applied to the event listener. | ||||||
|  |    */ | ||||||
|   emit (name, ...args) { |   emit (name, ...args) { | ||||||
|     this._initStateListener(name).resolve() |     this._initStateListener(name).resolve() | ||||||
|     const listener = this._eventListener.get(name) |     const listener = this._eventListener.get(name) | ||||||
|  | |||||||
| @ -1,4 +1,17 @@ | |||||||
| 
 | 
 | ||||||
|  | function rotate (tree, parent, newParent, n) { | ||||||
|  |   if (parent === null) { | ||||||
|  |     tree.root = newParent | ||||||
|  |     newParent._parent = null | ||||||
|  |   } else if (parent.left === n) { | ||||||
|  |     parent.left = newParent | ||||||
|  |   } else if (parent.right === n) { | ||||||
|  |     parent.right = newParent | ||||||
|  |   } else { | ||||||
|  |     throw new Error('The elements are wrongly connected!') | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| class N { | class N { | ||||||
|   // A created node is always red!
 |   // A created node is always red!
 | ||||||
|   constructor (val) { |   constructor (val) { | ||||||
| @ -41,21 +54,12 @@ class N { | |||||||
|     this._right = n |     this._right = n | ||||||
|   } |   } | ||||||
|   rotateLeft (tree) { |   rotateLeft (tree) { | ||||||
|     var parent = this.parent |     const parent = this.parent | ||||||
|     var newParent = this.right |     const newParent = this.right | ||||||
|     var newRight = this.right.left |     const newRight = this.right.left | ||||||
|     newParent.left = this |     newParent.left = this | ||||||
|     this.right = newRight |     this.right = newRight | ||||||
|     if (parent === null) { |     rotate(tree, parent, newParent, this) | ||||||
|       tree.root = newParent |  | ||||||
|       newParent._parent = null |  | ||||||
|     } else if (parent.left === this) { |  | ||||||
|       parent.left = newParent |  | ||||||
|     } else if (parent.right === this) { |  | ||||||
|       parent.right = newParent |  | ||||||
|     } else { |  | ||||||
|       throw new Error('The elements are wrongly connected!') |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|   next () { |   next () { | ||||||
|     if (this.right !== null) { |     if (this.right !== null) { | ||||||
| @ -90,21 +94,12 @@ class N { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   rotateRight (tree) { |   rotateRight (tree) { | ||||||
|     var parent = this.parent |     const parent = this.parent | ||||||
|     var newParent = this.left |     const newParent = this.left | ||||||
|     var newLeft = this.left.right |     const newLeft = this.left.right | ||||||
|     newParent.right = this |     newParent.right = this | ||||||
|     this.left = newLeft |     this.left = newLeft | ||||||
|     if (parent === null) { |     rotate(tree, parent, newParent, this) | ||||||
|       tree.root = newParent |  | ||||||
|       newParent._parent = null |  | ||||||
|     } else if (parent.left === this) { |  | ||||||
|       parent.left = newParent |  | ||||||
|     } else if (parent.right === this) { |  | ||||||
|       parent.right = newParent |  | ||||||
|     } else { |  | ||||||
|       throw new Error('The elements are wrongly connected!') |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|   getUncle () { |   getUncle () { | ||||||
|     // we can assume that grandparent exists when this is called!
 |     // we can assume that grandparent exists when this is called!
 | ||||||
| @ -467,5 +462,4 @@ export default class Tree { | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   flush () {} |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import ID from './ID.js' | import ID from './ID/ID.js' | ||||||
|  | import isParentOf from './isParentOf.js' | ||||||
| 
 | 
 | ||||||
| class ReverseOperation { | class ReverseOperation { | ||||||
|   constructor (y, transaction) { |   constructor (y, transaction) { | ||||||
| @ -15,16 +16,6 @@ class ReverseOperation { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function isStructInScope (y, struct, scope) { |  | ||||||
|   while (struct !== y) { |  | ||||||
|     if (struct === scope) { |  | ||||||
|       return true |  | ||||||
|     } |  | ||||||
|     struct = struct._parent |  | ||||||
|   } |  | ||||||
|   return false |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function applyReverseOperation (y, scope, reverseBuffer) { | function applyReverseOperation (y, scope, reverseBuffer) { | ||||||
|   let performedUndo = false |   let performedUndo = false | ||||||
|   y.transact(() => { |   y.transact(() => { | ||||||
| @ -38,7 +29,7 @@ function applyReverseOperation (y, scope, reverseBuffer) { | |||||||
|           while (op._deleted && op._redone !== null) { |           while (op._deleted && op._redone !== null) { | ||||||
|             op = op._redone |             op = op._redone | ||||||
|           } |           } | ||||||
|           if (op._deleted === false && isStructInScope(y, op, scope)) { |           if (op._deleted === false && isParentOf(scope, op)) { | ||||||
|             performedUndo = true |             performedUndo = true | ||||||
|             op._delete(y) |             op._delete(y) | ||||||
|           } |           } | ||||||
| @ -46,7 +37,7 @@ function applyReverseOperation (y, scope, reverseBuffer) { | |||||||
|       } |       } | ||||||
|       for (let op of undoOp.deletedStructs) { |       for (let op of undoOp.deletedStructs) { | ||||||
|         if ( |         if ( | ||||||
|           isStructInScope(y, op, scope) && |           isParentOf(scope, op) && | ||||||
|           op._parent !== y && |           op._parent !== y && | ||||||
|           ( |           ( | ||||||
|             op._id.user !== y.userID || |             op._id.user !== y.userID || | ||||||
| @ -64,7 +55,15 @@ function applyReverseOperation (y, scope, reverseBuffer) { | |||||||
|   return performedUndo |   return performedUndo | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Saves a history of locally applied operations. The UndoManager handles the | ||||||
|  |  * undoing and redoing of locally created changes. | ||||||
|  |  */ | ||||||
| export default class UndoManager { | export default class UndoManager { | ||||||
|  |   /** | ||||||
|  |    * @param {YType} scope The scope on which to listen for changes. | ||||||
|  |    * @param {Object} options Optionally provided configuration. | ||||||
|  |    */ | ||||||
|   constructor (scope, options = {}) { |   constructor (scope, options = {}) { | ||||||
|     this.options = options |     this.options = options | ||||||
|     options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout |     options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout | ||||||
| @ -76,6 +75,7 @@ export default class UndoManager { | |||||||
|     this._lastTransactionWasUndo = false |     this._lastTransactionWasUndo = false | ||||||
|     const y = scope._y |     const y = scope._y | ||||||
|     this.y = y |     this.y = y | ||||||
|  |     y._hasUndoManager = true | ||||||
|     y.on('afterTransaction', (y, transaction, remote) => { |     y.on('afterTransaction', (y, transaction, remote) => { | ||||||
|       if (!remote && transaction.changedParentTypes.has(scope)) { |       if (!remote && transaction.changedParentTypes.has(scope)) { | ||||||
|         let reverseOperation = new ReverseOperation(y, transaction) |         let reverseOperation = new ReverseOperation(y, transaction) | ||||||
| @ -109,12 +109,20 @@ export default class UndoManager { | |||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Undo the last locally created change. | ||||||
|  |    */ | ||||||
|   undo () { |   undo () { | ||||||
|     this._undoing = true |     this._undoing = true | ||||||
|     const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer) |     const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer) | ||||||
|     this._undoing = false |     this._undoing = false | ||||||
|     return performedUndo |     return performedUndo | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Redo the last locally created change. | ||||||
|  |    */ | ||||||
|   redo () { |   redo () { | ||||||
|     this._redoing = true |     this._redoing = true | ||||||
|     const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer) |     const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer) | ||||||
|  | |||||||
| @ -1,28 +1,36 @@ | |||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * YEvent describes the changes on a YType. | ||||||
|  |  */ | ||||||
| export default class YEvent { | export default class YEvent { | ||||||
|  |   /** | ||||||
|  |    * @param {YType} target The changed type. | ||||||
|  |    */ | ||||||
|   constructor (target) { |   constructor (target) { | ||||||
|  |     /** | ||||||
|  |      * The type on which this event was created on. | ||||||
|  |      * @type {YType} | ||||||
|  |      */ | ||||||
|     this.target = target |     this.target = target | ||||||
|  |     /** | ||||||
|  |      * The current target on which the observe callback is called. | ||||||
|  |      * @type {YType} | ||||||
|  |      */ | ||||||
|     this.currentTarget = target |     this.currentTarget = target | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Computes the path from `y` to the changed type. | ||||||
|  |    * | ||||||
|  |    * The following property holds: | ||||||
|  |    * @example | ||||||
|  |    *   let type = y | ||||||
|  |    *   event.path.forEach(function (dir) { | ||||||
|  |    *     type = type.get(dir) | ||||||
|  |    *   }) | ||||||
|  |    *   type === event.target // => true
 | ||||||
|  |    */ | ||||||
|   get path () { |   get path () { | ||||||
|     const path = [] |     return this.currentTarget.getPathTo(this.target) | ||||||
|     let type = this.target |  | ||||||
|     const y = type._y |  | ||||||
|     while (type !== this.currentTarget && type !== y) { |  | ||||||
|       let parent = type._parent |  | ||||||
|       if (type._parentSub !== null) { |  | ||||||
|         path.unshift(type._parentSub) |  | ||||||
|       } else { |  | ||||||
|         // parent is array-ish
 |  | ||||||
|         for (let [i, child] of parent) { |  | ||||||
|           if (child === type) { |  | ||||||
|             path.unshift(i) |  | ||||||
|             break |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       type = parent |  | ||||||
|     } |  | ||||||
|     return path |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| 
 | 
 | ||||||
| import ID from '../Util/ID.js' | import ID from '../Util/ID/ID.js' | ||||||
| import ItemJSON from '../Struct/ItemJSON.js' | import ItemJSON from '../Struct/ItemJSON.js' | ||||||
| import ItemString from '../Struct/ItemString.js' | import ItemString from '../Struct/ItemString.js' | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| /* global crypto */ | /* global crypto */ | ||||||
| 
 | 
 | ||||||
| export function generateUserID () { | export function generateRandomUint32 () { | ||||||
|   if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) { |   if (typeof crypto !== 'undefined' && crypto.getRandomValue != null) { | ||||||
|     // browser
 |     // browser
 | ||||||
|     let arr = new Uint32Array(1) |     let arr = new Uint32Array(1) | ||||||
							
								
								
									
										20
									
								
								src/Util/isParentOf.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/Util/isParentOf.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Check if `parent` is a parent of `child`. | ||||||
|  |  * | ||||||
|  |  * @param {Type} parent | ||||||
|  |  * @param {Type} child | ||||||
|  |  * @return {Boolean} Whether `parent` is a parent of `child`. | ||||||
|  |  * | ||||||
|  |  * @public | ||||||
|  |  */ | ||||||
|  | export default function isParentOf (parent, child) { | ||||||
|  |   child = child._parent | ||||||
|  |   while (child !== null) { | ||||||
|  |     if (child === parent) { | ||||||
|  |       return true | ||||||
|  |     } | ||||||
|  |     child = child._parent | ||||||
|  |   } | ||||||
|  |   return false | ||||||
|  | } | ||||||
| @ -1,4 +1,22 @@ | |||||||
| 
 | 
 | ||||||
|  | // TODO: rename mutex
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Creates a mutual exclude function with the following property: | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * const mutualExclude = createMutualExclude() | ||||||
|  |  * mutualExclude(function () { | ||||||
|  |  *   // This function is immediately executed
 | ||||||
|  |  *   mutualExclude(function () { | ||||||
|  |  *     // This function is never executed, as it is called with the same
 | ||||||
|  |  *     // mutualExclude
 | ||||||
|  |  *   }) | ||||||
|  |  * }) | ||||||
|  |  * | ||||||
|  |  * @return {Function} A mutual exclude function | ||||||
|  |  * @public | ||||||
|  |  */ | ||||||
| export function createMutualExclude () { | export function createMutualExclude () { | ||||||
|   var token = true |   var token = true | ||||||
|   return function mutualExclude (f) { |   return function mutualExclude (f) { | ||||||
|  | |||||||
| @ -1,7 +1,46 @@ | |||||||
| import ID from './ID.js' | import ID from './ID/ID.js' | ||||||
| import RootID from './RootID.js' | import RootID from './ID/RootID.js' | ||||||
|  | import GC from '../Struct/GC.js' | ||||||
| 
 | 
 | ||||||
|  | // TODO: Implement function to describe ranges
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * A relative position that is based on the Yjs model. In contrast to an | ||||||
|  |  * absolute position (position by index), the relative position can be | ||||||
|  |  * recomputed when remote changes are received. For example: | ||||||
|  |  * | ||||||
|  |  * ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position. | ||||||
|  |  * | ||||||
|  |  * A relative cursor position can be obtained with the function | ||||||
|  |  * {@link getRelativePosition} and it can be transformed to an absolute position | ||||||
|  |  * with {@link fromRelativePosition}. | ||||||
|  |  * | ||||||
|  |  * Pro tip: Use this to implement shared cursor locations in YText or YXml! | ||||||
|  |  * The relative position is {@link encodable}, so you can send it to other | ||||||
|  |  * clients. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * // Current cursor position is at position 10
 | ||||||
|  |  * let relativePosition = getRelativePosition(yText, 10) | ||||||
|  |  * // modify yText
 | ||||||
|  |  * yText.insert(0, 'abc') | ||||||
|  |  * yText.delete(3, 10) | ||||||
|  |  * // Compute the cursor position
 | ||||||
|  |  * let absolutePosition = fromRelativePosition(y, relativePosition) | ||||||
|  |  * absolutePosition.type // => yText
 | ||||||
|  |  * console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3
 | ||||||
|  |  * | ||||||
|  |  * @typedef {encodable} RelativePosition | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Create a relativePosition based on a absolute position. | ||||||
|  |  * | ||||||
|  |  * @param {YType} type The base type (e.g. YText or YArray). | ||||||
|  |  * @param {Integer} offset The absolute position. | ||||||
|  |  */ | ||||||
| export function getRelativePosition (type, offset) { | export function getRelativePosition (type, offset) { | ||||||
|  |   // TODO: rename to createRelativePosition
 | ||||||
|   let t = type._start |   let t = type._start | ||||||
|   while (t !== null) { |   while (t !== null) { | ||||||
|     if (t._deleted === false) { |     if (t._deleted === false) { | ||||||
| @ -15,6 +54,20 @@ export function getRelativePosition (type, offset) { | |||||||
|   return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null] |   return ['endof', type._id.user, type._id.clock || null, type._id.name || null, type._id.type || null] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @typedef {Object} AbsolutePosition The result of {@link fromRelativePosition} | ||||||
|  |  * @property {YType} type The type on which to apply the absolute position. | ||||||
|  |  * @property {Integer} offset The absolute offset.r | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Transforms a relative position back to a relative position. | ||||||
|  |  * | ||||||
|  |  * @param {Y} y The Yjs instance in which to query for the absolute position. | ||||||
|  |  * @param {RelativePosition} rpos The relative position. | ||||||
|  |  * @return {AbsolutePosition} The absolute position in the Yjs model | ||||||
|  |  *                            (type + offset). | ||||||
|  |  */ | ||||||
| export function fromRelativePosition (y, rpos) { | export function fromRelativePosition (y, rpos) { | ||||||
|   if (rpos[0] === 'endof') { |   if (rpos[0] === 'endof') { | ||||||
|     let id |     let id | ||||||
| @ -24,6 +77,9 @@ export function fromRelativePosition (y, rpos) { | |||||||
|       id = new RootID(rpos[3], rpos[4]) |       id = new RootID(rpos[3], rpos[4]) | ||||||
|     } |     } | ||||||
|     const type = y.os.get(id) |     const type = y.os.get(id) | ||||||
|  |     if (type === null || type.constructor === GC) { | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|     return { |     return { | ||||||
|       type, |       type, | ||||||
|       offset: type.length |       offset: type.length | ||||||
| @ -32,7 +88,7 @@ export function fromRelativePosition (y, rpos) { | |||||||
|     let offset = 0 |     let offset = 0 | ||||||
|     let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val |     let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val | ||||||
|     const parent = struct._parent |     const parent = struct._parent | ||||||
|     if (parent._deleted) { |     if (struct.constructor === GC || parent._deleted) { | ||||||
|       return null |       return null | ||||||
|     } |     } | ||||||
|     if (!struct._deleted) { |     if (!struct._deleted) { | ||||||
|  | |||||||
| @ -1,4 +1,32 @@ | |||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A SimpleDiff describes a change on a String. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * console.log(a) // the old value
 | ||||||
|  |  * console.log(b) // the updated value
 | ||||||
|  |  * // Apply changes of diff (pseudocode)
 | ||||||
|  |  * a.remove(diff.pos, diff.remove) // Remove `diff.remove` characters
 | ||||||
|  |  * a.insert(diff.pos, diff.insert) // Insert `diff.insert`
 | ||||||
|  |  * a === b // values match
 | ||||||
|  |  * | ||||||
|  |  * @typedef {Object} SimpleDiff | ||||||
|  |  * @property {Number} pos The index where changes were applied | ||||||
|  |  * @property {Number} delete The number of characters to delete starting | ||||||
|  |  *                                  at `index`. | ||||||
|  |  * @property {String} insert The new text to insert at `index` after applying | ||||||
|  |  *                           `delete` | ||||||
|  |  */ | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Create a diff between two strings. This diff implementation is highly | ||||||
|  |  * efficient, but not very sophisticated. | ||||||
|  |  * | ||||||
|  |  * @public | ||||||
|  |  * @param {String} a The old version of the string | ||||||
|  |  * @param {String} b The updated version of the string | ||||||
|  |  * @return {SimpleDiff} The diff description. | ||||||
|  |  */ | ||||||
| export default function simpleDiff (a, b) { | export default function simpleDiff (a, b) { | ||||||
|   let left = 0 // number of same characters counting from left
 |   let left = 0 // number of same characters counting from left
 | ||||||
|   let right = 0 // number of same characters counting from right
 |   let right = 0 // number of same characters counting from right
 | ||||||
| @ -12,7 +40,7 @@ export default function simpleDiff (a, b) { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   return { |   return { | ||||||
|     pos: left, |     pos: left, // TODO: rename to index (also in type above)
 | ||||||
|     remove: a.length - left - right, |     remove: a.length - left - right, | ||||||
|     insert: b.slice(left, b.length - right) |     insert: b.slice(left, b.length - right) | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -1,36 +1,59 @@ | |||||||
| import YArray from '../Type/YArray.js' | import YArray from '../Types/YArray/YArray.js' | ||||||
| import YMap from '../Type/YMap.js' | import YMap from '../Types/YMap/YMap.js' | ||||||
| import YText from '../Type/YText.js' | import YText from '../Types/YText/YText.js' | ||||||
| import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Type/y-xml/y-xml.js' | import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Types/YXml/YXml.js' | ||||||
| 
 | 
 | ||||||
| import Delete from '../Struct/Delete.js' | import Delete from '../Struct/Delete.js' | ||||||
| import ItemJSON from '../Struct/ItemJSON.js' | import ItemJSON from '../Struct/ItemJSON.js' | ||||||
| import ItemString from '../Struct/ItemString.js' | import ItemString from '../Struct/ItemString.js' | ||||||
|  | import ItemFormat from '../Struct/ItemFormat.js' | ||||||
|  | import ItemEmbed from '../Struct/ItemEmbed.js' | ||||||
|  | import GC from '../Struct/GC.js' | ||||||
| 
 | 
 | ||||||
| const structs = new Map() | const structs = new Map() | ||||||
| const references = new Map() | const references = new Map() | ||||||
| 
 | 
 | ||||||
| export function addStruct (reference, structConstructor) { | /** | ||||||
|  |  * Register a new Yjs types. The same type must be defined with the same | ||||||
|  |  * reference on all clients! | ||||||
|  |  * | ||||||
|  |  * @param {Number} reference | ||||||
|  |  * @param {class} structConstructor | ||||||
|  |  * | ||||||
|  |  * @public | ||||||
|  |  */ | ||||||
|  | export function registerStruct (reference, structConstructor) { | ||||||
|   structs.set(reference, structConstructor) |   structs.set(reference, structConstructor) | ||||||
|   references.set(structConstructor, reference) |   references.set(structConstructor, reference) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
| export function getStruct (reference) { | export function getStruct (reference) { | ||||||
|   return structs.get(reference) |   return structs.get(reference) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function getReference (typeConstructor) { | /** | ||||||
|  |  * @private | ||||||
|  |  */ | ||||||
|  | export function getStructReference (typeConstructor) { | ||||||
|   return references.get(typeConstructor) |   return references.get(typeConstructor) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| addStruct(0, ItemJSON) | // TODO: reorder (Item* should have low numbers)
 | ||||||
| addStruct(1, ItemString) | registerStruct(0, ItemJSON) | ||||||
| addStruct(2, Delete) | registerStruct(1, ItemString) | ||||||
|  | registerStruct(10, ItemFormat) | ||||||
|  | registerStruct(11, ItemEmbed) | ||||||
|  | registerStruct(2, Delete) | ||||||
| 
 | 
 | ||||||
| addStruct(3, YArray) | registerStruct(3, YArray) | ||||||
| addStruct(4, YMap) | registerStruct(4, YMap) | ||||||
| addStruct(5, YText) | registerStruct(5, YText) | ||||||
| addStruct(6, YXmlFragment) | registerStruct(6, YXmlFragment) | ||||||
| addStruct(7, YXmlElement) | registerStruct(7, YXmlElement) | ||||||
| addStruct(8, YXmlText) | registerStruct(8, YXmlText) | ||||||
| addStruct(9, YXmlHook) | registerStruct(9, YXmlHook) | ||||||
|  | 
 | ||||||
|  | registerStruct(12, GC) | ||||||
|  | |||||||
| @ -1,33 +0,0 @@ | |||||||
| 
 |  | ||||||
| import YMap from '../Type/YMap' |  | ||||||
| import YArray from '../Type/YArray' |  | ||||||
| 
 |  | ||||||
| export function writeObjectToYMap (object, type) { |  | ||||||
|   for (var key in object) { |  | ||||||
|     var val = object[key] |  | ||||||
|     if (Array.isArray(val)) { |  | ||||||
|       type.set(key, YArray) |  | ||||||
|       writeArrayToYArray(val, type.get(key)) |  | ||||||
|     } else if (typeof val === 'object') { |  | ||||||
|       type.set(key, YMap) |  | ||||||
|       writeObjectToYMap(val, type.get(key)) |  | ||||||
|     } else { |  | ||||||
|       type.set(key, val) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export function writeArrayToYArray (array, type) { |  | ||||||
|   for (var i = array.length - 1; i >= 0; i--) { |  | ||||||
|     var val = array[i] |  | ||||||
|     if (Array.isArray(val)) { |  | ||||||
|       type.insert(0, [YArray]) |  | ||||||
|       writeArrayToYArray(val, type.get(0)) |  | ||||||
|     } else if (typeof val === 'object') { |  | ||||||
|       type.insert(0, [YMap]) |  | ||||||
|       writeObjectToYMap(val, type.get(0)) |  | ||||||
|     } else { |  | ||||||
|       type.insert(0, [val]) |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
							
								
								
									
										59
									
								
								src/Y.dist.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/Y.dist.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | |||||||
|  | 
 | ||||||
|  | import Y from './Y.js' | ||||||
|  | import UndoManager from './Util/UndoManager.js' | ||||||
|  | import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js' | ||||||
|  | 
 | ||||||
|  | import { messageToString, messageToRoomname } from './MessageHandler/messageToString.js' | ||||||
|  | 
 | ||||||
|  | import Connector from './Connector.js' | ||||||
|  | import Persistence from './Persistence.js' | ||||||
|  | import YArray from './Types/YArray/YArray.js' | ||||||
|  | import YMap from './Types/YMap/YMap.js' | ||||||
|  | import YText from './Types/YText/YText.js' | ||||||
|  | import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Types/YXml/YXml.js' | ||||||
|  | import BinaryDecoder from './Util/Binary/Decoder.js' | ||||||
|  | import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js' | ||||||
|  | import { registerStruct } from './Util/structReferences.js' | ||||||
|  | import TextareaBinding from './Bindings/TextareaBinding/TextareaBinding.js' | ||||||
|  | import QuillBinding from './Bindings/QuillBinding/QuillBinding.js' | ||||||
|  | import DomBinding from './Bindings/DomBinding/DomBinding.js' | ||||||
|  | import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' | ||||||
|  | 
 | ||||||
|  | import debug from 'debug' | ||||||
|  | import domToType from './Bindings/DomBinding/domToType.js' | ||||||
|  | import { domsToTypes, switchAssociation } from './Bindings/DomBinding/util.js' | ||||||
|  | 
 | ||||||
|  | // TODO: The following assignments should be moved to yjs-dist
 | ||||||
|  | Y.AbstractConnector = Connector | ||||||
|  | Y.AbstractPersistence = Persistence | ||||||
|  | Y.Array = YArray | ||||||
|  | Y.Map = YMap | ||||||
|  | Y.Text = YText | ||||||
|  | Y.XmlElement = YXmlElement | ||||||
|  | Y.XmlFragment = YXmlFragment | ||||||
|  | Y.XmlText = YXmlText | ||||||
|  | Y.XmlHook = YXmlHook | ||||||
|  | 
 | ||||||
|  | Y.TextareaBinding = TextareaBinding | ||||||
|  | Y.QuillBinding = QuillBinding | ||||||
|  | Y.DomBinding = DomBinding | ||||||
|  | 
 | ||||||
|  | DomBinding.domToType = domToType | ||||||
|  | DomBinding.domsToTypes = domsToTypes | ||||||
|  | DomBinding.switchAssociation = switchAssociation | ||||||
|  | 
 | ||||||
|  | Y.utils = { | ||||||
|  |   BinaryDecoder, | ||||||
|  |   UndoManager, | ||||||
|  |   getRelativePosition, | ||||||
|  |   fromRelativePosition, | ||||||
|  |   registerStruct, | ||||||
|  |   integrateRemoteStructs, | ||||||
|  |   toBinary, | ||||||
|  |   fromBinary | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | Y.debug = debug | ||||||
|  | debug.formatters.Y = messageToString | ||||||
|  | debug.formatters.y = messageToRoomname | ||||||
|  | export default Y | ||||||
							
								
								
									
										178
									
								
								src/Y.js
									
									
									
									
									
								
							
							
						
						
									
										178
									
								
								src/Y.js
									
									
									
									
									
								
							| @ -1,41 +1,51 @@ | |||||||
| import DeleteStore from './Store/DeleteStore.js' | import DeleteStore from './Store/DeleteStore.js' | ||||||
| import OperationStore from './Store/OperationStore.js' | import OperationStore from './Store/OperationStore.js' | ||||||
| import StateStore from './Store/StateStore.js' | import StateStore from './Store/StateStore.js' | ||||||
| import { generateUserID } from './Util/generateUserID.js' | import { generateRandomUint32 } from './Util/generateRandomUint32.js' | ||||||
| import RootID from './Util/RootID.js' | import RootID from './Util/ID/RootID.js' | ||||||
| import NamedEventHandler from './Util/NamedEventHandler.js' | import NamedEventHandler from './Util/NamedEventHandler.js' | ||||||
| import UndoManager from './Util/UndoManager.js' |  | ||||||
| import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js' |  | ||||||
| 
 |  | ||||||
| import { messageToString, messageToRoomname } from './MessageHandler/messageToString.js' |  | ||||||
| 
 |  | ||||||
| import Connector from './Connector.js' |  | ||||||
| import Persistence from './Persistence.js' |  | ||||||
| import YArray from './Type/YArray.js' |  | ||||||
| import YMap from './Type/YMap.js' |  | ||||||
| import YText from './Type/YText.js' |  | ||||||
| import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Type/y-xml/y-xml.js' |  | ||||||
| import BinaryDecoder from './Binary/Decoder.js' |  | ||||||
| import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js' |  | ||||||
| import { addStruct as addType } from './Util/structReferences.js' |  | ||||||
| 
 |  | ||||||
| import debug from 'debug' |  | ||||||
| import Transaction from './Transaction.js' | import Transaction from './Transaction.js' | ||||||
| 
 | 
 | ||||||
| import TextareaBinding from './Binding/TextareaBinding.js' | export { default as DomBinding } from './Bindings/DomBinding/DomBinding.js' | ||||||
| 
 | 
 | ||||||
| import { toBinary, fromBinary } from './MessageHandler/binaryEncode.js' | /** | ||||||
|  |  * Anything that can be encoded with `JSON.stringify` and can be decoded with | ||||||
|  |  * `JSON.parse`. | ||||||
|  |  * | ||||||
|  |  * The following property should hold: | ||||||
|  |  * `JSON.parse(JSON.stringify(key))===key` | ||||||
|  |  * | ||||||
|  |  * At the moment the only safe values are number and string. | ||||||
|  |  * | ||||||
|  |  * @typedef {(number|string)} encodable | ||||||
|  |  */ | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * A Yjs instance handles the state of shared data. | ||||||
|  |  * | ||||||
|  |  * @param {string} room Users in the same room share the same content | ||||||
|  |  * @param {Object} opts Connector definition | ||||||
|  |  * @param {AbstractPersistence} persistence Persistence adapter instance | ||||||
|  |  */ | ||||||
| export default class Y extends NamedEventHandler { | export default class Y extends NamedEventHandler { | ||||||
|   constructor (room, opts, persistence) { |   constructor (room, opts, persistence) { | ||||||
|     super() |     super() | ||||||
|  |     /** | ||||||
|  |      * The room name that this Yjs instance connects to. | ||||||
|  |      * @type {String} | ||||||
|  |      */ | ||||||
|     this.room = room |     this.room = room | ||||||
|     if (opts != null) { |     if (opts != null) { | ||||||
|       opts.connector.room = room |       opts.connector.room = room | ||||||
|     } |     } | ||||||
|     this._contentReady = false |     this._contentReady = false | ||||||
|     this._opts = opts |     this._opts = opts | ||||||
|     this.userID = generateUserID() |     if (typeof opts.userID !== 'number') { | ||||||
|  |       this.userID = generateRandomUint32() | ||||||
|  |     } else { | ||||||
|  |       this.userID = opts.userID | ||||||
|  |     } | ||||||
|  |     // TODO: This should be a Map so we can use encodables as keys
 | ||||||
|     this.share = {} |     this.share = {} | ||||||
|     this.ds = new DeleteStore(this) |     this.ds = new DeleteStore(this) | ||||||
|     this.os = new OperationStore(this) |     this.os = new OperationStore(this) | ||||||
| @ -43,6 +53,10 @@ export default class Y extends NamedEventHandler { | |||||||
|     this._missingStructs = new Map() |     this._missingStructs = new Map() | ||||||
|     this._readyToIntegrate = [] |     this._readyToIntegrate = [] | ||||||
|     this._transaction = null |     this._transaction = null | ||||||
|  |     /** | ||||||
|  |      * The {@link AbstractConnector}.that is used by this Yjs instance. | ||||||
|  |      * @type {AbstractConnector} | ||||||
|  |      */ | ||||||
|     this.connector = null |     this.connector = null | ||||||
|     this.connected = false |     this.connected = false | ||||||
|     let initConnection = () => { |     let initConnection = () => { | ||||||
| @ -52,13 +66,20 @@ export default class Y extends NamedEventHandler { | |||||||
|         this.emit('connectorReady') |         this.emit('connectorReady') | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |     /** | ||||||
|  |      * The {@link AbstractPersistence} that is used by this Yjs instance. | ||||||
|  |      * @type {AbstractPersistence} | ||||||
|  |      */ | ||||||
|  |     this.persistence = null | ||||||
|     if (persistence != null) { |     if (persistence != null) { | ||||||
|       this.persistence = persistence |       this.persistence = persistence | ||||||
|       persistence._init(this).then(initConnection) |       persistence._init(this).then(initConnection) | ||||||
|     } else { |     } else { | ||||||
|       this.persistence = null |  | ||||||
|       initConnection() |       initConnection() | ||||||
|     } |     } | ||||||
|  |     // for compatibility with isParentOf
 | ||||||
|  |     this._parent = null | ||||||
|  |     this._hasUndoManager = false | ||||||
|   } |   } | ||||||
|   _setContentReady () { |   _setContentReady () { | ||||||
|     if (!this._contentReady) { |     if (!this._contentReady) { | ||||||
| @ -76,6 +97,17 @@ export default class Y extends NamedEventHandler { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   _beforeChange () {} |   _beforeChange () {} | ||||||
|  |   /** | ||||||
|  |    * Changes that happen inside of a transaction are bundled. This means that | ||||||
|  |    * the observer fires _after_ the transaction is finished and that all changes | ||||||
|  |    * that happened inside of the transaction are sent as one message to the | ||||||
|  |    * other peers. | ||||||
|  |    * | ||||||
|  |    * @param {Function} f The function that should be executed as a transaction | ||||||
|  |    * @param {?Boolean} remote Optional. Whether this transaction is initiated by | ||||||
|  |    *                          a remote peer. This should not be set manually! | ||||||
|  |    *                          Defaults to false. | ||||||
|  |    */ | ||||||
|   transact (f, remote = false) { |   transact (f, remote = false) { | ||||||
|     let initialCall = this._transaction === null |     let initialCall = this._transaction === null | ||||||
|     if (initialCall) { |     if (initialCall) { | ||||||
| @ -116,13 +148,55 @@ export default class Y extends NamedEventHandler { | |||||||
|       this.emit('afterTransaction', this, transaction, remote) |       this.emit('afterTransaction', this, transaction, remote) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   // fake _start for root properties (y.set('name', type))
 | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Fake _start for root properties (y.set('name', type)) | ||||||
|  |    */ | ||||||
|   get _start () { |   get _start () { | ||||||
|     return null |     return null | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @private | ||||||
|  |    * Fake _start for root properties (y.set('name', type)) | ||||||
|  |    */ | ||||||
|   set _start (start) { |   set _start (start) { | ||||||
|     return null |     return null | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Define a shared data type. | ||||||
|  |    * | ||||||
|  |    * Multiple calls of `y.define(name, TypeConstructor)` yield the same result | ||||||
|  |    * and do not overwrite each other. I.e. | ||||||
|  |    * `y.define(name, type) === y.define(name, type)` | ||||||
|  |    * | ||||||
|  |    * After this method is called, the type is also available on `y.share[name]`. | ||||||
|  |    * | ||||||
|  |    * *Best Practices:* | ||||||
|  |    * Either define all types right after the Yjs instance is created or always | ||||||
|  |    * use `y.define(..)` when accessing a type. | ||||||
|  |    * | ||||||
|  |    * @example | ||||||
|  |    *   // Option 1
 | ||||||
|  |    *   const y = new Y(..) | ||||||
|  |    *   y.define('myArray', YArray) | ||||||
|  |    *   y.define('myMap', YMap) | ||||||
|  |    *   // .. when accessing the type use y.share[name]
 | ||||||
|  |    *   y.share.myArray.insert(..) | ||||||
|  |    *   y.share.myMap.set(..) | ||||||
|  |    * | ||||||
|  |    *   // Option2
 | ||||||
|  |    *   const y = new Y(..) | ||||||
|  |    *   // .. when accessing the type use `y.define(..)`
 | ||||||
|  |    *   y.define('myArray', YArray).insert(..) | ||||||
|  |    *   y.define('myMap', YMap).set(..) | ||||||
|  |    * | ||||||
|  |    * @param {String} name | ||||||
|  |    * @param {YType Constructor} TypeConstructor The constructor of the type definition | ||||||
|  |    * @returns {YType} The created type | ||||||
|  |    */ | ||||||
|   define (name, TypeConstructor) { |   define (name, TypeConstructor) { | ||||||
|     let id = new RootID(name, TypeConstructor) |     let id = new RootID(name, TypeConstructor) | ||||||
|     let type = this.os.get(id) |     let type = this.os.get(id) | ||||||
| @ -133,9 +207,23 @@ export default class Y extends NamedEventHandler { | |||||||
|     } |     } | ||||||
|     return type |     return type | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get a defined type. The type must be defined locally. First define the | ||||||
|  |    * type with {@link define}. | ||||||
|  |    * | ||||||
|  |    * This returns the same value as `y.share[name]` | ||||||
|  |    * | ||||||
|  |    * @param {String} name The typename | ||||||
|  |    */ | ||||||
|   get (name) { |   get (name) { | ||||||
|     return this.share[name] |     return this.share[name] | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Disconnect this Yjs Instance from the network. The connector will | ||||||
|  |    * unsubscribe from the room and document updates are not shared anymore. | ||||||
|  |    */ | ||||||
|   disconnect () { |   disconnect () { | ||||||
|     if (this.connected) { |     if (this.connected) { | ||||||
|       this.connected = false |       this.connected = false | ||||||
| @ -144,6 +232,10 @@ export default class Y extends NamedEventHandler { | |||||||
|       return Promise.resolve() |       return Promise.resolve() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * If disconnected, tell the connector to reconnect to the room. | ||||||
|  |    */ | ||||||
|   reconnect () { |   reconnect () { | ||||||
|     if (!this.connected) { |     if (!this.connected) { | ||||||
|       this.connected = true |       this.connected = true | ||||||
| @ -152,6 +244,11 @@ export default class Y extends NamedEventHandler { | |||||||
|       return Promise.resolve() |       return Promise.resolve() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Disconnect from the room, and destroy all traces of this Yjs instance. | ||||||
|  |    * Persisted data will remain until removed by the persistence adapter. | ||||||
|  |    */ | ||||||
|   destroy () { |   destroy () { | ||||||
|     super.destroy() |     super.destroy() | ||||||
|     this.share = null |     this.share = null | ||||||
| @ -170,13 +267,6 @@ export default class Y extends NamedEventHandler { | |||||||
|     this.ds = null |     this.ds = null | ||||||
|     this.ss = null |     this.ss = null | ||||||
|   } |   } | ||||||
|   whenSynced () { |  | ||||||
|     return new Promise(resolve => { |  | ||||||
|       this.once('synced', () => { |  | ||||||
|         resolve() |  | ||||||
|       }) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| Y.extend = function extendYjs () { | Y.extend = function extendYjs () { | ||||||
| @ -189,31 +279,3 @@ Y.extend = function extendYjs () { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // TODO: The following assignments should be moved to yjs-dist
 |  | ||||||
| Y.AbstractConnector = Connector |  | ||||||
| Y.AbstractPersistence = Persistence |  | ||||||
| Y.Array = YArray |  | ||||||
| Y.Map = YMap |  | ||||||
| Y.Text = YText |  | ||||||
| Y.XmlElement = YXmlElement |  | ||||||
| Y.XmlFragment = YXmlFragment |  | ||||||
| Y.XmlText = YXmlText |  | ||||||
| Y.XmlHook = YXmlHook |  | ||||||
| 
 |  | ||||||
| Y.TextareaBinding = TextareaBinding |  | ||||||
| 
 |  | ||||||
| Y.utils = { |  | ||||||
|   BinaryDecoder, |  | ||||||
|   UndoManager, |  | ||||||
|   getRelativePosition, |  | ||||||
|   fromRelativePosition, |  | ||||||
|   addType, |  | ||||||
|   integrateRemoteStructs, |  | ||||||
|   toBinary, |  | ||||||
|   fromBinary |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| Y.debug = debug |  | ||||||
| debug.formatters.Y = messageToString |  | ||||||
| debug.formatters.y = messageToRoomname |  | ||||||
|  | |||||||
| @ -1,3 +0,0 @@ | |||||||
| 
 |  | ||||||
| import Y from './Y.js' |  | ||||||
| export default Y |  | ||||||
							
								
								
									
										82
									
								
								test/DeleteStore.tests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								test/DeleteStore.tests.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,82 @@ | |||||||
|  | import { test } from '../node_modules/cutest/cutest.mjs' | ||||||
|  | import Chance from 'chance' | ||||||
|  | import DeleteStore from '../src/Store/DeleteStore.js' | ||||||
|  | import ID from '../src/Util/ID/ID.js' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Converts a DS to an array of length 10. | ||||||
|  |  * | ||||||
|  |  * @example | ||||||
|  |  * const ds = new DeleteStore() | ||||||
|  |  * ds.mark(new ID(0, 0), 1, false) | ||||||
|  |  * ds.mark(new ID(0, 1), 1, true) | ||||||
|  |  * ds.mark(new ID(0, 3), 1, false) | ||||||
|  |  * dsToArray(ds) // => [0, 1, undefined, 0]
 | ||||||
|  |  * | ||||||
|  |  * @return {Array<(undefined|number)>} Array of numbers indicating if array[i] is deleted (0), garbage collected (1), or undeleted (undefined). | ||||||
|  |  */ | ||||||
|  | function dsToArray (ds) { | ||||||
|  |   const array = [] | ||||||
|  |   let i = 0 | ||||||
|  |   ds.iterate(null, null, function (n) { | ||||||
|  |     // fill with null
 | ||||||
|  |     while (i < n._id.clock) { | ||||||
|  |       array[i++] = null | ||||||
|  |     } | ||||||
|  |     while (i < n._id.clock + n.len) { | ||||||
|  |       array[i++] = n.gc ? 1 : 0 | ||||||
|  |     } | ||||||
|  |   }) | ||||||
|  |   return array | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | test('DeleteStore', async function ds1 (t) { | ||||||
|  |   const ds = new DeleteStore() | ||||||
|  |   ds.mark(new ID(0, 1), 1, false) | ||||||
|  |   ds.mark(new ID(0, 2), 1, false) | ||||||
|  |   ds.mark(new ID(0, 3), 1, false) | ||||||
|  |   t.compare(dsToArray(ds), [null, 0, 0, 0]) | ||||||
|  |   ds.mark(new ID(0, 2), 1, true) | ||||||
|  |   t.compare(dsToArray(ds), [null, 0, 1, 0]) | ||||||
|  |   ds.mark(new ID(0, 1), 1, true) | ||||||
|  |   t.compare(dsToArray(ds), [null, 1, 1, 0]) | ||||||
|  |   ds.mark(new ID(0, 3), 1, true) | ||||||
|  |   t.compare(dsToArray(ds), [null, 1, 1, 1]) | ||||||
|  |   ds.mark(new ID(0, 5), 1, true) | ||||||
|  |   ds.mark(new ID(0, 4), 1, true) | ||||||
|  |   t.compare(dsToArray(ds), [null, 1, 1, 1, 1, 1]) | ||||||
|  |   ds.mark(new ID(0, 0), 3, false) | ||||||
|  |   t.compare(dsToArray(ds), [0, 0, 0, 1, 1, 1]) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | test('random DeleteStore tests', async function randomDS (t) { | ||||||
|  |   const chance = new Chance(t.getSeed() * 1000000000) | ||||||
|  |   const ds = new DeleteStore() | ||||||
|  |   const dsArray = [] | ||||||
|  |   for (let i = 0; i < 200; i++) { | ||||||
|  |     const pos = chance.integer({ min: 0, max: 10 }) | ||||||
|  |     const len = chance.integer({ min: 0, max: 4 }) | ||||||
|  |     const gc = chance.bool() | ||||||
|  |     ds.mark(new ID(0, pos), len, gc) | ||||||
|  |     for (let j = 0; j < len; j++) { | ||||||
|  |       dsArray[pos + j] = gc ? 1 : 0 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   // fill empty fields
 | ||||||
|  |   for (let i = 0; i < dsArray.length; i++) { | ||||||
|  |     if (dsArray[i] !== 0 && dsArray[i] !== 1) { | ||||||
|  |       dsArray[i] = null | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   t.compare(dsToArray(ds), dsArray, 'expected DS result') | ||||||
|  |   let size = 0 | ||||||
|  |   let lastEl = null | ||||||
|  |   for (let i = 0; i < dsArray.length; i++) { | ||||||
|  |     let el = dsArray[i] | ||||||
|  |     if (lastEl !== el && el !== null) { | ||||||
|  |       size++ | ||||||
|  |     } | ||||||
|  |     lastEl = el | ||||||
|  |   } | ||||||
|  |   t.compare(size, ds.length, 'expected ds size') | ||||||
|  | }) | ||||||
| @ -1,7 +1,7 @@ | |||||||
| import { test } from '../node_modules/cutest/cutest.mjs' | import { test } from '../node_modules/cutest/cutest.mjs' | ||||||
| import BinaryEncoder from '../src/Binary/Encoder.js' | import BinaryEncoder from '../src/Util/Binary/Encoder.js' | ||||||
| import BinaryDecoder from '../src/Binary/Decoder.js' | import BinaryDecoder from '../src/Util/Binary/Decoder.js' | ||||||
| import { generateUserID } from '../src/Util/generateUserID.js' | import { generateRandomUint32 } from '../src/Util/generateRandomUint32.js' | ||||||
| import Chance from 'chance' | import Chance from 'chance' | ||||||
| 
 | 
 | ||||||
| function testEncoding (t, write, read, val) { | function testEncoding (t, write, read, val) { | ||||||
| @ -43,7 +43,7 @@ test('varUint random', async function varUintRandom (t) { | |||||||
| 
 | 
 | ||||||
| test('varUint random user id', async function varUintRandomUserId (t) { | test('varUint random user id', async function varUintRandomUserId (t) { | ||||||
|   t.getSeed() // enforces that this test is repeated
 |   t.getSeed() // enforces that this test is repeated
 | ||||||
|   testEncoding(t, writeVarUint, readVarUint, generateUserID()) |   testEncoding(t, writeVarUint, readVarUint, generateRandomUint32()) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| const writeVarString = (encoder, val) => encoder.writeVarString(val) | const writeVarString = (encoder, val) => encoder.writeVarString(val) | ||||||
|  | |||||||
| @ -3,6 +3,6 @@ | |||||||
|   <head> |   <head> | ||||||
|   </head> |   </head> | ||||||
|   <body> |   <body> | ||||||
|     <script type="module" src="./index.js"></script> |     <script type="module" src="./diff.tests.js"></script> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import './red-black-tree.js' | import './red-black-tree.js' | ||||||
| import './y-array.tests.js' | import './y-array.tests.js' | ||||||
|  | import './y-text.tests.js' | ||||||
| import './y-map.tests.js' | import './y-map.tests.js' | ||||||
| import './y-xml.tests.js' | import './y-xml.tests.js' | ||||||
| import './encode-decode.tests.js' | import './encode-decode.tests.js' | ||||||
|  | |||||||
| @ -1,5 +1,5 @@ | |||||||
| import RedBlackTree from '../src/Util/Tree.js' | 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 Chance from 'chance' | ||||||
| import { test, proxyConsole } from 'cutest' | import { test, proxyConsole } from 'cutest' | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										102
									
								
								test/y-text.tests.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								test/y-text.tests.js
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | }) | ||||||
| @ -3,19 +3,17 @@ import { test } from 'cutest' | |||||||
| 
 | 
 | ||||||
| test('set property', async function xml0 (t) { | test('set property', async function xml0 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) |   var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) | ||||||
|   xml0.setAttribute('height', 10) |   xml0.setAttribute('height', '10') | ||||||
|   t.assert(xml0.getAttribute('height') === 10, 'Simple set+get works') |   t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works') | ||||||
|   await flushAll(t, users) |   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) |   await compareUsers(t, users) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| /* TODO: Test YXml events! |  | ||||||
| test('events', async function xml1 (t) { | test('events', async function xml1 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) |   var { users, xml0, xml1 } = await initArrays(t, { users: 2 }) | ||||||
|   var event |   var event | ||||||
|   var remoteEvent |   var remoteEvent | ||||||
|   let expectedEvent |  | ||||||
|   xml0.observe(function (e) { |   xml0.observe(function (e) { | ||||||
|     delete e._content |     delete e._content | ||||||
|     delete e.nodes |     delete e.nodes | ||||||
| @ -29,48 +27,28 @@ test('events', async function xml1 (t) { | |||||||
|     remoteEvent = e |     remoteEvent = e | ||||||
|   }) |   }) | ||||||
|   xml0.setAttribute('key', 'value') |   xml0.setAttribute('key', 'value') | ||||||
|   expectedEvent = { |   t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on updated key') | ||||||
|     type: 'attributeChanged', |  | ||||||
|     value: 'value', |  | ||||||
|     name: 'key' |  | ||||||
|   } |  | ||||||
|   t.compare(event, expectedEvent, 'attribute changed event') |  | ||||||
|   await flushAll(t, users) |   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
 |   // check attributeRemoved
 | ||||||
|   xml0.removeAttribute('key') |   xml0.removeAttribute('key') | ||||||
|   expectedEvent = { |   t.assert(event.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute') | ||||||
|     type: 'attributeRemoved', |  | ||||||
|     name: 'key' |  | ||||||
|   } |  | ||||||
|   t.compare(event, expectedEvent, 'attribute deleted event') |  | ||||||
|   await flushAll(t, users) |   await flushAll(t, users) | ||||||
|   t.compare(remoteEvent, expectedEvent, 'attribute deleted event (remote)') |   t.assert(remoteEvent.attributesChanged.has('key'), 'YXmlEvent.attributesChanged on removed attribute (remote)') | ||||||
|   // test childInserted event
 |  | ||||||
|   expectedEvent = { |  | ||||||
|     type: 'childInserted', |  | ||||||
|     index: 0 |  | ||||||
|   } |  | ||||||
|   xml0.insert(0, [new Y.XmlText('some text')]) |   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) |   await flushAll(t, users) | ||||||
|   t.compare(remoteEvent, expectedEvent, 'child inserted event (remote)') |   t.assert(remoteEvent.childListChanged, 'YXmlEvent.childListChanged on inserted element (remote)') | ||||||
|   // test childRemoved
 |   // test childRemoved
 | ||||||
|   xml0.delete(0) |   xml0.delete(0) | ||||||
|   expectedEvent = { |   t.assert(event.childListChanged, 'YXmlEvent.childListChanged on deleted element') | ||||||
|     type: 'childRemoved', |  | ||||||
|     index: 0 |  | ||||||
|   } |  | ||||||
|   t.compare(event, expectedEvent, 'child deleted event') |  | ||||||
|   await flushAll(t, users) |   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) |   await compareUsers(t, users) | ||||||
| }) | }) | ||||||
| */ |  | ||||||
| 
 | 
 | ||||||
| test('attribute modifications (y -> dom)', async function xml2 (t) { | test('attribute modifications (y -> dom)', async function xml2 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.setAttribute('height', '100px') |   xml0.setAttribute('height', '100px') | ||||||
|   await wait() |   await wait() | ||||||
|   t.assert(dom0.getAttribute('height') === '100px', 'setAttribute') |   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) { | test('attribute modifications (dom -> y)', async function xml3 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   dom0.setAttribute('height', '100px') |   dom0.setAttribute('height', '100px') | ||||||
|   await wait() |   await wait() | ||||||
|   t.assert(xml0.getAttribute('height') === '100px', 'setAttribute') |   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) { | test('element insert (dom -> y)', async function xml4 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   dom0.insertBefore(document.createTextNode('some text'), null) |   dom0.insertBefore(document.createTextNode('some text'), null) | ||||||
|   dom0.insertBefore(document.createElement('p'), null) |   dom0.insertBefore(document.createElement('p'), null) | ||||||
|   await wait() |   await wait() | ||||||
| @ -110,8 +86,7 @@ test('element insert (dom -> y)', async function xml4 (t) { | |||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| test('element insert (y -> dom)', async function xml5 (t) { | test('element insert (y -> dom)', async function xml5 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.insert(0, [new Y.XmlText('some text')]) |   xml0.insert(0, [new Y.XmlText('some text')]) | ||||||
|   xml0.insert(1, [new Y.XmlElement('p')]) |   xml0.insert(1, [new Y.XmlElement('p')]) | ||||||
|   t.assert(dom0.childNodes[0].textContent === 'some text', 'Retrieve Text node') |   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) { | test('y on insert, then delete (dom -> y)', async function xml6 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   dom0.insertBefore(document.createElement('p'), null) |   dom0.insertBefore(document.createElement('p'), null) | ||||||
|   await wait() |   await wait() | ||||||
|   t.assert(xml0.length === 1, 'one node present') |   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) { | test('y on insert, then delete (y -> dom)', async function xml7 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.insert(0, [new Y.XmlElement('p')]) |   xml0.insert(0, [new Y.XmlElement('p')]) | ||||||
|   t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom') |   t.assert(dom0.childNodes[0].nodeName === 'P', 'Get inserted element from dom') | ||||||
|   xml0.delete(0, 1) |   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) { | test('delete consecutive (1) (Text)', async function xml8 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')]) |   xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')]) | ||||||
|   await wait() |   await wait() | ||||||
|   xml0.delete(1, 2) |   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) { | test('delete consecutive (2) (Text)', async function xml9 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')]) |   xml0.insert(0, [new Y.XmlText('1'), new Y.XmlText('2'), new Y.XmlText('3')]) | ||||||
|   await wait() |   await wait() | ||||||
|   xml0.delete(0, 1) |   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) { | test('delete consecutive (1) (Element)', async function xml10 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) |   xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) | ||||||
|   await wait() |   await wait() | ||||||
|   xml0.delete(1, 2) |   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) { | test('delete consecutive (2) (Element)', async function xml11 (t) { | ||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0, dom0 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) |   xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) | ||||||
|   await wait() |   await wait() | ||||||
|   xml0.delete(0, 1) |   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) { | test('Receive a bunch of elements (with disconnect)', async function xml12 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) |   var { users, xml0, xml1, dom0, dom1 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   let dom1 = xml1.getDom() |  | ||||||
|   users[1].disconnect() |   users[1].disconnect() | ||||||
|   xml0.insert(0, [new Y.XmlElement('A'), new Y.XmlElement('B'), new Y.XmlElement('C')]) |   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')]) |   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) { | test('move element to a different position', async function xml13 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) |   var { users, dom0, dom1 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   let dom1 = xml1.getDom() |  | ||||||
|   dom0.append(document.createElement('div')) |   dom0.append(document.createElement('div')) | ||||||
|   dom0.append(document.createElement('h1')) |   dom0.append(document.createElement('h1')) | ||||||
|   await flushAll(t, users) |   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) { | test('filter node', async function xml14 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) |   var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   let dom1 = xml1.getDom() |  | ||||||
|   let domFilter = (nodeName, attrs) => { |   let domFilter = (nodeName, attrs) => { | ||||||
|     if (nodeName === 'H1') { |     if (nodeName === 'H1') { | ||||||
|       return null |       return null | ||||||
| @ -237,8 +200,8 @@ test('filter node', async function xml14 (t) { | |||||||
|       return attrs |       return attrs | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   xml0.setDomFilter(domFilter) |   domBinding0.setFilter(domFilter) | ||||||
|   xml1.setDomFilter(domFilter) |   domBinding1.setFilter(domFilter) | ||||||
|   dom0.append(document.createElement('div')) |   dom0.append(document.createElement('div')) | ||||||
|   dom0.append(document.createElement('h1')) |   dom0.append(document.createElement('h1')) | ||||||
|   await flushAll(t, users) |   await flushAll(t, users) | ||||||
| @ -248,15 +211,13 @@ test('filter node', async function xml14 (t) { | |||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| test('filter attribute', async function xml15 (t) { | test('filter attribute', async function xml15 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) |   var { users, dom0, dom1, domBinding0, domBinding1 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   let dom1 = xml1.getDom() |  | ||||||
|   let domFilter = (nodeName, attrs) => { |   let domFilter = (nodeName, attrs) => { | ||||||
|     attrs.delete('hidden') |     attrs.delete('hidden') | ||||||
|     return attrs |     return attrs | ||||||
|   } |   } | ||||||
|   xml0.setDomFilter(domFilter) |   domBinding0.setFilter(domFilter) | ||||||
|   xml1.setDomFilter(domFilter) |   domBinding1.setFilter(domFilter) | ||||||
|   dom0.setAttribute('hidden', 'true') |   dom0.setAttribute('hidden', 'true') | ||||||
|   dom0.setAttribute('style', 'height: 30px') |   dom0.setAttribute('style', 'height: 30px') | ||||||
|   dom0.setAttribute('data-me', '77') |   dom0.setAttribute('data-me', '77') | ||||||
| @ -269,9 +230,7 @@ test('filter attribute', async function xml15 (t) { | |||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| test('deep element insert', async function xml16 (t) { | test('deep element insert', async function xml16 (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) |   var { users, dom0, dom1 } = await initArrays(t, { users: 3 }) | ||||||
|   let dom0 = xml0.getDom() |  | ||||||
|   let dom1 = xml1.getDom() |  | ||||||
|   let deepElement = document.createElement('p') |   let deepElement = document.createElement('p') | ||||||
|   let boldElement = document.createElement('b') |   let boldElement = document.createElement('b') | ||||||
|   let attrElement = document.createElement('img') |   let attrElement = document.createElement('img') | ||||||
| @ -291,8 +250,8 @@ test('treeWalker', async function xml17 (t) { | |||||||
|   var { users, xml0 } = await initArrays(t, { users: 3 }) |   var { users, xml0 } = await initArrays(t, { users: 3 }) | ||||||
|   let paragraph1 = new Y.XmlElement('p') |   let paragraph1 = new Y.XmlElement('p') | ||||||
|   let paragraph2 = new Y.XmlElement('p') |   let paragraph2 = new Y.XmlElement('p') | ||||||
|   let text1 = new Y.Text('init') |   let text1 = new Y.XmlText('init') | ||||||
|   let text2 = new Y.Text('text') |   let text2 = new Y.XmlText('text') | ||||||
|   paragraph1.insert(0, [text1, text2]) |   paragraph1.insert(0, [text1, text2]) | ||||||
|   xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')]) |   xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')]) | ||||||
|   let allParagraphs = xml0.querySelectorAll('p') |   let allParagraphs = xml0.querySelectorAll('p') | ||||||
| @ -309,8 +268,8 @@ test('treeWalker', async function xml17 (t) { | |||||||
|  * Incoming changes that contain malicious attributes should be deleted. |  * Incoming changes that contain malicious attributes should be deleted. | ||||||
|  */ |  */ | ||||||
| test('Filtering remote changes', async function xmlFilteringRemote (t) { | test('Filtering remote changes', async function xmlFilteringRemote (t) { | ||||||
|   var { users, xml0, xml1 } = await initArrays(t, { users: 3 }) |   var { users, xml0, xml1, domBinding0 } = await initArrays(t, { users: 3 }) | ||||||
|   xml0.setDomFilter(function (nodeName, attributes) { |   domBinding0.setFilter(function (nodeName, attributes) { | ||||||
|     attributes.delete('malicious') |     attributes.delete('malicious') | ||||||
|     if (nodeName === 'HIDEME') { |     if (nodeName === 'HIDEME') { | ||||||
|       return null |       return null | ||||||
| @ -320,10 +279,6 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { | |||||||
|       return attributes |       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 paragraph = new Y.XmlElement('p') | ||||||
|   let hideMe = new Y.XmlElement('hideMe') |   let hideMe = new Y.XmlElement('hideMe') | ||||||
|   let span = new Y.XmlElement('span') |   let span = new Y.XmlElement('span') | ||||||
| @ -337,8 +292,8 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { | |||||||
|   paragraph.insert(0, [tag2]) |   paragraph.insert(0, [tag2]) | ||||||
|   await flushAll(t, users) |   await flushAll(t, users) | ||||||
|   // check dom
 |   // check dom
 | ||||||
|   paragraph.getDom().setAttribute('malicious', 'true') |   domBinding0.typeToDom.get(paragraph).setAttribute('malicious', 'true') | ||||||
|   span.getDom().setAttribute('malicious', 'true') |   domBinding0.typeToDom.get(span).setAttribute('malicious', 'true') | ||||||
|   // check incoming attributes
 |   // check incoming attributes
 | ||||||
|   xml1.get(0).get(0).setAttribute('malicious', 'true') |   xml1.get(0).get(0).setAttribute('malicious', 'true') | ||||||
|   xml1.insert(0, [new Y.XmlElement('hideMe')]) |   xml1.insert(0, [new Y.XmlElement('hideMe')]) | ||||||
| @ -350,35 +305,35 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) { | |||||||
| // TODO: move elements
 | // TODO: move elements
 | ||||||
| var xmlTransactions = [ | var xmlTransactions = [ | ||||||
|   function attributeChange (t, user, chance) { |   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) { |   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) { |   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 |     var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null | ||||||
|     dom.insertBefore(document.createTextNode(chance.word()), succ) |     dom.insertBefore(document.createTextNode(chance.word()), succ) | ||||||
|   }, |   }, | ||||||
|   function insertHiddenDom (t, user, chance) { |   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 |     var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null | ||||||
|     dom.insertBefore(document.createElement('hidden'), succ) |     dom.insertBefore(document.createElement('hidden'), succ) | ||||||
|   }, |   }, | ||||||
|   function insertDom (t, user, chance) { |   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 |     var succ = dom.children.length > 0 ? chance.pickone(dom.children) : null | ||||||
|     dom.insertBefore(document.createElement(chance.word()), succ) |     dom.insertBefore(document.createElement(chance.word()), succ) | ||||||
|   }, |   }, | ||||||
|   function deleteChild (t, user, chance) { |   function deleteChild (t, user, chance) { | ||||||
|     let dom = user.get('xml', Y.XmlElement).getDom() |     let dom = user.dom | ||||||
|     if (dom.childNodes.length > 0) { |     if (dom.childNodes.length > 0) { | ||||||
|       var d = chance.pickone(dom.childNodes) |       var d = chance.pickone(dom.childNodes) | ||||||
|       d.remove() |       d.remove() | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   function insertTextSecondLayer (t, user, chance) { |   function insertTextSecondLayer (t, user, chance) { | ||||||
|     let dom = user.get('xml', Y.XmlElement).getDom() |     let dom = user.dom | ||||||
|     if (dom.children.length > 0) { |     if (dom.children.length > 0) { | ||||||
|       let dom2 = chance.pickone(dom.children) |       let dom2 = chance.pickone(dom.children) | ||||||
|       let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null |       let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null | ||||||
| @ -386,7 +341,7 @@ var xmlTransactions = [ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   function insertDomSecondLayer (t, user, chance) { |   function insertDomSecondLayer (t, user, chance) { | ||||||
|     let dom = user.get('xml', Y.XmlElement).getDom() |     let dom = user.dom | ||||||
|     if (dom.children.length > 0) { |     if (dom.children.length > 0) { | ||||||
|       let dom2 = chance.pickone(dom.children) |       let dom2 = chance.pickone(dom.children) | ||||||
|       let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null |       let succ = dom2.childNodes.length > 0 ? chance.pickone(dom2.childNodes) : null | ||||||
| @ -394,7 +349,7 @@ var xmlTransactions = [ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   function deleteChildSecondLayer (t, user, chance) { |   function deleteChildSecondLayer (t, user, chance) { | ||||||
|     let dom = user.get('xml', Y.XmlElement).getDom() |     let dom = user.dom | ||||||
|     if (dom.children.length > 0) { |     if (dom.children.length > 0) { | ||||||
|       let dom2 = chance.pickone(dom.children) |       let dom2 = chance.pickone(dom.children) | ||||||
|       if (dom2.childNodes.length > 0) { |       if (dom2.childNodes.length > 0) { | ||||||
|  | |||||||
| @ -1,19 +1,22 @@ | |||||||
| 
 | 
 | ||||||
| import _Y from '../src/Y.js' | import _Y from '../src/Y.dist.js' | ||||||
| import yTest from './test-connector.js' | import { DomBinding } from '../src/Y.js' | ||||||
|  | import TestConnector from './test-connector.js' | ||||||
| 
 | 
 | ||||||
| import Chance from 'chance' | import Chance from 'chance' | ||||||
| import ItemJSON from '../src/Struct/ItemJSON.js' | import ItemJSON from '../src/Struct/ItemJSON.js' | ||||||
| import ItemString from '../src/Struct/ItemString.js' | import ItemString from '../src/Struct/ItemString.js' | ||||||
| import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' | import { defragmentItemContent } from '../src/Util/defragmentItemContent.js' | ||||||
|  | import Quill from 'quill' | ||||||
|  | import GC from '../src/Struct/GC.js' | ||||||
| 
 | 
 | ||||||
| export const Y = _Y | export const Y = _Y | ||||||
| 
 | 
 | ||||||
| Y.extend(yTest) |  | ||||||
| 
 |  | ||||||
| export const database = { name: 'memory' } | export const database = { name: 'memory' } | ||||||
| export const connector = { name: 'test', url: 'http://localhost:1234' } | export const connector = { name: 'test', url: 'http://localhost:1234' } | ||||||
| 
 | 
 | ||||||
|  | Y.test = TestConnector | ||||||
|  | 
 | ||||||
| function getStateSet (y) { | function getStateSet (y) { | ||||||
|   let ss = {} |   let ss = {} | ||||||
|   for (let [user, clock] of y.ss.state) { |   for (let [user, clock] of y.ss.state) { | ||||||
| @ -39,39 +42,6 @@ function getDeleteSet (y) { | |||||||
|   return ds |   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 |  * 1. reconnect and flush all | ||||||
|  * 2. user 0 gc |  * 2. user 0 gc | ||||||
| @ -92,16 +62,29 @@ export async function compareUsers (t, users) { | |||||||
|   await wait() |   await wait() | ||||||
|   await flushAll(t, users) |   await flushAll(t, users) | ||||||
| 
 | 
 | ||||||
|   var userArrayValues = users.map(u => u.get('array', Y.Array).toJSON().map(val => JSON.stringify(val))) |   var userArrayValues = users.map(u => u.define('array', Y.Array).toJSON().map(val => JSON.stringify(val))) | ||||||
|   var userMapValues = users.map(u => u.get('map', Y.Map).toJSON()) |   var userMapValues = users.map(u => u.define('map', Y.Map).toJSON()) | ||||||
|   var userXmlValues = users.map(u => u.get('xml', Y.Xml).toString()) |   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 => { |   var data = users.map(u => { | ||||||
|     defragmentItemContent(u) |     defragmentItemContent(u) | ||||||
|     var data = {} |     var data = {} | ||||||
|     let ops = [] |     let ops = [] | ||||||
|     u.os.iterate(null, null, function (op) { |     u.os.iterate(null, null, function (op) { | ||||||
|       const json = { |       let json | ||||||
|  |       if (op.constructor === GC) { | ||||||
|  |         json = { | ||||||
|  |           type: 'GC', | ||||||
|  |           id: op._id, | ||||||
|  |           length: op._length | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         json = { | ||||||
|           id: op._id, |           id: op._id, | ||||||
|           left: op._left === null ? null : op._left._lastId, |           left: op._left === null ? null : op._left._lastId, | ||||||
|           right: op._right === null ? null : op._right._id, |           right: op._right === null ? null : op._right._id, | ||||||
| @ -109,6 +92,7 @@ export async function compareUsers (t, users) { | |||||||
|           deleted: op._deleted, |           deleted: op._deleted, | ||||||
|           parent: op._parent._id |           parent: op._parent._id | ||||||
|         } |         } | ||||||
|  |       } | ||||||
|       if (op instanceof ItemJSON || op instanceof ItemString) { |       if (op instanceof ItemJSON || op instanceof ItemString) { | ||||||
|         json.content = op._content |         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(userArrayValues[i], userArrayValues[i + 1], 'array types') | ||||||
|       t.compare(userMapValues[i], userMapValues[i + 1], 'map types') |       t.compare(userMapValues[i], userMapValues[i + 1], 'map types') | ||||||
|       t.compare(userXmlValues[i], userXmlValues[i + 1], 'xml 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].os, data[i + 1].os, 'os') | ||||||
|       t.compare(data[i].ds, data[i + 1].ds, 'ds') |       t.compare(data[i].ds, data[i + 1].ds, 'ds') | ||||||
|       t.compare(data[i].ss, data[i + 1].ss, 'ss') |       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()) |   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) { | export async function initArrays (t, opts) { | ||||||
|   var result = { |   var result = { | ||||||
|     users: [] |     users: [] | ||||||
|   } |   } | ||||||
|   var chance = opts.chance || new Chance(t.getSeed() * 1000000000) |   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++) { |   for (let i = 0; i < opts.users; i++) { | ||||||
|     let connOpts |     let connOpts | ||||||
|     if (i === 0) { |     if (i === 0) { | ||||||
| @ -146,23 +140,28 @@ export async function initArrays (t, opts) { | |||||||
|       connOpts = Object.assign({ role: 'slave' }, conn) |       connOpts = Object.assign({ role: 'slave' }, conn) | ||||||
|     } |     } | ||||||
|     let y = new Y(connOpts.room, { |     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 |       connector: connOpts | ||||||
|     }) |     }) | ||||||
|     result.users.push(y) |     result.users.push(y) | ||||||
|     result['array' + i] = y.define('array', Y.Array) |     result['array' + i] = y.define('array', Y.Array) | ||||||
|     result['map' + i] = y.define('map', Y.Map) |     result['map' + i] = y.define('map', Y.Map) | ||||||
|     result['xml' + i] = y.define('xml', Y.XmlElement) |     const yxml = y.define('xml', Y.XmlElement) | ||||||
|     y.get('xml').setDomFilter(function (nodeName, attrs) { |     result['xml' + i] = yxml | ||||||
|       if (nodeName === 'HIDDEN') { |     const dom = document.createElement('my-dom') | ||||||
|         return null |     const domBinding = new DomBinding(yxml, dom, { domFilter }) | ||||||
|       } |     result['domBinding' + i] = domBinding | ||||||
|       attrs.delete('hidden') |     result['dom' + i] = dom | ||||||
|       return attrs |     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 () { |     y.on('afterTransaction', function () { | ||||||
|       for (let missing of y._missingStructs.values()) { |       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!')) |           console.error(new Error('Test check in "afterTransaction": missing should be empty!')) | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| /* global Y */ |  | ||||||
| import { wait } from './helper' | import { wait } from './helper' | ||||||
| import { messageToString } from '../src/MessageHandler/messageToString' | import { messageToString } from '../src/MessageHandler/messageToString' | ||||||
|  | import AbstractConnector from '../src/Connector.js' | ||||||
| 
 | 
 | ||||||
| var rooms = {} | var rooms = {} | ||||||
| 
 | 
 | ||||||
| @ -64,8 +64,7 @@ function getTestRoom (roomname) { | |||||||
|   return rooms[roomname] |   return rooms[roomname] | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default function extendTestConnector (Y) { | export default class TestConnector extends AbstractConnector { | ||||||
|   class TestConnector extends Y.AbstractConnector { |  | ||||||
|   constructor (y, options) { |   constructor (y, options) { | ||||||
|     if (options === undefined) { |     if (options === undefined) { | ||||||
|       throw new Error('Options must not be undefined!') |       throw new Error('Options must not be undefined!') | ||||||
| @ -161,10 +160,3 @@ export default function extendTestConnector (Y) { | |||||||
|     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) |  | ||||||
| } |  | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user