move repository to yjs org
This commit is contained in:
parent
bb1c0b809f
commit
6cb64b3707
@ -17,7 +17,7 @@
|
||||
"useCollapsibles": true,
|
||||
"collapse": true,
|
||||
"resources": {
|
||||
"y-js.org": "yjs.website"
|
||||
"yjs.dev": "Yjs website"
|
||||
},
|
||||
"logo": {
|
||||
"url": "https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png",
|
||||
|
305
README.v12.md
Normal file
305
README.v12.md
Normal file
@ -0,0 +1,305 @@
|
||||
|
||||
# 
|
||||
|
||||
Yjs is a framework for offline-first p2p shared editing on structured data like
|
||||
text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
|
||||
most of the complexity of concurrent editing. For additional information, demos,
|
||||
and tutorials visit [y-js.org](http://y-js.org/).
|
||||
|
||||
:warning: Checkout the [v13 docs](./README.md) for the upcoming release :warning:
|
||||
|
||||
### Extensions
|
||||
Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
|
||||
|
||||
* *Connector* - a communication protocol that propagates changes to the clients
|
||||
* *Database* - a database to store your changes
|
||||
* one or more *Types* - that represent the shared data
|
||||
|
||||
Connectors, Databases, and Types are available as modules that extend Yjs. Here
|
||||
is a list of the modules we know of:
|
||||
|
||||
##### Connectors
|
||||
|
||||
|Name | Description |
|
||||
|----------------|-----------------------------------|
|
||||
|[webrtc](https://github.com/y-js/y-webrtc) | Propagate updates Browser2Browser via WebRTC|
|
||||
|[websockets](https://github.com/y-js/y-websockets-client) | Set up [a central server](https://github.com/y-js/y-websockets-client), and connect to it via websockets |
|
||||
|[xmpp](https://github.com/y-js/y-xmpp) | Propagate updates in a XMPP multi-user-chat room ([XEP-0045](http://xmpp.org/extensions/xep-0045.html))|
|
||||
|[ipfs](https://github.com/ipfs-labs/y-ipfs-connector) | Connector for the [Interplanetary File System](https://ipfs.io/)!|
|
||||
|[test](https://github.com/y-js/y-test) | A Connector for testing purposes. It is designed to simulate delays that happen in worst case scenarios|
|
||||
|
||||
##### Database adapters
|
||||
|
||||
|Name | Description |
|
||||
|----------------|-----------------------------------|
|
||||
|[memory](https://github.com/y-js/y-memory) | In-memory storage. |
|
||||
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|
||||
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
|
||||
|
||||
##### Types
|
||||
|
||||
| Name | Description |
|
||||
|----------|-------------------|
|
||||
|[map](https://github.com/y-js/y-map) | A shared Map implementation. Maps from text to any stringify-able object |
|
||||
|[array](https://github.com/y-js/y-array) | A shared Array implementation |
|
||||
|[xml](https://github.com/y-js/y-xml) | An implementation of the DOM. You can create a two way binding to Browser DOM objects |
|
||||
|[text](https://github.com/y-js/y-text) | Collaborate on text. Supports two way binding to the [Ace Editor](https://ace.c9.io), [CodeMirror](https://codemirror.net/), [Monaco](https://github.com/Microsoft/monaco-editor), textareas, input elements, and HTML elements (e.g. <*h1*>, or <*p*>) |
|
||||
|[richtext](https://github.com/y-js/y-richtext) | Collaborate on rich text. Supports two way binding to the [Quill Rich Text Editor](http://quilljs.com/)|
|
||||
|
||||
##### Other
|
||||
|
||||
| Name | Description |
|
||||
|-----------|-------------------|
|
||||
|[y-element](http://y-js.org/y-element/) | Yjs Polymer Element |
|
||||
|
||||
## Use it!
|
||||
Install Yjs, and its modules with [bower](http://bower.io/), or
|
||||
[npm](https://www.npmjs.org/package/yjs).
|
||||
|
||||
### Bower
|
||||
|
||||
```
|
||||
bower install --save yjs y-array % add all y-* modules you want to use
|
||||
```
|
||||
You only need to include the `y.js` file. Yjs is able to automatically require
|
||||
missing modules.
|
||||
```
|
||||
<script src="./bower_components/yjs/y.js"></script>
|
||||
```
|
||||
|
||||
### CDN
|
||||
|
||||
```
|
||||
<script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
|
||||
// ..
|
||||
// do the same for all modules you want to use
|
||||
```
|
||||
|
||||
### Npm
|
||||
|
||||
```
|
||||
npm install --save yjs % add all y-* modules you want to use
|
||||
```
|
||||
|
||||
If you don't include via script tag, you have to explicitly include all modules!
|
||||
(Same goes for other module systems)
|
||||
```
|
||||
var Y = require('yjs')
|
||||
require('y-array')(Y) // add the y-array type to Yjs
|
||||
require('y-websockets-client')(Y)
|
||||
require('y-memory')(Y)
|
||||
require('y-map')(Y)
|
||||
require('y-text')(Y)
|
||||
// ..
|
||||
// do the same for all modules you want to use
|
||||
```
|
||||
|
||||
### ES6 Syntax
|
||||
|
||||
```
|
||||
import Y from 'yjs'
|
||||
import yArray from 'y-array'
|
||||
import yWebsocketsClient from 'y-webrtc'
|
||||
import yMemory from 'y-memory'
|
||||
import yMap from 'y-map'
|
||||
import yText from 'y-text'
|
||||
// ..
|
||||
Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
|
||||
```
|
||||
|
||||
# Text editing example
|
||||
|
||||
Install dependencies
|
||||
```
|
||||
bower i yjs y-memory y-webrtc y-array y-text
|
||||
```
|
||||
|
||||
Here is a simple example of a shared textarea
|
||||
```HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<script src="./bower_components/yjs/y.js"></script>
|
||||
<!-- Yjs automatically includes all missing dependencies (browser only) -->
|
||||
<script>
|
||||
Y({
|
||||
db: {
|
||||
name: 'memory' // use memory database adapter.
|
||||
// name: 'indexeddb' // use indexeddb database adapter instead for offline apps
|
||||
},
|
||||
connector: {
|
||||
name: 'webrtc', // use webrtc connector
|
||||
// name: 'websockets-client'
|
||||
// name: 'xmpp'
|
||||
room: 'my-room' // clients connecting to the same room share data
|
||||
},
|
||||
sourceDir: './bower_components', // location of the y-* modules (browser only)
|
||||
share: {
|
||||
textarea: 'Text' // y.share.textarea is of type y-text
|
||||
}
|
||||
}).then(function (y) {
|
||||
// The Yjs instance `y` is available
|
||||
// y.share.* contains the shared types
|
||||
|
||||
// Bind `y.share.textarea` to `<textarea/>`
|
||||
y.share.textarea.bind(document.querySelector('textarea'))
|
||||
})
|
||||
</script>
|
||||
<textarea></textarea>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Get Help & Give Help
|
||||
There are some friendly people on [](https://gitter.im/y-js/yjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) who are eager to help, and answer questions. Please join!
|
||||
|
||||
Report _any_ issues to the
|
||||
[Github issue page](https://github.com/y-js/yjs/issues)! I try to fix them very
|
||||
soon, if possible.
|
||||
|
||||
# API
|
||||
|
||||
### Y(options)
|
||||
* Y.extend(module1, module2, ..)
|
||||
* Add extensions to Y
|
||||
* `Y.extend(require('y-webrtc'))` has the same semantics as
|
||||
`require('y-webrtc')(Y)`
|
||||
* options.db
|
||||
* Will be forwarded to the database adapter. Specify the database adaper on
|
||||
`options.db.name`.
|
||||
* Have a look at the used database adapter repository to see all available
|
||||
options.
|
||||
* options.connector
|
||||
* Will be forwarded to the connector adapter. Specify the connector adaper on
|
||||
`options.connector.name`.
|
||||
* All our connectors implement a `room` property. Clients that specify the
|
||||
same room share the same data.
|
||||
* All of our connectors specify an `url` property that defines the connection
|
||||
endpoint of the used connector.
|
||||
* All of our connectors also have a default connection endpoint that you can
|
||||
use for development.
|
||||
* Set `options.connector.generateUserId = true` in order to genenerate a
|
||||
userid, instead of receiving one from the server. This way the `Y(..)` is
|
||||
immediately going to be resolved, without waiting for any confirmation from
|
||||
the server. Use with caution.
|
||||
* Have a look at the used connector repository to see all available options.
|
||||
* *Only if you know what you are doing:* Set
|
||||
`options.connector.preferUntransformed = true` in order receive the shared
|
||||
data untransformed. This is very efficient as the database content is simply
|
||||
copied to this client. This does only work if this client receives content
|
||||
from only one client.
|
||||
* options.sourceDir (browser only)
|
||||
* Path where all y-* modules are stored
|
||||
* Defaults to `/bower_components`
|
||||
* Not required when running on `nodejs` / `iojs`
|
||||
* When using nodejs you need to manually extend Yjs:
|
||||
```
|
||||
var Y = require('yjs')
|
||||
// you have to require a db, connector, and *all* types you use!
|
||||
require('y-memory')(Y)
|
||||
require('y-webrtc')(Y)
|
||||
require('y-map')(Y)
|
||||
// ..
|
||||
```
|
||||
* options.share
|
||||
* Specify on `options.share[arbitraryName]` types that are shared among all
|
||||
users.
|
||||
* E.g. Specify `options.share[arbitraryName] = 'Array'` to require y-array and
|
||||
create an y-array type on `y.share[arbitraryName]`.
|
||||
* If userA doesn't specify `options.share[arbitraryName]`, it won't be
|
||||
available for userA.
|
||||
* If userB specifies `options.share[arbitraryName]`, it still won't be
|
||||
available for userA. But all the updates are send from userB to userA.
|
||||
* In contrast to y-map, types on `y.share.*` cannot be overwritten or deleted.
|
||||
Instead, they are merged among all users. This feature is only available on
|
||||
`y.share.*`
|
||||
* Weird behavior: It is supported that two users specify different types with
|
||||
the same property name.
|
||||
E.g. userA specifies `options.share.x = 'Array'`, and userB specifies
|
||||
`options.share.x = 'Text'`. But they only share data if they specified the
|
||||
same type with the same property name
|
||||
* options.type (browser only)
|
||||
* Array of modules that Yjs needs to require, before instantiating a shared
|
||||
type.
|
||||
* By default Yjs requires the specified database adapter, the specified
|
||||
connector, and all modules that are used in `options.share.*`
|
||||
* Put all types here that you intend to use, but are not used in y.share.*
|
||||
|
||||
### Instantiated Y object (y)
|
||||
`Y(options)` returns a promise that is fulfilled when..
|
||||
|
||||
* All modules are loaded
|
||||
* The specified database adapter is loaded
|
||||
* The specified connector is loaded
|
||||
* All types are included
|
||||
* The connector is initialized, and a unique user id is set (received from the
|
||||
server)
|
||||
* Note: When using y-indexeddb, a retrieved user id is stored on `localStorage`
|
||||
|
||||
The promise returns an instance of Y. We denote it with a lower case `y`.
|
||||
|
||||
* y.share.*
|
||||
* Instances of the types you specified on options.share.*
|
||||
* y.share.* can only be defined once when you instantiate Y!
|
||||
* y.connector is an instance of Y.AbstractConnector
|
||||
* y.connector.onUserEvent(function (event) {..})
|
||||
* Observe user events (event.action is either 'userLeft' or 'userJoined')
|
||||
* y.connector.whenSynced(listener)
|
||||
* `listener` is executed when y synced with at least one user.
|
||||
* `listener` is not called when no other user is in the same room.
|
||||
* y-websockets-client aways waits to sync with the server
|
||||
* y.connector.disconnect()
|
||||
* Force to disconnect this instance from the other instances
|
||||
* y.connector.connect()
|
||||
* Try to reconnect to the other instances (needs to be supported by the
|
||||
connector)
|
||||
* Not supported by y-xmpp
|
||||
* y.close()
|
||||
* Destroy this object.
|
||||
* Destroys all types (they will throw weird errors if you still use them)
|
||||
* Disconnects from the other instances (via connector)
|
||||
* Returns a promise
|
||||
* y.destroy()
|
||||
* calls y.close()
|
||||
* Removes all data from the database
|
||||
* Returns a promise
|
||||
* y.db.stopGarbageCollector()
|
||||
* Stop the garbage collector. Call y.db.garbageCollect() to continue garbage
|
||||
collection
|
||||
* y.db.gc :: Boolean
|
||||
* Whether gc is turned on
|
||||
* y.db.gcTimeout :: Number (defaults to 50000 ms)
|
||||
* Time interval between two garbage collect cycles
|
||||
* It is required that all instances exchanged all messages after two garbage
|
||||
collect cycles (after 100000 ms per default)
|
||||
* y.db.userId :: String
|
||||
* The used user id for this client. **Never overwrite this**
|
||||
|
||||
### Logging
|
||||
Yjs uses [debug](https://github.com/visionmedia/debug) for logging. The flag
|
||||
`y*` enables logging for all y-* components. You can selectively remove
|
||||
components you are not interested in: E.g. The flag `y*,-y:connector-message`
|
||||
will not log the long `y:connector-message` messages.
|
||||
|
||||
##### Enable logging in Node.js
|
||||
```sh
|
||||
DEBUG=y* node app.js
|
||||
```
|
||||
|
||||
Remove the colors in order to log to a file:
|
||||
```sh
|
||||
DEBUG_COLORS=0 DEBUG=y* node app.js > log
|
||||
```
|
||||
|
||||
##### Enable logging in the browser
|
||||
```js
|
||||
localStorage.debug = 'y*'
|
||||
```
|
||||
|
||||
## License
|
||||
Yjs is licensed under the [MIT License](./LICENSE).
|
874
README.v13.md
874
README.v13.md
@ -1,874 +0,0 @@
|
||||
|
||||
# 
|
||||
|
||||
> A CRDT framework with a powerful abstraction of shared data
|
||||
|
||||
Yjs is a [CRDT implementation](#Yjs-CRDT-Algorithm) that exposes its internal
|
||||
data structure as *shared types*. Shared types are common data types like `Map`
|
||||
or `Array` with superpowers: changes are automatically distributed to other
|
||||
peers and merged without merge conflicts.
|
||||
|
||||
Yjs is **network agnostic** (p2p!), supports many existing **rich text
|
||||
editors**, **offline editing**, **version snapshots**, **undo/redo** and
|
||||
**shared cursors**. It scales well with an unlimited number of users and is well
|
||||
suited for even large documents.
|
||||
|
||||
* Chat: [https://gitter.im/y-js/yjs](https://gitter.im/y-js/yjs)
|
||||
* Demos: [https://github.com/y-js/yjs-demos](https://github.com/y-js/yjs-demos)
|
||||
* Benchmarks: [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
* [Overview](#Overview)
|
||||
* [Bindings](#Bindings)
|
||||
* [Providers](#Providers)
|
||||
* [Getting Started](#Getting-Started)
|
||||
* [API](#API)
|
||||
* [Shared Types](#Shared-Types)
|
||||
* [Y.Doc](#YDoc)
|
||||
* [Document Updates](#Document-Updates)
|
||||
* [Relative Positions](#Relative-Positions)
|
||||
* [Y.UndoManager](#YUndoManager)
|
||||
* [Miscellaneous](#Miscellaneous)
|
||||
* [Typescript Declarations](#Typescript-Declarations)
|
||||
* [Yjs CRDT Algorithm](#Yjs-CRDT-Algorithm)
|
||||
* [Evaluation](#Evaluation)
|
||||
* [Existing shared editing libraries](#Exisisting-Javascript-Libraries)
|
||||
* [CRDT Algorithms](#CRDT-Algorithms)
|
||||
* [Comparison of CRDT with OT](#Comparing-CRDT-with-OT)
|
||||
* [Comparison of CRDT Algorithms](#Comparing-CRDT-Algorithms)
|
||||
* [Comparison of Yjs with other Implementations](#Comparing-Yjs-with-other-Implementations)
|
||||
* [License and Author](#License-and-Author)
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains a collection of shared types that can be observed for
|
||||
changes and manipulated concurrently. Network functionality and two-way-bindings
|
||||
are implemented in separate modules.
|
||||
|
||||
### Bindings
|
||||
|
||||
| Name | Cursors | Binding | Demo |
|
||||
|---|:-:|---|---|
|
||||
| [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
|
||||
|
||||
Setting up the communication between clients, managing awareness information,
|
||||
and storing shared data for offline usage is quite a hassle. **Providers**
|
||||
manage all that for you and are the perfect starting point for your
|
||||
collaborative app.
|
||||
|
||||
<dl>
|
||||
<dt><a href="http://github.com/y-js/y-websocket">y-websocket</a></dt>
|
||||
<dd>
|
||||
A module that contains a simple websocket backend and a websocket client that
|
||||
connects to that backend. The backend can be extended to persist updates in a
|
||||
leveldb database.
|
||||
</dd>
|
||||
<dt><a href="http://github.com/y-js/y-mesh">y-mesh</a></dt>
|
||||
<dd>
|
||||
[WIP] Creates a connected graph of webrtc connections with a high
|
||||
<a href="https://en.wikipedia.org/wiki/Strength_of_a_graph">strength</a>. It
|
||||
requires a signalling server that connects a client to the first peer. But after
|
||||
that the network manages itself. It is well suited for large and small networks.
|
||||
</dd>
|
||||
<dt><a href="http://github.com/y-js/y-dat">y-dat</a></dt>
|
||||
<dd>
|
||||
[WIP] Write document updates effinciently to the dat network using
|
||||
<a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has
|
||||
an append-only log of CRDT local updates (hypercore). Multifeed manages and sync
|
||||
hypercores and y-dat listens to changes and applies them to the Yjs document.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install Yjs and a provider with your favorite package manager:
|
||||
|
||||
```sh
|
||||
npm i yjs@13.0.0-82 y-websocket@1.0.0-3 y-textarea
|
||||
```
|
||||
|
||||
Start the y-websocket server:
|
||||
|
||||
```sh
|
||||
PORT=1234 node ./node_modules/y-websocket/bin/server.js
|
||||
```
|
||||
|
||||
### Example: Textarea Binding
|
||||
|
||||
This is a complete example on how to create a connection to a
|
||||
[y-websocket](https://github.com/y-js/y-websocket) server instance, sync the
|
||||
shared document to all clients in a *room*, and bind a Y.Text type to a dom
|
||||
textarea. All changes to the textarea are automatically shared with everyone in
|
||||
the same room.
|
||||
|
||||
```js
|
||||
import * as Y from 'yjs'
|
||||
import { WebsocketProvider } from 'y-websocket'
|
||||
import { TextareaBinding } from 'y-textarea'
|
||||
|
||||
const doc = Y.Doc()
|
||||
const provider = new WebsocketProvider('ws://localhost:1234', 'roomname', doc)
|
||||
|
||||
// Define a shared type on the document.
|
||||
const ytext = doc.getText('my resume')
|
||||
|
||||
// use data bindings to bind types to editors
|
||||
const binding = new TextareaBinding(ytext, document.querySelector('textarea'))
|
||||
```
|
||||
|
||||
#### Example: Observe types
|
||||
|
||||
```js
|
||||
const yarray = doc.getArray('my-array')
|
||||
yarray.observe(event => {
|
||||
console.log('yarray was modified')
|
||||
})
|
||||
// every time a local or remote client modifies yarray, the observer is called
|
||||
yarray.insert(0, ['val']) // => "yarray was modified"
|
||||
```
|
||||
|
||||
#### Example: Nest types
|
||||
|
||||
Remember, shared types are just plain old data types. The only limitation is
|
||||
that a shared type must exist only once in the shared document.
|
||||
|
||||
```js
|
||||
const ymap = doc.getMap('map')
|
||||
const foodArray = new Y.Array()
|
||||
foodArray.insert(0, ['apple', 'banana'])
|
||||
ymap.set('food', foodArray)
|
||||
ymap.get('food') === foodArray // => true
|
||||
ymap.set('fruit', foodArray) // => Error! foodArray is already defined
|
||||
```
|
||||
|
||||
Now you understand how types are defined on a shared document. Next you can jump
|
||||
to the [demo repository](https://github.com/y-js/yjs-demos) or continue reading
|
||||
the API docs.
|
||||
|
||||
## API
|
||||
|
||||
```js
|
||||
import * as Y from 'yjs'
|
||||
```
|
||||
|
||||
### Shared Types
|
||||
|
||||
<details>
|
||||
<summary><b>Y.Array</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable Array-like type that supports efficient insert/delete of elements
|
||||
at any position. Internally it uses a linked list of Arrays that is split when
|
||||
necessary.
|
||||
</p>
|
||||
<pre>const yarray = new Y.Array()</pre>
|
||||
<dl>
|
||||
<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|boolean|Array|string|number|Uint8Array|Y.Type>)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b>
|
||||
<code>
|
||||
forEach(function(value:object|boolean|Array|string|number|Uint8Array|Y.Type,
|
||||
index:number, array: Y.Array))
|
||||
</code>
|
||||
</b>
|
||||
<dd></dd>
|
||||
<b><code>map(function(T, number, YArray):M):Array<M></code></b>
|
||||
<dd></dd>
|
||||
<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|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>
|
||||
Returns an YArray Iterator that contains the values for each index in the array.
|
||||
<pre>for (let value of yarray) { .. }</pre>
|
||||
</dd>
|
||||
<b><code>observe(function(YArrayEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type is modified. In the case this type is modified in the event listener,
|
||||
the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YArrayEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type or any of its children is modified. In the case this type is modified
|
||||
in the event listener, the event listener will be called again after the current
|
||||
event listener returns. The event listener receives all Events created by itself
|
||||
or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
<details>
|
||||
<summary><b>Y.Map</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable Map type.
|
||||
</p>
|
||||
<pre><code>const ymap = new Y.Map()</code></pre>
|
||||
<dl>
|
||||
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
||||
<dd></dd>
|
||||
<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>
|
||||
<b><code>has(key:string):boolean</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array></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>forEach(function(key:string,value:object|boolean|Array|string|number|Uint8Array|Y.Type))</code></b>
|
||||
<dd>
|
||||
Execute the provided function once for every key-value pair.
|
||||
</dd>
|
||||
<b><code>[Symbol.Iterator]</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of <code>[key, value]</code> pairs.
|
||||
<pre>for (let [key, value] of ymap) { .. }</pre>
|
||||
</dd>
|
||||
<b><code>entries()</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of <code>[key, value]</code> pairs.
|
||||
</dd>
|
||||
<b><code>values()</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of all values.
|
||||
</dd>
|
||||
<b><code>keys()</code></b>
|
||||
<dd>
|
||||
Returns an Iterator of all keys.
|
||||
</dd>
|
||||
<b><code>observe(function(YMapEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type is modified. In the case this type is modified in the event listener,
|
||||
the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YMapEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type or any of its children is modified. In the case this type is modified
|
||||
in the event listener, the event listener will be called again after the current
|
||||
event listener returns. The event listener receives all Events created by itself
|
||||
or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Y.Text</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable type that is optimized for shared editing on text. It allows to
|
||||
assign properties to ranges in the text. This makes it possible to implement
|
||||
rich-text bindings to this type.
|
||||
</p>
|
||||
<p>
|
||||
This type can also be transformed to the
|
||||
<a href="https://quilljs.com/docs/delta">delta format</a>. Similarly the
|
||||
YTextEvents compute changes as deltas.
|
||||
</p>
|
||||
<pre>const ytext = new Y.Text()</pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:string, [formattingAttributes:Object<string,string>])</code></b>
|
||||
<dd>
|
||||
Insert a string at <var>index</var> and assign formatting attributes to it.
|
||||
<pre>ytext.insert(0, 'bold text', { bold: true })</pre>
|
||||
</dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>format(index:number, length:number, formattingAttributes:Object<string,string>)</code></b>
|
||||
<dd>Assign formatting attributes to a range in the text</dd>
|
||||
<b><code>applyDelta(delta)</code></b>
|
||||
<dd>See <a href="https://quilljs.com/docs/delta/">Quill Delta</a></dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>toString():string</code></b>
|
||||
<dd>Transforms this type, without formatting options, into a string.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code></dd>
|
||||
<b><code>toDelta():Delta</code></b>
|
||||
<dd>
|
||||
Transforms this type to a <a href="https://quilljs.com/docs/delta/">Quill Delta</a>
|
||||
</dd>
|
||||
<b><code>observe(function(YTextEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type is modified. In the case this type is modified in the event listener,
|
||||
the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YTextEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type or any of its children is modified. In the case this type is modified
|
||||
in the event listener, the event listener will be called again after the current
|
||||
event listener returns. The event listener receives all Events created by itself
|
||||
or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>YXmlFragment</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A container that holds an Array of Y.XmlElements.
|
||||
</p>
|
||||
<pre><code>const yxml = new Y.XmlFragment()</code></pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||
<dd>Copies the children to a new Array.</dd>
|
||||
<b><code>toDOM():DocumentFragment</code></b>
|
||||
<dd>Transforms this type and all children to new DOM elements.</dd>
|
||||
<b><code>toString():string</code></b>
|
||||
<dd>Get the XML serialization of all descendants.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code>.</dd>
|
||||
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type is modified. In the case this type is modified in the event listener,
|
||||
the event listener will be called again after the current event listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YXmlEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type or any of its children is modified. In the case this type is modified
|
||||
in the event listener, the event listener will be called again after the current
|
||||
event listener returns. The event listener receives all Events created by itself
|
||||
or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Y.XmlElement</b></summary>
|
||||
<br>
|
||||
<p>
|
||||
A shareable type that represents an XML Element. It has a <code>nodeName</code>,
|
||||
attributes, and a list of children. But it makes no effort to validate its
|
||||
content and be actually XML compliant.
|
||||
</p>
|
||||
<pre><code>const yxml = new Y.XmlElement()</code></pre>
|
||||
<dl>
|
||||
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>delete(index:number, length:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>get(index:number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>length:number</code></b>
|
||||
<dd></dd>
|
||||
<b><code>setAttribute(attributeName:string, attributeValue:string)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>removeAttribute(attributeName:string)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>getAttribute(attributeName:string):string</code></b>
|
||||
<dd></dd>
|
||||
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
||||
<dd></dd>
|
||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||
<dd>Copies the children to a new Array.</dd>
|
||||
<b><code>toDOM():Element</code></b>
|
||||
<dd>Transforms this type and all children to a new DOM element.</dd>
|
||||
<b><code>toString():string</code></b>
|
||||
<dd>Get the XML serialization of all descendants.</dd>
|
||||
<b><code>toJSON():string</code></b>
|
||||
<dd>See <code>toString</code>.</dd>
|
||||
<b><code>observe(function(YXmlEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every
|
||||
time this type is modified. In the case this type is modified in the event
|
||||
listener, the event listener will be called again after the current event
|
||||
listener returns.
|
||||
</dd>
|
||||
<b><code>unobserve(function(YXmlEvent, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observe</code> event listener from this type.
|
||||
</dd>
|
||||
<b><code>observeDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Adds an event listener to this type that will be called synchronously every time
|
||||
this type or any of its children is modified. In the case this type is modified
|
||||
in the event listener, the event listener will be called again after the current
|
||||
event listener returns. The event listener receives all Events created by itself
|
||||
or any of its children.
|
||||
</dd>
|
||||
<b><code>unobserveDeep(function(Array<YEvent>, Transaction):void)</code></b>
|
||||
<dd>
|
||||
Removes an <code>observeDeep</code> event listener from this type.
|
||||
</dd>
|
||||
</dl>
|
||||
</details>
|
||||
|
||||
### Y.Doc
|
||||
|
||||
```js
|
||||
const doc = new Y.Doc()
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>clientID</code></b>
|
||||
<dd>A unique id that identifies this client. (readonly)</dd>
|
||||
<b><code>transact(function(Transaction):void [, origin:any])</code></b>
|
||||
<dd>
|
||||
Every change on the shared document happens in a transaction. Observer calls and
|
||||
the <code>update</code> event are called after each transaction. You should
|
||||
<i>bundle</i> changes into a single transaction to reduce the amount of event
|
||||
calls. I.e. <code>doc.transact(() => { yarray.insert(..); ymap.set(..) })</code>
|
||||
triggers a single change event. <br>You can specify an optional <code>origin</code>
|
||||
parameter that is stored on <code>transaction.origin</code> and
|
||||
<code>on('update', (update, origin) => ..)</code>.
|
||||
</dd>
|
||||
<b><code>get(string, Y.[TypeClass]):[Type]</code></b>
|
||||
<dd>Define a shared type.</dd>
|
||||
<b><code>getArray(string):Y.Array</code></b>
|
||||
<dd>Define a shared Y.Array type. Is equivalent to <code>y.get(string, Y.Array)</code>.</dd>
|
||||
<b><code>getMap(string):Y.Map</code></b>
|
||||
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
|
||||
<b><code>getXmlFragment(string):Y.XmlFragment</code></b>
|
||||
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
|
||||
<b><code>on(string, function)</code></b>
|
||||
<dd>Register an event listener on the shared type</dd>
|
||||
<b><code>off(string, function)</code></b>
|
||||
<dd>Unregister an event listener from the shared type</dd>
|
||||
</dl>
|
||||
|
||||
#### Y.Doc Events
|
||||
|
||||
<dl>
|
||||
<b><code>on('update', function(updateMessage:Uint8Array, origin:any, Y.Doc):void)</code></b>
|
||||
<dd>
|
||||
Listen to document updates. Document updates must be transmitted to all other
|
||||
peers. You can apply document updates in any order and multiple times.
|
||||
</dd>
|
||||
<b><code>on('beforeTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
|
||||
<dd>Emitted before each transaction.</dd>
|
||||
<b><code>on('afterTransaction', function(Y.Transaction, Y.Doc):void)</code></b>
|
||||
<dd>Emitted after each transaction.</dd>
|
||||
</dl>
|
||||
|
||||
### Document Updates
|
||||
|
||||
Changes on the shared document are encoded into *document updates*. Document
|
||||
updates are *commutative* and *idempotent*. This means that they can be applied
|
||||
in any order and multiple times.
|
||||
|
||||
#### Example: Listen to update events and apply them on remote client
|
||||
|
||||
```js
|
||||
const doc1 = new Y.Doc()
|
||||
const doc2 = new Y.Doc()
|
||||
|
||||
doc1.on('update', update => {
|
||||
Y.applyUpdate(doc2, update)
|
||||
})
|
||||
|
||||
doc2.on('update', update => {
|
||||
Y.applyUpdate(doc1, update)
|
||||
})
|
||||
|
||||
// All changes are also applied to the other document
|
||||
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.
|
||||
|
||||
#### Example: Sync two clients by exchanging the complete document structure
|
||||
|
||||
```js
|
||||
const state1 = Y.encodeStateAsUpdate(ydoc1)
|
||||
const state2 = Y.encodeStateAsUpdate(ydoc2)
|
||||
Y.applyUpdate(ydoc1, state2)
|
||||
Y.applyUpdate(ydoc2, state1)
|
||||
```
|
||||
|
||||
#### Example: Sync two clients by computing the differences
|
||||
|
||||
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.encodeStateVector(ydoc1)
|
||||
const stateVector2 = Y.encodeStateVector(ydoc2)
|
||||
const diff1 = Y.encodeStateAsUpdate(ydoc1, stateVector2)
|
||||
const diff2 = Y.encodeStateAsUpdate(ydoc2, stateVector1)
|
||||
Y.applyUpdate(ydoc1, diff2)
|
||||
Y.applyUpdate(ydoc2, diff1)
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>Y.applyUpdate(Y.Doc, update:Uint8Array, [transactionOrigin:any])</code></b>
|
||||
<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.encodeStateVector(Y.Doc):Uint8Array</code></b>
|
||||
<dd>Computes the state vector and encodes it into an Uint8Array.</dd>
|
||||
</dl>
|
||||
|
||||
### Relative Positions
|
||||
|
||||
> This API is not stable yet
|
||||
|
||||
This feature is intended for managing selections / cursors. When working with
|
||||
other users that manipulate the shared document, you can't trust that an index
|
||||
position (an integer) will stay at the intended location. A *relative position*
|
||||
is fixated to an element in the shared document and is not affected by remote
|
||||
changes. I.e. given the document `"a|c"`, the relative position is attached to
|
||||
`c`. When a remote user modifies the document by inserting a character before
|
||||
the cursor, the cursor will stay attached to the character `c`. `insert(1,
|
||||
'x')("a|c") = "ax|c"`. When the *relative position* is set to the end of the
|
||||
document, it will stay attached to the end of the document.
|
||||
|
||||
#### Example: Transform to RelativePosition and back
|
||||
|
||||
```js
|
||||
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
|
||||
const pos = Y.createAbsolutePositionFromRelativePosition(relPos, doc)
|
||||
pos.type === ytext // => true
|
||||
pos.index === 2 // => true
|
||||
```
|
||||
|
||||
#### Example: Send relative position to remote client (json)
|
||||
|
||||
```js
|
||||
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
|
||||
const encodedRelPos = JSON.stringify(relPos)
|
||||
// send encodedRelPos to remote client..
|
||||
const parsedRelPos = JSON.parse(encodedRelPos)
|
||||
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
|
||||
pos.type === remoteytext // => true
|
||||
pos.index === 2 // => true
|
||||
```
|
||||
|
||||
#### Example: Send relative position to remote client (Uint8Array)
|
||||
|
||||
```js
|
||||
const relPos = Y.createRelativePositionFromTypeIndex(ytext, 2)
|
||||
const encodedRelPos = Y.encodeRelativePosition(relPos)
|
||||
// send encodedRelPos to remote client..
|
||||
const parsedRelPos = Y.decodeRelativePosition(encodedRelPos)
|
||||
const pos = Y.createAbsolutePositionFromRelativePosition(parsedRelPos, remoteDoc)
|
||||
pos.type === remoteytext // => true
|
||||
pos.index === 2 // => true
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc)</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.encodeRelativePosition(RelativePosition):Uint8Array</code></b>
|
||||
<dd></dd>
|
||||
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
|
||||
<dd></dd>
|
||||
</dl>
|
||||
|
||||
### Y.UndoManager
|
||||
|
||||
Yjs ships with an Undo/Redo manager for selective undo/redo of of changes on a
|
||||
Yjs type. The changes can be optionally scoped to transaction origins.
|
||||
|
||||
```js
|
||||
const ytext = doc.getArray('array')
|
||||
const undoManager = new Y.UndoManager(ytext)
|
||||
|
||||
ytext.insert(0, 'abc')
|
||||
undoManager.undo()
|
||||
ytext.toString() // => ''
|
||||
undoManager.redo()
|
||||
ytext.toString() // => 'abc'
|
||||
```
|
||||
|
||||
<dl>
|
||||
<b><code>constructor(scope:Y.AbstractType|Array<Y.AbstractType>,
|
||||
[[{captureTimeout:number,trackedOrigins:Set<any>,deleteFilter:function(item):boolean}]])</code></b>
|
||||
<dd>Accepts either single type as scope or an array of types.</dd>
|
||||
<b><code>undo()</code></b>
|
||||
<dd></dd>
|
||||
<b><code>redo()</code></b>
|
||||
<dd></dd>
|
||||
<b><code>stopCapturing()</code></b>
|
||||
<dd></dd>
|
||||
<b>
|
||||
<code>
|
||||
on('stack-item-added', { stackItem: { meta: Map<any,any> }, type: 'undo'
|
||||
| 'redo' })
|
||||
</code>
|
||||
</b>
|
||||
<dd>
|
||||
Register an event that is called when a <code>StackItem</code> is added to the
|
||||
undo- or the redo-stack.
|
||||
</dd>
|
||||
<b>
|
||||
<code>
|
||||
on('stack-item-popped', { stackItem: { meta: Map<any,any> }, type: 'undo'
|
||||
| 'redo' })
|
||||
</code>
|
||||
</b>
|
||||
<dd>
|
||||
Register an event that is called when a <code>StackItem</code> is popped from
|
||||
the undo- or the redo-stack.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
#### Example: Stop Capturing
|
||||
|
||||
UndoManager merges Undo-StackItems if they are created within time-gap
|
||||
smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
|
||||
StackItem won't be merged.
|
||||
|
||||
```js
|
||||
// without stopCapturing
|
||||
ytext.insert(0, 'a')
|
||||
ytext.insert(1, 'b')
|
||||
um.undo()
|
||||
ytext.toString() // => '' (note that 'ab' was removed)
|
||||
// with stopCapturing
|
||||
ytext.insert(0, 'a')
|
||||
um.stopCapturing()
|
||||
ytext.insert(0, 'b')
|
||||
um.undo()
|
||||
ytext.toString() // => 'a' (note that only 'b' was removed)
|
||||
```
|
||||
|
||||
#### Example: Specify tracked origins
|
||||
|
||||
Every change on the shared document has an origin. If no origin was specified,
|
||||
it defaults to `null`. By specifying `trackedTransactionOrigins` you can
|
||||
selectively specify which changes should be tracked by `UndoManager`. The
|
||||
UndoManager instance is always added to `trackedTransactionOrigins`.
|
||||
|
||||
```js
|
||||
class CustomBinding {}
|
||||
|
||||
const ytext = doc.getArray('array')
|
||||
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
|
||||
|
||||
ytext.insert(0, 'abc')
|
||||
undoManager.undo()
|
||||
ytext.toString() // => 'abc' (does not track because origin `null` and not part
|
||||
// of `trackedTransactionOrigins`)
|
||||
ytext.delete(0, 3) // revert change
|
||||
|
||||
doc.transact(() => {
|
||||
ytext.insert(0, 'abc')
|
||||
}, 42)
|
||||
undoManager.undo()
|
||||
ytext.toString() // => '' (tracked because origin is an instance of `trackedTransactionorigins`)
|
||||
|
||||
doc.transact(() => {
|
||||
ytext.insert(0, 'abc')
|
||||
}, 41)
|
||||
undoManager.undo()
|
||||
ytext.toString() // => '' (not tracked because 41 is not an instance of
|
||||
// `trackedTransactionorigins`)
|
||||
ytext.delete(0, 3) // revert change
|
||||
|
||||
doc.transact(() => {
|
||||
ytext.insert(0, 'abc')
|
||||
}, new CustomBinding())
|
||||
undoManager.undo()
|
||||
ytext.toString() // => '' (tracked because origin is a `CustomBinding` and
|
||||
// `CustomBinding` is in `trackedTransactionorigins`)
|
||||
```
|
||||
|
||||
#### Example: Add additional information to the StackItems
|
||||
|
||||
When undoing or redoing a previous action, it is often expected to restore
|
||||
additional meta information like the cursor location or the view on the
|
||||
document. You can assign meta-information to Undo-/Redo-StackItems.
|
||||
|
||||
```js
|
||||
const ytext = doc.getArray('array')
|
||||
const undoManager = new Y.UndoManager(ytext, new Set([42, CustomBinding]))
|
||||
|
||||
undoManager.on('stack-item-added', event => {
|
||||
// save the current cursor location on the stack-item
|
||||
event.stackItem.meta.set('cursor-location', getRelativeCursorLocation())
|
||||
})
|
||||
|
||||
undoManager.on('stack-item-popped', event => {
|
||||
// restore the current cursor location on the stack-item
|
||||
restoreCursorLocation(event.stackItem.meta.get('cursor-location'))
|
||||
})
|
||||
```
|
||||
|
||||
## Miscellaneous
|
||||
|
||||
### Typescript Declarations
|
||||
|
||||
Yjs has type descriptions. But until [this
|
||||
ticket](https://github.com/Microsoft/TypeScript/issues/7546) is fixed, this is
|
||||
how you can make use of Yjs type declarations.
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
},
|
||||
"maxNodeModuleJsDepth": 5
|
||||
}
|
||||
```
|
||||
|
||||
## 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 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 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).
|
||||
|
||||
**Examples:**
|
||||
|
||||
1. If a user inserts elements in sequence, the struct will be merged into a
|
||||
single struct. E.g. `array.insert(0, ['a']), array.insert(0, ['b']);` is
|
||||
first represented as two structs (`[{id: {client, clock: 0}, content: 'a'},
|
||||
{id: {client, clock: 1}, content: 'b'}`) and then merged into a single
|
||||
struct: `[{id: {client, clock: 0}, content: 'ab'}]`.
|
||||
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](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.
|
||||
|
||||
## License and Author
|
||||
|
||||
Yjs and all related projects are [**MIT licensed**](./LICENSE).
|
||||
|
||||
Yjs is based on my research as a student at the [RWTH
|
||||
i5](http://dbis.rwth-aachen.de/). Now I am working on Yjs in my spare time.
|
||||
|
||||
Fund this project by donating on [Patreon](https://www.patreon.com/dmonad) or
|
||||
hiring [me](https://github.com/dmonad) for professional support.
|
14
package.json
14
package.json
@ -10,8 +10,8 @@
|
||||
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
|
||||
"dist": "rm -rf dist && rollup -c",
|
||||
"watch": "rollup -wc",
|
||||
"lint": "markdownlint README.v13.md && standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
|
||||
"lint": "markdownlint README.md && standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||
"serve-docs": "npm run docs && serve ./docs/",
|
||||
"preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000",
|
||||
"postversion": "git push && git push --tags",
|
||||
@ -38,20 +38,20 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/y-js/yjs.git"
|
||||
"url": "https://github.com/yjs/yjs.git"
|
||||
},
|
||||
"keywords": [
|
||||
"crdt"
|
||||
],
|
||||
"author": "Kevin Jahns",
|
||||
"email": "kevin.jahns@rwth-aachen.de",
|
||||
"email": "kevin.jahns@protonmail.com",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/y-js/yjs/issues"
|
||||
"url": "https://github.com/yjs/yjs/issues"
|
||||
},
|
||||
"homepage": "http://y-js.org",
|
||||
"homepage": "https://yjs.dev",
|
||||
"dependencies": {
|
||||
"lib0": "0.0.6"
|
||||
"lib0": "^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^3.6.1",
|
||||
|
@ -238,7 +238,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
|
||||
}
|
||||
|
||||
/**
|
||||
* This issue has been reported here https://github.com/y-js/yjs/issues/155
|
||||
* This issue has been reported here https://github.com/yjs/yjs/issues/155
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testNewChildDoesNotEmitEventInTransaction = tc => {
|
||||
|
Loading…
x
Reference in New Issue
Block a user