Compare commits
120 Commits
v13.0.0-34
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d897f1844 | ||
|
|
fb2f9bc493 | ||
|
|
6f9ae0c4fc | ||
|
|
9df20fac8a | ||
|
|
a1fb1a6258 | ||
|
|
417d0ef3b5 | ||
|
|
9be256231b | ||
|
|
c122bdc750 | ||
|
|
4ef36ab81c | ||
|
|
cccc0e1015 | ||
|
|
684d38d6c8 | ||
|
|
44fa064eb2 | ||
|
|
9b6fffd880 | ||
|
|
e9993b2643 | ||
|
|
762e9e8a3a | ||
|
|
6ddeb788c7 | ||
|
|
b9245f323c | ||
|
|
c0e630b635 | ||
|
|
e56457a0ef | ||
|
|
ca13849828 | ||
|
|
92c2fbd6d3 | ||
|
|
65b8921f05 | ||
|
|
1ace7f4b73 | ||
|
|
6336064516 | ||
|
|
49d2e42b41 | ||
|
|
c098e8e745 | ||
|
|
38558a7fad | ||
|
|
bdb3782f8f | ||
|
|
bc32f7348e | ||
|
|
09a94f053e | ||
|
|
0df0079fa3 | ||
|
|
a54d826d6d | ||
|
|
99f92cb9a0 | ||
|
|
e788ad1333 | ||
|
|
1fe37c565e | ||
|
|
ed2273e2ed | ||
|
|
94933a704d | ||
|
|
ef6eb08335 | ||
|
|
d915c8dd13 | ||
|
|
32207cbca0 | ||
|
|
135c6d31be | ||
|
|
61149b458a | ||
|
|
ba97bfdd9e | ||
|
|
689bca8602 | ||
|
|
6dd43cde17 | ||
|
|
026675b438 | ||
|
|
941a22b257 | ||
|
|
4aa41b98a9 | ||
|
|
acf443aacb | ||
|
|
aa8c934833 | ||
|
|
814af5a3d7 | ||
|
|
bbc207aaa6 | ||
|
|
a9b610479d | ||
|
|
079de07eff | ||
|
|
54453e87fa | ||
|
|
1b0e3659c3 | ||
|
|
dc22a79ac4 | ||
|
|
384a4b72b0 | ||
|
|
f35c056bde | ||
|
|
250050e83b | ||
|
|
248d08be30 | ||
|
|
641f426339 | ||
|
|
fcbca65d8f | ||
|
|
5f8ae0dd43 | ||
|
|
de14fe0f3e | ||
|
|
5e4b071693 | ||
|
|
937de2c59f | ||
|
|
f1f1bff901 | ||
|
|
da748a78f4 | ||
|
|
4855b2d590 | ||
|
|
908ce31e2f | ||
|
|
e4d4c23f0b | ||
|
|
fc500a8247 | ||
|
|
4b84541d76 | ||
|
|
a3ab42c157 | ||
|
|
bbd3317d62 | ||
|
|
5d3922cb64 | ||
|
|
a81a2cd553 | ||
|
|
c0d24bdba4 | ||
|
|
40e913e9c5 | ||
|
|
94f6a0fd9c | ||
|
|
41a88dbc43 | ||
|
|
1d4f283955 | ||
|
|
fc3a4c376c | ||
|
|
acb0affa33 | ||
|
|
0b510b64a3 | ||
|
|
c8f0cf5556 | ||
|
|
11a4271fd1 | ||
|
|
c7670915c7 | ||
|
|
eb2d596538 | ||
|
|
48e17ea1a7 | ||
|
|
1a22fdd45e | ||
|
|
07cf0b3436 | ||
|
|
5a68b9f4ad | ||
|
|
445dd3e0da | ||
|
|
0ba97d78f8 | ||
|
|
fc5be5c7cc | ||
|
|
f2debc150c | ||
|
|
08f37a86e3 | ||
|
|
f5d17e6236 | ||
|
|
8f3bd7170a | ||
|
|
5586334549 | ||
|
|
24c1e4dcc8 | ||
|
|
d61bbecf4e | ||
|
|
85492ad2e0 | ||
|
|
02253f9a8d | ||
|
|
8105bef1af | ||
|
|
4efa16e2dd | ||
|
|
ad44f59def | ||
|
|
9c471ea24d | ||
|
|
d9e76014f5 | ||
|
|
4091b7d004 | ||
|
|
dfc183643d | ||
|
|
cf8698f2b6 | ||
|
|
3595f14da7 | ||
|
|
c6e671b1d5 | ||
|
|
e4c10fd6b3 | ||
|
|
e70aa09f88 | ||
|
|
7808b143da | ||
|
|
b35092928e |
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}
|
||||
}
|
||||
}]
|
||||
}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
node_modules
|
||||
bower_components
|
||||
docs
|
||||
/y.*
|
||||
/examples/yjs-dist.js*
|
||||
.vscode
|
||||
.yjsPersisted
|
||||
|
||||
14
README.md
14
README.md
@@ -64,6 +64,18 @@ missing modules.
|
||||
<script src="./bower_components/yjs/y.js"></script>
|
||||
```
|
||||
|
||||
### CDN
|
||||
```
|
||||
<script src="https://cdn.jsdelivr.net/npm/yjs@12/src/y.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
|
||||
// ..
|
||||
// do the same for all modules you want to use
|
||||
```
|
||||
|
||||
### Npm
|
||||
```
|
||||
npm install --save yjs % add all y-* modules you want to use
|
||||
@@ -76,7 +88,6 @@ var Y = require('yjs')
|
||||
require('y-array')(Y) // add the y-array type to Yjs
|
||||
require('y-websockets-client')(Y)
|
||||
require('y-memory')(Y)
|
||||
require('y-array')(Y)
|
||||
require('y-map')(Y)
|
||||
require('y-text')(Y)
|
||||
// ..
|
||||
@@ -89,7 +100,6 @@ import Y from 'yjs'
|
||||
import yArray from 'y-array'
|
||||
import yWebsocketsClient from 'y-webrtc'
|
||||
import yMemory from 'y-memory'
|
||||
import yArray from 'y-array'
|
||||
import yMap from 'y-map'
|
||||
import yText from 'y-text'
|
||||
// ..
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
<body>
|
||||
|
||||
<div id="aceContainer"></div>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/ace-builds/src/ace.js"></script>
|
||||
|
||||
<script src="./index.js"></script>
|
||||
|
||||
@@ -1,24 +1,17 @@
|
||||
/* global Y, ace */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('ace-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'ace-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
ace: 'Text' // y.share.textarea is of type Y.Text
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yAce = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
var editor = ace.edit('aceContainer')
|
||||
editor.setTheme('ace/theme/chrome')
|
||||
editor.getSession().setMode('ace/mode/javascript')
|
||||
|
||||
y.share.ace.bindAce(editor)
|
||||
})
|
||||
|
||||
window.yAce = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
var editor = ace.edit('aceContainer')
|
||||
editor.setTheme('ace/theme/chrome')
|
||||
editor.getSession().setMode('ace/mode/javascript')
|
||||
|
||||
y.define('ace', Y.Text).bindAce(editor)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-websockets-client/dist/y-websockets-client.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
var y = new Y({
|
||||
let y = new Y('chat-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'chat-example'
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -23,6 +22,7 @@ function appendMessage (message, position) {
|
||||
p.appendChild(document.createTextNode(message.message))
|
||||
chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
|
||||
}
|
||||
|
||||
// This function makes sure that only 7 messages exist in the chat history.
|
||||
// The rest is deleted
|
||||
function cleanupChat () {
|
||||
@@ -30,23 +30,17 @@ function cleanupChat () {
|
||||
chatprotocol.delete(0, chatprotocol.length - 7)
|
||||
}
|
||||
}
|
||||
cleanupChat()
|
||||
|
||||
// Insert the initial content
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
cleanupChat()
|
||||
|
||||
// whenever content changes, make sure to reflect the changes in the DOM
|
||||
chatprotocol.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
appendMessage(event.values[i], event.index + i)
|
||||
}
|
||||
} else if (event.type === 'delete') {
|
||||
for (let i = 0; i < event.length; i++) {
|
||||
chatcontainer.children[event.index].remove()
|
||||
}
|
||||
}
|
||||
// concurrent insertions may result in a history > 7, so cleanup here
|
||||
cleanupChat()
|
||||
chatcontainer.innerHTML = ''
|
||||
chatprotocol.toArray().forEach(appendMessage)
|
||||
})
|
||||
document.querySelector('#chatform').onsubmit = function (event) {
|
||||
// the form is submitted
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<body>
|
||||
<div id="codeMirrorContainer"></div>
|
||||
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/codemirror/lib/codemirror.js"></script>
|
||||
<script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
|
||||
<link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('codemirror-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
})
|
||||
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
|
||||
|
||||
@@ -13,11 +13,8 @@
|
||||
<button type="button" id="clearDrawingCanvas">Clear Drawing</button>
|
||||
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,84 +1,74 @@
|
||||
/* globals Y, d3 */
|
||||
'strict mode'
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('drawing-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'drawing-example',
|
||||
url: 'localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
drawing: 'Array'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yDrawing = y
|
||||
var drawing = y.share.drawing
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basis')
|
||||
|
||||
var svg = d3.select('#drawingCanvas')
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
// we only implement insert events that are appended to the end of the array
|
||||
event.values.forEach(function (value) {
|
||||
line.datum().push(value)
|
||||
})
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
y.share.drawing.observe(function (event) {
|
||||
if (event.type === 'insert') {
|
||||
event.values.forEach(drawLine)
|
||||
} else {
|
||||
// just remove all elements (thats what we do anyway)
|
||||
svg.selectAll('path').remove()
|
||||
}
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i))
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
sharedLine = drawing.get(drawing.length - 1)
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 33)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yDrawing = y
|
||||
var drawing = y.define('drawing', Y.Array)
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basic')
|
||||
|
||||
var svg = d3.select('#drawingCanvas')
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
line.remove()
|
||||
line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
drawing.observe(function (event) {
|
||||
event.removedElements.forEach(function () {
|
||||
// if one is deleted, all will be deleted!!
|
||||
svg.selectAll('path').remove()
|
||||
})
|
||||
event.addedElements.forEach(function (path) {
|
||||
drawLine(path)
|
||||
})
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i))
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
sharedLine = drawing.get(drawing.length - 1)
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 10)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
|
||||
36
examples/html-editor-drawing-hook/index.html
Normal file
36
examples/html-editor-drawing-hook/index.html
Normal file
@@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.min.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<style>
|
||||
magic-drawing .drawingCanvas path {
|
||||
fill: none;
|
||||
stroke: blue;
|
||||
stroke-width: 2px;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
magic-drawing .drawingCanvas {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
cursor: default;
|
||||
padding:1px;
|
||||
border:1px solid #021a40;
|
||||
}
|
||||
magic-drawing .clearDrawingButton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
magic-drawing {
|
||||
position: relative;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
134
examples/html-editor-drawing-hook/index.js
Normal file
134
examples/html-editor-drawing-hook/index.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/* 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.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks })
|
||||
}
|
||||
|
||||
window.addMagicDrawing = function addMagicDrawing () {
|
||||
let mt = document.createElement('magic-drawing')
|
||||
mt.setAttribute('data-yjs-hook', 'magic-drawing')
|
||||
document.body.append(mt)
|
||||
}
|
||||
|
||||
var renderPath = d3.svg.line()
|
||||
.x(function (d) { return d[0] })
|
||||
.y(function (d) { return d[1] })
|
||||
.interpolate('basic')
|
||||
|
||||
function initDrawingBindings (type, dom) {
|
||||
dom.contentEditable = 'false'
|
||||
dom.setAttribute('data-yjs-hook', 'magic-drawing')
|
||||
var drawing = type.get('drawing')
|
||||
if (drawing === undefined) {
|
||||
drawing = type.set('drawing', new Y.Array())
|
||||
}
|
||||
var canvas = dom.querySelector('.drawingCanvas')
|
||||
if (canvas == null) {
|
||||
canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
canvas.setAttribute('class', 'drawingCanvas')
|
||||
canvas.setAttribute('viewbox', '0 0 100 100')
|
||||
dom.insertBefore(canvas, null)
|
||||
}
|
||||
var clearDrawingButton = dom.querySelector('.clearDrawingButton')
|
||||
if (clearDrawingButton == null) {
|
||||
clearDrawingButton = document.createElement('button')
|
||||
clearDrawingButton.setAttribute('type', 'button')
|
||||
clearDrawingButton.setAttribute('class', 'clearDrawingButton')
|
||||
clearDrawingButton.innerText = 'Clear Drawing'
|
||||
dom.insertBefore(clearDrawingButton, null)
|
||||
}
|
||||
var svg = d3.select(canvas)
|
||||
.call(d3.behavior.drag()
|
||||
.on('dragstart', dragstart)
|
||||
.on('drag', drag)
|
||||
.on('dragend', dragend))
|
||||
// create line from a shared array object and update the line when the array changes
|
||||
function drawLine (yarray, svg) {
|
||||
var line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
yarray.observe(function (event) {
|
||||
line.remove()
|
||||
line = svg.append('path').datum(yarray.toArray())
|
||||
line.attr('d', renderPath)
|
||||
})
|
||||
}
|
||||
// call drawLine every time an array is appended
|
||||
drawing.observe(function (event) {
|
||||
event.removedElements.forEach(function () {
|
||||
// if one is deleted, all will be deleted!!
|
||||
svg.selectAll('path').remove()
|
||||
})
|
||||
event.addedElements.forEach(function (path) {
|
||||
drawLine(path, svg)
|
||||
})
|
||||
})
|
||||
// draw all existing content
|
||||
for (var i = 0; i < drawing.length; i++) {
|
||||
drawLine(drawing.get(i), svg)
|
||||
}
|
||||
|
||||
// clear canvas on request
|
||||
clearDrawingButton.onclick = function () {
|
||||
drawing.delete(0, drawing.length)
|
||||
}
|
||||
|
||||
var sharedLine = null
|
||||
function dragstart () {
|
||||
drawing.insert(drawing.length, [Y.Array])
|
||||
sharedLine = drawing.get(drawing.length - 1)
|
||||
}
|
||||
|
||||
// After one dragged event is recognized, we ignore them for 33ms.
|
||||
var ignoreDrag = null
|
||||
function drag () {
|
||||
if (sharedLine != null && ignoreDrag == null) {
|
||||
ignoreDrag = window.setTimeout(function () {
|
||||
ignoreDrag = null
|
||||
}, 10)
|
||||
sharedLine.push([d3.mouse(this)])
|
||||
}
|
||||
}
|
||||
|
||||
function dragend () {
|
||||
sharedLine = null
|
||||
window.clearTimeout(ignoreDrag)
|
||||
ignoreDrag = null
|
||||
}
|
||||
}
|
||||
|
||||
let y = new Y('html-editor-drawing-hook-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yXml = y
|
||||
window.yXmlType = y.define('xml', Y.XmlFragment)
|
||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
||||
captureTimeout: 500
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && e.metaKey) {
|
||||
if (!e.shiftKey) {
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<!-- jquery is not required for y-xml. 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="../yjs-dist.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
<script src="./index.mjs" type="module"></script>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
<body>
|
||||
<label for="room">Room: </label>
|
||||
<input type="text" id="room" name="room">
|
||||
<div id="content" contenteditable style="position:absolute;top:35px;left:0;right:0;bottom:0;outline: 0px solid transparent;"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/* global Y, HTMLElement, customElements */
|
||||
|
||||
class MagicTable extends HTMLElement {
|
||||
constructor () {
|
||||
super()
|
||||
var shadow = this.attachShadow({mode: 'open'})
|
||||
setTimeout(() => {
|
||||
shadow.append(this.childNodes[0])
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
customElements.define('magic-table', MagicTable)
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
let y = new Y({
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234',
|
||||
room: 'html-editor-example6'
|
||||
// maxBufferLength: 100
|
||||
}
|
||||
})
|
||||
window.yXml = y
|
||||
window.yXmlType = y.define('xml', Y.XmlFragment)
|
||||
window.onload = function () {
|
||||
console.log('start!')
|
||||
// Bind children of XmlFragment to the document.body
|
||||
window.yXmlType.bindToDom(document.body)
|
||||
}
|
||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
|
||||
captureTimeout: 500
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && e.metaKey) {
|
||||
if (!e.shiftKey) {
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
77
examples/html-editor/index.mjs
Normal file
77
examples/html-editor/index.mjs
Normal file
@@ -0,0 +1,77 @@
|
||||
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
|
||||
import Y from '../../src/Y.mjs'
|
||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.mjs'
|
||||
import UndoManager from '../../src/Util/UndoManager.mjs'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
|
||||
import YXmlText from '../../src/Types/YXml/YXmlText.mjs'
|
||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.mjs'
|
||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.mjs'
|
||||
|
||||
const connector = new YWebsocketsConnector()
|
||||
const persistence = new YIndexdDBPersistence()
|
||||
|
||||
const roomInput = document.querySelector('#room')
|
||||
|
||||
let currentRoomName = null
|
||||
let y = null
|
||||
let domBinding = null
|
||||
|
||||
function setRoomName (roomName) {
|
||||
if (currentRoomName !== roomName) {
|
||||
console.log(`change room: "${roomName}"`)
|
||||
roomInput.value = roomName
|
||||
currentRoomName = roomName
|
||||
location.hash = '#' + roomName
|
||||
if (y !== null) {
|
||||
domBinding.destroy()
|
||||
}
|
||||
|
||||
const room = connector._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
y = room.y
|
||||
} else {
|
||||
y = new Y(roomName, null, null, { gc: true })
|
||||
persistence.connectY(roomName, y).then(() => {
|
||||
// connect after persisted content was applied to y
|
||||
// If we don't wait for persistence, the other peer will send all data, waisting
|
||||
// network bandwidth..
|
||||
connector.connectY(roomName, y)
|
||||
})
|
||||
window.y = y
|
||||
}
|
||||
|
||||
window.y = y
|
||||
window.yXmlType = y.define('xml', YXmlFragment)
|
||||
|
||||
domBinding = new DomBinding(window.yXmlType, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
|
||||
}
|
||||
}
|
||||
window.setRoomName = setRoomName
|
||||
|
||||
window.createRooms = function (i = 0) {
|
||||
setInterval(function () {
|
||||
setRoomName(i + '')
|
||||
i++
|
||||
const nodes = []
|
||||
for (let j = 0; j < 100; j++) {
|
||||
const node = new YXmlElement('p')
|
||||
node.insert(0, [new YXmlText(`This is the ${i}th paragraph of room ${i}`)])
|
||||
nodes.push(node)
|
||||
}
|
||||
y.share.xml.insert(0, nodes)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
connector.syncPersistence(persistence)
|
||||
|
||||
window.connector = connector
|
||||
window.persistence = persistence
|
||||
|
||||
window.onload = function () {
|
||||
setRoomName((location.hash || '#default').slice(1))
|
||||
roomInput.addEventListener('input', e => {
|
||||
const roomName = e.target.value
|
||||
setRoomName(roomName)
|
||||
})
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./index.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src='../../../y-indexeddb/y-indexeddb.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
/* global Y, CodeMirror */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
const persistence = new Y.IndexedDB()
|
||||
const connector = {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'codemirror-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
codemirror: 'Text' // y.share.codemirror is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yCodeMirror = y
|
||||
}
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
y.share.codemirror.bindCodeMirror(editor)
|
||||
const y = new Y('codemirror-example', connector, persistence)
|
||||
window.yCodeMirror = y
|
||||
|
||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
|
||||
mode: 'javascript',
|
||||
lineNumbers: true
|
||||
})
|
||||
|
||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
.one {
|
||||
grid-column: 1 ;
|
||||
}
|
||||
.two {
|
||||
.two {
|
||||
grid-column: 2;
|
||||
}
|
||||
.three {
|
||||
@@ -49,10 +49,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/dist/y-text.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,64 +1,38 @@
|
||||
/* global Y */
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y1 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea1'))
|
||||
})
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-second.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y2 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea2'))
|
||||
function bindYjsInstance (y, suffix) {
|
||||
y.define('textarea', Y.Text).bind(document.getElementById('textarea' + suffix))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container2').removeAttribute('disconnected')
|
||||
document.getElementById('container' + suffix).removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container2').setAttribute('disconnected', true)
|
||||
document.getElementById('container' + suffix).setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y1 = new Y('infinite-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example',
|
||||
url: 'https://yjs-v13-third.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.y3 = y
|
||||
y.share.textarea.bind(document.getElementById('textarea3'))
|
||||
y.connector.socket.on('connection', function () {
|
||||
document.getElementById('container3').removeAttribute('disconnected')
|
||||
})
|
||||
y.connector.socket.on('disconnect', function () {
|
||||
document.getElementById('container3').setAttribute('disconnected', true)
|
||||
})
|
||||
})
|
||||
window.y1 = y1
|
||||
bindYjsInstance(y1, '1')
|
||||
|
||||
let y2 = new Y('infinite-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
window.y2 = y2
|
||||
bindYjsInstance(y2, '2')
|
||||
|
||||
let y3 = new Y('infinite-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
window.y3 = y3
|
||||
bindYjsInstance(y1, '3')
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
</g>
|
||||
</svg>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-map/dist/y-map.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../bower_components/d3/d3.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,74 +1,67 @@
|
||||
/* @flow */
|
||||
/* global Y, d3 */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('jigsaw-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Puzzle-example',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
share: {
|
||||
piece1: 'Map',
|
||||
piece2: 'Map',
|
||||
piece3: 'Map',
|
||||
piece4: 'Map'
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yJigsaw = y
|
||||
var origin // mouse start position - translation of piece
|
||||
var drag = d3.behavior.drag()
|
||||
.on('dragstart', function (params) {
|
||||
// get the translation of the element
|
||||
var translation = d3
|
||||
.select(this)
|
||||
.attr('transform')
|
||||
.slice(10, -1)
|
||||
.split(',')
|
||||
.map(Number)
|
||||
// mouse coordinates
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
origin = {
|
||||
x: mouse[0] - translation[0],
|
||||
y: mouse[1] - translation[1]
|
||||
}
|
||||
})
|
||||
.on('drag', function () {
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||
var y = mouse[1] - origin.y
|
||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||
})
|
||||
.on('dragend', function (piece, i) {
|
||||
// save the current translation of the puzzle piece
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x
|
||||
var y = mouse[1] - origin.y
|
||||
piece.set('translation', {x: x, y: y})
|
||||
})
|
||||
})
|
||||
|
||||
var data = [y.share.piece1, y.share.piece2, y.share.piece3, y.share.piece4]
|
||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||
let jigsaw = y.define('jigsaw', Y.Map)
|
||||
window.yJigsaw = y
|
||||
|
||||
pieces
|
||||
.classed('draggable', true)
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
}).call(drag)
|
||||
var origin // mouse start position - translation of piece
|
||||
var drag = d3.behavior.drag()
|
||||
.on('dragstart', function (params) {
|
||||
// get the translation of the element
|
||||
var translation = d3
|
||||
.select(this)
|
||||
.attr('transform')
|
||||
.slice(10, -1)
|
||||
.split(',')
|
||||
.map(Number)
|
||||
// mouse coordinates
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
origin = {
|
||||
x: mouse[0] - translation[0],
|
||||
y: mouse[1] - translation[1]
|
||||
}
|
||||
})
|
||||
.on('drag', function () {
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
|
||||
var y = mouse[1] - origin.y
|
||||
d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
|
||||
})
|
||||
.on('dragend', function (piece, i) {
|
||||
// save the current translation of the puzzle piece
|
||||
var mouse = d3.mouse(this.parentNode)
|
||||
var x = mouse[0] - origin.x
|
||||
var y = mouse[1] - origin.y
|
||||
jigsaw.set(piece, {x: x, y: y})
|
||||
})
|
||||
|
||||
data.forEach(function (piece) {
|
||||
piece.observe(function () {
|
||||
// whenever a property of a piece changes, update the translation of the pieces
|
||||
pieces
|
||||
.transition()
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
var data = ['piece1', 'piece2', 'piece3', 'piece4']
|
||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
|
||||
|
||||
pieces
|
||||
.classed('draggable', true)
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get('translation') || {x: 0, y: 0}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
}).call(drag)
|
||||
|
||||
data.forEach(function (piece) {
|
||||
jigsaw.observe(function () {
|
||||
// whenever a property of a piece changes, update the translation of the pieces
|
||||
pieces
|
||||
.transition()
|
||||
.attr('transform', function (piece) {
|
||||
var translation = piece.get(piece)
|
||||
if (translation == null || typeof translation.x !== 'number' || typeof translation.y !== 'number') {
|
||||
translation = { x: 0, y: 0 }
|
||||
}
|
||||
return 'translate(' + translation.x + ',' + translation.y + ')'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="../bower_components/yjs/y.js"></script>
|
||||
<script src="../bower_components/y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -2,29 +2,21 @@
|
||||
|
||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
// Initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'monaco-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
monaco: 'Text' // y.share.monaco is of type Y.Text
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yMonaco = y
|
||||
|
||||
// Create Monaco editor
|
||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||
language: 'javascript'
|
||||
})
|
||||
|
||||
// Bind to y.share.monaco
|
||||
y.share.monaco.bindMonaco(editor)
|
||||
})
|
||||
let y = new Y('monaco-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
window.yMonaco = y
|
||||
|
||||
// Create Monaco editor
|
||||
var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
|
||||
language: 'javascript'
|
||||
})
|
||||
|
||||
// Bind to y.share.monaco
|
||||
y.define('monaco', Y.Text).bindMonaco(editor)
|
||||
})
|
||||
|
||||
8
examples/notes/index.html
Normal file
8
examples/notes/index.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</head>
|
||||
<script src="./index.mjs" type="module"></script>
|
||||
</head>
|
||||
<body contenteditable="true">
|
||||
</body>
|
||||
</html>
|
||||
48
examples/notes/index.mjs
Normal file
48
examples/notes/index.mjs
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
import IndexedDBPersistence from '../../src/Persistences/IndexeddbPersistence.mjs'
|
||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
|
||||
import Y from '../../src/Y.mjs'
|
||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
|
||||
|
||||
const yCollection = new YCollection(new YWebsocketsConnector(), new IndexedDBPersistence())
|
||||
|
||||
const y = yCollection.getDocument('my-notes')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
persistence.addConnector(persistence)
|
||||
|
||||
const y = new Y()
|
||||
await persistence.persistY(y)
|
||||
|
||||
|
||||
connector.connectY('html-editor', y)
|
||||
persistence.connectY('html-editor', y)
|
||||
|
||||
|
||||
|
||||
|
||||
window.connector = connector
|
||||
|
||||
window.onload = function () {
|
||||
window.domBinding = new DomBinding(window.yXmlType, document.body, { scrollingElement: document.scrollingElement })
|
||||
}
|
||||
|
||||
window.y = y
|
||||
window.yXmlType = y.define('xml', YXmlFragment)
|
||||
window.undoManager = new UndoManager(window.yXmlType, {
|
||||
captureTimeout: 500
|
||||
})
|
||||
|
||||
document.onkeydown = function interceptUndoRedo (e) {
|
||||
if (e.keyCode === 90 && (e.metaKey || e.ctrlKey)) {
|
||||
if (!e.shiftKey) {
|
||||
window.undoManager.undo()
|
||||
} else {
|
||||
window.undoManager.redo()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
1173
examples/package-lock.json
generated
1173
examples/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -9,12 +9,15 @@
|
||||
"author": "Kevin Jahns",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"monaco-editor": "^0.8.3"
|
||||
"monaco-editor": "^0.8.3",
|
||||
"rollup": "^0.52.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"standard": "^10.0.2"
|
||||
},
|
||||
"standard": {
|
||||
"ignore": ["bower_components"]
|
||||
"ignore": [
|
||||
"bower_components"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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,35 +1,18 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!-- quill does not include dist files! We are using the hosted version instead -->
|
||||
<!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
|
||||
<link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
|
||||
<style>
|
||||
#quill-container {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 0px 0px 10px gray;
|
||||
}
|
||||
</style>
|
||||
<!-- Main Quill library -->
|
||||
<script src="../../node_modules/quill/dist/quill.min.js"></script>
|
||||
<link href="../../node_modules/quill/dist/quill.snow.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="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-array/y-array.js"></script>
|
||||
<script src="../../../y-richtext/dist/y-richtext.js"></script>
|
||||
<script src="../../../y-memory/y-memory.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,40 +1,33 @@
|
||||
/* global Y, Quill */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('quill-cursors-0', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'richtext-example-quill-1.0-test',
|
||||
url: 'http://localhost:1234'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yQuill = y
|
||||
|
||||
// create quill element
|
||||
window.quill = new Quill('#quill', {
|
||||
modules: {
|
||||
formula: true,
|
||||
syntax: true,
|
||||
toolbar: [
|
||||
[{ size: ['small', false, 'large', 'huge'] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }]
|
||||
]
|
||||
},
|
||||
theme: 'snow'
|
||||
})
|
||||
// bind quill to richtext type
|
||||
y.share.richtext.bind(window.quill)
|
||||
})
|
||||
|
||||
let quill = new Quill('#quill-container', {
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block'],
|
||||
[{ color: [] }, { background: [] }], // Snow theme fills in values
|
||||
[{ script: 'sub' }, { script: 'super' }],
|
||||
['link', 'image'],
|
||||
['link', 'code-block'],
|
||||
[{ list: 'ordered' }, { list: 'bullet' }]
|
||||
]
|
||||
},
|
||||
placeholder: 'Compose an epic...',
|
||||
theme: 'snow' // or 'bubble'
|
||||
})
|
||||
|
||||
let yText = y.define('quill', Y.Text)
|
||||
|
||||
let quillBinding = new Y.QuillBinding(yText, quill)
|
||||
window.quillBinding = quillBinding
|
||||
window.yText = yText
|
||||
window.y = y
|
||||
window.quill = quill
|
||||
|
||||
@@ -4,10 +4,12 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
entry: 'yjs-dist.esm',
|
||||
dest: 'yjs-dist.js',
|
||||
moduleName: 'Y',
|
||||
format: 'umd',
|
||||
input: 'yjs-dist.mjs',
|
||||
name: 'Y',
|
||||
output: {
|
||||
file: 'yjs-dist.js',
|
||||
format: 'umd'
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
main: true,
|
||||
@@ -16,7 +18,7 @@ export default {
|
||||
}),
|
||||
commonjs()
|
||||
],
|
||||
sourceMap: true,
|
||||
sourcemap: true,
|
||||
banner: `
|
||||
/**
|
||||
* ${pkg.name} - ${pkg.description}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<textarea style="width:80%;" rows=40 id="textfield" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
|
||||
<script src="../../y.js"></script>
|
||||
<script src="../../../y-array/y-array.js"></script>
|
||||
<script src="../../../y-text/y-text.js"></script>
|
||||
<script src="../../../y-websockets-client/y-websockets-client.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('textarea-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
room: 'Textarea-example2',
|
||||
// url: '//localhost:1234',
|
||||
url: 'https://yjs-v13.herokuapp.com/'
|
||||
},
|
||||
share: {
|
||||
textarea: 'Text'
|
||||
},
|
||||
timeout: 5000 // reject if no connection was established within 5 seconds
|
||||
}).then(function (y) {
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
y.share.textarea.bind(document.getElementById('textfield'))
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
})
|
||||
|
||||
window.yTextarea = y
|
||||
|
||||
// bind the textarea to a shared text element
|
||||
let type = y.define('textarea', Y.Text)
|
||||
let textarea = document.querySelector('textarea')
|
||||
window.binding = new Y.TextareaBinding(type, textarea)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
</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="../yjs-dist.js"></script>
|
||||
<script src="../../y.js"></script>
|
||||
<script src='../../../y-websockets-client/y-websockets-client.js'></script>
|
||||
<script src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -23,14 +24,16 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var commands = document.querySelectorAll(".command");
|
||||
Array.prototype.forEach.call(document.querySelectorAll('.command'), function (command) {
|
||||
var execute = function(){
|
||||
eval(command.querySelector("input").value);
|
||||
/* global $ */
|
||||
var commands = document.querySelectorAll('.command')
|
||||
Array.prototype.forEach.call(commands, function (command) {
|
||||
var execute = function () {
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(command.querySelector('input').value)
|
||||
}
|
||||
command.querySelector("button").onclick = execute
|
||||
$(command.querySelector("input")).keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
command.querySelector('button').onclick = execute
|
||||
$(command.querySelector('input')).keyup(function (e) {
|
||||
if (e.keyCode === 13) {
|
||||
execute()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
/* global Y */
|
||||
|
||||
// initialize a shared object. This function call returns a promise!
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory'
|
||||
},
|
||||
let y = new Y('xml-example', {
|
||||
connector: {
|
||||
name: 'websockets-client',
|
||||
// url: 'http://127.0.0.1:1234',
|
||||
url: 'http://192.168.178.81:1234',
|
||||
room: 'Xml-example'
|
||||
},
|
||||
sourceDir: '/bower_components',
|
||||
share: {
|
||||
xml: 'Xml("p")' // y.share.xml is of type Y.Xml with tagname "p"
|
||||
url: 'http://127.0.0.1:1234'
|
||||
}
|
||||
}).then(function (y) {
|
||||
window.yXml = y
|
||||
// bind xml type to a dom, and put it in body
|
||||
window.sharedDom = y.share.xml.getDom()
|
||||
document.body.appendChild(window.sharedDom)
|
||||
})
|
||||
|
||||
window.yXml = y
|
||||
// bind xml type to a dom, and put it in body
|
||||
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
|
||||
document.body.appendChild(window.sharedDom)
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
|
||||
import Y from '../src/Y.js'
|
||||
import yWebsocketsClient from '../../y-websockets-client/src/y-websockets-client.js'
|
||||
|
||||
Y.extend(yWebsocketsClient)
|
||||
|
||||
export default Y
|
||||
2845
package-lock.json
generated
2845
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,22 +1,33 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-34",
|
||||
"version": "13.0.0-60",
|
||||
"description": "A framework for real-time p2p shared editing on any data",
|
||||
"main": "./y.node.js",
|
||||
"browser": "./y.js",
|
||||
"module": "./src/y.js",
|
||||
"scripts": {
|
||||
"start": "node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs",
|
||||
"test": "npm run lint",
|
||||
"debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
|
||||
"lint": "standard",
|
||||
"lint": "standard src/**/*.mjs test/**/*.mjs tests-lib/**/*.mjs",
|
||||
"docs": "esdoc",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
|
||||
"watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
|
||||
"postversion": "npm run dist",
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag"
|
||||
"postpublish": "tag-dist-files --overwrite-existing-tag",
|
||||
"demos": "concurrently 'node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs' 'http-server'"
|
||||
},
|
||||
"now": {
|
||||
"engines": {
|
||||
"node": "10.x.x"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"y.*",
|
||||
"src/*"
|
||||
"src/*",
|
||||
".esdoc.json",
|
||||
"docs/*"
|
||||
],
|
||||
"standard": {
|
||||
"ignore": [
|
||||
@@ -51,8 +62,14 @@
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-latest": "^6.24.1",
|
||||
"chance": "^1.0.9",
|
||||
"codemirror": "^5.37.0",
|
||||
"concurrently": "^3.4.0",
|
||||
"cutest": "^0.1.9",
|
||||
"esdoc": "^1.0.4",
|
||||
"esdoc-standard-plugin": "^1.0.0",
|
||||
"quill": "^1.3.5",
|
||||
"quill-cursors": "^1.0.2",
|
||||
"rollup": "^0.58.2",
|
||||
"rollup-plugin-babel": "^2.7.1",
|
||||
"rollup-plugin-commonjs": "^8.0.2",
|
||||
"rollup-plugin-inject": "^2.0.0",
|
||||
@@ -61,13 +78,10 @@
|
||||
"rollup-plugin-uglify": "^1.0.2",
|
||||
"rollup-regenerator-runtime": "^6.23.1",
|
||||
"rollup-watch": "^3.2.2",
|
||||
"standard": "^10.0.2",
|
||||
"standard": "^11.0.1",
|
||||
"tag-dist-files": "^0.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": "^2.6.8",
|
||||
"fast-diff": "^1.1.2",
|
||||
"utf-8": "^1.0.0",
|
||||
"utf8": "^2.1.2"
|
||||
"uws": "^10.148.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: 'src/Y.js',
|
||||
input: 'src/Y.dist.mjs',
|
||||
name: 'Y',
|
||||
sourcemap: true,
|
||||
output: {
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
babel(),
|
||||
uglify({
|
||||
mangle: {
|
||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
||||
except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
|
||||
},
|
||||
output: {
|
||||
comments: function (node, comment) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
var pkg = require('./package.json')
|
||||
|
||||
export default {
|
||||
input: 'src/y-dist.cjs.js',
|
||||
input: 'src/Y.dist.mjs',
|
||||
nameame: 'Y',
|
||||
sourcemap: true,
|
||||
output: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
|
||||
import multiEntry from 'rollup-plugin-multi-entry'
|
||||
|
||||
export default {
|
||||
input: 'test/y-xml.tests.js',
|
||||
input: 'test/index.mjs',
|
||||
name: 'y-tests',
|
||||
sourcemap: true,
|
||||
output: {
|
||||
@@ -11,12 +11,12 @@ export default {
|
||||
format: 'umd'
|
||||
},
|
||||
plugins: [
|
||||
multiEntry(),
|
||||
nodeResolve({
|
||||
main: true,
|
||||
module: true,
|
||||
browser: true
|
||||
}),
|
||||
commonjs(),
|
||||
multiEntry()
|
||||
commonjs()
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import utf8 from 'utf-8'
|
||||
import ID from '../Util/ID.js'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/RootID.js'
|
||||
|
||||
export default class BinaryDecoder {
|
||||
constructor (buffer) {
|
||||
if (buffer instanceof ArrayBuffer) {
|
||||
this.uint8arr = new Uint8Array(buffer)
|
||||
} else if (buffer instanceof Uint8Array || (typeof Buffer !== 'undefined' && buffer instanceof Buffer)) {
|
||||
this.uint8arr = buffer
|
||||
} else {
|
||||
throw new Error('Expected an ArrayBuffer or Uint8Array!')
|
||||
}
|
||||
this.pos = 0
|
||||
}
|
||||
/**
|
||||
* Clone this decoder instance
|
||||
* Optionally set a new position parameter
|
||||
*/
|
||||
clone (newPos = this.pos) {
|
||||
let decoder = new BinaryDecoder(this.uint8arr)
|
||||
decoder.pos = newPos
|
||||
return decoder
|
||||
}
|
||||
/**
|
||||
* Number of bytes
|
||||
*/
|
||||
get length () {
|
||||
return this.uint8arr.length
|
||||
}
|
||||
/**
|
||||
* Skip one byte, jump to the next position
|
||||
*/
|
||||
skip8 () {
|
||||
this.pos++
|
||||
}
|
||||
/**
|
||||
* Read one byte as unsigned integer
|
||||
*/
|
||||
readUint8 () {
|
||||
return this.uint8arr[this.pos++]
|
||||
}
|
||||
/**
|
||||
* Read 4 bytes as unsigned integer
|
||||
*/
|
||||
readUint32 () {
|
||||
let uint =
|
||||
this.uint8arr[this.pos] +
|
||||
(this.uint8arr[this.pos + 1] << 8) +
|
||||
(this.uint8arr[this.pos + 2] << 16) +
|
||||
(this.uint8arr[this.pos + 3] << 24)
|
||||
this.pos += 4
|
||||
return uint
|
||||
}
|
||||
/**
|
||||
* Look ahead without incrementing position
|
||||
* to the next byte and read it as unsigned integer
|
||||
*/
|
||||
peekUint8 () {
|
||||
return this.uint8arr[this.pos]
|
||||
}
|
||||
/**
|
||||
* Read unsigned integer (32bit) with variable length
|
||||
* 1/8th of the storage is used as encoding overhead
|
||||
* - numbers < 2^7 is stored in one byte
|
||||
* - numbers < 2^14 is stored in two bytes
|
||||
* ..
|
||||
*/
|
||||
readVarUint () {
|
||||
let num = 0
|
||||
let len = 0
|
||||
while (true) {
|
||||
let r = this.uint8arr[this.pos++]
|
||||
num = num | ((r & 0b1111111) << len)
|
||||
len += 7
|
||||
if (r < 1 << 7) {
|
||||
return num >>> 0 // return unsigned number!
|
||||
}
|
||||
if (len > 35) {
|
||||
throw new Error('Integer out of range!')
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Read string of variable length
|
||||
* - varUint is used to store the length of the string
|
||||
*/
|
||||
readVarString () {
|
||||
let len = this.readVarUint()
|
||||
let bytes = new Array(len)
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = this.uint8arr[this.pos++]
|
||||
}
|
||||
return utf8.getStringFromBytes(bytes)
|
||||
}
|
||||
/**
|
||||
* Look ahead and read varString without incrementing position
|
||||
*/
|
||||
peekVarString () {
|
||||
let pos = this.pos
|
||||
let s = this.readVarString()
|
||||
this.pos = pos
|
||||
return s
|
||||
}
|
||||
/**
|
||||
* Read ID
|
||||
* - If first varUint read is 0xFFFFFF a RootID is returned
|
||||
* - Otherwise an ID is returned
|
||||
*/
|
||||
readID () {
|
||||
let user = this.readVarUint()
|
||||
if (user === RootFakeUserID) {
|
||||
// read property name and type id
|
||||
const rid = new RootID(this.readVarString(), null)
|
||||
rid.type = this.readVarUint()
|
||||
return rid
|
||||
}
|
||||
return new ID(user, this.readVarUint())
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import utf8 from 'utf-8'
|
||||
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 bytes = utf8.setBytesFromString(str)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Bindings/Binding.mjs
Normal file
47
src/Bindings/Binding.mjs
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
56
src/Bindings/CodeMirrorBinding/CodeMirrorBinding.mjs
Normal file
56
src/Bindings/CodeMirrorBinding/CodeMirrorBinding.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import Binding from '../Binding.mjs'
|
||||
import simpleDiff from '../../Util/simpleDiff.mjs'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
|
||||
|
||||
function typeObserver () {
|
||||
this._mutualExclude(() => {
|
||||
const textarea = this.target
|
||||
const textType = this.type
|
||||
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
|
||||
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
|
||||
textarea.value = textType.toString()
|
||||
const start = fromRelativePosition(textType._y, relativeStart)
|
||||
const end = fromRelativePosition(textType._y, relativeEnd)
|
||||
textarea.setSelectionRange(start, end)
|
||||
})
|
||||
}
|
||||
|
||||
function domObserver () {
|
||||
this._mutualExclude(() => {
|
||||
let diff = simpleDiff(this.type.toString(), this.target.value)
|
||||
this.type.delete(diff.pos, diff.remove)
|
||||
this.type.insert(diff.pos, diff.insert)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
constructor (textType, domTextarea) {
|
||||
// Binding handles textType as this.type and domTextarea as this.target
|
||||
super(textType, domTextarea)
|
||||
// set initial value
|
||||
domTextarea.value = textType.toString()
|
||||
// Observers are handled by this class
|
||||
this._typeObserver = typeObserver.bind(this)
|
||||
this._domObserver = domObserver.bind(this)
|
||||
textType.observe(this._typeObserver)
|
||||
domTextarea.addEventListener('input', this._domObserver)
|
||||
}
|
||||
destroy () {
|
||||
// Remove everything that is handled by this class
|
||||
this.type.unobserve(this._typeObserver)
|
||||
this.target.unobserve(this._domObserver)
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
140
src/Bindings/DomBinding/DomBinding.mjs
Normal file
140
src/Bindings/DomBinding/DomBinding.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
/* global MutationObserver */
|
||||
|
||||
import Binding from '../Binding.mjs'
|
||||
import { createAssociation, removeAssociation } from './util.mjs'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.mjs'
|
||||
import { defaultFilter, applyFilterOnType } from './filter.mjs'
|
||||
import typeObserver from './typeObserver.mjs'
|
||||
import domObserver from './domObserver.mjs'
|
||||
|
||||
/**
|
||||
* 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 || {}
|
||||
this.scrollingElement = opts.scrollingElement || null
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.unobserveDeep(this._typeObserver)
|
||||
this._mutationObserver.disconnect()
|
||||
const y = this.type._y
|
||||
y.off('beforeTransaction', this._beforeTransactionHandler)
|
||||
y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
|
||||
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
|
||||
*/
|
||||
144
src/Bindings/DomBinding/domObserver.mjs
Normal file
144
src/Bindings/DomBinding/domObserver.mjs
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
|
||||
import {
|
||||
iterateUntilUndeleted,
|
||||
removeAssociation,
|
||||
insertNodeHelper } from './util.mjs'
|
||||
import diff from '../../Util/simpleDiff.mjs'
|
||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.mjs'
|
||||
|
||||
/**
|
||||
* 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.
|
||||
removeAssociation(binding, childNode, childType)
|
||||
childType._delete(y)
|
||||
}
|
||||
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 === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom
|
||||
let parent = dom
|
||||
let yParent
|
||||
do {
|
||||
parent = parent.parentElement
|
||||
yParent = this.domToType.get(parent)
|
||||
} while (yParent === undefined && parent !== null)
|
||||
if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
|
||||
diffChildren.add(parent)
|
||||
}
|
||||
return
|
||||
} else if (yxml === false || yxml.constructor === YXmlHook) {
|
||||
// dom element is filtered / a dom hook
|
||||
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) {
|
||||
const yxml = this.domToType.get(dom)
|
||||
applyChangesFromDom(this, dom, yxml, _document)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
61
src/Bindings/DomBinding/domToType.mjs
Normal file
61
src/Bindings/DomBinding/domToType.mjs
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
import YXmlText from '../../Types/YXml/YXmlText.mjs'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
|
||||
import YXmlElement from '../../Types/YXml/YXmlElement.mjs'
|
||||
import { createAssociation, domsToTypes } from './util.mjs'
|
||||
import { filterDomAttributes, defaultFilter } from './filter.mjs'
|
||||
|
||||
/**
|
||||
* 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.mjs
Normal file
60
src/Bindings/DomBinding/filter.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
import isParentOf from '../../Util/isParentOf.mjs'
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,42 @@
|
||||
/* globals getSelection */
|
||||
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
|
||||
|
||||
let browserSelection = null
|
||||
let relativeSelection = null
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export let beforeTransactionSelectionFixer
|
||||
if (typeof getSelection !== 'undefined') {
|
||||
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, transaction, remote) {
|
||||
beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||
if (!remote) {
|
||||
return
|
||||
}
|
||||
relativeSelection = { from: null, to: null, fromY: null, toY: null }
|
||||
browserSelection = getSelection()
|
||||
const anchorNode = browserSelection.anchorNode
|
||||
if (anchorNode !== null && anchorNode._yxml != null) {
|
||||
const yxml = anchorNode._yxml
|
||||
relativeSelection.from = getRelativePosition(yxml, browserSelection.anchorOffset)
|
||||
relativeSelection.fromY = yxml._y
|
||||
const anchorNodeType = domBinding.domToType.get(anchorNode)
|
||||
if (anchorNode !== null && anchorNodeType !== undefined) {
|
||||
relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
|
||||
relativeSelection.fromY = anchorNodeType._y
|
||||
}
|
||||
const focusNode = browserSelection.focusNode
|
||||
if (focusNode !== null && focusNode._yxml != null) {
|
||||
const yxml = focusNode._yxml
|
||||
relativeSelection.to = getRelativePosition(yxml, browserSelection.focusOffset)
|
||||
relativeSelection.toY = yxml._y
|
||||
const focusNodeType = domBinding.domToType.get(focusNode)
|
||||
if (focusNode !== null && focusNodeType !== undefined) {
|
||||
relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
|
||||
relativeSelection.toY = focusNodeType._y
|
||||
}
|
||||
}
|
||||
} else {
|
||||
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
|
||||
}
|
||||
|
||||
export function afterTransactionSelectionFixer (y, transaction, remote) {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
|
||||
if (relativeSelection === null || !remote) {
|
||||
return
|
||||
}
|
||||
@@ -46,7 +52,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
||||
if (from !== null) {
|
||||
let sel = fromRelativePosition(fromY, from)
|
||||
if (sel !== null) {
|
||||
let node = sel.type.getDom()
|
||||
let node = domBinding.typeToDom.get(sel.type)
|
||||
let offset = sel.offset
|
||||
if (node !== anchorNode || offset !== anchorOffset) {
|
||||
anchorNode = node
|
||||
@@ -58,7 +64,7 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
||||
if (to !== null) {
|
||||
let sel = fromRelativePosition(toY, to)
|
||||
if (sel !== null) {
|
||||
let node = sel.type.getDom()
|
||||
let node = domBinding.typeToDom.get(sel.type)
|
||||
let offset = sel.offset
|
||||
if (node !== focusNode || offset !== focusOffset) {
|
||||
focusNode = node
|
||||
@@ -75,7 +81,4 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
|
||||
focusOffset
|
||||
)
|
||||
}
|
||||
// delete, so the objects can be gc'd
|
||||
relativeSelection = null
|
||||
browserSelection = null
|
||||
}
|
||||
99
src/Bindings/DomBinding/typeObserver.mjs
Normal file
99
src/Bindings/DomBinding/typeObserver.mjs
Normal file
@@ -0,0 +1,99 @@
|
||||
/* global getSelection */
|
||||
|
||||
import YXmlText from '../../Types/YXml/YXmlText.mjs'
|
||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
|
||||
import { removeDomChildrenUntilElementFound } from './util.mjs'
|
||||
|
||||
function findScrollReference (scrollingElement) {
|
||||
if (scrollingElement !== null) {
|
||||
let anchor = getSelection().anchorNode
|
||||
if (anchor == null) {
|
||||
let children = scrollingElement.children // only iterate through non-text nodes
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const elem = children[i]
|
||||
const rect = elem.getBoundingClientRect()
|
||||
if (rect.top >= 0) {
|
||||
return { elem, top: rect.top }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (anchor.nodeType === document.TEXT_NODE) {
|
||||
anchor = anchor.parentElement
|
||||
}
|
||||
const top = anchor.getBoundingClientRect().top
|
||||
return { elem: anchor, top: top }
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function fixScroll (scrollingElement, ref) {
|
||||
if (ref !== null) {
|
||||
const { elem, top } = ref
|
||||
const currentTop = elem.getBoundingClientRect().top
|
||||
const newScroll = scrollingElement.scrollTop + currentTop - top
|
||||
if (newScroll >= 0) {
|
||||
scrollingElement.scrollTop = newScroll
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export default function typeObserver (events) {
|
||||
this._mutualExclude(() => {
|
||||
const scrollRef = findScrollReference(this.scrollingElement)
|
||||
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()
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
fixScroll(this.scrollingElement, scrollRef)
|
||||
})
|
||||
}
|
||||
124
src/Bindings/DomBinding/util.mjs
Normal file
124
src/Bindings/DomBinding/util.mjs
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
import domToType from './domToType.mjs'
|
||||
|
||||
/**
|
||||
* 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.mjs
Normal file
53
src/Bindings/QuillBinding/QuillBinding.mjs
Normal file
@@ -0,0 +1,53 @@
|
||||
import Binding from '../Binding.mjs'
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
56
src/Bindings/TextareaBinding/TextareaBinding.mjs
Normal file
56
src/Bindings/TextareaBinding/TextareaBinding.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import Binding from '../Binding.mjs'
|
||||
import simpleDiff from '../../Util/simpleDiff.mjs'
|
||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
|
||||
|
||||
function typeObserver () {
|
||||
this._mutualExclude(() => {
|
||||
const textarea = this.target
|
||||
const textType = this.type
|
||||
const relativeStart = getRelativePosition(textType, textarea.selectionStart)
|
||||
const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
|
||||
textarea.value = textType.toString()
|
||||
const start = fromRelativePosition(textType._y, relativeStart)
|
||||
const end = fromRelativePosition(textType._y, relativeEnd)
|
||||
textarea.setSelectionRange(start, end)
|
||||
})
|
||||
}
|
||||
|
||||
function domObserver () {
|
||||
this._mutualExclude(() => {
|
||||
let diff = simpleDiff(this.type.toString(), this.target.value)
|
||||
this.type.delete(diff.pos, diff.remove)
|
||||
this.type.insert(diff.pos, diff.insert)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
constructor (textType, domTextarea) {
|
||||
// Binding handles textType as this.type and domTextarea as this.target
|
||||
super(textType, domTextarea)
|
||||
// set initial value
|
||||
domTextarea.value = textType.toString()
|
||||
// Observers are handled by this class
|
||||
this._typeObserver = typeObserver.bind(this)
|
||||
this._domObserver = domObserver.bind(this)
|
||||
textType.observe(this._typeObserver)
|
||||
domTextarea.addEventListener('input', this._domObserver)
|
||||
}
|
||||
destroy () {
|
||||
// Remove everything that is handled by this class
|
||||
this.type.unobserve(this._typeObserver)
|
||||
this.target.unobserve(this._domObserver)
|
||||
super.destroy()
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import BinaryEncoder from './Binary/Encoder.js'
|
||||
import BinaryDecoder from './Binary/Decoder.js'
|
||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
|
||||
import BinaryDecoder from './Util/Binary/Decoder.mjs'
|
||||
|
||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.js'
|
||||
import { readSyncStep2 } from './MessageHandler/syncStep2.js'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.js'
|
||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.mjs'
|
||||
import { readSyncStep2 } from './MessageHandler/syncStep2.mjs'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.mjs'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
// TODO: rename Connector
|
||||
|
||||
export default class AbstractConnector {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
@@ -132,6 +134,7 @@ export default class AbstractConnector {
|
||||
f()
|
||||
}
|
||||
this.whenSyncedListeners = []
|
||||
this.y._setContentReady()
|
||||
this.y.emit('synced')
|
||||
}
|
||||
}
|
||||
@@ -268,7 +271,7 @@ export default class AbstractConnector {
|
||||
if (messageType === 'sync step 2' && senderConn.auth === 'write') {
|
||||
readSyncStep2(decoder, encoder, y, senderConn, sender)
|
||||
} else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
|
||||
integrateRemoteStructs(decoder, encoder, y, senderConn, sender)
|
||||
integrateRemoteStructs(y, decoder)
|
||||
} else {
|
||||
throw new Error('Unable to receive message')
|
||||
}
|
||||
140
src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs
Normal file
140
src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
|
||||
/* global WebSocket */
|
||||
import NamedEventHandler from '../../Util/NamedEventHandler.mjs'
|
||||
import decodeMessage, { messageSS, messageSubscribe, messageStructs } from './decodeMessage.mjs'
|
||||
import { createMutualExclude } from '../../Util/mutualExclude.mjs'
|
||||
import { messageCheckUpdateCounter } from './decodeMessage.mjs'
|
||||
|
||||
export const STATE_DISCONNECTED = 0
|
||||
export const STATE_CONNECTED = 1
|
||||
|
||||
export default class WebsocketsConnector extends NamedEventHandler {
|
||||
constructor (url = 'ws://localhost:1234') {
|
||||
super()
|
||||
this.url = url
|
||||
this._state = STATE_DISCONNECTED
|
||||
this._socket = null
|
||||
this._rooms = new Map()
|
||||
this._connectToServer = true
|
||||
this._reconnectTimeout = 300
|
||||
this._mutualExclude = createMutualExclude()
|
||||
this._persistence = null
|
||||
this.connect()
|
||||
}
|
||||
|
||||
getRoom (roomName) {
|
||||
return this._rooms.get(roomName) || { y: null, roomName, localUpdateCounter: 1 }
|
||||
}
|
||||
|
||||
syncPersistence (persistence) {
|
||||
this._persistence = persistence
|
||||
if (this._state === STATE_CONNECTED) {
|
||||
persistence.getAllDocuments().then(docs => {
|
||||
const encoder = new BinaryEncoder()
|
||||
docs.forEach(doc => {
|
||||
messageCheckUpdateCounter(doc.roomName, encoder, doc.remoteUpdateCounter)
|
||||
});
|
||||
this.send(encoder)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
let room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
throw new Error('Room is already taken! There can be only one Yjs instance per roomName!')
|
||||
}
|
||||
this._rooms.set(roomName, {
|
||||
roomName,
|
||||
y,
|
||||
localUpdateCounter: 1
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
this._mutualExclude(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const room = this._rooms.get(roomName)
|
||||
messageStructs(roomName, y, encoder, transaction.encodedStructs, ++room.localUpdateCounter)
|
||||
this.send(encoder)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (this._state === STATE_CONNECTED) {
|
||||
const encoder = new BinaryEncoder()
|
||||
messageSS(roomName, y, encoder)
|
||||
messageSubscribe(roomName, y, encoder)
|
||||
this.send(encoder)
|
||||
}
|
||||
}
|
||||
|
||||
_setState (state) {
|
||||
this._state = state
|
||||
this.emit('stateChanged', {
|
||||
state: this.state
|
||||
})
|
||||
}
|
||||
|
||||
get state () {
|
||||
return this._state === STATE_DISCONNECTED ? 'disconnected' : 'connected'
|
||||
}
|
||||
|
||||
_onOpen () {
|
||||
this._setState(STATE_CONNECTED)
|
||||
if (this._persistence === null) {
|
||||
const encoder = new BinaryEncoder()
|
||||
for (const [roomName, room] of this._rooms) {
|
||||
const y = room.y
|
||||
messageSS(roomName, y, encoder)
|
||||
messageSubscribe(roomName, y, encoder)
|
||||
}
|
||||
this.send(encoder)
|
||||
} else {
|
||||
this.syncPersistence(this._persistence)
|
||||
}
|
||||
}
|
||||
|
||||
send (encoder) {
|
||||
if (encoder.length > 0 && this._socket.readyState === WebSocket.OPEN) {
|
||||
this._socket.send(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
|
||||
_onClose () {
|
||||
this._setState(STATE_DISCONNECTED)
|
||||
this._socket = null
|
||||
if (this._connectToServer) {
|
||||
setTimeout(() => {
|
||||
if (this._connectToServer) {
|
||||
this.connect()
|
||||
}
|
||||
}, this._reconnectTimeout)
|
||||
this.connect()
|
||||
}
|
||||
}
|
||||
|
||||
_onMessage (message) {
|
||||
if (message.data.byteLength > 0) {
|
||||
const reply = decodeMessage(this, message.data, null, false, this._persistence)
|
||||
this.send(reply)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect (code = 1000, reason = 'Client manually disconnected') {
|
||||
const socket = this._socket
|
||||
this._connectToServer = false
|
||||
socket.close(code, reason)
|
||||
}
|
||||
|
||||
connect () {
|
||||
if (this._socket === null) {
|
||||
const socket = new WebSocket(this.url)
|
||||
socket.binaryType = 'arraybuffer'
|
||||
this._socket = socket
|
||||
this._connectToServer = true
|
||||
// Connection opened
|
||||
socket.addEventListener('open', this._onOpen.bind(this))
|
||||
socket.addEventListener('close', this._onClose.bind(this))
|
||||
socket.addEventListener('message', this._onMessage.bind(this))
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src/Connectors/WebsocketsConnector/decodeMessage.mjs
Normal file
159
src/Connectors/WebsocketsConnector/decodeMessage.mjs
Normal file
@@ -0,0 +1,159 @@
|
||||
import BinaryDecoder from '../../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
|
||||
import { readStateSet, writeStateSet } from '../../MessageHandler/stateSet.mjs'
|
||||
import { writeStructs } from '../../MessageHandler/syncStep1.mjs'
|
||||
import { writeDeleteSet, readDeleteSet } from '../../MessageHandler/deleteSet.mjs'
|
||||
import { integrateRemoteStructs } from '../../MessageHandler/integrateRemoteStructs.mjs'
|
||||
|
||||
const CONTENT_GET_SS = 4
|
||||
export function messageGetSS (roomName, y, encoder) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_GET_SS)
|
||||
}
|
||||
|
||||
const CONTENT_SUBSCRIBE = 3
|
||||
export function messageSubscribe (roomName, y, encoder) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_SUBSCRIBE)
|
||||
}
|
||||
|
||||
const CONTENT_SS = 0
|
||||
export function messageSS (roomName, y, encoder) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_SS)
|
||||
writeStateSet(y, encoder)
|
||||
}
|
||||
|
||||
const CONTENT_STRUCTS_DSS = 2
|
||||
export function messageStructsDSS (roomName, y, encoder, ss, updateCounter) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_STRUCTS_DSS)
|
||||
encoder.writeVarUint(updateCounter)
|
||||
const structsDS = new BinaryEncoder()
|
||||
writeStructs(y, structsDS, ss)
|
||||
writeDeleteSet(y, structsDS)
|
||||
encoder.writeVarUint(structsDS.length)
|
||||
encoder.writeBinaryEncoder(structsDS)
|
||||
}
|
||||
|
||||
const CONTENT_STRUCTS = 5
|
||||
export function messageStructs (roomName, y, encoder, structsBinaryEncoder, updateCounter) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_STRUCTS)
|
||||
encoder.writeVarUint(updateCounter)
|
||||
encoder.writeVarUint(structsBinaryEncoder.length)
|
||||
encoder.writeBinaryEncoder(structsBinaryEncoder)
|
||||
}
|
||||
|
||||
const CONTENT_CHECK_COUNTER = 6
|
||||
export function messageCheckUpdateCounter (roomName, encoder, updateCounter = 0) {
|
||||
encoder.writeVarString(roomName)
|
||||
encoder.writeVarUint(CONTENT_CHECK_COUNTER)
|
||||
encoder.writeVarUint(updateCounter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a client-message.
|
||||
*
|
||||
* A client-message consists of multiple message-elements that are concatenated without delimiter.
|
||||
* Each has the following structure:
|
||||
* - roomName
|
||||
* - content_type
|
||||
* - content (additional info that is encoded based on the value of content_type)
|
||||
*
|
||||
* The message is encoded until no more message-elements are available.
|
||||
*
|
||||
* @param {*} connector The connector that handles the connections
|
||||
* @param {*} message The binary encoded message
|
||||
* @param {*} ws The connection object
|
||||
*/
|
||||
export default function decodeMessage (connector, message, ws, isServer = false, persistence) {
|
||||
const decoder = new BinaryDecoder(message)
|
||||
const encoder = new BinaryEncoder()
|
||||
while (decoder.hasContent()) {
|
||||
const roomName = decoder.readVarString()
|
||||
const contentType = decoder.readVarUint()
|
||||
const room = connector.getRoom(roomName)
|
||||
const y = room.y
|
||||
switch (contentType) {
|
||||
case CONTENT_CHECK_COUNTER:
|
||||
const updateCounter = decoder.readVarUint()
|
||||
if (room.localUpdateCounter !== updateCounter) {
|
||||
messageGetSS(roomName, y, encoder)
|
||||
}
|
||||
connector.subscribe(roomName, ws)
|
||||
break
|
||||
case CONTENT_STRUCTS:
|
||||
console.log(`${roomName}: received update`)
|
||||
connector._mutualExclude(() => {
|
||||
const remoteUpdateCounter = decoder.readVarUint()
|
||||
persistence.setRemoteUpdateCounter(roomName, remoteUpdateCounter)
|
||||
const messageLen = decoder.readVarUint()
|
||||
if (y === null) {
|
||||
persistence._persistStructs(roomName, decoder.readArrayBuffer(messageLen))
|
||||
} else {
|
||||
y.transact(() => {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
}, true)
|
||||
}
|
||||
})
|
||||
break
|
||||
case CONTENT_GET_SS:
|
||||
if (y !== null) {
|
||||
messageSS(roomName, y, encoder)
|
||||
} else {
|
||||
persistence._createYInstance(roomName).then(y => {
|
||||
const encoder = new BinaryEncoder()
|
||||
messageSS(roomName, y, encoder)
|
||||
connector.send(encoder, ws)
|
||||
})
|
||||
}
|
||||
break
|
||||
case CONTENT_SUBSCRIBE:
|
||||
connector.subscribe(roomName, ws)
|
||||
break
|
||||
case CONTENT_SS:
|
||||
// received state set
|
||||
// reply with missing content
|
||||
const ss = readStateSet(decoder)
|
||||
const sendStructsDSS = () => {
|
||||
if (y !== null) { // TODO: how to sync local content?
|
||||
const encoder = new BinaryEncoder()
|
||||
messageStructsDSS(roomName, y, encoder, ss, room.localUpdateCounter) // room.localUpdateHandler in case it changes
|
||||
if (isServer) {
|
||||
messageSS(roomName, y, encoder)
|
||||
}
|
||||
connector.send(encoder, ws)
|
||||
}
|
||||
}
|
||||
if (room.persistenceLoaded !== undefined) {
|
||||
room.persistenceLoaded.then(sendStructsDSS)
|
||||
} else {
|
||||
sendStructsDSS()
|
||||
}
|
||||
break
|
||||
case CONTENT_STRUCTS_DSS:
|
||||
console.log(`${roomName}: synced`)
|
||||
connector._mutualExclude(() => {
|
||||
const remoteUpdateCounter = decoder.readVarUint()
|
||||
persistence.setRemoteUpdateCounter(roomName, remoteUpdateCounter)
|
||||
const messageLen = decoder.readVarUint()
|
||||
if (y === null) {
|
||||
persistence._persistStructsDS(roomName, decoder.readArrayBuffer(messageLen))
|
||||
} else {
|
||||
y.transact(() => {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
}, true)
|
||||
}
|
||||
})
|
||||
break
|
||||
default:
|
||||
console.error('Unexpected content type!')
|
||||
if (ws !== null) {
|
||||
ws.close() // TODO: specify reason
|
||||
}
|
||||
}
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
124
src/Connectors/WebsocketsConnector/server.mjs
Normal file
124
src/Connectors/WebsocketsConnector/server.mjs
Normal file
@@ -0,0 +1,124 @@
|
||||
import Y from '../../Y.mjs'
|
||||
import uws from 'uws'
|
||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
|
||||
import decodeMessage, { messageStructs } from './decodeMessage.mjs'
|
||||
import FilePersistence from '../../Persistences/FilePersistence.mjs'
|
||||
|
||||
const WebsocketsServer = uws.Server
|
||||
const persistence = new FilePersistence('.yjsPersisted')
|
||||
/**
|
||||
* Maps from room-name to ..
|
||||
* {
|
||||
* connections, // Set of ws-clients that listen to the room
|
||||
* y // Yjs instance that handles the room
|
||||
* }
|
||||
*/
|
||||
const rooms = new Map()
|
||||
/**
|
||||
* Maps from ws-connection to Set<roomName> - the set of connected roomNames
|
||||
*/
|
||||
const connections = new Map()
|
||||
const port = process.env.PORT || 1234
|
||||
const wss = new WebsocketsServer({
|
||||
port,
|
||||
perMessageDeflate: {}
|
||||
})
|
||||
|
||||
/**
|
||||
* Set of room names that are scheduled to be sweeped (destroyed because they don't have a connection anymore)
|
||||
*/
|
||||
const scheduledSweeps = new Set()
|
||||
/* TODO: enable sweeping
|
||||
setInterval(function sweepRoomes () {
|
||||
scheduledSweeps.forEach(roomName => {
|
||||
const room = rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
if (room.connections.size === 0) {
|
||||
persistence.saveState(roomName, room.y).then(() => {
|
||||
if (room.connections.size === 0) {
|
||||
room.y.destroy()
|
||||
rooms.delete(roomName)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
scheduledSweeps.clear()
|
||||
}, 5000) */
|
||||
|
||||
const wsConnector = {
|
||||
send: (encoder, ws) => {
|
||||
const message = encoder.createBuffer()
|
||||
ws.send(message, null, null, true)
|
||||
},
|
||||
_mutualExclude: f => { f() },
|
||||
subscribe: function subscribe (roomName, ws) {
|
||||
let roomNames = connections.get(ws)
|
||||
if (roomNames === undefined) {
|
||||
roomNames = new Set()
|
||||
connections.set(ws, roomNames)
|
||||
}
|
||||
roomNames.add(roomName)
|
||||
const room = this.getRoom(roomName)
|
||||
room.connections.add(ws)
|
||||
},
|
||||
getRoom: function getRoom (roomName) {
|
||||
let room = rooms.get(roomName)
|
||||
if (room === undefined) {
|
||||
const y = new Y(roomName, null, null, { gc: true })
|
||||
const persistenceLoaded = persistence.readState(roomName, y)
|
||||
room = {
|
||||
name: roomName,
|
||||
connections: new Set(),
|
||||
y,
|
||||
persistenceLoaded,
|
||||
localUpdateCounter: 1
|
||||
}
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
// save to persistence
|
||||
persistence.saveUpdate(roomName, y, transaction.encodedStructs)
|
||||
// forward update to clients
|
||||
persistence._mutex(() => { // do not broadcast if persistence.readState is called
|
||||
const encoder = new BinaryEncoder()
|
||||
messageStructs(roomName, y, encoder, transaction.encodedStructs, ++room.localUpdateCounter)
|
||||
const message = encoder.createBuffer()
|
||||
// when changed, broakcast update to all connections
|
||||
room.connections.forEach(conn => {
|
||||
conn.send(message, null, null, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
rooms.set(roomName, room)
|
||||
}
|
||||
return room
|
||||
}
|
||||
}
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
ws.on('message', function onWSMessage (message) {
|
||||
if (message.byteLength > 0) {
|
||||
const reply = decodeMessage(wsConnector, message, ws, true, persistence)
|
||||
if (reply.length > 0) {
|
||||
ws.send(reply.createBuffer(), null, null, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
ws.on('close', function onWSClose () {
|
||||
const roomNames = connections.get(ws)
|
||||
if (roomNames !== undefined) {
|
||||
roomNames.forEach(roomName => {
|
||||
const room = rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
const connections = room.connections
|
||||
connections.delete(ws)
|
||||
if (connections.size === 0) {
|
||||
scheduledSweeps.add(roomName)
|
||||
}
|
||||
}
|
||||
})
|
||||
connections.delete(ws)
|
||||
}
|
||||
})
|
||||
})
|
||||
32
src/MessageHandler/binaryEncode.mjs
Normal file
32
src/MessageHandler/binaryEncode.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
|
||||
import { writeStructs } from './syncStep1.mjs'
|
||||
import { integrateRemoteStructs } from './integrateRemoteStructs.mjs'
|
||||
import { readDeleteSet, writeDeleteSet } from './deleteSet.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
y.transact(function () {
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(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) {
|
||||
let encoder = new BinaryEncoder()
|
||||
writeStructs(y, encoder, new Map())
|
||||
writeDeleteSet(y, encoder)
|
||||
return encoder
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { deleteItemRange } from '../Struct/Delete.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { deleteItemRange } from '../Struct/Delete.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
|
||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
|
||||
let dsLength = decoder.readUint32()
|
||||
@@ -92,7 +92,7 @@ export function readDeleteSet (y, decoder) {
|
||||
// delete maximum the len of d
|
||||
// else delete as much as possible
|
||||
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])
|
||||
} else {
|
||||
// 3)
|
||||
@@ -100,7 +100,7 @@ export function readDeleteSet (y, decoder) {
|
||||
if (d[2] && !n.gc) {
|
||||
// d marks as gc'd but n does not
|
||||
// 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])])
|
||||
}
|
||||
}
|
||||
@@ -117,12 +117,12 @@ export function readDeleteSet (y, decoder) {
|
||||
// Adapt the Tree implementation to support delete while iterating
|
||||
for (let i = deletions.length - 1; i >= 0; 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 (; pos < dv.length; 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]])
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import BinaryDecoder from '../Binary/Decoder.js'
|
||||
import { logID } from './messageToString.js'
|
||||
import { getStruct } from '../Util/structReferences.mjs'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import { logID } from './messageToString.mjs'
|
||||
import GC from '../Struct/GC.mjs'
|
||||
|
||||
class MissingEntry {
|
||||
constructor (decoder, missing, struct) {
|
||||
@@ -11,6 +12,7 @@ class MissingEntry {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Integrate remote struct
|
||||
* When a remote struct is integrated, other structs might be ready to ready to
|
||||
* integrate.
|
||||
@@ -23,7 +25,14 @@ function _integrateRemoteStructHelper (y, struct) {
|
||||
if (y.ss.getState(id.user) > id.clock) {
|
||||
return
|
||||
}
|
||||
struct._integrate(y)
|
||||
if (!y.gcEnabled || 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)
|
||||
} else {
|
||||
// Is an Item. parent was deleted.
|
||||
struct._gc(y)
|
||||
}
|
||||
let msu = y._missingStructs.get(id.user)
|
||||
if (msu != null) {
|
||||
let clock = id.clock
|
||||
@@ -65,7 +74,7 @@ export function stringifyStructs (y, decoder, strBuilder) {
|
||||
}
|
||||
}
|
||||
|
||||
export function integrateRemoteStructs (decoder, encoder, y) {
|
||||
export function integrateRemoteStructs (y, decoder) {
|
||||
const len = decoder.readUint32()
|
||||
for (let i = 0; i < len; i++) {
|
||||
let reference = decoder.readVarUint()
|
||||
@@ -1,10 +1,10 @@
|
||||
import BinaryDecoder from '../Binary/Decoder.js'
|
||||
import { stringifyStructs } from './integrateRemoteStructs.js'
|
||||
import { stringifySyncStep1 } from './syncStep1.js'
|
||||
import { stringifySyncStep2 } from './syncStep2.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import RootID from '../Util/RootID.js'
|
||||
import Y from '../Y.js'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import { stringifyStructs } from './integrateRemoteStructs.mjs'
|
||||
import { stringifySyncStep1 } from './syncStep1.mjs'
|
||||
import { stringifySyncStep2 } from './syncStep2.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import RootID from '../Util/ID/RootID.mjs'
|
||||
import Y from '../Y.mjs'
|
||||
|
||||
export function messageToString ([y, buffer]) {
|
||||
let decoder = new BinaryDecoder(buffer)
|
||||
@@ -46,3 +46,20 @@ export function logID (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)},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 { readStateSet, writeStateSet } from './stateSet.js'
|
||||
import { writeDeleteSet } from './deleteSet.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
import { readStateSet, writeStateSet } from './stateSet.mjs'
|
||||
import { writeDeleteSet } from './deleteSet.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { RootFakeUserID } from '../Util/ID/RootID.mjs'
|
||||
|
||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
|
||||
let auth = decoder.readVarString()
|
||||
@@ -30,14 +30,27 @@ export function sendSyncStep1 (connector, syncUser) {
|
||||
connector.send(syncUser, encoder.createBuffer())
|
||||
}
|
||||
|
||||
export default function writeStructs (encoder, decoder, y, ss) {
|
||||
/**
|
||||
* @private
|
||||
* Write all Items that are not not included in ss to
|
||||
* the encoder object.
|
||||
*/
|
||||
export function writeStructs (y, encoder, ss) {
|
||||
const lenPos = encoder.pos
|
||||
encoder.writeUint32(0)
|
||||
let len = 0
|
||||
for (let user of y.ss.state.keys()) {
|
||||
let clock = ss.get(user) || 0
|
||||
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)
|
||||
len++
|
||||
})
|
||||
@@ -60,7 +73,7 @@ export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
|
||||
encoder.writeVarString('sync step 2')
|
||||
encoder.writeVarString(y.connector.authInfo || '')
|
||||
const ss = readStateSet(decoder)
|
||||
writeStructs(encoder, decoder, y, ss)
|
||||
writeStructs(y, encoder, ss)
|
||||
writeDeleteSet(y, encoder)
|
||||
y.connector.send(senderConn.uid, encoder.createBuffer())
|
||||
senderConn.receivedSyncStep2 = true
|
||||
@@ -1,5 +1,5 @@
|
||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.js'
|
||||
import { readDeleteSet } from './deleteSet.js'
|
||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.mjs'
|
||||
import { readDeleteSet } from './deleteSet.mjs'
|
||||
|
||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
|
||||
strBuilder.push(' - auth: ' + decoder.readVarString())
|
||||
@@ -22,7 +22,7 @@ export function stringifySyncStep2 (y, decoder, strBuilder) {
|
||||
}
|
||||
|
||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
|
||||
integrateRemoteStructs(decoder, encoder, y)
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
y.connector._setSyncedWith(sender)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// import BinaryEncoder from './Binary/Encoder.js'
|
||||
|
||||
export default function extendPersistence (Y) {
|
||||
class AbstractPersistence {
|
||||
constructor (y, opts) {
|
||||
this.y = y
|
||||
this.opts = opts
|
||||
this.saveOperationsBuffer = []
|
||||
this.log = Y.debug('y:persistence')
|
||||
}
|
||||
|
||||
saveToMessageQueue (binary) {
|
||||
this.log('Room %s: Save message to message queue', this.y.options.connector.room)
|
||||
}
|
||||
|
||||
saveOperations (ops) {
|
||||
ops = ops.map(function (op) {
|
||||
return Y.Struct[op.struct].encode(op)
|
||||
})
|
||||
/*
|
||||
const saveOperations = () => {
|
||||
if (this.saveOperationsBuffer.length > 0) {
|
||||
let encoder = new BinaryEncoder()
|
||||
encoder.writeVarString(this.opts.room)
|
||||
encoder.writeVarString('update')
|
||||
let ops = this.saveOperationsBuffer
|
||||
this.saveOperationsBuffer = []
|
||||
let length = ops.length
|
||||
encoder.writeUint32(length)
|
||||
for (var i = 0; i < length; i++) {
|
||||
let op = ops[i]
|
||||
Y.Struct[op.struct].binaryEncode(encoder, op)
|
||||
}
|
||||
this.saveToMessageQueue(encoder.createBuffer())
|
||||
}
|
||||
}
|
||||
*/
|
||||
if (this.saveOperationsBuffer.length === 0) {
|
||||
this.saveOperationsBuffer = ops
|
||||
} else {
|
||||
this.saveOperationsBuffer = this.saveOperationsBuffer.concat(ops)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Y.AbstractPersistence = AbstractPersistence
|
||||
}
|
||||
122
src/Persistence.mjs
Normal file
122
src/Persistence.mjs
Normal file
@@ -0,0 +1,122 @@
|
||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
|
||||
import BinaryDecoder from './Util/Binary/Decoder.mjs'
|
||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.mjs'
|
||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.mjs'
|
||||
import { createMutualExclude } from './Util/mutualExclude.mjs'
|
||||
|
||||
function getFreshCnf () {
|
||||
let buffer = new BinaryEncoder()
|
||||
buffer.writeUint32(0)
|
||||
return {
|
||||
len: 0,
|
||||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract persistence class.
|
||||
*/
|
||||
export default class AbstractPersistence {
|
||||
constructor (opts) {
|
||||
this.opts = opts
|
||||
this.ys = new Map()
|
||||
}
|
||||
|
||||
_init (y) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf === undefined) {
|
||||
cnf = getFreshCnf()
|
||||
cnf.mutualExclude = createMutualExclude()
|
||||
this.ys.set(y, cnf)
|
||||
return this.init(y).then(() => {
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf.len > 0) {
|
||||
cnf.buffer.setUint32(0, cnf.len)
|
||||
this.saveUpdate(y, cnf.buffer.createBuffer(), transaction)
|
||||
let _cnf = getFreshCnf()
|
||||
for (let key in _cnf) {
|
||||
cnf[key] = _cnf[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
return this.retrieve(y)
|
||||
}).then(function () {
|
||||
return Promise.resolve(cnf)
|
||||
})
|
||||
} else {
|
||||
return Promise.resolve(cnf)
|
||||
}
|
||||
}
|
||||
deinit (y) {
|
||||
this.ys.delete(y)
|
||||
y.persistence = null
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.ys = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*
|
||||
* ** Must be overwritten! **
|
||||
*/
|
||||
removePersistedData (room, destroyYjsInstances = true) {
|
||||
this.ys.forEach((cnf, y) => {
|
||||
if (y.room === room) {
|
||||
if (destroyYjsInstances) {
|
||||
y.destroy()
|
||||
} else {
|
||||
this.deinit(y)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* overwrite */
|
||||
saveUpdate (buffer) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Save struct to update buffer.
|
||||
* saveUpdate is called when transaction ends
|
||||
*/
|
||||
saveStruct (y, struct) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf !== undefined) {
|
||||
cnf.mutualExclude(function () {
|
||||
struct._toBinary(cnf.buffer)
|
||||
cnf.len++
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* overwrite */
|
||||
retrieve (y, model, updates) {
|
||||
let cnf = this.ys.get(y)
|
||||
if (cnf !== undefined) {
|
||||
cnf.mutualExclude(function () {
|
||||
y.transact(function () {
|
||||
if (model != null) {
|
||||
fromBinary(y, new BinaryDecoder(new Uint8Array(model)))
|
||||
}
|
||||
if (updates != null) {
|
||||
for (let i = 0; i < updates.length; i++) {
|
||||
integrateRemoteStructs(y, new BinaryDecoder(new Uint8Array(updates[i])))
|
||||
}
|
||||
}
|
||||
})
|
||||
y.emit('persistenceReady')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* overwrite */
|
||||
persist (y) {
|
||||
return toBinary(y).createBuffer()
|
||||
}
|
||||
}
|
||||
2
src/Persistences/AbstractPersistence.mjs
Normal file
2
src/Persistences/AbstractPersistence.mjs
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
export default class AbstractPersistence {}
|
||||
72
src/Persistences/FilePersistence.mjs
Normal file
72
src/Persistences/FilePersistence.mjs
Normal file
@@ -0,0 +1,72 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.mjs'
|
||||
|
||||
function createFilePath (persistence, roomName) {
|
||||
// TODO: filename checking!
|
||||
return path.join(persistence.dir, roomName)
|
||||
}
|
||||
|
||||
export default class FilePersistence {
|
||||
constructor (dir) {
|
||||
this.dir = dir
|
||||
this._mutex = createMutualExclude()
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
// TODO: implement
|
||||
// nop
|
||||
}
|
||||
saveUpdate (room, y, encodedStructs) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._mutex(() => {
|
||||
const filePath = createFilePath(this, room)
|
||||
const updateMessage = new BinaryEncoder()
|
||||
encodeUpdate(y, encodedStructs, updateMessage)
|
||||
fs.appendFile(filePath, Buffer.from(updateMessage.createBuffer()), (err) => {
|
||||
if (err !== null) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}, resolve)
|
||||
})
|
||||
}
|
||||
saveState (roomName, y) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
const filePath = createFilePath(this, roomName)
|
||||
fs.writeFile(filePath, Buffer.from(encoder.createBuffer()), (err) => {
|
||||
if (err !== null) {
|
||||
reject(err)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
readState (roomName, y) {
|
||||
// Check if the file exists in the current directory.
|
||||
return new Promise((resolve, reject) => {
|
||||
const filePath = path.join(this.dir, roomName)
|
||||
fs.readFile(filePath, (err, data) => {
|
||||
if (err !== null) {
|
||||
resolve()
|
||||
// reject(err)
|
||||
} else {
|
||||
this._mutex(() => {
|
||||
console.info(`unpacking data (${data.length})`)
|
||||
console.time('unpacking')
|
||||
decodePersisted(y, new BinaryDecoder(data))
|
||||
console.timeEnd('unpacking')
|
||||
})
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
283
src/Persistences/IndexedDBPersistence.mjs
Normal file
283
src/Persistences/IndexedDBPersistence.mjs
Normal file
@@ -0,0 +1,283 @@
|
||||
/* global indexedDB, location, BroadcastChannel */
|
||||
|
||||
import Y from '../Y.mjs'
|
||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
|
||||
import { decodePersisted, encodeStructsDS, encodeUpdate } from './decodePersisted.mjs'
|
||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
|
||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
|
||||
import { PERSIST_STRUCTS_DS } from './decodePersisted.mjs';
|
||||
import { PERSIST_UPDATE } from './decodePersisted.mjs';
|
||||
/*
|
||||
* Request to Promise transformer
|
||||
*/
|
||||
function rtop (request) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
resolve(event.target.result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openDB (room) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open(room)
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('updates')) {
|
||||
db.deleteObjectStore('updates')
|
||||
}
|
||||
db.createObjectStore('updates', {autoIncrement: true})
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function persist (room) {
|
||||
let t = room.db.transaction(['updates'], 'readwrite')
|
||||
let updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.getAll())
|
||||
.then(updates => {
|
||||
// apply all previous updates before deleting them
|
||||
room.mutex(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
})
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
// delete all pending updates
|
||||
rtop(updatesStore.clear()).then(() => {
|
||||
// write current model
|
||||
updatesStore.put(encoder.createBuffer())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveUpdate (room, updateBuffer) {
|
||||
const db = room.db
|
||||
if (db !== null) {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
const updatePut = rtop(updatesStore.put(updateBuffer))
|
||||
rtop(updatesStore.count()).then(cnt => {
|
||||
if (cnt >= PREFERRED_TRIM_SIZE) {
|
||||
persist(room)
|
||||
}
|
||||
})
|
||||
return updatePut
|
||||
}
|
||||
}
|
||||
|
||||
function registerRoomInPersistence (documentsDB, roomName) {
|
||||
return documentsDB.then(
|
||||
db => Promise.all([
|
||||
db,
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
])
|
||||
).then(
|
||||
([db, doc]) => {
|
||||
if (doc === undefined) {
|
||||
return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const PREFERRED_TRIM_SIZE = 400
|
||||
|
||||
export default class IndexedDBPersistence {
|
||||
constructor () {
|
||||
this._rooms = new Map()
|
||||
this._documentsDB = new Promise(function (resolve, reject) {
|
||||
let request = indexedDB.open('_yjs_documents')
|
||||
request.onupgradeneeded = function (event) {
|
||||
const db = event.target.result
|
||||
if (db.objectStoreNames.contains('documents')) {
|
||||
db.deleteObjectStore('documents')
|
||||
}
|
||||
db.createObjectStore('documents', { keyPath: "roomName" })
|
||||
}
|
||||
request.onerror = function (event) {
|
||||
reject(new Error(event.target.error))
|
||||
}
|
||||
request.onblocked = function () {
|
||||
location.reload()
|
||||
}
|
||||
request.onsuccess = function (event) {
|
||||
const db = event.target.result
|
||||
db.onversionchange = function () { db.close() }
|
||||
resolve(db)
|
||||
}
|
||||
})
|
||||
addEventListener('unload', () => {
|
||||
// close everything when page unloads
|
||||
this._rooms.forEach(room => {
|
||||
if (room.db !== null) {
|
||||
room.db.close()
|
||||
} else {
|
||||
room.dbPromise.then(db => db.close())
|
||||
}
|
||||
})
|
||||
this._documentsDB.then(db => db.close())
|
||||
})
|
||||
}
|
||||
getAllDocuments () {
|
||||
return this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
|
||||
)
|
||||
}
|
||||
setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
|
||||
this._documentsDB.then(
|
||||
db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
|
||||
)
|
||||
}
|
||||
|
||||
_createYInstance (roomName) {
|
||||
const room = this._rooms.get(roomName)
|
||||
if (room !== undefined) {
|
||||
return room.y
|
||||
}
|
||||
const y = new Y()
|
||||
return openDB(roomName).then(
|
||||
db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
|
||||
).then(
|
||||
updates =>
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
).then(() => Promise.resolve(y))
|
||||
}
|
||||
|
||||
_persistStructsDS (roomName, structsDS) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
encoder.writeArrayBuffer(structsDS)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
_persistStructs (roomName, structs) {
|
||||
const encoder = new BinaryEncoder()
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeArrayBuffer(structs)
|
||||
return openDB(roomName).then(db => {
|
||||
const t = db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
return rtop(updatesStore.put(encoder.createBuffer()))
|
||||
})
|
||||
}
|
||||
|
||||
connectY (roomName, y) {
|
||||
if (this._rooms.has(roomName)) {
|
||||
throw new Error('A Y instance is already bound to this room!')
|
||||
}
|
||||
let room = {
|
||||
db: null,
|
||||
dbPromise: null,
|
||||
channel: null,
|
||||
mutex: createMutualExclude(),
|
||||
y
|
||||
}
|
||||
if (typeof BroadcastChannel !== 'undefined') {
|
||||
room.channel = new BroadcastChannel('__yjs__' + roomName)
|
||||
room.channel.addEventListener('message', e => {
|
||||
room.mutex(function () {
|
||||
decodePersisted(y, new BinaryDecoder(e.data))
|
||||
})
|
||||
})
|
||||
}
|
||||
y.on('destroyed', () => {
|
||||
this.disconnectY(roomName, y)
|
||||
})
|
||||
y.on('afterTransaction', (y, transaction) => {
|
||||
room.mutex(() => {
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
const encoder = new BinaryEncoder()
|
||||
const update = new BinaryEncoder()
|
||||
encodeUpdate(y, transaction.encodedStructs, update)
|
||||
const updateBuffer = update.createBuffer()
|
||||
if (room.channel !== null) {
|
||||
room.channel.postMessage(updateBuffer)
|
||||
}
|
||||
if (transaction.encodedStructsLen > 0) {
|
||||
if (room.db !== null) {
|
||||
saveUpdate(room, updateBuffer)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
// register document in documentsDB
|
||||
this._documentsDB.then(
|
||||
db =>
|
||||
rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
|
||||
.then(
|
||||
doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
|
||||
)
|
||||
)
|
||||
// open room db and read existing data
|
||||
return room.dbPromise = openDB(roomName)
|
||||
.then(db => {
|
||||
room.db = db
|
||||
const t = room.db.transaction(['updates'], 'readwrite')
|
||||
const updatesStore = t.objectStore('updates')
|
||||
// write current state as update
|
||||
const encoder = new BinaryEncoder()
|
||||
encodeStructsDS(y, encoder)
|
||||
return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
|
||||
// read persisted state
|
||||
return rtop(updatesStore.getAll()).then(updates => {
|
||||
room.mutex(() => {
|
||||
y.transact(() => {
|
||||
updates.forEach(update => {
|
||||
decodePersisted(y, new BinaryDecoder(update))
|
||||
})
|
||||
}, true)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
disconnectY (roomName) {
|
||||
const {
|
||||
db, channel
|
||||
} = this._rooms.get(roomName)
|
||||
db.close()
|
||||
if (channel !== null) {
|
||||
channel.close()
|
||||
}
|
||||
this._rooms.delete(roomName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all persisted data that belongs to a room.
|
||||
* Automatically destroys all Yjs all Yjs instances that persist to
|
||||
* the room. If `destroyYjsInstances = false` the persistence functionality
|
||||
* will be removed from the Yjs instances.
|
||||
*/
|
||||
removePersistedData (roomName, destroyYjsInstances = true) {
|
||||
this.disconnectY(roomName)
|
||||
return rtop(indexedDB.deleteDatabase(roomName))
|
||||
}
|
||||
}
|
||||
51
src/Persistences/decodePersisted.mjs
Normal file
51
src/Persistences/decodePersisted.mjs
Normal file
@@ -0,0 +1,51 @@
|
||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.mjs'
|
||||
import { writeStructs } from '../MessageHandler/syncStep1.mjs'
|
||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.mjs'
|
||||
|
||||
export const PERSIST_UPDATE = 0
|
||||
/**
|
||||
* Write an update to an encoder.
|
||||
*
|
||||
* @param {Yjs} y A Yjs instance
|
||||
* @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs
|
||||
*/
|
||||
export function encodeUpdate (y, updateEncoder, encoder) {
|
||||
encoder.writeVarUint(PERSIST_UPDATE)
|
||||
encoder.writeBinaryEncoder(updateEncoder)
|
||||
}
|
||||
|
||||
export const PERSIST_STRUCTS_DS = 1
|
||||
|
||||
/**
|
||||
* Write the current Yjs data model to an encoder.
|
||||
*
|
||||
* @param {Yjs} y A Yjs instance
|
||||
* @param {BinaryEncoder} encoder An encoder to write to
|
||||
*/
|
||||
export function encodeStructsDS (y, encoder) {
|
||||
encoder.writeVarUint(PERSIST_STRUCTS_DS)
|
||||
writeStructs(y, encoder, new Map())
|
||||
writeDeleteSet(y, encoder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed the Yjs instance with the persisted state
|
||||
* @param {Yjs} y A Yjs instance.
|
||||
* @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
|
||||
*/
|
||||
export function decodePersisted (y, decoder) {
|
||||
y.transact(() => {
|
||||
while (decoder.hasContent()) {
|
||||
const contentType = decoder.readVarUint()
|
||||
switch (contentType) {
|
||||
case PERSIST_UPDATE:
|
||||
integrateRemoteStructs(y, decoder)
|
||||
break
|
||||
case PERSIST_STRUCTS_DS:
|
||||
integrateRemoteStructs(y, decoder)
|
||||
readDeleteSet(y, decoder)
|
||||
break
|
||||
}
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import Tree from '../Util/Tree.js'
|
||||
import ID from '../Util/ID.js'
|
||||
|
||||
class DSNode {
|
||||
constructor (id, len, gc) {
|
||||
this._id = id
|
||||
this.len = len
|
||||
this.gc = gc
|
||||
}
|
||||
clone () {
|
||||
return new DSNode(this._id, this.len, this.gc)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DeleteStore extends Tree {
|
||||
logTable () {
|
||||
const deletes = []
|
||||
this.iterate(null, null, function (n) {
|
||||
deletes.push({
|
||||
user: n._id.user,
|
||||
clock: n._id.clock,
|
||||
len: n.len,
|
||||
gc: n.gc
|
||||
})
|
||||
})
|
||||
console.table(deletes)
|
||||
}
|
||||
isDeleted (id) {
|
||||
var n = this.findWithUpperBound(id)
|
||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
||||
}
|
||||
/*
|
||||
* Mark an operation as deleted. returns the deleted node
|
||||
*/
|
||||
markDeleted (id, length) {
|
||||
if (length == null) {
|
||||
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
|
||||
}
|
||||
}
|
||||
90
src/Store/DeleteStore.mjs
Normal file
90
src/Store/DeleteStore.mjs
Normal file
@@ -0,0 +1,90 @@
|
||||
|
||||
import Tree from '../Util/Tree.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
|
||||
class DSNode {
|
||||
constructor (id, len, gc) {
|
||||
this._id = id
|
||||
this.len = len
|
||||
this.gc = gc
|
||||
}
|
||||
clone () {
|
||||
return new DSNode(this._id, this.len, this.gc)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DeleteStore extends Tree {
|
||||
logTable () {
|
||||
const deletes = []
|
||||
this.iterate(null, null, function (n) {
|
||||
deletes.push({
|
||||
user: n._id.user,
|
||||
clock: n._id.clock,
|
||||
len: n.len,
|
||||
gc: n.gc
|
||||
})
|
||||
})
|
||||
console.table(deletes)
|
||||
}
|
||||
isDeleted (id) {
|
||||
var n = this.findWithUpperBound(id)
|
||||
return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
|
||||
}
|
||||
mark (id, length, gc) {
|
||||
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) {
|
||||
this.mark(id, length, false)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import Tree from '../Util/Tree.js'
|
||||
import RootID from '../Util/RootID.js'
|
||||
import { getStruct } from '../Util/structReferences.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import Tree from '../Util/Tree.mjs'
|
||||
import RootID from '../Util/ID/RootID.mjs'
|
||||
import { getStruct } from '../Util/structReferences.mjs'
|
||||
import { logID } from '../MessageHandler/messageToString.mjs'
|
||||
import GC from '../Struct/GC.mjs'
|
||||
|
||||
export default class OperationStore extends Tree {
|
||||
constructor (y) {
|
||||
@@ -11,17 +12,25 @@ export default class OperationStore extends Tree {
|
||||
logTable () {
|
||||
const items = []
|
||||
this.iterate(null, null, function (item) {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||
left: logID(item._left === null ? null : item._left._lastId),
|
||||
right: logID(item._right),
|
||||
right_origin: logID(item._right_origin),
|
||||
parent: logID(item._parent),
|
||||
parentSub: item._parentSub,
|
||||
deleted: item._deleted,
|
||||
content: JSON.stringify(item._content)
|
||||
})
|
||||
if (item.constructor === GC) {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
content: item._length,
|
||||
deleted: 'GC'
|
||||
})
|
||||
} else {
|
||||
items.push({
|
||||
id: logID(item),
|
||||
origin: logID(item._origin === null ? null : item._origin._lastId),
|
||||
left: logID(item._left === null ? null : item._left._lastId),
|
||||
right: logID(item._right),
|
||||
right_origin: logID(item._right_origin),
|
||||
parent: logID(item._parent),
|
||||
parentSub: item._parentSub,
|
||||
deleted: item._deleted,
|
||||
content: JSON.stringify(item._content)
|
||||
})
|
||||
}
|
||||
})
|
||||
console.table(items)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import ID from '../Util/ID.js'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
|
||||
export default class StateStore {
|
||||
constructor (y) {
|
||||
@@ -1,84 +0,0 @@
|
||||
import { getReference } from '../Util/structReferences.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
|
||||
/**
|
||||
* Delete all items in an ID-range
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||
*/
|
||||
export function deleteItemRange (y, user, clock, range) {
|
||||
const createDelete = y.connector._forwardAppliedStructs
|
||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||
if (item !== null) {
|
||||
if (!item._deleted) {
|
||||
item._splitAt(y, range)
|
||||
item._delete(y, createDelete)
|
||||
}
|
||||
let itemLen = item._length
|
||||
range -= itemLen
|
||||
clock += itemLen
|
||||
if (range > 0) {
|
||||
let node = y.os.findNode(new ID(user, clock))
|
||||
while (node !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||
const nodeVal = node.val
|
||||
if (!nodeVal._deleted) {
|
||||
nodeVal._splitAt(y, range)
|
||||
nodeVal._delete(y, createDelete)
|
||||
}
|
||||
const nodeLen = nodeVal._length
|
||||
range -= nodeLen
|
||||
clock += nodeLen
|
||||
node = node.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete is not a real struct. It will not be saved in OS
|
||||
*/
|
||||
export default class Delete {
|
||||
constructor () {
|
||||
this._target = null
|
||||
this._length = null
|
||||
}
|
||||
_fromBinary (y, decoder) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
const targetID = decoder.readID()
|
||||
this._targetID = targetID
|
||||
this._length = decoder.readVarUint()
|
||||
if (y.os.getItem(targetID) === null) {
|
||||
return [targetID]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
_toBinary (encoder) {
|
||||
encoder.writeUint8(getReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
encoder.writeVarUint(this._length)
|
||||
}
|
||||
/**
|
||||
* - If created remotely (a remote user deleted something),
|
||||
* this Delete is applied to all structs in id-range.
|
||||
* - If created lokally (e.g. when y-array deletes a range of elements),
|
||||
* this struct is broadcasted only (it is already executed)
|
||||
*/
|
||||
_integrate (y, locallyCreated = false) {
|
||||
if (!locallyCreated) {
|
||||
// from remote
|
||||
const id = this._targetID
|
||||
deleteItemRange(y, id.user, id.clock, this._length)
|
||||
} else {
|
||||
// from local
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveOperations(this)
|
||||
}
|
||||
}
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
124
src/Struct/Delete.mjs
Normal file
124
src/Struct/Delete.mjs
Normal file
@@ -0,0 +1,124 @@
|
||||
import { getStructReference } from '../Util/structReferences.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { logID } from '../MessageHandler/messageToString.mjs'
|
||||
import { writeStructToTransaction } from '../Transaction.mjs'
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Delete all items in an ID-range
|
||||
* TODO: implement getItemCleanStartNode for better performance (only one lookup)
|
||||
*/
|
||||
export function deleteItemRange (y, user, clock, range, gcChildren) {
|
||||
const createDelete = y.connector !== null && y.connector._forwardAppliedStructs
|
||||
let item = y.os.getItemCleanStart(new ID(user, clock))
|
||||
if (item !== null) {
|
||||
if (!item._deleted) {
|
||||
item._splitAt(y, range)
|
||||
item._delete(y, createDelete, true)
|
||||
}
|
||||
let itemLen = item._length
|
||||
range -= itemLen
|
||||
clock += itemLen
|
||||
if (range > 0) {
|
||||
let node = y.os.findNode(new ID(user, clock))
|
||||
while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
|
||||
const nodeVal = node.val
|
||||
if (!nodeVal._deleted) {
|
||||
nodeVal._splitAt(y, range)
|
||||
nodeVal._delete(y, createDelete, gcChildren)
|
||||
}
|
||||
const nodeLen = nodeVal._length
|
||||
range -= nodeLen
|
||||
clock += nodeLen
|
||||
node = node.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
constructor () {
|
||||
this._target = 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) {
|
||||
// TODO: set target, and add it to missing if not found
|
||||
// There is an edge case in p2p networks!
|
||||
const targetID = decoder.readID()
|
||||
this._targetID = targetID
|
||||
this._length = decoder.readVarUint()
|
||||
if (y.os.getItem(targetID) === null) {
|
||||
return [targetID]
|
||||
} else {
|
||||
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) {
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
encoder.writeID(this._targetID)
|
||||
encoder.writeVarUint(this._length)
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* * If created lokally (e.g. when y-array deletes a range of elements),
|
||||
* this struct is broadcasted only (it is already executed)
|
||||
*/
|
||||
_integrate (y, locallyCreated = false) {
|
||||
if (!locallyCreated) {
|
||||
// from remote
|
||||
const id = this._targetID
|
||||
deleteItemRange(y, id.user, id.clock, this._length, false)
|
||||
} else if (y.connector !== null) {
|
||||
// from local
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
writeStructToTransaction(y._transaction, this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
* Useful for logging as all Items and Delete implement this method.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
|
||||
}
|
||||
}
|
||||
96
src/Struct/GC.mjs
Normal file
96
src/Struct/GC.mjs
Normal file
@@ -0,0 +1,96 @@
|
||||
import { getStructReference } from '../Util/structReferences.mjs'
|
||||
import { RootFakeUserID } from '../Util/ID/RootID.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { writeStructToTransaction } from '../Transaction.mjs'
|
||||
|
||||
// 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)
|
||||
}
|
||||
writeStructToTransaction(y._transaction, 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 ID from '../Util/ID.js'
|
||||
import { RootFakeUserID } from '../Util/RootID.js'
|
||||
import Delete from './Delete.js'
|
||||
import { transactionTypeChanged } from '../Transaction.js'
|
||||
import { getStructReference } from '../Util/structReferences.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.mjs'
|
||||
import Delete from './Delete.mjs'
|
||||
import { transactionTypeChanged, writeStructToTransaction } from '../Transaction.mjs'
|
||||
import GC from './GC.mjs'
|
||||
|
||||
/**
|
||||
* Helper utility to split an Item (see _splitAt)
|
||||
* - copy all properties from a to b
|
||||
* - connect a to b
|
||||
* @private
|
||||
* Helper utility to split an Item (see {@link Item#_splitAt})
|
||||
* - copies all properties from a to b
|
||||
* - connects a to b
|
||||
* - assigns the correct _id
|
||||
* - save b to os
|
||||
* - saves b to os
|
||||
*/
|
||||
export function splitHelper (y, a, b, diff) {
|
||||
const aID = a._id
|
||||
@@ -39,46 +41,166 @@ export function splitHelper (y, a, b, diff) {
|
||||
o = o._right
|
||||
}
|
||||
y.os.put(b)
|
||||
if (y._transaction.newTypes.has(a)) {
|
||||
y._transaction.newTypes.add(b)
|
||||
} else if (y._transaction.deletedStructs.has(a)) {
|
||||
y._transaction.deletedStructs.add(b)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class that represents any content.
|
||||
*/
|
||||
export default class Item {
|
||||
constructor () {
|
||||
/**
|
||||
* The uniqe identifier of this type.
|
||||
* @type {ID}
|
||||
*/
|
||||
this._id = null
|
||||
/**
|
||||
* The item that was originally to the left of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._origin = null
|
||||
/**
|
||||
* The item that is currently to the left of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._left = null
|
||||
/**
|
||||
* The item that is currently to the right of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._right = null
|
||||
/**
|
||||
* The item that was originally to the right of this item.
|
||||
* @type {Item}
|
||||
*/
|
||||
this._right_origin = null
|
||||
/**
|
||||
* The parent type.
|
||||
* @type {Y|YType}
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* Whether this item was deleted or not.
|
||||
* @type {Boolean}
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the effect of struct
|
||||
* Creates an Item with the same effect as this Item (without position effect)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_copy () {
|
||||
let struct = new this.constructor()
|
||||
struct._origin = this._left
|
||||
struct._left = this._left
|
||||
struct._right = this
|
||||
struct._right_origin = this
|
||||
struct._parent = this._parent
|
||||
return new this.constructor()
|
||||
}
|
||||
|
||||
/**
|
||||
* Redoes the effect of this operation.
|
||||
*
|
||||
* @param {Y} y The Yjs instance.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_redo (y) {
|
||||
if (this._redone !== null) {
|
||||
return this._redone
|
||||
}
|
||||
let struct = this._copy()
|
||||
let left = this._left
|
||||
let right = this
|
||||
let parent = this._parent
|
||||
// make sure that parent is redone
|
||||
if (parent._deleted === true && parent._redone === null) {
|
||||
parent._redo(y)
|
||||
}
|
||||
if (parent._redone !== null) {
|
||||
parent = parent._redone
|
||||
// find next cloned items
|
||||
while (left !== null) {
|
||||
if (left._redone !== null && left._redone._parent === parent) {
|
||||
left = left._redone
|
||||
break
|
||||
}
|
||||
left = left._left
|
||||
}
|
||||
while (right !== null) {
|
||||
if (right._redone !== null && right._redone._parent === parent) {
|
||||
right = right._redone
|
||||
}
|
||||
right = right._right
|
||||
}
|
||||
}
|
||||
struct._origin = left
|
||||
struct._left = left
|
||||
struct._right = right
|
||||
struct._right_origin = right
|
||||
struct._parent = parent
|
||||
struct._parentSub = this._parentSub
|
||||
struct._integrate(y)
|
||||
this._redone = struct
|
||||
return struct
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the last content address of this Item.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
get _lastId () {
|
||||
return new ID(this._id.user, this._id.clock + this._length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the length of this Item.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
get _length () {
|
||||
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
|
||||
* Returns right part after split
|
||||
* - diff === 0 => this
|
||||
* - diff === length => this._right
|
||||
* - otherwise => split _content and return right part of split
|
||||
* (see ItemJSON/ItemString for implementation)
|
||||
* * diff === 0 => this
|
||||
* * diff === length => this._right
|
||||
* * otherwise => split _content and return right part of split
|
||||
* (see {@link ItemJSON}/{@link ItemString} for implementation)
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
@@ -86,34 +208,74 @@ export default class Item {
|
||||
}
|
||||
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) {
|
||||
this._deleted = true
|
||||
y.ds.markDeleted(this._id, this._length)
|
||||
if (createDelete) {
|
||||
if (!this._deleted) {
|
||||
this._deleted = true
|
||||
y.ds.mark(this._id, this._length, false)
|
||||
let del = new Delete()
|
||||
del._targetID = this._id
|
||||
del._length = this._length
|
||||
del._integrate(y, true)
|
||||
if (createDelete) {
|
||||
// broadcast and persists Delete
|
||||
del._integrate(y, true)
|
||||
} else if (y.persistence !== null) {
|
||||
// only persist Delete
|
||||
y.persistence.saveStruct(y, del)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
y._transaction.deletedStructs.add(this)
|
||||
}
|
||||
transactionTypeChanged(y, this._parent, this._parentSub)
|
||||
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
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_beforeChange () {
|
||||
// nop
|
||||
}
|
||||
/*
|
||||
* - Integrate the struct so that other types/structs can see it
|
||||
* - Add this struct to y.os
|
||||
* - Check if this is struct deleted
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* * 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) {
|
||||
y._transaction.newTypes.add(this)
|
||||
const parent = this._parent
|
||||
const selfID = this._id
|
||||
const userState = selfID === null ? 0 : y.ss.getState(selfID.user)
|
||||
const user = selfID === null ? y.userID : selfID.user
|
||||
const userState = y.ss.getState(user)
|
||||
if (selfID === null) {
|
||||
this._id = y.ss.getNextID(this._length)
|
||||
} else if (selfID.user === RootFakeUserID) {
|
||||
@@ -132,6 +294,7 @@ export default class Item {
|
||||
// or this types is new
|
||||
this._parent._beforeChange()
|
||||
}
|
||||
|
||||
/*
|
||||
# $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
|
||||
@@ -216,16 +379,28 @@ export default class Item {
|
||||
y.os.put(this)
|
||||
transactionTypeChanged(y, parent, parentSub)
|
||||
if (this._id.user !== RootFakeUserID) {
|
||||
if (y.connector._forwardAppliedStructs || this._id.user === y.userID) {
|
||||
if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) {
|
||||
y.connector.broadcastStruct(this)
|
||||
}
|
||||
if (y.persistence !== null) {
|
||||
y.persistence.saveOperations(this)
|
||||
y.persistence.saveStruct(y, this)
|
||||
}
|
||||
writeStructToTransaction(y._transaction, 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(getReference(this.constructor))
|
||||
encoder.writeUint8(getStructReference(this.constructor))
|
||||
let info = 0
|
||||
if (this._origin !== null) {
|
||||
info += 0b1 // origin is defined
|
||||
@@ -264,6 +439,17 @@ export default class Item {
|
||||
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) {
|
||||
let missing = []
|
||||
const info = decoder.readUint8()
|
||||
@@ -301,7 +487,12 @@ export default class Item {
|
||||
const parentID = decoder.readID()
|
||||
// parent does not change, so we don't have to search for it again
|
||||
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) {
|
||||
missing.push(parentID)
|
||||
} else {
|
||||
@@ -310,9 +501,19 @@ export default class Item {
|
||||
}
|
||||
} else if (this._parent === null) {
|
||||
if (this._origin !== null) {
|
||||
this._parent = this._origin._parent
|
||||
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
|
||||
}
|
||||
} else if (this._right_origin !== null) {
|
||||
this._parent = this._right_origin._parent
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
if (info & 0b1000) {
|
||||
35
src/Struct/ItemEmbed.mjs
Normal file
35
src/Struct/ItemEmbed.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import Item from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
|
||||
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.mjs
Normal file
42
src/Struct/ItemFormat.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
import Item from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
|
||||
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 { logID } from '../MessageHandler/messageToString.js'
|
||||
import Item, { splitHelper } from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
|
||||
export default class ItemJSON extends Item {
|
||||
constructor () {
|
||||
@@ -45,10 +45,14 @@ export default class ItemJSON extends Item {
|
||||
encoder.writeVarString(encoded)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
* Useful for logging as all Items and Delete implement this method.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
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})`
|
||||
return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { splitHelper, default as Item } from './Item.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import Item, { splitHelper } from './Item.mjs'
|
||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
|
||||
|
||||
export default class ItemString extends Item {
|
||||
constructor () {
|
||||
@@ -23,10 +23,14 @@ export default class ItemString extends Item {
|
||||
super._toBinary(encoder)
|
||||
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 () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
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})`
|
||||
return logItemHelper('ItemString', this, `content:"${this._content}"`)
|
||||
}
|
||||
_splitAt (y, diff) {
|
||||
if (diff === 0) {
|
||||
@@ -1,6 +1,6 @@
|
||||
import Item from './Item.js'
|
||||
import EventHandler from '../Util/EventHandler.js'
|
||||
import ID from '../Util/ID.js'
|
||||
import Item from './Item.mjs'
|
||||
import EventHandler from '../Util/EventHandler.mjs'
|
||||
import ID from '../Util/ID/ID.mjs'
|
||||
|
||||
// restructure children as if they were inserted one after another
|
||||
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 {
|
||||
constructor () {
|
||||
super()
|
||||
@@ -39,32 +50,52 @@ export default class Type extends Item {
|
||||
this._eventHandler = 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) {
|
||||
if (type === this) {
|
||||
return []
|
||||
}
|
||||
const path = []
|
||||
const y = this._y
|
||||
while (type._parent !== this && this._parent !== y) {
|
||||
while (type !== this && type !== y) {
|
||||
let parent = type._parent
|
||||
if (type._parentSub !== null) {
|
||||
path.push(type._parentSub)
|
||||
path.unshift(type._parentSub)
|
||||
} else {
|
||||
// parent is array-ish
|
||||
for (let [i, child] of parent) {
|
||||
if (child === type) {
|
||||
path.push(i)
|
||||
path.unshift(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
type = parent
|
||||
}
|
||||
if (this._parent !== this) {
|
||||
if (type !== this) {
|
||||
throw new Error('The type is not a child of this node')
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Call event listeners with an event. This will also add an event to all
|
||||
* parents (for `.observeDeep` handlers).
|
||||
*/
|
||||
_callEventHandler (transaction, event) {
|
||||
const changedParentTypes = transaction.changedParentTypes
|
||||
this._eventHandler.callEventListeners(transaction, event)
|
||||
@@ -79,39 +110,14 @@ export default class Type extends Item {
|
||||
type = type._parent
|
||||
}
|
||||
}
|
||||
_copy (undeleteChildren) {
|
||||
let copy = super._copy()
|
||||
let map = new Map()
|
||||
copy._map = map
|
||||
for (let [key, value] of this._map) {
|
||||
if (undeleteChildren.has(value) || !value.deleted) {
|
||||
let _item = value._copy(undeleteChildren)
|
||||
_item._parent = copy
|
||||
map.set(key, value._copy(undeleteChildren))
|
||||
}
|
||||
}
|
||||
let prevUndeleted = null
|
||||
copy._start = null
|
||||
let item = this._start
|
||||
while (item !== null) {
|
||||
if (undeleteChildren.has(item) || !item.deleted) {
|
||||
let _item = item._copy(undeleteChildren)
|
||||
_item._left = prevUndeleted
|
||||
_item._origin = prevUndeleted
|
||||
_item._right = null
|
||||
_item._right_origin = null
|
||||
_item._parent = copy
|
||||
if (prevUndeleted === null) {
|
||||
copy._start = _item
|
||||
} else {
|
||||
prevUndeleted._right = _item
|
||||
}
|
||||
prevUndeleted = _item
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
/**
|
||||
* @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) {
|
||||
const y = this._y
|
||||
if (y !== null) {
|
||||
@@ -120,20 +126,54 @@ export default class Type extends Item {
|
||||
f(y)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe all events that are created on this type.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
observe (f) {
|
||||
this._eventHandler.addEventListener(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe all events that are created by this type and its children.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
observeDeep (f) {
|
||||
this._deepEventHandler.addEventListener(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an observer function.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
unobserve (f) {
|
||||
this._eventHandler.removeEventListener(f)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister an observer function.
|
||||
*
|
||||
* @param {Function} f Observer function
|
||||
*/
|
||||
unobserveDeep (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) {
|
||||
y._transaction.newTypes.add(this)
|
||||
super._integrate(y)
|
||||
this._y = y
|
||||
// when integrating children we must make sure to
|
||||
@@ -151,22 +191,53 @@ export default class Type extends Item {
|
||||
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 || !y.gcEnabled) {
|
||||
gcChildren = y._hasUndoManager === false && y.gcEnabled
|
||||
}
|
||||
super._delete(y, createDelete, gcChildren)
|
||||
y._transaction.changedTypes.delete(this)
|
||||
// delete map types
|
||||
for (let value of this._map.values()) {
|
||||
if (value instanceof Item && !value._deleted) {
|
||||
value._delete(y, false)
|
||||
value._delete(y, false, gcChildren)
|
||||
}
|
||||
}
|
||||
// delete array types
|
||||
let t = this._start
|
||||
while (t !== null) {
|
||||
if (!t._deleted) {
|
||||
t._delete(y, false)
|
||||
t._delete(y, false, gcChildren)
|
||||
}
|
||||
t = t._right
|
||||
}
|
||||
if (gcChildren) {
|
||||
this._gcChildren(y)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
|
||||
export default class Transaction {
|
||||
constructor (y) {
|
||||
this.y = y
|
||||
// types added during transaction
|
||||
this.newTypes = new Set()
|
||||
// changed types (does not include new types)
|
||||
// maps from type to parentSubs (item._parentSub = null for array elements)
|
||||
this.changedTypes = new Map()
|
||||
this.deletedStructs = new Set()
|
||||
this.beforeState = new Map()
|
||||
this.changedParentTypes = new Map()
|
||||
}
|
||||
}
|
||||
|
||||
export function transactionTypeChanged (y, type, sub) {
|
||||
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
|
||||
const changedTypes = y._transaction.changedTypes
|
||||
let subs = changedTypes.get(type)
|
||||
if (subs === undefined) {
|
||||
// create if it doesn't exist yet
|
||||
subs = new Set()
|
||||
changedTypes.set(type, subs)
|
||||
}
|
||||
subs.add(sub)
|
||||
}
|
||||
}
|
||||
91
src/Transaction.mjs
Normal file
91
src/Transaction.mjs
Normal file
@@ -0,0 +1,91 @@
|
||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
constructor (y) {
|
||||
/**
|
||||
* @type {Y} The Yjs instance.
|
||||
*/
|
||||
this.y = y
|
||||
/**
|
||||
* All new types that are added during a transaction.
|
||||
* @type {Set<Item>}
|
||||
*/
|
||||
this.newTypes = new Set()
|
||||
/**
|
||||
* 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()
|
||||
// TODO: rename deletedTypes
|
||||
/**
|
||||
* Set of all deleted Types and Structs.
|
||||
* @type {Set<Item>}
|
||||
*/
|
||||
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()
|
||||
/**
|
||||
* 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.encodedStructsLen = 0
|
||||
this._encodedStructs = new BinaryEncoder()
|
||||
this._encodedStructs.writeUint32(0)
|
||||
}
|
||||
get encodedStructs () {
|
||||
this._encodedStructs.setUint32(0, this.encodedStructsLen)
|
||||
return this._encodedStructs
|
||||
}
|
||||
}
|
||||
|
||||
export function writeStructToTransaction (transaction, struct) {
|
||||
transaction.encodedStructsLen++
|
||||
struct._toBinary(transaction._encodedStructs)
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export function transactionTypeChanged (y, type, sub) {
|
||||
if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
|
||||
const changedTypes = y._transaction.changedTypes
|
||||
let subs = changedTypes.get(type)
|
||||
if (subs === undefined) {
|
||||
// create if it doesn't exist yet
|
||||
subs = new Set()
|
||||
changedTypes.set(type, subs)
|
||||
}
|
||||
subs.add(sub)
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import Type from '../Struct/Type.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import ItemString from '../Struct/ItemString.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import YEvent from '../Util/YEvent.js'
|
||||
|
||||
class YArrayEvent extends YEvent {
|
||||
constructor (yarray, remote) {
|
||||
super(yarray)
|
||||
this.remote = remote
|
||||
}
|
||||
}
|
||||
|
||||
export default class YArray extends Type {
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YArrayEvent(this, remote))
|
||||
}
|
||||
get (pos) {
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
if (pos < n._length) {
|
||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
||||
return n._content[pos]
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
}
|
||||
pos -= n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
toArray () {
|
||||
return this.map(c => c)
|
||||
}
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
if (c.toJSON !== null) {
|
||||
return c.toJSON()
|
||||
} else {
|
||||
return c.toString()
|
||||
}
|
||||
}
|
||||
return c
|
||||
})
|
||||
}
|
||||
map (f) {
|
||||
const res = []
|
||||
this.forEach((c, i) => {
|
||||
res.push(f(c, i, this))
|
||||
})
|
||||
return res
|
||||
}
|
||||
forEach (f) {
|
||||
let pos = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
if (n instanceof Type) {
|
||||
f(n, pos++, this)
|
||||
} else {
|
||||
const content = n._content
|
||||
const contentLen = content.length
|
||||
for (let i = 0; i < contentLen; i++) {
|
||||
pos++
|
||||
f(content[i], pos, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
get length () {
|
||||
let length = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
length += n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
return length
|
||||
}
|
||||
[Symbol.iterator] () {
|
||||
return {
|
||||
next: function () {
|
||||
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
||||
// item is deleted or itemElement does not exist (is deleted)
|
||||
this._item = this._item._right
|
||||
this._itemElement = 0
|
||||
}
|
||||
if (this._item === null) {
|
||||
return {
|
||||
done: true
|
||||
}
|
||||
}
|
||||
let content
|
||||
if (this._item instanceof Type) {
|
||||
content = this._item
|
||||
} else {
|
||||
content = this._item._content[this._itemElement++]
|
||||
}
|
||||
return {
|
||||
value: [this._count, content],
|
||||
done: false
|
||||
}
|
||||
},
|
||||
_item: this._start,
|
||||
_itemElement: 0,
|
||||
_count: 0
|
||||
}
|
||||
}
|
||||
delete (pos, length = 1) {
|
||||
this._y.transact(() => {
|
||||
let item = this._start
|
||||
let count = 0
|
||||
while (item !== null && length > 0) {
|
||||
if (!item._deleted) {
|
||||
if (count <= pos && pos < count + item._length) {
|
||||
const diffDel = pos - count
|
||||
item = item._splitAt(this._y, diffDel)
|
||||
item._splitAt(this._y, length)
|
||||
length -= item._length
|
||||
item._delete(this._y)
|
||||
count += diffDel
|
||||
} else {
|
||||
count += item._length
|
||||
}
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
})
|
||||
if (length > 0) {
|
||||
throw new Error('Delete exceeds the range of the YArray')
|
||||
}
|
||||
}
|
||||
insertAfter (left, content) {
|
||||
this._transact(y => {
|
||||
let right
|
||||
if (left === null) {
|
||||
right = this._start
|
||||
} else {
|
||||
right = left._right
|
||||
}
|
||||
let prevJsonIns = null
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let c = content[i]
|
||||
if (typeof c === 'function') {
|
||||
c = new c() // eslint-disable-line new-cap
|
||||
}
|
||||
if (c instanceof Type) {
|
||||
if (prevJsonIns !== null) {
|
||||
if (y !== null) {
|
||||
prevJsonIns._integrate(y)
|
||||
}
|
||||
left = prevJsonIns
|
||||
prevJsonIns = null
|
||||
}
|
||||
c._origin = left
|
||||
c._left = left
|
||||
c._right = right
|
||||
c._right_origin = right
|
||||
c._parent = this
|
||||
if (y !== null) {
|
||||
c._integrate(y)
|
||||
} else if (left === null) {
|
||||
this._start = c
|
||||
} else {
|
||||
left._right = c
|
||||
}
|
||||
left = c
|
||||
} else {
|
||||
if (prevJsonIns === null) {
|
||||
prevJsonIns = new ItemJSON()
|
||||
prevJsonIns._origin = left
|
||||
prevJsonIns._left = left
|
||||
prevJsonIns._right = right
|
||||
prevJsonIns._right_origin = right
|
||||
prevJsonIns._parent = this
|
||||
prevJsonIns._content = []
|
||||
}
|
||||
prevJsonIns._content.push(c)
|
||||
}
|
||||
}
|
||||
if (prevJsonIns !== null && y !== null) {
|
||||
prevJsonIns._integrate(y)
|
||||
}
|
||||
})
|
||||
}
|
||||
insert (pos, content) {
|
||||
let left = null
|
||||
let right = this._start
|
||||
let count = 0
|
||||
const y = this._y
|
||||
while (right !== null) {
|
||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||
if (count <= pos && pos <= count + rightLen) {
|
||||
const splitDiff = pos - count
|
||||
right = right._splitAt(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!')
|
||||
}
|
||||
this.insertAfter(left, content)
|
||||
}
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
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,67 +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) {
|
||||
this._transact(y => {
|
||||
let left = null
|
||||
let right = this._start
|
||||
let count = 0
|
||||
while (right !== null) {
|
||||
if (count <= pos && pos < count + right._length) {
|
||||
const splitDiff = pos - count
|
||||
right = right._splitAt(this._y, pos - count)
|
||||
left = right._left
|
||||
count += splitDiff
|
||||
break
|
||||
}
|
||||
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,129 +0,0 @@
|
||||
// import diff from 'fast-diff'
|
||||
import { defaultDomFilter } from './utils.js'
|
||||
|
||||
import YMap from '../YMap.js'
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
|
||||
export default class YXmlElement extends YXmlFragment {
|
||||
constructor (arg1, arg2, _document) {
|
||||
super()
|
||||
this.nodeName = null
|
||||
this._scrollElement = null
|
||||
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'
|
||||
}
|
||||
if (typeof arg2 === 'function') {
|
||||
this._domFilter = arg2
|
||||
}
|
||||
}
|
||||
_copy (undeleteChildren) {
|
||||
let struct = super._copy(undeleteChildren)
|
||||
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]
|
||||
attributes.set(attr.name, attr.value)
|
||||
}
|
||||
attributes = this._domFilter(dom, 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
|
||||
}
|
||||
}
|
||||
_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 instanceof YXmlFragment) {
|
||||
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,335 +0,0 @@
|
||||
/* global MutationObserver */
|
||||
|
||||
import { defaultDomFilter, applyChangesFromDom, reflectChangesOnDom } from './utils.js'
|
||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
|
||||
|
||||
import YArray from '../YArray.js'
|
||||
import YXmlText from './YXmlText.js'
|
||||
import YXmlEvent from './YXmlEvent.js'
|
||||
import { logID } from '../../MessageHandler/messageToString.js'
|
||||
import diff from 'fast-diff'
|
||||
|
||||
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
|
||||
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._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
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
// TODO: refine this..
|
||||
if ((this.constructor !== YXmlFragment && this._parent !== this._y) || this._parent === null) {
|
||||
// TODO: only top level YXmlFragment can bind. Also allow YXmlElements..
|
||||
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._y.on('beforeTransaction', () => {
|
||||
this._domObserverListener(this._domObserver.takeRecords())
|
||||
})
|
||||
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) {
|
||||
// dom element is filtered
|
||||
return
|
||||
}
|
||||
switch (mutation.type) {
|
||||
case 'characterData':
|
||||
var diffs = diff(yxml.toString(), dom.nodeValue)
|
||||
var pos = 0
|
||||
for (var i = 0; i < diffs.length; i++) {
|
||||
var d = diffs[i]
|
||||
if (d[0] === 0) { // EQUAL
|
||||
pos += d[1].length
|
||||
} else if (d[0] === -1) { // DELETE
|
||||
yxml.delete(pos, d[1].length)
|
||||
} else { // INSERT
|
||||
yxml.insert(pos, d[1])
|
||||
pos += d[1].length
|
||||
}
|
||||
}
|
||||
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._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,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,252 +0,0 @@
|
||||
import YXmlText from './YXmlText.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
|
||||
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 {
|
||||
// update attributes
|
||||
event.attributesChanged.forEach(attributeName => {
|
||||
const value = yxml.getAttribute(attributeName)
|
||||
if (value === undefined) {
|
||||
dom.removeAttribute(attributeName)
|
||||
} else {
|
||||
dom.setAttribute(attributeName, value)
|
||||
}
|
||||
})
|
||||
if (event.childListChanged) {
|
||||
// create fragment of undeleted nodes
|
||||
const fragment = _document.createDocumentFragment()
|
||||
yxml.forEach(function (t) {
|
||||
fragment.appendChild(t.getDom(_document))
|
||||
})
|
||||
// remove remainding nodes
|
||||
let lastChild = dom.lastChild
|
||||
while (lastChild !== null) {
|
||||
dom.removeChild(lastChild)
|
||||
lastChild = dom.lastChild
|
||||
}
|
||||
// insert fragment of undeleted nodes
|
||||
dom.appendChild(fragment)
|
||||
}
|
||||
}
|
||||
/* 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,9 +0,0 @@
|
||||
|
||||
import YXmlFragment from './YXmlFragment.js'
|
||||
import YXmlElement from './YXmlElement.js'
|
||||
|
||||
export { default as YXmlFragment } from './YXmlFragment.js'
|
||||
export { default as YXmlElement } from './YXmlElement.js'
|
||||
export { default as YXmlText } from './YXmlText.js'
|
||||
|
||||
YXmlFragment._YXmlElement = YXmlElement
|
||||
378
src/Types/YArray/YArray.mjs
Normal file
378
src/Types/YArray/YArray.mjs
Normal file
@@ -0,0 +1,378 @@
|
||||
import Type from '../../Struct/Type.mjs'
|
||||
import ItemJSON from '../../Struct/ItemJSON.mjs'
|
||||
import ItemString from '../../Struct/ItemString.mjs'
|
||||
import { logID, logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import YEvent from '../../Util/YEvent.mjs'
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
super(yarray)
|
||||
this.remote = remote
|
||||
this._transaction = transaction
|
||||
this._addedElements = null
|
||||
this._removedElements = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Child elements that were added in this transaction.
|
||||
*
|
||||
* @return {Set}
|
||||
*/
|
||||
get addedElements () {
|
||||
if (this._addedElements === null) {
|
||||
const target = this.target
|
||||
const transaction = this._transaction
|
||||
const addedElements = new Set()
|
||||
transaction.newTypes.forEach(function (type) {
|
||||
if (type._parent === target && !transaction.deletedStructs.has(type)) {
|
||||
addedElements.add(type)
|
||||
}
|
||||
})
|
||||
this._addedElements = addedElements
|
||||
}
|
||||
return this._addedElements
|
||||
}
|
||||
|
||||
/**
|
||||
* Child elements that were removed in this transaction.
|
||||
*
|
||||
* @return {Set}
|
||||
*/
|
||||
get removedElements () {
|
||||
if (this._removedElements === null) {
|
||||
const target = this.target
|
||||
const transaction = this._transaction
|
||||
const removedElements = new Set()
|
||||
transaction.deletedStructs.forEach(function (struct) {
|
||||
if (struct._parent === target && !transaction.newTypes.has(struct)) {
|
||||
removedElements.add(struct)
|
||||
}
|
||||
})
|
||||
this._removedElements = removedElements
|
||||
}
|
||||
return this._removedElements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared Array implementation.
|
||||
*/
|
||||
export default class YArray extends Type {
|
||||
/**
|
||||
* @private
|
||||
* Creates YArray Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
if (index < n._length) {
|
||||
if (n.constructor === ItemJSON || n.constructor === ItemString) {
|
||||
return n._content[index]
|
||||
} else {
|
||||
return n
|
||||
}
|
||||
}
|
||||
index -= n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
toArray () {
|
||||
return this.map(c => c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
toJSON () {
|
||||
return this.map(c => {
|
||||
if (c instanceof Type) {
|
||||
if (c.toJSON !== null) {
|
||||
return c.toJSON()
|
||||
} else {
|
||||
return c.toString()
|
||||
}
|
||||
}
|
||||
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) {
|
||||
const res = []
|
||||
this.forEach((c, i) => {
|
||||
res.push(f(c, i, this))
|
||||
})
|
||||
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) {
|
||||
let index = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
if (n instanceof Type) {
|
||||
f(n, index++, this)
|
||||
} else {
|
||||
const content = n._content
|
||||
const contentLen = content.length
|
||||
for (let i = 0; i < contentLen; i++) {
|
||||
index++
|
||||
f(content[i], index, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the length of this YArray.
|
||||
*/
|
||||
get length () {
|
||||
let length = 0
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
if (!n._deleted && n._countable) {
|
||||
length += n._length
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
return length
|
||||
}
|
||||
|
||||
[Symbol.iterator] () {
|
||||
return {
|
||||
next: function () {
|
||||
while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
|
||||
// item is deleted or itemElement does not exist (is deleted)
|
||||
this._item = this._item._right
|
||||
this._itemElement = 0
|
||||
}
|
||||
if (this._item === null) {
|
||||
return {
|
||||
done: true
|
||||
}
|
||||
}
|
||||
let content
|
||||
if (this._item instanceof Type) {
|
||||
content = this._item
|
||||
} else {
|
||||
content = this._item._content[this._itemElement++]
|
||||
}
|
||||
return {
|
||||
value: content,
|
||||
done: false
|
||||
}
|
||||
},
|
||||
_item: this._start,
|
||||
_itemElement: 0,
|
||||
_count: 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(() => {
|
||||
let item = this._start
|
||||
let count = 0
|
||||
while (item !== null && length > 0) {
|
||||
if (!item._deleted && item._countable) {
|
||||
if (count <= index && index < count + item._length) {
|
||||
const diffDel = index - count
|
||||
item = item._splitAt(this._y, diffDel)
|
||||
item._splitAt(this._y, length)
|
||||
length -= item._length
|
||||
item._delete(this._y)
|
||||
count += diffDel
|
||||
} else {
|
||||
count += item._length
|
||||
}
|
||||
}
|
||||
item = item._right
|
||||
}
|
||||
})
|
||||
if (length > 0) {
|
||||
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) {
|
||||
this._transact(y => {
|
||||
let right
|
||||
if (left === null) {
|
||||
right = this._start
|
||||
} else {
|
||||
right = left._right
|
||||
}
|
||||
let prevJsonIns = null
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let c = content[i]
|
||||
if (typeof c === 'function') {
|
||||
c = new c() // eslint-disable-line new-cap
|
||||
}
|
||||
if (c instanceof Type) {
|
||||
if (prevJsonIns !== null) {
|
||||
if (y !== null) {
|
||||
prevJsonIns._integrate(y)
|
||||
}
|
||||
left = prevJsonIns
|
||||
prevJsonIns = null
|
||||
}
|
||||
c._origin = left
|
||||
c._left = left
|
||||
c._right = right
|
||||
c._right_origin = right
|
||||
c._parent = this
|
||||
if (y !== null) {
|
||||
c._integrate(y)
|
||||
} else if (left === null) {
|
||||
this._start = c
|
||||
} else {
|
||||
left._right = c
|
||||
}
|
||||
left = c
|
||||
} else {
|
||||
if (prevJsonIns === null) {
|
||||
prevJsonIns = new ItemJSON()
|
||||
prevJsonIns._origin = left
|
||||
prevJsonIns._left = left
|
||||
prevJsonIns._right = right
|
||||
prevJsonIns._right_origin = right
|
||||
prevJsonIns._parent = this
|
||||
prevJsonIns._content = []
|
||||
}
|
||||
prevJsonIns._content.push(c)
|
||||
}
|
||||
}
|
||||
if (prevJsonIns !== null) {
|
||||
if (y !== null) {
|
||||
prevJsonIns._integrate(y)
|
||||
} else if (prevJsonIns._left === null) {
|
||||
this._start = prevJsonIns
|
||||
}
|
||||
}
|
||||
})
|
||||
return 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 right = this._start
|
||||
let count = 0
|
||||
const y = this._y
|
||||
while (right !== null) {
|
||||
const rightLen = right._deleted ? 0 : (right._length - 1)
|
||||
if (count <= index && index <= count + rightLen) {
|
||||
const splitDiff = index - count
|
||||
right = right._splitAt(y, splitDiff)
|
||||
left = right._left
|
||||
count += splitDiff
|
||||
break
|
||||
}
|
||||
if (!right._deleted) {
|
||||
count += right._length
|
||||
}
|
||||
left = right
|
||||
right = right._right
|
||||
}
|
||||
if (index > count) {
|
||||
throw new Error('Index exceeds array range!')
|
||||
}
|
||||
this.insertAfter(left, content)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends content to this YArray.
|
||||
*
|
||||
* @param {Array} content Array of content to append.
|
||||
*/
|
||||
push (content) {
|
||||
let n = this._start
|
||||
let lastUndeleted = null
|
||||
while (n !== null) {
|
||||
if (!n._deleted) {
|
||||
lastUndeleted = n
|
||||
}
|
||||
n = n._right
|
||||
}
|
||||
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 () {
|
||||
return logItemHelper('YArray', this, `start:${logID(this._start)}"`)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import Type from '../Struct/Type.js'
|
||||
import Item from '../Struct/Item.js'
|
||||
import ItemJSON from '../Struct/ItemJSON.js'
|
||||
import { logID } from '../MessageHandler/messageToString.js'
|
||||
import YEvent from '../Util/YEvent.js'
|
||||
import Item from '../../Struct/Item.mjs'
|
||||
import Type from '../../Struct/Type.mjs'
|
||||
import ItemJSON from '../../Struct/ItemJSON.mjs'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import YEvent from '../../Util/YEvent.mjs'
|
||||
|
||||
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) {
|
||||
super(ymap)
|
||||
this.keysChanged = subs
|
||||
@@ -12,10 +19,23 @@ class YMapEvent extends YEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared Map implementation.
|
||||
*/
|
||||
export default class YMap extends Type {
|
||||
/**
|
||||
* @private
|
||||
* Creates YMap Event and calls observers.
|
||||
*/
|
||||
_callObserver (transaction, parentSubs, remote) {
|
||||
this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Object}
|
||||
*/
|
||||
toJSON () {
|
||||
const map = {}
|
||||
for (let [key, item] of this._map) {
|
||||
@@ -35,7 +55,14 @@ export default class YMap extends Type {
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys for each element in the YMap Type.
|
||||
*
|
||||
* @return {Array}
|
||||
*/
|
||||
keys () {
|
||||
// TODO: Should return either Iterator or Set!
|
||||
let keys = []
|
||||
for (let [key, value] of this._map) {
|
||||
if (!value._deleted) {
|
||||
@@ -44,6 +71,12 @@ export default class YMap extends Type {
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specified element from this YMap.
|
||||
*
|
||||
* @param {encodable} key The key of the element to remove.
|
||||
*/
|
||||
delete (key) {
|
||||
this._transact((y) => {
|
||||
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) {
|
||||
this._transact(y => {
|
||||
const old = this._map.get(key) || null
|
||||
if (old !== null) {
|
||||
if (old instanceof ItemJSON && old._content[0] === value) {
|
||||
if (
|
||||
old.constructor === ItemJSON &&
|
||||
!old._deleted && old._content[0] === value
|
||||
) {
|
||||
// Trying to overwrite with same value
|
||||
// break here
|
||||
return value
|
||||
@@ -87,6 +131,12 @@ export default class YMap extends Type {
|
||||
})
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specified element from this YMap.
|
||||
*
|
||||
* @param {encodable} key The key of the element to return.
|
||||
*/
|
||||
get (key) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
@@ -98,6 +148,12 @@ export default class YMap extends Type {
|
||||
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) {
|
||||
let v = this._map.get(key)
|
||||
if (v === undefined || v._deleted) {
|
||||
@@ -106,9 +162,14 @@ export default class YMap extends Type {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform this YXml Type to a readable format.
|
||||
* Useful for logging as all Items and Delete implement this method.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_logString () {
|
||||
const left = this._left !== null ? this._left._lastId : null
|
||||
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})`
|
||||
return logItemHelper('YMap', this, `mapSize:${this._map.size}`)
|
||||
}
|
||||
}
|
||||
656
src/Types/YText/YText.mjs
Normal file
656
src/Types/YText/YText.mjs
Normal file
@@ -0,0 +1,656 @@
|
||||
import ItemEmbed from '../../Struct/ItemEmbed.mjs'
|
||||
import ItemString from '../../Struct/ItemString.mjs'
|
||||
import ItemFormat from '../../Struct/ItemFormat.mjs'
|
||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
|
||||
import { YArrayEvent, default as YArray } from '../YArray/YArray.mjs'
|
||||
|
||||
/**
|
||||
* @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[key] === undefined) {
|
||||
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:
|
||||
const attr = attributes[right.key]
|
||||
if (attr !== undefined) {
|
||||
if (attr === 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)
|
||||
const attr = attributes[item.key]
|
||||
if (attr !== undefined) {
|
||||
if (attr !== 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.retain !== undefined && lastOp.attributes === undefined) {
|
||||
// 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.insert !== undefined) {
|
||||
;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {})
|
||||
} else if (op.retain !== undefined) {
|
||||
;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {})
|
||||
} else if (op.delete !== undefined) {
|
||||
;[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)
|
||||
}
|
||||
}
|
||||
190
src/Types/YXml/YXmlElement.mjs
Normal file
190
src/Types/YXml/YXmlElement.mjs
Normal file
@@ -0,0 +1,190 @@
|
||||
import YMap from '../YMap/YMap.mjs'
|
||||
import YXmlFragment from './YXmlFragment.mjs'
|
||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
YXmlFragment._YXmlElement = YXmlElement
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user