Compare commits

...

54 Commits

Author SHA1 Message Date
Kevin Jahns
952a9b2c41 13.0.0-92 2019-06-23 13:05:30 +02:00
Kevin Jahns
03458dc641 Port Undo/Redo approach with a clean API 2019-06-23 13:04:14 +02:00
Kevin Jahns
14df5b72af fix consistency bug - ref.toStruct does not correctly create GC when offset is specified 2019-06-18 18:46:19 +02:00
Kevin Jahns
338968031b 13.0.0-91 2019-06-18 18:05:39 +02:00
Kevin Jahns
1aac245b93 New types dont fire events - fixes #155 2019-06-18 17:41:19 +02:00
Kevin Jahns
1faff323c1 13.0.0-90 2019-06-14 16:00:02 +02:00
Kevin Jahns
e7280c7ae2 allow case sensitive yxml nodes 2019-06-14 15:59:00 +02:00
Kevin Jahns
4c38619b5d 13.0.0-89 2019-06-13 10:33:35 +02:00
Kevin Jahns
b4e5c5cc1f Correctly insert embed when using YText.applyDelta 2019-06-13 10:30:39 +02:00
Kevin Jahns
b0dbd84f7f lint markdown 2019-06-13 10:28:30 +02:00
Kevin Jahns
4a990963d9 13.0.0-88 2019-06-05 18:37:21 +02:00
Kevin Jahns
7e7c9d5b11 add relevant type information 2019-06-05 14:53:00 +02:00
Kevin Jahns
775f6eed1d fix websocket example 2019-06-02 15:16:14 +02:00
Kevin Jahns
1e83b9418c 13.0.0-87 2019-05-28 14:20:44 +02:00
Kevin Jahns
ac3f672c80 Merge branch 'master' of github.com:y-js/yjs 2019-05-28 14:19:11 +02:00
Kevin Jahns
2192aa5821 Use generic Item with typed content to reduce cache misses 2019-05-28 14:18:20 +02:00
Kevin Jahns
70bb523005 Merge branch 'master' of github.com:y-js/yjs 2019-05-27 12:50:21 +02:00
Kevin Jahns
10ce6de57a import statement fix 2019-05-27 12:50:12 +02:00
Kevin Jahns
3fba4f25a5 Merge pull request #153 from calibr/124-text-embeds
process embeds in YText.toDelta
2019-05-25 13:04:10 +02:00
Kevin Jahns
66c35d8499 testing: do not stringify array values before comparing 2019-05-25 12:54:30 +02:00
Kevin Jahns
4c14157dcf 13.0.0-86 2019-05-25 12:50:05 +02:00
Kevin Jahns
ef6c382e20 fix array iterator on merged content. fixes #152 2019-05-25 12:49:08 +02:00
calibr
ee45b4fdd6 process embeds in YText.toDelta 2019-05-25 13:48:57 +03:00
Kevin Jahns
668e9e8a9b 13.0.0-85 2019-05-25 03:13:54 +02:00
Kevin Jahns
37a6d68543 implement support for boolean values. fixes #151 2019-05-25 03:12:56 +02:00
Kevin Jahns
f893198769 remove examples. fixes #149 2019-05-22 17:32:51 +02:00
Kevin Jahns
d3ee1a0ec2 Add editor support to v13 readme 2019-05-22 01:26:13 +02:00
Kevin Jahns
d6593412a2 13.0.0-84 2019-05-19 21:49:36 +02:00
Kevin Jahns
d31bf36531 use generated esm module by default 2019-05-19 21:48:09 +02:00
Kevin Jahns
a485f550db 13.0.0-83 2019-05-19 20:59:56 +02:00
Kevin Jahns
0610b16227 bump lib0 for webpack compatibility 2019-05-19 20:43:18 +02:00
Kevin Jahns
72e470c5f0 Fix ytext event.delta - items that are synced and deleted
When items are added and deleted in the same transaction, event.delta would recognize them as added (though they are actually deleted). Now it just ignores them.
2019-05-19 20:42:53 +02:00
Kevin Jahns
4d12a02e2f fix offset in state vector 2019-05-16 12:31:53 +02:00
Kevin Jahns
4a7d6f0a2d fix sorting bug that only affects older node versions (probably because old sorting algorithms are not stable) 2019-05-14 15:21:34 +02:00
Kevin Jahns
c80f446b5f README: update provider tutorial 2019-05-12 11:18:43 +02:00
Kevin Jahns
81a529d8dc update *getting started* yjs version 2019-05-07 15:43:09 +02:00
Kevin Jahns
4f0ab78914 13.0.0-82 2019-05-07 13:54:00 +02:00
Kevin Jahns
8c36f67f0b rework and document api 2019-05-07 13:44:23 +02:00
Kevin Jahns
77687d94e6 13.0.0-81 2019-04-28 17:32:05 +02:00
Kevin Jahns
4644511303 bump y-protocols dependency 2019-04-28 17:30:52 +02:00
Kevin Jahns
20005eecdb Merge deleted items more efficiently.
Previously deleted items were simply added to transaction._mergeStructs. But this inherently inefficient as it will splice the struct store for every item.

Now Yjs iterates over transaction.ds and tries to merge structs. It iterates from right to left so merging should be more efficient that before. But more work needs to be done.

For example we could set structs[i] = null and filter the structs after merging is done.
2019-04-28 17:20:35 +02:00
Kevin Jahns
c9dda245bf v13 api docs 2019-04-28 02:53:25 +02:00
Kevin Jahns
1417470156 update demos link 2019-04-27 03:44:48 +02:00
Kevin Jahns
584e5dfd40 Link to v13 docs from README 2019-04-27 03:35:44 +02:00
Kevin Jahns
805acbb9f5 13.0.0-80 2019-04-26 19:55:14 +02:00
Kevin Jahns
32c4c09072 update parent._map when splitting an item 2019-04-26 19:54:00 +02:00
Kevin Jahns
8c5a06bbf8 fix gc when item is deleted in observer call 2019-04-26 18:37:38 +02:00
Kevin Jahns
a336cc167c order observer and transaction cleanups after one another 2019-04-26 13:31:00 +02:00
Kevin Jahns
21d86cd2be Delete all children of ItemType when it is deleted 2019-04-26 12:29:28 +02:00
Kevin Jahns
1d0f9faa91 AbstractItem.mergeWith helper outsourced into separate function 2019-04-24 18:10:33 +02:00
Kevin Jahns
45237571b7 gc more efficiently 2019-04-23 20:51:32 +02:00
Kevin Jahns
bb6f6cd141 13.0.0-79 2019-04-20 00:03:30 +02:00
Kevin Jahns
729c1f16b8 fix test provider 2019-04-20 00:02:40 +02:00
Kevin Jahns
b6059704aa update dependencies 2019-04-20 00:00:09 +02:00
68 changed files with 5496 additions and 8085 deletions

4
.markdownlint.json Normal file
View File

@@ -0,0 +1,4 @@
{
"default": true,
"no-inline-html": false
}

View File

@@ -6,8 +6,11 @@ text, richtext, json, or XML. It is fairly easy to get started, as Yjs hides
most of the complexity of concurrent editing. For additional information, demos, most of the complexity of concurrent editing. For additional information, demos,
and tutorials visit [y-js.org](http://y-js.org/). and tutorials visit [y-js.org](http://y-js.org/).
:warning: Checkout the [v13 docs](./README.v13.md) for the upcoming release :warning:
### Extensions ### Extensions
Yjs only knows how to resolve conflicts on shared data. You have to choose a .. Yjs only knows how to resolve conflicts on shared data. You have to choose a ..
* *Connector* - a communication protocol that propagates changes to the clients * *Connector* - a communication protocol that propagates changes to the clients
* *Database* - a database to store your changes * *Database* - a database to store your changes
* one or more *Types* - that represent the shared data * one or more *Types* - that represent the shared data
@@ -33,7 +36,6 @@ is a list of the modules we know of:
|[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser | |[indexeddb](https://github.com/y-js/y-indexeddb) | Offline storage for the browser |
|[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps | |[leveldb](https://github.com/y-js/y-leveldb) | Persistent storage for node apps |
##### Types ##### Types
| Name | Description | | Name | Description |
@@ -55,6 +57,7 @@ Install Yjs, and its modules with [bower](http://bower.io/), or
[npm](https://www.npmjs.org/package/yjs). [npm](https://www.npmjs.org/package/yjs).
### Bower ### Bower
``` ```
bower install --save yjs y-array % add all y-* modules you want to use bower install --save yjs y-array % add all y-* modules you want to use
``` ```
@@ -65,6 +68,7 @@ missing modules.
``` ```
### CDN ### CDN
``` ```
<script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script> <script src="https://cdn.jsdelivr.net/npm/yjs@12/dist/y.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script> <script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
@@ -77,6 +81,7 @@ missing modules.
``` ```
### Npm ### Npm
``` ```
npm install --save yjs % add all y-* modules you want to use npm install --save yjs % add all y-* modules you want to use
``` ```
@@ -95,6 +100,7 @@ require('y-text')(Y)
``` ```
### ES6 Syntax ### ES6 Syntax
``` ```
import Y from 'yjs' import Y from 'yjs'
import yArray from 'y-array' import yArray from 'y-array'
@@ -107,6 +113,7 @@ Y.extend(yArray, yWebsocketsClient, yMemory, yArray, yMap, yText /*, .. */)
``` ```
# Text editing example # Text editing example
Install dependencies Install dependencies
``` ```
bower i yjs y-memory y-webrtc y-array y-text bower i yjs y-memory y-webrtc y-array y-text
@@ -294,12 +301,5 @@ DEBUG_COLORS=0 DEBUG=y* node app.js > log
localStorage.debug = 'y*' localStorage.debug = 'y*'
``` ```
## Contribution
I created this framework during my bachelor thesis at the chair of computer
science 5 [(i5)](http://dbis.rwth-aachen.de/cms), RWTH University. Since
December 2014 I'm working on Yjs as a part of my student worker job at the i5.
## License ## License
Yjs is licensed under the [MIT License](./LICENSE). Yjs is licensed under the [MIT License](./LICENSE).
<yjs@dbis.rwth-aachen.de>

File diff suppressed because it is too large Load Diff

1
examples/.gitignore vendored
View File

@@ -1 +0,0 @@
build

View File

@@ -1,70 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs CodeMirror Example</title>
<link rel=stylesheet href="https://codemirror.net/lib/codemirror.css">
<style>
#container {
border: grey;
border-style: solid;
border-width: thin;
}
</style>
</head>
<body>
<p>This example shows how to bind a YText type to <a href="https://codemirror.net/">CodeMirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<style>
.remote-caret {
position: absolute;
border-left: black;
border-left-style: solid;
border-left-width: 2px;
height: 1em;
}
.remote-caret > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
</style>
<div id="container"></div>
</div>
<!-- The actual source file for the following code is found in ./codemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/codemirror.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { CodeMirrorBinding } from 'yjs/bindings/codemirror.js'
import * as conf from './exampleConfig.js'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('codemirror')
const ytext = ydocument.define('codemirror', Y.Text)
const editor = new CodeMirror(document.querySelector('#container'), {
mode: 'javascript',
lineNumbers: true
})
const binding = new CodeMirrorBinding(ytext, editor)
window.codemirrorExample = {
binding, editor, ytext, ydocument
}
</script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
import { WebsocketProvider } from 'y-websocket'
import { CodeMirrorBinding } from 'y-codemirror'
import * as conf from './exampleConfig.js'
import CodeMirror from 'codemirror'
import 'codemirror/mode/javascript/javascript.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('codemirror')
const ytext = ydocument.getText('codemirror')
const editor = new CodeMirror(document.querySelector('#container'), {
mode: 'javascript',
lineNumbers: true
})
const binding = new CodeMirrorBinding(ytext, editor)
window.codemirrorExample = {
binding, editor, ytext, ydocument
}

View File

@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="https://prosemirror.net/css/editor.css">
<style>
#content {
min-height: 500px;
}
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to an arbitrary DOM element. We set the DOM element to contenteditable so it basically behaves like a very powerful rich-text editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<hr>
<div class="code-html">
<div id="content" contenteditable=""></div>
</div>
<!-- The actual source file for the following code is found in ./dom.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/dom.js" type="module">
import * as Y from 'yjs/index.js'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { DomBinding } from 'yjs/bindings/dom.js'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('dom')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
window.example = {
provider, ydocument, type, binding
}
</script>
</body>
</html>

View File

@@ -1,13 +0,0 @@
import * as Y from '../src/index.js'
import { WebsocketProvider } from 'y-websocket'
import { DomBinding } from 'y-dom'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('dom')
const type = ydocument.define('xml', Y.XmlFragment)
const binding = new DomBinding(type, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
window.example = {
provider, ydocument, type, binding
}

View File

@@ -1,9 +0,0 @@
/* eslint-env browser */
const isDeployed = location.hostname === 'yjs.website'
if (!isDeployed) {
console.log('%cYjs: Start your local websocket server by running %c`npm run websocket-server`', 'color:blue', 'color: grey; font-weight: bold')
}
export const serverAddress = isDeployed ? 'wss://api.yjs.website' : 'ws://localhost:1234'

View File

@@ -1,17 +0,0 @@
{
"codemirror": {
"title": "CodeMirror Binding"
},
"prosemirror": {
"title": "ProseMirror Binding"
},
"textarea": {
"title": "Textarea Binding"
},
"quill": {
"title": "Quill Binding"
},
"dom": {
"title": "Dom Binding"
}
}

View File

@@ -1,159 +0,0 @@
import { Plugin } from 'prosemirror-state'
import crel from 'crel'
import * as Y from '../src/index.js'
import { prosemirrorPluginKey } from 'y-prosemirror'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as historyProtocol from 'y-protocols/history.js'
const niceColors = ['#3cb44b', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#008080', '#9a6324', '#800000', '#808000', '#000075', '#808080']
const createUserCSS = (userid, username, color = 'rgb(250, 129, 0)', color2 = 'rgba(250, 129, 0, .41)') => `
[ychange_state][ychange_user="${userid}"]:hover::before {
content: "${username}" !important;
background-color: ${color} !important;
}
[ychange_state="added"][ychange_user="${userid}"] {
background-color: ${color2} !important;
}
[ychange_state="removed"][ychange_user="${userid}"] {
color: ${color} !important;
}
`
export const noteHistoryPlugin = new Plugin({
state: {
init (initargs, state) {
return new NoteHistoryPlugin()
},
apply (tr, pluginState) {
return pluginState
}
},
view (editorView) {
const hstate = noteHistoryPlugin.getState(editorView.state)
hstate.init(editorView)
return {
destroy: hstate.destroy.bind(hstate)
}
}
})
const createWrapper = () => {
const wrapper = crel('div', { style: 'display: flex;' })
const historyContainer = crel('div', { style: 'align-self: baseline; flex-basis: 250px;', class: 'shared-history' })
wrapper.insertBefore(historyContainer, null)
const userStyleContainer = crel('style')
wrapper.insertBefore(userStyleContainer, null)
return { wrapper, historyContainer, userStyleContainer }
}
class NoteHistoryPlugin {
init (editorView) {
this.editorView = editorView
const { historyContainer, wrapper, userStyleContainer } = createWrapper()
this.userStyleContainer = userStyleContainer
this.wrapper = wrapper
this.historyContainer = historyContainer
const n = editorView.dom.parentNode.parentNode
n.parentNode.replaceChild(this.wrapper, n)
n.style['flex-grow'] = '1'
wrapper.insertBefore(n, this.wrapper.firstChild)
this.render()
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
history.observe(this.render.bind(this))
}
destroy () {
this.wrapper.parentNode.replaceChild(this.wrapper.firstChild, this.wrapper)
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
history.unobserve(this.render)
}
render () {
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array).toArray()
const fragment = document.createDocumentFragment()
const snapshotBtn = crel('button', { type: 'button' }, ['snapshot'])
fragment.insertBefore(snapshotBtn, null)
let _prevSnap = null // empty
snapshotBtn.addEventListener('click', () => {
const awareness = y.getAwarenessInfo()
const userMap = new Map()
const aw = y.getLocalAwarenessInfo()
userMap.set(y.userID, aw.name || 'unknown')
awareness.forEach((a, userID) => {
userMap.set(userID, a.name || 'Unknown')
})
this.snapshot(userMap)
})
history.forEach(buf => {
const decoder = decoding.createDecoder(buf)
const snapshot = historyProtocol.readHistorySnapshot(decoder)
const date = new Date(decoding.readUint32(decoder) * 1000)
const restoreBtn = crel('button', { type: 'button' }, ['restore'])
const a = crel('a', [
'• ' + date.toUTCString(), restoreBtn
])
const el = crel('div', [ a ])
let prevSnapshot = _prevSnap // rebind to new variable
restoreBtn.addEventListener('click', event => {
if (prevSnapshot === null) {
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
}
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot, restore: true }))
event.stopPropagation()
})
a.addEventListener('click', () => {
console.log('setting snapshot')
if (prevSnapshot === null) {
prevSnapshot = { ds: snapshot.ds, sm: new Map() }
}
this.renderSnapshot(snapshot, prevSnapshot)
})
fragment.insertBefore(el, null)
_prevSnap = snapshot
})
this.historyContainer.innerHTML = ''
this.historyContainer.insertBefore(fragment, null)
}
renderSnapshot (snapshot, prevSnapshot) {
this.editorView.dispatch(this.editorView.state.tr.setMeta(prosemirrorPluginKey, { snapshot, prevSnapshot }))
/**
* @type {Array<string|null>}
*/
let colors = niceColors.slice()
let style = ''
snapshot.userMap.forEach((name, userid) => {
/**
* @type {any}
*/
const randInt = name.split('').map(s => s.charCodeAt(0)).reduce((a, b) => a + b)
let color = null
let i = 0
for (; i < colors.length && color === null; i++) {
color = colors[(randInt + i) % colors.length]
}
if (color === null) {
colors = niceColors.slice()
i = 0
color = colors[randInt % colors.length]
}
colors[randInt % colors.length] = null
style += createUserCSS(userid, name, color, color + '69')
})
this.userStyleContainer.innerHTML = style
}
/**
* @param {Map<number, string>} [updatedUserMap] Maps from userid (yjs model) to account name (e.g. mail address)
*/
snapshot (updatedUserMap = new Map()) {
const y = prosemirrorPluginKey.getState(this.editorView.state).y
const history = y.define('history', Y.Array)
const encoder = encoding.createEncoder()
historyProtocol.writeHistorySnapshot(encoder, y, updatedUserMap)
encoding.writeUint32(encoder, Math.floor(Date.now() / 1000))
history.push([encoding.toBuffer(encoder)])
}
}

View File

@@ -1,197 +0,0 @@
import { Schema } from 'prosemirror-model'
const brDOM = ['br']
const calcYchangeDomAttrs = (attrs, domAttrs = {}) => {
domAttrs = Object.assign({}, domAttrs)
if (attrs.ychange !== null) {
domAttrs.ychange_user = attrs.ychange.user
domAttrs.ychange_state = attrs.ychange.state
}
return domAttrs
}
// :: Object
// [Specs](#model.NodeSpec) for the nodes defined in this schema.
export const nodes = {
// :: NodeSpec The top level document node.
doc: {
content: 'block+'
},
// :: NodeSpec A plain paragraph textblock. Represented in the DOM
// as a `<p>` element.
paragraph: {
attrs: { ychange: { default: null } },
content: 'inline*',
group: 'block',
parseDOM: [{ tag: 'p' }],
toDOM (node) { return ['p', calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
blockquote: {
attrs: { ychange: { default: null } },
content: 'block+',
group: 'block',
defining: true,
parseDOM: [{ tag: 'blockquote' }],
toDOM (node) { return ['blockquote', calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A horizontal rule (`<hr>`).
horizontal_rule: {
attrs: { ychange: { default: null } },
group: 'block',
parseDOM: [{ tag: 'hr' }],
toDOM (node) {
return ['hr', calcYchangeDomAttrs(node.attrs)]
}
},
// :: NodeSpec A heading textblock, with a `level` attribute that
// should hold the number 1 to 6. Parsed and serialized as `<h1>` to
// `<h6>` elements.
heading: {
attrs: {
level: { default: 1 },
ychange: { default: null }
},
content: 'inline*',
group: 'block',
defining: true,
parseDOM: [{ tag: 'h1', attrs: { level: 1 } },
{ tag: 'h2', attrs: { level: 2 } },
{ tag: 'h3', attrs: { level: 3 } },
{ tag: 'h4', attrs: { level: 4 } },
{ tag: 'h5', attrs: { level: 5 } },
{ tag: 'h6', attrs: { level: 6 } }],
toDOM (node) { return ['h' + node.attrs.level, calcYchangeDomAttrs(node.attrs), 0] }
},
// :: NodeSpec A code listing. Disallows marks or non-text inline
// nodes by default. Represented as a `<pre>` element with a
// `<code>` element inside of it.
code_block: {
attrs: { ychange: { default: null } },
content: 'text*',
marks: '',
group: 'block',
code: true,
defining: true,
parseDOM: [{ tag: 'pre', preserveWhitespace: 'full' }],
toDOM (node) { return ['pre', calcYchangeDomAttrs(node.attrs), ['code', 0]] }
},
// :: NodeSpec The text node.
text: {
group: 'inline'
},
// :: NodeSpec An inline image (`<img>`) node. Supports `src`,
// `alt`, and `href` attributes. The latter two default to the empty
// string.
image: {
inline: true,
attrs: {
ychange: { default: null },
src: {},
alt: { default: null },
title: { default: null }
},
group: 'inline',
draggable: true,
parseDOM: [{ tag: 'img[src]',
getAttrs (dom) {
return {
src: dom.getAttribute('src'),
title: dom.getAttribute('title'),
alt: dom.getAttribute('alt')
}
} }],
toDOM (node) {
const domAttrs = {
src: node.attrs.src,
title: node.attrs.title,
alt: node.attrs.alt
}
return ['img', calcYchangeDomAttrs(node.attrs, domAttrs)]
}
},
// :: NodeSpec A hard line break, represented in the DOM as `<br>`.
hard_break: {
inline: true,
group: 'inline',
selectable: false,
parseDOM: [{ tag: 'br' }],
toDOM () { return brDOM }
}
}
const emDOM = ['em', 0]; const strongDOM = ['strong', 0]; const codeDOM = ['code', 0]
// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
export const marks = {
// :: MarkSpec A link. Has `href` and `title` attributes. `title`
// defaults to the empty string. Rendered and parsed as an `<a>`
// element.
link: {
attrs: {
href: {},
title: { default: null }
},
inclusive: false,
parseDOM: [{ tag: 'a[href]',
getAttrs (dom) {
return { href: dom.getAttribute('href'), title: dom.getAttribute('title') }
} }],
toDOM (node) { return ['a', node.attrs, 0] }
},
// :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
// Has parse rules that also match `<i>` and `font-style: italic`.
em: {
parseDOM: [{ tag: 'i' }, { tag: 'em' }, { style: 'font-style=italic' }],
toDOM () { return emDOM }
},
// :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
// also match `<b>` and `font-weight: bold`.
strong: {
parseDOM: [{ tag: 'strong' },
// This works around a Google Docs misbehavior where
// pasted content will be inexplicably wrapped in `<b>`
// tags with a font-weight normal.
{ tag: 'b', getAttrs: node => node.style.fontWeight !== 'normal' && null },
{ style: 'font-weight', getAttrs: value => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null }],
toDOM () { return strongDOM }
},
// :: MarkSpec Code font mark. Represented as a `<code>` element.
code: {
parseDOM: [{ tag: 'code' }],
toDOM () { return codeDOM }
},
ychange: {
attrs: {
user: { default: null },
state: { default: null }
},
inclusive: false,
parseDOM: [{ tag: 'ychange' }],
toDOM (node) {
return ['ychange', { ychange_user: node.attrs.user, ychange_state: node.attrs.state }, 0]
}
}
}
// :: Schema
// This schema rougly corresponds to the document schema used by
// [CommonMark](http://commonmark.org/), minus the list elements,
// which are defined in the [`prosemirror-schema-list`](#schema-list)
// module.
//
// To reuse elements from this schema, extend or read from its
// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
export const schema = new Schema({ nodes, marks })

View File

@@ -1,330 +0,0 @@
.ProseMirror {
position: relative;
}
.ProseMirror {
word-wrap: break-word;
white-space: pre-wrap;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror li {
position: relative;
}
.ProseMirror-hideselection *::selection { background: transparent; }
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
.ProseMirror-hideselection { caret-color: transparent; }
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px; top: -2px; bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
.ProseMirror-textblock-dropdown {
min-width: 3em;
}
.ProseMirror-menu {
margin: 0 -4px;
line-height: 1;
}
.ProseMirror-tooltip .ProseMirror-menu {
width: -webkit-fit-content;
width: fit-content;
white-space: pre;
}
.ProseMirror-menuitem {
margin-right: 3px;
display: inline-block;
}
.ProseMirror-menuseparator {
border-right: 1px solid #ddd;
margin-right: 3px;
}
.ProseMirror-menu-dropdown, .ProseMirror-menu-dropdown-menu {
font-size: 90%;
white-space: nowrap;
}
.ProseMirror-menu-dropdown {
vertical-align: 1px;
cursor: pointer;
position: relative;
padding-right: 15px;
}
.ProseMirror-menu-dropdown-wrap {
padding: 1px 0 1px 4px;
display: inline-block;
position: relative;
}
.ProseMirror-menu-dropdown:after {
content: "";
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 2px);
}
.ProseMirror-menu-dropdown-menu, .ProseMirror-menu-submenu {
position: absolute;
background: white;
color: #666;
border: 1px solid #aaa;
padding: 2px;
}
.ProseMirror-menu-dropdown-menu {
z-index: 15;
min-width: 6em;
}
.ProseMirror-menu-dropdown-item {
cursor: pointer;
padding: 2px 8px 2px 4px;
}
.ProseMirror-menu-dropdown-item:hover {
background: #f2f2f2;
}
.ProseMirror-menu-submenu-wrap {
position: relative;
margin-right: -4px;
}
.ProseMirror-menu-submenu-label:after {
content: "";
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 4px solid currentColor;
opacity: .6;
position: absolute;
right: 4px;
top: calc(50% - 4px);
}
.ProseMirror-menu-submenu {
display: none;
min-width: 4em;
left: 100%;
top: -3px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-active {
background: #eee;
border-radius: 4px;
}
.ProseMirror-menu-disabled {
opacity: .3;
}
.ProseMirror-menu-submenu-wrap:hover .ProseMirror-menu-submenu, .ProseMirror-menu-submenu-wrap-active .ProseMirror-menu-submenu {
display: block;
}
.ProseMirror-menubar {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
position: relative;
min-height: 1em;
color: #666;
padding: 1px 6px;
top: 0; left: 0; right: 0;
border-bottom: 1px solid silver;
background: white;
z-index: 10;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow: visible;
}
.ProseMirror-icon {
display: inline-block;
line-height: .8;
vertical-align: -2px; /* Compensate for padding */
padding: 2px 8px;
cursor: pointer;
}
.ProseMirror-menu-disabled.ProseMirror-icon {
cursor: default;
}
.ProseMirror-icon svg {
fill: currentColor;
height: 1em;
}
.ProseMirror-icon span {
vertical-align: text-top;
}
.ProseMirror-gapcursor {
display: none;
pointer-events: none;
position: absolute;
}
.ProseMirror-gapcursor:after {
content: "";
display: block;
position: absolute;
top: -2px;
width: 20px;
border-top: 1px solid black;
animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite;
}
@keyframes ProseMirror-cursor-blink {
to {
visibility: hidden;
}
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
/* Add space around the hr to make clicking it easier */
.ProseMirror-example-setup-style hr {
padding: 2px 10px;
border: none;
margin: 1em 0;
}
.ProseMirror-example-setup-style hr:after {
content: "";
display: block;
height: 1px;
background-color: silver;
line-height: 2px;
}
.ProseMirror ul, .ProseMirror ol {
padding-left: 30px;
}
.ProseMirror blockquote {
padding-left: 1em;
border-left: 3px solid #eee;
margin-left: 0; margin-right: 0;
}
.ProseMirror-example-setup-style img {
cursor: default;
}
.ProseMirror-prompt {
background: white;
padding: 5px 10px 5px 15px;
border: 1px solid silver;
position: fixed;
border-radius: 3px;
z-index: 11;
box-shadow: -.5px 2px 5px rgba(0, 0, 0, .2);
}
.ProseMirror-prompt h5 {
margin: 0;
font-weight: normal;
font-size: 100%;
color: #444;
}
.ProseMirror-prompt input[type="text"],
.ProseMirror-prompt textarea {
background: #eee;
border: none;
outline: none;
}
.ProseMirror-prompt input[type="text"] {
padding: 0 4px;
}
.ProseMirror-prompt-close {
position: absolute;
left: 2px; top: 1px;
color: #666;
border: none; background: transparent; padding: 0;
}
.ProseMirror-prompt-close:after {
content: "✕";
font-size: 12px;
}
.ProseMirror-invalid {
background: #ffc;
border: 1px solid #cc7;
border-radius: 4px;
padding: 5px 10px;
position: absolute;
min-width: 10em;
}
.ProseMirror-prompt-buttons {
margin-top: 5px;
display: none;
}
#editor, .editor {
background: white;
color: black;
background-clip: padding-box;
border-radius: 4px;
border: 2px solid rgba(0, 0, 0, 0.2);
padding: 5px 0;
margin-bottom: 23px;
}
.ProseMirror p:first-child,
.ProseMirror h1:first-child,
.ProseMirror h2:first-child,
.ProseMirror h3:first-child,
.ProseMirror h4:first-child,
.ProseMirror h5:first-child,
.ProseMirror h6:first-child {
margin-top: 10px;
}
.ProseMirror {
padding: 4px 8px 4px 14px;
line-height: 1.2;
outline: none;
}
.ProseMirror p { margin-bottom: 1em }

View File

@@ -1,117 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Prosemirror Example</title>
<link rel=stylesheet href="./prosemirror.css">
<style>
placeholder {
display: inline;
border: 1px solid #ccc;
color: #ccc;
}
placeholder:after {
content: "☁";
font-size: 200%;
line-height: 0.1;
font-weight: bold;
}
.ProseMirror img { max-width: 100px }
/* this is a rough fix for the first cursor position when the first paragraph is empty */
.ProseMirror > .ProseMirror-yjs-cursor:first-child {
margin-top: 16px;
}
.ProseMirror p:first-child, .ProseMirror h1:first-child, .ProseMirror h2:first-child, .ProseMirror h3:first-child, .ProseMirror h4:first-child, .ProseMirror h5:first-child, .ProseMirror h6:first-child {
margin-top: 16px
}
.ProseMirror-yjs-cursor {
position: absolute;
border-left: black;
border-left-style: solid;
border-left-width: 2px;
border-color: orange;
height: 1em;
word-break: normal;
}
.ProseMirror-yjs-cursor > div {
position: relative;
top: -1.05em;
font-size: 13px;
background-color: rgb(250, 129, 0);
font-family: serif;
font-style: normal;
font-weight: normal;
line-height: normal;
user-select: none;
color: white;
padding-left: 2px;
padding-right: 2px;
}
[ychange_state] {
position: relative;
}
[ychange_state]:hover::before {
content: attr(ychange_user);
background-color: #fa8100;
position: absolute;
top: -14px;
right: 0;
font-size: 12px;
padding: 0 2px;
border-radius: 3px 3px 0 0;
color: #fdfdfe;
user-select: none;
word-break: normal;
}
*[ychange_state='added'] {
background-color: #fa810069;
}
ychange[ychange_state='removed'] {
color: rgb(250, 129, 0);
text-decoration: line-through;
}
*:not(ychange)[ychange_state='removed'] {
background-color: #ff9494c9;
text-decoration: line-through;
}
img[ychange_state='removed'] {
padding: 2px;
}
</style>
</head>
<body>
<p>This example shows how to bind a YXmlFragment type to a <a href="http://prosemirror.net">Prosemirror</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<div id="editor" style="margin-bottom: 23px"></div>
<div style="display: none" id="content"></div>
</div>
<!-- The actual source file for the following code is found in ./prosemirror.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/prosemirror.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { prosemirrorPlugin, cursorPlugin } from 'yjs/bindings/prosemirror'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { DOMParser } from 'prosemirror-model'
import { schema } from 'prosemirror-schema-basic'
import { exampleSetup } from 'prosemirror-example-setup'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror')
const type = ydocument.define('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({schema}).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
window.example = { provider, ydocument, type, prosemirrorView }
</script>
</body>
</html>

View File

@@ -1,25 +0,0 @@
import * as Y from 'yjs'
import { WebsocketProvider } from 'y-websocket'
import { prosemirrorPlugin, cursorPlugin } from 'y-prosemirror'
import * as conf from './exampleConfig.js'
import { EditorState } from 'prosemirror-state'
import { EditorView } from 'prosemirror-view'
import { DOMParser } from 'prosemirror-model'
import { schema } from './prosemirror-schema.js'
import { exampleSetup } from 'prosemirror-example-setup'
// import { noteHistoryPlugin } from './prosemirror-history.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('prosemirror', { gc: false })
const type = ydocument.define('prosemirror', Y.XmlFragment)
const prosemirrorView = new EditorView(document.querySelector('#editor'), {
state: EditorState.create({
doc: DOMParser.fromSchema(schema).parse(document.querySelector('#content')),
plugins: exampleSetup({ schema }).concat([prosemirrorPlugin(type), cursorPlugin])
})
})
window.example = { provider, ydocument, type, prosemirrorView }

View File

@@ -1,51 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Quill Example</title>
<link rel="stylesheet" href="https://cdn.quilljs.com/1.3.6/quill.snow.css">
</head>
<body>
<p>This example shows how to bind a YText type to <a href="https://quilljs.com">Quill</a> editor.</p>
<p>The content of this editor is shared with every client who visits this domain.</p>
<div class="code-html">
<div id="quill-container">
<div id="quill">
</div>
</div>
</div>
<!-- The actual source file for the following code is found in ./quill.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/quill.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { QuillBinding } from 'yjs/bindings/quill.js'
import * as conf from './exampleConfig.js'
import Quill from 'quill'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)
const quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
window.quillBinding = new QuillBinding(ytext, quill)
</script>
</body>
</html>

View File

@@ -1,30 +0,0 @@
import * as Y from '../src/index.js'
import { WebsocketProvider } from 'y-websocket'
import { QuillBinding } from 'y-quill'
import * as conf from './exampleConfig.js'
import Quill from 'quill'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('quill')
const ytext = ydocument.define('quill', Y.Text)
const quill = new Quill('#quill-container', {
modules: {
toolbar: [
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }]
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
})
window.quillBinding = new QuillBinding(ytext, quill)

View File

@@ -1,29 +0,0 @@
footer img {
display: none;
}
nav .title h1 a {
display: none;
}
footer {
background-color: #b93c1d;
}
#resizer {
background-color: #b93c1d;
}
.main section article.readme h1:first-child img {
display: none;
}
.main section article.readme h1:first-child {
margin-bottom: 16px;
margin-top: 30px;
}
.main section article.readme h1:first-child::before {
content: "Yjs";
font-size: 2em;
}

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Yjs Textarea Example</title>
</head>
<body>
<p>This example shows how to bind a YText type to a DOM Textarea.</p>
<p>The content of this textarea is shared with every client who visits this domain.</p>
<div class="code-html">
<textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
</div>
<!-- The actual source file for the following code is found in ./textarea.js. Run `npm run watch` to compile the files -->
<script class="code-js" src="./build/textarea.js" type="module">
import * as Y from 'yjs'
import { WebsocketProvider } from 'yjs/provider/websocket.js'
import { TextareaBinding } from 'yjs/bindings/textarea.js'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.define('textarea', Y.Text)
const textarea = document.querySelector('textarea')
const binding = new TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}
</script>
</body>
</html>

View File

@@ -1,14 +0,0 @@
import { WebsocketProvider } from 'y-websocket'
import { TextareaBinding } from 'y-textarea'
import * as conf from './exampleConfig.js'
const provider = new WebsocketProvider(conf.serverAddress)
const ydocument = provider.get('textarea')
const type = ydocument.getText('textarea')
const textarea = document.querySelector('textarea')
const binding = new TextareaBinding(type, textarea)
window.textareaExample = {
provider, ydocument, type, textarea, binding
}

5589
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,39 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.0.0-78", "version": "13.0.0-92",
"description": "A ", "description": "Shared Editing Library",
"main": "./dist/yjs.js", "main": "./dist/yjs.js",
"module": "./dist/yjs.mjs'", "module": "./dist/yjs.mjs",
"sideEffects": false, "sideEffects": false,
"scripts": { "scripts": {
"test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production", "test": "npm run dist && PRODUCTION=1 node ./dist/tests.js --repitition-time 50 --production",
"test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000", "test-exhaustive": "npm run lint && npm run dist && node ./dist/tests.js --repitition-time 10000",
"dist": "rm -rf dist examples/build && rollup -c", "dist": "rm -rf dist && rollup -c",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "standard && tsc", "lint": "markdownlint README.v13.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.v13.md --package ./package.json || true",
"serve-docs": "npm run docs && serve ./docs/", "serve-docs": "npm run docs && serve ./docs/",
"postversion": "npm run lint && PRODUCTION=1 npm run dist && node ./dist/tests.js --repitition-time 1000", "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.js --repitition-time 1000",
"postversion": "git push && git push --tags",
"debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'", "debug": "concurrently 'live-server --port=3443 --entry-file=test.html' 'npm run watch'",
"trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js", "trace-deopt": "clear && rollup -c && node --trace-deopt dist/test.js",
"trace-opt": "clear && rollup -c && node --trace-opt dist/test.js" "trace-opt": "clear && rollup -c && node --trace-opt dist/test.js"
}, },
"files": [ "files": [
"dist/*", "dist/*",
"examples/*", "src/*",
"docs/*", "tests/*",
"README.md", "docs/*"
"LICENSE"
], ],
"dictionaries": { "dictionaries": {
"doc": "docs", "doc": "docs",
"example": "examples",
"test": "tests" "test": "tests"
}, },
"standard": { "standard": {
"ignore": [ "ignore": [
"/dist", "/dist",
"/node_modules", "/node_modules",
"/docs", "/docs"
"/examples/build"
] ]
}, },
"repository": { "repository": {
@@ -53,28 +51,18 @@
}, },
"homepage": "http://y-js.org", "homepage": "http://y-js.org",
"dependencies": { "dependencies": {
"lib0": "0.0.0" "lib0": "0.0.5"
}, },
"devDependencies": { "devDependencies": {
"codemirror": "^5.42.0",
"concurrently": "^3.6.1", "concurrently": "^3.6.1",
"esdoc": "^1.1.0", "jsdoc": "^3.6.2",
"esdoc-standard-plugin": "^1.0.0",
"jsdoc": "^3.5.5",
"live-server": "^1.2.1", "live-server": "^1.2.1",
"prosemirror-example-setup": "^1.0.1", "rollup": "^1.11.3",
"prosemirror-schema-basic": "^1.0.0",
"prosemirror-state": "^1.2.2",
"prosemirror-view": "^1.6.5",
"quill": "^1.3.6",
"quill-cursors": "^1.0.3",
"rollup": "^1.1.2",
"rollup-cli": "^1.0.9", "rollup-cli": "^1.0.9",
"rollup-plugin-commonjs": "^9.2.0", "rollup-plugin-node-resolve": "^4.2.4",
"rollup-plugin-node-resolve": "^4.0.0",
"rollup-plugin-terser": "^4.0.4",
"standard": "^11.0.1", "standard": "^11.0.1",
"tui-jsdoc-template": "^1.2.2", "tui-jsdoc-template": "^1.2.2",
"typescript": "^3.3.3333" "typescript": "^3.4.5",
"y-protocols": "0.0.6"
} }
} }

View File

@@ -1,6 +1,6 @@
import nodeResolve from 'rollup-plugin-node-resolve' import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'
import { terser } from 'rollup-plugin-terser' const localImports = process.env.LOCALIMPORTS
const customModules = new Set([ const customModules = new Set([
'y-websocket', 'y-websocket',
@@ -23,31 +23,18 @@ const debugResolve = {
if (importee === 'yjs') { if (importee === 'yjs') {
return `${process.cwd()}/src/index.js` return `${process.cwd()}/src/index.js`
} }
if (customModules.has(importee.split('/')[0])) { if (localImports) {
return `${process.cwd()}/../${importee}/src/${importee}.js` if (customModules.has(importee.split('/')[0])) {
} return `${process.cwd()}/../${importee}/src/${importee}.js`
if (customLibModules.has(importee.split('/')[0])) { }
return `${process.cwd()}/../${importee}` if (customLibModules.has(importee.split('/')[0])) {
return `${process.cwd()}/../${importee}`
}
} }
return null return null
} }
} }
const minificationPlugins = process.env.PRODUCTION ? [terser({
module: true,
compress: {
hoist_vars: true,
module: true,
passes: 5,
pure_getters: true,
unsafe_comps: true,
unsafe_undefined: true
},
mangle: {
toplevel: true
}
})] : []
export default [{ export default [{
input: './src/index.js', input: './src/index.js',
output: [{ output: [{
@@ -80,26 +67,7 @@ export default [{
debugResolve, debugResolve,
nodeResolve({ nodeResolve({
sourcemap: true, sourcemap: true,
module: true, mainFields: ['module', 'browser', 'main']
browser: true })
}),
commonjs()
]
}, {
input: ['./examples/codemirror.js', './examples/textarea.js'], // './examples/quill.js', './examples/dom.js', './examples/prosemirror.js'
output: {
dir: 'examples/build',
format: 'esm',
sourcemap: true
},
plugins: [
debugResolve,
nodeResolve({
sourcemap: true,
module: true,
browser: true
}),
commonjs(),
...minificationPlugins
] ]
}] }]

View File

@@ -1,6 +1,6 @@
export { export {
Y, Doc,
Transaction, Transaction,
YArray as Array, YArray as Array,
YMap as Map, YMap as Map,
@@ -9,18 +9,38 @@ export {
YXmlHook as XmlHook, YXmlHook as XmlHook,
YXmlElement as XmlElement, YXmlElement as XmlElement,
YXmlFragment as XmlFragment, YXmlFragment as XmlFragment,
createCursorFromTypeOffset, YXmlEvent,
createCursorFromJSON, YMapEvent,
createAbsolutePositionFromCursor, YArrayEvent,
writeCursor, YEvent,
readCursor, Item,
AbstractStruct,
GC,
ContentBinary,
ContentDeleted,
ContentEmbed,
ContentFormat,
ContentJSON,
ContentString,
ContentType,
AbstractType,
RelativePosition,
createRelativePositionFromTypeIndex,
createRelativePositionFromJSON,
createAbsolutePositionFromRelativePosition,
compareRelativePositions,
writeRelativePosition,
readRelativePosition,
ID, ID,
createID, createID,
compareIDs, compareIDs,
getState, getState,
getStates, Snapshot,
readStatesAsMap, findRootTypeKey,
writeStates, typeListToArraySnapshot,
writeModel, typeMapGetSnapshot,
readModel iterateDeletedStructs,
applyUpdate,
encodeStateAsUpdate,
encodeStateVector
} from './internals.js' } from './internals.js'

View File

@@ -2,32 +2,33 @@ export * from './utils/DeleteSet.js'
export * from './utils/EventHandler.js' export * from './utils/EventHandler.js'
export * from './utils/ID.js' export * from './utils/ID.js'
export * from './utils/isParentOf.js' export * from './utils/isParentOf.js'
export * from './utils/cursor.js' export * from './utils/RelativePosition.js'
export * from './utils/Snapshot.js' export * from './utils/Snapshot.js'
export * from './utils/StructStore.js' export * from './utils/StructStore.js'
export * from './utils/Transaction.js' export * from './utils/Transaction.js'
// export * from './utils/UndoManager.js' export * from './utils/UndoManager.js'
export * from './utils/Y.js' export * from './utils/Doc.js'
export * from './utils/YEvent.js' export * from './utils/YEvent.js'
export * from './types/AbstractType.js' export * from './types/AbstractType.js'
export * from './types/YArray.js' export * from './types/YArray.js'
export * from './types/YMap.js' export * from './types/YMap.js'
export * from './types/YText.js' export * from './types/YText.js'
export * from './types/YXmlFragment.js'
export * from './types/YXmlElement.js' export * from './types/YXmlElement.js'
export * from './types/YXmlEvent.js' export * from './types/YXmlEvent.js'
export * from './types/YXmlHook.js' export * from './types/YXmlHook.js'
export * from './types/YXmlText.js' export * from './types/YXmlText.js'
export * from './structs/AbstractStruct.js' export * from './structs/AbstractStruct.js'
export * from './structs/AbstractItem.js'
export * from './structs/GC.js' export * from './structs/GC.js'
export * from './structs/ItemBinary.js' export * from './structs/ContentBinary.js'
export * from './structs/ItemDeleted.js' export * from './structs/ContentDeleted.js'
export * from './structs/ItemEmbed.js' export * from './structs/ContentEmbed.js'
export * from './structs/ItemFormat.js' export * from './structs/ContentFormat.js'
export * from './structs/ItemJSON.js' export * from './structs/ContentJSON.js'
export * from './structs/ItemString.js' export * from './structs/ContentString.js'
export * from './structs/ItemType.js' export * from './structs/ContentType.js'
export * from './structs/Item.js'
export * from './utils/encoding.js' export * from './utils/encoding.js'

View File

@@ -1,6 +1,6 @@
import { import {
Y, StructStore, ID, Transaction // eslint-disable-line StructStore, ID, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' // eslint-disable-line import * as encoding from 'lib0/encoding.js' // eslint-disable-line
@@ -12,14 +12,16 @@ import * as error from 'lib0/error.js'
export class AbstractStruct { export class AbstractStruct {
/** /**
* @param {ID} id * @param {ID} id
* @param {number} length
*/ */
constructor (id) { constructor (id, length) {
/** /**
* The uniqe identifier of this struct. * The uniqe identifier of this struct.
* @type {ID} * @type {ID}
* @readonly * @readonly
*/ */
this.id = id this.id = id
this.length = length
this.deleted = false this.deleted = false
} }
/** /**
@@ -32,12 +34,6 @@ export class AbstractStruct {
mergeWith (right) { mergeWith (right) {
return false return false
} }
/**
* @type {number}
*/
get length () {
throw error.methodUnimplemented()
}
/** /**
* @param {encoding.Encoder} encoder The encoder to write data to. * @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset * @param {number} offset
@@ -89,10 +85,4 @@ export class AbstractStructRef {
toStruct (transaction, store, offset) { toStruct (transaction, store, offset) {
throw error.methodUnimplemented() throw error.methodUnimplemented()
} }
/**
* @type {number}
*/
get length () {
return 1
}
} }

View File

@@ -0,0 +1,92 @@
import {
StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as buffer from 'lib0/buffer.js'
import * as error from 'lib0/error.js'
/**
* @private
*/
export class ContentBinary {
/**
* @param {Uint8Array} content
*/
constructor (content) {
this.content = content
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.content]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentBinary}
*/
copy () {
return new ContentBinary(this.content)
}
/**
* @param {number} offset
* @return {ContentBinary}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentBinary} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoding.writeVarUint8Array(encoder, this.content)
}
/**
* @return {number}
*/
getRef () {
return 3
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentBinary}
*/
export const readContentBinary = decoder => new ContentBinary(buffer.copyUint8Array(decoding.readVarUint8Array(decoder)))

View File

@@ -0,0 +1,98 @@
import {
addToDeleteSet,
StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentDeleted {
/**
* @param {number} len
*/
constructor (len) {
this.len = len
}
/**
* @return {number}
*/
getLength () {
return this.len
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentDeleted}
*/
copy () {
return new ContentDeleted(this.len)
}
/**
* @param {number} offset
* @return {ContentDeleted}
*/
splice (offset) {
const right = new ContentDeleted(this.len - offset)
this.len = offset
return right
}
/**
* @param {ContentDeleted} right
* @return {boolean}
*/
mergeWith (right) {
this.len += right.len
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
addToDeleteSet(transaction.deleteSet, item.id, this.len)
item.deleted = true
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoding.writeVarUint(encoder, this.len - offset)
}
/**
* @return {number}
*/
getRef () {
return 1
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentDeleted}
*/
export const readContentDeleted = decoder => new ContentDeleted(decoding.readVarUint(decoder))

View File

@@ -0,0 +1,92 @@
import {
StructStore, Item, Transaction // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js'
/**
* @private
*/
export class ContentEmbed {
/**
* @param {Object} embed
*/
constructor (embed) {
this.embed = embed
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.embed]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentEmbed}
*/
copy () {
return new ContentEmbed(this.embed)
}
/**
* @param {number} offset
* @return {ContentEmbed}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentEmbed} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoding.writeVarString(encoder, JSON.stringify(this.embed))
}
/**
* @return {number}
*/
getRef () {
return 5
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentEmbed}
*/
export const readContentEmbed = decoder => new ContentEmbed(JSON.parse(decoding.readVarString(decoder)))

View File

@@ -0,0 +1,95 @@
import {
Item, StructStore, Transaction // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js'
/**
* @private
*/
export class ContentFormat {
/**
* @param {string} key
* @param {Object} value
*/
constructor (key, value) {
this.key = key
this.value = value
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentFormat}
*/
copy () {
return new ContentFormat(this.key, this.value)
}
/**
* @param {number} offset
* @return {ContentFormat}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentFormat} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value))
}
/**
* @return {number}
*/
getRef () {
return 6
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentFormat}
*/
export const readContentFormat = decoder => new ContentFormat(decoding.readVarString(decoder), JSON.parse(decoding.readVarString(decoder)))

113
src/structs/ContentJSON.js Normal file
View File

@@ -0,0 +1,113 @@
import {
Transaction, Item, StructStore // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentJSON {
/**
* @param {Array<any>} arr
*/
constructor (arr) {
/**
* @type {Array<any>}
*/
this.arr = arr
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentJSON}
*/
copy () {
return new ContentJSON(this.arr)
}
/**
* @param {number} offset
* @return {ContentJSON}
*/
splice (offset) {
const right = new ContentJSON(this.arr.slice(offset))
this.arr = this.arr.slice(0, offset)
return right
}
/**
* @param {ContentJSON} right
* @return {boolean}
*/
mergeWith (right) {
this.arr = this.arr.concat(right.arr)
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
const len = this.arr.length
encoding.writeVarUint(encoder, len - offset)
for (let i = offset; i < len; i++) {
const c = this.arr[i]
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
}
}
/**
* @return {number}
*/
getRef () {
return 2
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentJSON}
*/
export const readContentJSON = decoder => {
const len = decoding.readVarUint(decoder)
const cs = []
for (let i = 0; i < len; i++) {
const c = decoding.readVarString(decoder)
if (c === 'undefined') {
cs.push(undefined)
} else {
cs.push(JSON.parse(c))
}
}
return new ContentJSON(cs)
}

View File

@@ -0,0 +1,96 @@
import {
Transaction, Item, StructStore // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export class ContentString {
/**
* @param {string} str
*/
constructor (str) {
/**
* @type {string}
*/
this.str = str
}
/**
* @return {number}
*/
getLength () {
return this.str.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.str.split('')
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentString}
*/
copy () {
return new ContentString(this.str)
}
/**
* @param {number} offset
* @return {ContentString}
*/
splice (offset) {
const right = new ContentString(this.str.slice(offset))
this.str = this.str.slice(0, offset)
return right
}
/**
* @param {ContentString} right
* @return {boolean}
*/
mergeWith (right) {
this.str += right.str
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoding.writeVarString(encoder, offset === 0 ? this.str : this.str.slice(offset))
}
/**
* @return {number}
*/
getRef () {
return 4
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentString}
*/
export const readContentString = decoder => new ContentString(decoding.readVarString(decoder))

163
src/structs/ContentType.js Normal file
View File

@@ -0,0 +1,163 @@
import {
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText,
StructStore, Transaction, Item, YEvent, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js'
/**
* @type {Array<function(decoding.Decoder):AbstractType<any>>}
* @private
*/
export const typeRefs = [
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText
]
export const YArrayRefID = 0
export const YMapRefID = 1
export const YTextRefID = 2
export const YXmlElementRefID = 3
export const YXmlFragmentRefID = 4
export const YXmlHookRefID = 5
export const YXmlTextRefID = 6
/**
* @private
*/
export class ContentType {
/**
* @param {AbstractType<YEvent>} type
*/
constructor (type) {
/**
* @type {AbstractType<any>}
*/
this.type = type
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.type]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentType}
*/
copy () {
return new ContentType(this.type._copy())
}
/**
* @param {number} offset
* @return {ContentType}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {ContentType} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
this.type._integrate(transaction.doc, item)
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
let item = this.type._start
while (item !== null) {
if (!item.deleted) {
item.delete(transaction)
} else {
// Whis will be gc'd later and we want to merge it if possible
// We try to merge all deleted items after each transaction,
// but we have no knowledge about that this needs to be merged
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
transaction._mergeStructs.add(item.id)
}
item = item.right
}
this.type._map.forEach(item => {
if (!item.deleted) {
item.delete(transaction)
} else {
// same as above
transaction._mergeStructs.add(item.id)
}
})
transaction.changed.delete(this.type)
}
/**
* @param {StructStore} store
*/
gc (store) {
let item = this.type._start
while (item !== null) {
item.gc(store, true)
item = item.right
}
this.type._start = null
this.type._map.forEach(/** @param {Item | null} item */ (item) => {
while (item !== null) {
item.gc(store, true)
item = item.left
}
})
this.type._map = new Map()
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
this.type._write(encoder)
}
/**
* @return {number}
*/
getRef () {
return 7
}
}
/**
* @private
*
* @param {decoding.Decoder} decoder
* @return {ContentType}
*/
export const readContentType = decoder => new ContentType(typeRefs[decoding.readVarUint(decoder)](decoder))

View File

@@ -4,7 +4,7 @@ import {
AbstractStruct, AbstractStruct,
createID, createID,
addStruct, addStruct,
Y, StructStore, Transaction, ID // eslint-disable-line StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
@@ -21,26 +21,18 @@ export class GC extends AbstractStruct {
* @param {number} length * @param {number} length
*/ */
constructor (id, length) { constructor (id, length) {
super(id) super(id, length)
/**
* @type {number}
*/
this._len = length
this.deleted = true this.deleted = true
} }
get length () {
return this._len
}
delete () {} delete () {}
/** /**
* @param {AbstractStruct} right * @param {GC} right
* @return {boolean} * @return {boolean}
*/ */
mergeWith (right) { mergeWith (right) {
this._len += right.length this.length += right.length
return true return true
} }
@@ -48,7 +40,7 @@ export class GC extends AbstractStruct {
* @param {Transaction} transaction * @param {Transaction} transaction
*/ */
integrate (transaction) { integrate (transaction) {
addStruct(transaction.y.store, this) addStruct(transaction.doc.store, this)
} }
/** /**
@@ -57,7 +49,7 @@ export class GC extends AbstractStruct {
*/ */
write (encoder, offset) { write (encoder, offset) {
encoding.writeUint8(encoder, structGCRefNumber) encoding.writeUint8(encoder, structGCRefNumber)
encoding.writeVarUint(encoder, this._len - offset) encoding.writeVarUint(encoder, this.length - offset)
} }
} }
@@ -75,15 +67,7 @@ export class GCRef extends AbstractStructRef {
/** /**
* @type {number} * @type {number}
*/ */
this._len = decoding.readVarUint(decoder) this.length = decoding.readVarUint(decoder)
}
get length () {
return this._len
}
missing () {
return [
createID(this.id.client, this.id.clock - 1)
]
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
@@ -95,11 +79,11 @@ export class GCRef extends AbstractStructRef {
if (offset > 0) { if (offset > 0) {
// @ts-ignore // @ts-ignore
this.id = createID(this.id.client, this.id.clock + offset) this.id = createID(this.id.client, this.id.clock + offset)
this._len = this._len - offset this.length -= offset
} }
return new GC( return new GC(
this.id, this.id,
this._len this.length
) )
} }
} }

View File

@@ -10,14 +10,20 @@ import {
replaceStruct, replaceStruct,
addStruct, addStruct,
addToDeleteSet, addToDeleteSet,
ItemDeleted,
findRootTypeKey, findRootTypeKey,
compareIDs, compareIDs,
getItem, getItem,
getItemType,
getItemCleanEnd, getItemCleanEnd,
getItemCleanStart, getItemCleanStart,
YEvent, StructStore, ID, AbstractType, Y, Transaction // eslint-disable-line readContentDeleted,
readContentBinary,
readContentJSON,
readContentString,
readContentEmbed,
readContentFormat,
readContentType,
addChangedTypeToTransaction,
ContentType, ContentDeleted, StructStore, ID, AbstractType, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error.js'
@@ -27,12 +33,27 @@ import * as maplib from 'lib0/map.js'
import * as set from 'lib0/set.js' import * as set from 'lib0/set.js'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary.js'
/**
* Make sure that neither item nor any of its parents is ever deleted.
*
* This property does not persist when storing it into a database or when
* sending it to other peers
*
* @param {Item|null} item
*/
export const keepItem = item => {
while (item !== null && !item.keep) {
item.keep = true
item = item.parent._item
}
}
/** /**
* Split leftItem into two items * Split leftItem into two items
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractItem} leftItem * @param {Item} leftItem
* @param {number} diff * @param {number} diff
* @return {AbstractItem} * @return {Item}
* *
* @function * @function
* @private * @private
@@ -40,18 +61,22 @@ import * as binary from 'lib0/binary.js'
export const splitItem = (transaction, leftItem, diff) => { export const splitItem = (transaction, leftItem, diff) => {
const id = leftItem.id const id = leftItem.id
// create rightItem // create rightItem
const rightItem = leftItem.copy( const rightItem = new Item(
createID(id.client, id.clock + diff), createID(id.client, id.clock + diff),
leftItem, leftItem,
createID(id.client, id.clock + diff - 1), createID(id.client, id.clock + diff - 1),
leftItem.right, leftItem.right,
leftItem.rightOrigin, leftItem.rightOrigin,
leftItem.parent, leftItem.parent,
leftItem.parentSub leftItem.parentSub,
leftItem.content.splice(diff)
) )
if (leftItem.deleted) { if (leftItem.deleted) {
rightItem.deleted = true rightItem.deleted = true
} }
if (leftItem.keep) {
rightItem.keep = true
}
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
leftItem.right = rightItem leftItem.right = rightItem
// update right // update right
@@ -60,24 +85,130 @@ export const splitItem = (transaction, leftItem, diff) => {
} }
// right is more specific. // right is more specific.
transaction._mergeStructs.add(rightItem.id) transaction._mergeStructs.add(rightItem.id)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
rightItem.parent._map.set(rightItem.parentSub, rightItem)
}
leftItem.length = diff
return rightItem return rightItem
} }
/**
* Redoes the effect of this operation.
*
* @param {Transaction} transaction The Yjs instance.
* @param {Item} item
* @param {Set<Item>} redoitems
*
* @return {Item|null}
*
* @private
*/
export const redoItem = (transaction, item, redoitems) => {
if (item.redone !== null) {
return item.redone
}
let parentItem = item.parent._item
/**
* @type {Item|null}
*/
let left
/**
* @type {Item|null}
*/
let right
if (item.parentSub === null) {
// Is an array item. Insert at the old position
left = item.left
right = item
} else {
// Is a map item. Insert as current value
left = item
while (left.right !== null) {
left = left.right
if (left.id.client !== transaction.doc.clientID) {
// It is not possible to redo this item because it conflicts with a
// change from another client
return null
}
}
if (left.right !== null) {
left = /** @type {Item} */ (item.parent._map.get(item.parentSub))
}
right = null
}
// make sure that parent is redone
if (parentItem !== null && parentItem.deleted === true && parentItem.redone === null) {
// try to undo parent if it will be undone anyway
if (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems) === null) {
return null
}
}
if (parentItem !== null && parentItem.redone !== null) {
while (parentItem.redone !== null) {
parentItem = parentItem.redone
}
// find next cloned_redo items
while (left !== null) {
/**
* @type {Item|null}
*/
let leftTrace = left
// trace redone until parent matches
while (leftTrace !== null && leftTrace.parent._item !== parentItem) {
leftTrace = leftTrace.redone
}
if (leftTrace !== null && leftTrace.parent._item === parentItem) {
left = leftTrace
break
}
left = left.left
}
while (right !== null) {
/**
* @type {Item|null}
*/
let rightTrace = right
// trace redone until parent matches
while (rightTrace !== null && rightTrace.parent._item !== parentItem) {
rightTrace = rightTrace.redone
}
if (rightTrace !== null && rightTrace.parent._item === parentItem) {
right = rightTrace
break
}
right = right.right
}
}
const redoneItem = new Item(
nextID(transaction),
left, left === null ? null : left.lastId,
right, right === null ? null : right.id,
parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type,
item.parentSub,
item.content.copy()
)
item.redone = redoneItem
redoneItem.integrate(transaction)
return redoneItem
}
/** /**
* Abstract class that represents any content. * Abstract class that represents any content.
*/ */
export class AbstractItem extends AbstractStruct { export class Item extends AbstractStruct {
/** /**
* @param {ID} id * @param {ID} id
* @param {AbstractItem | null} left * @param {Item | null} left
* @param {ID | null} origin * @param {ID | null} origin
* @param {AbstractItem | null} right * @param {Item | null} right
* @param {ID | null} rightOrigin * @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string | null} parentSub * @param {string | null} parentSub
* @param {AbstractContent} content
*/ */
constructor (id, left, origin, right, rightOrigin, parent, parentSub) { constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
super(id) super(id, content.getLength())
/** /**
* The item that was originally to the left of this item. * The item that was originally to the left of this item.
* @type {ID | null} * @type {ID | null}
@@ -86,12 +217,12 @@ export class AbstractItem extends AbstractStruct {
this.origin = origin this.origin = origin
/** /**
* The item that is currently to the left of this item. * The item that is currently to the left of this item.
* @type {AbstractItem | null} * @type {Item | null}
*/ */
this.left = left this.left = left
/** /**
* The item that is currently to the right of this item. * The item that is currently to the right of this item.
* @type {AbstractItem | null} * @type {Item | null}
*/ */
this.right = right this.right = right
/** /**
@@ -123,9 +254,19 @@ export class AbstractItem extends AbstractStruct {
/** /**
* If this type's effect is reundone this type refers to the type that undid * If this type's effect is reundone this type refers to the type that undid
* this operation. * this operation.
* @type {AbstractItem | null} * @type {Item | null}
*/ */
this.redone = null this.redone = null
/**
* @type {AbstractContent}
*/
this.content = content
this.length = content.getLength()
this.countable = content.isCountable()
/**
* If true, do not garbage collect this Item.
*/
this.keep = false
} }
/** /**
@@ -133,13 +274,13 @@ export class AbstractItem extends AbstractStruct {
* @private * @private
*/ */
integrate (transaction) { integrate (transaction) {
const store = transaction.y.store const store = transaction.doc.store
const id = this.id const id = this.id
const parent = this.parent const parent = this.parent
const parentSub = this.parentSub const parentSub = this.parentSub
const length = this.length const length = this.length
/** /**
* @type {AbstractItem|null} * @type {Item|null}
*/ */
let o let o
// set o to the first conflicting item // set o to the first conflicting item
@@ -155,11 +296,11 @@ export class AbstractItem extends AbstractStruct {
} }
// TODO: use something like DeleteSet here (a tree implementation would be best) // TODO: use something like DeleteSet here (a tree implementation would be best)
/** /**
* @type {Set<AbstractItem>} * @type {Set<Item>}
*/ */
const conflictingItems = new Set() const conflictingItems = new Set()
/** /**
* @type {Set<AbstractItem>} * @type {Set<Item>}
*/ */
const itemsBeforeOrigin = new Set() const itemsBeforeOrigin = new Set()
// Let c in conflictingItems, b in itemsBeforeOrigin // Let c in conflictingItems, b in itemsBeforeOrigin
@@ -218,8 +359,9 @@ export class AbstractItem extends AbstractStruct {
parent._length += length parent._length += length
} }
addStruct(store, this) addStruct(store, this)
maplib.setIfUndefined(transaction.changed, parent, set.create).add(parentSub) this.content.integrate(transaction, this)
// @ts-ignore // add parent to transaction.changed
addChangedTypeToTransaction(transaction, parent, parentSub)
if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) { if ((parent._item !== null && parent._item.deleted) || (this.right !== null && parentSub !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent // delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction) this.delete(transaction)
@@ -250,148 +392,44 @@ export class AbstractItem extends AbstractStruct {
return n return n
} }
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @return {AbstractItem}
*
* @private
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
throw new Error('unimplemented')
}
/**
* Redoes the effect of this operation.
*
* @param {Transaction} transaction The Yjs instance.
* @param {Set<AbstractItem>} redoitems
*
* @private
*/
redo (transaction, redoitems) {
if (this.redone !== null) {
return this.redone
}
/**
* @type {any}
*/
let parent = this.parent
if (parent === null) {
return
}
let left, right
if (this.parentSub === null) {
// Is an array item. Insert at the old position
left = this.left
right = this
} else {
// Is a map item. Insert as current value
left = parent.type._map.get(this.parentSub)
right = null
}
// make sure that parent is redone
if (parent._deleted === true && parent.redone === null) {
// try to undo parent if it will be undone anyway
if (!redoitems.has(parent) || !parent.redo(transaction, redoitems)) {
return false
}
}
if (parent.redone !== null) {
while (parent.redone !== null) {
parent = parent.redone
}
// find next cloned_redo items
while (left !== null) {
if (left.redone !== null && left.redone.parent === parent) {
left = left.redone
break
}
left = left.left
}
while (right !== null) {
if (right.redone !== null && right.redone.parent === parent) {
right = right.redone
}
right = right.right
}
}
this.redone = this.copy(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, this.parentSub)
this.redone.integrate(transaction)
return true
}
/** /**
* Computes the last content address of this Item. * Computes the last content address of this Item.
*/ */
get lastId () { get lastId () {
return createID(this.id.client, this.id.clock + this.length - 1) return createID(this.id.client, this.id.clock + this.length - 1)
} }
/** /**
* Computes the length of this Item. * Try to merge two items
*/
get length () {
return 1
}
/**
* Should return false if this Item is some kind of meta information
* (e.g. format information).
* *
* * Whether this Item should be addressable via `yarray.get(i)` * @param {Item} right
* * Whether this Item should be counted when computing yarray.length
*/
get countable () {
return true
}
/**
* Do not call directly. Always split via StructStore!
*
* Splits this Item so that another Item can be inserted in-between.
* This must be overwritten if _length > 1
* Returns right part after split
*
* (see {@link ItemJSON}/{@link ItemString} for implementation)
*
* Does not integrate the struct, nor store it in struct store.
*
* This method should only be cally by StructStore.
*
* @param {Transaction} transaction
* @param {number} diff
* @return {AbstractItem}
*
* @private
*/
splitAt (transaction, diff) {
throw new Error('unimplemented')
}
/**
* @param {AbstractItem} right
* @return {boolean} * @return {boolean}
*
* @private
*/ */
mergeWith (right) { mergeWith (right) {
if (compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin)) { if (
compareIDs(right.origin, this.lastId) &&
this.right === right &&
compareIDs(this.rightOrigin, right.rightOrigin) &&
this.id.client === right.id.client &&
this.id.clock + this.length === right.id.clock &&
this.deleted === right.deleted &&
this.redone === null &&
right.redone === null &&
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
if (right.keep) {
this.keep = true
}
this.right = right.right this.right = right.right
if (this.right !== null) { if (this.right !== null) {
this.right.left = this this.right.left = this
} }
this.length += right.length
return true return true
} }
return false return false
} }
/** /**
* Mark this Item as deleted. * Mark this Item as deleted.
* *
@@ -407,49 +445,26 @@ export class AbstractItem extends AbstractStruct {
this.deleted = true this.deleted = true
addToDeleteSet(transaction.deleteSet, this.id, this.length) addToDeleteSet(transaction.deleteSet, this.id, this.length)
maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub) maplib.setIfUndefined(transaction.changed, parent, set.create).add(this.parentSub)
this.content.delete(transaction)
} }
} }
/** /**
* @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
* @param {boolean} parentGCd
* *
* @private * @private
*/ */
gcChildren (transaction, store) { } gc (store, parentGCd) {
if (!this.deleted) {
/** throw error.unexpectedCase()
* @param {Transaction} transaction }
* @param {StructStore} store this.content.gc(store)
* if (parentGCd) {
* @private replaceStruct(store, this, new GC(this.id, this.length))
*/
gc (transaction, store) {
let r
if (this.parent._item !== null && this.parent._item.deleted) {
r = new GC(this.id, this.length)
} else { } else {
r = new ItemDeleted(this.id, this.left, this.origin, this.right, this.rightOrigin, this.parent, this.parentSub, this.length) this.content = new ContentDeleted(this.length)
if (r.right !== null) {
r.right.left = r
} else if (r.parentSub !== null) {
r.parent._map.set(r.parentSub, r)
}
if (r.left !== null) {
r.left.right = r
} else if (r.parentSub === null) {
r.parent._start = r
}
} }
replaceStruct(store, this, r)
transaction._mergeStructs.add(r.id)
}
/**
* @return {Array<any>}
*/
getContent () {
throw error.methodUnimplemented()
} }
/** /**
@@ -460,15 +475,14 @@ export class AbstractItem extends AbstractStruct {
* *
* @param {encoding.Encoder} encoder The encoder to write data to. * @param {encoding.Encoder} encoder The encoder to write data to.
* @param {number} offset * @param {number} offset
* @param {number} encodingRef
* *
* @private * @private
*/ */
write (encoder, offset, encodingRef) { write (encoder, offset) {
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin
const rightOrigin = this.rightOrigin const rightOrigin = this.rightOrigin
const parentSub = this.parentSub const parentSub = this.parentSub
const info = (encodingRef & binary.BITS5) | const info = (this.content.getRef() & binary.BITS5) |
(origin === null ? 0 : binary.BIT8) | // origin is defined (origin === null ? 0 : binary.BIT8) | // origin is defined
(rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined (rightOrigin === null ? 0 : binary.BIT7) | // right origin is defined
(parentSub === null ? 0 : binary.BIT6) // parentSub is non-null (parentSub === null ? 0 : binary.BIT6) // parentSub is non-null
@@ -484,26 +498,129 @@ export class AbstractItem extends AbstractStruct {
if (parent._item === null) { if (parent._item === null) {
// parent type on y._map // parent type on y._map
// find the correct key // find the correct key
// @ts-ignore we know that y exists
const ykey = findRootTypeKey(parent) const ykey = findRootTypeKey(parent)
encoding.writeVarUint(encoder, 1) // write parentYKey encoding.writeVarUint(encoder, 1) // write parentYKey
encoding.writeVarString(encoder, ykey) encoding.writeVarString(encoder, ykey)
} else { } else {
encoding.writeVarUint(encoder, 0) // write parent id encoding.writeVarUint(encoder, 0) // write parent id
// @ts-ignore _item is defined because parent is integrated
writeID(encoder, parent._item.id) writeID(encoder, parent._item.id)
} }
if (parentSub !== null) { if (parentSub !== null) {
encoding.writeVarString(encoder, parentSub) encoding.writeVarString(encoder, parentSub)
} }
} }
this.content.write(encoder, offset)
}
}
/**
* @param {decoding.Decoder} decoder
* @param {number} info
*/
const readItemContent = (decoder, info) => contentRefs[info & binary.BITS5](decoder)
/**
* A lookup map for reading Item content.
*
* @type {Array<function(decoding.Decoder):AbstractContent>}
*/
export const contentRefs = [
() => { throw error.unexpectedCase() }, // GC is not ItemContent
readContentDeleted,
readContentJSON,
readContentBinary,
readContentString,
readContentEmbed,
readContentFormat,
readContentType
]
/**
* Do not implement this class!
*/
export class AbstractContent {
/**
* @return {number}
*/
getLength () {
throw error.methodUnimplemented()
}
/**
* @return {Array<any>}
*/
getContent () {
throw error.methodUnimplemented()
}
/**
* Should return false if this Item is some kind of meta information
* (e.g. format information).
*
* * Whether this Item should be addressable via `yarray.get(i)`
* * Whether this Item should be counted when computing yarray.length
*
* @return {boolean}
*/
isCountable () {
throw error.methodUnimplemented()
}
/**
* @return {AbstractContent}
*/
copy () {
throw error.methodUnimplemented()
}
/**
* @param {number} offset
* @return {AbstractContent}
*/
splice (offset) {
throw error.methodUnimplemented()
}
/**
* @param {AbstractContent} right
* @return {boolean}
*/
mergeWith (right) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
throw error.methodUnimplemented()
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
throw error.methodUnimplemented()
}
/**
* @param {StructStore} store
*/
gc (store) {
throw error.methodUnimplemented()
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
throw error.methodUnimplemented()
}
/**
* @return {number}
*/
getRef () {
throw error.methodUnimplemented()
} }
} }
/** /**
* @private * @private
*/ */
export class AbstractItemRef extends AbstractStructRef { export class ItemRef extends AbstractStructRef {
/** /**
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {ID} id * @param {ID} id
@@ -553,79 +670,70 @@ export class AbstractItemRef extends AbstractStructRef {
if (this.parent !== null) { if (this.parent !== null) {
missing.push(this.parent) missing.push(this.parent)
} }
/**
* @type {AbstractContent}
*/
this.content = readItemContent(decoder, info)
this.length = this.content.getLength()
} }
}
/**
* @param {AbstractItemRef} item
* @param {number} offset
*
* @function
* @private
*/
export const changeItemRefOffset = (item, offset) => {
item.id = createID(item.id.client, item.id.clock + offset)
item.left = createID(item.id.client, item.id.clock - 1)
}
export class ItemParams {
/** /**
* @param {AbstractItem?} left * @param {Transaction} transaction
* @param {AbstractItem?} right * @param {StructStore} store
* @param {AbstractType<YEvent>?} parent * @param {number} offset
* @param {string|null} parentSub * @return {Item|GC}
*/ */
constructor (left, right, parent, parentSub) { toStruct (transaction, store, offset) {
this.left = left if (offset > 0) {
this.right = right /**
this.parent = parent * @type {ID}
this.parentSub = parentSub */
} const id = this.id
} this.id = createID(id.client, id.clock + offset)
this.left = createID(this.id.client, this.id.clock - 1)
this.content = this.content.splice(offset)
this.length -= offset
}
/** const left = this.left === null ? null : getItemCleanEnd(transaction, store, this.left)
* Outsourcing some of the logic of computing the item params from a received struct. const right = this.right === null ? null : getItemCleanStart(transaction, store, this.right)
* If parent === null, it is expected to gc the read struct. Otherwise apply it. let parent = null
* let parentSub = this.parentSub
* @param {Transaction} transaction if (this.parent !== null) {
* @param {StructStore} store const parentItem = getItem(store, this.parent)
* @param {ID|null} leftid // Edge case: toStruct is called with an offset > 0. In this case left is defined.
* @param {ID|null} rightid // Depending in which order structs arrive, left may be GC'd and the parent not
* @param {ID|null} parentid // deleted. This is why we check if left is GC'd. Strictly we don't have
* @param {string|null} parentSub // to check if right is GC'd, but we will in case we run into future issues
* @param {string|null} parentYKey if (!parentItem.deleted && (left === null || left.constructor !== GC) && (right === null || right.constructor !== GC)) {
* @return {ItemParams} parent = /** @type {ContentType} */ (parentItem.content).type
* }
* @private } else if (this.parentYKey !== null) {
* @function parent = transaction.doc.get(this.parentYKey)
*/ } else if (left !== null) {
export const computeItemParams = (transaction, store, leftid, rightid, parentid, parentSub, parentYKey) => { if (left.constructor !== GC) {
const left = leftid === null ? null : getItemCleanEnd(transaction, store, leftid) parent = left.parent
const right = rightid === null ? null : getItemCleanStart(transaction, store, rightid) parentSub = left.parentSub
let parent = null }
if (parentid !== null) { } else if (right !== null) {
const parentItem = getItemType(store, parentid) if (right.constructor !== GC) {
switch (parentItem.constructor) { parent = right.parent
case ItemDeleted: parentSub = right.parentSub
case GC: }
break } else {
default: throw error.unexpectedCase()
parent = parentItem.type
} }
} else if (parentYKey !== null) {
parent = transaction.y.get(parentYKey) return parent === null
} else if (left !== null) { ? new GC(this.id, this.length)
if (left.constructor !== GC) { : new Item(
parent = left.parent this.id,
parentSub = left.parentSub left,
} this.left,
} else if (right !== null) { right,
if (right.constructor !== GC) { this.right,
parent = right.parent parent,
parentSub = right.parentSub parentSub,
} this.content
} else { )
throw error.unexpectedCase()
} }
return new ItemParams(left, right, parent, parentSub)
} }

View File

@@ -1,98 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
GC,
StructStore, Transaction, AbstractType, ID // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export const structBinaryRefNumber = 1
/**
* @private
*/
export class ItemBinary extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {ArrayBuffer} content
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
this.content = content
}
getContent () {
return [this.content]
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemBinary(id, left, origin, right, rightOrigin, parent, parentSub, this.content)
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structBinaryRefNumber)
encoding.writePayload(encoder, this.content)
}
}
/**
* @private
*/
export class ItemBinaryRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {ArrayBuffer}
*/
this.content = decoding.readPayload(decoder)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemBinary|GC}
*/
toStruct (transaction, store, offset) {
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemBinary(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.content
)
}
}

View File

@@ -1,142 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
changeItemRefOffset,
GC,
splitItem,
addToDeleteSet,
Y, StructStore, Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export const structDeletedRefNumber = 2
/**
* @private
*/
export class ItemDeleted extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {number} length
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, length) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
this._len = length
this.deleted = true
}
get length () {
return this._len
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemDeleted(id, left, origin, right, rightOrigin, parent, parentSub, this.length)
}
/**
* @param {Transaction} transaction
*/
integrate (transaction) {
super.integrate(transaction)
addToDeleteSet(transaction.deleteSet, this.id, this.length)
}
/**
* @param {Transaction} transaction
* @param {number} diff
*/
splitAt (transaction, diff) {
/**
* @type {ItemDeleted}
*/
// @ts-ignore
const right = splitItem(transaction, this, diff)
right._len -= diff
this._len = diff
return right
}
/**
* @param {ItemDeleted} right
* @return {boolean}
*/
mergeWith (right) {
if (super.mergeWith(right)) {
this._len += right._len
return true
}
return false
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structDeletedRefNumber)
encoding.writeVarUint(encoder, this.length - offset)
}
}
/**
* @private
*/
export class ItemDeletedRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {number}
*/
this.len = decoding.readVarUint(decoder)
}
get length () {
return this.len
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemDeleted|GC}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
changeItemRefOffset(this, offset)
this.len = this.len - offset
}
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemDeleted(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.len
)
}
}

View File

@@ -1,95 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
GC,
Transaction, StructStore, ID, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export const structEmbedRefNumber = 3
/**
* @private
*/
export class ItemEmbed extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {Object} embed
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, embed) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
this.embed = embed
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemEmbed(id, left, origin, right, rightOrigin, parent, parentSub, this.embed)
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structEmbedRefNumber)
encoding.writeVarString(encoder, JSON.stringify(this.embed))
}
}
/**
* @private
*/
export class ItemEmbedRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {ArrayBuffer}
*/
this.embed = JSON.parse(decoding.readVarString(decoder))
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemEmbed|GC}
*/
toStruct (transaction, store, offset) {
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemEmbed(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.embed
)
}
}

View File

@@ -1,103 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
GC,
Transaction, StructStore, ID, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export const structFormatRefNumber = 4
/**
* @private
*/
export class ItemFormat extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {string} key
* @param {any} value
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, key, value) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
this.key = key
this.value = value
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemFormat(id, left, origin, right, rightOrigin, parent, parentSub, this.key, this.value)
}
get countable () {
return false
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structFormatRefNumber)
encoding.writeVarString(encoder, this.key)
encoding.writeVarString(encoder, JSON.stringify(this.value))
}
}
/**
* @private
*/
export class ItemFormatRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {string}
*/
this.key = decoding.readVarString(decoder)
this.value = JSON.parse(decoding.readVarString(decoder))
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemFormat|GC}
*/
toStruct (transaction, store, offset) {
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemFormat(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.key,
this.value
)
}
}

View File

@@ -1,152 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
splitItem,
changeItemRefOffset,
GC,
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export const structJSONRefNumber = 5
/**
* @private
*/
export class ItemJSON extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {Array<any>} content
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
/**
* @type {Array<any>}
*/
this.content = content
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemJSON(id, left, origin, right, rightOrigin, parent, parentSub, this.content)
}
get length () {
return this.content.length
}
getContent () {
return this.content
}
/**
* @param {Transaction} transaction
* @param {number} diff
*/
splitAt (transaction, diff) {
/**
* @type {ItemJSON}
*/
// @ts-ignore
const right = splitItem(transaction, this, diff)
right.content = this.content.splice(diff)
return right
}
/**
* @param {ItemJSON} right
* @return {boolean}
*/
mergeWith (right) {
if (super.mergeWith(right)) {
this.content = this.content.concat(right.content)
return true
}
return false
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structJSONRefNumber)
const len = this.content.length
encoding.writeVarUint(encoder, len - offset)
for (let i = offset; i < len; i++) {
const c = this.content[i]
encoding.writeVarString(encoder, c === undefined ? 'undefined' : JSON.stringify(c))
}
}
}
/**
* @private
*/
export class ItemJSONRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
const len = decoding.readVarUint(decoder)
const cs = []
for (let i = 0; i < len; i++) {
const c = decoding.readVarString(decoder)
if (c === 'undefined') {
cs.push(undefined)
} else {
cs.push(JSON.parse(c))
}
}
/**
* @type {Array<any>}
*/
this.content = cs
}
get length () {
return this.content.length
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemJSON|GC}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
changeItemRefOffset(this, offset)
this.content = this.content.slice(offset)
}
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemJSON(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.content
)
}
}

View File

@@ -1,137 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
splitItem,
changeItemRefOffset,
GC,
Transaction, StructStore, Y, ID, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js'
export const structStringRefNumber = 6
/**
* @private
*/
export class ItemString extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {string} string
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, string) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
/**
* @type {string}
*/
this.string = string
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemString(id, left, origin, right, rightOrigin, parent, parentSub, this.string)
}
getContent () {
return this.string.split('')
}
get length () {
return this.string.length
}
/**
* @param {Transaction} transaction
* @param {number} diff
* @return {ItemString}
*/
splitAt (transaction, diff) {
/**
* @type {ItemString}
*/
// @ts-ignore
const right = splitItem(transaction, this, diff)
right.string = this.string.slice(diff)
this.string = this.string.slice(0, diff)
return right
}
/**
* @param {ItemString} right
* @return {boolean}
*/
mergeWith (right) {
if (super.mergeWith(right)) {
this.string += right.string
return true
}
return false
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structStringRefNumber)
encoding.writeVarString(encoder, offset === 0 ? this.string : this.string.slice(offset))
}
}
/**
* @private
*/
export class ItemStringRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
/**
* @type {string}
*/
this.string = decoding.readVarString(decoder)
}
get length () {
return this.string.length
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemString|GC}
*/
toStruct (transaction, store, offset) {
if (offset > 0) {
changeItemRefOffset(this, offset)
this.string = this.string.slice(offset)
}
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemString(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.string
)
}
}

View File

@@ -1,178 +0,0 @@
import {
AbstractItem,
AbstractItemRef,
computeItemParams,
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText,
StructStore, Y, GC, Transaction, ID, AbstractType // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.js'
/**
* @private
*/
export const structTypeRefNumber = 7
/**
* @type {Array<function(decoding.Decoder):AbstractType<any>>}
* @private
*/
export const typeRefs = [
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText
]
export const YArrayRefID = 0
export const YMapRefID = 1
export const YTextRefID = 2
export const YXmlElementRefID = 3
export const YXmlFragmentRefID = 4
export const YXmlHookRefID = 5
export const YXmlTextRefID = 6
/**
* @private
*/
export class ItemType extends AbstractItem {
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @param {AbstractType<any>} type
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, type) {
super(id, left, origin, right, rightOrigin, parent, parentSub)
this.type = type
}
getContent () {
return [this.type]
}
/**
* @param {ID} id
* @param {AbstractItem | null} left
* @param {ID | null} origin
* @param {AbstractItem | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>} parent
* @param {string | null} parentSub
* @return {ItemType}
*/
copy (id, left, origin, right, rightOrigin, parent, parentSub) {
return new ItemType(id, left, origin, right, rightOrigin, parent, parentSub, this.type._copy())
}
/**
* @param {Transaction} transaction
*/
integrate (transaction) {
super.integrate(transaction)
this.type._integrate(transaction.y, this)
}
/**
* @param {encoding.Encoder} encoder
* @param {number} offset
*/
write (encoder, offset) {
super.write(encoder, offset, structTypeRefNumber)
this.type._write(encoder)
}
/**
* Mark this Item as deleted.
*
* @param {Transaction} transaction The Yjs instance
* @private
*/
delete (transaction) {
super.delete(transaction)
transaction.changed.delete(this.type)
transaction.changedParentTypes.delete(this.type)
this.gcChildren(transaction, transaction.y.store)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
*/
gcChildren (transaction, store) {
let item = this.type._start
while (item !== null) {
item.gc(transaction, store)
item = item.right
}
this.type._start = null
this.type._map.forEach(item => {
while (item !== null) {
item.gc(transaction, store)
// @ts-ignore
item = item.left
}
})
this._map = new Map()
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
*/
gc (transaction, store) {
super.gc(transaction, store)
this.gcChildren(transaction, store)
}
}
/**
* @private
*/
export class ItemTypeRef extends AbstractItemRef {
/**
* @param {decoding.Decoder} decoder
* @param {ID} id
* @param {number} info
*/
constructor (decoder, id, info) {
super(decoder, id, info)
const typeRef = decoding.readVarUint(decoder)
/**
* @type {AbstractType<any>}
*/
this.type = typeRefs[typeRef](decoder)
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @param {number} offset
* @return {ItemType|GC}
*/
toStruct (transaction, store, offset) {
const { left, right, parent, parentSub } = computeItemParams(transaction, store, this.left, this.right, this.parent, this.parentSub, this.parentYKey)
return parent === null
? new GC(this.id, this.length)
: new ItemType(
this.id,
left,
this.left,
right,
this.right,
parent,
parentSub,
this.type
)
}
}

View File

@@ -4,14 +4,14 @@ import {
callEventHandlerListeners, callEventHandlerListeners,
addEventHandlerListener, addEventHandlerListener,
createEventHandler, createEventHandler,
ItemType,
nextID, nextID,
isVisible, isVisible,
ItemJSON, ContentType,
ItemBinary, ContentJSON,
ContentBinary,
createID, createID,
getItemCleanStart, getItemCleanStart,
Y, Snapshot, Transaction, EventHandler, YEvent, AbstractItem, // eslint-disable-line Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
@@ -49,24 +49,24 @@ export const callTypeObservers = (type, transaction, event) => {
export class AbstractType { export class AbstractType {
constructor () { constructor () {
/** /**
* @type {ItemType|null} * @type {Item|null}
*/ */
this._item = null this._item = null
/** /**
* @private * @private
* @type {Map<string,AbstractItem>} * @type {Map<string,Item>}
*/ */
this._map = new Map() this._map = new Map()
/** /**
* @private * @private
* @type {AbstractItem|null} * @type {Item|null}
*/ */
this._start = null this._start = null
/** /**
* @private * @private
* @type {Y|null} * @type {Doc|null}
*/ */
this._y = null this.doc = null
this._length = 0 this._length = 0
/** /**
* Event handlers * Event handlers
@@ -87,12 +87,12 @@ export class AbstractType {
* * This type is sent to other client * * This type is sent to other client
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Y} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType|null} item * @param {Item|null} item
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
this._y = y this.doc = y
this._item = item this._item = item
} }
@@ -101,7 +101,7 @@ export class AbstractType {
* @private * @private
*/ */
_copy () { _copy () {
throw new Error('unimplemented') throw error.methodUnimplemented()
} }
/** /**
@@ -182,12 +182,35 @@ export class AbstractType {
* @private * @private
* @function * @function
*/ */
export const typeArrayToArray = type => { export const typeListToArray = type => {
const cs = [] const cs = []
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && !n.deleted) { if (n.countable && !n.deleted) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/**
* @param {AbstractType<any>} type
* @param {Snapshot} snapshot
* @return {Array<any>}
*
* @private
* @function
*/
export const typeListToArraySnapshot = (type, snapshot) => {
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && isVisible(n, snapshot)) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
cs.push(c[i]) cs.push(c[i])
} }
@@ -201,17 +224,17 @@ export const typeArrayToArray = type => {
* Executes a provided function on once on overy element of this YArray. * Executes a provided function on once on overy element of this YArray.
* *
* @param {AbstractType<any>} type * @param {AbstractType<any>} type
* @param {function(any,number,AbstractType<any>):void} f A function to execute on every element of this YArray. * @param {function(any,number,any):void} f A function to execute on every element of this YArray.
* *
* @private * @private
* @function * @function
*/ */
export const typeArrayForEach = (type, f) => { export const typeListForEach = (type, f) => {
let index = 0 let index = 0
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && !n.deleted) { if (n.countable && !n.deleted) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
f(c[i], index++, type) f(c[i], index++, type)
} }
@@ -229,12 +252,12 @@ export const typeArrayForEach = (type, f) => {
* @private * @private
* @function * @function
*/ */
export const typeArrayMap = (type, f) => { export const typeListMap = (type, f) => {
/** /**
* @type {Array<any>} * @type {Array<any>}
*/ */
const result = [] const result = []
typeArrayForEach(type, (c, i) => { typeListForEach(type, (c, i) => {
result.push(f(c, i, type)) result.push(f(c, i, type))
}) })
return result return result
@@ -247,7 +270,7 @@ export const typeArrayMap = (type, f) => {
* @private * @private
* @function * @function
*/ */
export const typeArrayCreateIterator = type => { export const typeListCreateIterator = type => {
let n = type._start let n = type._start
/** /**
* @type {Array<any>|null} * @type {Array<any>|null}
@@ -264,18 +287,15 @@ export const typeArrayCreateIterator = type => {
while (n !== null && n.deleted) { while (n !== null && n.deleted) {
n = n.right n = n.right
} }
} // check if we reached the end, no need to check currentContent, because it does not exist
// check if we reached the end, no need to check currentContent, because it does not exist if (n === null) {
if (n === null) { return {
return { done: true,
done: true, value: undefined
value: undefined }
} }
}
// currentContent could exist from the last iteration
if (currentContent === null) {
// we found n, so we can set currentContent // we found n, so we can set currentContent
currentContent = n.getContent() currentContent = n.content.getContent()
currentContentIndex = 0 currentContentIndex = 0
n = n.right // we used the content of n, now iterate to next n = n.right // we used the content of n, now iterate to next
} }
@@ -303,12 +323,12 @@ export const typeArrayCreateIterator = type => {
* @private * @private
* @function * @function
*/ */
export const typeArrayForEachSnapshot = (type, f, snapshot) => { export const typeListForEachSnapshot = (type, f, snapshot) => {
let index = 0 let index = 0
let n = type._start let n = type._start
while (n !== null) { while (n !== null) {
if (n.countable && isVisible(n, snapshot)) { if (n.countable && isVisible(n, snapshot)) {
const c = n.getContent() const c = n.content.getContent()
for (let i = 0; i < c.length; i++) { for (let i = 0; i < c.length; i++) {
f(c[i], index++, type) f(c[i], index++, type)
} }
@@ -325,11 +345,11 @@ export const typeArrayForEachSnapshot = (type, f, snapshot) => {
* @private * @private
* @function * @function
*/ */
export const typeArrayGet = (type, index) => { export const typeListGet = (type, index) => {
for (let n = type._start; n !== null; n = n.right) { for (let n = type._start; n !== null; n = n.right) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
if (index < n.length) { if (index < n.length) {
return n.getContent()[index] return n.content.getContent()[index]
} }
index -= n.length index -= n.length
} }
@@ -339,13 +359,13 @@ export const typeArrayGet = (type, index) => {
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem?} referenceItem * @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|number|string|ArrayBuffer>} content * @param {Array<Object<string,any>|Array<any>|boolean|number|string|Uint8Array>} content
* *
* @private * @private
* @function * @function
*/ */
export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem, content) => { export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
let left = referenceItem let left = referenceItem
const right = referenceItem === null ? parent._start : referenceItem.right const right = referenceItem === null ? parent._start : referenceItem.right
/** /**
@@ -354,7 +374,7 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
let jsonContent = [] let jsonContent = []
const packJsonContent = () => { const packJsonContent = () => {
if (jsonContent.length > 0) { if (jsonContent.length > 0) {
left = new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, jsonContent) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentJSON(jsonContent))
left.integrate(transaction) left.integrate(transaction)
jsonContent = [] jsonContent = []
} }
@@ -363,6 +383,7 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
switch (c.constructor) { switch (c.constructor) {
case Number: case Number:
case Object: case Object:
case Boolean:
case Array: case Array:
case String: case String:
jsonContent.push(c) jsonContent.push(c)
@@ -370,15 +391,14 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
default: default:
packJsonContent() packJsonContent()
switch (c.constructor) { switch (c.constructor) {
case Uint8Array:
case ArrayBuffer: case ArrayBuffer:
// @ts-ignore c is definitely an ArrayBuffer left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))))
left = new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, c)
// @ts-ignore
left.integrate(transaction) left.integrate(transaction)
break break
default: default:
if (c instanceof AbstractType) { if (c instanceof AbstractType) {
left = new ItemType(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, c) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentType(c))
left.integrate(transaction) left.integrate(transaction)
} else { } else {
throw new Error('Unexpected content type in insert operation') throw new Error('Unexpected content type in insert operation')
@@ -393,14 +413,14 @@ export const typeArrayInsertGenericsAfter = (transaction, parent, referenceItem,
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {number} index * @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|string|ArrayBuffer>} content * @param {Array<Object<string,any>|Array<any>|number|string|Uint8Array>} content
* *
* @private * @private
* @function * @function
*/ */
export const typeArrayInsertGenerics = (transaction, parent, index, content) => { export const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index === 0) { if (index === 0) {
return typeArrayInsertGenericsAfter(transaction, parent, null, content) return typeListInsertGenericsAfter(transaction, parent, null, content)
} }
let n = parent._start let n = parent._start
for (; n !== null; n = n.right) { for (; n !== null; n = n.right) {
@@ -408,14 +428,14 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
if (index <= n.length) { if (index <= n.length) {
if (index < n.length) { if (index < n.length) {
// insert in-between // insert in-between
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index)) getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index))
} }
break break
} }
index -= n.length index -= n.length
} }
} }
return typeArrayInsertGenericsAfter(transaction, parent, n, content) return typeListInsertGenericsAfter(transaction, parent, n, content)
} }
/** /**
@@ -427,17 +447,14 @@ export const typeArrayInsertGenerics = (transaction, parent, index, content) =>
* @private * @private
* @function * @function
*/ */
export const typeArrayDelete = (transaction, parent, index, length) => { export const typeListDelete = (transaction, parent, index, length) => {
if (length === 0) { return } if (length === 0) { return }
let n = parent._start let n = parent._start
// compute the first item to be deleted // compute the first item to be deleted
for (; n !== null; n = n.right) { for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
if (index <= n.length) { if (index < n.length) {
if (index < n.length && index > 0) { getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + index))
n = getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + index))
}
break
} }
index -= n.length index -= n.length
} }
@@ -446,7 +463,7 @@ export const typeArrayDelete = (transaction, parent, index, length) => {
while (length > 0 && n !== null) { while (length > 0 && n !== null) {
if (!n.deleted) { if (!n.deleted) {
if (length < n.length) { if (length < n.length) {
getItemCleanStart(transaction, transaction.y.store, createID(n.id.client, n.id.clock + length)) getItemCleanStart(transaction, transaction.doc.store, createID(n.id.client, n.id.clock + length))
} }
n.delete(transaction) n.delete(transaction)
length -= n.length length -= n.length
@@ -477,52 +494,55 @@ export const typeMapDelete = (transaction, parent, key) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string} key * @param {string} key
* @param {Object|number|Array<any>|string|ArrayBuffer|AbstractType<any>} value * @param {Object|number|Array<any>|string|Uint8Array|AbstractType<any>} value
* *
* @private * @private
* @function * @function
*/ */
export const typeMapSet = (transaction, parent, key, value) => { export const typeMapSet = (transaction, parent, key, value) => {
const left = parent._map.get(key) || null const left = parent._map.get(key) || null
let content
if (value == null) { if (value == null) {
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction) content = new ContentJSON([value])
return } else {
} switch (value.constructor) {
switch (value.constructor) { case Number:
case Number: case Object:
case Object: case Boolean:
case Array: case Array:
case String: case String:
new ItemJSON(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, [value]).integrate(transaction) content = new ContentJSON([value])
break break
case ArrayBuffer: case Uint8Array:
new ItemBinary(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction) content = new ContentBinary(value)
break break
default: default:
if (value instanceof AbstractType) { if (value instanceof AbstractType) {
new ItemType(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, value).integrate(transaction) content = new ContentType(value)
} else { } else {
throw new Error('Unexpected content type') throw new Error('Unexpected content type')
} }
}
} }
new Item(nextID(transaction), left, left === null ? null : left.lastId, null, null, parent, key, content).integrate(transaction)
} }
/** /**
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string} key * @param {string} key
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined} * @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
* *
* @private * @private
* @function * @function
*/ */
export const typeMapGet = (parent, key) => { export const typeMapGet = (parent, key) => {
const val = parent._map.get(key) const val = parent._map.get(key)
return val !== undefined && !val.deleted ? val.getContent()[0] : undefined return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
} }
/** /**
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @return {Object<string,Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined>} * @return {Object<string,Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
* *
* @private * @private
* @function * @function
@@ -534,7 +554,7 @@ export const typeMapGetAll = (parent) => {
let res = {} let res = {}
for (const [key, value] of parent._map) { for (const [key, value] of parent._map) {
if (!value.deleted) { if (!value.deleted) {
res[key] = value.getContent()[value.length - 1] res[key] = value.content.getContent()[value.length - 1]
} }
} }
return res return res
@@ -557,7 +577,7 @@ export const typeMapHas = (parent, key) => {
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {string} key * @param {string} key
* @param {Snapshot} snapshot * @param {Snapshot} snapshot
* @return {Object<string,any>|number|Array<any>|string|ArrayBuffer|AbstractType<any>|undefined} * @return {Object<string,any>|number|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
* *
* @private * @private
* @function * @function
@@ -567,14 +587,14 @@ export const typeMapGetSnapshot = (parent, key, snapshot) => {
while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) { while (v !== null && (!snapshot.sm.has(v.id.client) || v.id.clock >= (snapshot.sm.get(v.id.client) || 0))) {
v = v.left v = v.left
} }
return v !== null && isVisible(v, snapshot) ? v.getContent()[v.length - 1] : undefined return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined
} }
/** /**
* @param {Map<string,AbstractItem>} map * @param {Map<string,Item>} map
* @return {IterableIterator<Array<any>>} * @return {IterableIterator<Array<any>>}
* *
* @private * @private
* @function * @function
*/ */
export const createMapIterator = map => iterator.iteratorFilter(map.entries(), entry => !entry[1].deleted) export const createMapIterator = map => iterator.iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted)

View File

@@ -5,17 +5,17 @@
import { import {
YEvent, YEvent,
AbstractType, AbstractType,
typeArrayGet, typeListGet,
typeArrayToArray, typeListToArray,
typeArrayForEach, typeListForEach,
typeArrayCreateIterator, typeListCreateIterator,
typeArrayInsertGenerics, typeListInsertGenerics,
typeArrayDelete, typeListDelete,
typeArrayMap, typeListMap,
YArrayRefID, YArrayRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Y, Transaction, ItemType, // eslint-disable-line Doc, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@@ -58,17 +58,21 @@ export class YArray extends AbstractType {
* * This type is sent to other client * * This type is sent to other client
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Y} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType} item * @param {Item} item
* *
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore this.insert(0, /** @type {Array} */ (this._prelimContent))
this.insert(0, this._prelimContent)
this._prelimContent = null this._prelimContent = null
} }
_copy () {
return new YArray()
}
get length () { get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length return this._prelimContent === null ? this._length : this._prelimContent.length
} }
@@ -84,6 +88,57 @@ export class YArray extends AbstractType {
callTypeObservers(this, transaction, new YArrayEvent(this, transaction)) callTypeObservers(this, transaction, new YArrayEvent(this, transaction))
} }
/**
* Inserts new content at an index.
*
* Important: This function expects an array of content. Not just a content
* object. The reason for this "weirdness" is that inserting several elements
* is very efficient when it is done as a single operation.
*
* @example
* // Insert character 'a' at position 0
* yarray.insert(0, ['a'])
* // Insert numbers 1, 2 at position 1
* yarray.insert(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {Array<T>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
})
} else {
/** @type {Array} */ (this._prelimContent).splice(index, 0, ...content)
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*/
push (content) {
this.insert(this.length, content)
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
/** @type {Array} */ (this._prelimContent).splice(index, length)
}
}
/** /**
* Returns the i-th element from a YArray. * Returns the i-th element from a YArray.
* *
@@ -91,7 +146,7 @@ export class YArray extends AbstractType {
* @return {T} * @return {T}
*/ */
get (index) { get (index) {
return typeArrayGet(this, index) return typeListGet(this, index)
} }
/** /**
@@ -100,7 +155,7 @@ export class YArray extends AbstractType {
* @return {Array<T>} * @return {Array<T>}
*/ */
toArray () { toArray () {
return typeArrayToArray(this) return typeListToArray(this)
} }
/** /**
@@ -122,77 +177,23 @@ export class YArray extends AbstractType {
* callback function * callback function
*/ */
map (f) { map (f) {
// @ts-ignore return typeListMap(this, /** @type {any} */ (f))
return typeArrayMap(this, f)
} }
/** /**
* Executes a provided function on once on overy element of this YArray. * Executes a provided function on once on overy element of this YArray.
* *
* @param {function(T,number):void} f A function to execute on every element of this YArray. * @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/ */
forEach (f) { forEach (f) {
typeArrayForEach(this, f) typeListForEach(this, f)
} }
/** /**
* @return {IterableIterator<T>} * @return {IterableIterator<T>}
*/ */
[Symbol.iterator] () { [Symbol.iterator] () {
return typeArrayCreateIterator(this) return typeListCreateIterator(this)
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Inserts new content at an index.
*
* Important: This function expects an array of content. Not just a content
* object. The reason for this "weirdness" is that inserting several elements
* is very efficient when it is done as a single operation.
*
* @example
* // Insert character 'a' at position 0
* yarray.insert(0, ['a'])
* // Insert numbers 1, 2 at position 1
* yarray.insert(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {Array<T>} content The array of content
*/
insert (index, content) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*/
push (content) {
this.insert(this.length, content)
} }
/** /**

View File

@@ -14,7 +14,7 @@ import {
YMapRefID, YMapRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Y, Transaction, ItemType, // eslint-disable-line Doc, Transaction, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -38,7 +38,7 @@ export class YMapEvent extends YEvent {
} }
/** /**
* @template T number|string|Object|Array|ArrayBuffer * @template T number|string|Object|Array|Uint8Array
* A shared Map implementation. * A shared Map implementation.
* *
* @extends AbstractType<YMapEvent<T>> * @extends AbstractType<YMapEvent<T>>
@@ -60,19 +60,23 @@ export class YMap extends AbstractType {
* * This type is sent to other client * * This type is sent to other client
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Y} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType} item * @param {Item} item
* *
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore for (let [key, value] of /** @type {Map<string, any>} */ (this._prelimContent)) {
for (let [key, value] of this._prelimContent) {
this.set(key, value) this.set(key, value)
} }
this._prelimContent = null this._prelimContent = null
} }
_copy () {
return new YMap()
}
/** /**
* Creates YMapEvent and calls observers. * Creates YMapEvent and calls observers.
* *
@@ -97,7 +101,7 @@ export class YMap extends AbstractType {
const map = {} const map = {}
for (let [key, item] of this._map) { for (let [key, item] of this._map) {
if (!item.deleted) { if (!item.deleted) {
const v = item.getContent()[0] const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v map[key] = v instanceof AbstractType ? v.toJSON() : v
} }
} }
@@ -107,18 +111,46 @@ export class YMap extends AbstractType {
/** /**
* Returns the keys for each element in the YMap Type. * Returns the keys for each element in the YMap Type.
* *
* @return {Iterator<string>} * @return {IterableIterator<string>}
*/ */
keys () { keys () {
return iterator.iteratorMap(createMapIterator(this._map), v => v[0]) return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0])
} }
/** /**
* Returns the value for each element in the YMap Type. * Returns the keys for each element in the YMap Type.
* *
* @return {IterableIterator<T>} * @return {IterableIterator<string>}
*/
values () {
return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/ */
entries () { entries () {
return iterator.iteratorMap(createMapIterator(this._map), v => v[1].getContent()[0]) return iterator.iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
}
/**
* Executes a provided function on once on overy key-value pair.
*
* @param {function(T,string,YMap<T>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
/**
* @type {Object<string,T>}
*/
const map = {}
for (let [key, item] of this._map) {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this)
}
}
return map
} }
/** /**
@@ -134,13 +166,12 @@ export class YMap extends AbstractType {
* @param {string} key The key of the element to remove. * @param {string} key The key of the element to remove.
*/ */
delete (key) { delete (key) {
if (this._y !== null) { if (this.doc !== null) {
transact(this._y, transaction => { transact(this.doc, transaction => {
typeMapDelete(transaction, this, key) typeMapDelete(transaction, this, key)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string, any>} */ (this._prelimContent).delete(key)
this._prelimContent.delete(key)
} }
} }
@@ -151,13 +182,12 @@ export class YMap extends AbstractType {
* @param {T} value The value of the element to add * @param {T} value The value of the element to add
*/ */
set (key, value) { set (key, value) {
if (this._y !== null) { if (this.doc !== null) {
transact(this._y, transaction => { transact(this.doc, transaction => {
typeMapSet(transaction, this, key, value) typeMapSet(transaction, this, key, value)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
this._prelimContent.set(key, value)
} }
return value return value
} }
@@ -169,8 +199,7 @@ export class YMap extends AbstractType {
* @return {T|undefined} * @return {T|undefined}
*/ */
get (key) { get (key) {
// @ts-ignore return /** @type {any} */ (typeMapGet(this, key))
return typeMapGet(this, key)
} }
/** /**

View File

@@ -5,9 +5,6 @@
import { import {
YEvent, YEvent,
ItemEmbed,
ItemString,
ItemFormat,
AbstractType, AbstractType,
nextID, nextID,
createID, createID,
@@ -16,7 +13,10 @@ import {
YTextRefID, YTextRefID,
callTypeObservers, callTypeObservers,
transact, transact,
Y, ItemType, AbstractItem, Snapshot, StructStore, Transaction // eslint-disable-line ContentEmbed,
ContentFormat,
ContentString,
Doc, Item, Snapshot, StructStore, Transaction // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line import * as decoding from 'lib0/decoding.js' // eslint-disable-line
@@ -24,8 +24,8 @@ import * as encoding from 'lib0/encoding.js'
export class ItemListPosition { export class ItemListPosition {
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
*/ */
constructor (left, right) { constructor (left, right) {
this.left = left this.left = left
@@ -35,8 +35,8 @@ export class ItemListPosition {
export class ItemTextListPosition extends ItemListPosition { export class ItemTextListPosition extends ItemListPosition {
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
*/ */
constructor (left, right, currentAttributes) { constructor (left, right, currentAttributes) {
@@ -47,8 +47,8 @@ export class ItemTextListPosition extends ItemListPosition {
export class ItemInsertionResult extends ItemListPosition { export class ItemInsertionResult extends ItemListPosition {
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} negatedAttributes * @param {Map<string,any>} negatedAttributes
*/ */
constructor (left, right, negatedAttributes) { constructor (left, right, negatedAttributes) {
@@ -61,8 +61,8 @@ export class ItemInsertionResult extends ItemListPosition {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {number} count * @param {number} count
* @return {ItemTextListPosition} * @return {ItemTextListPosition}
* *
@@ -71,9 +71,9 @@ export class ItemInsertionResult extends ItemListPosition {
*/ */
const findNextPosition = (transaction, store, currentAttributes, left, right, count) => { const findNextPosition = (transaction, store, currentAttributes, left, right, count) => {
while (right !== null && count > 0) { while (right !== null && count > 0) {
switch (right.constructor) { switch (right.content.constructor) {
case ItemEmbed: case ContentEmbed:
case ItemString: case ContentString:
if (!right.deleted) { if (!right.deleted) {
if (count < right.length) { if (count < right.length) {
// split right // split right
@@ -82,10 +82,9 @@ const findNextPosition = (transaction, store, currentAttributes, left, right, co
count -= right.length count -= right.length
} }
break break
case ItemFormat: case ContentFormat:
if (!right.deleted) { if (!right.deleted) {
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
} }
break break
} }
@@ -117,8 +116,8 @@ const findPosition = (transaction, store, parent, index) => {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} negatedAttributes * @param {Map<string,any>} negatedAttributes
* @return {ItemListPosition} * @return {ItemListPosition}
* *
@@ -130,36 +129,33 @@ const insertNegatedAttributes = (transaction, parent, left, right, negatedAttrib
while ( while (
right !== null && ( right !== null && (
right.deleted === true || ( right.deleted === true || (
right.constructor === ItemFormat && right.content.constructor === ContentFormat &&
// @ts-ignore right is ItemFormat (negatedAttributes.get(/** @type {ContentFormat} */ (right.content).key) === /** @type {ContentFormat} */ (right.content).value)
(negatedAttributes.get(right.key) === right.value)
) )
) )
) { ) {
if (!right.deleted) { if (!right.deleted) {
// @ts-ignore right is ItemFormat negatedAttributes.delete(/** @type {ContentFormat} */ (right.content).key)
negatedAttributes.delete(right.key)
} }
left = right left = right
right = right.right right = right.right
} }
for (let [key, val] of negatedAttributes) { for (let [key, val] of negatedAttributes) {
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction) left.integrate(transaction)
} }
return {left, right} return { left, right }
} }
/** /**
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {ItemFormat} item * @param {ContentFormat} format
* *
* @private * @private
* @function * @function
*/ */
const updateCurrentAttributes = (currentAttributes, item) => { const updateCurrentAttributes = (currentAttributes, format) => {
const value = item.value const { key, value } = format
const key = item.key
if (value === null) { if (value === null) {
currentAttributes.delete(key) currentAttributes.delete(key)
} else { } else {
@@ -168,8 +164,8 @@ const updateCurrentAttributes = (currentAttributes, item) => {
} }
/** /**
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
* @return {ItemListPosition} * @return {ItemListPosition}
@@ -184,11 +180,9 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
break break
} else if (right.deleted) { } else if (right.deleted) {
// continue // continue
// @ts-ignore right is ItemFormat } else if (right.content.constructor === ContentFormat && (attributes[(/** @type {ContentFormat} */ (right.content)).key] || null) === /** @type {ContentFormat} */ (right.content).value) {
} else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
// found a format, update currentAttributes and continue // found a format, update currentAttributes and continue
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
} else { } else {
break break
} }
@@ -201,8 +195,8 @@ const minimizeAttributeChanges = (left, right, currentAttributes, attributes) =>
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
* @return {ItemInsertionResult} * @return {ItemInsertionResult}
@@ -215,11 +209,11 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
// insert format-start items // insert format-start items
for (let key in attributes) { for (let key in attributes) {
const val = attributes[key] const val = attributes[key]
const currentVal = currentAttributes.get(key) const currentVal = currentAttributes.get(key) || null
if (currentVal !== val) { if (currentVal !== val) {
// save negated attribute (set null if currentVal undefined) // save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal || null) negatedAttributes.set(key, currentVal)
left = new ItemFormat(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, key, val) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentFormat(key, val))
left.integrate(transaction) left.integrate(transaction)
} }
} }
@@ -229,10 +223,10 @@ const insertAttributes = (transaction, parent, left, right, currentAttributes, a
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {string} text * @param {string|object} text
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
* @return {ItemListPosition} * @return {ItemListPosition}
* *
@@ -250,11 +244,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
left = insertPos.left left = insertPos.left
right = insertPos.right right = insertPos.right
// insert content // insert content
if (text.constructor === String) { const content = text.constructor === String ? new ContentString(text) : new ContentEmbed(text)
left = new ItemString(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, text) left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, content)
} else {
left = new ItemEmbed(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, text)
}
left.integrate(transaction) left.integrate(transaction)
return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes) return insertNegatedAttributes(transaction, parent, left, insertPos.right, insertPos.negatedAttributes)
} }
@@ -262,8 +253,8 @@ const insertText = (transaction, parent, left, right, currentAttributes, text, a
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {number} length * @param {number} length
* @param {Object<string,any>} attributes * @param {Object<string,any>} attributes
@@ -282,28 +273,24 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
// delete all formats with attributes[format.key] != null // delete all formats with attributes[format.key] != null
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (right.deleted === false) { if (right.deleted === false) {
switch (right.constructor) { switch (right.content.constructor) {
case ItemFormat: case ContentFormat:
// @ts-ignore right is ItemFormat const { key, value } = /** @type {ContentFormat} */ (right.content)
const attr = attributes[right.key] const attr = attributes[key]
if (attr !== undefined) { if (attr !== undefined) {
// @ts-ignore right is ItemFormat if (attr === value) {
if (attr === right.value) { negatedAttributes.delete(key)
// @ts-ignore right is ItemFormat
negatedAttributes.delete(right.key)
} else { } else {
// @ts-ignore right is ItemFormat negatedAttributes.set(key, value)
negatedAttributes.set(right.key, right.value)
} }
right.delete(transaction) right.delete(transaction)
} }
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
break break
case ItemEmbed: case ContentEmbed:
case ItemString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.y.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
break break
@@ -312,13 +299,24 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
left = right left = right
right = right.right right = right.right
} }
// Quill just assumes that the editor starts with a newline and that it always
// ends with a newline. We only insert that newline when a new newline is
// inserted - i.e when length is bigger than type.length
if (length > 0) {
let newlines = ''
for (; length > 0; length--) {
newlines += '\n'
}
left = new Item(nextID(transaction), left, left === null ? null : left.lastId, right, right === null ? null : right.id, parent, null, new ContentString(newlines))
left.integrate(transaction)
}
return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes) return insertNegatedAttributes(transaction, parent, left, right, negatedAttributes)
} }
/** /**
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {AbstractItem|null} left * @param {Item|null} left
* @param {AbstractItem|null} right * @param {Item|null} right
* @param {Map<string,any>} currentAttributes * @param {Map<string,any>} currentAttributes
* @param {number} length * @param {number} length
* @return {ItemListPosition} * @return {ItemListPosition}
@@ -329,15 +327,14 @@ const formatText = (transaction, parent, left, right, currentAttributes, length,
const deleteText = (transaction, left, right, currentAttributes, length) => { const deleteText = (transaction, left, right, currentAttributes, length) => {
while (length > 0 && right !== null) { while (length > 0 && right !== null) {
if (right.deleted === false) { if (right.deleted === false) {
switch (right.constructor) { switch (right.content.constructor) {
case ItemFormat: case ContentFormat:
// @ts-ignore right is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (right.content))
updateCurrentAttributes(currentAttributes, right)
break break
case ItemEmbed: case ContentEmbed:
case ItemString: case ContentString:
if (length < right.length) { if (length < right.length) {
getItemCleanStart(transaction, transaction.y.store, createID(right.id.client, right.id.clock + length)) getItemCleanStart(transaction, transaction.doc.store, createID(right.id.client, right.id.clock + length))
} }
length -= right.length length -= right.length
right.delete(transaction) right.delete(transaction)
@@ -388,7 +385,7 @@ const deleteText = (transaction, left, right, currentAttributes, length) => {
/** /**
* Event that describes the changes on a YText type. * Event that describes the changes on a YText type.
*/ */
class YTextEvent extends YEvent { export class YTextEvent extends YEvent {
/** /**
* @param {YText} ytext * @param {YText} ytext
* @param {Transaction} transaction * @param {Transaction} transaction
@@ -411,13 +408,10 @@ class YTextEvent extends YEvent {
*/ */
get delta () { get delta () {
if (this._delta === null) { if (this._delta === null) {
const y = this.target._y const y = /** @type {Doc} */ (this.target.doc)
// @ts-ignore this._delta = []
transact(y, transaction => { transact(y, transaction => {
/** const delta = /** @type {Array<DeltaItem>} */ (this._delta)
* @type {Array<DeltaItem>}
*/
const delta = []
const currentAttributes = new Map() // saves all current attributes for insert const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map() const oldAttributes = new Map()
let item = this.target._start let item = this.target._start
@@ -432,7 +426,6 @@ class YTextEvent extends YEvent {
let insert = '' let insert = ''
let retain = 0 let retain = 0
let deleteLen = 0 let deleteLen = 0
this._delta = delta
const addOp = () => { const addOp = () => {
if (action !== null) { if (action !== null) {
/** /**
@@ -472,14 +465,15 @@ class YTextEvent extends YEvent {
} }
} }
while (item !== null) { while (item !== null) {
switch (item.constructor) { switch (item.content.constructor) {
case ItemEmbed: case ContentEmbed:
if (this.adds(item)) { if (this.adds(item)) {
addOp() if (!this.deletes(item)) {
action = 'insert' addOp()
// @ts-ignore item is ItemFormat action = 'insert'
insert = item.embed insert = /** @type {ContentEmbed} */ (item.content).embed
addOp() addOp()
}
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
if (action !== 'delete') { if (action !== 'delete') {
addOp() addOp()
@@ -494,14 +488,15 @@ class YTextEvent extends YEvent {
retain += 1 retain += 1
} }
break break
case ItemString: case ContentString:
if (this.adds(item)) { if (this.adds(item)) {
if (action !== 'insert') { if (!this.deletes(item)) {
addOp() if (action !== 'insert') {
action = 'insert' addOp()
action = 'insert'
}
insert += /** @type {ContentString} */ (item.content).str
} }
// @ts-ignore
insert += item.string
} else if (this.deletes(item)) { } else if (this.deletes(item)) {
if (action !== 'delete') { if (action !== 'delete') {
addOp() addOp()
@@ -516,57 +511,45 @@ class YTextEvent extends YEvent {
retain += item.length retain += item.length
} }
break break
case ItemFormat: case ContentFormat:
const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { if (this.adds(item)) {
// @ts-ignore item is ItemFormat if (!this.deletes(item)) {
const curVal = currentAttributes.get(item.key) || null const curVal = currentAttributes.get(key) || null
// @ts-ignore item is ItemFormat if (curVal !== value) {
if (curVal !== item.value) {
if (action === 'retain') {
addOp()
}
// @ts-ignore item is ItemFormat
if (item.value === (oldAttributes.get(item.key) || null)) {
// @ts-ignore item is ItemFormat
delete attributes[item.key]
} else {
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value
}
} else {
item.delete(transaction)
}
} else if (this.deletes(item)) {
// @ts-ignore item is ItemFormat
oldAttributes.set(item.key, item.value)
// @ts-ignore item is ItemFormat
const curVal = currentAttributes.get(item.key) || null
// @ts-ignore item is ItemFormat
if (curVal !== item.value) {
if (action === 'retain') {
addOp()
}
// @ts-ignore item is ItemFormat
attributes[item.key] = curVal
}
} else if (!item.deleted) {
// @ts-ignore item is ItemFormat
oldAttributes.set(item.key, item.value)
// @ts-ignore item is ItemFormat
const attr = attributes[item.key]
if (attr !== undefined) {
// @ts-ignore item is ItemFormat
if (attr !== item.value) {
if (action === 'retain') { if (action === 'retain') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat if (value === (oldAttributes.get(key) || null)) {
if (item.value === null) { delete attributes[key]
// @ts-ignore item is ItemFormat
attributes[item.key] = item.value
} else { } else {
// @ts-ignore item is ItemFormat attributes[key] = value
delete attributes[item.key] }
} else {
item.delete(transaction)
}
}
} else if (this.deletes(item)) {
oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) || null
if (curVal !== value) {
if (action === 'retain') {
addOp()
}
attributes[key] = curVal
}
} else if (!item.deleted) {
oldAttributes.set(key, value)
const attr = attributes[key]
if (attr !== undefined) {
if (attr !== value) {
if (action === 'retain') {
addOp()
}
if (value === null) {
attributes[key] = value
} else {
delete attributes[key]
} }
} else { } else {
item.delete(transaction) item.delete(transaction)
@@ -577,26 +560,24 @@ class YTextEvent extends YEvent {
if (action === 'insert') { if (action === 'insert') {
addOp() addOp()
} }
// @ts-ignore item is ItemFormat updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
updateCurrentAttributes(currentAttributes, item)
} }
break break
} }
item = item.right item = item.right
} }
addOp() addOp()
while (this._delta.length > 0) { while (delta.length > 0) {
let lastOp = this._delta[this._delta.length - 1] let lastOp = delta[delta.length - 1]
if (lastOp.retain !== undefined && lastOp.attributes === undefined) { if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes // retain delta's if they don't assign attributes
this._delta.pop() delta.pop()
} else { } else {
break break
} }
} }
}) })
} }
// @ts-ignore _delta is defined above
return this._delta return this._delta
} }
} }
@@ -617,10 +598,11 @@ export class YText extends AbstractType {
constructor (string) { constructor (string) {
super() super()
/** /**
* @type {Array<string>?} * Array of pending operations on this type
* @type {Array<function():void>?}
* @private * @private
*/ */
this._prelimContent = string !== undefined ? [string] : [] this._pending = string !== undefined ? [() => this.insert(0, string)] : []
} }
get length () { get length () {
@@ -628,16 +610,23 @@ export class YText extends AbstractType {
} }
/** /**
* @param {Y} y * @param {Doc} y
* @param {ItemType} item * @param {Item} item
* *
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore this._prelimContent is still defined try {
this.insert(0, this._prelimContent.join('')) /** @type {Array<function>} */ (this._pending).forEach(f => f())
this._prelimContent = null } catch (e) {
console.error(e)
}
this._pending = null
}
_copy () {
return new YText()
} }
/** /**
@@ -652,10 +641,6 @@ export class YText extends AbstractType {
callTypeObservers(this, transaction, new YTextEvent(this, transaction)) callTypeObservers(this, transaction, new YTextEvent(this, transaction))
} }
toDom () {
return document.createTextNode(this.toString())
}
/** /**
* Returns the unformatted string representation of this YText type. * Returns the unformatted string representation of this YText type.
* *
@@ -664,53 +649,18 @@ export class YText extends AbstractType {
toString () { toString () {
let str = '' let str = ''
/** /**
* @type {AbstractItem|null} * @type {Item|null}
*/ */
let n = this._start let n = this._start
while (n !== null) { while (n !== null) {
if (!n.deleted && n.countable && n.constructor === ItemString) { if (!n.deleted && n.countable && n.content.constructor === ContentString) {
// @ts-ignore str += /** @type {ContentString} */ (n.content).str
str += n.string
} }
n = n.right n = n.right
} }
return str return str
} }
toDomString () {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = []
for (let nodeName in delta.attributes) {
const attrs = []
for (let key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += delta.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
})
}
/** /**
* Apply a {@link Delta} on this shared YText type. * Apply a {@link Delta} on this shared YText type.
* *
@@ -719,8 +669,8 @@ export class YText extends AbstractType {
* @public * @public
*/ */
applyDelta (delta) { applyDelta (delta) {
if (this._y !== null) { if (this.doc !== null) {
transact(this._y, transaction => { transact(this.doc, transaction => {
/** /**
* @type {ItemListPosition} * @type {ItemListPosition}
*/ */
@@ -729,7 +679,15 @@ export class YText extends AbstractType {
for (let i = 0; i < delta.length; i++) { for (let i = 0; i < delta.length; i++) {
const op = delta[i] const op = delta[i]
if (op.insert !== undefined) { if (op.insert !== undefined) {
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, op.insert, op.attributes || {}) // Quill assumes that the content starts with an empty paragraph.
// Yjs/Y.Text assumes that it starts empty. We always hide that
// there is a newline at the end of the content.
// If we omit this step, clients will see a different number of
// paragraphs, but nothing bad will happen.
const ins = (typeof op.insert === 'string' && i === delta.length - 1 && pos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
if (typeof ins !== 'string' || ins.length > 0) {
pos = insertText(transaction, this, pos.left, pos.right, currentAttributes, ins, op.attributes || {})
}
} else if (op.retain !== undefined) { } else if (op.retain !== undefined) {
pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {}) pos = formatText(transaction, this, pos.left, pos.right, currentAttributes, op.retain, op.attributes || {})
} else if (op.delete !== undefined) { } else if (op.delete !== undefined) {
@@ -737,6 +695,8 @@ export class YText extends AbstractType {
} }
} }
}) })
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(delta))
} }
} }
@@ -756,10 +716,6 @@ export class YText extends AbstractType {
const ops = [] const ops = []
const currentAttributes = new Map() const currentAttributes = new Map()
let str = '' let str = ''
/**
* @type {AbstractItem|null}
*/
// @ts-ignore
let n = this._start let n = this._start
function packStr () { function packStr () {
if (str.length > 0) { if (str.length > 0) {
@@ -786,8 +742,8 @@ export class YText extends AbstractType {
} }
while (n !== null) { while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) { if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.constructor) { switch (n.content.constructor) {
case ItemString: case ContentString:
const cur = currentAttributes.get('ychange') const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) { if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') { if (cur === undefined || cur.user !== n.id.client || cur.state !== 'removed') {
@@ -803,13 +759,17 @@ export class YText extends AbstractType {
packStr() packStr()
currentAttributes.delete('ychange') currentAttributes.delete('ychange')
} }
// @ts-ignore str += /** @type {ContentString} */ (n.content).str
str += n.string
break break
case ItemFormat: case ContentEmbed:
packStr() packStr()
// @ts-ignore ops.push({
updateCurrentAttributes(currentAttributes, n) insert: /** @type {ContentEmbed} */ (n.content).embed
})
break
case ContentFormat:
packStr()
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
break break
} }
} }
@@ -833,12 +793,14 @@ export class YText extends AbstractType {
if (text.length <= 0) { if (text.length <= 0) {
return return
} }
const y = this._y const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const {left, right, currentAttributes} = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, text, attributes) insertText(transaction, this, left, right, currentAttributes, text, attributes)
}) })
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
} }
} }
@@ -856,12 +818,14 @@ export class YText extends AbstractType {
if (embed.constructor !== Object) { if (embed.constructor !== Object) {
throw new Error('Embed must be an Object') throw new Error('Embed must be an Object')
} }
const y = this._y const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
insertText(transaction, this, left, right, currentAttributes, embed, attributes) insertText(transaction, this, left, right, currentAttributes, embed, attributes)
}) })
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes))
} }
} }
@@ -877,12 +841,14 @@ export class YText extends AbstractType {
if (length === 0) { if (length === 0) {
return return
} }
const y = this._y const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) const { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
deleteText(transaction, left, right, currentAttributes, length) deleteText(transaction, left, right, currentAttributes, length)
}) })
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
} }
} }
@@ -897,7 +863,7 @@ export class YText extends AbstractType {
* @public * @public
*/ */
format (index, length, attributes) { format (index, length, attributes) {
const y = this._y const y = this.doc
if (y !== null) { if (y !== null) {
transact(y, transaction => { transact(y, transaction => {
let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index) let { left, right, currentAttributes } = findPosition(transaction, y.store, this, index)
@@ -906,6 +872,8 @@ export class YText extends AbstractType {
} }
formatText(transaction, this, left, right, currentAttributes, length, attributes) formatText(transaction, this, left, right, currentAttributes, length, attributes)
}) })
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
} }
} }

View File

@@ -1,244 +1,19 @@
/**
* @module YXml
*/
import { import {
YXmlEvent, YXmlFragment,
AbstractType, transact,
typeArrayMap, typeMapDelete,
typeArrayForEach, typeMapSet,
typeMapGet, typeMapGet,
typeMapGetAll, typeMapGetAll,
typeArrayInsertGenerics, typeListForEach,
typeArrayDelete,
typeMapSet,
typeMapDelete,
YXmlElementRefID, YXmlElementRefID,
callTypeObservers, Snapshot, Doc, Item // eslint-disable-line
transact,
Y, Transaction, ItemType, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {ItemType | null}
*/
// @ts-ignore
this._currentNode = root._start
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
let n = this._currentNode
if (n !== null && (!this._firstCall || n.deleted || !this._filter(n.type))) { // if first call, we check if we can use the first item
do {
if (!n.deleted && (n.type.constructor === YXmlElement || n.type.constructor === YXmlFragment) && n.type._start !== null) {
// walk down in the tree
// @ts-ignore
n = n.type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
// @ts-ignore
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(n.type)))
}
this._firstCall = false
this._currentNode = n
if (n === null) {
// @ts-ignore return undefined if done=true (the expected result)
return { value: undefined, done: true }
}
// @ts-ignore
return { value: n.type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
}
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
toString () {
return this.toDomString()
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toDomString () {
return typeArrayMap(this, xml => xml.toDomString()).join('')
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDom (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeArrayForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
})
return fragment
}
}
/** /**
* 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}. * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
@@ -249,12 +24,7 @@ export class YXmlFragment extends AbstractType {
export class YXmlElement extends YXmlFragment { export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') { constructor (nodeName = 'UNDEFINED') {
super() super()
this.nodeName = nodeName.toUpperCase() this.nodeName = nodeName
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
/** /**
* @type {Map<string, any>|null} * @type {Map<string, any>|null}
* @private * @private
@@ -269,20 +39,16 @@ export class YXmlElement extends YXmlFragment {
* * This type is sent to other client * * This type is sent to other client
* * Observer functions are fired * * Observer functions are fired
* *
* @param {Y} y The Yjs instance * @param {Doc} y The Yjs instance
* @param {ItemType} item * @param {Item} item
* @private * @private
*/ */
_integrate (y, item) { _integrate (y, item) {
super._integrate(y, item) super._integrate(y, item)
// @ts-ignore ;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
this.insert(0, this._prelimContent)
this._prelimContent = null
// @ts-ignore
this._prelimAttrs.forEach((value, key) => {
this.setAttribute(key, value) this.setAttribute(key, value)
}) })
this._prelimContent = null this._prelimAttrs = null
} }
/** /**
@@ -295,20 +61,16 @@ export class YXmlElement extends YXmlFragment {
return new YXmlElement(this.nodeName) return new YXmlElement(this.nodeName)
} }
toString () {
return this.toDomString()
}
/** /**
* Returns the string representation of this YXmlElement. * Returns the XML serialization of this YXmlElement.
* The attributes are ordered by attribute-name, so you can easily use this * The attributes are ordered by attribute-name, so you can easily use this
* method to compare YXmlElements * method to compare YXmlElements
* *
* @return {String} The string representation of this type. * @return {string} The string representation of this type.
* *
* @public * @public
*/ */
toDomString () { toString () {
const attrs = this.getAttributes() const attrs = this.getAttributes()
const stringBuilder = [] const stringBuilder = []
const keys = [] const keys = []
@@ -323,7 +85,7 @@ export class YXmlElement extends YXmlFragment {
} }
const nodeName = this.nodeName.toLocaleLowerCase() const nodeName = this.nodeName.toLocaleLowerCase()
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : '' const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
return `<${nodeName}${attrsString}>${super.toDomString()}</${nodeName}>` return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
} }
/** /**
@@ -334,13 +96,12 @@ export class YXmlElement extends YXmlFragment {
* @public * @public
*/ */
removeAttribute (attributeName) { removeAttribute (attributeName) {
if (this._y !== null) { if (this.doc !== null) {
transact(this._y, transaction => { transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName) typeMapDelete(transaction, this, attributeName)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
this._prelimAttrs.delete(attributeName)
} }
} }
@@ -353,13 +114,12 @@ export class YXmlElement extends YXmlFragment {
* @public * @public
*/ */
setAttribute (attributeName, attributeValue) { setAttribute (attributeName, attributeValue) {
if (this._y !== null) { if (this.doc !== null) {
transact(this._y, transaction => { transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, attributeValue) typeMapSet(transaction, this, attributeName, attributeValue)
}) })
} else { } else {
// @ts-ignore /** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
this._prelimAttrs.set(attributeName, attributeValue)
} }
} }
@@ -373,8 +133,7 @@ export class YXmlElement extends YXmlFragment {
* @public * @public
*/ */
getAttribute (attributeName) { getAttribute (attributeName) {
// @ts-ignore return /** @type {any} */ (typeMapGet(this, attributeName))
return typeMapGet(this, attributeName)
} }
/** /**
@@ -389,44 +148,6 @@ export class YXmlElement extends YXmlFragment {
return typeMapGetAll(this) return typeMapGetAll(this)
} }
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this._y !== null) {
transact(this._y, transaction => {
typeArrayDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/** /**
* Creates a Dom Element that mirrors this YXmlElement. * Creates a Dom Element that mirrors this YXmlElement.
* *
@@ -442,14 +163,14 @@ export class YXmlElement extends YXmlFragment {
* *
* @public * @public
*/ */
toDom (_document = document, hooks = {}, binding) { toDOM (_document = document, hooks = {}, binding) {
const dom = _document.createElement(this.nodeName) const dom = _document.createElement(this.nodeName)
let attrs = this.getAttributes() let attrs = this.getAttributes()
for (let key in attrs) { for (let key in attrs) {
dom.setAttribute(key, attrs[key]) dom.setAttribute(key, attrs[key])
} }
typeArrayForEach(this, yxml => { typeListForEach(this, yxml => {
dom.appendChild(yxml.toDom(_document, hooks, binding)) dom.appendChild(yxml.toDOM(_document, hooks, binding))
}) })
if (binding !== undefined) { if (binding !== undefined) {
binding._createAssociation(dom, this) binding._createAssociation(dom, this)
@@ -480,11 +201,3 @@ export class YXmlElement extends YXmlFragment {
* @function * @function
*/ */
export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder)) export const readYXmlElement = decoder => new YXmlElement(decoding.readVarString(decoder))
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

335
src/types/YXmlFragment.js Normal file
View File

@@ -0,0 +1,335 @@
/**
* @module YXml
*/
import {
YXmlEvent,
YXmlElement,
AbstractType,
typeListMap,
typeListForEach,
typeListInsertGenerics,
typeListDelete,
typeListToArray,
YXmlFragmentRefID,
callTypeObservers,
transact,
Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, Snapshot // eslint-disable-line
} from '../internals.js'
import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' // eslint-disable-line
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {IterableIterator}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {Item}
*/
this._currentNode = /** @type {Item} */ (root._start)
this._firstCall = true
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
/**
* @type {Item|null}
*/
let n = this._currentNode
let type = /** @type {ContentType} */ (n.content).type
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
do {
type = /** @type {ContentType} */ (n.content).type
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
// walk down in the tree
n = type._start
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
n = n.right
break
} else if (n.parent === this._root) {
n = null
} else {
n = n.parent._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type)))
}
this._firstCall = false
if (n === null) {
// @ts-ignore
return { value: undefined, done: true }
}
this._currentNode = n
return { value: /** @type {any} */ (n.content).type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
*/
export class YXmlFragment extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>|null}
* @private
*/
this._prelimContent = []
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @private
*/
_integrate (y, item) {
super._integrate(y, item)
this.insert(0, /** @type {Array} */ (this._prelimContent))
this._prelimContent = null
}
_copy () {
return new YXmlFragment()
}
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return Array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
}
/**
* Creates YXmlEvent and calls observers.
* @private
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toString () {
return typeListMap(this, xml => xml.toString()).join('')
}
toJSON () {
return this.toString()
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeListForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
})
return fragment
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @private
* @param {encoding.Encoder} encoder The encoder to write data to.
*/
_write (encoder) {
encoding.writeVarUint(encoder, YXmlFragmentRefID)
}
}
/**
* @param {decoding.Decoder} decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
export const readYXmlFragment = decoder => new YXmlFragment()

View File

@@ -47,7 +47,7 @@ export class YXmlHook extends YMap {
* *
* @public * @public
*/ */
toDom (_document = document, hooks = {}, binding) { toDOM (_document = document, hooks = {}, binding) {
const hook = hooks[this.hookName] const hook = hooks[this.hookName]
let dom let dom
if (hook !== undefined) { if (hook !== undefined) {

View File

@@ -9,6 +9,9 @@ import * as decoding from 'lib0/decoding.js' // eslint-disable-line
* simple formatting information like bold and italic. * simple formatting information like bold and italic.
*/ */
export class YXmlText extends YText { export class YXmlText extends YText {
_copy () {
return new YXmlText()
}
/** /**
* Creates a Dom Element that mirrors this YXmlText. * Creates a Dom Element that mirrors this YXmlText.
* *
@@ -24,13 +27,52 @@ export class YXmlText extends YText {
* *
* @public * @public
*/ */
toDom (_document = document, hooks, binding) { toDOM (_document = document, hooks, binding) {
const dom = _document.createTextNode(this.toString()) const dom = _document.createTextNode(this.toString())
if (binding !== undefined) { if (binding !== undefined) {
binding._createAssociation(dom, this) binding._createAssociation(dom, this)
} }
return dom return dom
} }
toString () {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = []
for (let nodeName in delta.attributes) {
const attrs = []
for (let key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[i]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += delta.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
}).join('')
}
toJSON () {
return this.toString()
}
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* *

View File

@@ -3,7 +3,9 @@ import {
findIndexSS, findIndexSS,
createID, createID,
getState, getState,
AbstractItem, StructStore, Transaction, ID // eslint-disable-line splitItem,
iterateStructs,
Item, GC, StructStore, Transaction, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
@@ -11,10 +13,7 @@ import * as map from 'lib0/map.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
/** export class DeleteItem {
* @private
*/
class DeleteItem {
/** /**
* @param {number} clock * @param {number} clock
* @param {number} len * @param {number} len
@@ -37,8 +36,6 @@ class DeleteItem {
* - This DeleteSet is send to other clients * - This DeleteSet is send to other clients
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore * - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged. * - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
*
* @private
*/ */
export class DeleteSet { export class DeleteSet {
constructor () { constructor () {
@@ -50,6 +47,25 @@ export class DeleteSet {
} }
} }
/**
* Iterate over all structs that the DeleteSet gc's.
*
* @param {Transaction} transaction
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(GC|Item):void} f
*
* @function
*/
export const iterateDeletedStructs = (transaction, ds, store, f) =>
ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(clientid))
for (let i = 0; i < deletes.length; i++) {
const del = deletes[i]
iterateStructs(transaction, structs, del.clock, del.len, f)
}
})
/** /**
* @param {Array<DeleteItem>} dis * @param {Array<DeleteItem>} dis
* @param {number} clock * @param {number} clock
@@ -120,6 +136,27 @@ export const sortAndMergeDeleteSet = ds => {
}) })
} }
/**
* @param {DeleteSet} ds1
* @param {DeleteSet} ds2
* @return {DeleteSet} A fresh DeleteSet
*/
export const mergeDeleteSets = (ds1, ds2) => {
const merged = new DeleteSet()
// Write all keys from ds1 to merged. If ds2 has the same key, combine the sets.
ds1.clients.forEach((dels1, client) =>
merged.clients.set(client, dels1.concat(ds2.clients.get(client) || []))
)
// Write all missing keys from ds2 to merged.
ds2.clients.forEach((dels2, client) => {
if (!merged.clients.has(client)) {
merged.clients.set(client, dels2)
}
})
sortAndMergeDeleteSet(merged)
return merged
}
/** /**
* @param {DeleteSet} ds * @param {DeleteSet} ds
* @param {ID} id * @param {ID} id
@@ -213,13 +250,13 @@ export const readDeleteSet = (decoder, transaction, store) => {
let index = findIndexSS(structs, clock) let index = findIndexSS(structs, clock)
/** /**
* We can ignore the case of GC and Delete structs, because we are going to skip them * We can ignore the case of GC and Delete structs, because we are going to skip them
* @type {AbstractItem} * @type {Item}
*/ */
// @ts-ignore // @ts-ignore
let struct = structs[index] let struct = structs[index]
// split the first item if necessary // split the first item if necessary
if (!struct.deleted && struct.id.clock < clock) { if (!struct.deleted && struct.id.clock < clock) {
structs.splice(index + 1, 0, struct.splitAt(transaction, clock - struct.id.clock)) structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
index++ // increase we now want to use the next struct index++ // increase we now want to use the next struct
} }
while (index < structs.length) { while (index < structs.length) {
@@ -228,7 +265,7 @@ export const readDeleteSet = (decoder, transaction, store) => {
if (struct.id.clock < clock + len) { if (struct.id.clock < clock + len) {
if (!struct.deleted) { if (!struct.deleted) {
if (clock + len < struct.id.clock + struct.length) { if (clock + len < struct.id.clock + struct.length) {
structs.splice(index, 0, struct.splitAt(transaction, clock + len - struct.id.clock)) structs.splice(index, 0, splitItem(transaction, struct, clock + len - struct.id.clock))
} }
struct.delete(transaction) struct.delete(transaction)
} }
@@ -244,6 +281,6 @@ export const readDeleteSet = (decoder, transaction, store) => {
if (unappliedDS.clients.size > 0) { if (unappliedDS.clients.size > 0) {
const unappliedDSEncoder = encoding.createEncoder() const unappliedDSEncoder = encoding.createEncoder()
writeDeleteSet(unappliedDSEncoder, unappliedDS) writeDeleteSet(unappliedDSEncoder, unappliedDS)
store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toBuffer(unappliedDSEncoder))) store.pendingDeleteReaders.push(decoding.createDecoder(encoding.toUint8Array(unappliedDSEncoder)))
} }
} }

View File

@@ -10,7 +10,7 @@ import {
YMap, YMap,
YXmlFragment, YXmlFragment,
transact, transact,
Transaction, YEvent // eslint-disable-line Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { Observable } from 'lib0/observable.js' import { Observable } from 'lib0/observable.js'
@@ -21,13 +21,13 @@ import * as map from 'lib0/map.js'
* A Yjs instance handles the state of shared data. * A Yjs instance handles the state of shared data.
* @extends Observable<string> * @extends Observable<string>
*/ */
export class Y extends Observable { export class Doc extends Observable {
/** /**
* @param {Object|undefined} conf configuration * @param {Object|undefined} conf configuration
*/ */
constructor (conf = {}) { constructor (conf = {}) {
super() super()
// todo: change to clientId this.gc = conf.gc || true
this.clientID = random.uint32() this.clientID = random.uint32()
/** /**
* @type {Map<string, AbstractType<YEvent>>} * @type {Map<string, AbstractType<YEvent>>}
@@ -39,6 +39,11 @@ export class Y extends Observable {
* @private * @private
*/ */
this._transaction = null this._transaction = null
/**
* @type {Array<Transaction>}
* @private
*/
this._transactionCleanups = []
} }
/** /**
* Changes that happen inside of a transaction are bundled. This means that * Changes that happen inside of a transaction are bundled. This means that
@@ -47,11 +52,12 @@ export class Y extends Observable {
* other peers. * other peers.
* *
* @param {function(Transaction):void} f The function that should be executed as a transaction * @param {function(Transaction):void} f The function that should be executed as a transaction
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
* *
* @public * @public
*/ */
transact (f) { transact (f, origin = null) {
transact(this, f) transact(this, f, origin)
} }
/** /**
* Define a shared data type. * Define a shared data type.
@@ -74,7 +80,7 @@ export class Y extends Observable {
* } * }
* *
* @param {string} name * @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition * @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 * @return {AbstractType<any>} The created type. Constructed with TypeConstructor
* *
* @public * @public
@@ -89,9 +95,18 @@ export class Y extends Observable {
const Constr = type.constructor const Constr = type.constructor
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
if (Constr === AbstractType) { if (Constr === AbstractType) {
const t = new Constr() // @ts-ignore
const t = new TypeConstructor()
t._map = type._map t._map = type._map
type._map.forEach(/** @param {Item?} n */ n => {
for (; n !== null; n = n.left) {
n.parent = t
}
})
t._start = type._start t._start = type._start
for (let n = t._start; n !== null; n = n.right) {
n.parent = t
}
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)

View File

@@ -1,5 +1,5 @@
import { AbstractType } from '../internals' // eslint-disable-line import { AbstractType } from '../internals.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -22,16 +22,6 @@ export class ID {
*/ */
this.clock = clock this.clock = clock
} }
/**
* @deprecated
* @todo remove and adapt relative position implementation
*/
toJSON () {
return {
client: this.client,
clock: this.clock
}
}
} }
/** /**
@@ -91,7 +81,7 @@ export const readID = decoder =>
*/ */
export const findRootTypeKey = type => { export const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case // @ts-ignore _y must be defined, otherwise unexpected case
for (let [key, value] of type._y.share) { for (let [key, value] of type.doc.share) {
if (value === type) { if (value === type) {
return key return key
} }

View File

@@ -1,18 +1,15 @@
/**
* @module Cursors
*/
import { import {
getItem, getItem,
getItemType,
createID, createID,
writeID, writeID,
readID, readID,
compareIDs, compareIDs,
getState, getState,
findRootTypeKey, findRootTypeKey,
AbstractItem, Item,
ID, StructStore, Y, AbstractType // eslint-disable-line ContentType,
ID, Doc, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
@@ -20,29 +17,30 @@ import * as decoding from 'lib0/decoding.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error.js'
/** /**
* A Cursor is a relative position that is based on the Yjs model. In contrast to an * A relative position is based on the Yjs model and is not affected by document changes.
* absolute position (position by index), the Cursor can be * E.g. If you place a relative position before a certain character, it will always point to this character.
* recomputed when remote changes are received. For example: * If you place a relative position at the end of a type, it will always point to the end of the type.
* *
* ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the cursor position. * A numeric position is often unsuited for user selections, because it does not change when content is inserted
* before or after.
* *
* A relative cursor position can be obtained with the function * ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position.
* *
* One of the properties must be defined. * One of the properties must be defined.
* *
* @example * @example
* // Current cursor position is at position 10 * // Current cursor position is at position 10
* const relativePosition = createCursorFromOffset(yText, 10) * const relativePosition = createRelativePositionFromIndex(yText, 10)
* // modify yText * // modify yText
* yText.insert(0, 'abc') * yText.insert(0, 'abc')
* yText.delete(3, 10) * yText.delete(3, 10)
* // Compute the cursor position * // Compute the cursor position
* const absolutePosition = toAbsolutePosition(y, relativePosition) * const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition)
* absolutePosition.type === yText // => true * absolutePosition.type === yText // => true
* console.log('cursor location is ' + absolutePosition.offset) // => cursor location is 3 * console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3
* *
*/ */
export class Cursor { export class RelativePosition {
/** /**
* @param {ID|null} type * @param {ID|null} type
* @param {string|null} tname * @param {string|null} tname
@@ -62,35 +60,22 @@ export class Cursor {
*/ */
this.item = item this.item = item
} }
toJSON () {
const json = {}
if (this.type !== null) {
json.type = this.type.toJSON()
}
if (this.tname !== null) {
json.tname = this.tname
}
if (this.item !== null) {
json.item = this.item.toJSON()
}
return json
}
} }
/** /**
* @param {Object} json * @param {Object} json
* @return {Cursor} * @return {RelativePosition}
* *
* @function * @function
*/ */
export const createCursorFromJSON = json => new Cursor(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock)) export const createRelativePositionFromJSON = json => new RelativePosition(json.type == null ? null : createID(json.type.client, json.type.clock), json.tname || null, json.item == null ? null : createID(json.item.client, json.item.clock))
export class AbsolutePosition { export class AbsolutePosition {
/** /**
* @param {AbstractType<any>} type * @param {AbstractType<any>} type
* @param {number} offset * @param {number} index
*/ */
constructor (type, offset) { constructor (type, index) {
/** /**
* @type {AbstractType<any>} * @type {AbstractType<any>}
*/ */
@@ -98,17 +83,17 @@ export class AbsolutePosition {
/** /**
* @type {number} * @type {number}
*/ */
this.offset = offset this.index = index
} }
} }
/** /**
* @param {AbstractType<any>} type * @param {AbstractType<any>} type
* @param {number} offset * @param {number} index
* *
* @function * @function
*/ */
export const createAbsolutePosition = (type, offset) => new AbsolutePosition(type, offset) export const createAbsolutePosition = (type, index) => new AbsolutePosition(type, index)
/** /**
* @param {AbstractType<any>} type * @param {AbstractType<any>} type
@@ -116,7 +101,7 @@ export const createAbsolutePosition = (type, offset) => new AbsolutePosition(typ
* *
* @function * @function
*/ */
export const createCursor = (type, item) => { export const createRelativePosition = (type, item) => {
let typeid = null let typeid = null
let tname = null let tname = null
if (type._item === null) { if (type._item === null) {
@@ -124,40 +109,40 @@ export const createCursor = (type, item) => {
} else { } else {
typeid = type._item.id typeid = type._item.id
} }
return new Cursor(typeid, tname, item) return new RelativePosition(typeid, tname, item)
} }
/** /**
* Create a relativePosition based on a absolute position. * Create a relativePosition based on a absolute position.
* *
* @param {AbstractType<any>} type The base type (e.g. YText or YArray). * @param {AbstractType<any>} type The base type (e.g. YText or YArray).
* @param {number} offset The absolute position. * @param {number} index The absolute position.
* @return {Cursor} * @return {RelativePosition}
* *
* @function * @function
*/ */
export const createCursorFromTypeOffset = (type, offset) => { export const createRelativePositionFromTypeIndex = (type, index) => {
let t = type._start let t = type._start
while (t !== null) { while (t !== null) {
if (!t.deleted && t.countable) { if (!t.deleted && t.countable) {
if (t.length > offset) { if (t.length > index) {
// case 1: found position somewhere in the linked list // case 1: found position somewhere in the linked list
return createCursor(type, createID(t.id.client, t.id.clock + offset)) return createRelativePosition(type, createID(t.id.client, t.id.clock + index))
} }
offset -= t.length index -= t.length
} }
t = t.right t = t.right
} }
return createCursor(type, null) return createRelativePosition(type, null)
} }
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {Cursor} rpos * @param {RelativePosition} rpos
* *
* @function * @function
*/ */
export const writeCursor = (encoder, rpos) => { export const writeRelativePosition = (encoder, rpos) => {
const { type, tname, item } = rpos const { type, tname, item } = rpos
if (item !== null) { if (item !== null) {
encoding.writeVarUint(encoder, 0) encoding.writeVarUint(encoder, 0)
@@ -176,15 +161,23 @@ export const writeCursor = (encoder, rpos) => {
return encoder return encoder
} }
/**
* @param {RelativePosition} rpos
* @return {Uint8Array}
*/
export const encodeRelativePosition = rpos => {
const encoder = encoding.createEncoder()
writeRelativePosition(encoder, rpos)
return encoding.toUint8Array(encoder)
}
/** /**
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {Y} y * @return {RelativePosition|null}
* @param {StructStore} store
* @return {Cursor|null}
* *
* @function * @function
*/ */
export const readCursor = (decoder, y, store) => { export const readRelativePosition = decoder => {
let type = null let type = null
let tname = null let tname = null
let itemID = null let itemID = null
@@ -202,65 +195,78 @@ export const readCursor = (decoder, y, store) => {
type = readID(decoder) type = readID(decoder)
} }
} }
return new Cursor(type, tname, itemID) return new RelativePosition(type, tname, itemID)
} }
/** /**
* @param {Cursor} cursor * @param {Uint8Array} uint8Array
* @param {Y} y * @return {RelativePosition|null}
*/
export const decodeRelativePosition = uint8Array => readRelativePosition(decoding.createDecoder(uint8Array))
/**
* @param {RelativePosition} rpos
* @param {Doc} doc
* @return {AbsolutePosition|null} * @return {AbsolutePosition|null}
* *
* @function * @function
*/ */
export const createAbsolutePositionFromCursor = (cursor, y) => { export const createAbsolutePositionFromRelativePosition = (rpos, doc) => {
const store = y.store const store = doc.store
const rightID = cursor.item const rightID = rpos.item
const typeID = cursor.type const typeID = rpos.type
const tname = cursor.tname const tname = rpos.tname
let type = null let type = null
let offset = 0 let index = 0
if (rightID !== null) { if (rightID !== null) {
if (getState(store, rightID.client) <= rightID.clock) { if (getState(store, rightID.client) <= rightID.clock) {
return null return null
} }
const right = getItem(store, rightID) const right = getItem(store, rightID)
if (!(right instanceof AbstractItem)) { if (!(right instanceof Item)) {
return null return null
} }
offset = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock index = right.deleted || !right.countable ? 0 : rightID.clock - right.id.clock
let n = right.left let n = right.left
while (n !== null) { while (n !== null) {
if (!n.deleted && n.countable) { if (!n.deleted && n.countable) {
offset += n.length index += n.length
} }
n = n.left n = n.left
} }
type = right.parent type = right.parent
} else { } else {
if (tname !== null) { if (tname !== null) {
type = y.get(tname) type = doc.get(tname)
} else if (typeID !== null) { } else if (typeID !== null) {
type = getItemType(store, typeID).type if (getState(store, typeID.client) <= typeID.clock) {
// type does not exist yet
return null
}
const struct = getItem(store, typeID)
if (struct instanceof Item && struct.content instanceof ContentType) {
type = struct.content.type
} else {
// struct is garbage collected
return null
}
} else { } else {
throw error.unexpectedCase() throw error.unexpectedCase()
} }
offset = type._length index = type._length
} }
if (type._item !== null && type._item.deleted) { if (type._item !== null && type._item.deleted) {
return null return null
} }
return createAbsolutePosition(type, offset) return createAbsolutePosition(type, index)
} }
/** /**
* @param {Cursor|null} a * @param {RelativePosition|null} a
* @param {Cursor|null} b * @param {RelativePosition|null} b
* *
* @function * @function
*/ */
export const compareCursors = (a, b) => a === b || ( export const compareRelativePositions = (a, b) => a === b || (
a !== null && b !== null && a.tname === b.tname && ( a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type)
(a.item !== null && b.item !== null && compareIDs(a.item, b.item)) ||
(a.type !== null && b.type !== null && compareIDs(a.type, b.type))
)
) )

View File

@@ -1,39 +1,37 @@
import { import {
DeleteSet,
isDeleted, isDeleted,
AbstractItem // eslint-disable-line DeleteSet, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
export class Snapshot { export class Snapshot {
/** /**
* @param {DeleteSet} ds delete store * @param {DeleteSet} ds
* @param {Map<number,number>} sm state map * @param {Map<number,number>} sm state map
* @param {Map<number,string>} userMap
* @private
*/ */
constructor (ds, sm, userMap) { constructor (ds, sm) {
/** /**
* @type {DeleteSet} * @type {DeleteSet}
* @private * @private
*/ */
this.ds = new DeleteSet() this.ds = ds
/** /**
* State Map * State Map
* @type {Map<number,number>} * @type {Map<number,number>}
* @private * @private
*/ */
this.sm = sm this.sm = sm
/**
* @type {Map<number,string>}
* @private
*/
this.userMap = userMap
} }
} }
/** /**
* @param {AbstractItem} item * @param {DeleteSet} ds
* @param {Map<number,number>} sm
*/
export const createSnapshot = (ds, sm) => new Snapshot(ds, sm)
/**
* @param {Item} item
* @param {Snapshot|undefined} snapshot * @param {Snapshot|undefined} snapshot
* *
* @protected * @protected

View File

@@ -1,18 +1,18 @@
import { import {
GC, GC,
Transaction, AbstractStructRef, ID, ItemType, AbstractItem, AbstractStruct // eslint-disable-line splitItem,
GCRef, ItemRef, Transaction, ID, Item // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as error from 'lib0/error.js' import * as error from 'lib0/error.js'
import * as encoding from 'lib0/encoding.js' import * as decoding from 'lib0/decoding.js' // eslint-disable-line
import * as decoding from 'lib0/decoding.js'
export class StructStore { export class StructStore {
constructor () { constructor () {
/** /**
* @type {Map<number,Array<AbstractStruct>>} * @type {Map<number,Array<GC|Item>>}
* @private * @private
*/ */
this.clients = new Map() this.clients = new Map()
@@ -22,14 +22,14 @@ export class StructStore {
* We could shift the array of refs instead, but shift is incredible * We could shift the array of refs instead, but shift is incredible
* slow in Chrome for arrays with more than 100k elements * slow in Chrome for arrays with more than 100k elements
* @see tryResumePendingStructRefs * @see tryResumePendingStructRefs
* @type {Map<number,{i:number,refs:Array<AbstractStructRef>}>} * @type {Map<number,{i:number,refs:Array<GCRef|ItemRef>}>}
* @private * @private
*/ */
this.pendingClientsStructRefs = new Map() this.pendingClientsStructRefs = new Map()
/** /**
* Stack of pending structs waiting for struct dependencies * Stack of pending structs waiting for struct dependencies
* Maximum length of stack is structReaders.size * Maximum length of stack is structReaders.size
* @type {Array<AbstractStructRef>} * @type {Array<GCRef|ItemRef>}
* @private * @private
*/ */
this.pendingStack = [] this.pendingStack = []
@@ -51,7 +51,7 @@ export class StructStore {
* @public * @public
* @function * @function
*/ */
export const getStates = store => { export const getStateVector = store => {
const sm = new Map() const sm = new Map()
store.clients.forEach((structs, client) => { store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1] const struct = structs[structs.length - 1]
@@ -97,7 +97,7 @@ export const integretyCheck = store => {
/** /**
* @param {StructStore} store * @param {StructStore} store
* @param {AbstractStruct} struct * @param {GC|Item} struct
* *
* @private * @private
* @function * @function
@@ -151,14 +151,14 @@ export const findIndexSS = (structs, clock) => {
* *
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {AbstractStruct} * @return {GC|Item}
* *
* @private * @private
* @function * @function
*/ */
export const find = (store, id) => { export const find = (store, id) => {
/** /**
* @type {Array<AbstractStruct>} * @type {Array<GC|Item>}
*/ */
// @ts-ignore // @ts-ignore
const structs = store.clients.get(id.client) const structs = store.clients.get(id.client)
@@ -170,7 +170,7 @@ export const find = (store, id) => {
* *
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {AbstractItem} * @return {Item}
* *
* @private * @private
* @function * @function
@@ -179,45 +179,18 @@ export const find = (store, id) => {
export const getItem = (store, id) => find(store, id) export const getItem = (store, id) => find(store, id)
/** /**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {StructStore} store
* @param {ID} id
* @return {ItemType}
*
* @private
* @function
*/
// @ts-ignore
export const getItemType = (store, id) => find(store, id)
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {Array<Item|GC>} structs
* @param {ID} id * @param {number} clock
* @return {AbstractItem}
*
* @private
* @function
*/ */
export const getItemCleanStart = (transaction, store, id) => { export const findIndexCleanStart = (transaction, structs, clock) => {
/** const index = findIndexSS(structs, clock)
* @type {Array<AbstractItem>}
*/
// @ts-ignore
const structs = store.clients.get(id.client)
const index = findIndexSS(structs, id.clock)
/**
* @type {AbstractItem}
*/
let struct = structs[index] let struct = structs[index]
if (struct.id.clock < id.clock && struct.constructor !== GC) { if (struct.id.clock < clock && struct instanceof Item) {
struct = struct.splitAt(transaction, id.clock - struct.id.clock) structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock))
structs.splice(index + 1, 0, struct) return index + 1
} }
return struct return index
} }
/** /**
@@ -226,21 +199,37 @@ export const getItemCleanStart = (transaction, store, id) => {
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {StructStore} store * @param {StructStore} store
* @param {ID} id * @param {ID} id
* @return {AbstractItem} * @return {Item}
*
* @private
* @function
*/
export const getItemCleanStart = (transaction, store, id) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(id.client))
return /** @type {Item} */ (structs[findIndexCleanStart(transaction, structs, id.clock)])
}
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @param {ID} id
* @return {Item}
* *
* @private * @private
* @function * @function
*/ */
export const getItemCleanEnd = (transaction, store, id) => { export const getItemCleanEnd = (transaction, store, id) => {
/** /**
* @type {Array<AbstractItem>} * @type {Array<Item>}
*/ */
// @ts-ignore // @ts-ignore
const structs = store.clients.get(id.client) const structs = store.clients.get(id.client)
const index = findIndexSS(structs, id.clock) const index = findIndexSS(structs, id.clock)
const struct = structs[index] const struct = structs[index]
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
structs.splice(index + 1, 0, struct.splitAt(transaction, id.clock - struct.id.clock + 1)) structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1))
} }
return struct return struct
} }
@@ -248,56 +237,40 @@ export const getItemCleanEnd = (transaction, store, id) => {
/** /**
* Replace `item` with `newitem` in store * Replace `item` with `newitem` in store
* @param {StructStore} store * @param {StructStore} store
* @param {AbstractStruct} struct * @param {GC|Item} struct
* @param {AbstractStruct} newStruct * @param {GC|Item} newStruct
* *
* @private * @private
* @function * @function
*/ */
export const replaceStruct = (store, struct, newStruct) => { export const replaceStruct = (store, struct, newStruct) => {
/** const structs = /** @type {Array<GC|Item>} */ (store.clients.get(struct.id.client))
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(struct.id.client)
structs[findIndexSS(structs, struct.id.clock)] = newStruct structs[findIndexSS(structs, struct.id.clock)] = newStruct
} }
/** /**
* Read StateMap from Decoder and return as Map * Iterate over a range of structs
* *
* @param {decoding.Decoder} decoder * @param {Transaction} transaction
* @return {Map<number,number>} * @param {Array<Item|GC>} structs
* @param {number} clockStart Inclusive start
* @param {number} len
* @param {function(GC|Item):void} f
* *
* @private
* @function * @function
*/ */
export const readStatesAsMap = decoder => { export const iterateStructs = (transaction, structs, clockStart, len, f) => {
const ss = new Map() if (len === 0) {
const ssLength = decoding.readVarUint(decoder) return
for (let i = 0; i < ssLength; i++) {
const client = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(client, clock)
} }
return ss const clockEnd = clockStart + len
} let index = findIndexCleanStart(transaction, structs, clockStart)
let struct
/** do {
* Write StateMap to Encoder struct = structs[index++]
* if (clockEnd < struct.id.clock + struct.length) {
* @param {encoding.Encoder} encoder findIndexCleanStart(transaction, structs, clockEnd)
* @param {StructStore} store }
* f(struct)
* @private } while (index < structs.length && structs[index].id.clock < clockEnd)
* @function
*/
export const writeStates = (encoder, store) => {
encoding.writeVarUint(encoder, store.clients.size)
store.clients.forEach((structs, client) => {
const id = structs[structs.length - 1].id
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock)
})
return encoder
} }

View File

@@ -6,17 +6,17 @@ import {
writeDeleteSet, writeDeleteSet,
DeleteSet, DeleteSet,
sortAndMergeDeleteSet, sortAndMergeDeleteSet,
getStates, getStateVector,
findIndexSS, findIndexSS,
callEventHandlerListeners, callEventHandlerListeners,
AbstractItem, Item,
ItemDeleted, ID, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
ID, AbstractType, AbstractStruct, YEvent, Y // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as map from 'lib0/map.js' import * as map from 'lib0/map.js'
import * as math from 'lib0/math.js' import * as math from 'lib0/math.js'
import * as set from 'lib0/set.js'
/** /**
* A transaction is created for every change on the Yjs model. It is possible * A transaction is created for every change on the Yjs model. It is possible
@@ -44,14 +44,15 @@ import * as math from 'lib0/math.js'
*/ */
export class Transaction { export class Transaction {
/** /**
* @param {Y} y * @param {Doc} doc
* @param {any} origin
*/ */
constructor (y) { constructor (doc, origin) {
/** /**
* The Yjs instance. * The Yjs instance.
* @type {Y} * @type {Doc}
*/ */
this.y = y this.doc = doc
/** /**
* Describes the set of deleted items by ids * Describes the set of deleted items by ids
* @type {DeleteSet} * @type {DeleteSet}
@@ -61,7 +62,7 @@ export class Transaction {
* Holds the state before the transaction started. * Holds the state before the transaction started.
* @type {Map<Number,Number>} * @type {Map<Number,Number>}
*/ */
this.beforeState = getStates(y.store) this.beforeState = getStateVector(doc.store)
/** /**
* Holds the state after the transaction. * Holds the state after the transaction.
* @type {Map<Number,Number>} * @type {Map<Number,Number>}
@@ -80,32 +81,30 @@ export class Transaction {
* @type {Map<AbstractType<YEvent>,Array<YEvent>>} * @type {Map<AbstractType<YEvent>,Array<YEvent>>}
*/ */
this.changedParentTypes = new Map() this.changedParentTypes = new Map()
/**
* @type {encoding.Encoder|null}
* @private
*/
this._updateMessage = null
/** /**
* @type {Set<ID>} * @type {Set<ID>}
* @private * @private
*/ */
this._mergeStructs = new Set() this._mergeStructs = new Set()
/**
* @type {any}
*/
this.origin = origin
} }
/** }
* @type {encoding.Encoder|null}
* @public /**
*/ * @param {Transaction} transaction
get updateMessage () { */
// only create if content was added in transaction (state or ds changed) export const computeUpdateMessageFromTransaction = transaction => {
if (this._updateMessage === null && (this.deleteSet.clients.size > 0 || map.any(this.afterState, (clock, client) => this.beforeState.get(client) !== clock))) { if (transaction.deleteSet.clients.size === 0 && !map.any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
const encoder = encoding.createEncoder() return null
sortAndMergeDeleteSet(this.deleteSet)
writeStructsFromTransaction(encoder, this)
writeDeleteSet(encoder, this.deleteSet)
this._updateMessage = encoder
}
return this._updateMessage
} }
const encoder = encoding.createEncoder()
sortAndMergeDeleteSet(transaction.deleteSet)
writeStructsFromTransaction(encoder, transaction)
writeDeleteSet(encoder, transaction.deleteSet)
return encoder
} }
/** /**
@@ -115,130 +114,178 @@ export class Transaction {
* @function * @function
*/ */
export const nextID = transaction => { export const nextID = transaction => {
const y = transaction.y const y = transaction.doc
return createID(y.clientID, getState(y.store, y.clientID)) return createID(y.clientID, getState(y.store, y.clientID))
} }
/**
* If `type.parent` was added in current transaction, `type` technically
* did not change, it was just added and we should not fire events for `type`.
*
* @param {Transaction} transaction
* @param {AbstractType<YEvent>} type
* @param {string|null} parentSub
*/
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {
const item = type._item
if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) {
map.setIfUndefined(transaction.changed, type, set.create).add(parentSub)
}
}
/** /**
* Implements the functionality of `y.transact(()=>{..})` * Implements the functionality of `y.transact(()=>{..})`
* *
* @param {Y} y * @param {Doc} doc
* @param {function(Transaction):void} f * @param {function(Transaction):void} f
* @param {any} [origin]
* *
* @private * @private
* @function * @function
*/ */
export const transact = (y, f) => { export const transact = (doc, f, origin = null) => {
const transactionCleanups = doc._transactionCleanups
let initialCall = false let initialCall = false
if (y._transaction === null) { if (doc._transaction === null) {
initialCall = true initialCall = true
y._transaction = new Transaction(y) doc._transaction = new Transaction(doc, origin)
y.emit('beforeTransaction', [y._transaction, y]) transactionCleanups.push(doc._transaction)
doc.emit('beforeTransaction', [doc._transaction, doc])
} }
const transaction = y._transaction
try { try {
f(transaction) f(doc._transaction)
} finally { } finally {
if (initialCall) { if (initialCall && transactionCleanups[0] === doc._transaction) {
y._transaction = null // The first transaction ended, now process observer calls.
y.emit('beforeObserverCalls', [transaction, y]) // Observer call may create new transactions for which we need to call the observers and do cleanup.
// emit change events on changed types // We don't want to nest these calls, so we execute these calls one after another
transaction.changed.forEach((subs, itemtype) => { for (let i = 0; i < transactionCleanups.length; i++) {
itemtype._callObserver(transaction, subs) const transaction = transactionCleanups[i]
}) const store = transaction.doc.store
transaction.changedParentTypes.forEach((events, type) => { const ds = transaction.deleteSet
events = events sortAndMergeDeleteSet(ds)
.filter(event => transaction.afterState = getStateVector(transaction.doc.store)
event.target._item === null || !event.target._item.deleted doc._transaction = null
) doc.emit('beforeObserverCalls', [transaction, doc])
events // emit change events on changed types
.forEach(event => { transaction.changed.forEach((subs, itemtype) => {
event.currentTarget = type if (itemtype._item === null || !itemtype._item.deleted) {
}) itemtype._callObserver(transaction, subs)
// we don't need to check for events.length }
// because we know it has at least one element })
callEventHandlerListeners(type._dEH, events, transaction) transaction.changedParentTypes.forEach((events, type) => {
}) // We need to think about the possibility that the user transforms the
// only call afterTransaction listeners if anything changed // Y.Doc in the event.
transaction.afterState = getStates(transaction.y.store) if (type._item === null || !type._item.deleted) {
// when all changes & events are processed, emit afterTransaction event events = events
// transaction cleanup .filter(event =>
const store = transaction.y.store event.target._item === null || !event.target._item.deleted
const ds = transaction.deleteSet )
// replace deleted items with ItemDeleted / GC events
sortAndMergeDeleteSet(ds) .forEach(event => {
y.emit('afterTransaction', [transaction, y]) event.currentTarget = type
for (const [client, deleteItems] of ds.clients) { })
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction)
}
})
doc.emit('afterTransaction', [transaction, doc])
/** /**
* @type {Array<AbstractStruct>} * @param {Array<AbstractStruct>} structs
* @param {number} pos
*/ */
// @ts-ignore const tryToMergeWithLeft = (structs, pos) => {
const structs = store.clients.get(client) const left = structs[pos - 1]
for (let di = 0; di < deleteItems.length; di++) { const right = structs[pos]
const deleteItem = deleteItems[di] if (left.deleted === right.deleted && left.constructor === right.constructor) {
for (let si = findIndexSS(structs, deleteItem.clock); si < structs.length; si++) { if (left.mergeWith(right)) {
const struct = structs[si] structs.splice(pos, 1)
if (deleteItem.clock + deleteItem.len <= struct.id.clock) { if (right instanceof Item && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) {
break right.parent._map.set(right.parentSub, /** @type {Item} */ (left))
} }
if (struct.deleted && struct instanceof AbstractItem && (struct.constructor !== ItemDeleted || (struct.parent._item !== null && struct.parent._item.deleted))) {
// check if we can GC
struct.gc(transaction, store)
} }
} }
} }
} // Replace deleted items with ItemDeleted / GC.
/** // This is where content is actually remove from the Yjs Doc.
* @param {Array<AbstractStruct>} structs if (doc.gc) {
* @param {number} pos for (const [client, deleteItems] of ds.clients) {
*/ const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const tryToMergeWithLeft = (structs, pos) => { for (let di = deleteItems.length - 1; di >= 0; di--) {
const left = structs[pos - 1] const deleteItem = deleteItems[di]
const right = structs[pos] const endDeleteItemClock = deleteItem.clock + deleteItem.len
if (left.deleted === right.deleted && left.constructor === right.constructor) { for (
if (left.mergeWith(right)) { let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
structs.splice(pos, 1) si < structs.length && struct.id.clock < endDeleteItemClock;
if (right instanceof AbstractItem && right.parentSub !== null && right.parent._map.get(right.parentSub) === right) { struct = structs[++si]
// @ts-ignore we already did a constructor check above ) {
right.parent._map.set(right.parentSub, left) const struct = structs[si]
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep) {
struct.gc(store, false)
}
}
} }
} }
} }
} // try to merge deleted / gc'd items
// on all affected store.clients props, try to merge // merge from right to left for better efficiecy and so we don't miss any merge targets
for (const [client, clock] of transaction.afterState) { for (const [client, deleteItems] of ds.clients) {
const beforeClock = transaction.beforeState.get(client) || 0 const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
if (beforeClock !== clock) { for (let di = deleteItems.length - 1; di >= 0; di--) {
/** const deleteItem = deleteItems[di]
* @type {Array<AbstractStruct>} // start with merging the item next to the last deleted item
*/ const mostRightIndexToCheck = math.min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1))
// @ts-ignore for (
const structs = store.clients.get(client) let si = mostRightIndexToCheck, struct = structs[si];
// we iterate from right to left so we can safely remove entries si > 0 && struct.id.clock >= deleteItem.clock;
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1) struct = structs[--si]
for (let i = structs.length - 1; i >= firstChangePos; i--) { ) {
tryToMergeWithLeft(structs, i) tryToMergeWithLeft(structs, si)
}
}
}
// on all affected store.clients props, try to merge
for (const [client, clock] of transaction.afterState) {
const beforeClock = transaction.beforeState.get(client) || 0
if (beforeClock !== clock) {
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
// we iterate from right to left so we can safely remove entries
const firstChangePos = math.max(findIndexSS(structs, beforeClock), 1)
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i)
}
}
}
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
const structs = /** @type {Array<AbstractStruct>} */ (store.clients.get(client))
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc])
if (doc._observers.has('update')) {
const updateMessage = computeUpdateMessageFromTransaction(transaction)
if (updateMessage !== null) {
doc.emit('update', [encoding.toUint8Array(updateMessage), transaction.origin, doc])
} }
} }
} }
// try to merge mergeStructs doc._transactionCleanups = []
for (const mid of transaction._mergeStructs) {
const client = mid.client
const clock = mid.clock
/**
* @type {Array<AbstractStruct>}
*/
// @ts-ignore
const structs = store.clients.get(client)
const replacedStructPos = findIndexSS(structs, clock)
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1)
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos)
}
}
y.emit('afterTransactionCleanup', [transaction, y])
} }
} }
} }

View File

@@ -1,202 +1,207 @@
// @ts-nocheck
import { import {
mergeDeleteSets,
iterateDeletedStructs,
keepItem,
transact,
redoItem,
iterateStructs,
isParentOf, isParentOf,
createID, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
transact
} from '../internals.js' } from '../internals.js'
/** import * as time from 'lib0/time.js'
* @private import { Observable } from 'lib0/observable'
*/
class ReverseOperation { class StackItem {
constructor (y, transaction, bindingInfos) { /**
this.created = new Date() * @param {DeleteSet} ds
const beforeState = transaction.beforeState * @param {number} start clock start of the local client
if (beforeState.has(y.userID)) { * @param {number} len
this.toState = createID(y.userID, y.ss.getState(y.userID) - 1) */
this.fromState = createID(y.userID, beforeState.get(y.userID)) constructor (ds, start, len) {
} else { this.ds = ds
this.toState = null this.start = start
this.fromState = null this.len = len
}
this.deletedStructs = new Set()
transaction.deletedStructs.forEach(struct => {
this.deletedStructs.add({
from: struct._id,
len: struct._length
})
})
/** /**
* Maps from binding to binding information (e.g. cursor information) * Use this to save and restore metadata like selection range
*/ */
this.bindingInfos = bindingInfos this.meta = new Map()
} }
} }
/** /**
* @private * @param {UndoManager} undoManager
* @function * @param {Array<StackItem>} stack
* @param {string} eventType
* @return {StackItem?}
*/ */
function applyReverseOperation (y, scope, reverseBuffer) { const popStackItem = (undoManager, stack, eventType) => {
let performedUndo = false
let undoOp = null
transact(y, () => {
while (!performedUndo && reverseBuffer.length > 0) {
undoOp = reverseBuffer.pop()
// make sure that it is possible to iterate {from}-{to}
if (undoOp.fromState !== null) {
y.os.getItemCleanStart(undoOp.fromState)
y.os.getItemCleanEnd(undoOp.toState)
y.os.iterate(undoOp.fromState, undoOp.toState, op => {
while (op._deleted && op._redone !== null) {
op = op._redone
}
if (op._deleted === false && isParentOf(scope, op)) {
performedUndo = true
op._delete(y)
}
})
}
const redoitems = new Set()
for (let del of undoOp.deletedStructs) {
const fromState = del.from
const toState = createID(fromState.user, fromState.clock + del.len - 1)
y.os.getItemCleanStart(fromState)
y.os.getItemCleanEnd(toState)
y.os.iterate(fromState, toState, op => {
if (
isParentOf(scope, op) &&
op._parent !== y &&
(
op._id.user !== y.userID ||
undoOp.fromState === null ||
op._id.clock < undoOp.fromState.clock ||
op._id.clock > undoOp.toState.clock
)
) {
redoitems.add(op)
}
})
}
redoitems.forEach(op => {
const opUndone = op._redo(y, redoitems)
performedUndo = performedUndo || opUndone
})
}
})
if (performedUndo && undoOp !== null) {
// should be performed after the undo transaction
undoOp.bindingInfos.forEach((info, binding) => {
binding._restoreUndoStackInfo(info)
})
}
return performedUndo
}
/**
* Saves a history of locally applied operations. The UndoManager handles the
* undoing and redoing of locally created changes.
*
* @private
* @function
*/
export class UndoManager {
/** /**
* @param {YType} scope The scope on which to listen for changes. * Whether a change happened
* @param {Object} options Optionally provided configuration. * @type {StackItem?}
*/ */
constructor (scope, options = {}) { let result = null
this.options = options const doc = undoManager.doc
this._bindings = new Set(options.bindings) const type = undoManager.type
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout transact(doc, transaction => {
this._undoBuffer = [] while (stack.length > 0 && result === null) {
this._redoBuffer = [] const store = doc.store
this._scope = scope const stackItem = /** @type {StackItem} */ (stack.pop())
this._undoing = false const itemsToRedo = new Set()
this._redoing = false let performedChange = false
this._lastTransactionWasUndo = false iterateDeletedStructs(transaction, stackItem.ds, store, struct => {
const y = scope._y if (struct instanceof Item && isParentOf(type, struct)) {
this.y = y itemsToRedo.add(struct)
let bindingInfos
y.on('beforeTransaction', (y, transaction, remote) => {
if (!remote) {
// Store binding information before transaction is executed
// By restoring the binding information, we can make sure that the state
// before the transaction can be recovered
bindingInfos = new Map()
this._bindings.forEach(binding => {
bindingInfos.set(binding, binding._getUndoStackInfo())
})
}
})
y.on('afterTransaction', (y, transaction, remote) => {
if (!remote && transaction.changedParentTypes.has(scope)) {
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
if (!this._undoing) {
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
if (
this._redoing === false &&
this._lastTransactionWasUndo === false &&
lastUndoOp !== null &&
((options.captureTimeout < 0) || (reverseOperation.created.getTime() - lastUndoOp.created.getTime()) <= options.captureTimeout)
) {
lastUndoOp.created = reverseOperation.created
if (reverseOperation.toState !== null) {
lastUndoOp.toState = reverseOperation.toState
if (lastUndoOp.fromState === null) {
lastUndoOp.fromState = reverseOperation.fromState
}
}
reverseOperation.deletedStructs.forEach(lastUndoOp.deletedStructs.add, lastUndoOp.deletedStructs)
} else {
this._lastTransactionWasUndo = false
this._undoBuffer.push(reverseOperation)
}
if (!this._redoing) {
this._redoBuffer = []
}
} else {
this._lastTransactionWasUndo = true
this._redoBuffer.push(reverseOperation)
} }
})
itemsToRedo.forEach(item => {
performedChange = redoItem(transaction, item, itemsToRedo) !== null || performedChange
})
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(doc.clientID))
iterateStructs(transaction, structs, stackItem.start, stackItem.len, struct => {
if (!struct.deleted && isParentOf(type, /** @type {Item} */ (struct))) {
struct.delete(transaction)
performedChange = true
}
})
result = stackItem
}
}, undoManager)
if (result != null) {
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType }, undoManager])
}
return result
}
/**
* 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
* metadata property on `event.stackItem.metadata` (it is a `Map` of metadata properties).
* 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.metadata`.
*
* @extends {Observable<'stack-item-added'|'stack-item-popped'>}
*/
export class UndoManager extends Observable {
/**
* @param {AbstractType<any>} type
* @param {Set<any>} [trackedTransactionOrigins=new Set([null])]
* @param {object} [options={captureTimeout=500}]
*/
constructor (type, trackedTransactionOrigins = new Set([null]), { captureTimeout = 500 } = {}) {
super()
this.type = type
trackedTransactionOrigins.add(this)
this.trackedTransactionOrigins = trackedTransactionOrigins
/**
* @type {Array<StackItem>}
*/
this.undoStack = []
/**
* @type {Array<StackItem>}
*/
this.redoStack = []
/**
* Whether the client is currently undoing (calling UndoManager.undo)
*
* @type {boolean}
*/
this.undoing = false
this.redoing = false
this.doc = /** @type {Doc} */ (type.doc)
this.lastChange = 0
type.observeDeep((events, transaction) => {
// Only track certain transactions
if (!this.trackedTransactionOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedTransactionOrigins.has(transaction.origin.constructor))) {
return
} }
const undoing = this.undoing
const redoing = this.redoing
const stack = undoing ? this.redoStack : this.undoStack
if (undoing) {
this.stopCapturing() // next undo should not be appended to last stack item
} else if (!redoing) {
// neither undoing nor redoing: delete redoStack
this.redoStack = []
}
const beforeState = transaction.beforeState.get(this.doc.clientID) || 0
const afterState = transaction.afterState.get(this.doc.clientID) || 0
const now = time.getUnixTime()
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op
const lastOp = stack[stack.length - 1]
lastOp.ds = mergeDeleteSets(lastOp.ds, transaction.deleteSet)
lastOp.len = afterState - lastOp.start
} else {
// create a new stack op
stack.push(new StackItem(transaction.deleteSet, beforeState, afterState - beforeState))
}
if (!undoing && !redoing) {
this.lastChange = now
}
// make sure that deleted structs are not gc'd
iterateDeletedStructs(transaction, transaction.deleteSet, transaction.doc.store, /** @param {Item|GC} item */ item => {
if (item instanceof Item && isParentOf(type, item)) {
keepItem(item)
}
})
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo' }, this])
}) })
} }
/** /**
* Enforce that the next change is created as a separate item in the undo stack * UndoManager merges Undo-StackItem if they are created within time-gap
* smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next
* StackItem won't be merged.
*
*
* @example
* // without stopCapturing
* ytext.insert(0, 'a')
* ytext.insert(1, 'b')
* um.undo()
* ytext.toString() // => '' (note that 'ab' was removed)
* // with stopCapturing
* ytext.insert(0, 'a')
* um.stopCapturing()
* ytext.insert(0, 'b')
* um.undo()
* ytext.toString() // => 'a' (note that only 'b' was removed)
* *
* @private
* @function
*/ */
flushChanges () { stopCapturing () {
this._lastTransactionWasUndo = true this.lastChange = 0
} }
/** /**
* Undo the last locally created change. * Undo last changes on type.
* *
* @private * @return {StackItem?} Returns StackItem if a change was applied
* @function
*/ */
undo () { undo () {
this._undoing = true this.undoing = true
const performedUndo = applyReverseOperation(this.y, this._scope, this._undoBuffer) let res
this._undoing = false try {
return performedUndo res = popStackItem(this, this.undoStack, 'undo')
} finally {
this.undoing = false
}
return res
} }
/** /**
* Redo the last locally created change. * Redo last undo operation.
* *
* @private * @return {StackItem?} Returns StackItem if a change was applied
* @function
*/ */
redo () { redo () {
this._redoing = true this.redoing = true
const performedRedo = applyReverseOperation(this.y, this._scope, this._redoBuffer) let res
this._redoing = false try {
return performedRedo res = popStackItem(this, this.redoStack, 'redo')
} finally {
this.redoing = false
}
return res
} }
} }

View File

@@ -1,47 +1,38 @@
/** /**
* @module encoding * @module encoding
*
* We use the first five bits in the info flag for determining the type of the struct.
*
* 0: GC
* 1: Item with Deleted content
* 2: Item with JSON content
* 3: Item with Binary content
* 4: Item with String content
* 5: Item with Embed content (for richtext content)
* 6: Item with Format content (a formatting marker for richtext content)
* 7: Item with Type
*/ */
import { import {
findIndexSS, findIndexSS,
GCRef, GCRef,
ItemBinaryRef, ItemRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef,
writeID, writeID,
createID, createID,
readID, readID,
getState, getState,
getStates, getStateVector,
readDeleteSet, readDeleteSet,
writeDeleteSet, writeDeleteSet,
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
Transaction, AbstractStruct, AbstractStructRef, StructStore, ID // eslint-disable-line Doc, Transaction, AbstractStruct, StructStore, ID // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import * as binary from 'lib0/binary.js' import * as binary from 'lib0/binary.js'
/**
* @private
*/
export const structRefs = [
GCRef,
ItemBinaryRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef
]
/** /**
* @param {encoding.Encoder} encoder * @param {encoding.Encoder} encoder
* @param {Array<AbstractStruct>} structs All structs by `client` * @param {Array<AbstractStruct>} structs All structs by `client`
@@ -68,19 +59,19 @@ const writeStructs = (encoder, structs, client, clock) => {
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {number} numOfStructs * @param {number} numOfStructs
* @param {ID} nextID * @param {ID} nextID
* @return {Array<AbstractStructRef>} * @return {Array<GCRef|ItemRef>}
* *
* @private * @private
* @function * @function
*/ */
const readStructRefs = (decoder, numOfStructs, nextID) => { const readStructRefs = (decoder, numOfStructs, nextID) => {
/** /**
* @type {Array<AbstractStructRef>} * @type {Array<GCRef|ItemRef>}
*/ */
const refs = [] const refs = []
for (let i = 0; i < numOfStructs; i++) { for (let i = 0; i < numOfStructs; i++) {
const info = decoding.readUint8(decoder) const info = decoding.readUint8(decoder)
const ref = new structRefs[binary.BITS5 & info](decoder, nextID, info) const ref = (binary.BITS5 & info) === 0 ? new GCRef(decoder, nextID, info) : new ItemRef(decoder, nextID, info)
nextID = createID(nextID.client, nextID.clock + ref.length) nextID = createID(nextID.client, nextID.clock + ref.length)
refs.push(ref) refs.push(ref)
} }
@@ -104,7 +95,7 @@ export const writeClientsStructs = (encoder, store, _sm) => {
sm.set(client, clock) sm.set(client, clock)
} }
}) })
getStates(store).forEach((clock, client) => { getStateVector(store).forEach((clock, client) => {
if (!_sm.has(client)) { if (!_sm.has(client)) {
sm.set(client, 0) sm.set(client, 0)
} }
@@ -119,14 +110,14 @@ export const writeClientsStructs = (encoder, store, _sm) => {
/** /**
* @param {decoding.Decoder} decoder The decoder object to read data from. * @param {decoding.Decoder} decoder The decoder object to read data from.
* @return {Map<number,Array<AbstractStructRef>>} * @return {Map<number,Array<GCRef|ItemRef>>}
* *
* @private * @private
* @function * @function
*/ */
export const readClientsStructRefs = decoder => { export const readClientsStructRefs = decoder => {
/** /**
* @type {Map<number,Array<AbstractStructRef>>} * @type {Map<number,Array<GCRef|ItemRef>>}
*/ */
const clientRefs = new Map() const clientRefs = new Map()
const numOfStateUpdates = decoding.readVarUint(decoder) const numOfStateUpdates = decoding.readVarUint(decoder)
@@ -193,7 +184,7 @@ const resumeStructIntegration = (transaction, store) => {
structRefs.refs[structRefs.i] = ref structRefs.refs[structRefs.i] = ref
stack[stack.length - 1] = r stack[stack.length - 1] = r
// sort the set because this approach might bring the list out of order // sort the set because this approach might bring the list out of order
structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.client - r2.id.client) structRefs.refs = structRefs.refs.slice(structRefs.i).sort((r1, r2) => r1.id.clock - r2.id.clock)
structRefs.i = 0 structRefs.i = 0
continue continue
} }
@@ -250,11 +241,11 @@ export const tryResumePendingDeleteReaders = (transaction, store) => {
* @private * @private
* @function * @function
*/ */
export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.y.store, transaction.beforeState) export const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState)
/** /**
* @param {StructStore} store * @param {StructStore} store
* @param {Map<number, Array<AbstractStructRef>>} clientsStructsRefs * @param {Map<number, Array<GCRef|ItemRef>>} clientsStructsRefs
* *
* @private * @private
* @function * @function
@@ -297,25 +288,128 @@ export const readStructs = (decoder, transaction, store) => {
} }
/** /**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
*
* @param {decoding.Decoder} decoder * @param {decoding.Decoder} decoder
* @param {Transaction} transaction * @param {Doc} ydoc
* @param {StructStore} store * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* *
* @function * @function
*/ */
export const readModel = (decoder, transaction, store) => { export const readUpdate = (decoder, ydoc, transactionOrigin) =>
readStructs(decoder, transaction, store) ydoc.transact(transaction => {
readDeleteSet(decoder, transaction, store) readStructs(decoder, transaction, ydoc.store)
readDeleteSet(decoder, transaction, ydoc.store)
}, transactionOrigin)
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
export const applyUpdate = (ydoc, update, transactionOrigin) =>
readUpdate(decoding.createDecoder(update), ydoc, transactionOrigin)
/**
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
* only write the operations that are missing.
*
* @param {encoding.Encoder} encoder
* @param {Doc} doc
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
*
* @function
*/
export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
writeClientsStructs(encoder, doc.store, targetStateVector)
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store))
} }
/** /**
* @param {encoding.Encoder} encoder * Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* @param {StructStore} store * only write the operations that are missing.
* @param {Map<number,number>} [targetState] The state of the target that receives the update. Leave empty to write all known structs *
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @return {Uint8Array}
* *
* @function * @function
*/ */
export const writeModel = (encoder, store, targetState = new Map()) => { export const encodeStateAsUpdate = (doc, encodedTargetStateVector) => {
writeClientsStructs(encoder, store, targetState) const encoder = encoding.createEncoder()
writeDeleteSet(encoder, createDeleteSetFromStructStore(store)) const targetStateVector = encodedTargetStateVector == null ? new Map() : decodeStateVector(encodedTargetStateVector)
writeStateAsUpdate(encoder, doc, targetStateVector)
return encoding.toUint8Array(encoder)
}
/**
* Read state vector from Decoder and return as Map
*
* @param {decoding.Decoder} decoder
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
export const readStateVector = decoder => {
const ss = new Map()
const ssLength = decoding.readVarUint(decoder)
for (let i = 0; i < ssLength; i++) {
const client = decoding.readVarUint(decoder)
const clock = decoding.readVarUint(decoder)
ss.set(client, clock)
}
return ss
}
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
export const decodeStateVector = decodedState => readStateVector(decoding.createDecoder(decodedState))
/**
* Write State Vector to `lib0/encoding.js#Encoder`.
*
* @param {encoding.Encoder} encoder
* @param {Doc} doc
*
* @function
*/
export const writeDocumentStateVector = (encoder, doc) => {
encoding.writeVarUint(encoder, doc.store.clients.size)
doc.store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1]
const id = struct.id
encoding.writeVarUint(encoder, id.client)
encoding.writeVarUint(encoder, id.clock + struct.length)
})
return encoder
}
/**
* Encode State as Uint8Array.
*
* @param {Doc} doc
* @return {Uint8Array}
*
* @function
*/
export const encodeStateVector = doc => {
const encoder = encoding.createEncoder()
writeDocumentStateVector(encoder, doc)
return encoding.toUint8Array(encoder)
} }

View File

@@ -1,22 +1,22 @@
import { AbstractType } from '../internals.js' // eslint-disable-line import { AbstractType, Item } from '../internals.js' // eslint-disable-line
/** /**
* Check if `parent` is a parent of `child`. * Check if `parent` is a parent of `child`.
* *
* @param {AbstractType<any>} parent * @param {AbstractType<any>} parent
* @param {AbstractType<any>} child * @param {Item|null} child
* @return {Boolean} Whether `parent` is a parent of `child`. * @return {Boolean} Whether `parent` is a parent of `child`.
* *
* @private * @private
* @function * @function
*/ */
export const isParentOf = (parent, child) => { export const isParentOf = (parent, child) => {
while (child._item !== null) { while (child !== null) {
if (child === parent) { if (child.parent === parent) {
return true return true
} }
child = child._item.parent child = child.parent._item
} }
return false return false
} }

View File

@@ -1,36 +1,26 @@
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
import { import {
structRefs, contentRefs,
structGCRefNumber, readContentBinary,
structBinaryRefNumber, readContentDeleted,
structDeletedRefNumber, readContentString,
structEmbedRefNumber, readContentJSON,
structFormatRefNumber, readContentEmbed,
structJSONRefNumber, readContentType,
structStringRefNumber, readContentFormat
structTypeRefNumber,
GCRef,
ItemBinaryRef,
ItemDeletedRef,
ItemEmbedRef,
ItemFormatRef,
ItemJSONRef,
ItemStringRef,
ItemTypeRef
} from '../src/internals.js' } from '../src/internals.js'
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testStructReferences = tc => { export const testStructReferences = tc => {
t.assert(structRefs.length === 8) t.assert(contentRefs.length === 8)
t.assert(structRefs[structGCRefNumber] === GCRef) t.assert(contentRefs[1] === readContentDeleted)
t.assert(structRefs[structBinaryRefNumber] === ItemBinaryRef) t.assert(contentRefs[2] === readContentJSON)
t.assert(structRefs[structDeletedRefNumber] === ItemDeletedRef) t.assert(contentRefs[3] === readContentBinary)
t.assert(structRefs[structEmbedRefNumber] === ItemEmbedRef) t.assert(contentRefs[4] === readContentString)
t.assert(structRefs[structFormatRefNumber] === ItemFormatRef) t.assert(contentRefs[5] === readContentEmbed)
t.assert(structRefs[structJSONRefNumber] === ItemJSONRef) t.assert(contentRefs[6] === readContentFormat)
t.assert(structRefs[structStringRefNumber] === ItemStringRef) t.assert(contentRefs[7] === readContentType)
t.assert(structRefs[structTypeRefNumber] === ItemTypeRef)
} }

View File

@@ -4,6 +4,7 @@ import * as map from './y-map.tests.js'
import * as text from './y-text.tests.js' import * as text from './y-text.tests.js'
import * as xml from './y-xml.tests.js' import * as xml from './y-xml.tests.js'
import * as encoding from './encoding.tests.js' import * as encoding from './encoding.tests.js'
import * as undoredo from './undo-redo.tests.js'
import { runTests } from 'lib0/testing.js' import { runTests } from 'lib0/testing.js'
import { isBrowser, isNode } from 'lib0/environment.js' import { isBrowser, isNode } from 'lib0/environment.js'
@@ -13,7 +14,7 @@ if (isBrowser) {
log.createVConsole(document.body) log.createVConsole(document.body)
} }
runTests({ runTests({
map, array, text, xml, encoding map, array, text, xml, encoding, undoredo
}).then(success => { }).then(success => {
/* istanbul ignore next */ /* istanbul ignore next */
if (isNode) { if (isNode) {

View File

@@ -2,36 +2,21 @@ import * as Y from '../src/index.js'
import { import {
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
getStates, getStateVector,
AbstractItem, Item,
DeleteSet, StructStore // eslint-disable-line DeleteItem, DeleteSet, StructStore, Doc // eslint-disable-line
} from '../src/internals.js' } from '../src/internals.js'
import * as t from 'lib0/testing.js' import * as t from 'lib0/testing.js'
import * as prng from 'lib0/prng.js' import * as prng from 'lib0/prng.js'
import { createMutex } from 'lib0/mutex.js'
import * as encoding from 'lib0/encoding.js' import * as encoding from 'lib0/encoding.js'
import * as decoding from 'lib0/decoding.js' import * as decoding from 'lib0/decoding.js'
import * as syncProtocol from 'y-protocols/sync.js' import * as syncProtocol from 'y-protocols/sync.js'
export * from '../src/internals.js'
/**
* @param {TestYInstance} y
* @param {Y.Transaction} transaction
*/
const afterTransaction = (y, transaction) => {
y.mMux(() => {
const m = transaction.updateMessage
if (m !== null) {
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, m)
broadcastMessage(y, encoding.toBuffer(encoder))
}
})
}
/** /**
* @param {TestYInstance} y // publish message created by `y` to all other online clients * @param {TestYInstance} y // publish message created by `y` to all other online clients
* @param {ArrayBuffer} m * @param {Uint8Array} m
*/ */
const broadcastMessage = (y, m) => { const broadcastMessage = (y, m) => {
if (y.tc.onlineConns.has(y)) { if (y.tc.onlineConns.has(y)) {
@@ -43,7 +28,7 @@ const broadcastMessage = (y, m) => {
} }
} }
export class TestYInstance extends Y.Y { export class TestYInstance extends Doc {
/** /**
* @param {TestConnector} testConnector * @param {TestConnector} testConnector
* @param {number} clientID * @param {number} clientID
@@ -56,17 +41,18 @@ export class TestYInstance extends Y.Y {
*/ */
this.tc = testConnector this.tc = testConnector
/** /**
* @type {Map<TestYInstance, Array<ArrayBuffer>>} * @type {Map<TestYInstance, Array<Uint8Array>>}
*/ */
this.receiving = new Map() this.receiving = new Map()
/**
* Message mutex
* @type {Function}
*/
this.mMux = createMutex()
testConnector.allConns.add(this) testConnector.allConns.add(this)
// set up observe on local model // set up observe on local model
this.on('afterTransactionCleanup', afterTransaction) this.on('update', /** @param {Uint8Array} update @param {any} origin */ (update, origin) => {
if (origin !== testConnector) {
const encoder = encoding.createEncoder()
syncProtocol.writeUpdate(encoder, update)
broadcastMessage(this, encoding.toUint8Array(encoder))
}
})
this.connect() this.connect()
} }
/** /**
@@ -84,15 +70,15 @@ export class TestYInstance extends Y.Y {
if (!this.tc.onlineConns.has(this)) { if (!this.tc.onlineConns.has(this)) {
this.tc.onlineConns.add(this) this.tc.onlineConns.add(this)
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
syncProtocol.writeSyncStep1(encoder, this.store) syncProtocol.writeSyncStep1(encoder, this)
// publish SyncStep1 // publish SyncStep1
broadcastMessage(this, encoding.toBuffer(encoder)) broadcastMessage(this, encoding.toUint8Array(encoder))
this.tc.onlineConns.forEach(remoteYInstance => { this.tc.onlineConns.forEach(remoteYInstance => {
if (remoteYInstance !== this) { if (remoteYInstance !== this) {
// remote instance sends instance to this instance // remote instance sends instance to this instance
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
syncProtocol.writeSyncStep1(encoder, remoteYInstance.store) syncProtocol.writeSyncStep1(encoder, remoteYInstance)
this._receive(encoding.toBuffer(encoder), remoteYInstance) this._receive(encoding.toUint8Array(encoder), remoteYInstance)
} }
}) })
} }
@@ -101,7 +87,7 @@ export class TestYInstance extends Y.Y {
* Receive a message from another client. This message is only appended to the list of receiving messages. * Receive a message from another client. This message is only appended to the list of receiving messages.
* TestConnector decides when this client actually reads this message. * TestConnector decides when this client actually reads this message.
* *
* @param {ArrayBuffer} message * @param {Uint8Array} message
* @param {TestYInstance} remoteClient * @param {TestYInstance} remoteClient
*/ */
_receive (message, remoteClient) { _receive (message, remoteClient) {
@@ -165,14 +151,12 @@ export class TestConnector {
return this.flushRandomMessage() return this.flushRandomMessage()
} }
const encoder = encoding.createEncoder() const encoder = encoding.createEncoder()
receiver.mMux(() => { // console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver))
// console.log('receive (' + sender.userID + '->' + receiver.userID + '):\n', syncProtocol.stringifySyncMessage(decoding.createDecoder(m), receiver)) // do not publish data created when this function is executed (could be ss2 or update message)
// do not publish data created when this function is executed (could be ss2 or update message) syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver, receiver.tc)
syncProtocol.readSyncMessage(decoding.createDecoder(m), encoder, receiver)
})
if (encoding.length(encoder) > 0) { if (encoding.length(encoder) > 0) {
// send reply message // send reply message
sender._receive(encoding.toBuffer(encoder), receiver) sender._receive(encoding.toUint8Array(encoder), receiver)
} }
return true return true
} }
@@ -230,11 +214,13 @@ export class TestConnector {
} }
/** /**
* @template T
* @param {t.TestCase} tc * @param {t.TestCase} tc
* @param {{users?:number}} conf * @param {{users?:number}} conf
* @return {{testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}} * @param {InitTestObjectCallback<T>} [initTestObject]
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
*/ */
export const init = (tc, { users = 5 } = {}) => { export const init = (tc, { users = 5 } = {}, initTestObject) => {
/** /**
* @type {Object<string,any>} * @type {Object<string,any>}
*/ */
@@ -254,8 +240,8 @@ export const init = (tc, { users = 5 } = {}) => {
result['text' + i] = y.get('text', Y.Text) result['text' + i] = y.get('text', Y.Text)
} }
testConnector.syncAll() testConnector.syncAll()
// @ts-ignore result.testObjects = result.users.map(initTestObject || (() => null))
return result return /** @type {any} */ (result)
} }
/** /**
@@ -270,7 +256,7 @@ export const init = (tc, { users = 5 } = {}) => {
export const compare = users => { export const compare = users => {
users.forEach(u => u.connect()) users.forEach(u => u.connect())
while (users[0].tc.flushAllMessages()) {} while (users[0].tc.flushAllMessages()) {}
const userArrayValues = users.map(u => u.getArray('array').toJSON().map(val => JSON.stringify(val))) const userArrayValues = users.map(u => u.getArray('array').toJSON())
const userMapValues = users.map(u => u.getMap('map').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON())
const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
const userTextValues = users.map(u => u.getText('text').toDelta()) const userTextValues = users.map(u => u.getText('text').toDelta())
@@ -279,15 +265,29 @@ export const compare = users => {
t.assert(u.store.pendingStack.length === 0) t.assert(u.store.pendingStack.length === 0)
t.assert(u.store.pendingClientsStructRefs.size === 0) t.assert(u.store.pendingClientsStructRefs.size === 0)
} }
// Test Array iterator
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
// Test Map iterator
const ymapkeys = Array.from(users[0].getMap('map').keys())
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
ymapkeys.forEach(key => t.assert(userMapValues[0].hasOwnProperty(key)))
/**
* @type {Object<string,any>}
*/
const mapRes = {}
for (let [k, v] of users[0].getMap('map')) {
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
}
t.compare(userMapValues[0], mapRes)
// Compare all users
for (let i = 0; i < users.length - 1; i++) { for (let i = 0; i < users.length - 1; i++) {
t.compare(userArrayValues[i].length, users[i].getArray('array').length) t.compare(userArrayValues[i].length, users[i].getArray('array').length)
t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userArrayValues[i], userArrayValues[i + 1])
t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1])
// @ts-ignore t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
t.compare(userTextValues[i].map(a => a.insert).join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1]) t.compare(userTextValues[i], userTextValues[i + 1])
t.compare(getStates(users[i].store), getStates(users[i + 1].store)) t.compare(getStateVector(users[i].store), getStateVector(users[i + 1].store))
compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store)) compareDS(createDeleteSetFromStructStore(users[i].store), createDeleteSetFromStructStore(users[i + 1].store))
compareStructStores(users[i].store, users[i + 1].store) compareStructStores(users[i].store, users[i + 1].store)
} }
@@ -295,8 +295,8 @@ export const compare = users => {
} }
/** /**
* @param {AbstractItem?} a * @param {Item?} a
* @param {AbstractItem?} b * @param {Item?} b
* @return {boolean} * @return {boolean}
*/ */
export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id)) export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y.compareIDs(a.id, b.id))
@@ -308,11 +308,10 @@ export const compareItemIDs = (a, b) => a === b || (a !== null && b != null && Y
export const compareStructStores = (ss1, ss2) => { export const compareStructStores = (ss1, ss2) => {
t.assert(ss1.clients.size === ss2.clients.size) t.assert(ss1.clients.size === ss2.clients.size)
for (const [client, structs1] of ss1.clients) { for (const [client, structs1] of ss1.clients) {
const structs2 = ss2.clients.get(client) const structs2 = /** @type {Array<Y.AbstractStruct>} */ (ss2.clients.get(client))
t.assert(structs2 !== undefined && structs1.length === structs2.length) t.assert(structs2 !== undefined && structs1.length === structs2.length)
for (let i = 0; i < structs1.length; i++) { for (let i = 0; i < structs1.length; i++) {
const s1 = structs1[i] const s1 = structs1[i]
// @ts-ignore
const s2 = structs2[i] const s2 = structs2[i]
// checks for abstract struct // checks for abstract struct
if ( if (
@@ -323,9 +322,9 @@ export const compareStructStores = (ss1, ss2) => {
) { ) {
t.fail('Structs dont match') t.fail('Structs dont match')
} }
if (s1 instanceof AbstractItem) { if (s1 instanceof Item) {
if ( if (
!(s2 instanceof AbstractItem) || !(s2 instanceof Item) ||
!((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) || !((s1.left === null && s2.left === null) || (s1.left !== null && s2.left !== null && Y.compareIDs(s1.left.lastId, s2.left.lastId))) ||
!compareItemIDs(s1.right, s2.right) || !compareItemIDs(s1.right, s2.right) ||
!Y.compareIDs(s1.origin, s2.origin) || !Y.compareIDs(s1.origin, s2.origin) ||
@@ -351,11 +350,10 @@ export const compareStructStores = (ss1, ss2) => {
export const compareDS = (ds1, ds2) => { export const compareDS = (ds1, ds2) => {
t.assert(ds1.clients.size === ds2.clients.size) t.assert(ds1.clients.size === ds2.clients.size)
for (const [client, deleteItems1] of ds1.clients) { for (const [client, deleteItems1] of ds1.clients) {
const deleteItems2 = ds2.clients.get(client) const deleteItems2 = /** @type {Array<DeleteItem>} */ (ds2.clients.get(client))
t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length) t.assert(deleteItems2 !== undefined && deleteItems1.length === deleteItems2.length)
for (let i = 0; i < deleteItems1.length; i++) { for (let i = 0; i < deleteItems1.length; i++) {
const di1 = deleteItems1[i] const di1 = deleteItems1[i]
// @ts-ignore
const di2 = deleteItems2[i] const di2 = deleteItems2[i]
if (di1.clock !== di2.clock || di1.len !== di2.len) { if (di1.clock !== di2.clock || di1.len !== di2.len) {
t.fail('DeleteSets dont match') t.fail('DeleteSets dont match')
@@ -365,15 +363,24 @@ export const compareDS = (ds1, ds2) => {
} }
/** /**
* @param {t.TestCase} tc * @template T
* @param {Array<function(TestYInstance,prng.PRNG):void>} mods * @callback InitTestObjectCallback
* @param {number} iterations * @param {TestYInstance} y
* @return {T}
*/ */
export const applyRandomTests = (tc, mods, iterations) => {
/**
* @template T
* @param {t.TestCase} tc
* @param {Array<function(Y.Doc,prng.PRNG,T):void>} mods
* @param {number} iterations
* @param {InitTestObjectCallback<T>} [initTestObject]
*/
export const applyRandomTests = (tc, mods, iterations, initTestObject) => {
const gen = tc.prng const gen = tc.prng
const result = init(tc, { users: 5 }) const result = init(tc, { users: 5 }, initTestObject || (() => null))
const { testConnector, users } = result const { testConnector, users } = result
for (var i = 0; i < iterations; i++) { for (let i = 0; i < iterations; i++) {
if (prng.int31(gen, 0, 100) <= 2) { if (prng.int31(gen, 0, 100) <= 2) {
// 2% chance to disconnect/reconnect a random user // 2% chance to disconnect/reconnect a random user
if (prng.bool(gen)) { if (prng.bool(gen)) {
@@ -388,9 +395,9 @@ export const applyRandomTests = (tc, mods, iterations) => {
// 50% chance to flush a random message // 50% chance to flush a random message
testConnector.flushRandomMessage() testConnector.flushRandomMessage()
} }
let user = prng.oneOf(gen, users) const user = prng.int31(gen, 0, users.length - 1)
var test = prng.oneOf(gen, mods) const test = prng.oneOf(gen, mods)
test(user, gen) test(users[user], gen, result.testObjects[user])
} }
compare(users) compare(users)
return result return result

182
tests/undo-redo.tests.js Normal file
View File

@@ -0,0 +1,182 @@
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
import {
UndoManager
} from '../src/internals.js'
import * as Y from '../src/index.js'
import * as t from 'lib0/testing.js'
/**
* @param {t.TestCase} tc
*/
export const testUndoText = tc => {
const { testConnector, text0, text1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0)
text0.insert(0, 'abc')
text1.insert(0, 'xyz')
testConnector.syncAll()
undoManager.undo()
t.assert(text0.toString() === 'xyz')
undoManager.redo()
t.assert(text0.toString() === 'abcxyz')
testConnector.syncAll()
text1.delete(0, 1)
testConnector.syncAll()
undoManager.undo()
t.assert(text0.toString() === 'xyz')
undoManager.redo()
t.assert(text0.toString() === 'bcxyz')
// test marks
text0.format(1, 3, { bold: true })
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
undoManager.undo()
t.compare(text0.toDelta(), [{ insert: 'bcxyz' }])
undoManager.redo()
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }])
}
/**
* @param {t.TestCase} tc
*/
export const testUndoMap = tc => {
const { testConnector, map0, map1 } = init(tc, { users: 2 })
map0.set('a', 0)
const undoManager = new UndoManager(map0)
map0.set('a', 1)
undoManager.undo()
t.assert(map0.get('a') === 0)
undoManager.redo()
t.assert(map0.get('a') === 1)
// testing sub-types and if it can restore a whole type
const subType = new Y.Map()
map0.set('a', subType)
subType.set('x', 42)
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } }))
undoManager.undo()
t.assert(map0.get('a') === 1)
undoManager.redo()
t.compare(map0.toJSON(), /** @type {any} */ ({ 'a': { x: 42 } }))
testConnector.syncAll()
// if content is overwritten by another user, undo operations should be skipped
map1.set('a', 44)
testConnector.syncAll()
undoManager.undo()
t.assert(map0.get('a') === 44)
undoManager.redo()
t.assert(map0.get('a') === 44)
}
/**
* @param {t.TestCase} tc
*/
export const testUndoArray = tc => {
const { testConnector, array0, array1 } = init(tc, { users: 3 })
const undoManager = new UndoManager(array0)
array0.insert(0, [1, 2, 3])
array1.insert(0, [4, 5, 6])
testConnector.syncAll()
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
undoManager.undo()
t.compare(array0.toArray(), [4, 5, 6])
undoManager.redo()
t.compare(array0.toArray(), [1, 2, 3, 4, 5, 6])
testConnector.syncAll()
array1.delete(0, 1) // user1 deletes [1]
testConnector.syncAll()
undoManager.undo()
t.compare(array0.toArray(), [4, 5, 6])
undoManager.redo()
t.compare(array0.toArray(), [2, 3, 4, 5, 6])
array0.delete(0, 5)
// test nested structure
const ymap = new Y.Map()
array0.insert(0, [ymap])
t.compare(array0.toJSON(), [{}])
undoManager.stopCapturing()
ymap.set('a', 1)
t.compare(array0.toJSON(), [{ a: 1 }])
undoManager.undo()
t.compare(array0.toJSON(), [{}])
undoManager.undo()
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
undoManager.redo()
t.compare(array0.toJSON(), [{}])
undoManager.redo()
t.compare(array0.toJSON(), [{ a: 1 }])
testConnector.syncAll()
array1.get(0).set('b', 2)
testConnector.syncAll()
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
undoManager.undo()
t.compare(array0.toJSON(), [{ b: 2 }])
undoManager.undo()
t.compare(array0.toJSON(), [2, 3, 4, 5, 6])
undoManager.redo()
t.compare(array0.toJSON(), [{ b: 2 }])
undoManager.redo()
t.compare(array0.toJSON(), [{ a: 1, b: 2 }])
}
/**
* @param {t.TestCase} tc
*/
export const testUndoXml = tc => {
const { xml0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(xml0)
const child = new Y.XmlElement('p')
xml0.insert(0, [child])
const textchild = new Y.XmlText('content')
child.insert(0, [textchild])
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
// format textchild and revert that change
undoManager.stopCapturing()
textchild.format(3, 4, { bold: {} })
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
undoManager.undo()
t.assert(xml0.toString() === '<undefined><p>content</p></undefined>')
undoManager.redo()
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
xml0.delete(0, 1)
t.assert(xml0.toString() === '<undefined></undefined>')
undoManager.undo()
t.assert(xml0.toString() === '<undefined><p>con<bold>tent</bold></p></undefined>')
}
/**
* @param {t.TestCase} tc
*/
export const testUndoEvents = tc => {
const { text0 } = init(tc, { users: 3 })
const undoManager = new UndoManager(text0)
let counter = 0
let receivedMetadata = -1
undoManager.on('stack-item-added', /** @param {any} event */ event => {
t.assert(event.type != null)
event.stackItem.meta.set('test', counter++)
})
undoManager.on('stack-item-popped', /** @param {any} event */ event => {
t.assert(event.type != null)
receivedMetadata = event.stackItem.meta.get('test')
})
text0.insert(0, 'abc')
undoManager.undo()
t.assert(receivedMetadata === 0)
undoManager.redo()
t.assert(receivedMetadata === 1)
}
/**
* @param {t.TestCase} tc
*/
export const testTrackClass = tc => {
const { users, text0 } = init(tc, { users: 3 })
// only track origins that are numbers
const undoManager = new UndoManager(text0, new Set([Number]))
users[0].transact(() => {
text0.insert(0, 'abc')
}, 42)
t.assert(text0.toString() === 'abc')
undoManager.undo()
t.assert(text0.toString() === '')
}

View File

@@ -1,4 +1,4 @@
import { init, compare, applyRandomTests, TestYInstance } from './testHelper.js' // eslint-disable-line import { init, compare, applyRandomTests, Doc } 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.js' import * as t from 'lib0/testing.js'
@@ -25,10 +25,10 @@ export const testDeleteInsert = tc => {
*/ */
export const testInsertThreeElementsTryRegetProperty = tc => { export const testInsertThreeElementsTryRegetProperty = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, 2, 3]) array0.insert(0, [1, true, false])
t.compare(array0.toJSON(), [1, 2, 3], '.toJSON() works') t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
testConnector.flushAllMessages() testConnector.flushAllMessages()
t.compare(array1.toJSON(), [1, 2, 3], '.toJSON() works after sync') t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync')
compare(users) compare(users)
} }
@@ -144,6 +144,32 @@ export const testInsertAndDeleteEvents = tc => {
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testNestedObserverEvents = tc => {
const { array0, users } = init(tc, { users: 2 })
/**
* @type {Array<number>}
*/
const vals = []
array0.observe(e => {
if (array0.length === 1) {
// inserting, will call this observer again
// we expect that this observer is called after this event handler finishedn
array0.insert(1, [1])
vals.push(0)
} else {
// this should be called the second time an element is inserted (above case)
vals.push(1)
}
})
array0.insert(0, [0])
t.compareArrays(vals, [0, 1])
t.compareArrays(array0.toArray(), [0, 1])
compare(users)
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -184,6 +210,24 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
compare(users) compare(users)
} }
/**
* This issue has been reported here https://github.com/y-js/yjs/issues/155
* @param {t.TestCase} tc
*/
export const testNewChildDoesNotEmitEventInTransaction = tc => {
const { array0, users } = init(tc, { users: 2 })
let fired = false
users[0].transact(() => {
const newMap = new Y.Map()
newMap.observe(() => {
fired = true
})
array0.insert(0, [newMap])
newMap.set('tst', 42)
})
t.assert(!fired, 'Event does not trigger')
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
@@ -237,7 +281,7 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testIteratingArrayContainingTypes = tc => { export const testIteratingArrayContainingTypes = tc => {
const y = new Y.Y() const y = new Y.Doc()
const arr = y.getArray('arr') const arr = y.getArray('arr')
const numItems = 10 const numItems = 10
for (let i = 0; i < numItems; i++) { for (let i = 0; i < numItems; i++) {
@@ -256,7 +300,7 @@ let _uniqueNumber = 0
const getUniqueNumber = () => _uniqueNumber++ const getUniqueNumber = () => _uniqueNumber++
/** /**
* @type {Array<function(TestYInstance,prng.PRNG):void>} * @type {Array<function(Doc,prng.PRNG,any):void>}
*/ */
const arrayTransactions = [ const arrayTransactions = [
function insert (user, gen) { function insert (user, gen) {
@@ -309,8 +353,8 @@ const arrayTransactions = [
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testRepeatGeneratingYarrayTests20 = tc => { export const testRepeatGeneratingYarrayTests4 = tc => {
applyRandomTests(tc, arrayTransactions, 3) applyRandomTests(tc, arrayTransactions, 4)
} }
/** /**

View File

@@ -1,4 +1,4 @@
import { init, compare, applyRandomTests, TestYInstance } from './testHelper.js' // eslint-disable-line import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
import { import {
compareIDs compareIDs
@@ -19,6 +19,8 @@ export const testBasicMapTests = tc => {
map0.set('string', 'hello Y') map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } }) map0.set('object', { key: { key2: 'value' } })
map0.set('y-map', new Y.Map()) map0.set('y-map', new Y.Map())
map0.set('boolean1', true)
map0.set('boolean0', false)
const map = map0.get('y-map') const map = map0.get('y-map')
map.set('y-array', new Y.Array()) map.set('y-array', new Y.Array())
const array = map.get('y-array') const array = map.get('y-array')
@@ -27,6 +29,8 @@ export const testBasicMapTests = tc => {
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)') t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)') t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)') t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
@@ -35,12 +39,16 @@ export const testBasicMapTests = tc => {
t.assert(map1.get('number') === 1, 'client 1 received the update (number)') t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)') t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)') t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
// compare disconnected user // compare disconnected user
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected') t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)')
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected') t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected') t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
compare(users) compare(users)
@@ -320,7 +328,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
} }
/** /**
* @type {Array<function(TestYInstance,prng.PRNG):void>} * @type {Array<function(Doc,prng.PRNG):void>}
*/ */
const mapTransactions = [ const mapTransactions = [
function set (user, gen) { function set (user, gen) {

View File

@@ -27,6 +27,13 @@ export const testBasicInsertAndDelete = tc => {
text0.delete(1, 1) text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(delta, [{ retain: 1 }, { delete: 1 }]) t.compare(delta, [{ retain: 1 }, { delete: 1 }])
users[0].transact(() => {
text0.insert(0, '1')
text0.delete(0, 1)
})
t.compare(delta, [])
compare(users) compare(users)
} }
@@ -56,7 +63,7 @@ export const testBasicFormat = tc => {
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }]) t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }]) t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
// @ts-ignore // @ts-ignore
t.assert(text0._start.right.right.right.string === 'b', 'Does not insert duplicate attribute marker') t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker')
text0.insert(0, 'y') text0.insert(0, 'y')
t.assert(text0.toString() === 'yzb') t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }]) t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
@@ -67,3 +74,16 @@ export const testBasicFormat = tc => {
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }]) t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
compare(users) compare(users)
} }
/**
* @param {t.TestCase} tc
*/
export const testGetDeltaWithEmbeds = tc => {
const { text0 } = init(tc, { users: 1 })
text0.applyDelta([{
insert: {linebreak: 's'}
}])
t.compare(text0.toDelta(), [{
insert: {linebreak: 's'}
}])
}

View File

@@ -36,8 +36,10 @@
/* Module Resolution Options */ /* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ "paths": {
"yjs": ["./src/index.js"]
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */ // "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
@@ -58,5 +60,5 @@
// "types": ["./src/utils/typedefs.js"] // "types": ["./src/utils/typedefs.js"]
}, },
"include": ["./src/**/*", "./tests/**/*"], "include": ["./src/**/*", "./tests/**/*"],
"exclude": ["../lib0/**/*", "node_modules"] "exclude": ["../lib0/**/*", "node_modules/**/*", "dist", "dist/**/*.js"]
} }