Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8582442e3 | ||
|
|
f54ea625e2 | ||
|
|
ce06b2abec | ||
|
|
e1bce03ed8 | ||
|
|
16d9638bc8 | ||
|
|
415a645874 | ||
|
|
1cb52dc863 | ||
|
|
7a8ca6eaa5 | ||
|
|
e348255bb1 | ||
|
|
79c095d4dc | ||
|
|
0241fd3c40 | ||
|
|
cf78ce12b2 | ||
|
|
77bd74127d | ||
|
|
fe36ffd122 | ||
|
|
28ccd5e0dd | ||
|
|
1d4f2e5435 | ||
|
|
2c0daeb071 |
101
README.md
101
README.md
@@ -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. [](https://github.com/sponsors/dmonad)
|
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
|
## 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. [ 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. [ 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. [ 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,37 @@ 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>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
# Ports
|
# Ports
|
||||||
@@ -684,6 +739,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>
|
||||||
|
|||||||
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",
|
"name": "yjs",
|
||||||
"version": "13.6.9",
|
"version": "13.6.12",
|
||||||
"description": "Shared Editing Library",
|
"description": "Shared Editing Library",
|
||||||
"main": "./dist/yjs.cjs",
|
"main": "./dist/yjs.cjs",
|
||||||
"module": "./dist/yjs.mjs",
|
"module": "./dist/yjs.mjs",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export {
|
|||||||
getItem,
|
getItem,
|
||||||
typeListToArraySnapshot,
|
typeListToArraySnapshot,
|
||||||
typeMapGetSnapshot,
|
typeMapGetSnapshot,
|
||||||
|
typeMapGetAllSnapshot,
|
||||||
createDocFromSnapshot,
|
createDocFromSnapshot,
|
||||||
iterateDeletedStructs,
|
iterateDeletedStructs,
|
||||||
applyUpdate,
|
applyUpdate,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
readYArray,
|
readYArray,
|
||||||
readYMap,
|
readYMap,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
addStruct,
|
addStruct,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
getState,
|
getState,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractStruct,
|
AbstractStruct,
|
||||||
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, ID // eslint-disable-line
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
removeEventHandlerListener,
|
removeEventHandlerListener,
|
||||||
callEventHandlerListeners,
|
callEventHandlerListeners,
|
||||||
@@ -925,6 +924,34 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
|
|||||||
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {AbstractType<any>} parent
|
||||||
|
* @param {Snapshot} snapshot
|
||||||
|
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @function
|
||||||
|
*/
|
||||||
|
export const typeMapGetAllSnapshot = (parent, snapshot) => {
|
||||||
|
/**
|
||||||
|
* @type {Object<string,any>}
|
||||||
|
*/
|
||||||
|
const res = {}
|
||||||
|
parent._map.forEach((value, key) => {
|
||||||
|
/**
|
||||||
|
* @type {Item|null}
|
||||||
|
*/
|
||||||
|
let v = value
|
||||||
|
while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) {
|
||||||
|
v = v.left
|
||||||
|
}
|
||||||
|
if (v !== null && isVisible(v, snapshot)) {
|
||||||
|
res[key] = v.content.getContent()[v.length - 1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Map<string,Item>} map
|
* @param {Map<string,Item>} map
|
||||||
* @return {IterableIterator<Array<any>>}
|
* @return {IterableIterator<Array<any>>}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module YMap
|
* @module YMap
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
typeMapSet,
|
typeMapSet,
|
||||||
typeMapGet,
|
typeMapGet,
|
||||||
typeMapGetAll,
|
typeMapGetAll,
|
||||||
|
typeMapGetAllSnapshot,
|
||||||
typeListForEach,
|
typeListForEach,
|
||||||
YXmlElementRefID,
|
YXmlElementRefID,
|
||||||
YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -19,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
|
||||||
@@ -192,12 +193,13 @@ export class YXmlElement extends YXmlFragment {
|
|||||||
/**
|
/**
|
||||||
* Returns all attribute name/value pairs in a JSON Object.
|
* Returns all attribute name/value pairs in a JSON Object.
|
||||||
*
|
*
|
||||||
|
* @param {Snapshot} [snapshot]
|
||||||
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
|
||||||
*
|
*
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getAttributes () {
|
getAttributes (snapshot) {
|
||||||
return /** @type {any} */ (typeMapGetAll(this))
|
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YEvent,
|
YEvent,
|
||||||
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YMap,
|
YMap,
|
||||||
YXmlHookRefID,
|
YXmlHookRefID,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YText,
|
YText,
|
||||||
YXmlTextRefID,
|
YXmlTextRefID,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { Observable } from 'lib0/observable'
|
import { Observable } from 'lib0/observable'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
findIndexSS,
|
findIndexSS,
|
||||||
getState,
|
getState,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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
|
||||||
@@ -180,6 +181,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 +190,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 +221,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 +237,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 +247,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 +258,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 +278,6 @@ export class Doc extends Observable {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
getXmlFragment (name = '') {
|
getXmlFragment (name = '') {
|
||||||
// @ts-ignore
|
|
||||||
return this.get(name, YXmlFragment)
|
return this.get(name, YXmlFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
YArray,
|
YArray,
|
||||||
YMap,
|
YMap,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
writeID,
|
writeID,
|
||||||
readID,
|
readID,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isDeleted,
|
isDeleted,
|
||||||
createDeleteSetFromStructStore,
|
createDeleteSetFromStructStore,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
GC,
|
GC,
|
||||||
splitItem,
|
splitItem,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getState,
|
getState,
|
||||||
writeStructsFromTransaction,
|
writeStructsFromTransaction,
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
isDeleted,
|
isDeleted,
|
||||||
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @module encoding
|
* @module encoding
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
import { AbstractType, Item } from '../internals.js' // eslint-disable-line
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
AbstractType // eslint-disable-line
|
AbstractType // eslint-disable-line
|
||||||
} from '../internals.js'
|
} from '../internals.js'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,21 @@ export const testBasic = _tc => {
|
|||||||
t.assert(restored.getText().toString() === 'world!')
|
t.assert(restored.getText().toString() === 'world!')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {t.TestCase} _tc
|
||||||
|
*/
|
||||||
|
export const testBasicXmlAttributes = _tc => {
|
||||||
|
const ydoc = new Y.Doc({ gc: false })
|
||||||
|
const yxml = ydoc.getMap().set('el', new Y.XmlElement('div'))
|
||||||
|
const snapshot1 = Y.snapshot(ydoc)
|
||||||
|
yxml.setAttribute('a', '1')
|
||||||
|
const snapshot2 = Y.snapshot(ydoc)
|
||||||
|
yxml.setAttribute('a', '2')
|
||||||
|
t.compare(yxml.getAttributes(), { a: '2' })
|
||||||
|
t.compare(yxml.getAttributes(snapshot2), { a: '1' })
|
||||||
|
t.compare(yxml.getAttributes(snapshot1), {})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {t.TestCase} _tc
|
* @param {t.TestCase} _tc
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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])
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user