Compare commits

...

13 Commits

Author SHA1 Message Date
Kevin Jahns
668e9e8a9b 13.0.0-85 2019-05-25 03:13:54 +02:00
Kevin Jahns
37a6d68543 implement support for boolean values. fixes #151 2019-05-25 03:12:56 +02:00
Kevin Jahns
f893198769 remove examples. fixes #149 2019-05-22 17:32:51 +02:00
Kevin Jahns
d3ee1a0ec2 Add editor support to v13 readme 2019-05-22 01:26:13 +02:00
Kevin Jahns
d6593412a2 13.0.0-84 2019-05-19 21:49:36 +02:00
Kevin Jahns
d31bf36531 use generated esm module by default 2019-05-19 21:48:09 +02:00
Kevin Jahns
a485f550db 13.0.0-83 2019-05-19 20:59:56 +02:00
Kevin Jahns
0610b16227 bump lib0 for webpack compatibility 2019-05-19 20:43:18 +02:00
Kevin Jahns
72e470c5f0 Fix ytext event.delta - items that are synced and deleted
When items are added and deleted in the same transaction, event.delta would recognize them as added (though they are actually deleted). Now it just ignores them.
2019-05-19 20:42:53 +02:00
Kevin Jahns
4d12a02e2f fix offset in state vector 2019-05-16 12:31:53 +02:00
Kevin Jahns
4a7d6f0a2d fix sorting bug that only affects older node versions (probably because old sorting algorithms are not stable) 2019-05-14 15:21:34 +02:00
Kevin Jahns
c80f446b5f README: update provider tutorial 2019-05-12 11:18:43 +02:00
Kevin Jahns
81a529d8dc update *getting started* yjs version 2019-05-07 15:43:09 +02:00
30 changed files with 382 additions and 2037 deletions

View File

@@ -40,11 +40,13 @@ This repository contains a collection of shared types that can be observed for c
| Name                                                   | Cursors | Binding | Demo |
|---|:-:|---|---|
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [link](https://yjs.website/tutorial-prosemirror.html) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [link](https://yjs.website/tutorial-quill.html) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [link](https://yjs.website/tutorial-codemirror.html) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/y-js/y-ace) | [link]() |
| [Monaco](https://microsoft.github.io/monaco-editor/) | | [y-monaco](http://github.com/y-js/y-monaco) | [link]() |
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/y-js/y-prosemirror) | [demo](https://yjs-demos.now.sh/prosemirror/) |
| [Quill](https://quilljs.com/) | | [y-quill](http://github.com/y-js/y-quill) | [demo](https://yjs-demos.now.sh/quill/) |
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/y-js/y-codemirror) | [demo](https://yjs-demos.now.sh/codemirror/) |
| [Monaco](https://microsoft.github.io/monaco-editor/) | | [y-monaco](http://github.com/y-js/y-monaco) | [demo](https://yjs-demos.now.sh/monaco/) |
| [Ace](https://ace.c9.io/) | | [y-ace](http://github.com/y-js/y-ace) | [demo](https://yjs-demos.now.sh/ace/) |
| [Textarea](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea) | | [y-textarea](http://github.com/y-js/y-textarea) | [demo](https://yjs-demos.now.sh/textarea/) |
| [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) | | [y-dom](http://github.com/y-js/y-dom) | [demo](https://yjs-demos.now.sh/dom/) |
### Providers
@@ -65,7 +67,7 @@ Setting up the communication between clients, managing awareness information, an
Install Yjs and a provider with your favorite package manager.
```sh
npm i yjs@13.0.0-81 y-websocket@1.0.0-2 y-textarea
npm i yjs@13.0.0-82 y-websocket@1.0.0-3 y-textarea
```
**Start the y-websocket server**
@@ -83,8 +85,10 @@ import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { TextareaBinding } from 'y-textarea'
const provider = new WebsocketProvider('http://localhost:1234')
const doc = provider.get('roomname')
const doc = Y.Doc()
const provider = new WebsocketProvider('http://localhost:1234', 'roomname')
// sync all document updates through the websocket connection
provider.sync('doc')
// Define a shared type on the document.
const ytext = doc.getText('my resume')
@@ -135,11 +139,11 @@ import * as Y from 'yjs'
</p>
<pre>const yarray = new Y.Array()</pre>
<dl>
<b><code>insert(index:number, content:Array&lt;object|string|number|Uint8Array|Y.Type&gt;)</code></b>
<b><code>insert(index:number, content:Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd>
Insert content at <var>index</var>. Note that content is an array of elements. I.e. <code>array.insert(0, [1]</code> splices the list and inserts 1 at position 0.
</dd>
<b><code>push(Array&lt;Object|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<b><code>push(Array&lt;Object|boolean|Array|string|number|Uint8Array|Y.Type&gt;)</code></b>
<dd></dd>
<b><code>delete(index:number, length:number)</code></b>
<dd></dd>
@@ -149,9 +153,9 @@ import * as Y from 'yjs'
<dd></dd>
<b><code>map(function(T, number, YArray):M):Array&lt;M&gt;</code></b>
<dd></dd>
<b><code>toArray():Array&lt;Object|Array|string|number|Uint8Array|Y.Type&gt;</code></b>
<b><code>toArray():Array&lt;object|boolean|Array|string|number|Uint8Array|Y.Type&gt;</code></b>
<dd>Copies the content of this YArray to a new Array.</dd>
<b><code>toJSON():Array&lt;Object|Array|string|number&gt;</code></b>
<b><code>toJSON():Array&lt;Object|boolean|Array|string|number&gt;</code></b>
<dd>Copies the content of this YArray to a new Array. It transforms all child types to JSON using their <code>toJSON</code> method.</dd>
<b><code>[Symbol.Iterator]</code></b>
<dd>
@@ -184,9 +188,9 @@ import * as Y from 'yjs'
</p>
<pre><code>const ymap = new Y.Map()</code></pre>
<dl>
<b><code>get(key:string):object|string|number|Uint8Array|Y.Type</code></b>
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
<dd></dd>
<b><code>set(key:string, value:object|string|number|Uint8Array|Y.Type)</code></b>
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
<dd></dd>
<b><code>delete(key:string)</code></b>
<dd></dd>
@@ -194,7 +198,7 @@ import * as Y from 'yjs'
<dd></dd>
<b><code>get(index:number)</code></b>
<dd></dd>
<b><code>toJSON():Object&lt;string, Object|Array|string|number&gt;</code></b>
<b><code>toJSON():Object&lt;string, Object|boolean|Array|string|number&gt;</code></b>
<dd>Copies the <code>[key,value]</code> pairs of this YMap to a new Object. It transforms all child types to JSON using their <code>toJSON</code> method.</dd>
<b><code>[Symbol.Iterator]</code></b>
<dd>
@@ -433,7 +437,7 @@ doc1.getArray('myarray').insert(0, ['Hello doc2, you got this?'])
doc2.getArray('myarray').get(0) // => 'Hello doc2, you got this?'
```
Yjs internally maintains a [State Vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences.
Yjs internally maintains a [state vector](#State-Vector) that denotes the next expected clock from each client. In a different interpretation it holds the number of structs created by each client. When two clients sync, you can either exchange the complete document structure or only the differences by sending the state vector to compute the differences.
**Example: Sync two clients by exchanging the complete document structure**
@@ -449,8 +453,8 @@ Y.applyUpdate(ydoc2, state1)
This example shows how to sync two clients with the minimal amount of exchanged data by computing only the differences using the state vector of the remote client. Syncing clients using the state vector requires another roundtrip, but can safe a lot of bandwidth.
```js
const stateVector1 = Y.encodeDocumentStateVector(ydoc1)
const stateVector2 = Y.encodeDocumentStateVector(ydoc2)
const stateVector1 = Y.encodeStateVector(ydoc1)
const stateVector2 = Y.encodeStateVector(ydoc2)
const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)
const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)
Y.applyUpdate(ydoc1, diff2)
@@ -462,7 +466,7 @@ Y.applyUpdate(ydoc2, diff1)
<dd>Apply a document update on the shared document. Optionally you can specify <code>transactionOrigin</code> that will be stored on <code>transaction.origin</code> and <code>ydoc.on('update', (update, origin) => ..)</code>.</dd>
<b><code>Y.encodeStateAsUpdate(Y.Doc, [encodedTargetStateVector:Uint8Array]):Uint8Array</code></b>
<dd>Encode the document state as a single update message that can be applied on the remote document. Optionally specify the target state vector to only write the differences to the update message.</dd>
<b><code>Y.encodeDocumentStateVector(Y.Doc):Uint8Array</code></b>
<b><code>Y.encodeStateVector(Y.Doc):Uint8Array</code></b>
<dd>Computes the state vector and encodes it into an Uint8Array.</dd>
</dl>
@@ -530,9 +534,9 @@ Yjs has type descriptions. But until [this ticket](https://github.com/Microsoft/
## Yjs CRDT Algorithm
*Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differenciation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth / central source of failure.
*Conflict-free replicated data types* (CRDT) for collaborative editing are an alternative approach to *operational transformation* (OT). A very simple differenciation between the two approaches is that OT attempts to transform index positions to ensure convergence (all clients end up with the same content), while CRDTs use mathematical models that usually do not involve index transformations, like linked lists. OT is currently the de-facto standard for shared editing on text. OT approaches that support shared editing without a central source of truth (a central server) require too much bookkeeping to be viable in practice. CRDTs are better suited for distributed systems, provide additional guarantees that the document can be synced with remote clients, and do not require a central source of truth.
Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). I will eventually publish a paper that describes the new approach. Note: Since operations make up the document structure, we prefer the term *struct* now.
Yjs implements a modified version of the algorithm described in [this paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types). I will eventually publish a paper that describes why this approach works so well in practice. Note: Since operations make up the document structure, we prefer the term *struct* now.
CRDTs suitable for shared text editing suffer from the fact that they only grow in size. There are CRDTs that do not grow in size, but they do not have the characteristics that are benificial for shared text editing (like intention preservation). Yjs implements many improvements to the original algorithm that diminish the trade-off that the document only grows in size. We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs. But we can 1. merge preceeding structs into a single struct to reduce the amount of meta information, 2. we can delete content from the struct if it is deleted, and 3. we can garbage collect tombstones if we don't care about the order of the structs anymore (e.g. if the parent was deleted).
@@ -541,7 +545,7 @@ CRDTs suitable for shared text editing suffer from the fact that they only grow
2. When a struct that contains content (e.g. `ItemString`) is deleted, the struct will be replaced with an `ItemDeleted` that does not contain content anymore.
3. When a type is deleted, all child elements are transformed to `GC` structs. A `GC` struct only denotes the existence of a struct and that it is deleted. `GC` structs can always be merged with other `GC` structs if the id's are adjacent.
Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](#Benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks showt that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents.
Especially when working on structured content (e.g. shared editing on ProseMirror), these improvements yield very good results when [benchmarking](https://github.com/dmonad/crdt-benchmarks) random document edits. In practice they show even better results, because users usually edit text in sequence, resulting in structs that can easily be merged. The benchmarks show that even in the worst case scenario that a user edits text from right to left, Yjs achieves good performance even for huge documents.
#### State Vector
Yjs has the ability to exchange only the differences when syncing two clients. We use lamport timestamps to identify structs and to track in which order a client created them. Each struct has an `struct.id = { client: number, clock: number}` that uniquely identifies a struct. We define the next expected `clock` by each client as the *state vector*. This data structure is similar to the [version vectors](https://en.wikipedia.org/wiki/Version_vector) data structure. But we use state vectors only to describe the state of the local document, so we can compute the missing struct of the remote client. We do not use it to track causality.

1
examples/.gitignore vendored
View File

@@ -1 +0,0 @@
build

View File

@@ -1,70 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs CodeMirror Example</title>
<link rel=stylesheet href="https://codemirror.net/lib/codemirror.css">
<style>
#container {
border: grey;
border-style: solid;
border-width: thin;
}
</style>
</head>
<body>
<p>This example shows how to bind a YText type to <a href="https://codemirror.net/">CodeMirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<style>
.remote-caret {
position: absolute;
border-left: black;
border-left-style: solid;
border-left-width: 2px;
height: 1em;
}
.remote-caret > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
</style>
<div id="container"></div>
</div>
<!-- The actual source file for the following code is found in ./codemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/codemirror.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { CodeMirrorBinding } from 'yjs/bindings/codemirror.js'
import * as conf from './exampleConfig.js'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('codemirror')
const ytext = ydocument.define('codemirror', Y.Text)
const editor = new CodeMirror(document.querySelector('#container'), {
mode: 'javascript',
lineNumbers: true
})
const binding = new CodeMirrorBinding(ytext, editor)
window.codemirrorExample = {
binding, editor, ytext, ydocument
}
</script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
import { WebsocketProvider } from 'y-websocket'
import { CodeMirrorBinding } from 'y-codemirror'
import * as conf from './exampleConfig.js'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('codemirror')
const ytext = ydocument.getText('codemirror')
const editor = new CodeMirror(document.querySelector('#container'), {
mode: 'javascript',
lineNumbers: true
})
const binding = new CodeMirrorBinding(ytext, editor)
window.codemirrorExample = {
binding, editor, ytext, ydocument
}

View File

@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
<style>
#content {
min-height: 500px;
}
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to an arbitrary DOM element. We set the DOM element to contenteditable so it basically behaves like a very powerful rich-text editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<hr>
<div class="code-html">
<div id="content" contenteditable=""></div>
</div>
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/dom.js" type="module">
import * as Y from 'yjs/index.js'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { DomBinding } from 'yjs/bindings/dom.js'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('dom')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
window.example = {
provider, ydocument, type, binding
}
</script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
import * as Y from '../src/index.js'
import { WebsocketProvider } from 'y-websocket'
import { DomBinding } from 'y-dom'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('dom')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
window.example = {
provider, ydocument, type, binding
}

View File

@@ -1,9 +0,0 @@
/* eslint-env browser */
const isDeployed = location.hostname === 'yjs.website'
if (!isDeployed) {
console.log('%cYjs: Start your local websocket server by running %c`npm run websocket-server`', 'color:blue', 'color: grey; font-weight: bold')
}
export const serverAddress = isDeployed ? 'wss://api.yjs.website' : 'ws://localhost:1234'

View File

@@ -1,17 +0,0 @@
{
"codemirror": {
"title": "CodeMirror Binding"
},
"prosemirror": {
"title": "ProseMirror Binding"
},
"textarea": {
"title": "Textarea Binding"
},
"quill": {
"title": "Quill Binding"
},
"dom": {
"title": "Dom Binding"
}
}

View File

@@ -1,159 +0,0 @@
import { Plugin } from 'prosemirror-state'
import crel from 'crel'
import * as Y from '../src/index.js'
import { prosemirrorPluginKey } from 'y-prosemirror'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as historyProtocol from 'y-protocols/history.js'
const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
[ychange_state][ychange_user="${userid}"]:hover::before {
content: "${username}" !important;
background-color: ${color} !important;
}
[ychange_state="added"][ychange_user="${userid}"] {
background-color: ${color2} !important;
}
[ychange_state="removed"][ychange_user="${userid}"] {
color: ${color} !important;
}
`
export const noteHistoryPlugin = new Plugin({
state: {
init (initargs, state) {
return new NoteHistoryPlugin()
},
apply (tr, pluginState) {
return pluginState
}
},
view (editorView) {
const hstate = noteHistoryPlugin.getState(editorView.state)
hstate.init(editorView)
return {
destroy: hstate.destroy.bind(hstate)
}
}
})
const createWrapper = () => {
const wrapper = crel('div', { style: 'display: flex;' })
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
wrapper.insertBefore(historyContainer, null)
const userStyleContainer = crel('style')
wrapper.insertBefore(userStyleContainer, null)
return { wrapper, historyContainer, userStyleContainer }
}
class NoteHistoryPlugin {
init (editorView) {
this.editorView = editorView
const { historyContainer, wrapper, userStyleContainer } = createWrapper()
this.userStyleContainer = userStyleContainer
this.wrapper = wrapper
this.historyContainer = historyContainer
const n = editorView.dom.parentNode.parentNode
n.parentNode.replaceChild(this.wrapper, n)
n.style['flex-grow'] = '1'
wrapper.insertBefore(n, this.wrapper.firstChild)
this.render()
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
history.observe(this.render.bind(this))
}
destroy () {
this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
history.unobserve(this.render)
}
render () {
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array).toArray()
const fragment = document.createDocumentFragment()
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
fragment.insertBefore(snapshotBtn, null)
let _prevSnap = null // empty
snapshotBtn.addEventListener('click', () => {
const awareness = y.getAwarenessInfo()
const userMap = new Map()
const aw = y.getLocalAwarenessInfo()
userMap.set(y.userID, aw.name || 'unknown')
awareness.forEach((a, userID) => {
userMap.set(userID, a.name || 'Unknown')
})
this.snapshot(userMap)
})
history.forEach(buf => {
const decoder = decoding.createDecoder(buf)
const snapshot = historyProtocol.readHistorySnapshot(decoder)
const date = new Date(decoding.readUint32(decoder) * 1000)
const restoreBtn = crel('button', { type: 'button' }, ['restore'])
const a = crel('a', [
'• ' + date.toUTCString(), restoreBtn
])
const el = crel('div', [ a ])
let prevSnapshot = _prevSnap // rebind to new variable
restoreBtn.addEventListener('click', event => {
if (prevSnapshot === null) {
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
}
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
event.stopPropagation()
})
a.addEventListener('click', () => {
console.log('setting snapshot')
if (prevSnapshot === null) {
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
}
this.renderSnapshot(snapshot, prevSnapshot)
})
fragment.insertBefore(el, null)
_prevSnap = snapshot
})
this.historyContainer.innerHTML = ''
this.historyContainer.insertBefore(fragment, null)
}
renderSnapshot (snapshot, prevSnapshot) {
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
/**
* @type {Array<string|null>}
*/
let colors = niceColors.slice()
let style = ''
snapshot.userMap.forEach((name, userid) => {
/**
* @type {any}
*/
const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
let color = null
let i = 0
for (; i < colors.length && color === null; i++) {
color = colors[(randInt + i) % colors.length]
}
if (color === null) {
colors = niceColors.slice()
i = 0
color = colors[randInt % colors.length]
}
colors[randInt % colors.length] = null
style += createUserCSS(userid, name, color, color + '69')
})
this.userStyleContainer.innerHTML = style
}
/**
* @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
*/
snapshot (updatedUserMap = new Map()) {
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
const encoder = encoding.createEncoder()
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
history.push([encoding.toUint8Array(encoder)])
}
}

View File

@@ -1,197 +0,0 @@
import { Schema } from 'prosemirror-model'
const brDOM = ['br']
const calcYchangeDomAttrs = (attrs, domAttrs = {}) => {
domAttrs = Object.assign({}, domAttrs)
if (attrs.ychange !== null) {
domAttrs.ychange_user = attrs.ychange.user
domAttrs.ychange_state = attrs.ychange.state
}
return domAttrs
}
// :: Object
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes = {
// :: NodeSpec The top level document node.
doc: {
content: 'block+'
},
// :: NodeSpec A plain paragraph textblock. Represented in the DOM
// as a `<p>` element.
paragraph: {
attrs: { ychange: { default: null } },
content: 'inline*',
group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM (node) { return ['p', calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
attrs: { ychange: { default: null } },
content: 'block+',
group: 'block',
defining: true,
parseDOM: [{ tag: 'blockquote' }],
toDOM (node) { return ['blockquote', calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A horizontal rule (`<hr>`).
horizontal_rule: {
attrs: { ychange: { default: null } },
group: 'block',
parseDOM: [{ tag: 'hr' }],
toDOM (node) {
return ['hr', calcYchangeDomAttrs(node.attrs)]
}
},
// :: NodeSpec A heading textblock, with a `level` attribute that
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to
// `<h6>` elements.
heading: {
attrs: {
level: { default: 1 },
ychange: { default: null }
},
content: 'inline*',
group: 'block',
defining: true,
parseDOM: [{ tag: 'h1', attrs: { level: 1 } },
{ tag: 'h2', attrs: { level: 2 } },
{ tag: 'h3', attrs: { level: 3 } },
{ tag: 'h4', attrs: { level: 4 } },
{ tag: 'h5', attrs: { level: 5 } },
{ tag: 'h6', attrs: { level: 6 } }],
toDOM (node) { return ['h' + node.attrs.level, calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A code listing. Disallows marks or non-text inline
// nodes by default. Represented as a `<pre>` element with a
// `<code>` element inside of it.
code_block: {
attrs: { ychange: { default: null } },
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
toDOM (node) { return ['pre', calcYchangeDomAttrs(node.attrs), ['code', 0]] }
},
// :: NodeSpec The text node.
text: {
group: 'inline'
},
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
// `alt`, and `href` attributes. The latter two default to the empty
// string.
image: {
inline: true,
attrs: {
ychange: { default: null },
src: {},
alt: { default: null },
title: { default: null }
},
group: 'inline',
draggable: true,
parseDOM: [{ tag: 'img[src]',
getAttrs (dom) {
return {
src: dom.getAttribute('src'),
title: dom.getAttribute('title'),
alt: dom.getAttribute('alt')
}
} }],
toDOM (node) {
const domAttrs = {
src: node.attrs.src,
title: node.attrs.title,
alt: node.attrs.alt
}
return ['img', calcYchangeDomAttrs(node.attrs, domAttrs)]
}
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
inline: true,
group: 'inline',
selectable: false,
parseDOM: [{ tag: 'br' }],
toDOM () { return brDOM }
}
}
const emDOM = ['em', 0]; const strongDOM = ['strong', 0]; const codeDOM = ['code', 0]
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks = {
// :: MarkSpec A link. Has `href` and `title` attributes. `title`
// defaults to the empty string. Rendered and parsed as an `<a>`
// element.
link: {
attrs: {
href: {},
title: { default: null }
},
inclusive: false,
parseDOM: [{ tag: 'a[href]',
getAttrs (dom) {
return { href: dom.getAttribute('href'), title: dom.getAttribute('title') }
} }],
toDOM (node) { return ['a', node.attrs, 0] }
},
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }],
toDOM () { return emDOM }
},
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
// also match `<b>` and `font-weight: bold`.
strong: {
parseDOM: [{ tag: 'strong' },
// This works around a Google Docs misbehavior where
// pasted content will be inexplicably wrapped in `<b>`
// tags with a font-weight normal.
{ tag: 'b', getAttrs: node => node.style.fontWeight !== 'normal' && null },
{ style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null }],
toDOM () { return strongDOM }
},
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
parseDOM: [{ tag: 'code' }],
toDOM () { return codeDOM }
},
ychange: {
attrs: {
user: { default: null },
state: { default: null }
},
inclusive: false,
parseDOM: [{ tag: 'ychange' }],
toDOM (node) {
return ['ychange', { ychange_user: node.attrs.user, ychange_state: node.attrs.state }, 0]
}
}
}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
// which are defined in the [`prosemirror-schema-list`](#schema-list)
// module.
//
// To reuse elements from this schema, extend or read from its
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
export const schema = new Schema({ nodes, marks })

View File

@@ -1,330 +0,0 @@
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror li {
position: relative;
}
.ProseMirror-hideselection *::selection { background: transparent; }
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
.ProseMirror-hideselection { caret-color: transparent; }
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px; top: -2px; bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
.ProseMirror-textblock-dropdown {
min-width: 3em;
}
.ProseMirror-menu {
margin: 0 -4px;
line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
width: -webkit-fit-content;
width: fit-content;
white-space: pre;
}
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
}
.ProseMirror-menuseparator {
border-right: 1px solid #ddd;
margin-right: 3px;
}
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
font-size: 90%;
white-space: nowrap;
}
.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
padding-right: 15px;
}
.ProseMirror-menu-dropdown-wrap {
padding: 1px 0 1px 4px;
display: inline-block;
position: relative;
}
.ProseMirror-menu-dropdown:after {
content: "";
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
position: absolute;
background: white;
color: #666;
border: 1px solid #aaa;
padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
z-index: 15;
min-width: 6em;
}
.ProseMirror-menu-dropdown-item {
cursor: pointer;
padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
background: #f2f2f2;
}
.ProseMirror-menu-submenu-wrap {
position: relative;
margin-right: -4px;
}
.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
display: none;
min-width: 4em;
left: 100%;
top: -3px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-disabled {
opacity: .3;
}
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
display: block;
}
.ProseMirror-menubar {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
position: relative;
min-height: 1em;
color: #666;
padding: 1px 6px;
top: 0; left: 0; right: 0;
border-bottom: 1px solid silver;
background: white;
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-icon {
display: inline-block;
line-height: .8;
vertical-align: -2px; /* Compensate for padding */
padding: 2px 8px;
cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
cursor: default;
}
.ProseMirror-icon svg {
fill: currentColor;
height: 1em;
}
.ProseMirror-icon span {
vertical-align: text-top;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
/* Add space around the hr to make clicking it easier */
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
margin: 1em 0;
}
.ProseMirror-example-setup-style hr:after {
content: "";
display: block;
height: 1px;
background-color: silver;
line-height: 2px;
}
.ProseMirror ul, .ProseMirror ol {
padding-left: 30px;
}
.ProseMirror blockquote {
padding-left: 1em;
border-left: 3px solid #eee;
margin-left: 0; margin-right: 0;
}
.ProseMirror-example-setup-style img {
cursor: default;
}
.ProseMirror-prompt {
background: white;
padding: 5px 10px 5px 15px;
border: 1px solid silver;
position: fixed;
border-radius: 3px;
z-index: 11;
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
}
.ProseMirror-prompt h5 {
margin: 0;
font-weight: normal;
font-size: 100%;
color: #444;
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
background: #eee;
border: none;
outline: none;
}
.ProseMirror-prompt input[type="text"] {
padding: 0 4px;
}
.ProseMirror-prompt-close {
position: absolute;
left: 2px; top: 1px;
color: #666;
border: none; background: transparent; padding: 0;
}
.ProseMirror-prompt-close:after {
content: "✕";
font-size: 12px;
}
.ProseMirror-invalid {
background: #ffc;
border: 1px solid #cc7;
border-radius: 4px;
padding: 5px 10px;
position: absolute;
min-width: 10em;
}
.ProseMirror-prompt-buttons {
margin-top: 5px;
display: none;
}
#editor, .editor {
background: white;
color: black;
background-clip: padding-box;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0;
margin-bottom: 23px;
}
.ProseMirror p:first-child,
.ProseMirror h1:first-child,
.ProseMirror h2:first-child,
.ProseMirror h3:first-child,
.ProseMirror h4:first-child,
.ProseMirror h5:first-child,
.ProseMirror h6:first-child {
margin-top: 10px;
}
.ProseMirror {
padding: 4px 8px 4px 14px;
line-height: 1.2;
outline: none;
}
.ProseMirror p { margin-bottom: 1em }

View File

@@ -1,123 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="./prosemirror.css">
<style>
placeholder {
display: inline;
border: 1px solid #ccc;
color: #ccc;
}
placeholder:after {
content: "☁";
font-size: 200%;
line-height: 0.1;
font-weight: bold;
}
.ProseMirror img { max-width: 100px }
/* this is a rough fix for the first cursor position when the first paragraph is empty */
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
margin-top: 16px;
}
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
margin-top: 16px
}
.ProseMirror-yjs-cursor {
position: absolute;
border-left: black;
border-left-style: solid;
border-left-width: 2px;
border-color: orange;
height: 1em;
word-break: normal;
}
.ProseMirror-yjs-cursor > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
[ychange_state] {
position: relative;
}
[ychange_state]:hover::before {
content: attr(ychange_user);
background-color: #fa8100;
position: absolute;
top: -14px;
right: 0;
font-size: 12px;
padding: 0 2px;
border-radius: 3px 3px 0 0;
color: #fdfdfe;
user-select: none;
word-break: normal;
}
*[ychange_state='added'] {
background-color: #fa810069;
}
ychange[ychange_state='removed'] {
color: rgb(250, 129, 0);
text-decoration: line-through;
}
*:not(ychange)[ychange_state='removed'] {
background-color: #ff9494c9;
text-decoration: line-through;
}
img[ychange_state='removed'] {
padding: 2px;
}
.y-connect-btn {
position: absolute;
top: 20px;
right: 20px;
}
</style>
</head>
<body>
<button type="button" class="y-connect-btn">Disconnect</button>
<p>This example shows how to bind a YXmlFragment to a <a href="http://prosemirror.net">Prosemirror</a> editor using <a href="https://github.com/y-js/y-prosemirror">y-prosemirror</a>.</p>
<p>The content of this editor is shared with every client that visits this domain.</p>
<div class="code-html">
<div id="editor" style="margin-bottom: 23px"></div>
<div style="display: none" id="content"></div>
</div>
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/prosemirror.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from 'yjs/bindings/prosemirror'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { DOMParser } from 'prosemirror-model'
import { schema } from 'prosemirror-schema-basic'
import { exampleSetup } from 'prosemirror-example-setup'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
window.example = { provider, ydocument, type, prosemirrorView }
</script>
</body>
</html>

View File

@@ -1,36 +0,0 @@
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { prosemirrorPlugin, cursorPlugin } from 'y-prosemirror'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { DOMParser } from 'prosemirror-model'
import { schema } from './prosemirror-schema.js'
import { exampleSetup } from 'prosemirror-example-setup'
// import { noteHistoryPlugin } from './prosemirror-history.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror' /*, { gc: false } */)
const type = ydocument.get('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({ schema }).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
const connectBtn = document.querySelector('.y-connect-btn')
connectBtn.addEventListener('click', () => {
if (ydocument.wsconnected) {
ydocument.disconnect()
connectBtn.textContent = 'Connect'
} else {
ydocument.connect()
connectBtn.textContent = 'Disconnect'
}
})
window.example = { provider, ydocument, type, prosemirrorView }

View File

@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Quill Example</title>
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
</head>
<body>
<p>This example shows how to bind a YText type to <a href="https://quilljs.com">Quill</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<div id="quill-container">
<div id="quill">
</div>
</div>
</div>
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/quill.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { QuillBinding } from 'yjs/bindings/quill.js'
import * as conf from './exampleConfig.js'
import Quill from 'quill'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)
const 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'
})
window.quillBinding = new QuillBinding(ytext, quill)
</script>
</body>
</html>

View File

@@ -1,30 +0,0 @@
import * as Y from '../src/index.js'
import { WebsocketProvider } from 'y-websocket'
import { QuillBinding } from 'y-quill'
import * as conf from './exampleConfig.js'
import Quill from 'quill'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)
const 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'
})
window.quillBinding = new QuillBinding(ytext, quill)

View File

@@ -1,29 +0,0 @@
footer img {
display: none;
}
nav .title h1 a {
display: none;
}
footer {
background-color: #b93c1d;
}
#resizer {
background-color: #b93c1d;
}
.main section article.readme h1:first-child img {
display: none;
}
.main section article.readme h1:first-child {
margin-bottom: 16px;
margin-top: 30px;
}
.main section article.readme h1:first-child::before {
content: "Yjs";
font-size: 2em;
}

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Textarea Example</title>
</head>
<body>
<p>This example shows how to bind a YText type to a DOM Textarea.</p>
<p>The content of this textarea is shared with every client who visits this domain.</p>
<div class="code-html">
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/textarea.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { TextareaBinding } from 'yjs/bindings/textarea.js'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')
const binding = new TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}
</script>
</body>
</html>

View File

@@ -1,14 +0,0 @@
import { WebsocketProvider } from 'y-websocket'
import { TextareaBinding } from 'y-textarea'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.getText('textarea')
const textarea = document.querySelector('textarea')
const binding = new TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}

1045
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,14 @@
{
"name": "yjs",
"version": "13.0.0-82",
"version": "13.0.0-85",
"description": "Shared Editing Library",
"main": "./dist/yjs.js",
"module": "./src/index.js",
"module": "./dist/yjs.mjs",
"sideEffects": false,
"scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
"dist": "rm -rf dist examples/build && rollup -c",
"serve-examples": "concurrently 'npm run watch' 'serve examples'",
"dist": "rm -rf dist && rollup -c",
"watch": "rollup -wc",
"lint": "standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
@@ -28,15 +27,13 @@
],
"dictionaries": {
"doc": "docs",
"example": "examples",
"test": "tests"
},
"standard": {
"ignore": [
"/dist",
"/node_modules",
"/docs",
"/examples/build"
"/docs"
]
},
"repository": {
@@ -54,27 +51,20 @@
},
"homepage": "http://y-js.org",
"dependencies": {
"lib0": "0.0.4"
"lib0": "0.0.5"
},
"devDependencies": {
"y-protocols": "0.0.4",
"codemirror": "^5.42.0",
"concurrently": "^3.6.1",
"jsdoc": "^3.5.5",
"jsdoc": "^3.6.2",
"live-server": "^1.2.1",
"prosemirror-example-setup": "^1.0.1",
"prosemirror-schema-basic": "^1.0.0",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.6.5",
"quill": "^1.3.6",
"quill-cursors": "^1.0.3",
"rollup": "^1.1.2",
"rollup": "^1.11.3",
"rollup-cli": "^1.0.9",
"rollup-plugin-commonjs": "^9.2.0",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-commonjs": "^9.3.4",
"rollup-plugin-node-resolve": "^4.2.4",
"rollup-plugin-terser": "^4.0.4",
"standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^3.3.3333"
"typescript": "^3.4.5",
"y-protocols": "0.0.6"
}
}

View File

@@ -23,12 +23,14 @@ const debugResolve = {
if (importee === 'yjs') {
return `${process.cwd()}/src/index.js`
}
/*
if (customModules.has(importee.split('/')[0])) {
return `${process.cwd()}/../${importee}/src/${importee}.js`
}
if (customLibModules.has(importee.split('/')[0])) {
return `${process.cwd()}/../${importee}`
}
*/
return null
}
}
@@ -80,26 +82,8 @@ export default [{
debugResolve,
nodeResolve({
sourcemap: true,
module: true,
browser: true
}),
commonjs()
]
}, {
input: ['./examples/textarea.js', './examples/prosemirror.js'], // './examples/quill.js', './examples/dom.js', './examples/codemirror.js'
output: {
dir: 'examples/build',
format: 'esm',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
module: true,
browser: true
}),
commonjs(),
...minificationPlugins
mainFields: ['module', 'browser', 'main']
})
// commonjs()
]
}]

View File

@@ -42,5 +42,5 @@ export {
iterateDeletedStructs,
applyUpdate,
encodeStateAsUpdate,
encodeDocumentStateVector
encodeStateVector
} from './internals.js'

View File

@@ -285,7 +285,7 @@ export class AbstractItem extends AbstractStruct {
* @private
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
throw new Error('unimplemented')
throw error.methodUnimplemented()
}
/**
@@ -393,7 +393,7 @@ export class AbstractItem extends AbstractStruct {
* @private
*/
splitAt (transaction, diff) {
throw new Error('unimplemented')
throw error.methodUnimplemented()
}
/**

View File

@@ -101,7 +101,7 @@ export class AbstractType {
* @private
*/
_copy () {
throw new Error('unimplemented')
throw error.methodUnimplemented()
}
/**
@@ -363,7 +363,7 @@ export const typeListGet = (type, index) => {
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {AbstractItem?} referenceItem
* @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
* @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
*
* @private
* @function
@@ -386,6 +386,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c)
@@ -510,6 +511,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
switch (value.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction)

View File

@@ -388,7 +388,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
/**
* Event that describes the changes on a YText type.
*/
class YTextEvent extends YEvent {
export class YTextEvent extends YEvent {
/**
* @param {YText} ytext
* @param {Transaction} transaction
@@ -475,11 +475,13 @@ class YTextEvent extends YEvent {
switch (item.constructor) {
case ItemEmbed:
if (this.adds(item)) {
addOp()
action = 'insert'
// @ts-ignore item is ItemFormat
insert = item.embed
addOp()
if (!this.deletes(item)) {
addOp()
action = 'insert'
// @ts-ignore item is ItemFormat
insert = item.embed
addOp()
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp()
@@ -496,12 +498,14 @@ class YTextEvent extends YEvent {
break
case ItemString:
if (this.adds(item)) {
if (action !== 'insert') {
addOp()
action = 'insert'
if (!this.deletes(item)) {
if (action !== 'insert') {
addOp()
action = 'insert'
}
// @ts-ignore
insert += item.string
}
// @ts-ignore
insert += item.string
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp()
@@ -518,23 +522,25 @@ class YTextEvent extends YEvent {
break
case ItemFormat:
if (this.adds(item)) {
// @ts-ignore item is ItemFormat
const curVal = currentAttributes.get(item.key) || null
// @ts-ignore item is ItemFormat
if (curVal !== item.value) {
if (action === 'retain') {
addOp()
}
if (!this.deletes(item)) {
// @ts-ignore item is ItemFormat
if (item.value === (oldAttributes.get(item.key) || null)) {
const curVal = currentAttributes.get(item.key) || null
// @ts-ignore item is ItemFormat
if (curVal !== item.value) {
if (action === 'retain') {
addOp()
}
// @ts-ignore item is ItemFormat
delete attributes[item.key]
if (item.value === (oldAttributes.get(item.key) || null)) {
// @ts-ignore item is ItemFormat
delete attributes[item.key]
} else {
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value
}
} else {
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value
item.delete(transaction)
}
} else {
item.delete(transaction)
}
} else if (this.deletes(item)) {
// @ts-ignore item is ItemFormat

View File

@@ -193,7 +193,7 @@ const resumeStructIntegration = (transaction, store) => {
structRefs.refs[structRefs.i] = ref
stack[stack.length - 1] = r
// sort the set because this approach might bring the list out of order
structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.client - r2.id.client)
structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.clock - r2.id.clock)
structRefs.i = 0
continue
}
@@ -401,9 +401,10 @@ export const decodeStateVector = decodedState => readStateVector(decoding.create
export const writeDocumentStateVector = (encoder, doc) => {
encoding.writeVarUint(encoder, doc.store.clients.size)
doc.store.clients.forEach((structs, client) => {
const id = structs[structs.length - 1].id
const struct = structs[structs.length - 1]
const id = struct.id
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock)
encoding.writeVarUint(encoder, id.clock + struct.length)
})
return encoder
}
@@ -416,7 +417,7 @@ export const writeDocumentStateVector = (encoder, doc) => {
*
* @function
*/
export const encodeDocumentStateVector = doc => {
export const encodeStateVector = doc => {
const encoder = encoding.createEncoder()
writeDocumentStateVector(encoder, doc)
return encoding.toUint8Array(encoder)

View File

@@ -360,7 +360,7 @@ export const compareDS = (ds1, ds2) => {
/**
* @template T
* @param {t.TestCase} tc
* @param {Array<function(TestYInstance,prng.PRNG,T):void>} mods
* @param {Array<function(Y.Doc,prng.PRNG,T):void>} mods
* @param {number} iterations
* @param {InitTestObjectCallback<T>} [initTestObject]
*/

View File

@@ -25,10 +25,10 @@ export const testDeleteInsert = tc => {
*/
export const testInsertThreeElementsTryRegetProperty = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, 2, 3])
t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works')
array0.insert(0, [1, true, false])
t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
testConnector.flushAllMessages()
t.compare(array1.toJSON(), [1, 2, 3], '.toJSON() works after sync')
t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync')
compare(users)
}

View File

@@ -19,6 +19,8 @@ export const testBasicMapTests = tc => {
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
map0.set('y-map', new Y.Map())
map0.set('boolean1', true)
map0.set('boolean0', false)
const map = map0.get('y-map')
map.set('y-array', new Y.Array())
const array = map.get('y-array')
@@ -27,6 +29,8 @@ export const testBasicMapTests = tc => {
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
@@ -35,12 +39,16 @@ export const testBasicMapTests = tc => {
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
// compare disconnected user
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)')
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
compare(users)

View File

@@ -27,6 +27,13 @@ export const testBasicInsertAndDelete = tc => {
text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
users[0].transact(() => {
text0.insert(0, '1')
text0.delete(0, 1)
})
t.compare(delta, [])
compare(users)
}