Compare commits
54 Commits
v13-refact
...
v13.0.0-92
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
952a9b2c41 | ||
|
|
03458dc641 | ||
|
|
14df5b72af | ||
|
|
338968031b | ||
|
|
1aac245b93 | ||
|
|
1faff323c1 | ||
|
|
e7280c7ae2 | ||
|
|
4c38619b5d | ||
|
|
b4e5c5cc1f | ||
|
|
b0dbd84f7f | ||
|
|
4a990963d9 | ||
|
|
7e7c9d5b11 | ||
|
|
775f6eed1d | ||
|
|
1e83b9418c | ||
|
|
ac3f672c80 | ||
|
|
2192aa5821 | ||
|
|
70bb523005 | ||
|
|
10ce6de57a | ||
|
|
3fba4f25a5 | ||
|
|
66c35d8499 | ||
|
|
4c14157dcf | ||
|
|
ef6c382e20 | ||
|
|
ee45b4fdd6 | ||
|
|
668e9e8a9b | ||
|
|
37a6d68543 | ||
|
|
f893198769 | ||
|
|
d3ee1a0ec2 | ||
|
|
d6593412a2 | ||
|
|
d31bf36531 | ||
|
|
a485f550db | ||
|
|
0610b16227 | ||
|
|
72e470c5f0 | ||
|
|
4d12a02e2f | ||
|
|
4a7d6f0a2d | ||
|
|
c80f446b5f | ||
|
|
81a529d8dc | ||
|
|
4f0ab78914 | ||
|
|
8c36f67f0b | ||
|
|
77687d94e6 | ||
|
|
4644511303 | ||
|
|
20005eecdb | ||
|
|
c9dda245bf | ||
|
|
1417470156 | ||
|
|
584e5dfd40 | ||
|
|
805acbb9f5 | ||
|
|
32c4c09072 | ||
|
|
8c5a06bbf8 | ||
|
|
a336cc167c | ||
|
|
21d86cd2be | ||
|
|
1d0f9faa91 | ||
|
|
45237571b7 | ||
|
|
bb6f6cd141 | ||
|
|
729c1f16b8 | ||
|
|
b6059704aa |
4
.markdownlint.json
Normal file
4
.markdownlint.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"no-inline-html": false
|
||||||
|
}
|
||||||
16
README.md
16
README.md
@@ -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>
|
|
||||||
|
|||||||
1095
README.v13.md
1095
README.v13.md
File diff suppressed because it is too large
Load Diff
1
examples/.gitignore
vendored
1
examples/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
build
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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'
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"codemirror": {
|
|
||||||
"title": "CodeMirror Binding"
|
|
||||||
},
|
|
||||||
"prosemirror": {
|
|
||||||
"title": "ProseMirror Binding"
|
|
||||||
},
|
|
||||||
"textarea": {
|
|
||||||
"title": "Textarea Binding"
|
|
||||||
},
|
|
||||||
"quill": {
|
|
||||||
"title": "Quill Binding"
|
|
||||||
},
|
|
||||||
"dom": {
|
|
||||||
"title": "Dom Binding"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 })
|
|
||||||
@@ -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 }
|
|
||||||
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 }
|
|
||||||
@@ -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>
|
|
||||||
@@ -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)
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
5589
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
|
|||||||
42
src/index.js
42
src/index.js
@@ -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'
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/structs/ContentBinary.js
Normal file
92
src/structs/ContentBinary.js
Normal 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)))
|
||||||
98
src/structs/ContentDeleted.js
Normal file
98
src/structs/ContentDeleted.js
Normal 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))
|
||||||
92
src/structs/ContentEmbed.js
Normal file
92
src/structs/ContentEmbed.js
Normal 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)))
|
||||||
95
src/structs/ContentFormat.js
Normal file
95
src/structs/ContentFormat.js
Normal 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
113
src/structs/ContentJSON.js
Normal 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)
|
||||||
|
}
|
||||||
96
src/structs/ContentString.js
Normal file
96
src/structs/ContentString.js
Normal 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
163
src/structs/ContentType.js
Normal 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))
|
||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
335
src/types/YXmlFragment.js
Normal 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()
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
182
tests/undo-redo.tests.js
Normal 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() === '')
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
}])
|
||||||
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user