Compare commits

..

48 Commits

Author SHA1 Message Date
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
Kevin Jahns
3f34777201 13.5.27 2022-02-04 12:42:59 +01:00
Kevin Jahns
24eddb2d75 fix concurrent formatting / cleanup bug 2022-02-04 12:41:13 +01:00
Kevin Jahns
8ce107bd17 Merge branch 'ja2nicholl-update-attributes-on-delete' 2022-02-04 11:27:07 +01:00
Kevin Jahns
2d1e3fde43 fixed edge formatting case 2022-02-04 11:26:32 +01:00
Kevin Jahns
04009f0d42 Merge branch 'update-attributes-on-delete' of git://github.com/ja2nicholl/yjs into ja2nicholl-update-attributes-on-delete 2022-02-04 10:39:41 +01:00
Jeremy Nicholl
84e95f11cb Fix formatting 2022-02-03 15:19:57 -05:00
Jeremy Nicholl
164b38f0cd Avoid copying attribute map when deleting
Calling cleanupFormattingGap should not make a copy of the
attributes because it needs to be able to update them.
2022-01-31 14:49:16 -05:00
Ville Immonen
81a36a2762 add more accurate typing for YEvent.target 2022-01-15 14:22:17 +02:00
23 changed files with 4703 additions and 519 deletions

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.
The update object is an Uint8Array that efficiently encodes `Item` objects and
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
Uint8Array.

View File

@@ -25,44 +25,30 @@ build collaborative or distributed applications ping us at
<yjs@tag1consulting.com>. Otherwise you can find help on our
[discussion board](https://discuss.yjs.dev).
## Sponsors
## Sponsorship
I'm currently looking for sponsors that allow me to be less dependent on
contracting work. These awesome backers already fund further development of
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)
Please contribute to the project financially if your company relies on Yjs.
Support the project and receive more attention on your tickets.
[![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
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app.
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star2:
* [Input](https://input.com/) A collaborative note taking app. :star2:
* [Room.sh](https://room.sh/) A meeting application with integrated
collaborative drawing, editing, and coding tools. :star:
* [Nimbus Note](https://nimbusweb.me/note.php) A note-taking app designed by
Nimbus Web.
* [JoeDocs](https://joedocs.com/) An open collaborative wiki.
Nimbus Web. :star:
* [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
collaboration platform.
* [Living Spec](https://livingspec.com/) A modern way for product teams to collaborate.
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
## Table of Contents
@@ -95,6 +81,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) |
| [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) |
| [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) |
### Providers
@@ -885,6 +872,16 @@ undo- or the redo-stack.
</dd>
<b>
<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'
| 'redo' })
</code>
@@ -893,6 +890,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
the undo- or the redo-stack.
</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>
#### Example: Stop Capturing

4804
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.5.26",
"version": "13.5.34",
"description": "Shared Editing Library",
"main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs",
@@ -73,7 +73,7 @@
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.43"
"lib0": "^0.2.49"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",

View File

@@ -67,6 +67,8 @@ export {
decodeStateVector,
logUpdate,
logUpdateV2,
decodeUpdate,
decodeUpdateV2,
relativePositionToJSON,
isDeleted,
isParentOf,

View File

@@ -100,4 +100,4 @@ export class ContentFormat {
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @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 {
/**
* @param {AbstractType<YEvent>} type
* @param {AbstractType<any>} type
*/
constructor (type) {
/**

View File

@@ -127,12 +127,13 @@ export const splitItem = (transaction, leftItem, diff) => {
* @param {Item} item
* @param {Set<Item>} redoitems
* @param {DeleteSet} itemsToDelete
* @param {boolean} ignoreRemoteMapChanges
*
* @return {Item|null}
*
* @private
*/
export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges) => {
const doc = transaction.doc
const store = doc.store
const ownClientID = doc.clientID
@@ -152,7 +153,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
// make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true) {
// 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
}
while (parentItem.redone !== null) {
@@ -198,7 +199,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete) => {
}
} else {
right = null
if (item.right) {
if (item.right && !ignoreRemoteMapChanges) {
left = item
// 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.

View File

@@ -278,7 +278,7 @@ export class AbstractType {
this._eH = createEventHandler()
/**
* Deep event handlers
* @type {EventHandler<Array<YEvent>,Transaction>}
* @type {EventHandler<Array<YEvent<any>>,Transaction>}
*/
this._dEH = createEventHandler()
/**
@@ -364,7 +364,7 @@ export class AbstractType {
/**
* 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) {
addEventHandlerListener(this._dEH, f)
@@ -382,7 +382,7 @@ export class AbstractType {
/**
* 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) {
removeEventHandlerListener(this._dEH, f)

View File

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

View File

@@ -21,6 +21,7 @@ import * as iterator from 'lib0/iterator'
/**
* @template T
* @extends YEvent<YMap<T>>
* Event that describes the changes on a YMap.
*/
export class YMapEvent extends YEvent {

View File

@@ -291,7 +291,8 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// iterate until first non-format or null is found
// 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
while (currPos.right !== null && (length > 0 || currPos.right.content.constructor === ContentFormat)) {
if (!currPos.right.deleted) {
switch (currPos.right.content.constructor) {
case ContentFormat: {
@@ -338,14 +339,16 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
*
* @param {Transaction} transaction
* @param {Item} start
* @param {Item|null} end exclusive end, automatically iterates to the next Content Item
* @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
* @param {Map<string,any>} startAttributes
* @param {Map<string,any>} endAttributes This attribute is modified!
* @param {Map<string,any>} currAttributes
* @return {number} The amount of formatting Items deleted.
*
* @function
*/
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => {
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
let end = curr
const endAttributes = map.copy(currAttributes)
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
@@ -353,7 +356,11 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
end = end.right
}
let cleanups = 0
let reachedEndOfCurr = false
while (start !== end) {
if (curr === start) {
reachedEndOfCurr = true
}
if (!start.deleted) {
const content = start.content
switch (content.constructor) {
@@ -363,6 +370,9 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction)
cleanups++
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
currAttributes.delete(key)
}
}
break
}
@@ -465,7 +475,7 @@ const deleteText = (transaction, currPos, length) => {
currPos.forward()
}
if (start) {
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, map.copy(currPos.currentAttributes))
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes)
}
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
if (parent._searchMarker) {
@@ -502,6 +512,7 @@ const deleteText = (transaction, currPos, length) => {
*/
/**
* @extends YEvent<YText>
* Event that describes the changes on a YText type.
*/
export class YTextEvent extends YEvent {
@@ -684,7 +695,7 @@ export class YTextEvent extends YEvent {
} else {
attributes[key] = value
}
} else {
} else if (value !== null) {
item.delete(transaction)
}
}
@@ -710,7 +721,7 @@ export class YTextEvent extends YEvent {
} else {
attributes[key] = value
}
} else {
} else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function
item.delete(transaction)
}
}

View File

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

View File

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

View File

@@ -75,13 +75,13 @@ export class Transaction {
* All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set.
* 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()
/**
* Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`.
* @type {Map<AbstractType<YEvent>,Array<YEvent>>}
* @type {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>}
*/
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`.
*
* @param {Transaction} transaction
* @param {AbstractType<YEvent>} type
* @param {AbstractType<YEvent<any>>} type
* @param {string|null} parentSub
*/
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {

View File

@@ -14,6 +14,7 @@ import {
} from '../internals.js'
import * as time from 'lib0/time'
import * as array from 'lib0/array'
import { Observable } from 'lib0/observable'
class StackItem {
@@ -30,6 +31,18 @@ class StackItem {
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
@@ -88,7 +101,7 @@ const popStackItem = (undoManager, stack, eventType) => {
}
})
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
// parents, so we have more information available when items are filtered.
@@ -124,6 +137,7 @@ const popStackItem = (undoManager, stack, eventType) => {
* filter returns false, the type/item won't be deleted even it is in the
* undo/redo scope.
* @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,16 +147,20 @@ const popStackItem = (undoManager, stack, eventType) => {
* 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`.
*
* @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 {
/**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options
*/
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]) } = {}) {
constructor (typeScope, { captureTimeout = 500, deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false } = {}) {
super()
this.scope = typeScope instanceof Array ? typeScope : [typeScope]
/**
* @type {Array<AbstractType<any>>}
*/
this.scope = []
this.addToScope(typeScope)
this.deleteFilter = deleteFilter
trackedOrigins.add(this)
this.trackedOrigins = trackedOrigins
@@ -163,7 +181,11 @@ export class UndoManager extends Observable {
this.redoing = false
this.doc = /** @type {Doc} */ (this.scope[0].doc)
this.lastChange = 0
this.doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
/**
* @param {Transaction} transaction
*/
this.afterTransactionHandler = transaction => {
// 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)))) {
return
@@ -175,7 +197,7 @@ export class UndoManager extends Observable {
this.stopCapturing() // next undo should not be appended to last stack item
} else if (!redoing) {
// neither undoing nor redoing: delete redoStack
this.redoStack = []
this.clear(false, true)
}
const insertions = new DeleteSet()
transaction.afterState.forEach((endClock, client) => {
@@ -206,29 +228,59 @@ export class UndoManager extends Observable {
keepItem(item, true)
}
})
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
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 {StackItem} stackItem
*/
const clearItem = stackItem => {
iterateDeletedStructs(transaction, stackItem.deletions, item => {
if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
keepItem(item, false)
}
})
}
this.undoStack.forEach(clearItem)
this.redoStack.forEach(clearItem)
})
this.undoStack = []
this.redoStack = []
/**
* @param {any} origin
*/
addTrackedOrigin (origin) {
this.trackedOrigins.add(origin)
}
/**
* @param {any} origin
*/
removeTrackedOrigin (origin) {
this.trackedOrigins.delete(origin)
}
clear (clearUndoStack = true, clearRedoStack = true) {
if ((clearUndoStack && this.canUndo()) || (clearRedoStack && this.canRedo())) {
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 +356,10 @@ export class UndoManager extends Observable {
canRedo () {
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) {
const clock = this.keyMap.get(key)
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.stringEncoder.write(key)
} 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'
/**
* @template {AbstractType<any>} T
* YEvent describes the changes on a YType.
*/
export class YEvent {
/**
* @param {AbstractType<any>} target The changed type.
* @param {T} target The changed type.
* @param {Transaction} transaction
*/
constructor (target, transaction) {
/**
* The type on which this event was created on.
* @type {AbstractType<any>}
* @type {T}
*/
this.target = target
/**

View File

@@ -112,6 +112,30 @@ export const logUpdateV2 = (update, YDecoder = UpdateDecoderV2) => {
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 {
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder

View File

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

View File

@@ -527,3 +527,64 @@ export const testUndoBlockBug = tc => {
undoManager.redo() // {"text":{}}
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 {Array<Uint8Array>} updates - expecting at least 4 updates

View File

@@ -6,6 +6,10 @@ import * as math from 'lib0/math'
const { init, compare } = Y
/**
* In this test we are mainly interested in the cleanup behavior and whether the resulting delta makes sense.
* It is fine if the resulting delta is not minimal. But applying the delta to a rich-text editor should result in a
* synced document.
*
* @param {t.TestCase} tc
*/
export const testDeltaAfterConcurrentFormatting = tc => {
@@ -14,12 +18,17 @@ export const testDeltaAfterConcurrentFormatting = tc => {
testConnector.flushAllMessages()
text0.format(0, 3, { bold: true })
text1.format(2, 2, { bold: true })
let delta = null
/**
* @type {any}
*/
const deltas = []
text1.observe(event => {
delta = event.delta
if (event.delta.length > 0) {
deltas.push(event.delta)
}
})
testConnector.flushAllMessages()
t.compare(delta, [])
t.compare(deltas, [[{ retain: 3, attributes: { bold: true } }, { retain: 2, attributes: { bold: null } }]])
}
/**
@@ -138,6 +147,28 @@ export const testNotMergeEmptyLinesFormat = tc => {
])
}
/**
* @param {t.TestCase} tc
*/
export const testPreserveAttributesThroughDelete = tc => {
const ydoc = new Y.Doc()
const testText = ydoc.getText('test')
testText.applyDelta([
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } },
{ insert: '\n' }
])
testText.applyDelta([
{ retain: 4 },
{ delete: 1 },
{ retain: 1, attributes: { title: true } }
])
t.compare(testText.toDelta(), [
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } }
])
}
/**
* @param {t.TestCase} tc
*/
@@ -576,6 +607,35 @@ export const testFormattingBug = async tc => {
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
let charCounter = 0