Compare commits
15 Commits
v13.0.0-82
...
v13.0.0-86
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c14157dcf | ||
|
|
ef6c382e20 | ||
|
|
668e9e8a9b | ||
|
|
37a6d68543 | ||
|
|
f893198769 | ||
|
|
d3ee1a0ec2 | ||
|
|
d6593412a2 | ||
|
|
d31bf36531 | ||
|
|
a485f550db | ||
|
|
0610b16227 | ||
|
|
72e470c5f0 | ||
|
|
4d12a02e2f | ||
|
|
4a7d6f0a2d | ||
|
|
c80f446b5f | ||
|
|
81a529d8dc |
@@ -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<object|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<b><code>insert(index:number, content:Array<object|boolean|Array|string|number|Uint8Array|Y.Type>)</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<Object|Array|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<b><code>push(Array<Object|boolean|Array|string|number|Uint8Array|Y.Type>)</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<M></code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Object|Array|string|number|Uint8Array|Y.Type></code></b>
|
||||
<b><code>toArray():Array<object|boolean|Array|string|number|Uint8Array|Y.Type></code></b>
|
||||
<dd>Copies the content of this YArray to a new Array.</dd>
|
||||
<b><code>toJSON():Array<Object|Array|string|number></code></b>
|
||||
<b><code>toJSON():Array<Object|boolean|Array|string|number></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<string, Object|Array|string|number></code></b>
|
||||
<b><code>toJSON():Object<string, Object|boolean|Array|string|number></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
1
examples/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
build
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"codemirror": {
|
||||
"title": "CodeMirror Binding"
|
||||
},
|
||||
"prosemirror": {
|
||||
"title": "ProseMirror Binding"
|
||||
},
|
||||
"textarea": {
|
||||
"title": "Textarea Binding"
|
||||
},
|
||||
"quill": {
|
||||
"title": "Quill Binding"
|
||||
},
|
||||
"dom": {
|
||||
"title": "Dom Binding"
|
||||
}
|
||||
}
|
||||
@@ -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)])
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
1045
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -1,15 +1,14 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.0.0-82",
|
||||
"version": "13.0.0-86",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
}]
|
||||
|
||||
@@ -42,5 +42,5 @@ export {
|
||||
iterateDeletedStructs,
|
||||
applyUpdate,
|
||||
encodeStateAsUpdate,
|
||||
encodeDocumentStateVector
|
||||
encodeStateVector
|
||||
} from './internals.js'
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -101,7 +101,7 @@ export class AbstractType {
|
||||
* @private
|
||||
*/
|
||||
_copy () {
|
||||
throw new Error('unimplemented')
|
||||
throw error.methodUnimplemented()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,16 +287,13 @@ export const typeListCreateIterator = type => {
|
||||
while (n !== null && n.deleted) {
|
||||
n = n.right
|
||||
}
|
||||
}
|
||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||
if (n === null) {
|
||||
return {
|
||||
done: true,
|
||||
value: undefined
|
||||
// check if we reached the end, no need to check currentContent, because it does not exist
|
||||
if (n === null) {
|
||||
return {
|
||||
done: true,
|
||||
value: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
// currentContent could exist from the last iteration
|
||||
if (currentContent === null) {
|
||||
// we found n, so we can set currentContent
|
||||
currentContent = n.getContent()
|
||||
currentContentIndex = 0
|
||||
@@ -363,7 +360,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 +383,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 +508,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -265,6 +265,18 @@ export const compare = users => {
|
||||
t.assert(u.store.pendingStack.length === 0)
|
||||
t.assert(u.store.pendingClientsStructRefs.size === 0)
|
||||
}
|
||||
// Test Array iterator
|
||||
t.compare(userArrayValues[0], Array.from(users[0].getArray('array')).map(v => JSON.stringify(v)))
|
||||
// Test Map iterator
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
const mapRes = {}
|
||||
for (let [k, v] of users[0].getMap('map')) {
|
||||
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
|
||||
}
|
||||
t.compare(userMapValues[0], mapRes)
|
||||
// Compare all users
|
||||
for (let i = 0; i < users.length - 1; i++) {
|
||||
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
|
||||
t.compare(userArrayValues[i], userArrayValues[i + 1])
|
||||
@@ -360,7 +372,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]
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user