Compare commits

..

12 Commits

Author SHA1 Message Date
Kevin Jahns
16e01ad3d5 v13.0.0-41 -- distribution files 2017-12-14 14:30:17 +01:00
Kevin Jahns
24c1e4dcc8 13.0.0-41 2017-12-14 14:30:02 +01:00
Kevin Jahns
d61bbecf4e fix tree walker on YXmlFragment 2017-12-14 14:29:16 +01:00
Kevin Jahns
85492ad2e0 fix drawing example. Add drawing hook for y-xml 2017-12-13 12:49:34 +01:00
Kevin Jahns
02253f9a8d fix log outputs 2017-12-13 10:28:19 +01:00
Kevin Jahns
8105bef1af work on drawing demo 2017-12-06 19:20:52 -08:00
Kevin Jahns
4efa16e2dd 13.0.0-40 2017-12-05 21:50:34 -08:00
Kevin Jahns
ad44f59def implement new dom update algorithm 2017-12-05 21:50:00 -08:00
Kevin Jahns
9c471ea24d 13.0.0-39 2017-12-05 17:06:01 -08:00
Kevin Jahns
d9e76014f5 fix remaining cursor relocation issues 2017-12-05 17:05:12 -08:00
Kevin Jahns
4091b7d004 13.0.0-38 2017-12-05 00:53:25 -08:00
Kevin Jahns
dfc183643d support data-yjs-hook attribute for yjs hooks 2017-12-05 00:52:52 -08:00
18 changed files with 1156 additions and 944 deletions

View File

@@ -12,12 +12,8 @@
</style> </style>
<button type="button" id="clearDrawingCanvas">Clear Drawing</button> <button type="button" id="clearDrawingCanvas">Clear Drawing</button>
<svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg> <svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
<script src="../../y.js"></script> <script src="../yjs-dist.js"></script>
<script src="../../../y-array/y-array.js"></script> <script src="../bower_components/d3/d3.min.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="./index.js"></script> <script src="./index.js"></script>
</body> </body>
</html> </html>

View File

@@ -1,84 +1,76 @@
/* globals Y, d3 */ /* globals Y, d3 */
'strict mode'
Y({ let y = new Y({
db: {
name: 'memory'
},
connector: { connector: {
name: 'websockets-client', name: 'websockets-client',
room: 'drawing-example', url: 'http://127.0.0.1:1234',
url: 'localhost:1234' room: 'drawing-example'
}, // maxBufferLength: 100
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
} }
}) })
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
}

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
</head>
<script src="../yjs-dist.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>

View File

@@ -0,0 +1,134 @@
/* global Y, d3 */
window.onload = function () {
window.yXmlType.bindToDom(document.body)
}
window.addMagicDrawing = function addMagicDrawing () {
let mt = document.createElement('magic-drawing')
mt.dataset.yjsHook = '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.dataset.yjsHook = '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
}
}
Y.XmlHook.addHook('magic-drawing', {
fillType: function (dom, type) {
initDrawingBindings(type, dom)
},
createDom: function (type) {
const dom = document.createElement('magic-drawing')
initDrawingBindings(type, dom)
return dom
}
})
// 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.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()
}
}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-37", "version": "13.0.0-41",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-37", "version": "13.0.0-41",
"description": "A framework for real-time p2p shared editing on any data", "description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js", "main": "./y.node.js",
"browser": "./y.js", "browser": "./y.js",

View File

@@ -3,7 +3,7 @@ import commonjs from 'rollup-plugin-commonjs'
import multiEntry from 'rollup-plugin-multi-entry' import multiEntry from 'rollup-plugin-multi-entry'
export default { export default {
input: 'test/y-xml.tests.js', input: 'test/*.tests.js',
name: 'y-tests', name: 'y-tests',
sourcemap: true, sourcemap: true,
output: { output: {

View File

@@ -5,15 +5,38 @@ import { logID } from '../MessageHandler/messageToString.js'
import YEvent from '../Util/YEvent.js' import YEvent from '../Util/YEvent.js'
class YArrayEvent extends YEvent { class YArrayEvent extends YEvent {
constructor (yarray, remote) { constructor (yarray, remote, transaction) {
super(yarray) super(yarray)
this.remote = remote this.remote = remote
this._transaction = transaction
}
get addedElements () {
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)
}
})
return addedElements
}
get removedElements () {
const target = this.target
const transaction = this._transaction
const removedElements = new Set()
transaction.deletedStructs.forEach(function (struct) {
if (struct._parent === target && !transaction.newTypes.has(struct)) {
removedElements.add(struct)
}
})
return removedElements
} }
} }
export default class YArray extends Type { export default class YArray extends Type {
_callObserver (transaction, parentSubs, remote) { _callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YArrayEvent(this, remote)) this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
} }
get (pos) { get (pos) {
let n = this._start let n = this._start
@@ -214,6 +237,17 @@ export default class YArray extends Type {
} }
this.insertAfter(left, content) this.insertAfter(left, content)
} }
push (content) {
let n = this._start
let lastUndeleted = null
while (n !== null) {
if (!n._deleted) {
lastUndeleted = n
}
n = n._right
}
this.insertAfter(lastUndeleted, content)
}
_logString () { _logString () {
const left = this._left !== null ? this._left._lastId : null const left = this._left !== null ? this._left._lastId : null
const origin = this._origin !== null ? this._origin._lastId : null const origin = this._origin !== null ? this._origin._lastId : null

View File

@@ -17,7 +17,7 @@ function domToYXml (parent, doms, _document) {
} }
if (parent._domFilter(d.nodeName, new Map()) !== null) { if (parent._domFilter(d.nodeName, new Map()) !== null) {
let type let type
const hookName = d._yjsHook const hookName = d._yjsHook || (d.dataset != null ? d.dataset.yjsHook : undefined)
if (hookName !== undefined) { if (hookName !== undefined) {
type = new YXmlHook(hookName, d) type = new YXmlHook(hookName, d)
} else if (d.nodeType === d.TEXT_NODE) { } else if (d.nodeType === d.TEXT_NODE) {
@@ -55,7 +55,7 @@ class YXmlTreeWalker {
} }
} }
do { do {
if (!n._deleted && n.constructor === YXmlFragment._YXmlElement && n._start !== null) { if (!n._deleted && (n.constructor === YXmlFragment._YXmlElement || n.constructor === YXmlFragment) && n._start !== null) {
// walk down in the tree // walk down in the tree
n = n._start n = n._start
} else { } else {

View File

@@ -68,7 +68,6 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
} }
} }
if (shouldUpdate) { if (shouldUpdate) {
console.info('updating selection!!')
browserSelection.setBaseAndExtent( browserSelection.setBaseAndExtent(
anchorNode, anchorNode,
anchorOffset, anchorOffset,
@@ -76,7 +75,4 @@ export function afterTransactionSelectionFixer (y, transaction, remote) {
focusOffset focusOffset
) )
} }
// delete, so the objects can be gc'd
relativeSelection = null
browserSelection = null
} }

View File

@@ -193,19 +193,27 @@ export function reflectChangesOnDom (events, _document) {
* only in the attributes (above) * only in the attributes (above)
*/ */
if (event.childListChanged && yxml.constructor !== YXmlHook) { if (event.childListChanged && yxml.constructor !== YXmlHook) {
// create fragment of undeleted nodes let currentChild = dom.firstChild
const fragment = _document.createDocumentFragment()
yxml.forEach(function (t) { yxml.forEach(function (t) {
fragment.appendChild(t.getDom(_document)) let expectedChild = t.getDom(_document)
if (expectedChild.parentNode === dom) {
// is already attached to the dom. Look for it
while (currentChild !== expectedChild) {
let del = currentChild
currentChild = currentChild.nextSibling
dom.removeChild(del)
}
currentChild = currentChild.nextSibling
} else {
// this dom is not yet attached to dom
dom.insertBefore(expectedChild, currentChild)
}
}) })
// remove remainding nodes while (currentChild !== null) {
let lastChild = dom.lastChild let tmp = currentChild.nextSibling
while (lastChild !== null) { dom.removeChild(currentChild)
dom.removeChild(lastChild) currentChild = tmp
lastChild = dom.lastChild
} }
// insert fragment of undeleted nodes
dom.appendChild(fragment)
} }
} }
/* TODO: smartscrolling /* TODO: smartscrolling

View File

@@ -280,7 +280,6 @@ test('deep element insert', async function xml16 (t) {
deepElement.append(boldElement) deepElement.append(boldElement)
deepElement.append(attrElement) deepElement.append(attrElement)
dom0.append(deepElement) dom0.append(deepElement)
console.log(dom0.outerHTML)
let str0 = dom0.outerHTML let str0 = dom0.outerHTML
await flushAll(t, users) await flushAll(t, users)
let str1 = dom1.outerHTML let str1 = dom1.outerHTML
@@ -340,7 +339,6 @@ test('Filtering remote changes', async function xmlFilteringRemote (t) {
// check dom // check dom
paragraph.getDom().setAttribute('malicious', 'true') paragraph.getDom().setAttribute('malicious', 'true')
span.getDom().setAttribute('malicious', 'true') span.getDom().setAttribute('malicious', 'true')
console.log(xml0.toString())
// check incoming attributes // check incoming attributes
xml1.get(0).get(0).setAttribute('malicious', 'true') xml1.get(0).get(0).setAttribute('malicious', 'true')
xml1.insert(0, [new Y.XmlElement('hideMe')]) xml1.insert(0, [new Y.XmlElement('hideMe')])

8
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
/** /**
* yjs - A framework for real-time p2p shared editing on any data * yjs - A framework for real-time p2p shared editing on any data
* @version v13.0.0-37 * @version v13.0.0-41
* @license MIT * @license MIT
*/ */
@@ -2204,15 +2204,38 @@ class YEvent {
} }
class YArrayEvent extends YEvent { class YArrayEvent extends YEvent {
constructor (yarray, remote) { constructor (yarray, remote, transaction) {
super(yarray); super(yarray);
this.remote = remote; this.remote = remote;
this._transaction = transaction;
}
get addedElements () {
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);
}
});
return addedElements
}
get removedElements () {
const target = this.target;
const transaction = this._transaction;
const removedElements = new Set();
transaction.deletedStructs.forEach(function (struct) {
if (struct._parent === target && !transaction.newTypes.has(struct)) {
removedElements.add(struct);
}
});
return removedElements
} }
} }
class YArray extends Type { class YArray extends Type {
_callObserver (transaction, parentSubs, remote) { _callObserver (transaction, parentSubs, remote) {
this._callEventHandler(transaction, new YArrayEvent(this, remote)); this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction));
} }
get (pos) { get (pos) {
let n = this._start; let n = this._start;
@@ -2413,6 +2436,17 @@ class YArray extends Type {
} }
this.insertAfter(left, content); this.insertAfter(left, content);
} }
push (content) {
let n = this._start;
let lastUndeleted = null;
while (n !== null) {
if (!n._deleted) {
lastUndeleted = n;
}
n = n._right;
}
this.insertAfter(lastUndeleted, content);
}
_logString () { _logString () {
const left = this._left !== null ? this._left._lastId : null; const left = this._left !== null ? this._left._lastId : null;
const origin = this._origin !== null ? this._origin._lastId : null; const origin = this._origin !== null ? this._origin._lastId : null;
@@ -2745,19 +2779,27 @@ function reflectChangesOnDom (events, _document) {
* only in the attributes (above) * only in the attributes (above)
*/ */
if (event.childListChanged && yxml.constructor !== YXmlHook) { if (event.childListChanged && yxml.constructor !== YXmlHook) {
// create fragment of undeleted nodes let currentChild = dom.firstChild;
const fragment = _document.createDocumentFragment();
yxml.forEach(function (t) { yxml.forEach(function (t) {
fragment.appendChild(t.getDom(_document)); let expectedChild = t.getDom(_document);
if (expectedChild.parentNode === dom) {
// is already attached to the dom. Look for it
while (currentChild !== expectedChild) {
let del = currentChild;
currentChild = currentChild.nextSibling;
dom.removeChild(del);
}
currentChild = currentChild.nextSibling;
} else {
// this dom is not yet attached to dom
dom.insertBefore(expectedChild, currentChild);
}
}); });
// remove remainding nodes while (currentChild !== null) {
let lastChild = dom.lastChild; let tmp = currentChild.nextSibling;
while (lastChild !== null) { dom.removeChild(currentChild);
dom.removeChild(lastChild); currentChild = tmp;
lastChild = dom.lastChild;
} }
// insert fragment of undeleted nodes
dom.appendChild(fragment);
} }
} }
/* TODO: smartscrolling /* TODO: smartscrolling
@@ -2939,7 +2981,6 @@ function afterTransactionSelectionFixer (y, transaction, remote) {
} }
} }
if (shouldUpdate) { if (shouldUpdate) {
console.info('updating selection!!');
browserSelection.setBaseAndExtent( browserSelection.setBaseAndExtent(
anchorNode, anchorNode,
anchorOffset, anchorOffset,
@@ -2947,9 +2988,6 @@ function afterTransactionSelectionFixer (y, transaction, remote) {
focusOffset focusOffset
); );
} }
// delete, so the objects can be gc'd
relativeSelection = null;
browserSelection = null;
} }
class YXmlEvent extends YEvent { class YXmlEvent extends YEvent {
@@ -3717,7 +3755,7 @@ function domToYXml (parent, doms, _document) {
} }
if (parent._domFilter(d.nodeName, new Map()) !== null) { if (parent._domFilter(d.nodeName, new Map()) !== null) {
let type; let type;
const hookName = d._yjsHook; const hookName = d._yjsHook || (d.dataset != null ? d.dataset.yjsHook : undefined);
if (hookName !== undefined) { if (hookName !== undefined) {
type = new YXmlHook(hookName, d); type = new YXmlHook(hookName, d);
} else if (d.nodeType === d.TEXT_NODE) { } else if (d.nodeType === d.TEXT_NODE) {
@@ -3755,7 +3793,7 @@ class YXmlTreeWalker {
} }
} }
do { do {
if (!n._deleted && n.constructor === YXmlFragment._YXmlElement && n._start !== null) { if (!n._deleted && (n.constructor === YXmlFragment._YXmlElement || n.constructor === YXmlFragment) && n._start !== null) {
// walk down in the tree // walk down in the tree
n = n._start; n = n._start;
} else { } else {

File diff suppressed because one or more lines are too long

1603
y.test.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long