Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3404d22d12 | ||
|
|
d3b56702ad | ||
|
|
d5e6c26420 | ||
|
|
e497f07f7a | ||
|
|
510354d99f | ||
|
|
c3342d0b34 | ||
|
|
45af21f31e | ||
|
|
972d15dda5 | ||
|
|
fdf2063943 | ||
|
|
e81267d4df | ||
|
|
563c34f81a | ||
|
|
ba713983e3 | ||
|
|
bf2ee3680b | ||
|
|
b812a3dd6c | ||
|
|
b3f5b50377 | ||
|
|
7bcd4a828d | ||
|
|
cb705922b4 | ||
|
|
1ed58909d3 | ||
|
|
0aca7bbefa | ||
|
|
e1f0324840 | ||
|
|
7bac783490 | ||
|
|
1508c44f68 | ||
|
|
3dd843372f | ||
|
|
d6be4d9391 | ||
|
|
53f2344017 | ||
|
|
86f7631d1e | ||
|
|
3bb107504f | ||
|
|
4c46ebfb45 | ||
|
|
9d0d63ead7 | ||
|
|
39803c1d11 | ||
|
|
46fae57036 | ||
|
|
e9cb07da55 | ||
|
|
114f28f48e | ||
|
|
a1da486c8a | ||
|
|
4fb9cc2a30 | ||
|
|
e2c9eb7f01 | ||
|
|
6fd33c0720 | ||
|
|
72f3ce75b2 |
7
.circleci/config.yml
Normal file
7
.circleci/config.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2.1
|
||||||
|
orbs:
|
||||||
|
node: circleci/node@3.0.0
|
||||||
|
workflows:
|
||||||
|
node-tests:
|
||||||
|
jobs:
|
||||||
|
- node/test
|
||||||
29
.github/workflows/node.js.yml
vendored
Normal file
29
.github/workflows/node.js.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
|
||||||
|
|
||||||
|
name: Node.js CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [10.x, 12.x, 14.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build --if-present
|
||||||
|
- run: npm test
|
||||||
@@ -12,7 +12,7 @@ which aren't described in the paper. The most notable is that items have an
|
|||||||
`originRight` as well as an `origin` property, which improves performance when
|
`originRight` as well as an `origin` property, which improves performance when
|
||||||
many concurrent inserts happen after the same character.
|
many concurrent inserts happen after the same character.
|
||||||
|
|
||||||
At it heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
|
At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
|
||||||
reuse the CRDT resolution algorithm:
|
reuse the CRDT resolution algorithm:
|
||||||
|
|
||||||
- Arrays are easy - they're lists of arbitrary items.
|
- Arrays are easy - they're lists of arbitrary items.
|
||||||
|
|||||||
71
README.md
71
README.md
@@ -31,29 +31,30 @@ I'm currently looking for sponsors that allow me to be less dependent on
|
|||||||
contracting work. These awesome backers already fund further development of
|
contracting work. These awesome backers already fund further development of
|
||||||
Yjs:
|
Yjs:
|
||||||
|
|
||||||
[](https://github.com/vwall)
|
[](https://github.com/davidhq)
|
||||||
[<img src="https://user-images.githubusercontent.com/5553757/83337333-a7bcb380-a2ba-11ea-837b-e404eb35d318.png"
|
[](https://github.com/ifiokjr)
|
||||||
height="60px" />](https://input.com/)
|
[](https://github.com/burke)
|
||||||
[](https://github.com/canadaduane)
|
[](https://github.com/cben)
|
||||||
[](https://github.com/ISNIT0)
|
[](https://github.com/tommoor)
|
||||||
[<img src="https://room.sh/img/icons/android-chrome-192x192.png" height="60px" />](https://room.sh/)
|
[](https://github.com/michaelemeyers)
|
||||||
|
[](https://github.com/csbenjamin)
|
||||||
|
[](https://github.com/AdventureBeard)
|
||||||
|
[](https://github.com/nimbuswebinc)
|
||||||
[](https://github.com/journeyapps)
|
[](https://github.com/journeyapps)
|
||||||
[](https://github.com/adabru)
|
[](https://github.com/adabru)
|
||||||
[](https://github.com/NathanaelA)
|
[](https://github.com/NathanaelA)
|
||||||
[](https://github.com/gremloon)
|
[<img src="https://room.sh/img/icons/android-chrome-192x192.png" height="60px" />](https://room.sh/)
|
||||||
[](https://github.com/ifiokjr)
|
|
||||||
[](https://github.com/mrfambo)
|
|
||||||
|
|
||||||
Sponsorship also comes with special perks! [](https://github.com/sponsors/dmonad)
|
Sponsorship also comes with special perks! [](https://github.com/sponsors/dmonad)
|
||||||
|
|
||||||
## Who is using Yjs
|
## Who is using Yjs
|
||||||
|
|
||||||
* [Relm](http://www.relm.us/) A collaborative gameworld for teamwork and
|
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
|
||||||
community. :star2:
|
community. :star2:
|
||||||
* [Input](https://input.com/) A collaborative note taking app. :star2:
|
* [Input](https://input.com/) A collaborative note taking app. :star2:
|
||||||
* [Room.sh](https://room.sh/) A meeting application with integrated
|
* [Room.sh](https://room.sh/) A meeting application with integrated
|
||||||
collaborative drawing, editing, and coding tools. :star:
|
collaborative drawing, editing, and coding tools. :star:
|
||||||
* [http://coronavirustechhandbook.com/](https://coronavirustechhandbook.com/)
|
* [https://coronavirustechhandbook.com/](https://coronavirustechhandbook.com/)
|
||||||
A collaborative wiki that is edited by thousands of different people to work
|
A collaborative wiki that is edited by thousands of different people to work
|
||||||
on a rapid and sophisticated response to the coronavirus outbreak and
|
on a rapid and sophisticated response to the coronavirus outbreak and
|
||||||
subsequent impacts. :star:
|
subsequent impacts. :star:
|
||||||
@@ -90,10 +91,10 @@ are implemented in separate modules.
|
|||||||
|
|
||||||
| Name | Cursors | Binding | Demo |
|
| Name | Cursors | Binding | Demo |
|
||||||
|---|:-:|---|---|
|
|---|:-:|---|---|
|
||||||
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](http://github.com/yjs/y-prosemirror) | [demo](https://demos.yjs.dev/prosemirror/prosemirror.html) |
|
| [ProseMirror](https://prosemirror.net/) | ✔ | [y-prosemirror](https://github.com/yjs/y-prosemirror) | [demo](https://demos.yjs.dev/prosemirror/prosemirror.html) |
|
||||||
| [Quill](https://quilljs.com/) | ✔ | [y-quill](http://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
|
| [Quill](https://quilljs.com/) | ✔ | [y-quill](https://github.com/yjs/y-quill) | [demo](https://demos.yjs.dev/quill/quill.html) |
|
||||||
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](http://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
| [CodeMirror](https://codemirror.net/) | ✔ | [y-codemirror](https://github.com/yjs/y-codemirror) | [demo](https://demos.yjs.dev/codemirror/codemirror.html) |
|
||||||
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](http://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://github.com/yjs/y-monaco) | [demo](https://demos.yjs.dev/monaco/monaco.html) |
|
||||||
|
|
||||||
### Providers
|
### Providers
|
||||||
|
|
||||||
@@ -103,7 +104,7 @@ manage all that for you and are the perfect starting point for your
|
|||||||
collaborative app.
|
collaborative app.
|
||||||
|
|
||||||
<dl>
|
<dl>
|
||||||
<dt><a href="http://github.com/yjs/y-webrtc">y-webrtc</a></dt>
|
<dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
Propagates document updates peer-to-peer using WebRTC. The peers exchange
|
Propagates document updates peer-to-peer using WebRTC. The peers exchange
|
||||||
signaling data over signaling servers. Publically available signaling servers
|
signaling data over signaling servers. Publically available signaling servers
|
||||||
@@ -111,19 +112,19 @@ are available. Communication over the signaling servers can be encrypted by
|
|||||||
providing a shared secret, keeping the connection information and the shared
|
providing a shared secret, keeping the connection information and the shared
|
||||||
document private.
|
document private.
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="http://github.com/yjs/y-websocket">y-websocket</a></dt>
|
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
A module that contains a simple websocket backend and a websocket client that
|
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
|
connects to that backend. The backend can be extended to persist updates in a
|
||||||
leveldb database.
|
leveldb database.
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="http://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
|
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
Efficiently persists document updates to the browsers indexeddb database.
|
Efficiently persists document updates to the browsers indexeddb database.
|
||||||
The document is immediately available and only diffs need to be synced through the
|
The document is immediately available and only diffs need to be synced through the
|
||||||
network provider.
|
network provider.
|
||||||
</dd>
|
</dd>
|
||||||
<dt><a href="http://github.com/yjs/y-dat">y-dat</a></dt>
|
<dt><a href="https://github.com/yjs/y-dat">y-dat</a></dt>
|
||||||
<dd>
|
<dd>
|
||||||
[WIP] Write document updates effinciently to the dat network using
|
[WIP] Write document updates effinciently to the dat network using
|
||||||
<a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has
|
<a href="https://github.com/kappa-db/multifeed">multifeed</a>. Each client has
|
||||||
@@ -241,6 +242,8 @@ necessary.
|
|||||||
</p>
|
</p>
|
||||||
<pre>const yarray = new Y.Array()</pre>
|
<pre>const yarray = new Y.Array()</pre>
|
||||||
<dl>
|
<dl>
|
||||||
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
<b><code>insert(index:number, content:Array<object|boolean|Array|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>
|
<dd>
|
||||||
Insert content at <var>index</var>. Note that content is an array of elements.
|
Insert content at <var>index</var>. Note that content is an array of elements.
|
||||||
@@ -255,6 +258,8 @@ position 0.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>get(index:number)</code></b>
|
<b><code>get(index:number)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>slice(start:number, end:number):Array<Object|boolean|Array|string|number|Uint8Array|Y.Type></code></b>
|
||||||
|
<dd>Retrieve a range of content</dd>
|
||||||
<b><code>length:number</code></b>
|
<b><code>length:number</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b>
|
<b>
|
||||||
@@ -310,6 +315,8 @@ or any of its children.
|
|||||||
</p>
|
</p>
|
||||||
<pre><code>const ymap = new Y.Map()</code></pre>
|
<pre><code>const ymap = new Y.Map()</code></pre>
|
||||||
<dl>
|
<dl>
|
||||||
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
<b><code>get(key:string):object|boolean|string|number|Uint8Array|Y.Type</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
|
<b><code>set(key:string, value:object|boolean|string|number|Uint8Array|Y.Type)</code></b>
|
||||||
@@ -320,6 +327,8 @@ or any of its children.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>get(index:number)</code></b>
|
<b><code>get(index:number)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>clone():Y.Map</code></b>
|
||||||
|
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||||
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array></code></b>
|
<b><code>toJSON():Object<string, Object|boolean|Array|string|number|Uint8Array></code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It
|
Copies the <code>[key,value]</code> pairs of this YMap to a new Object.It
|
||||||
@@ -387,6 +396,8 @@ YTextEvents compute changes as deltas.
|
|||||||
</p>
|
</p>
|
||||||
<pre>const ytext = new Y.Text()</pre>
|
<pre>const ytext = new Y.Text()</pre>
|
||||||
<dl>
|
<dl>
|
||||||
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
<b><code>insert(index:number, content:string, [formattingAttributes:Object<string,string>])</code></b>
|
<b><code>insert(index:number, content:string, [formattingAttributes:Object<string,string>])</code></b>
|
||||||
<dd>
|
<dd>
|
||||||
Insert a string at <var>index</var> and assign formatting attributes to it.
|
Insert a string at <var>index</var> and assign formatting attributes to it.
|
||||||
@@ -445,14 +456,22 @@ or any of its children.
|
|||||||
</p>
|
</p>
|
||||||
<pre><code>const yxml = new Y.XmlFragment()</code></pre>
|
<pre><code>const yxml = new Y.XmlFragment()</code></pre>
|
||||||
<dl>
|
<dl>
|
||||||
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
|
<b><code>firstChild:Y.XmlElement|Y.XmlText|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</code></b>
|
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>delete(index:number, length:number)</code></b>
|
<b><code>delete(index:number, length:number)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>get(index:number)</code></b>
|
<b><code>get(index:number)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText></code></b>
|
||||||
|
<dd>Retrieve a range of content</dd>
|
||||||
<b><code>length:number</code></b>
|
<b><code>length:number</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>clone():Y.XmlFragment</code></b>
|
||||||
|
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||||
<dd>Copies the children to a new Array.</dd>
|
<dd>Copies the children to a new Array.</dd>
|
||||||
<b><code>toDOM():DocumentFragment</code></b>
|
<b><code>toDOM():DocumentFragment</code></b>
|
||||||
@@ -496,6 +515,14 @@ content and be actually XML compliant.
|
|||||||
</p>
|
</p>
|
||||||
<pre><code>const yxml = new Y.XmlElement()</code></pre>
|
<pre><code>const yxml = new Y.XmlElement()</code></pre>
|
||||||
<dl>
|
<dl>
|
||||||
|
<b><code>parent:Y.AbstractType|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
|
<b><code>firstChild:Y.XmlElement|Y.XmlText|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
|
<b><code>nextSibling:Y.XmlElement|Y.XmlText|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
|
<b><code>prevSibling:Y.XmlElement|Y.XmlText|null</code></b>
|
||||||
|
<dd></dd>
|
||||||
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</code></b>
|
<b><code>insert(index:number, content:Array<Y.XmlElement|Y.XmlText>)</code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>delete(index:number, length:number)</code></b>
|
<b><code>delete(index:number, length:number)</code></b>
|
||||||
@@ -512,6 +539,12 @@ content and be actually XML compliant.
|
|||||||
<dd></dd>
|
<dd></dd>
|
||||||
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
<b><code>getAttributes(attributeName:string):Object<string,string></code></b>
|
||||||
<dd></dd>
|
<dd></dd>
|
||||||
|
<b><code>get(i:number):Y.XmlElement|Y.XmlText</code></b>
|
||||||
|
<dd>Retrieve the i-th element.</dd>
|
||||||
|
<b><code>slice(start:number, end:number):Array<Y.XmlElement|Y.XmlText></code></b>
|
||||||
|
<dd>Retrieve a range of content</dd>
|
||||||
|
<b><code>clone():Y.XmlElement</code></b>
|
||||||
|
<dd>Clone this type into a fresh Yjs type.</dd>
|
||||||
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
<b><code>toArray():Array<Y.XmlElement|Y.XmlText></code></b>
|
||||||
<dd>Copies the children to a new Array.</dd>
|
<dd>Copies the children to a new Array.</dd>
|
||||||
<b><code>toDOM():Element</code></b>
|
<b><code>toDOM():Element</code></b>
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.4.0",
|
"version": "13.4.7",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "yjs",
|
"name": "yjs",
|
||||||
"version": "13.4.0",
|
"version": "13.4.7",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ export {
|
|||||||
snapshot,
|
snapshot,
|
||||||
emptySnapshot,
|
emptySnapshot,
|
||||||
findRootTypeKey,
|
findRootTypeKey,
|
||||||
|
getItem,
|
||||||
typeListToArraySnapshot,
|
typeListToArraySnapshot,
|
||||||
typeMapGetSnapshot,
|
typeMapGetSnapshot,
|
||||||
createDocFromSnapshot,
|
createDocFromSnapshot,
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ export class ContentString {
|
|||||||
splice (offset) {
|
splice (offset) {
|
||||||
const right = new ContentString(this.str.slice(offset))
|
const right = new ContentString(this.str.slice(offset))
|
||||||
this.str = this.str.slice(0, offset)
|
this.str = this.str.slice(0, offset)
|
||||||
|
|
||||||
|
// Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248
|
||||||
|
const firstCharCode = this.str.charCodeAt(offset - 1)
|
||||||
|
if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) {
|
||||||
|
// Last character of the left split is the start of a surrogate utf16/ucs2 pair.
|
||||||
|
// We don't support splitting of surrogate pairs because this may lead to invalid documents.
|
||||||
|
// Replace the invalid character with a unicode replacement character (<28> / U+FFFD)
|
||||||
|
this.str = this.str.slice(0, offset - 1) + '<27>'
|
||||||
|
// replace right as well
|
||||||
|
right.str = '<27>' + right.str.slice(1)
|
||||||
|
}
|
||||||
return right
|
return right
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ import {
|
|||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
import * as error from 'lib0/error.js'
|
import * as error from 'lib0/error.js'
|
||||||
import * as maplib from 'lib0/map.js'
|
|
||||||
import * as set from 'lib0/set.js'
|
|
||||||
import * as binary from 'lib0/binary.js'
|
import * as binary from 'lib0/binary.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -594,7 +592,7 @@ export class Item extends AbstractStruct {
|
|||||||
}
|
}
|
||||||
this.markDeleted()
|
this.markDeleted()
|
||||||
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length)
|
||||||
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
|
addChangedTypeToTransaction(transaction, parent, this.parentSub)
|
||||||
this.content.delete(transaction)
|
this.content.delete(transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,6 +287,13 @@ export class AbstractType {
|
|||||||
this._searchMarker = null
|
this._searchMarker = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {AbstractType<any>|null}
|
||||||
|
*/
|
||||||
|
get parent () {
|
||||||
|
return this._item ? /** @type {AbstractType<any>} */ (this._item.parent) : null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integrate this type into the Yjs instance.
|
* Integrate this type into the Yjs instance.
|
||||||
*
|
*
|
||||||
@@ -309,6 +316,13 @@ export class AbstractType {
|
|||||||
throw error.methodUnimplemented()
|
throw error.methodUnimplemented()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {AbstractType<EventType>}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
throw error.methodUnimplemented()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {AbstractUpdateEncoder} encoder
|
||||||
*/
|
*/
|
||||||
@@ -381,6 +395,43 @@ export class AbstractType {
|
|||||||
toJSON () {}
|
toJSON () {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} type
|
||||||
|
* @param {number} start
|
||||||
|
* @param {number} end
|
||||||
|
* @return {Array<any>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeListSlice = (type, start, end) => {
|
||||||
|
if (start < 0) {
|
||||||
|
start = type._length + start
|
||||||
|
}
|
||||||
|
if (end < 0) {
|
||||||
|
end = type._length + end
|
||||||
|
}
|
||||||
|
let len = end - start
|
||||||
|
const cs = []
|
||||||
|
let n = type._start
|
||||||
|
while (n !== null && len > 0) {
|
||||||
|
if (n.countable && !n.deleted) {
|
||||||
|
const c = n.content.getContent()
|
||||||
|
if (c.length <= start) {
|
||||||
|
start -= c.length
|
||||||
|
} else {
|
||||||
|
for (let i = start; i < c.length && len > 0; i++) {
|
||||||
|
cs.push(c[i])
|
||||||
|
len--
|
||||||
|
}
|
||||||
|
start = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n = n.right
|
||||||
|
}
|
||||||
|
return cs
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractType<any>} type
|
* @param {AbstractType<any>} type
|
||||||
* @return {Array<any>}
|
* @return {Array<any>}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
transact,
|
transact,
|
||||||
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, Transaction, Item // eslint-disable-line
|
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, Transaction, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
import { typeListSlice } from './AbstractType.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event that describes the changes on a YArray
|
* Event that describes the changes on a YArray
|
||||||
@@ -53,6 +54,18 @@ export class YArray extends AbstractType {
|
|||||||
this._searchMarker = []
|
this._searchMarker = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new YArray containing the specified items.
|
||||||
|
* @template T
|
||||||
|
* @param {Array<T>} items
|
||||||
|
* @return {YArray<T>}
|
||||||
|
*/
|
||||||
|
static from (items) {
|
||||||
|
const a = new YArray()
|
||||||
|
a.push(items)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integrate this type into the Yjs instance.
|
* Integrate this type into the Yjs instance.
|
||||||
*
|
*
|
||||||
@@ -73,6 +86,17 @@ export class YArray extends AbstractType {
|
|||||||
return new YArray()
|
return new YArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YArray<T>}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const arr = new YArray()
|
||||||
|
arr.insert(0, this.toArray().map(el =>
|
||||||
|
el instanceof AbstractType ? el.clone() : el
|
||||||
|
))
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
get length () {
|
get length () {
|
||||||
return this._prelimContent === null ? this._length : this._prelimContent.length
|
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||||
}
|
}
|
||||||
@@ -167,6 +191,17 @@ export class YArray extends AbstractType {
|
|||||||
return typeListToArray(this)
|
return typeListToArray(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this YArray to a JavaScript Array.
|
||||||
|
*
|
||||||
|
* @param {number} [start]
|
||||||
|
* @param {number} [end]
|
||||||
|
* @return {Array<T>}
|
||||||
|
*/
|
||||||
|
slice (start = 0, end = this.length) {
|
||||||
|
return typeListSlice(this, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transforms this Shared Type to a JSON object.
|
* Transforms this Shared Type to a JSON object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -84,6 +84,17 @@ export class YMap extends AbstractType {
|
|||||||
return new YMap()
|
return new YMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YMap<T>}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const map = new YMap()
|
||||||
|
this.forEach((value, key) => {
|
||||||
|
map.set(key, value instanceof AbstractType ? value.clone() : value)
|
||||||
|
})
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates YMapEvent and calls observers.
|
* Creates YMapEvent and calls observers.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ import {
|
|||||||
iterateDeletedStructs,
|
iterateDeletedStructs,
|
||||||
iterateStructs,
|
iterateStructs,
|
||||||
findMarker,
|
findMarker,
|
||||||
|
typeMapDelete,
|
||||||
|
typeMapSet,
|
||||||
|
typeMapGet,
|
||||||
|
typeMapGetAll,
|
||||||
updateMarkerChanges,
|
updateMarkerChanges,
|
||||||
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
ArraySearchMarker, AbstractUpdateDecoder, AbstractUpdateEncoder, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
@@ -512,13 +516,32 @@ export class YTextEvent extends YEvent {
|
|||||||
/**
|
/**
|
||||||
* @param {YText} ytext
|
* @param {YText} ytext
|
||||||
* @param {Transaction} transaction
|
* @param {Transaction} transaction
|
||||||
|
* @param {Set<any>} subs The keys that changed
|
||||||
*/
|
*/
|
||||||
constructor (ytext, transaction) {
|
constructor (ytext, transaction, subs) {
|
||||||
super(ytext, transaction)
|
super(ytext, transaction)
|
||||||
/**
|
/**
|
||||||
* @type {Array<DeltaItem>|null}
|
* @type {Array<DeltaItem>|null}
|
||||||
*/
|
*/
|
||||||
this._delta = null
|
this._delta = null
|
||||||
|
/**
|
||||||
|
* Whether the children changed.
|
||||||
|
* @type {Boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.childListChanged = false
|
||||||
|
/**
|
||||||
|
* Set of all changed attributes.
|
||||||
|
* @type {Set<string>}
|
||||||
|
*/
|
||||||
|
this.keysChanged = new Set()
|
||||||
|
subs.forEach((sub) => {
|
||||||
|
if (sub === null) {
|
||||||
|
this.childListChanged = true
|
||||||
|
} else {
|
||||||
|
this.keysChanged.add(sub)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -762,6 +785,15 @@ export class YText extends AbstractType {
|
|||||||
return new YText()
|
return new YText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YText}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const text = new YText()
|
||||||
|
text.applyDelta(this.toDelta())
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates YTextEvent and calls observers.
|
* Creates YTextEvent and calls observers.
|
||||||
*
|
*
|
||||||
@@ -770,7 +802,7 @@ export class YText extends AbstractType {
|
|||||||
*/
|
*/
|
||||||
_callObserver (transaction, parentSubs) {
|
_callObserver (transaction, parentSubs) {
|
||||||
super._callObserver(transaction, parentSubs)
|
super._callObserver(transaction, parentSubs)
|
||||||
const event = new YTextEvent(this, transaction)
|
const event = new YTextEvent(this, transaction, parentSubs)
|
||||||
const doc = transaction.doc
|
const doc = transaction.doc
|
||||||
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
// If a remote change happened, we try to cleanup potential formatting duplicates.
|
||||||
if (!transaction.local) {
|
if (!transaction.local) {
|
||||||
@@ -1102,6 +1134,74 @@ export class YText extends AbstractType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes an attribute.
|
||||||
|
*
|
||||||
|
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that is to be removed.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
removeAttribute (attributeName) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeMapDelete(transaction, this, attributeName)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.removeAttribute(attributeName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets or updates an attribute.
|
||||||
|
*
|
||||||
|
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that is to be set.
|
||||||
|
* @param {any} attributeValue The attribute value that is to be set.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
setAttribute (attributeName, attributeValue) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
typeMapSet(transaction, this, attributeName, attributeValue)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
/** @type {Array<function>} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an attribute value that belongs to the attribute name.
|
||||||
|
*
|
||||||
|
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||||
|
*
|
||||||
|
* @param {String} attributeName The attribute name that identifies the
|
||||||
|
* queried value.
|
||||||
|
* @return {any} The queried attribute value.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getAttribute (attributeName) {
|
||||||
|
return /** @type {any} */ (typeMapGet(this, attributeName))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
|
*
|
||||||
|
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
|
||||||
|
*
|
||||||
|
* @param {Snapshot} [snapshot]
|
||||||
|
* @return {Object<string, any>} A JSON Object that describes the attributes.
|
||||||
|
*
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
getAttributes (snapshot) {
|
||||||
|
return typeMapGetAll(this)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {AbstractUpdateEncoder} encoder
|
* @param {AbstractUpdateEncoder} encoder
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
YXmlElementRefID,
|
YXmlElementRefID,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line
|
YXmlText, ContentType, AbstractType, AbstractUpdateDecoder, AbstractUpdateEncoder, Snapshot, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +28,22 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
this._prelimAttrs = new Map()
|
this._prelimAttrs = new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {YXmlElement|YXmlText|null}
|
||||||
|
*/
|
||||||
|
get nextSibling () {
|
||||||
|
const n = this._item ? this._item.next : null
|
||||||
|
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {YXmlElement|YXmlText|null}
|
||||||
|
*/
|
||||||
|
get prevSibling () {
|
||||||
|
const n = this._item ? this._item.prev : null
|
||||||
|
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integrate this type into the Yjs instance.
|
* Integrate this type into the Yjs instance.
|
||||||
*
|
*
|
||||||
@@ -55,6 +71,20 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
return new YXmlElement(this.nodeName)
|
return new YXmlElement(this.nodeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YXmlElement}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const el = new YXmlElement(this.nodeName)
|
||||||
|
const attrs = this.getAttributes()
|
||||||
|
for (const key in attrs) {
|
||||||
|
el.setAttribute(key, attrs[key])
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the XML serialization of this YXmlElement.
|
* Returns the XML serialization of this YXmlElement.
|
||||||
* The attributes are ordered by attribute-name, so you can easily use this
|
* The attributes are ordered by attribute-name, so you can easily use this
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YEvent,
|
YEvent,
|
||||||
YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
*/
|
*/
|
||||||
export class YXmlEvent extends YEvent {
|
export class YXmlEvent extends YEvent {
|
||||||
/**
|
/**
|
||||||
* @param {YXmlElement|YXmlFragment} target The target on which the event is created.
|
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
|
||||||
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
|
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
|
||||||
* child list changed.
|
* child list changed.
|
||||||
* @param {Transaction} transaction The transaction instance with wich the
|
* @param {Transaction} transaction The transaction instance with wich the
|
||||||
@@ -25,7 +25,7 @@ export class YXmlEvent extends YEvent {
|
|||||||
this.childListChanged = false
|
this.childListChanged = false
|
||||||
/**
|
/**
|
||||||
* Set of all changed attributes.
|
* Set of all changed attributes.
|
||||||
* @type {Set<string|null>}
|
* @type {Set<string>}
|
||||||
*/
|
*/
|
||||||
this.attributesChanged = new Set()
|
this.attributesChanged = new Set()
|
||||||
subs.forEach((sub) => {
|
subs.forEach((sub) => {
|
||||||
|
|||||||
@@ -9,14 +9,19 @@ import {
|
|||||||
typeListMap,
|
typeListMap,
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
typeListInsertGenerics,
|
typeListInsertGenerics,
|
||||||
|
typeListInsertGenericsAfter,
|
||||||
typeListDelete,
|
typeListDelete,
|
||||||
typeListToArray,
|
typeListToArray,
|
||||||
YXmlFragmentRefID,
|
YXmlFragmentRefID,
|
||||||
callTypeObservers,
|
callTypeObservers,
|
||||||
transact,
|
transact,
|
||||||
|
typeListGet,
|
||||||
|
typeListSlice,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
AbstractUpdateDecoder, AbstractUpdateEncoder, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
|
import * as error from 'lib0/error.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define the elements to which a set of CSS queries apply.
|
* Define the elements to which a set of CSS queries apply.
|
||||||
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
|
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
|
||||||
@@ -128,6 +133,14 @@ export class YXmlFragment extends AbstractType {
|
|||||||
this._prelimContent = []
|
this._prelimContent = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {YXmlElement|YXmlText|null}
|
||||||
|
*/
|
||||||
|
get firstChild () {
|
||||||
|
const first = this._first
|
||||||
|
return first ? first.content.getContent()[0] : null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integrate this type into the Yjs instance.
|
* Integrate this type into the Yjs instance.
|
||||||
*
|
*
|
||||||
@@ -148,6 +161,16 @@ export class YXmlFragment extends AbstractType {
|
|||||||
return new YXmlFragment()
|
return new YXmlFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YXmlFragment}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const el = new YXmlFragment()
|
||||||
|
// @ts-ignore
|
||||||
|
el.insert(0, el.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
get length () {
|
get length () {
|
||||||
return this._prelimContent === null ? this._length : this._prelimContent.length
|
return this._prelimContent === null ? this._length : this._prelimContent.length
|
||||||
}
|
}
|
||||||
@@ -290,6 +313,32 @@ export class YXmlFragment extends AbstractType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts new content at an index.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // Insert character 'a' at position 0
|
||||||
|
* xml.insert(0, [new Y.XmlText('text')])
|
||||||
|
*
|
||||||
|
* @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at
|
||||||
|
* @param {Array<YXmlElement|YXmlText>} content The array of content
|
||||||
|
*/
|
||||||
|
insertAfter (ref, content) {
|
||||||
|
if (this.doc !== null) {
|
||||||
|
transact(this.doc, transaction => {
|
||||||
|
const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref
|
||||||
|
typeListInsertGenericsAfter(transaction, this, refItem, content)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const pc = /** @type {Array<any>} */ (this._prelimContent)
|
||||||
|
const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1
|
||||||
|
if (index === 0 && ref !== null) {
|
||||||
|
throw error.create('Reference item not found')
|
||||||
|
}
|
||||||
|
pc.splice(index, 0, ...content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes elements starting from an index.
|
* Deletes elements starting from an index.
|
||||||
*
|
*
|
||||||
@@ -316,6 +365,45 @@ export class YXmlFragment extends AbstractType {
|
|||||||
return typeListToArray(this)
|
return typeListToArray(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends content to this YArray.
|
||||||
|
*
|
||||||
|
* @param {Array<YXmlElement|YXmlText>} content Array of content to append.
|
||||||
|
*/
|
||||||
|
push (content) {
|
||||||
|
this.insert(this.length, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preppends content to this YArray.
|
||||||
|
*
|
||||||
|
* @param {Array<YXmlElement|YXmlText>} content Array of content to preppend.
|
||||||
|
*/
|
||||||
|
unshift (content) {
|
||||||
|
this.insert(0, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the i-th element from a YArray.
|
||||||
|
*
|
||||||
|
* @param {number} index The index of the element to return from the YArray
|
||||||
|
* @return {YXmlElement|YXmlText}
|
||||||
|
*/
|
||||||
|
get (index) {
|
||||||
|
return typeListGet(this, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms this YArray to a JavaScript Array.
|
||||||
|
*
|
||||||
|
* @param {number} [start]
|
||||||
|
* @param {number} [end]
|
||||||
|
* @return {Array<YXmlElement|YXmlText>}
|
||||||
|
*/
|
||||||
|
slice (start = 0, end = this.length) {
|
||||||
|
return typeListSlice(this, start, end)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transform the properties of this type to binary and write it to an
|
* Transform the properties of this type to binary and write it to an
|
||||||
* BinaryEncoder.
|
* BinaryEncoder.
|
||||||
|
|||||||
@@ -29,6 +29,17 @@ export class YXmlHook extends YMap {
|
|||||||
return new YXmlHook(this.hookName)
|
return new YXmlHook(this.hookName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YXmlHook}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const el = new YXmlHook(this.hookName)
|
||||||
|
this.forEach((value, key) => {
|
||||||
|
el.set(key, value)
|
||||||
|
})
|
||||||
|
return el
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Dom Element that mirrors this YXmlElement.
|
* Creates a Dom Element that mirrors this YXmlElement.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
YText,
|
YText,
|
||||||
YXmlTextRefID,
|
YXmlTextRefID,
|
||||||
AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
|
ContentType, YXmlElement, AbstractUpdateDecoder, AbstractUpdateEncoder // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,10 +10,35 @@ import {
|
|||||||
* simple formatting information like bold and italic.
|
* simple formatting information like bold and italic.
|
||||||
*/
|
*/
|
||||||
export class YXmlText extends YText {
|
export class YXmlText extends YText {
|
||||||
|
/**
|
||||||
|
* @type {YXmlElement|YXmlText|null}
|
||||||
|
*/
|
||||||
|
get nextSibling () {
|
||||||
|
const n = this._item ? this._item.next : null
|
||||||
|
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {YXmlElement|YXmlText|null}
|
||||||
|
*/
|
||||||
|
get prevSibling () {
|
||||||
|
const n = this._item ? this._item.prev : null
|
||||||
|
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
|
||||||
|
}
|
||||||
|
|
||||||
_copy () {
|
_copy () {
|
||||||
return new YXmlText()
|
return new YXmlText()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {YXmlText}
|
||||||
|
*/
|
||||||
|
clone () {
|
||||||
|
const text = new YXmlText()
|
||||||
|
text.applyDelta(this.toDelta())
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Dom Element that mirrors this YXmlText.
|
* Creates a Dom Element that mirrors this YXmlText.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export class Doc extends Observable {
|
|||||||
}, null, true)
|
}, null, true)
|
||||||
}
|
}
|
||||||
this.emit('destroyed', [true])
|
this.emit('destroyed', [true])
|
||||||
|
this.emit('destroy', [this])
|
||||||
super.destroy()
|
super.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ export const addEventHandlerListener = (eventHandler, f) =>
|
|||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
export const removeEventHandlerListener = (eventHandler, f) => {
|
export const removeEventHandlerListener = (eventHandler, f) => {
|
||||||
eventHandler.l = eventHandler.l.filter(g => f !== g)
|
const l = eventHandler.l
|
||||||
|
const len = l.length
|
||||||
|
eventHandler.l = l.filter(g => f !== g)
|
||||||
|
if (len === eventHandler.l.length) {
|
||||||
|
console.error('[yjs] Tried to remove event handler that doesn\'t exist.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export class PermanentUserData {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(new DSDecoderV1(encodedDs)))))
|
this.dss.set(userDescription, mergeDeleteSets(ds.map(encodedDs => readDeleteSet(new DSDecoderV1(decoding.createDecoder(encodedDs))))))
|
||||||
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
|
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
|
||||||
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
|
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
|||||||
/**
|
/**
|
||||||
* @param {RelativePosition|null} a
|
* @param {RelativePosition|null} a
|
||||||
* @param {RelativePosition|null} b
|
* @param {RelativePosition|null} b
|
||||||
|
* @return {boolean}
|
||||||
*
|
*
|
||||||
* @function
|
* @function
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -284,6 +284,9 @@ const cleanupTransactions = (transactionCleanups, i) => {
|
|||||||
.forEach(event => {
|
.forEach(event => {
|
||||||
event.currentTarget = type
|
event.currentTarget = type
|
||||||
})
|
})
|
||||||
|
// sort events by path length so that top-level events are fired first.
|
||||||
|
events
|
||||||
|
.sort((event1, event2) => event1.path.length - event2.path.length)
|
||||||
// We don't need to check for events.length
|
// We don't need to check for events.length
|
||||||
// because we know it has at least one element
|
// because we know it has at least one element
|
||||||
callEventHandlerListeners(type._dEH, events, transaction)
|
callEventHandlerListeners(type._dEH, events, transaction)
|
||||||
|
|||||||
@@ -119,9 +119,6 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = stackItem
|
result = stackItem
|
||||||
if (result != null) {
|
|
||||||
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
transaction.changed.forEach((subProps, type) => {
|
transaction.changed.forEach((subProps, type) => {
|
||||||
// destroy search marker if necessary
|
// destroy search marker if necessary
|
||||||
@@ -130,6 +127,9 @@ const popStackItem = (undoManager, stack, eventType) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, undoManager)
|
}, undoManager)
|
||||||
|
if (result != null) {
|
||||||
|
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ export class YEvent {
|
|||||||
/**
|
/**
|
||||||
* Computes the path from `y` to the changed type.
|
* Computes the path from `y` to the changed type.
|
||||||
*
|
*
|
||||||
|
* @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with.
|
||||||
|
*
|
||||||
* The following property holds:
|
* The following property holds:
|
||||||
* @example
|
* @example
|
||||||
* let type = y
|
* let type = y
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as t from 'lib0/testing.js'
|
import * as t from 'lib0/testing.js'
|
||||||
|
import * as promise from 'lib0/promise.js'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
contentRefs,
|
contentRefs,
|
||||||
@@ -10,7 +11,11 @@ import {
|
|||||||
readContentType,
|
readContentType,
|
||||||
readContentFormat,
|
readContentFormat,
|
||||||
readContentAny,
|
readContentAny,
|
||||||
readContentDoc
|
readContentDoc,
|
||||||
|
Doc,
|
||||||
|
PermanentUserData,
|
||||||
|
encodeStateAsUpdate,
|
||||||
|
applyUpdate
|
||||||
} from '../src/internals.js'
|
} from '../src/internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,3 +33,31 @@ export const testStructReferences = tc => {
|
|||||||
t.assert(contentRefs[8] === readContentAny)
|
t.assert(contentRefs[8] === readContentAny)
|
||||||
t.assert(contentRefs[9] === readContentDoc)
|
t.assert(contentRefs[9] === readContentDoc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There is some custom encoding/decoding happening in PermanentUserData.
|
||||||
|
* This is why it landed here.
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testPermanentUserData = async tc => {
|
||||||
|
const ydoc1 = new Doc()
|
||||||
|
const ydoc2 = new Doc()
|
||||||
|
const pd1 = new PermanentUserData(ydoc1)
|
||||||
|
const pd2 = new PermanentUserData(ydoc2)
|
||||||
|
pd1.setUserMapping(ydoc1, ydoc1.clientID, 'user a')
|
||||||
|
pd2.setUserMapping(ydoc2, ydoc2.clientID, 'user b')
|
||||||
|
ydoc1.getText().insert(0, 'xhi')
|
||||||
|
ydoc1.getText().delete(0, 1)
|
||||||
|
ydoc2.getText().insert(0, 'hxxi')
|
||||||
|
ydoc2.getText().delete(1, 2)
|
||||||
|
await promise.wait(10)
|
||||||
|
applyUpdate(ydoc2, encodeStateAsUpdate(ydoc1))
|
||||||
|
applyUpdate(ydoc1, encodeStateAsUpdate(ydoc2))
|
||||||
|
|
||||||
|
// now sync a third doc with same name as doc1 and then create PermanentUserData
|
||||||
|
const ydoc3 = new Doc()
|
||||||
|
applyUpdate(ydoc3, encodeStateAsUpdate(ydoc1))
|
||||||
|
const pd3 = new PermanentUserData(ydoc3)
|
||||||
|
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,21 @@ export const testBasicUpdate = tc => {
|
|||||||
t.compare(doc2.getArray('array').toArray(), ['hi'])
|
t.compare(doc2.getArray('array').toArray(), ['hi'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSlice = tc => {
|
||||||
|
const doc1 = new Y.Doc()
|
||||||
|
const arr = doc1.getArray('array')
|
||||||
|
arr.insert(0, [1, 2, 3])
|
||||||
|
t.compareArrays(arr.slice(0), [1, 2, 3])
|
||||||
|
t.compareArrays(arr.slice(1), [2, 3])
|
||||||
|
t.compareArrays(arr.slice(0, -1), [1, 2])
|
||||||
|
arr.insert(0, [0])
|
||||||
|
t.compareArrays(arr.slice(0), [0, 1, 2, 3])
|
||||||
|
t.compareArrays(arr.slice(0, 2), [0, 1])
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
@@ -204,6 +219,34 @@ export const testInsertAndDeleteEventsForTypes = tc => {
|
|||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This issue has been reported in https://discuss.yjs.dev/t/order-in-which-events-yielded-by-observedeep-should-be-applied/261/2
|
||||||
|
*
|
||||||
|
* Deep observers generate multiple events. When an array added at item at, say, position 0,
|
||||||
|
* and item 1 changed then the array-add event should fire first so that the change event
|
||||||
|
* path is correct. A array binding might lead to an inconsistent state otherwise.
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testObserveDeepEventOrder = tc => {
|
||||||
|
const { array0, users } = init(tc, { users: 2 })
|
||||||
|
/**
|
||||||
|
* @type {Array<any>}
|
||||||
|
*/
|
||||||
|
let events = []
|
||||||
|
array0.observeDeep(e => {
|
||||||
|
events = e
|
||||||
|
})
|
||||||
|
array0.insert(0, [new Y.Map()])
|
||||||
|
users[0].transact(() => {
|
||||||
|
array0.get(0).set('a', 'a')
|
||||||
|
array0.insert(0, [0])
|
||||||
|
})
|
||||||
|
for (let i = 1; i < events.length; i++) {
|
||||||
|
t.assert(events[i - 1].path.length <= events[i].path.length, 'path size increases, fire top-level events first')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ export const testAppendChars = tc => {
|
|||||||
t.assert(text0.length === N)
|
t.assert(text0.length === N)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const largeDocumentSize = 100000
|
||||||
|
|
||||||
const id = Y.createID(0, 0)
|
const id = Y.createID(0, 0)
|
||||||
const c = new Y.ContentString('a')
|
const c = new Y.ContentString('a')
|
||||||
|
|
||||||
@@ -256,7 +258,7 @@ const c = new Y.ContentString('a')
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testBestCase = tc => {
|
export const testBestCase = tc => {
|
||||||
const N = 2000000
|
const N = largeDocumentSize
|
||||||
const items = new Array(N)
|
const items = new Array(N)
|
||||||
t.measureTime('time to create two million items in the best case', () => {
|
t.measureTime('time to create two million items in the best case', () => {
|
||||||
const parent = /** @type {any} */ ({})
|
const parent = /** @type {any} */ ({})
|
||||||
@@ -293,7 +295,7 @@ const tryGc = () => {
|
|||||||
* @param {t.TestCase} tc
|
* @param {t.TestCase} tc
|
||||||
*/
|
*/
|
||||||
export const testLargeFragmentedDocument = tc => {
|
export const testLargeFragmentedDocument = tc => {
|
||||||
const itemsToInsert = 1000000
|
const itemsToInsert = largeDocumentSize
|
||||||
let update = /** @type {any} */ (null)
|
let update = /** @type {any} */ (null)
|
||||||
;(() => {
|
;(() => {
|
||||||
const doc1 = new Y.Doc()
|
const doc1 = new Y.Doc()
|
||||||
@@ -321,6 +323,40 @@ export const testLargeFragmentedDocument = tc => {
|
|||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splitting surrogates can lead to invalid encoded documents.
|
||||||
|
*
|
||||||
|
* https://github.com/yjs/yjs/issues/248
|
||||||
|
*
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSplitSurrogateCharacter = tc => {
|
||||||
|
{
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
users[1].disconnect() // disconnecting forces the user to encode the split surrogate
|
||||||
|
text0.insert(0, '👾') // insert surrogate character
|
||||||
|
// split surrogate, which should not lead to an encoding error
|
||||||
|
text0.insert(1, 'hi!')
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
users[1].disconnect() // disconnecting forces the user to encode the split surrogate
|
||||||
|
text0.insert(0, '👾👾') // insert surrogate character
|
||||||
|
// partially delete surrogate
|
||||||
|
text0.delete(1, 2)
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const { users, text0 } = init(tc, { users: 2 })
|
||||||
|
users[1].disconnect() // disconnecting forces the user to encode the split surrogate
|
||||||
|
text0.insert(0, '👾👾') // insert surrogate character
|
||||||
|
// formatting will also split surrogates
|
||||||
|
text0.format(1, 2, { bold: true })
|
||||||
|
compare(users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RANDOM TESTS
|
// RANDOM TESTS
|
||||||
|
|
||||||
let charCounter = 0
|
let charCounter = 0
|
||||||
|
|||||||
@@ -73,3 +73,63 @@ export const testTreewalker = tc => {
|
|||||||
t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1')
|
t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1')
|
||||||
compare(users)
|
compare(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testYtextAttributes = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
|
||||||
|
ytext.observe(event => {
|
||||||
|
t.compare(event.changes.keys.get('test'), { action: 'add', oldValue: undefined })
|
||||||
|
})
|
||||||
|
ytext.setAttribute('test', 42)
|
||||||
|
t.compare(ytext.getAttribute('test'), 42)
|
||||||
|
t.compare(ytext.getAttributes(), { test: 42 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testSiblings = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yxml = ydoc.getXmlFragment()
|
||||||
|
const first = new Y.XmlText()
|
||||||
|
const second = new Y.XmlElement('p')
|
||||||
|
yxml.insert(0, [first, second])
|
||||||
|
t.assert(first.nextSibling === second)
|
||||||
|
t.assert(second.prevSibling === first)
|
||||||
|
t.assert(first.parent === yxml)
|
||||||
|
t.assert(yxml.parent === null)
|
||||||
|
t.assert(yxml.firstChild === first)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} tc
|
||||||
|
*/
|
||||||
|
export const testInsertafter = tc => {
|
||||||
|
const ydoc = new Y.Doc()
|
||||||
|
const yxml = ydoc.getXmlFragment()
|
||||||
|
const first = new Y.XmlText()
|
||||||
|
const second = new Y.XmlElement('p')
|
||||||
|
const third = new Y.XmlElement('p')
|
||||||
|
|
||||||
|
const deepsecond1 = new Y.XmlElement('span')
|
||||||
|
const deepsecond2 = new Y.XmlText()
|
||||||
|
second.insertAfter(null, [deepsecond1])
|
||||||
|
second.insertAfter(deepsecond1, [deepsecond2])
|
||||||
|
|
||||||
|
yxml.insertAfter(null, [first, second])
|
||||||
|
yxml.insertAfter(second, [third])
|
||||||
|
|
||||||
|
t.assert(yxml.length === 3)
|
||||||
|
t.assert(second.get(0) === deepsecond1)
|
||||||
|
t.assert(second.get(1) === deepsecond2)
|
||||||
|
|
||||||
|
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||||
|
|
||||||
|
t.fails(() => {
|
||||||
|
const el = new Y.XmlElement('p')
|
||||||
|
el.insertAfter(deepsecond1, [new Y.XmlText()])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user