Compare commits

..

64 Commits

Author SHA1 Message Date
Kevin Jahns
c0c2b3347b 13.5.39 2022-06-15 10:49:48 +02:00
Kevin Jahns
6258ba1ce9 Merge pull request #435 from yogas/export_UpdateEncoderV1
export UpdateEncoderV1
2022-06-15 10:47:04 +02:00
Vladimir Shapovalov
5a7ee74f68 export UpdateEncoderV1 2022-06-14 13:16:35 +07:00
Kevin Jahns
29fb4a0aab improve double-import error message 2022-06-13 10:27:04 +02:00
Kevin Jahns
4504196d5c add gitter & discord link 2022-05-19 22:15:10 +02:00
Kevin Jahns
0c8d29bfff Merge pull request #424 from KentoMoriwaki/patch-1
Add support for esm in node.js
2022-05-17 14:38:25 +02:00
Kento Moriwaki
43384e4148 Add support for esm in node.js 2022-05-17 21:18:04 +09:00
Kevin Jahns
a2b62b0a58 13.5.38 2022-05-14 18:11:57 +02:00
Kevin Jahns
6febf51b1a fix captureTransaction 2022-05-14 18:10:19 +02:00
Kevin Jahns
5a4816a1b2 13.5.37 2022-05-14 14:25:28 +02:00
Kevin Jahns
4ad8af9a80 Add option to UndoManager to filter transactions 2022-05-14 14:23:47 +02:00
Kevin Jahns
fc25136b25 13.5.36 2022-05-09 12:55:19 +02:00
Kevin Jahns
ece1fe5426 minimize changes when formatting text - #422 2022-05-09 12:53:26 +02:00
Kevin Jahns
40196ae0a3 add slidebeamer as user 2022-04-27 16:33:00 +02:00
Kevin Jahns
bdefe0526d 13.5.35 2022-04-23 15:15:47 +02:00
Kevin Jahns
dbbb86adc7 bump dependencies 2022-04-23 15:14:05 +02:00
Kevin Jahns
1c9c97ffe6 add yxml.forEach method - closes #421 2022-04-23 15:13:26 +02:00
Kevin Jahns
14c14de21e simplify testhelper 2022-04-20 18:01:33 +02:00
Kevin Jahns
71fad52854 fix typing information YMap.iterator - fixes #420 2022-04-20 13:09:56 +02:00
Kevin Jahns
3935ba1faa update sponsors message 2022-04-18 19:12:41 +02:00
Kevin Jahns
4aacb487d2 complete documentation about relative positions 2022-04-17 16:03:20 +02:00
Kevin Jahns
5f56baa23e remove circle-ci 2022-04-06 23:31:49 +02:00
Kevin Jahns
8d809ebacb Adding Skiff as a user 2022-04-05 09:54:45 +02:00
Kevin Jahns
92624afbff 13.5.34 2022-04-04 16:23:21 +02:00
Kevin Jahns
1e8efd5104 bump lib0 for compatibility with y-crdt 2022-04-04 16:21:41 +02:00
Kevin Jahns
7b680f1bda 13.5.33 2022-03-30 14:55:10 +02:00
Kevin Jahns
806bf3f6dd lint 2022-03-30 14:53:14 +02:00
Kevin Jahns
42fe19daf1 require updated lib0 dependency 2022-03-30 14:51:21 +02:00
Kevin Jahns
7d3de7fa07 add blocksurvey.io as a user 2022-03-28 10:50:48 +02:00
Kevin Jahns
63c1cb4eb9 bump dependencies 2022-03-28 10:38:54 +02:00
Kevin Jahns
be1449a7af 13.5.32 2022-03-26 10:31:13 +01:00
Kevin Jahns
a22b3cdbc1 add option to UndoManager to ignore remote map changes. implements #390 2022-03-26 10:29:19 +01:00
Kevin Jahns
e9a0dc4ed2 add destroy logic 2022-03-25 11:08:30 +01:00
Kevin Jahns
b0b276d964 13.5.31 2022-03-25 11:01:57 +01:00
Kevin Jahns
d3e117702c add method to add & remove tracked origins 2022-03-25 11:00:07 +01:00
Kevin Jahns
ff5067e149 13.5.30 2022-03-22 09:47:10 +01:00
Kevin Jahns
f80e39a477 Merge pull request #408 from rileyjshaw/upgrade-lib0
Upgrade lib0 from v0.2.43 to v0.2.47
2022-03-22 09:43:41 +01:00
Riley Shaw
f70198333a Upgrade lib0 from v0.2.43 to v0.2.47
After upgrading to the latest version (13.5.29), I started to see the
following error:

```
ERROR in ./node_modules/yjs/dist/yjs.mjs 4048:13-26
export 'isArray' (imported as 'array') was not found in 'lib0/array' (possible exports: appendTo, copy, create, equalFlat, every, flatten, from, last, some)
```

It looks like `array.isArray` was added to `lib0` in [v0.2.47](https://github.com/dmonad/lib0/releases/tag/v0.2.47).
Updating the version in `package.json` fixed the error in my project.
2022-03-21 19:16:23 -04:00
Kevin Jahns
3c31b22a92 13.5.29 2022-03-21 00:00:00 +01:00
Kevin Jahns
6b8cef29e2 address #398 2022-03-20 23:58:14 +01:00
Kevin Jahns
4a06492fb1 add stack-item-updated event to Y.UndoManager. implements #407 2022-03-20 22:49:23 +01:00
Kevin Jahns
46fbce0de8 more utility around Y.UndoManager 2022-03-20 22:41:33 +01:00
Kevin Jahns
239703fe5c Merge pull request #401 from dylans/patch-1
Add Living Spec to readme
2022-03-19 13:22:10 +01:00
Kevin Jahns
5e907e3281 Merge pull request #404 from alderzhang/alder/issue-403
fix decodeUpdateV2 bug
2022-03-15 13:53:12 +01:00
alderzhang
6aea35246b fix decodeUpdateV2 bug 2022-03-15 15:22:51 +08:00
Dylan Schiemann
5058189a46 Add Living Spec to readme
We've sponsored via GitHub and OpenCollective for a while and have made a few contributions. We'd like to be added to the list of users if appropriate. Thanks.
2022-03-13 04:23:17 -07:00
Kevin Jahns
4db3439bb1 Merge pull request #397 from sep2/patch-1
add immer-yjs to Bindings
2022-03-09 22:06:56 +01:00
Felix
aa5463b06d add immer-yjs to Bindings 2022-03-07 11:45:50 +08:00
Kevin Jahns
afe8e52840 13.5.28 2022-03-02 13:27:33 +01:00
Kevin Jahns
d0f9c4a27f lint 2022-03-02 13:25:56 +01:00
Kevin Jahns
a5ffdce342 bump dependencies 2022-03-02 13:23:18 +01:00
Kevin Jahns
67d27dfca2 update readme 2022-03-02 13:19:00 +01:00
Kevin Jahns
9f1548204a Merge pull request #376 from fson/yevent-target-type
Add more accurate typing for YEvent.target
2022-03-02 13:18:00 +01:00
Kevin Jahns
46e108f345 Merge pull request #388 from sanalabs/uk/decode-methods
Add decode methods
2022-03-01 15:16:59 +01:00
Kevin Jahns
bda622f523 Merge pull request #391 from Flamenco/patch-1
Fix typo
2022-03-01 15:15:58 +01:00
Kevin Jahns
fef9e39d91 Merge pull request #393 from dkuhnert/issue-392
cleanup redundant text attributes when delete attributes
2022-02-24 22:07:49 +01:00
Kevin Jahns
5751a12c11 add PRSM to projects 2022-02-24 14:45:17 +01:00
dkuhnert
fddb620d41 cleanup redundant text attributes when delete attributes
fixes #392
2022-02-23 18:20:26 +01:00
dkuhnert
abf3fab1b6 cleanup redundant text attributes when delete attributes
fixes #392
2022-02-23 14:53:31 +01:00
Flamenco
69e2375dc5 Fix typo 2022-02-18 08:15:45 -05:00
Kevin Jahns
058a50285c add dynaboard to the list of projects 2022-02-16 21:15:19 +01:00
Ulf Karlsson
8678ef62d6 Remove curr.info 2022-02-07 20:00:10 +01:00
Ulf Karlsson
db53b6c720 Add decode methods 2022-02-07 17:15:06 +01:00
Ville Immonen
81a36a2762 add more accurate typing for YEvent.target 2022-01-15 14:22:17 +02:00
26 changed files with 4868 additions and 595 deletions

View File

@@ -1,7 +0,0 @@
version: 2.1
orbs:
node: circleci/node@3.0.0
workflows:
node-tests:
jobs:
- node/test

View File

@@ -152,7 +152,7 @@ concepts that can be used to create a custom network protocol:
an incremental document updates that allows clients to sync with each other. an incremental document updates that allows clients to sync with each other.
The update object is an Uint8Array that efficiently encodes `Item` objects and The update object is an Uint8Array that efficiently encodes `Item` objects and
the delete set. the delete set.
* `state vector`: A state vector defines the know state of each user (a set of * `state vector`: A state vector defines the known state of each user (a set of
tuples `(client, clock)`). This object is also efficiently encoded as a tuples `(client, clock)`). This object is also efficiently encoded as a
Uint8Array. Uint8Array.

120
README.md
View File

@@ -15,54 +15,43 @@ suited for even large documents.
* Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos) * Demos: [https://github.com/yjs/yjs-demos](https://github.com/yjs/yjs-demos)
* Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev) * Discuss: [https://discuss.yjs.dev](https://discuss.yjs.dev)
* Chat: [Gitter](https://gitter.im/Yjs/community) | [Discord](https://discord.gg/T3nqMT6qbM)
* Benchmark Yjs vs. Automerge: * Benchmark Yjs vs. Automerge:
[https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks) [https://github.com/dmonad/crdt-benchmarks](https://github.com/dmonad/crdt-benchmarks)
* Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0) * Podcast [**"Yjs Deep Dive into real time collaborative editing solutions":**](https://www.tag1consulting.com/blog/deep-dive-real-time-collaborative-editing-solutions-tagteamtalk-001-0)
* Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/) * Podcast [**"Google Docs-style editing in Gutenberg with the YJS framework":**](https://publishpress.com/blog/yjs/)
:construction_worker_woman: If you are looking for professional (paid) support to :construction_worker_woman: If you are looking for professional support, please
build collaborative or distributed applications ping us at consider supporting this project via a "support contract" on
<yjs@tag1consulting.com>. Otherwise you can find help on our [GitHub Sponsors](https://github.com/sponsors/dmonad). I will attend your issues
[discussion board](https://discuss.yjs.dev). quicker and we can discuss questions and problems in regular video conferences.
Otherwise you can find help on our community [discussion board](https://discuss.yjs.dev).
## Sponsors ## Sponsorship
I'm currently looking for sponsors that allow me to be less dependent on Please contribute to the project financially - especially if your company relies
contracting work. These awesome backers already fund further development of on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
Yjs:
[![davidhq](https://github.com/davidhq.png?size=60)](https://github.com/davidhq)
[![Ifiok Jr.](https://github.com/ifiokjr.png?size=60)](https://github.com/ifiokjr)
[![Burke Libbey](https://github.com/burke.png?size=60)](https://github.com/burke)
[![Beni Cherniavsky-Paskin](https://github.com/cben.png?size=60)](https://github.com/cben)
[![Tom Moor](https://github.com/tommoor.png?size=60)](https://github.com/tommoor)
[![Michael Meyers](https://github.com/michaelemeyers.png?size=60)](https://github.com/michaelemeyers)
[![Cristiano Benjamin](https://github.com/csbenjamin.png?size=60)](https://github.com/csbenjamin)
[![Braden](https://github.com/AdventureBeard.png?size=60)](https://github.com/AdventureBeard)
[![nimbuswebinc](https://nimbusweb.me/new-style-img/note-icon.svg)](https://github.com/nimbuswebinc)
[![JourneyApps](https://github.com/journeyapps.png?size=60)](https://github.com/journeyapps)
[![Adam Brunnmeier](https://github.com/adabru.png?size=60)](https://github.com/adabru)
[![Nathanael Anderson](https://github.com/NathanaelA.png?size=60)](https://github.com/NathanaelA)
[<img src="https://room.sh/img/icons/android-chrome-192x192.png" height="60px" />](https://room.sh/)
Sponsorship also comes with special perks! [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%20Sponsor&message=%E2%9D%A4&logo=GitHub&style=flat&color=d42f2d)](https://github.com/sponsors/dmonad)
## Who is using Yjs ## Who is using Yjs
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted * [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
collaborative notes app.
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and * [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star2: community. :star:
* [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:
* [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by * [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by
Nimbus Web. Nimbus Web. :star:
* [JoeDocs](https://joedocs.com/) An open collaborative wiki.
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to * [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
collaboratively organize radio broadcasts. collaboratively organize radio broadcasts. :star:
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app.
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
* [Alldone](https://alldone.app/) A next-gen project management and * [Alldone](https://alldone.app/) A next-gen project management and
collaboration platform. collaboration platform.
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
* [Slidebeamer](https://slidebeamer.com/) Presentation app.
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
* [Skiff](https://skiff.org/) Private, decentralized workspace.
## Table of Contents ## Table of Contents
@@ -95,6 +84,7 @@ are implemented in separate modules.
| [Monaco](https://microsoft.github.io/monaco-editor/) | ✔ | [y-monaco](https://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) |
| [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) | | [Slate](https://github.com/ianstormtaylor/slate) | ✔ | [slate-yjs](https://github.com/bitphinix/slate-yjs) | [demo](https://bitphinix.github.io/slate-yjs-example) |
| [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) | | [valtio](https://github.com/pmndrs/valtio) | | [valtio-yjs](https://github.com/dai-shi/valtio-yjs) | [demo](https://codesandbox.io/s/valtio-yjs-demo-ox3iy) |
| [immer](https://github.com/immerjs/immer) | | [immer-yjs](https://github.com/sep2/immer-yjs) | [demo](https://codesandbox.io/s/immer-yjs-demo-6e0znb) |
| React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) | | React / Vue / Svelte / MobX | | [SyncedStore](https://syncedstore.org) | [demo](https://syncedstore.org/docs/react) |
### Providers ### Providers
@@ -803,6 +793,21 @@ Convert V2 update format to the V1 update format.
### Relative Positions ### Relative Positions
When working with collaborative documents, we often need to work with positions.
Positions may represent cursor locations, selection ranges, or even assign a
comment to a range of text. Normal index-positions (expressed as integers) are
not convenient to use because the index-range is invalidated as soon as a remote
change manipulates the document. Relative positions give you a powerful API to
express positions.
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 #### Example: Transform to RelativePosition and back
```js ```js
@@ -837,14 +842,35 @@ pos.index === 2 // => true
``` ```
<dl> <dl>
<b><code>Y.createRelativePositionFromTypeIndex(Uint8Array|Y.Type, number)</code></b> <b><code>
<dd></dd> Y.createRelativePositionFromTypeIndex(type:Uint8Array|Y.Type, index: number
<b><code>Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc)</code></b> [, assoc=0])
<dd></dd> </code></b>
<b><code>Y.encodeRelativePosition(RelativePosition):Uint8Array</code></b> <dd>
<dd></dd> Create a relative position fixated to the i-th element in any sequence-like
shared type (if <code>assoc >= 0</code>). By default, the position associates
with the character that comes after the specified index position. If
<code>assoc < 0</code>, then the relative position associates with the character
before the specified index position.
</dd>
<b><code>
Y.createAbsolutePositionFromRelativePosition(RelativePosition, Y.Doc):
{ type: Y.AbstractType, index: number, assoc: number } | null
</code></b>
<dd>
Create an absolute position from a relative position. If the relative position
cannot be referenced, or the type is deleted, then the result is null.
</dd>
<b><code>
Y.encodeRelativePosition(RelativePosition):Uint8Array
</code></b>
<dd>
Encode a relative position to an Uint8Array. Binary data is the preferred
encoding format for document updates. If you prefer JSON encoding, you can
simply JSON.stringify / JSON.parse the relative position instead.
</dd>
<b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b> <b><code>Y.decodeRelativePosition(Uint8Array):RelativePosition</code></b>
<dd></dd> <dd>Decode a binary-encoded relative position to a RelativePositon object.</dd>
</dl> </dl>
### Y.UndoManager ### Y.UndoManager
@@ -885,6 +911,16 @@ undo- or the redo-stack.
</dd> </dd>
<b> <b>
<code> <code>
on('stack-item-updated', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' })
</code>
</b>
<dd>
Register an event that is called when an existing <code>StackItem</code> is updated.
This happens when two changes happen within a "captureInterval".
</dd>
<b>
<code>
on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo' on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
| 'redo' }) | 'redo' })
</code> </code>
@@ -893,6 +929,14 @@ on('stack-item-popped', { stackItem: { meta: Map&lt;any,any&gt; }, type: 'undo'
Register an event that is called when a <code>StackItem</code> is popped from Register an event that is called when a <code>StackItem</code> is popped from
the undo- or the redo-stack. the undo- or the redo-stack.
</dd> </dd>
<b>
<code>
on('stack-cleared', { undoStackCleared: boolean, redoStackCleared: boolean })
</code>
</b>
<dd>
Register an event that is called when the undo- and/or the redo-stack is cleared.
</dd>
</dl> </dl>
#### Example: Stop Capturing #### Example: Stop Capturing

4959
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.27", "version": "13.5.39",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
@@ -26,6 +26,7 @@
}, },
"exports": { "exports": {
".": { ".": {
"types": "./dist/src/index.d.ts",
"import": "./dist/yjs.mjs", "import": "./dist/yjs.mjs",
"require": "./dist/yjs.cjs" "require": "./dist/yjs.cjs"
}, },
@@ -73,7 +74,7 @@
}, },
"homepage": "https://docs.yjs.dev", "homepage": "https://docs.yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.2.43" "lib0": "^0.2.49"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0", "@rollup/plugin-commonjs": "^17.0.0",

View File

@@ -67,6 +67,8 @@ export {
decodeStateVector, decodeStateVector,
logUpdate, logUpdate,
logUpdateV2, logUpdateV2,
decodeUpdate,
decodeUpdateV2,
relativePositionToJSON, relativePositionToJSON,
isDeleted, isDeleted,
isParentOf, isParentOf,
@@ -87,7 +89,8 @@ export {
diffUpdate, diffUpdate,
diffUpdateV2, diffUpdateV2,
convertUpdateFormatV1ToV2, convertUpdateFormatV1ToV2,
convertUpdateFormatV2ToV1 convertUpdateFormatV2ToV1,
UpdateEncoderV1
} from './internals.js' } from './internals.js'
const glo = /** @type {any} */ (typeof window !== 'undefined' const glo = /** @type {any} */ (typeof window !== 'undefined'
@@ -99,9 +102,9 @@ const importIdentifier = '__ $YJS$ __'
if (glo[importIdentifier] === true) { if (glo[importIdentifier] === true) {
/** /**
* Dear reader of this warning message. Please take this seriously. * Dear reader of this message. Please take this seriously.
* *
* If you see this message, please make sure that you only import one version of Yjs. In many cases, * If you see this message, make sure that you only import one version of Yjs. In many cases,
* your package manager installs two versions of Yjs that are used by different packages within your project. * your package manager installs two versions of Yjs that are used by different packages within your project.
* Another reason for this message is that some parts of your project use the commonjs version of Yjs * Another reason for this message is that some parts of your project use the commonjs version of Yjs
* and others use the EcmaScript version of Yjs. * and others use the EcmaScript version of Yjs.
@@ -110,6 +113,6 @@ if (glo[importIdentifier] === true) {
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to * e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
* do the constructor checks anymore - which might break the CRDT algorithm. * do the constructor checks anymore - which might break the CRDT algorithm.
*/ */
console.warn('Yjs was already imported. Importing different versions of Yjs often leads to issues.') console.error('Yjs was already imported. This breaks constructor checks and will lead to isssues!')
} }
glo[importIdentifier] = true glo[importIdentifier] = true

View File

@@ -100,4 +100,4 @@ export class ContentFormat {
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentFormat} * @return {ContentFormat}
*/ */
export const readContentFormat = decoder => new ContentFormat(decoder.readString(), decoder.readJSON()) export const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON())

View File

@@ -39,7 +39,7 @@ export const YXmlTextRefID = 6
*/ */
export class ContentType { export class ContentType {
/** /**
* @param {AbstractType<YEvent>} type * @param {AbstractType<any>} type
*/ */
constructor (type) { constructor (type) {
/** /**

View File

@@ -127,12 +127,13 @@ export const splitItem = (transaction, leftItem, diff) => {
* @param {Item} item * @param {Item} item
* @param {Set<Item>} redoitems * @param {Set<Item>} redoitems
* @param {DeleteSet} itemsToDelete * @param {DeleteSet} itemsToDelete
* @param {boolean} ignoreRemoteMapChanges
* *
* @return {Item|null} * @return {Item|null}
* *
* @private * @private
*/ */
export const redoItem = (transaction, item, redoitems, itemsToDelete) => { export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
const doc = transaction.doc const doc = transaction.doc
const store = doc.store const store = doc.store
const ownClientID = doc.clientID const ownClientID = doc.clientID
@@ -152,7 +153,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
// make sure that parent is redone // make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true) { if (parentItem !== null && parentItem.deleted === true) {
// try to undo parent if it will be undone anyway // try to undo parent if it will be undone anyway
if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete) === null)) { if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges) === null)) {
return null return null
} }
while (parentItem.redone !== null) { while (parentItem.redone !== null) {
@@ -198,7 +199,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
} }
} else { } else {
right = null right = null
if (item.right) { if (item.right && !ignoreRemoteMapChanges) {
left = item left = item
// Iterate right while right is in itemsToDelete // Iterate right while right is in itemsToDelete
// If it is intended to delete right while item is redone, we can expect that item should replace right. // If it is intended to delete right while item is redone, we can expect that item should replace right.

View File

@@ -278,7 +278,7 @@ export class AbstractType {
this._eH = createEventHandler() this._eH = createEventHandler()
/** /**
* Deep event handlers * Deep event handlers
* @type {EventHandler<Array<YEvent>,Transaction>} * @type {EventHandler<Array<YEvent<any>>,Transaction>}
*/ */
this._dEH = createEventHandler() this._dEH = createEventHandler()
/** /**
@@ -364,7 +364,7 @@ export class AbstractType {
/** /**
* Observe all events that are created by this type and its children. * Observe all events that are created by this type and its children.
* *
* @param {function(Array<YEvent>,Transaction):void} f Observer function * @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/ */
observeDeep (f) { observeDeep (f) {
addEventHandlerListener(this._dEH, f) addEventHandlerListener(this._dEH, f)
@@ -382,7 +382,7 @@ export class AbstractType {
/** /**
* Unregister an observer function. * Unregister an observer function.
* *
* @param {function(Array<YEvent>,Transaction):void} f Observer function * @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/ */
unobserveDeep (f) { unobserveDeep (f) {
removeEventHandlerListener(this._dEH, f) removeEventHandlerListener(this._dEH, f)

View File

@@ -23,6 +23,7 @@ import { typeListSlice } from './AbstractType.js'
/** /**
* Event that describes the changes on a YArray * Event that describes the changes on a YArray
* @template T * @template T
* @extends YEvent<YArray<T>>
*/ */
export class YArrayEvent extends YEvent { export class YArrayEvent extends YEvent {
/** /**

View File

@@ -21,6 +21,7 @@ import * as iterator from 'lib0/iterator'
/** /**
* @template T * @template T
* @extends YEvent<YMap<T>>
* Event that describes the changes on a YMap. * Event that describes the changes on a YMap.
*/ */
export class YMapEvent extends YEvent { export class YMapEvent extends YEvent {
@@ -179,7 +180,9 @@ export class YMap extends AbstractType {
} }
/** /**
* @return {IterableIterator<MapType>} * Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/ */
[Symbol.iterator] () { [Symbol.iterator] () {
return this.entries() return this.entries()

View File

@@ -291,7 +291,17 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes) const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// iterate until first non-format or null is found // iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null // delete all formats with attributes[format.key] != null
while (length > 0 && currPos.right !== null) { // also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
// eslint-disable-next-line no-labels
iterationLoop: while (
currPos.right !== null &&
(length > 0 ||
(
negatedAttributes.size > 0 &&
(currPos.right.deleted || currPos.right.content.constructor === ContentFormat)
)
)
) {
if (!currPos.right.deleted) { if (!currPos.right.deleted) {
switch (currPos.right.content.constructor) { switch (currPos.right.content.constructor) {
case ContentFormat: { case ContentFormat: {
@@ -301,9 +311,16 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
if (equalAttrs(attr, value)) { if (equalAttrs(attr, value)) {
negatedAttributes.delete(key) negatedAttributes.delete(key)
} else { } else {
if (length === 0) {
// no need to further extend negatedAttributes
// eslint-disable-next-line no-labels
break iterationLoop
}
negatedAttributes.set(key, value) negatedAttributes.set(key, value)
} }
currPos.right.delete(transaction) currPos.right.delete(transaction)
} else {
currPos.currentAttributes.set(key, value)
} }
break break
} }
@@ -511,6 +528,7 @@ const deleteText = (transaction, currPos, length) => {
*/ */
/** /**
* @extends YEvent<YText>
* Event that describes the changes on a YText type. * Event that describes the changes on a YText type.
*/ */
export class YTextEvent extends YEvent { export class YTextEvent extends YEvent {

View File

@@ -5,6 +5,7 @@ import {
} from '../internals.js' } from '../internals.js'
/** /**
* @extends YEvent<YXmlElement|YXmlText|YXmlFragment>
* An Event that describes changes on a YXml Element or Yxml Fragment * An Event that describes changes on a YXml Element or Yxml Fragment
*/ */
export class YXmlEvent extends YEvent { export class YXmlEvent extends YEvent {

View File

@@ -404,6 +404,15 @@ export class YXmlFragment extends AbstractType {
return typeListSlice(this, start, end) return typeListSlice(this, start, end)
} }
/**
* Executes a provided function on once on overy child element.
*
* @param {function(YXmlElement|YXmlText,number, typeof this):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
}
/** /**
* 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.

View File

@@ -48,7 +48,7 @@ export class Doc extends Observable {
this.guid = guid this.guid = guid
this.collectionid = collectionid this.collectionid = collectionid
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent<any>>>}
*/ */
this.share = new Map() this.share = new Map()
this.store = new StructStore() this.store = new StructStore()

View File

@@ -75,13 +75,13 @@ export class Transaction {
* All types that were directly modified (property added or child * All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set. * inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item.parentSub = null` for YArray) * Maps from type to parentSubs (`item.parentSub = null` for YArray)
* @type {Map<AbstractType<YEvent>,Set<String|null>>} * @type {Map<AbstractType<YEvent<any>>,Set<String|null>>}
*/ */
this.changed = new Map() this.changed = new Map()
/** /**
* Stores the events for the types that observe also child elements. * Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`. * It is mainly used by `observeDeep`.
* @type {Map<AbstractType<YEvent>,Array<YEvent>>} * @type {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>}
*/ */
this.changedParentTypes = new Map() this.changedParentTypes = new Map()
/** /**
@@ -148,7 +148,7 @@ export const nextID = transaction => {
* did not change, it was just added and we should not fire events for `type`. * did not change, it was just added and we should not fire events for `type`.
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<YEvent>} type * @param {AbstractType<YEvent<any>>} type
* @param {string|null} parentSub * @param {string|null} parentSub
*/ */
export const addChangedTypeToTransaction = (transaction, type, parentSub) => { export const addChangedTypeToTransaction = (transaction, type, parentSub) => {

View File

@@ -14,6 +14,7 @@ import {
} from '../internals.js' } from '../internals.js'
import * as time from 'lib0/time' import * as time from 'lib0/time'
import * as array from 'lib0/array'
import { Observable } from 'lib0/observable' import { Observable } from 'lib0/observable'
class StackItem { class StackItem {
@@ -30,6 +31,18 @@ class StackItem {
this.meta = new Map() this.meta = new Map()
} }
} }
/**
* @param {Transaction} tr
* @param {UndoManager} um
* @param {StackItem} stackItem
*/
const clearUndoManagerStackItem = (tr, um, stackItem) => {
iterateDeletedStructs(tr, stackItem.deletions, item => {
if (item instanceof Item && um.scope.some(type => isParentOf(type, item))) {
keepItem(item, false)
}
})
}
/** /**
* @param {UndoManager} undoManager * @param {UndoManager} undoManager
@@ -88,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
} }
}) })
itemsToRedo.forEach(struct => { itemsToRedo.forEach(struct => {
performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions) !== null || performedChange performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges) !== null || performedChange
}) })
// We want to delete in reverse order so that children are deleted before // We want to delete in reverse order so that children are deleted before
// parents, so we have more information available when items are filtered. // parents, so we have more information available when items are filtered.
@@ -119,11 +132,13 @@ const popStackItem = (undoManager, stack, eventType) => {
/** /**
* @typedef {Object} UndoManagerOptions * @typedef {Object} UndoManagerOptions
* @property {number} [UndoManagerOptions.captureTimeout=500] * @property {number} [UndoManagerOptions.captureTimeout=500]
* @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false.
* @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes * @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes
* it is necessary to filter whan an Undo/Redo operation can delete. If this * it is necessary to filter whan an Undo/Redo operation can delete. If this
* filter returns false, the type/item won't be deleted even it is in the * filter returns false, the type/item won't be deleted even it is in the
* undo/redo scope. * undo/redo scope.
* @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])] * @property {Set<any>} [UndoManagerOptions.trackedOrigins=new Set([null])]
* @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..).
*/ */
/** /**
@@ -133,19 +148,30 @@ const popStackItem = (undoManager, stack, eventType) => {
* Fires 'stack-item-popped' event when a stack item was popped from either the * Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`. * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
* *
* @extends {Observable<'stack-item-added'|'stack-item-popped'>} * @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>}
*/ */
export class UndoManager extends Observable { export class UndoManager extends Observable {
/** /**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types * @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options * @param {UndoManagerOptions} options
*/ */
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) { constructor (typeScope, {
captureTimeout = 500,
captureTransaction = tr => true,
deleteFilter = () => true,
trackedOrigins = new Set([null]),
ignoreRemoteMapChanges = false
} = {}) {
super() super()
this.scope = typeScope instanceof Array ? typeScope : [typeScope] /**
* @type {Array<AbstractType<any>>}
*/
this.scope = []
this.addToScope(typeScope)
this.deleteFilter = deleteFilter this.deleteFilter = deleteFilter
trackedOrigins.add(this) trackedOrigins.add(this)
this.trackedOrigins = trackedOrigins this.trackedOrigins = trackedOrigins
this.captureTransaction = captureTransaction
/** /**
* @type {Array<StackItem>} * @type {Array<StackItem>}
*/ */
@@ -163,9 +189,17 @@ export class UndoManager extends Observable {
this.redoing = false this.redoing = false
this.doc = /** @type {Doc} */ (this.scope[0].doc) this.doc = /** @type {Doc} */ (this.scope[0].doc)
this.lastChange = 0 this.lastChange = 0
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => { this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
/**
* @param {Transaction} transaction
*/
this.afterTransactionHandler = transaction => {
// Only track certain transactions // Only track certain transactions
if (!this.scope.some(type => transaction.changedParentTypes.has(type)) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))) { if (
!this.captureTransaction(transaction) ||
!this.scope.some(type => transaction.changedParentTypes.has(type)) ||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
) {
return return
} }
const undoing = this.undoing const undoing = this.undoing
@@ -175,7 +209,7 @@ export class UndoManager extends Observable {
this.stopCapturing() // next undo should not be appended to last stack item this.stopCapturing() // next undo should not be appended to last stack item
} else if (!redoing) { } else if (!redoing) {
// neither undoing nor redoing: delete redoStack // neither undoing nor redoing: delete redoStack
this.redoStack = [] this.clear(false, true)
} }
const insertions = new DeleteSet() const insertions = new DeleteSet()
transaction.afterState.forEach((endClock, client) => { transaction.afterState.forEach((endClock, client) => {
@@ -206,29 +240,59 @@ export class UndoManager extends Observable {
keepItem(item, true) keepItem(item, true)
} }
}) })
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
if (didAdd) { if (didAdd) {
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]) this.emit('stack-item-added', changeEvent)
} else {
this.emit('stack-item-updated', changeEvent)
}
}
this.doc.on('afterTransaction', this.afterTransactionHandler)
this.doc.on('destroy', () => {
this.destroy()
})
}
/**
* @param {Array<AbstractType<any>> | AbstractType<any>} ytypes
*/
addToScope (ytypes) {
ytypes = array.isArray(ytypes) ? ytypes : [ytypes]
ytypes.forEach(ytype => {
if (this.scope.every(yt => yt !== ytype)) {
this.scope.push(ytype)
} }
}) })
} }
clear () { /**
this.doc.transact(transaction => { * @param {any} origin
/** */
* @param {StackItem} stackItem addTrackedOrigin (origin) {
*/ this.trackedOrigins.add(origin)
const clearItem = stackItem => { }
iterateDeletedStructs(transaction, stackItem.deletions, item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) { /**
keepItem(item, false) * @param {any} origin
} */
}) removeTrackedOrigin (origin) {
} this.trackedOrigins.delete(origin)
this.undoStack.forEach(clearItem) }
this.redoStack.forEach(clearItem)
}) clear (clearUndoStack = true, clearRedoStack = true) {
this.undoStack = [] if ((clearUndoStack && this.canUndo()) || (clearRedoStack && this.canRedo())) {
this.redoStack = [] this.doc.transact(tr => {
if (clearUndoStack) {
this.undoStack.forEach(item => clearUndoManagerStackItem(tr, this, item))
this.undoStack = []
}
if (clearRedoStack) {
this.redoStack.forEach(item => clearUndoManagerStackItem(tr, this, item))
this.redoStack = []
}
this.emit('stack-cleared', [{ undoStackCleared: clearUndoStack, redoStackCleared: clearRedoStack }])
})
}
} }
/** /**
@@ -304,4 +368,10 @@ export class UndoManager extends Observable {
canRedo () { canRedo () {
return this.redoStack.length > 0 return this.redoStack.length > 0
} }
destroy () {
this.trackedOrigins.delete(this)
this.doc.off('afterTransaction', this.afterTransactionHandler)
super.destroy()
}
} }

View File

@@ -298,10 +298,24 @@ export class UpdateEncoderV2 extends DSEncoderV2 {
writeKey (key) { writeKey (key) {
const clock = this.keyMap.get(key) const clock = this.keyMap.get(key)
if (clock === undefined) { if (clock === undefined) {
/**
* @todo uncomment to introduce this feature finally
*
* Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString.
* Furthermore, I forgot to set the keyclock. So everything was working fine.
*
* However, this feature here is basically useless as it is not being used (it actually only consumes extra memory).
*
* I don't know yet how to reintroduce this feature..
*
* Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag.
*
*/
// this.keyMap.set(key, this.keyClock)
this.keyClockEncoder.write(this.keyClock++) this.keyClockEncoder.write(this.keyClock++)
this.stringEncoder.write(key) this.stringEncoder.write(key)
} else { } else {
this.keyClockEncoder.write(this.keyClock++) this.keyClockEncoder.write(clock)
} }
} }
} }

View File

@@ -8,17 +8,18 @@ import * as set from 'lib0/set'
import * as array from 'lib0/array' import * as array from 'lib0/array'
/** /**
* @template {AbstractType<any>} T
* YEvent describes the changes on a YType. * YEvent describes the changes on a YType.
*/ */
export class YEvent { export class YEvent {
/** /**
* @param {AbstractType<any>} target The changed type. * @param {T} target The changed type.
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
constructor (target, transaction) { constructor (target, transaction) {
/** /**
* The type on which this event was created on. * The type on which this event was created on.
* @type {AbstractType<any>} * @type {T}
*/ */
this.target = target this.target = target
/** /**

View File

@@ -112,6 +112,30 @@ export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
logging.print('DeleteSet: ', ds) logging.print('DeleteSet: ', ds)
} }
/**
* @param {Uint8Array} update
*
*/
export const decodeUpdate = (update) => decodeUpdateV2(update, UpdateDecoderV1)
/**
* @param {Uint8Array} update
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} [YDecoder]
*
*/
export const decodeUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
const structs = []
const updateDecoder = new YDecoder(decoding.createDecoder(update))
const lazyDecoder = new LazyStructReader(updateDecoder, false)
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
structs.push(curr)
}
return {
structs,
ds: readDeleteSet(updateDecoder)
}
}
export class LazyStructWriter { export class LazyStructWriter {
/** /**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder

View File

@@ -72,9 +72,9 @@ export const testPermanentUserData = async tc => {
export const testDiffStateVectorOfUpdateIsEmpty = tc => { export const testDiffStateVectorOfUpdateIsEmpty = tc => {
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
/** /**
* @type {null | Uint8Array} * @type {any}
*/ */
let sv = /* any */ (null) let sv = null
ydoc.getText().insert(0, 'a') ydoc.getText().insert(0, 'a')
ydoc.on('update', update => { ydoc.on('update', update => {
sv = Y.encodeStateVectorFromUpdate(update) sv = Y.encodeStateVectorFromUpdate(update)

View File

@@ -5,6 +5,7 @@ import * as encoding from 'lib0/encoding'
import * as decoding from 'lib0/decoding' import * as decoding from 'lib0/decoding'
import * as syncProtocol from 'y-protocols/sync' import * as syncProtocol from 'y-protocols/sync'
import * as object from 'lib0/object' import * as object from 'lib0/object'
import * as map from 'lib0/map'
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
export * from '../src/index.js' export * from '../src/index.js'
@@ -89,8 +90,8 @@ export class TestYInstance extends Y.Doc {
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, update) syncProtocol.writeUpdate(encoder, update)
broadcastMessage(this, encoding.toUint8Array(encoder)) broadcastMessage(this, encoding.toUint8Array(encoder))
this.updates.push(update)
} }
this.updates.push(update)
}) })
this.connect() this.connect()
} }
@@ -133,12 +134,7 @@ export class TestYInstance extends Y.Doc {
* @param {TestYInstance} remoteClient * @param {TestYInstance} remoteClient
*/ */
_receive (message, remoteClient) { _receive (message, remoteClient) {
let messages = this.receiving.get(remoteClient) map.setIfUndefined(this.receiving, remoteClient, () => []).push(message)
if (messages === undefined) {
messages = []
this.receiving.set(remoteClient, messages)
}
messages.push(message)
} }
} }
@@ -202,17 +198,6 @@ export class TestConnector {
// send reply message // send reply message
sender._receive(encoding.toUint8Array(encoder), receiver) sender._receive(encoding.toUint8Array(encoder), receiver)
} }
{
// If update message, add the received message to the list of received messages
const decoder = decoding.createDecoder(m)
const messageType = decoding.readVarUint(decoder)
switch (messageType) {
case syncProtocol.messageYjsUpdate:
case syncProtocol.messageYjsSyncStep2:
receiver.updates.push(decoding.readVarUint8Array(decoder))
break
}
}
return true return true
} }
return false return false

View File

@@ -527,3 +527,64 @@ export const testUndoBlockBug = tc => {
undoManager.redo() // {"text":{}} undoManager.redo() // {"text":{}}
t.compare(design.toJSON(), { text: { blocks: { text: '4' } } }) t.compare(design.toJSON(), { text: { blocks: { text: '4' } } })
} }
/**
* Undo text formatting delete should not corrupt peer state.
*
* @see https://github.com/yjs/yjs/issues/392
* @param {t.TestCase} tc
*/
export const testUndoDeleteTextFormat = tc => {
const doc = new Y.Doc()
const text = doc.getText()
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
const doc2 = new Y.Doc()
const text2 = doc2.getText()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const undoManager = new Y.UndoManager(text)
text.format(13, 7, { bold: true })
undoManager.stopCapturing()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(16, 4, { bold: null })
undoManager.stopCapturing()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
undoManager.undo()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const expect = [
{ insert: 'Attack ships ' },
{
insert: 'on fire',
attributes: { bold: true }
},
{ insert: ' off the shoulder of Orion.' }
]
t.compare(text.toDelta(), expect)
t.compare(text2.toDelta(), expect)
}
/**
* Undo text formatting delete should not corrupt peer state.
*
* @see https://github.com/yjs/yjs/issues/392
* @param {t.TestCase} tc
*/
export const testBehaviorOfIgnoreremotemapchangesProperty = tc => {
const doc = new Y.Doc()
const doc2 = new Y.Doc()
doc.on('update', update => Y.applyUpdate(doc2, update, doc))
doc2.on('update', update => Y.applyUpdate(doc, update, doc2))
const map1 = doc.getMap()
const map2 = doc2.getMap()
const um1 = new Y.UndoManager(map1, { ignoreRemoteMapChanges: true })
map1.set('x', 1)
map2.set('x', 2)
map1.set('x', 3)
map2.set('x', 4)
um1.undo()
t.assert(map1.get('x') === 2)
t.assert(map2.get('x') === 2)
}

View File

@@ -112,6 +112,24 @@ export const testMergeUpdates = tc => {
}) })
} }
/**
* @param {t.TestCase} tc
*/
export const testKeyEncoding = tc => {
const { users, text0, text1 } = init(tc, { users: 2 })
text0.insert(0, 'a', { italic: true })
text0.insert(0, 'b')
text0.insert(0, 'c', { italic: true })
const update = Y.encodeStateAsUpdateV2(users[0])
Y.applyUpdateV2(users[1], update)
t.compare(text1.toDelta(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }])
compare(users)
}
/** /**
* @param {Y.Doc} ydoc * @param {Y.Doc} ydoc
* @param {Array<Uint8Array>} updates - expecting at least 4 updates * @param {Array<Uint8Array>} updates - expecting at least 4 updates

View File

@@ -607,6 +607,35 @@ export const testFormattingBug = async tc => {
console.log(text1.toDelta()) console.log(text1.toDelta())
} }
/**
* Delete formatting should not leave redundant formatting items.
*
* @param {t.TestCase} tc
*/
export const testDeleteFormatting = tc => {
const doc = new Y.Doc()
const text = doc.getText()
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
const doc2 = new Y.Doc()
const text2 = doc2.getText()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(13, 7, { bold: true })
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(16, 4, { bold: null })
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const expected = [
{ insert: 'Attack ships ' },
{ insert: 'on ', attributes: { bold: true } },
{ insert: 'fire off the shoulder of Orion.' }
]
t.compare(text.toDelta(), expected)
t.compare(text2.toDelta(), expected)
}
// RANDOM TESTS // RANDOM TESTS
let charCounter = 0 let charCounter = 0