Merge branch 'main' into patch-1
This commit is contained in:
commit
0896ed42b2
@ -88,7 +88,7 @@ When a local insert happens, Yjs needs to map the insert position in the
|
||||
document (eg position 1000) to an ID. With just the linked list, this would
|
||||
require a slow O(n) linear scan of the list. But when editing a document, most
|
||||
inserts are either at the same position as the last insert, or nearby. To
|
||||
improve performance, Yjs stores a cache of the 10 most recently looked up
|
||||
improve performance, Yjs stores a cache of the 80 most recently looked up
|
||||
insert positions in the document. This is consulted and updated when a position
|
||||
is looked up to improve performance in the average case. The cache is updated
|
||||
using a heuristic that is still changing (currently, it is updated when a new
|
||||
|
82
README.md
82
README.md
@ -32,13 +32,35 @@ Otherwise you can find help on our community [discussion board](https://discuss.
|
||||
Please contribute to the project financially - especially if your company relies
|
||||
on Yjs. [](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
|
||||
|
||||
* [AFFiNE](https://affine.pro/) A local-first, privacy-first, open source
|
||||
knowledge base. 🏅
|
||||
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star2:
|
||||
* [Sana](https://sanalabs.com/) A learning platform with collaborative text
|
||||
editing powered by Yjs.
|
||||
knowledge base. :star2:
|
||||
* [Huly](https://huly.io/) - Open Source All-in-One Project Management Platform
|
||||
:star2:
|
||||
* [Cargo](https://cargo.site/) Site builder for designers and artists :star2:
|
||||
* [Gitbook](https://gitbook.com) Knowledge management for technical teams :star2:
|
||||
* [Evernote](https://evernote.com) Note-taking app :star2:
|
||||
* [Lessonspace](https://thelessonspace.com) Enterprise platform for virtual
|
||||
classrooms and online training :star2:
|
||||
* [Ellipsus]{ellipsus.com} - Collaborative writing app for storytelling etc.
|
||||
Supports versioning, change attribution, and "blame". A solution for the whole
|
||||
publishing process (also selling) :star:
|
||||
* [Dynaboard](https://dynaboard.com/) Build web apps collaboratively. :star:
|
||||
* [Relm](https://www.relm.us/) A collaborative gameworld for teamwork and
|
||||
community. :star:
|
||||
* [Room.sh](https://room.sh/) A meeting application with integrated
|
||||
@ -47,9 +69,15 @@ on Yjs. [ A web-based app to
|
||||
collaboratively organize radio broadcasts. :star:
|
||||
* [modyfi](https://www.modyfi.com) - Modyfi is the design platform built for
|
||||
multidisciplinary designers. Design, generate, animate, and more — without
|
||||
switching between apps. :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
|
||||
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)*
|
||||
* [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.
|
||||
@ -71,6 +99,21 @@ on Yjs. [ 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.
|
||||
* [btw](https://www.btw.so) - Personal website builder
|
||||
* [AWS SageMaker](https://aws.amazon.com/sagemaker/) - Machine Learning Service
|
||||
* [Arkiter](https://www.arkiter.com/) - Live interview software
|
||||
* [Appflowy](https://www.appflowy.io/) - They use Yrs
|
||||
* [Multi.app](https://multi.app) - Multiplayer app sharing: Point, draw and edit
|
||||
in shared apps as if they're on your computer. They are using Yrs.
|
||||
* [AppMaster](https://appmaster.io) A No-Code platform for creating
|
||||
production-ready applications with source code generation.
|
||||
* [Synthesia](https://www.synthesia.io) - Collaborative Video Editor
|
||||
* [thinkdeli](https://thinkdeli.com) - A fast and simple notes app powered by AI
|
||||
* [ourboard](https://github.com/raimohanska/ourboard) - A collaborative whiteboard
|
||||
applicaiton
|
||||
* [Ellie.ai](https://ellie.ai) - Data Product Design and Collaboration
|
||||
* [GoPeer](https://gopeer.org/) - Collaborative tutoring
|
||||
* [screen.garden](https://screen.garden) Collaborative backend for PKM apps.
|
||||
|
||||
## Table of Contents
|
||||
@ -108,6 +151,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) |
|
||||
| [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) |
|
||||
| [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
|
||||
|
||||
@ -125,9 +169,10 @@ collaborative app.
|
||||
<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.
|
||||
connects to that backend. <a href="https://github.com/yjs/y-redis/"><b>y-redis</b></a>,
|
||||
<b>y-sweet</b>, <b>ypy-websocket</b> and <a href="https://tiptap.dev/docs/hocuspocus/introduction">
|
||||
<b>Hocuspocus</b></a> (see below) are alternative
|
||||
backends to y-websocket.
|
||||
</dd>
|
||||
<dt><a href="https://github.com/yjs/y-webrtc">y-webrtc</a></dt>
|
||||
<dd>
|
||||
@ -149,6 +194,10 @@ browser DevTools extension.
|
||||
<dd>
|
||||
A standalone yjs server with persistence to S3 or filesystem. They offer a
|
||||
<a href="https://y-sweet.cloud">cloud service</a> as well.
|
||||
</dd>
|
||||
<dt><a href="https://github.com/ueberdosis/hocuspocus">Hocuspocus</a></dt>
|
||||
<dd>
|
||||
A standalone extensible yjs server with sqlite persistence, webhooks, auth and more.
|
||||
</dd>
|
||||
<dt><a href="https://docs.partykit.io/reference/y-partykit-api/">PartyKit</a></dt>
|
||||
<dd>
|
||||
@ -185,6 +234,15 @@ An ActionCable companion for Yjs clients. There is a fitting
|
||||
<dd>
|
||||
Websocket backend, written in Python.
|
||||
</dd>
|
||||
<dt><a href="https://tinybase.org/">Tinybase</a></dt>
|
||||
<dd>
|
||||
The reactive data store for local-first apps. They support multiple CRDTs and
|
||||
different network technologies.
|
||||
</dd>
|
||||
<dt><a href="https://codeberg.org/webxdc/y-webxdc">y-webxdc</a></dt>
|
||||
<dd>
|
||||
Provider for sharing data in <a href="https://webxdc.org">webxdc chat apps</a>.
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
#### Persistence Providers
|
||||
@ -206,6 +264,10 @@ y-websocket provider.
|
||||
<dd>
|
||||
Like y-indexeddb, but with sub-documents support and fully TypeScript.
|
||||
</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>
|
||||
|
||||
# Ports
|
||||
@ -719,6 +781,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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
@ -856,7 +920,7 @@ ydoc2.getText().toString() // => "00000000000"
|
||||
#### Using V2 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
|
||||
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
|
||||
|
1129
package-lock.json
generated
1129
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yjs",
|
||||
"version": "13.6.10",
|
||||
"version": "13.6.15",
|
||||
"description": "Shared Editing Library",
|
||||
"main": "./dist/yjs.cjs",
|
||||
"module": "./dist/yjs.mjs",
|
||||
@ -12,9 +12,10 @@
|
||||
"url": "https://github.com/sponsors/dmonad"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist docs",
|
||||
"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",
|
||||
"dist": "rm -rf dist && rollup -c && tsc",
|
||||
"dist": "npm run clean && rollup -c && tsc",
|
||||
"watch": "rollup -wc",
|
||||
"lint": "markdownlint README.md && standard && tsc",
|
||||
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export * from './utils/AbstractConnector.js'
|
||||
export * from './utils/DeleteSet.js'
|
||||
export * from './utils/Doc.js'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
UpdateEncoderV1, UpdateEncoderV2, ID, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
addToDeleteSet,
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
Doc, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Item, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
readYArray,
|
||||
readYMap,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
AbstractStruct,
|
||||
addStruct,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
GC,
|
||||
getState,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
AbstractStruct,
|
||||
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
removeEventHandlerListener,
|
||||
callEventHandlerListeners,
|
||||
@ -317,6 +316,10 @@ export class AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {AbstractType<EventType>}
|
||||
*/
|
||||
clone () {
|
||||
@ -478,7 +481,7 @@ export const typeListToArraySnapshot = (type, snapshot) => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on overy element of this YArray.
|
||||
* Executes a provided function on once on every element of this YArray.
|
||||
*
|
||||
* @param {AbstractType<any>} type
|
||||
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
|
||||
@ -570,7 +573,7 @@ export const typeListCreateIterator = type => {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on overy element of this YArray.
|
||||
* Executes a provided function on once on every element of this YArray.
|
||||
* Operates on a snapshotted state of the document.
|
||||
*
|
||||
* @param {AbstractType<any>} type
|
||||
|
@ -25,16 +25,7 @@ import { typeListSlice } from './AbstractType.js'
|
||||
* @template T
|
||||
* @extends YEvent<YArray<T>>
|
||||
*/
|
||||
export class YArrayEvent extends YEvent {
|
||||
/**
|
||||
* @param {YArray<T>} yarray The changed type
|
||||
* @param {Transaction} transaction The transaction object
|
||||
*/
|
||||
constructor (yarray, transaction) {
|
||||
super(yarray, transaction)
|
||||
this._transaction = transaction
|
||||
}
|
||||
}
|
||||
export class YArrayEvent extends YEvent {}
|
||||
|
||||
/**
|
||||
* A shared Array implementation.
|
||||
@ -95,6 +86,10 @@ export class YArray extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YArray<T>}
|
||||
*/
|
||||
clone () {
|
||||
@ -167,9 +162,9 @@ export class YArray extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Preppends content to this YArray.
|
||||
* Prepends content to this YArray.
|
||||
*
|
||||
* @param {Array<T>} content Array of content to preppend.
|
||||
* @param {Array<T>} content Array of content to prepend.
|
||||
*/
|
||||
unshift (content) {
|
||||
this.insert(0, content)
|
||||
@ -211,7 +206,8 @@ export class YArray extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
* Returns a portion of this YArray into a JavaScript Array selected
|
||||
* from start to end (end not included).
|
||||
*
|
||||
* @param {number} [start]
|
||||
* @param {number} [end]
|
||||
@ -244,7 +240,7 @@ export class YArray extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function once on overy element of this YArray.
|
||||
* Executes a provided function once on every element of this YArray.
|
||||
*
|
||||
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* @module YMap
|
||||
*/
|
||||
@ -89,6 +88,10 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YMap<MapType>}
|
||||
*/
|
||||
clone () {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* @module YText
|
||||
*/
|
||||
@ -118,14 +117,15 @@ const findNextPosition = (transaction, pos, count) => {
|
||||
* @param {Transaction} transaction
|
||||
* @param {AbstractType<any>} parent
|
||||
* @param {number} index
|
||||
* @param {boolean} useSearchMarker
|
||||
* @return {ItemTextListPosition}
|
||||
*
|
||||
* @private
|
||||
* @function
|
||||
*/
|
||||
const findPosition = (transaction, parent, index) => {
|
||||
const findPosition = (transaction, parent, index, useSearchMarker) => {
|
||||
const currentAttributes = new Map()
|
||||
const marker = findMarker(parent, index)
|
||||
const marker = useSearchMarker ? findMarker(parent, index) : null
|
||||
if (marker) {
|
||||
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes)
|
||||
return findNextPosition(transaction, pos, index - marker.index)
|
||||
@ -201,7 +201,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
|
||||
while (true) {
|
||||
if (currPos.right === null) {
|
||||
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 {
|
||||
break
|
||||
@ -227,7 +227,7 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
|
||||
// insert format-start items
|
||||
for (const key in attributes) {
|
||||
const val = attributes[key]
|
||||
const currentVal = currPos.currentAttributes.get(key) || null
|
||||
const currentVal = currPos.currentAttributes.get(key) ?? null
|
||||
if (!equalAttrs(currentVal, val)) {
|
||||
// save negated attribute (set null if currentVal undefined)
|
||||
negatedAttributes.set(key, currentVal)
|
||||
@ -389,12 +389,12 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
|
||||
switch (content.constructor) {
|
||||
case ContentFormat: {
|
||||
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) {
|
||||
// Either this format is overwritten or it is not necessary because the attribute already existed.
|
||||
start.delete(transaction)
|
||||
cleanups++
|
||||
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) {
|
||||
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
|
||||
if (startAttrValue === null) {
|
||||
currAttributes.delete(key)
|
||||
} else {
|
||||
@ -769,12 +769,12 @@ export class YTextEvent extends YEvent {
|
||||
const { key, value } = /** @type {ContentFormat} */ (item.content)
|
||||
if (this.adds(item)) {
|
||||
if (!this.deletes(item)) {
|
||||
const curVal = currentAttributes.get(key) || null
|
||||
const curVal = currentAttributes.get(key) ?? null
|
||||
if (!equalAttrs(curVal, value)) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
}
|
||||
if (equalAttrs(value, (oldAttributes.get(key) || null))) {
|
||||
if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
|
||||
delete attributes[key]
|
||||
} else {
|
||||
attributes[key] = value
|
||||
@ -785,7 +785,7 @@ export class YTextEvent extends YEvent {
|
||||
}
|
||||
} else if (this.deletes(item)) {
|
||||
oldAttributes.set(key, value)
|
||||
const curVal = currentAttributes.get(key) || null
|
||||
const curVal = currentAttributes.get(key) ?? null
|
||||
if (!equalAttrs(curVal, value)) {
|
||||
if (action === 'retain') {
|
||||
addOp()
|
||||
@ -897,6 +897,10 @@ export class YText extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YText}
|
||||
*/
|
||||
clone () {
|
||||
@ -1120,7 +1124,7 @@ export class YText extends AbstractType {
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index)
|
||||
const pos = findPosition(transaction, this, index, !attributes)
|
||||
if (!attributes) {
|
||||
attributes = {}
|
||||
// @ts-ignore
|
||||
@ -1138,20 +1142,20 @@ export class YText extends AbstractType {
|
||||
*
|
||||
* @param {number} index The index to insert the embed at.
|
||||
* @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
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
insertEmbed (index, embed, attributes = {}) {
|
||||
insertEmbed (index, embed, attributes) {
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index)
|
||||
insertText(transaction, this, pos, embed, attributes)
|
||||
const pos = findPosition(transaction, this, index, !attributes)
|
||||
insertText(transaction, this, pos, embed, attributes || {})
|
||||
})
|
||||
} 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 +1174,7 @@ export class YText extends AbstractType {
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
deleteText(transaction, findPosition(transaction, this, index), length)
|
||||
deleteText(transaction, findPosition(transaction, this, index, true), length)
|
||||
})
|
||||
} else {
|
||||
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
|
||||
@ -1194,7 +1198,7 @@ export class YText extends AbstractType {
|
||||
const y = this.doc
|
||||
if (y !== null) {
|
||||
transact(y, transaction => {
|
||||
const pos = findPosition(transaction, this, index)
|
||||
const pos = findPosition(transaction, this, index, false)
|
||||
if (pos.right === null) {
|
||||
return
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import {
|
||||
|
||||
/**
|
||||
* 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 childElements that must inherit from YXmlElement
|
||||
@ -81,6 +81,10 @@ export class YXmlElement extends YXmlFragment {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YXmlElement<KV>}
|
||||
*/
|
||||
clone () {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
YEvent,
|
||||
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
||||
|
@ -163,6 +163,10 @@ export class YXmlFragment extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YXmlFragment}
|
||||
*/
|
||||
clone () {
|
||||
@ -376,9 +380,9 @@ export class YXmlFragment extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Preppends content to this YArray.
|
||||
* Prepends content to this YArray.
|
||||
*
|
||||
* @param {Array<YXmlElement|YXmlText>} content Array of content to preppend.
|
||||
* @param {Array<YXmlElement|YXmlText>} content Array of content to prepend.
|
||||
*/
|
||||
unshift (content) {
|
||||
this.insert(0, content)
|
||||
@ -395,7 +399,8 @@ export class YXmlFragment extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms this YArray to a JavaScript Array.
|
||||
* Returns a portion of this YXmlFragment into a JavaScript Array selected
|
||||
* from start to end (end not included).
|
||||
*
|
||||
* @param {number} [start]
|
||||
* @param {number} [end]
|
||||
@ -406,7 +411,7 @@ export class YXmlFragment extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a provided function on once on overy child element.
|
||||
* Executes a provided function on once on every child element.
|
||||
*
|
||||
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
YMap,
|
||||
YXmlHookRefID,
|
||||
@ -30,6 +29,10 @@ export class YXmlHook extends YMap {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YXmlHook}
|
||||
*/
|
||||
clone () {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
YText,
|
||||
YXmlTextRefID,
|
||||
@ -31,6 +30,10 @@ export class YXmlText extends YText {
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a copy of this data type that can be included somewhere else.
|
||||
*
|
||||
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
|
||||
*
|
||||
* @return {YXmlText}
|
||||
*/
|
||||
clone () {
|
||||
|
@ -1,5 +1,4 @@
|
||||
|
||||
import { Observable } from 'lib0/observable'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
|
||||
import {
|
||||
Doc // eslint-disable-line
|
||||
@ -11,9 +10,9 @@ import {
|
||||
* @note This interface is experimental and it is not advised to actually inherit this class.
|
||||
* 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 {any} awareness
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
findIndexSS,
|
||||
getState,
|
||||
|
@ -8,12 +8,13 @@ import {
|
||||
YArray,
|
||||
YText,
|
||||
YMap,
|
||||
YXmlElement,
|
||||
YXmlFragment,
|
||||
transact,
|
||||
ContentDoc, Item, Transaction, YEvent // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import { Observable } from 'lib0/observable'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
import * as random from 'lib0/random'
|
||||
import * as map from 'lib0/map'
|
||||
import * as array from 'lib0/array'
|
||||
@ -33,10 +34,26 @@ export const generateNewClientId = random.uint32
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Yjs instance handles the state of shared data.
|
||||
* @extends Observable<string>
|
||||
* @typedef {Object} DocEvents
|
||||
* @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
|
||||
*/
|
||||
@ -114,7 +131,7 @@ export class Doc extends Observable {
|
||||
}
|
||||
this.isSynced = isSynced === undefined || isSynced === true
|
||||
if (this.isSynced && !this.isLoaded) {
|
||||
this.emit('load', [])
|
||||
this.emit('load', [this])
|
||||
}
|
||||
})
|
||||
/**
|
||||
@ -170,30 +187,31 @@ export class Doc extends Observable {
|
||||
/**
|
||||
* Define a shared data type.
|
||||
*
|
||||
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
|
||||
* Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result
|
||||
* and do not overwrite each other. I.e.
|
||||
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
|
||||
* `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)`
|
||||
*
|
||||
* After this method is called, the type is also available on `y.share.get(name)`.
|
||||
* After this method is called, the type is also available on `ydoc.share.get(name)`.
|
||||
*
|
||||
* *Best Practices:*
|
||||
* Define all types right after the Yjs instance is created and store them in a separate object.
|
||||
* Define all types right after the Y.Doc instance is created and store them in a separate object.
|
||||
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
|
||||
*
|
||||
* @template {typeof AbstractType<any>} Type
|
||||
* @example
|
||||
* const y = new Y(..)
|
||||
* const ydoc = new Y.Doc(..)
|
||||
* const appState = {
|
||||
* document: y.getText('document')
|
||||
* comments: y.getArray('comments')
|
||||
* document: ydoc.getText('document')
|
||||
* comments: ydoc.getArray('comments')
|
||||
* }
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
|
||||
* @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
|
||||
* @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
get (name, TypeConstructor = AbstractType) {
|
||||
get (name, TypeConstructor = /** @type {any} */ (AbstractType)) {
|
||||
const type = map.setIfUndefined(this.share, name, () => {
|
||||
// @ts-ignore
|
||||
const t = new TypeConstructor()
|
||||
@ -219,12 +237,12 @@ export class Doc extends Observable {
|
||||
t._length = type._length
|
||||
this.share.set(name, t)
|
||||
t._integrate(this, null)
|
||||
return t
|
||||
return /** @type {InstanceType<Type>} */ (t)
|
||||
} else {
|
||||
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
|
||||
*/
|
||||
getArray (name = '') {
|
||||
// @ts-ignore
|
||||
return this.get(name, YArray)
|
||||
return /** @type {YArray<T>} */ (this.get(name, YArray))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -246,7 +263,6 @@ export class Doc extends Observable {
|
||||
* @public
|
||||
*/
|
||||
getText (name = '') {
|
||||
// @ts-ignore
|
||||
return this.get(name, YText)
|
||||
}
|
||||
|
||||
@ -258,8 +274,17 @@ export class Doc extends Observable {
|
||||
* @public
|
||||
*/
|
||||
getMap (name = '') {
|
||||
// @ts-ignore
|
||||
return this.get(name, YMap)
|
||||
return /** @type {YMap<T>} */ (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
|
||||
*/
|
||||
getXmlFragment (name = '') {
|
||||
// @ts-ignore
|
||||
return this.get(name, YXmlFragment)
|
||||
}
|
||||
|
||||
@ -313,24 +337,9 @@ export class Doc extends Observable {
|
||||
transaction.subdocsRemoved.add(this)
|
||||
}, null, true)
|
||||
}
|
||||
this.emit('destroyed', [true])
|
||||
// @ts-ignore
|
||||
this.emit('destroyed', [true]) // DEPRECATED!
|
||||
this.emit('destroy', [this])
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import { AbstractType } from '../internals.js' // eslint-disable-line
|
||||
|
||||
import * as decoding from 'lib0/decoding'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
YArray,
|
||||
YMap,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
writeID,
|
||||
readID,
|
||||
@ -9,6 +8,7 @@ import {
|
||||
createID,
|
||||
ContentType,
|
||||
followRedone,
|
||||
getItem,
|
||||
ID, Doc, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
@ -257,13 +257,24 @@ export const readRelativePosition = decoder => {
|
||||
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
|
||||
|
||||
/**
|
||||
* Transform a relative position to an absolute position.
|
||||
*
|
||||
* If you want to share the relative position with other users, you should set
|
||||
* `followUndoneDeletions` to false to get consistent results across all clients.
|
||||
*
|
||||
* When calculating the absolute position, we try to follow the "undone deletions". This yields
|
||||
* better results for the user who performed undo. However, only the user who performed the undo
|
||||
* will get the better results, the other users don't know which operations recreated a deleted
|
||||
* range of content. There is more information in this ticket: https://github.com/yjs/yjs/issues/638
|
||||
*
|
||||
* @param {RelativePosition} rpos
|
||||
* @param {Doc} doc
|
||||
* @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638
|
||||
* @return {AbsolutePosition|null}
|
||||
*
|
||||
* @function
|
||||
*/
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => {
|
||||
const store = doc.store
|
||||
const rightID = rpos.item
|
||||
const typeID = rpos.type
|
||||
@ -275,7 +286,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
if (getState(store, rightID.client) <= rightID.clock) {
|
||||
return null
|
||||
}
|
||||
const res = followRedone(store, rightID)
|
||||
const res = followUndoneDeletions ? followRedone(store, rightID) : { item: getItem(store, rightID), diff: 0 }
|
||||
const right = res.item
|
||||
if (!(right instanceof Item)) {
|
||||
return null
|
||||
@ -299,7 +310,7 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
|
||||
// type does not exist yet
|
||||
return null
|
||||
}
|
||||
const { item } = followRedone(store, typeID)
|
||||
const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: getItem(store, typeID) }
|
||||
if (item instanceof Item && item.content instanceof ContentType) {
|
||||
type = item.content.type
|
||||
} else {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
isDeleted,
|
||||
createDeleteSetFromStructStore,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
GC,
|
||||
splitItem,
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
getState,
|
||||
writeStructsFromTransaction,
|
||||
@ -29,7 +28,8 @@ import { callAll } from 'lib0/function'
|
||||
* possible. Here is an example to illustrate the advantages of bundling:
|
||||
*
|
||||
* @example
|
||||
* const map = y.define('map', YMap)
|
||||
* const ydoc = new Y.Doc()
|
||||
* const map = ydoc.getMap('map')
|
||||
* // Log content when change is triggered
|
||||
* map.observe(() => {
|
||||
* console.log('change triggered')
|
||||
@ -38,7 +38,7 @@ import { callAll } from 'lib0/function'
|
||||
* map.set('a', 0) // => "change triggered"
|
||||
* map.set('b', 0) // => "change triggered"
|
||||
* // When put in a transaction, it will trigger the log after the transaction:
|
||||
* y.transact(() => {
|
||||
* ydoc.transact(() => {
|
||||
* map.set('a', 1)
|
||||
* map.set('b', 1)
|
||||
* }) // => "change triggered"
|
||||
@ -225,7 +225,7 @@ const tryGcDeleteSet = (ds, store, gcFilter) => {
|
||||
*/
|
||||
const tryMergeDeleteSet = (ds, store) => {
|
||||
// try to merge deleted / gc'd items
|
||||
// merge from right to left for better efficiecy and so we don't miss any merge targets
|
||||
// merge from right to left for better efficiency and so we don't miss any merge targets
|
||||
ds.clients.forEach((deleteItems, client) => {
|
||||
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
|
||||
for (let di = deleteItems.length - 1; di >= 0; di--) {
|
||||
|
@ -10,13 +10,13 @@ import {
|
||||
getItemCleanStart,
|
||||
isDeleted,
|
||||
addToDeleteSet,
|
||||
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||
YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as time from 'lib0/time'
|
||||
import * as array from 'lib0/array'
|
||||
import * as logging from 'lib0/logging'
|
||||
import { Observable } from 'lib0/observable'
|
||||
import { ObservableV2 } from 'lib0/observable'
|
||||
|
||||
export class StackItem {
|
||||
/**
|
||||
@ -48,15 +48,10 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
|
||||
/**
|
||||
* @param {UndoManager} undoManager
|
||||
* @param {Array<StackItem>} stack
|
||||
* @param {string} eventType
|
||||
* @param {'undo'|'redo'} eventType
|
||||
* @return {StackItem?}
|
||||
*/
|
||||
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
|
||||
* @type {any}
|
||||
@ -65,7 +60,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
const doc = undoManager.doc
|
||||
const scope = undoManager.scope
|
||||
transact(doc, transaction => {
|
||||
while (stack.length > 0 && result === null) {
|
||||
while (stack.length > 0 && undoManager.currStackItem === null) {
|
||||
const store = doc.store
|
||||
const stackItem = /** @type {StackItem} */ (stack.pop())
|
||||
/**
|
||||
@ -113,7 +108,7 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
performedChange = true
|
||||
}
|
||||
}
|
||||
result = performedChange ? stackItem : null
|
||||
undoManager.currStackItem = performedChange ? stackItem : null
|
||||
}
|
||||
transaction.changed.forEach((subProps, type) => {
|
||||
// destroy search marker if necessary
|
||||
@ -123,11 +118,12 @@ const popStackItem = (undoManager, stack, eventType) => {
|
||||
})
|
||||
_tr = transaction
|
||||
}, undoManager)
|
||||
if (result != null) {
|
||||
if (undoManager.currStackItem != null) {
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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
|
||||
* 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 {UndoManagerOptions} options
|
||||
@ -191,6 +195,12 @@ export class UndoManager extends Observable {
|
||||
*/
|
||||
this.undoing = 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.ignoreRemoteMapChanges = ignoreRemoteMapChanges
|
||||
this.captureTimeout = captureTimeout
|
||||
@ -244,6 +254,9 @@ export class UndoManager extends Observable {
|
||||
keepItem(item, true)
|
||||
}
|
||||
})
|
||||
/**
|
||||
* @type {[StackItemEvent, UndoManager]}
|
||||
*/
|
||||
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', changeEvent)
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
isDeleted,
|
||||
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* @module encoding
|
||||
*/
|
||||
@ -155,7 +154,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
||||
// @type {string|null}
|
||||
const struct = new Item(
|
||||
createID(client, clock),
|
||||
null, // leftd
|
||||
null, // left
|
||||
(info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null, // origin
|
||||
null, // right
|
||||
(info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null, // right origin
|
||||
@ -179,7 +178,7 @@ export const readClientsStructRefs = (decoder, doc) => {
|
||||
|
||||
const struct = new Item(
|
||||
createID(client, clock),
|
||||
null, // leftd
|
||||
null, // left
|
||||
origin, // origin
|
||||
null, // right
|
||||
rightOrigin, // right origin
|
||||
@ -371,7 +370,7 @@ export const writeStructsFromTransaction = (encoder, transaction) => writeClient
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
*
|
||||
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||
* This function has the same effect as `applyUpdate` but accepts a decoder.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Doc} ydoc
|
||||
@ -452,7 +451,7 @@ export const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = n
|
||||
/**
|
||||
* Read and apply a document update.
|
||||
*
|
||||
* This function has the same effect as `applyUpdate` but accepts an decoder.
|
||||
* This function has the same effect as `applyUpdate` but accepts a decoder.
|
||||
*
|
||||
* @param {decoding.Decoder} decoder
|
||||
* @param {Doc} ydoc
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
||||
|
||||
/**
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import {
|
||||
AbstractType // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import * as binary from 'lib0/binary'
|
||||
import * as decoding from 'lib0/decoding'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import * as Y from '../src/index.js'
|
||||
import * as t from 'lib0/testing'
|
||||
|
||||
@ -102,3 +101,25 @@ export const testRelativePositionAssociationDifference = tc => {
|
||||
t.assert(posRight != null && posRight.index === 2)
|
||||
t.assert(posLeft != null && posLeft.index === 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testRelativePositionWithUndo = tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
const ytext = ydoc.getText()
|
||||
ytext.insert(0, 'hello world')
|
||||
const rpos = Y.createRelativePositionFromTypeIndex(ytext, 1)
|
||||
const um = new Y.UndoManager(ytext)
|
||||
ytext.delete(0, 6)
|
||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 0)
|
||||
um.undo()
|
||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc)?.index === 1)
|
||||
const posWithoutFollow = Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)
|
||||
console.log({ posWithoutFollow })
|
||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydoc, false)?.index === 6)
|
||||
const ydocClone = new Y.Doc()
|
||||
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
|
||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone)?.index === 6)
|
||||
t.assert(Y.createAbsolutePositionFromRelativePosition(rpos, ydocClone, false)?.index === 6)
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import * as t from 'lib0/testing'
|
||||
import * as prng from 'lib0/prng'
|
||||
import * as encoding from 'lib0/encoding'
|
||||
@ -35,7 +34,7 @@ export const encV1 = {
|
||||
mergeUpdates: Y.mergeUpdates,
|
||||
applyUpdate: Y.applyUpdate,
|
||||
logUpdate: Y.logUpdate,
|
||||
updateEventName: 'update',
|
||||
updateEventName: /** @type {'update'} */ ('update'),
|
||||
diffUpdate: Y.diffUpdate
|
||||
}
|
||||
|
||||
@ -44,7 +43,7 @@ export const encV2 = {
|
||||
mergeUpdates: Y.mergeUpdatesV2,
|
||||
applyUpdate: Y.applyUpdateV2,
|
||||
logUpdate: Y.logUpdateV2,
|
||||
updateEventName: 'updateV2',
|
||||
updateEventName: /** @type {'updateV2'} */ ('updateV2'),
|
||||
diffUpdate: Y.diffUpdateV2
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,46 @@ import { init } from './testHelper.js' // eslint-disable-line
|
||||
import * as Y from '../src/index.js'
|
||||
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
|
||||
*/
|
||||
@ -675,3 +715,33 @@ export const testUndoDeleteInMap = (tc) => {
|
||||
undoManager.undo()
|
||||
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')
|
||||
}
|
||||
|
@ -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(Y.Doc):Uint8Array} Enc.encodeStateVector
|
||||
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
|
||||
* @property {string} Enc.updateEventName
|
||||
* @property {'update'|'updateV2'} Enc.updateEventName
|
||||
* @property {string} Enc.description
|
||||
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
|
||||
*/
|
||||
@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
|
||||
// t.info('Target State: ')
|
||||
// enc.logUpdate(targetState)
|
||||
|
||||
cases.forEach((mergedUpdates, i) => {
|
||||
cases.forEach((mergedUpdates) => {
|
||||
// t.info('State Case $' + i + ':')
|
||||
// enc.logUpdate(updates)
|
||||
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 => {
|
||||
encoders.forEach((enc, i) => {
|
||||
export const testMergeUpdates1 = _tc => {
|
||||
encoders.forEach((enc) => {
|
||||
t.info(`Using encoder: ${enc.description}`)
|
||||
const ydoc = new Y.Doc({ gc: false })
|
||||
const updates = /** @type {Array<Uint8Array>} */ ([])
|
||||
@ -299,16 +299,16 @@ export const testMergePendingUpdates = tc => {
|
||||
Y.applyUpdate(yDoc5, update4)
|
||||
Y.applyUpdate(yDoc5, serverUpdates[4])
|
||||
// @ts-ignore
|
||||
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
|
||||
const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
|
||||
|
||||
const yText5 = yDoc5.getText('textBlock')
|
||||
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 ytext = ydoc.getText('text')
|
||||
const ymap = ydoc.getMap('map')
|
||||
|
@ -1746,6 +1746,27 @@ export const testBasicFormat = tc => {
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} tc
|
||||
*/
|
||||
export const testFalsyFormats = tc => {
|
||||
const { users, text0 } = init(tc, { users: 2 })
|
||||
let delta
|
||||
text0.observe(event => {
|
||||
delta = event.delta
|
||||
})
|
||||
text0.insert(0, 'abcde', { falsy: false })
|
||||
t.compare(text0.toDelta(), [{ insert: 'abcde', attributes: { falsy: false } }])
|
||||
t.compare(delta, [{ insert: 'abcde', attributes: { falsy: false } }])
|
||||
text0.format(1, 3, { falsy: true })
|
||||
t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'bcd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
|
||||
t.compare(delta, [{ retain: 1 }, { retain: 3, attributes: { falsy: true } }])
|
||||
text0.format(2, 1, { falsy: false })
|
||||
t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'b', attributes: { falsy: true } }, { insert: 'c', attributes: { falsy: false } }, { insert: 'd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
|
||||
t.compare(delta, [{ retain: 2 }, { retain: 1, attributes: { falsy: false } }])
|
||||
compare(users)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {t.TestCase} _tc
|
||||
*/
|
||||
|
@ -189,7 +189,6 @@ export const testClone = _tc => {
|
||||
const third = new Y.XmlElement('p')
|
||||
yxml.push([first, second, third])
|
||||
t.compareArrays(yxml.toArray(), [first, second, third])
|
||||
|
||||
const cloneYxml = yxml.clone()
|
||||
ydoc.getArray('copyarr').insert(0, [cloneYxml])
|
||||
t.assert(cloneYxml.length === 3)
|
||||
@ -210,3 +209,15 @@ export const testFormattingBug = _tc => {
|
||||
yxml.applyDelta(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])
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user