Compare commits

...

27 Commits

Author SHA1 Message Date
Kevin Jahns
1e69d650b8 13.6.14 2024-03-01 11:31:21 +01:00
Kevin Jahns
133cfc9cdc allow falsy values in formatting attributes 2024-03-01 11:29:14 +01:00
Kevin Jahns
83db6c814c Merge pull request #619 from jul13579/allow-falsy-attribute-values
Allow falsy attribute values
2024-03-01 11:23:04 +01:00
Julian Lehrhuber
cdbb55818d Allow falsy attribute values 2024-03-01 10:37:51 +01:00
Kevin Jahns
90675be3ab 13.6.13 2024-02-29 17:37:25 +01:00
Kevin Jahns
541306b254 migrate to ObservableV2 2024-02-29 17:08:57 +01:00
Kevin Jahns
53173a9ea7 Merge branch 'mylesj-feat/undomanager-doingstackitem' 2024-02-29 14:47:00 +01:00
Kevin Jahns
29fa60ccf9 [Undo] add UndoManager.currStackItem 2024-02-29 14:46:43 +01:00
Myles J
917261a1ce Facilitate referencing UndoManager StackItem inside Type observers 2024-02-29 00:50:36 +00:00
Kevin Jahns
a9dc72fcc0 Merge pull request #612 from MentalGear/patch-1
docs: fix typo
2024-02-25 10:31:20 +01:00
Kevin Jahns
90a90ab010 add y-fire to provider list #189 2024-02-20 19:52:58 +01:00
MentalGear
009f6ab551 docs: fix typo 2024-02-17 20:38:53 +01:00
Kevin Jahns
a8582442e3 13.6.12 2024-02-09 23:38:50 +01:00
Kevin Jahns
f54ea625e2 Merge branch 'raineorshine-getXmlElement' 2024-02-09 23:31:29 +01:00
Kevin Jahns
ce06b2abec update deps 2024-02-09 23:31:07 +01:00
Kevin Jahns
e1bce03ed8 better typings for ydoc.get 2024-02-09 23:27:24 +01:00
Raine Revere
16d9638bc8 Add ydoc.getXmlElement 2024-02-05 13:35:32 +00:00
Kevin Jahns
415a645874 13.6.11 2024-01-21 11:30:14 +01:00
Kevin Jahns
1cb52dc863 fix Y.Text formatting issue - closes #606 2024-01-21 11:27:12 +01:00
Kevin Jahns
7a8ca6eaa5 add linear as a user of Yjs 2024-01-15 14:11:28 +01:00
Kevin Jahns
e348255bb1 Merge pull request #604 from lukasz-jazwa/proffesional-support-section
Updated readme.md with Professional Support section
2024-01-10 17:27:03 +01:00
Kevin Jahns
79c095d4dc Merge pull request #605 from xaviergonz/patch-1
Update README.md with mobx-keystone binding
2023-12-28 15:31:00 +01:00
Javier Gonzalez
0241fd3c40 Update README.md with mobx-keystone binding 2023-12-23 11:16:57 +01:00
lukasz jazwa
cf78ce12b2 Updated readme.md with Professional Support section 2023-12-14 21:55:53 +01:00
Kevin Jahns
77bd74127d Update who-is-using (Cargo.site) 2023-12-11 16:37:23 +01:00
Kevin Jahns
fe36ffd122 add AWS Sagemaker, JupyterLab, JupyterCAD as users 2023-11-28 16:22:37 +01:00
Kevin Jahns
28ccd5e0dd add providers (also mention some y-crdt based providers) 2023-11-21 19:55:29 +01:00
43 changed files with 1017 additions and 522 deletions

107
README.md
View File

@@ -32,13 +32,30 @@ Otherwise you can find help on our community [discussion board](https://discuss.
Please contribute to the project financially - especially if your company relies Please contribute to the project financially - especially if your company relies
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) 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)
## Professional Support
* [Support Contract with the Maintainer](https://github.com/sponsors/dmonad) -
By contributing financially to the open-source Yjs project, you can receive
professional support directly from the author. This includes the opportunity for
weekly video calls to discuss your specific challenges.
* [Synergy Codes](https://synergycodes.com/yjs-services/) - Specializing in
consulting and developing real-time collaborative editing solutions for visual
apps, Synergy Codes focuses on interactive diagrams, complex graphs, charts, and
various data visualization types. Their expertise empowers developers to build
engaging and interactive visual experiences leveraging the power of Yjs. See
their work in action at [Visual Collaboration
Showcase](https://yjs-diagram.synergy.codes/).
## Who is using Yjs ## Who is using Yjs
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source * [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
knowledge base. 🏅 knowledge base. 🏅
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2: * [Cargo](https://cargo.site/) Site builder for designers and artists :star2:
* [Sana](https://sanalabs.com/) A learning platform with collaborative text * [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:
editing powered by Yjs. * [Evernote](https://evernote.com) Note-taking app :star2:
* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual
classrooms and online training :star2:
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star:
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and * [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
community. :star: community. :star:
* [Room.sh](https://room.sh/) A meeting application with integrated * [Room.sh](https://room.sh/) A meeting application with integrated
@@ -47,6 +64,8 @@ on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
Nimbus Web. :star: Nimbus Web. :star:
* [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to * [Pluxbox RadioManager](https://getradiomanager.com/) A web-based app to
collaboratively organize radio broadcasts. :star: collaboratively organize radio broadcasts. :star:
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
editing powered by Yjs.
* [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted * [Serenity Notes](https://www.serenity.re/en/notes) End-to-end encrypted
collaborative notes app. collaborative notes app.
* [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)* * [PRSM](https://prsm.uk/) Collaborative mind-mapping and system visualisation. *[(source)](https://github.com/micrology/prsm)*
@@ -56,6 +75,9 @@ on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
* [Slidebeamer](https://slidebeamer.com/) Presentation app. * [Slidebeamer](https://slidebeamer.com/) Presentation app.
* [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys. * [BlockSurvey](https://blocksurvey.io) End-to-end encryption for your forms/surveys.
* [Skiff](https://skiff.org/) Private, decentralized workspace. * [Skiff](https://skiff.org/) Private, decentralized workspace.
* [JupyterLab](https://jupyter.org/) Collaborative computational Notebooks
* [JupyterCad](https://jupytercad.readthedocs.io/en/latest/) Extension to
JupyterLab that enables collaborative editing of 3d FreeCAD Models.
* [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for * [Hyperquery](https://hyperquery.ai/) A collaborative data workspace for
sharing analyses, documentation, spreadsheets, and dashboards. sharing analyses, documentation, spreadsheets, and dashboards.
* [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon * [Nosgestesclimat](https://nosgestesclimat.fr/groupe) The french carbon
@@ -66,6 +88,9 @@ on Yjs. [![Become a Sponsor](https://img.shields.io/static/v1?label=Become%20a%2
worldbuilding app for tabletop RPGs. worldbuilding app for tabletop RPGs.
* [IllumiDesk](https://illumidesk.com/) Build courses and content with A.I. * [IllumiDesk](https://illumidesk.com/) Build courses and content with A.I.
* [btw](https://www.btw.so) Open-source Medium alternative * [btw](https://www.btw.so) Open-source Medium alternative
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) Tools for building Machine
Learning Models
* [linear](https://linear.app) Streamline issues, projects, and product roadmaps.
## Table of Contents ## Table of Contents
@@ -102,6 +127,7 @@ are implemented in separate modules.
| [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) | | [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) |
| [mobx-keystone](https://mobx-keystone.js.org/) | | [mobx-keystone-yjs](https://github.com/xaviergonz/mobx-keystone/tree/master/packages/mobx-keystone-yjs) | [demo](https://mobx-keystone.js.org/examples/yjs-binding) |
### Providers ### Providers
@@ -110,7 +136,19 @@ and storing shared data for offline usage is quite a hassle. **Providers**
manage all that for you and are the perfect starting point for your manage all that for you and are the perfect starting point for your
collaborative app. collaborative app.
> This list of providers is incomplete. Please open PRs to add your providers to
> this list!
#### Connection Providers
<dl> <dl>
<dt><a href="https://github.com/yjs/y-websocket">y-websocket</a></dt>
<dd>
A module that contains a simple websocket backend and a websocket client that
connects to that backend. The backend can be extended to persist updates in a
leveldb database. <b>y-sweet</b> and <b>ypy-websocket</b> (see below) are
compatible to the y-wesocket protocol.
</dd>
<dt><a href="https://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
@@ -119,17 +157,22 @@ 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="https://github.com/yjs/y-websocket">y-websocket</a></dt> <dt><a href="https://github.com/liveblocks/liveblocks">@liveblocks/yjs</a></dt>
<dd> <dd>
A module that contains a simple websocket backend and a websocket client that <a href="https://liveblocks.io/document/yjs">Liveblocks Yjs</a> provides a fully
connects to that backend. The backend can be extended to persist updates in a hosted WebSocket infrastructure and persisted data store for Yjs
leveldb database. documents. No configuration or maintenance is required. It also features
Yjs webhook events, REST API to read and update Yjs documents, and a
browser DevTools extension.
</dd> </dd>
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt> <dt><a href="https://github.com/drifting-in-space/y-sweet">y-sweet</a></dt>
<dd> <dd>
Efficiently persists document updates to the browsers indexeddb database. A standalone yjs server with persistence to S3 or filesystem. They offer a
The document is immediately available and only diffs need to be synced through the <a href="https://y-sweet.cloud">cloud service</a> as well.
network provider. </dd>
<dt><a href="https://docs.partykit.io/reference/y-partykit-api/">PartyKit</a></dt>
<dd>
Cloud service for building multiplayer apps.
</dd> </dd>
<dt><a href="https://github.com/marcopolo/y-libp2p">y-libp2p</a></dt> <dt><a href="https://github.com/marcopolo/y-libp2p">y-libp2p</a></dt>
<dd> <dd>
@@ -144,14 +187,6 @@ Also includes a peer-sync mechanism to catch up on missed updates.
an append-only log of CRDT local updates (hypercore). Multifeed manages and sync an append-only log of CRDT local updates (hypercore). Multifeed manages and sync
hypercores and y-dat listens to changes and applies them to the Yjs document. hypercores and y-dat listens to changes and applies them to the Yjs document.
</dd> </dd>
<dt><a href="https://github.com/liveblocks/liveblocks">@liveblocks/yjs</a></dt>
<dd>
<a href="https://liveblocks.io/document/yjs">Liveblocks Yjs</a> provides a fully
hosted WebSocket infrastructure and persisted data store for Yjs
documents. No configuration or maintenance is required. It also features
Yjs webhook events, REST API to read and update Yjs documents, and a
browser DevTools extension.
</dd>
<dt><a href="https://github.com/yousefED/matrix-crdt">Matrix-CRDT</a></dt> <dt><a href="https://github.com/yousefED/matrix-crdt">Matrix-CRDT</a></dt>
<dd> <dd>
Use <a href="https://www.matrix.org">Matrix</a> as an off-the-shelf backend for Use <a href="https://www.matrix.org">Matrix</a> as an off-the-shelf backend for
@@ -160,17 +195,41 @@ Use Matrix as transport and storage of Yjs updates, so you can focus building
your client app and Matrix can provide powerful features like Authentication, your client app and Matrix can provide powerful features like Authentication,
Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End Authorization, Federation, hosting (self-hosting or SaaS) and even End-to-End
Encryption (E2EE). Encryption (E2EE).
</dd> </dd>
<dt><a href="https://github.com/y-crdt/yrb-actioncable">yrb-actioncable</a></dt>
<dd>
An ActionCable companion for Yjs clients. There is a fitting
<a href="https://github.com/y-crdt/yrb-redis">redis extension</a> as well.
</dd>
<dt><a href="https://github.com/y-crdt/ypy-websocket">ypy-websocket</a></dt>
<dd>
Websocket backend, written in Python.
</dd>
</dl>
#### Persistence Providers
<dl>
<dt><a href="https://github.com/yjs/y-indexeddb">y-indexeddb</a></dt>
<dd>
Efficiently persists document updates to the browsers indexeddb database.
The document is immediately available and only diffs need to be synced through the
network provider.
</dd>
<dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt> <dt><a href="https://github.com/MaxNoetzold/y-mongodb-provider">y-mongodb-provider</a></dt>
<dd> <dd>
Adds persistent storage to a server with MongoDB. Can be used with the Adds persistent storage to a server with MongoDB. Can be used with the
y-websocket provider. y-websocket provider.
</dd> </dd>
<dt><a href="https://github.com/toeverything/AFFiNE/tree/master/packages/y-indexeddb"> <dt><a href="https://github.com/toeverything/AFFiNE/tree/master/packages/y-indexeddb">
@toeverything/y-indexeddb</a></dt> @toeverything/y-indexeddb</a></dt>
<dd> <dd>
Like y-indexeddb, but with sub-documents support and fully TypeScript. Like y-indexeddb, but with sub-documents support and fully TypeScript.
</dd> </dd>
<dt><a href="https://github.com/podraven/y-fire">y-fire</a></dt>
<dd>
A database and connection provider for Yjs based on Firestore.
</dd>
</dl> </dl>
# Ports # Ports
@@ -684,6 +743,8 @@ type. Doesn't log types that have not been defined (using
<dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd> <dd>Define a shared Y.Map type. Is equivalent to <code>y.get(string, Y.Map)</code>.</dd>
<b><code>getText(string):Y.Text</code></b> <b><code>getText(string):Y.Text</code></b>
<dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd> <dd>Define a shared Y.Text type. Is equivalent to <code>y.get(string, Y.Text)</code>.</dd>
<b><code>getXmlElement(string, string):Y.XmlElement</code></b>
<dd>Define a shared Y.XmlElement type. Is equivalent to <code>y.get(string, Y.XmlElement)</code>.</dd>
<b><code>getXmlFragment(string):Y.XmlFragment</code></b> <b><code>getXmlFragment(string):Y.XmlFragment</code></b>
<dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd> <dd>Define a shared Y.XmlFragment type. Is equivalent to <code>y.get(string, Y.XmlFragment)</code>.</dd>
<b><code>on(string, function)</code></b> <b><code>on(string, function)</code></b>
@@ -821,7 +882,7 @@ ydoc2.getText().toString() // => "00000000000"
#### Using V2 update format #### Using V2 update format
Yjs implements two update formats. By default you are using the V1 update format. Yjs implements two update formats. By default you are using the V1 update format.
You can opt-in into the V2 update format wich provides much better compression. You can opt-in into the V2 update format which provides much better compression.
It is not yet used by all providers. However, you can already use it if It is not yet used by all providers. However, you can already use it if
you are building your own provider. All below functions are available with the you are building your own provider. All below functions are available with the
suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates

1129
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.6.10", "version": "13.6.14",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
@@ -12,9 +12,10 @@
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
}, },
"scripts": { "scripts": {
"clean": "rm -rf dist docs",
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50", "test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
"dist": "rm -rf dist && rollup -c && tsc", "dist": "npm run clean && rollup -c && tsc",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc", "lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",

View File

@@ -1,4 +1,3 @@
export * from './utils/AbstractConnector.js' export * from './utils/AbstractConnector.js'
export * from './utils/DeleteSet.js' export * from './utils/DeleteSet.js'
export * from './utils/Doc.js' export * from './utils/Doc.js'

View File

@@ -1,4 +1,3 @@
import { import {
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@@ -1,4 +1,3 @@
import { import {
addToDeleteSet, addToDeleteSet,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line

View File

@@ -1,4 +1,3 @@
import { import {
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@@ -1,4 +1,3 @@
import { import {
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@@ -1,4 +1,3 @@
import { import {
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@@ -1,4 +1,3 @@
import { import {
readYArray, readYArray,
readYMap, readYMap,

View File

@@ -1,4 +1,3 @@
import { import {
AbstractStruct, AbstractStruct,
addStruct, addStruct,

View File

@@ -1,4 +1,3 @@
import { import {
GC, GC,
getState, getState,

View File

@@ -1,4 +1,3 @@
import { import {
AbstractStruct, AbstractStruct,
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line

View File

@@ -1,4 +1,3 @@
import { import {
removeEventHandlerListener, removeEventHandlerListener,
callEventHandlerListeners, callEventHandlerListeners,

View File

@@ -1,4 +1,3 @@
/** /**
* @module YMap * @module YMap
*/ */

View File

@@ -1,4 +1,3 @@
/** /**
* @module YText * @module YText
*/ */
@@ -118,14 +117,15 @@ const findNextPosition = (transaction, pos, count) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {number} index
* @param {boolean} useSearchMarker
* @return {ItemTextListPosition} * @return {ItemTextListPosition}
* *
* @private * @private
* @function * @function
*/ */
const findPosition = (transaction, parent, index) => { const findPosition = (transaction, parent, index, useSearchMarker) => {
const currentAttributes = new Map() const currentAttributes = new Map()
const marker = findMarker(parent, index) const marker = useSearchMarker ? findMarker(parent, index) : null
if (marker) { if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes) const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
return findNextPosition(transaction, pos, index - marker.index) return findNextPosition(transaction, pos, index - marker.index)
@@ -201,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
while (true) { while (true) {
if (currPos.right === null) { if (currPos.right === null) {
break break
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) { } else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
// //
} else { } else {
break break
@@ -227,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
// insert format-start items // insert format-start items
for (const key in attributes) { for (const key in attributes) {
const val = attributes[key] const val = attributes[key]
const currentVal = currPos.currentAttributes.get(key) || null const currentVal = currPos.currentAttributes.get(key) ?? null
if (!equalAttrs(currentVal, val)) { if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal) negatedAttributes.set(key, currentVal)
@@ -389,12 +389,12 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
switch (content.constructor) { switch (content.constructor) {
case ContentFormat: { case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content) const { key, value } = /** @type {ContentFormat} */ (content)
const startAttrValue = startAttributes.get(key) || null const startAttrValue = startAttributes.get(key) ?? null
if (endFormats.get(key) !== content || startAttrValue === value) { if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed. // Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction) start.delete(transaction)
cleanups++ cleanups++
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) { if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
if (startAttrValue === null) { if (startAttrValue === null) {
currAttributes.delete(key) currAttributes.delete(key)
} else { } else {
@@ -769,12 +769,12 @@ export class YTextEvent extends YEvent {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
if (!this.deletes(item)) { if (!this.deletes(item)) {
const curVal = currentAttributes.get(key) || null const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) { if (!equalAttrs(curVal, value)) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
if (equalAttrs(value, (oldAttributes.get(key) || null))) { if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
delete attributes[key] delete attributes[key]
} else { } else {
attributes[key] = value attributes[key] = value
@@ -785,7 +785,7 @@ export class YTextEvent extends YEvent {
} }
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
oldAttributes.set(key, value) oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) || null const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) { if (!equalAttrs(curVal, value)) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
@@ -1120,7 +1120,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const pos = findPosition(transaction, this, index) const pos = findPosition(transaction, this, index, !attributes)
if (!attributes) { if (!attributes) {
attributes = {} attributes = {}
// @ts-ignore // @ts-ignore
@@ -1138,20 +1138,20 @@ export class YText extends AbstractType {
* *
* @param {number} index The index to insert the embed at. * @param {number} index The index to insert the embed at.
* @param {Object | AbstractType<any>} embed The Object that represents the embed. * @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the * @param {TextAttributes} [attributes] Attribute information to apply on the
* embed * embed
* *
* @public * @public
*/ */
insertEmbed (index, embed, attributes = {}) { insertEmbed (index, embed, attributes) {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const pos = findPosition(transaction, this, index) const pos = findPosition(transaction, this, index, !attributes)
insertText(transaction, this, pos, embed, attributes) insertText(transaction, this, pos, embed, attributes || {})
}) })
} else { } else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes)) /** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {}))
} }
} }
@@ -1170,7 +1170,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
deleteText(transaction, findPosition(transaction, this, index), length) deleteText(transaction, findPosition(transaction, this, index, true), length)
}) })
} else { } else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length)) /** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
@@ -1194,7 +1194,7 @@ export class YText extends AbstractType {
const y = this.doc const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const pos = findPosition(transaction, this, index) const pos = findPosition(transaction, this, index, false)
if (pos.right === null) { if (pos.right === null) {
return return
} }

View File

@@ -20,7 +20,7 @@ import {
/** /**
* An YXmlElement imitates the behavior of a * An YXmlElement imitates the behavior of a
* {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}. * https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
* *
* * An YXmlElement has attributes (key value pairs) * * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement * * An YXmlElement has childElements that must inherit from YXmlElement

View File

@@ -1,4 +1,3 @@
import { import {
YEvent, YEvent,
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line

View File

@@ -1,4 +1,3 @@
import { import {
YMap, YMap,
YXmlHookRefID, YXmlHookRefID,

View File

@@ -1,4 +1,3 @@
import { import {
YText, YText,
YXmlTextRefID, YXmlTextRefID,

View File

@@ -1,5 +1,4 @@
import { ObservableV2 } from 'lib0/observable'
import { Observable } from 'lib0/observable'
import { import {
Doc // eslint-disable-line Doc // eslint-disable-line
@@ -11,9 +10,9 @@ import {
* @note This interface is experimental and it is not advised to actually inherit this class. * @note This interface is experimental and it is not advised to actually inherit this class.
* It just serves as typing information. * It just serves as typing information.
* *
* @extends {Observable<any>} * @extends {ObservableV2<any>}
*/ */
export class AbstractConnector extends Observable { export class AbstractConnector extends ObservableV2 {
/** /**
* @param {Doc} ydoc * @param {Doc} ydoc
* @param {any} awareness * @param {any} awareness

View File

@@ -1,4 +1,3 @@
import { import {
findIndexSS, findIndexSS,
getState, getState,

View File

@@ -8,12 +8,13 @@ import {
YArray, YArray,
YText, YText,
YMap, YMap,
YXmlElement,
YXmlFragment, YXmlFragment,
transact, transact,
ContentDoc, Item, Transaction, YEvent // eslint-disable-line ContentDoc, Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
import * as random from 'lib0/random' import * as random from 'lib0/random'
import * as map from 'lib0/map' import * as map from 'lib0/map'
import * as array from 'lib0/array' import * as array from 'lib0/array'
@@ -33,10 +34,26 @@ export const generateNewClientId = random.uint32
*/ */
/** /**
* A Yjs instance handles the state of shared data. * @typedef {Object} DocEvents
* @extends Observable<string> * @property {function(Doc):void} DocEvents.destroy
* @property {function(Doc):void} DocEvents.load
* @property {function(boolean, Doc):void} DocEvents.sync
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2
* @property {function(Doc):void} DocEvents.beforeAllTransactions
* @property {function(Transaction, Doc):void} DocEvents.beforeTransaction
* @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls
* @property {function(Transaction, Doc):void} DocEvents.afterTransaction
* @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup
* @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions
* @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs
*/ */
export class Doc extends Observable {
/**
* A Yjs instance handles the state of shared data.
* @extends ObservableV2<DocEvents>
*/
export class Doc extends ObservableV2 {
/** /**
* @param {DocOpts} opts configuration * @param {DocOpts} opts configuration
*/ */
@@ -114,7 +131,7 @@ export class Doc extends Observable {
} }
this.isSynced = isSynced === undefined || isSynced === true this.isSynced = isSynced === undefined || isSynced === true
if (this.isSynced && !this.isLoaded) { if (this.isSynced && !this.isLoaded) {
this.emit('load', []) this.emit('load', [this])
} }
}) })
/** /**
@@ -180,6 +197,7 @@ export class Doc extends Observable {
* Define all types right after the Yjs instance is created and store them in a separate object. * Define all types right after the Yjs instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, .. * Also use the typed methods `getText(name)`, `getArray(name)`, ..
* *
* @template {typeof AbstractType<any>} Type
* @example * @example
* const y = new Y(..) * const y = new Y(..)
* const appState = { * const appState = {
@@ -188,12 +206,12 @@ export class Doc extends Observable {
* } * }
* *
* @param {string} name * @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor * @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
* *
* @public * @public
*/ */
get (name, TypeConstructor = AbstractType) { get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
const type = map.setIfUndefined(this.share, name, () => { const type = map.setIfUndefined(this.share, name, () => {
// @ts-ignore // @ts-ignore
const t = new TypeConstructor() const t = new TypeConstructor()
@@ -219,12 +237,12 @@ export class Doc extends Observable {
t._length = type._length t._length = type._length
this.share.set(name, t) this.share.set(name, t)
t._integrate(this, null) t._integrate(this, null)
return t return /** @type {InstanceType<Type>} */ (t)
} else { } else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`) throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
} }
} }
return type return /** @type {InstanceType<Type>} */ (type)
} }
/** /**
@@ -235,8 +253,7 @@ export class Doc extends Observable {
* @public * @public
*/ */
getArray (name = '') { getArray (name = '') {
// @ts-ignore return /** @type {YArray<T>} */ (this.get(name, YArray))
return this.get(name, YArray)
} }
/** /**
@@ -246,7 +263,6 @@ export class Doc extends Observable {
* @public * @public
*/ */
getText (name = '') { getText (name = '') {
// @ts-ignore
return this.get(name, YText) return this.get(name, YText)
} }
@@ -258,8 +274,17 @@ export class Doc extends Observable {
* @public * @public
*/ */
getMap (name = '') { getMap (name = '') {
// @ts-ignore return /** @type {YMap<T>} */ (this.get(name, YMap))
return this.get(name, YMap) }
/**
* @param {string} [name]
* @return {YXmlElement}
*
* @public
*/
getXmlElement (name = '') {
return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement))
} }
/** /**
@@ -269,7 +294,6 @@ export class Doc extends Observable {
* @public * @public
*/ */
getXmlFragment (name = '') { getXmlFragment (name = '') {
// @ts-ignore
return this.get(name, YXmlFragment) return this.get(name, YXmlFragment)
} }
@@ -313,24 +337,9 @@ export class Doc extends Observable {
transaction.subdocsRemoved.add(this) transaction.subdocsRemoved.add(this)
}, null, true) }, null, true)
} }
this.emit('destroyed', [true]) // @ts-ignore
this.emit('destroyed', [true]) // DEPRECATED!
this.emit('destroy', [this]) this.emit('destroy', [this])
super.destroy() super.destroy()
} }
/**
* @param {string} eventName
* @param {function(...any):any} f
*/
on (eventName, f) {
super.on(eventName, f)
}
/**
* @param {string} eventName
* @param {function} f
*/
off (eventName, f) {
super.off(eventName, f)
}
} }

View File

@@ -1,4 +1,3 @@
import { AbstractType } from '../internals.js' // eslint-disable-line import { AbstractType } from '../internals.js' // eslint-disable-line
import * as decoding from 'lib0/decoding' import * as decoding from 'lib0/decoding'

View File

@@ -1,4 +1,3 @@
import { import {
YArray, YArray,
YMap, YMap,

View File

@@ -1,4 +1,3 @@
import { import {
writeID, writeID,
readID, readID,

View File

@@ -1,4 +1,3 @@
import { import {
isDeleted, isDeleted,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,

View File

@@ -1,4 +1,3 @@
import { import {
GC, GC,
splitItem, splitItem,

View File

@@ -1,4 +1,3 @@
import { import {
getState, getState,
writeStructsFromTransaction, writeStructsFromTransaction,

View File

@@ -10,13 +10,13 @@ import {
getItemCleanStart, getItemCleanStart,
isDeleted, isDeleted,
addToDeleteSet, addToDeleteSet,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} 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 * as array from 'lib0/array'
import * as logging from 'lib0/logging' import * as logging from 'lib0/logging'
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
export class StackItem { export class StackItem {
/** /**
@@ -48,15 +48,10 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
/** /**
* @param {UndoManager} undoManager * @param {UndoManager} undoManager
* @param {Array<StackItem>} stack * @param {Array<StackItem>} stack
* @param {string} eventType * @param {'undo'|'redo'} eventType
* @return {StackItem?} * @return {StackItem?}
*/ */
const popStackItem = (undoManager, stack, eventType) => { const popStackItem = (undoManager, stack, eventType) => {
/**
* Whether a change happened
* @type {StackItem?}
*/
let result = null
/** /**
* Keep a reference to the transaction so we can fire the event with the changedParentTypes * Keep a reference to the transaction so we can fire the event with the changedParentTypes
* @type {any} * @type {any}
@@ -65,7 +60,7 @@ const popStackItem = (undoManager, stack, eventType) => {
const doc = undoManager.doc const doc = undoManager.doc
const scope = undoManager.scope const scope = undoManager.scope
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && undoManager.currStackItem === null) {
const store = doc.store const store = doc.store
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
/** /**
@@ -113,7 +108,7 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = true performedChange = true
} }
} }
result = performedChange ? stackItem : null undoManager.currStackItem = performedChange ? stackItem : null
} }
transaction.changed.forEach((subProps, type) => { transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary // destroy search marker if necessary
@@ -123,11 +118,12 @@ const popStackItem = (undoManager, stack, eventType) => {
}) })
_tr = transaction _tr = transaction
}, undoManager) }, undoManager)
if (result != null) { if (undoManager.currStackItem != null) {
const changedParentTypes = _tr.changedParentTypes const changedParentTypes = _tr.changedParentTypes
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager]) undoManager.emit('stack-item-popped', [{ stackItem: undoManager.currStackItem, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
undoManager.currStackItem = null
} }
return result return undoManager.currStackItem
} }
/** /**
@@ -143,6 +139,14 @@ const popStackItem = (undoManager, stack, eventType) => {
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
*/ */
/**
* @typedef {Object} StackItemEvent
* @property {StackItem} StackItemEvent.stackItem
* @property {any} StackItemEvent.origin
* @property {'undo'|'redo'} StackItemEvent.type
* @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
*/
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
@@ -150,9 +154,9 @@ 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'|'stack-cleared'|'stack-item-updated'>} * @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>}
*/ */
export class UndoManager extends Observable { export class UndoManager extends ObservableV2 {
/** /**
* @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
@@ -191,6 +195,12 @@ export class UndoManager extends Observable {
*/ */
this.undoing = false this.undoing = false
this.redoing = false this.redoing = false
/**
* The currently popped stack item if UndoManager.undoing or UndoManager.redoing
*
* @type {StackItem|null}
*/
this.currStackItem = null
this.lastChange = 0 this.lastChange = 0
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
this.captureTimeout = captureTimeout this.captureTimeout = captureTimeout
@@ -244,6 +254,9 @@ export class UndoManager extends Observable {
keepItem(item, true) keepItem(item, true)
} }
}) })
/**
* @type {[StackItemEvent, UndoManager]}
*/
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this] 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', changeEvent) this.emit('stack-item-added', changeEvent)

View File

@@ -1,4 +1,3 @@
import * as error from 'lib0/error' import * as error from 'lib0/error'
import * as encoding from 'lib0/encoding' import * as encoding from 'lib0/encoding'

View File

@@ -1,4 +1,3 @@
import { import {
isDeleted, isDeleted,
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line

View File

@@ -1,4 +1,3 @@
/** /**
* @module encoding * @module encoding
*/ */

View File

@@ -1,4 +1,3 @@
import { AbstractType, Item } from '../internals.js' // eslint-disable-line import { AbstractType, Item } from '../internals.js' // eslint-disable-line
/** /**

View File

@@ -1,4 +1,3 @@
import { import {
AbstractType // eslint-disable-line AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'

View File

@@ -1,4 +1,3 @@
import * as binary from 'lib0/binary' import * as binary from 'lib0/binary'
import * as decoding from 'lib0/decoding' import * as decoding from 'lib0/decoding'
import * as encoding from 'lib0/encoding' import * as encoding from 'lib0/encoding'

View File

@@ -1,4 +1,3 @@
/** /**
* Testing if encoding/decoding compatibility and integration compatiblity is given. * Testing if encoding/decoding compatibility and integration compatiblity is given.
* We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches. * We expect that the document always looks the same, even if we upgrade the integration algorithm, or add additional encoding approaches.

View File

@@ -1,4 +1,3 @@
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'

View File

@@ -1,4 +1,3 @@
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'

View File

@@ -1,4 +1,3 @@
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
import * as prng from 'lib0/prng' import * as prng from 'lib0/prng'
import * as encoding from 'lib0/encoding' import * as encoding from 'lib0/encoding'
@@ -35,7 +34,7 @@ export const encV1 = {
mergeUpdates: Y.mergeUpdates, mergeUpdates: Y.mergeUpdates,
applyUpdate: Y.applyUpdate, applyUpdate: Y.applyUpdate,
logUpdate: Y.logUpdate, logUpdate: Y.logUpdate,
updateEventName: 'update', updateEventName: /** @type {'update'} */ ('update'),
diffUpdate: Y.diffUpdate diffUpdate: Y.diffUpdate
} }
@@ -44,7 +43,7 @@ export const encV2 = {
mergeUpdates: Y.mergeUpdatesV2, mergeUpdates: Y.mergeUpdatesV2,
applyUpdate: Y.applyUpdateV2, applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2, logUpdate: Y.logUpdateV2,
updateEventName: 'updateV2', updateEventName: /** @type {'updateV2'} */ ('updateV2'),
diffUpdate: Y.diffUpdateV2 diffUpdate: Y.diffUpdateV2
} }

View File

@@ -3,6 +3,46 @@ import { init } from './testHelper.js' // eslint-disable-line
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
export const testInconsistentFormat = () => {
/**
* @param {Y.Doc} ydoc
*/
const testYjsMerge = ydoc => {
const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText))
content.format(0, 6, { bold: null })
content.format(6, 4, { type: 'text' })
t.compare(content.toDelta(), [
{
attributes: { type: 'text' },
insert: 'Merge Test'
},
{
attributes: { type: 'text', italic: true },
insert: ' After'
}
])
}
const initializeYDoc = () => {
const yDoc = new Y.Doc({ gc: false })
const content = /** @type {Y.XmlText} */ (yDoc.get('text', Y.XmlText))
content.insert(0, ' After', { type: 'text', italic: true })
content.insert(0, 'Test', { type: 'text' })
content.insert(0, 'Merge ', { type: 'text', bold: true })
return yDoc
}
{
const yDoc = initializeYDoc()
testYjsMerge(yDoc)
}
{
const initialYDoc = initializeYDoc()
const yDoc = new Y.Doc({ gc: false })
Y.applyUpdate(yDoc, Y.encodeStateAsUpdate(initialYDoc))
testYjsMerge(yDoc)
}
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -675,3 +715,33 @@ export const testUndoDeleteInMap = (tc) => {
undoManager.undo() undoManager.undo()
t.compare(map0.toJSON(), { a: 'a' }) t.compare(map0.toJSON(), { a: 'a' })
} }
/**
* It should expose the StackItem being processed if undoing
*
* @param {t.TestCase} _tc
*/
export const testUndoDoingStackItem = async (_tc) => {
const doc = new Y.Doc()
const text = doc.getText('text')
const undoManager = new Y.UndoManager([text])
undoManager.on('stack-item-added', /** @param {any} event */ event => {
event.stackItem.meta.set('str', '42')
})
let metaUndo = /** @type {any} */ (null)
let metaRedo = /** @type {any} */ (null)
text.observe((event) => {
const /** @type {Y.UndoManager} */ origin = event.transaction.origin
if (origin === undoManager && origin.undoing) {
metaUndo = origin.currStackItem?.meta.get('str')
} else if (origin === undoManager && origin.redoing) {
metaRedo = origin.currStackItem?.meta.get('str')
}
})
text.insert(0, 'abc')
undoManager.undo()
undoManager.redo()
t.compare(metaUndo, '42', 'currStackItem is accessible while undoing')
t.compare(metaRedo, '42', 'currStackItem is accessible while redoing')
t.compare(undoManager.currStackItem, null, 'currStackItem is null after observe/transaction')
}

View File

@@ -15,7 +15,7 @@ import * as object from 'lib0/object'
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta * @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector * @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
* @property {string} Enc.updateEventName * @property {'update'|'updateV2'} Enc.updateEventName
* @property {string} Enc.description * @property {string} Enc.description
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
*/ */
@@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
// t.info('Target State: ') // t.info('Target State: ')
// enc.logUpdate(targetState) // enc.logUpdate(targetState)
cases.forEach((mergedUpdates, i) => { cases.forEach((mergedUpdates) => {
// t.info('State Case $' + i + ':') // t.info('State Case $' + i + ':')
// enc.logUpdate(updates) // enc.logUpdate(updates)
const merged = new Y.Doc({ gc: false }) const merged = new Y.Doc({ gc: false })
@@ -218,10 +218,10 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testMergeUpdates1 = tc => { export const testMergeUpdates1 = _tc => {
encoders.forEach((enc, i) => { encoders.forEach((enc) => {
t.info(`Using encoder: ${enc.description}`) t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false }) const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([]) const updates = /** @type {Array<Uint8Array>} */ ([])
@@ -299,16 +299,16 @@ export const testMergePendingUpdates = tc => {
Y.applyUpdate(yDoc5, update4) Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4]) Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore // @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock') const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor') t.compareStrings(yText5.toString(), 'nenor')
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testObfuscateUpdates = tc => { export const testObfuscateUpdates = _tc => {
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
const ytext = ydoc.getText('text') const ytext = ydoc.getText('text')
const ymap = ydoc.getMap('map') const ymap = ydoc.getMap('map')

View File

@@ -189,7 +189,6 @@ export const testClone = _tc => {
const third = new Y.XmlElement('p') const third = new Y.XmlElement('p')
yxml.push([first, second, third]) yxml.push([first, second, third])
t.compareArrays(yxml.toArray(), [first, second, third]) t.compareArrays(yxml.toArray(), [first, second, third])
const cloneYxml = yxml.clone() const cloneYxml = yxml.clone()
ydoc.getArray('copyarr').insert(0, [cloneYxml]) ydoc.getArray('copyarr').insert(0, [cloneYxml])
t.assert(cloneYxml.length === 3) t.assert(cloneYxml.length === 3)
@@ -210,3 +209,15 @@ export const testFormattingBug = _tc => {
yxml.applyDelta(delta) yxml.applyDelta(delta)
t.compare(yxml.toDelta(), delta) t.compare(yxml.toDelta(), delta)
} }
/**
* @param {t.TestCase} _tc
*/
export const testElement = _tc => {
const ydoc = new Y.Doc()
const yxmlel = ydoc.getXmlElement()
const text1 = new Y.XmlText('text1')
const text2 = new Y.XmlText('text2')
yxmlel.insert(0, [text1, text2])
t.compareArrays(yxmlel.toArray(), [text1, text2])
}